From a442963f740c9532b6bbf3828a7c0f279b683539 Mon Sep 17 00:00:00 2001 From: tgstation-server Date: Fri, 29 May 2020 03:08:29 +0000 Subject: [PATCH] Deploy code docs to GitHub Pages for Travis build 3410 Commit: 86b45f01e5a669dbf8bcee78048b21ed4530cf5a --- ...3___m_s_initial_create_8_designer_8cs.html | 2 +- ..._initial_create_8_designer_8cs_source.html | 2 +- 20180906135553___m_s_initial_create_8cs.html | 2 +- ...35553___m_s_initial_create_8cs_source.html | 2 +- ...9___m_y_initial_create_8_designer_8cs.html | 2 +- ..._initial_create_8_designer_8cs_source.html | 2 +- 20180906143029___m_y_initial_create_8cs.html | 2 +- ...43029___m_y_initial_create_8cs_source.html | 2 +- ...y_add_minimum_security_8_designer_8cs.html | 2 +- ...inimum_security_8_designer_8cs_source.html | 2 +- ...020726___m_y_add_minimum_security_8cs.html | 2 +- ...__m_y_add_minimum_security_8cs_source.html | 2 +- ...s_add_minimum_security_8_designer_8cs.html | 2 +- ...inimum_security_8_designer_8cs_source.html | 2 +- ...021228___m_s_add_minimum_security_8cs.html | 2 +- ...__m_s_add_minimum_security_8cs_source.html | 2 +- ...nd_foreign_key_cleanup_8_designer_8cs.html | 2 +- ...ign_key_cleanup_8_designer_8cs_source.html | 2 +- ..._nullable_and_foreign_key_cleanup_8cs.html | 2 +- ...le_and_foreign_key_cleanup_8cs_source.html | 2 +- ...nd_foreign_key_cleanup_8_designer_8cs.html | 2 +- ...ign_key_cleanup_8_designer_8cs_source.html | 2 +- ..._nullable_and_foreign_key_cleanup_8cs.html | 2 +- ...le_and_foreign_key_cleanup_8cs_source.html | 2 +- ...h_compile_job_required_8_designer_8cs.html | 2 +- ...le_job_required_8_designer_8cs_source.html | 2 +- ...m_s_reattach_compile_job_required_8cs.html | 2 +- ...ttach_compile_job_required_8cs_source.html | 2 +- ...h_compile_job_required_8_designer_8cs.html | 2 +- ...le_job_required_8_designer_8cs_source.html | 2 +- ...m_y_reattach_compile_job_required_8cs.html | 2 +- ...ttach_compile_job_required_8cs_source.html | 2 +- ...gle_testmerge_comments_8_designer_8cs.html | 2 +- ...tmerge_comments_8_designer_8cs_source.html | 2 +- ...4___m_s_toggle_testmerge_comments_8cs.html | 2 +- ..._toggle_testmerge_comments_8cs_source.html | 2 +- ...gle_testmerge_comments_8_designer_8cs.html | 2 +- ...tmerge_comments_8_designer_8cs_source.html | 2 +- ...9___m_y_toggle_testmerge_comments_8cs.html | 2 +- ..._toggle_testmerge_comments_8cs_source.html | 2 +- ...m_s_fix_rev_info_index_8_designer_8cs.html | 2 +- ..._rev_info_index_8_designer_8cs_source.html | 2 +- ...19041825___m_s_fix_rev_info_index_8cs.html | 2 +- ...5___m_s_fix_rev_info_index_8cs_source.html | 2 +- ...m_y_fix_rev_info_index_8_designer_8cs.html | 2 +- ..._rev_info_index_8_designer_8cs_source.html | 2 +- ...19042440___m_y_fix_rev_info_index_8cs.html | 2 +- ...0___m_y_fix_rev_info_index_8cs_source.html | 2 +- ..._reconnection_interval_8_designer_8cs.html | 2 +- ...ection_interval_8_designer_8cs_source.html | 2 +- ...dd_chat_bot_reconnection_interval_8cs.html | 2 +- ..._bot_reconnection_interval_8cs_source.html | 2 +- ..._reconnection_interval_8_designer_8cs.html | 2 +- ...ection_interval_8_designer_8cs_source.html | 2 +- ...dd_chat_bot_reconnection_interval_8cs.html | 2 +- ..._bot_reconnection_interval_8cs_source.html | 2 +- ...m_s_tons_of_validation_8_designer_8cs.html | 2 +- ...s_of_validation_8_designer_8cs_source.html | 2 +- ...11164808___m_s_tons_of_validation_8cs.html | 2 +- ...8___m_s_tons_of_validation_8cs_source.html | 2 +- ...m_y_tons_of_validation_8_designer_8cs.html | 2 +- ...s_of_validation_8_designer_8cs_source.html | 2 +- ...11170010___m_y_tons_of_validation_8cs.html | 2 +- ...0___m_y_tons_of_validation_8cs_source.html | 2 +- ...9___m_s_limits_on_chat_8_designer_8cs.html | 2 +- ..._limits_on_chat_8_designer_8cs_source.html | 2 +- 20200420175359___m_s_limits_on_chat_8cs.html | 2 +- ...75359___m_s_limits_on_chat_8cs_source.html | 2 +- ...5___m_y_limits_on_chat_8_designer_8cs.html | 2 +- ..._limits_on_chat_8_designer_8cs_source.html | 2 +- 20200420181015___m_y_limits_on_chat_8cs.html | 2 +- ...81015___m_y_limits_on_chat_8cs_source.html | 2 +- ...le_job_d_m_api_version_8_designer_8cs.html | 2 +- ...d_m_api_version_8_designer_8cs_source.html | 2 +- ...s_add_compile_job_d_m_api_version_8cs.html | 2 +- ...ompile_job_d_m_api_version_8cs_source.html | 2 +- ...le_job_d_m_api_version_8_designer_8cs.html | 2 +- ...d_m_api_version_8_designer_8cs_source.html | 2 +- ...y_add_compile_job_d_m_api_version_8cs.html | 2 +- ...ompile_job_d_m_api_version_8cs_source.html | 2 +- ...m_y_checkpoint_rebuild_8_designer_8cs.html | 2 +- ...ckpoint_rebuild_8_designer_8cs_source.html | 2 +- ...23003841___m_y_checkpoint_rebuild_8cs.html | 2 +- ...1___m_y_checkpoint_rebuild_8cs_source.html | 2 +- ...m_s_checkpoint_rebuild_8_designer_8cs.html | 2 +- ...ckpoint_rebuild_8_designer_8cs_source.html | 2 +- ...23004015___m_s_checkpoint_rebuild_8cs.html | 2 +- ...5___m_s_checkpoint_rebuild_8cs_source.html | 2 +- ...ns_add_d_m_api_version_8_designer_8cs.html | 2 +- ...d_m_api_version_8_designer_8cs_source.html | 2 +- ..._s_kill_jsons_add_d_m_api_version_8cs.html | 2 +- ..._jsons_add_d_m_api_version_8cs_source.html | 2 +- ...ns_add_d_m_api_version_8_designer_8cs.html | 2 +- ...d_m_api_version_8_designer_8cs_source.html | 2 +- ..._y_kill_jsons_add_d_m_api_version_8cs.html | 2 +- ..._jsons_add_d_m_api_version_8cs_source.html | 2 +- ..._s_add_job_error_codes_8_designer_8cs.html | 2 +- ...job_error_codes_8_designer_8cs_source.html | 2 +- ...5024433___m_s_add_job_error_codes_8cs.html | 2 +- ...___m_s_add_job_error_codes_8cs_source.html | 2 +- ..._y_add_job_error_codes_8_designer_8cs.html | 2 +- ...job_error_codes_8_designer_8cs_source.html | 2 +- ...5024953___m_y_add_job_error_codes_8cs.html | 2 +- ...___m_y_add_job_error_codes_8cs_source.html | 2 +- ...s_fix_cascading_delete_8_designer_8cs.html | 2 +- ...ascading_delete_8_designer_8cs_source.html | 2 +- ...173852___m_s_fix_cascading_delete_8cs.html | 2 +- ...__m_s_fix_cascading_delete_8cs_source.html | 2 +- ...y_fix_cascading_delete_8_designer_8cs.html | 2 +- ...ascading_delete_8_designer_8cs_source.html | 2 +- ...180159___m_y_fix_cascading_delete_8cs.html | 2 +- ...__m_y_fix_cascading_delete_8cs_source.html | 2 +- ...27180904___s_l_rebuild_8_designer_8cs.html | 2 +- ...4___s_l_rebuild_8_designer_8cs_source.html | 2 +- 20200427180904___s_l_rebuild_8cs.html | 2 +- 20200427180904___s_l_rebuild_8cs_source.html | 2 +- ...57___m_s_add_heartbeat_8_designer_8cs.html | 2 +- ...s_add_heartbeat_8_designer_8cs_source.html | 2 +- 20200511005957___m_s_add_heartbeat_8cs.html | 2 +- ...005957___m_s_add_heartbeat_8cs_source.html | 2 +- ...48___m_y_add_heartbeat_8_designer_8cs.html | 2 +- ...y_add_heartbeat_8_designer_8cs_source.html | 2 +- 20200511011048___m_y_add_heartbeat_8cs.html | 2 +- ...011048___m_y_add_heartbeat_8cs_source.html | 2 +- ...17___s_l_add_heartbeat_8_designer_8cs.html | 2 +- ...l_add_heartbeat_8_designer_8cs_source.html | 2 +- 20200511012117___s_l_add_heartbeat_8cs.html | 2 +- ...012117___s_l_add_heartbeat_8cs_source.html | 2 +- ..._l_remove_soft_columns_8_designer_8cs.html | 2 +- ...ve_soft_columns_8_designer_8cs_source.html | 2 +- ...1162912___s_l_remove_soft_columns_8cs.html | 2 +- ...___s_l_remove_soft_columns_8cs_source.html | 2 +- ..._s_remove_soft_columns_8_designer_8cs.html | 2 +- ...ve_soft_columns_8_designer_8cs_source.html | 2 +- ...2181801___m_s_remove_soft_columns_8cs.html | 2 +- ...___m_s_remove_soft_columns_8cs_source.html | 2 +- ..._y_remove_soft_columns_8_designer_8cs.html | 2 +- ...ve_soft_columns_8_designer_8cs_source.html | 2 +- ...2182535___m_y_remove_soft_columns_8cs.html | 2 +- ...___m_y_remove_soft_columns_8cs_source.html | 2 +- ...516111712___p_g_create_8_designer_8cs.html | 122 +++ ...12___p_g_create_8_designer_8cs_source.html | 112 +++ 20200516111712___p_g_create_8cs.html | 122 +++ 20200516111712___p_g_create_8cs_source.html | 125 +++ _a_p_i_8dox.html | 2 +- _administration_8cs.html | 2 +- _administration_8cs_source.html | 2 +- _administration_client_8cs.html | 2 +- _administration_client_8cs_source.html | 2 +- _administration_controller_8cs.html | 2 +- _administration_controller_8cs_source.html | 4 +- _administration_rights_8cs.html | 2 +- _administration_rights_8cs_source.html | 2 +- _api_client_8cs.html | 2 +- _api_client_8cs_source.html | 7 +- _api_client_factory_8cs.html | 2 +- _api_client_factory_8cs_source.html | 2 +- _api_conflict_exception_8cs.html | 2 +- _api_conflict_exception_8cs_source.html | 2 +- _api_controller_8cs.html | 2 +- _api_controller_8cs_source.html | 8 +- _api_exception_8cs.html | 2 +- _api_exception_8cs_source.html | 2 +- _api_headers_8cs.html | 2 +- _api_headers_8cs_source.html | 2 +- _api_validation_status_8cs.html | 2 +- _api_validation_status_8cs_source.html | 2 +- _application_8cs.html | 2 +- _application_8cs_source.html | 18 +- _application_builder_extensions_8cs.html | 2 +- ...ication_builder_extensions_8cs_source.html | 5 +- _architecture_8dox.html | 2 +- _assembly_information_provider_8cs.html | 2 +- ...embly_information_provider_8cs_source.html | 2 +- _async_delayer_8cs.html | 2 +- _async_delayer_8cs_source.html | 2 +- _authentication_context_8cs.html | 2 +- _authentication_context_8cs_source.html | 2 +- _authentication_context_factory_8cs.html | 2 +- ...entication_context_factory_8cs_source.html | 6 +- _basic_watchdog_8cs.html | 2 +- _basic_watchdog_8cs_source.html | 8 +- _bool_converter_8cs.html | 2 +- _bool_converter_8cs_source.html | 2 +- _bridge_command_type_8cs.html | 2 +- _bridge_command_type_8cs_source.html | 2 +- _bridge_controller_8cs.html | 2 +- _bridge_controller_8cs_source.html | 2 +- _bridge_parameters_8cs.html | 2 +- _bridge_parameters_8cs_source.html | 2 +- _bridge_registration_8cs.html | 2 +- _bridge_registration_8cs_source.html | 2 +- _bridge_response_8cs.html | 2 +- _bridge_response_8cs_source.html | 2 +- _byond_8cs.html | 2 +- _byond_8cs_source.html | 2 +- _byond_client_8cs.html | 2 +- _byond_client_8cs_source.html | 2 +- _byond_command_8cs.html | 2 +- _byond_command_8cs_source.html | 2 +- _byond_controller_8cs.html | 2 +- _byond_controller_8cs_source.html | 4 +- _byond_executable_lock_8cs.html | 2 +- _byond_executable_lock_8cs_source.html | 2 +- _byond_installer_base_8cs.html | 2 +- _byond_installer_base_8cs_source.html | 2 +- _byond_manager_8cs.html | 2 +- _byond_manager_8cs_source.html | 2 +- _byond_rights_8cs.html | 2 +- _byond_rights_8cs_source.html | 2 +- _channel_mapping_8cs.html | 2 +- _channel_mapping_8cs_source.html | 2 +- _channel_representation_8cs.html | 2 +- _channel_representation_8cs_source.html | 2 +- _chat_bot_rights_8cs.html | 2 +- _chat_bot_rights_8cs_source.html | 2 +- _chat_bots_client_8cs.html | 2 +- _chat_bots_client_8cs_source.html | 2 +- _chat_channel_extensions_8cs.html | 2 +- _chat_channel_extensions_8cs_source.html | 2 +- _chat_command_8cs.html | 2 +- _chat_command_8cs_source.html | 2 +- _chat_connection_string_builder_8cs.html | 2 +- ..._connection_string_builder_8cs_source.html | 2 +- _chat_controller_8cs.html | 2 +- _chat_controller_8cs_source.html | 22 +- _chat_manager_8cs.html | 2 +- _chat_manager_8cs_source.html | 24 +- _chat_manager_factory_8cs.html | 2 +- _chat_manager_factory_8cs_source.html | 2 +- _chat_message_8cs.html | 2 +- _chat_message_8cs_source.html | 2 +- _chat_provider_8cs.html | 2 +- _chat_provider_8cs_source.html | 2 +- _chat_tracking_context_8cs.html | 2 +- _chat_tracking_context_8cs_source.html | 2 +- _chat_update_8cs.html | 2 +- _chat_update_8cs_source.html | 2 +- _chat_user_8cs.html | 2 +- _chat_user_8cs_source.html | 2 +- _claims_injector_8cs.html | 2 +- _claims_injector_8cs_source.html | 2 +- _client_exception_8cs.html | 2 +- _client_exception_8cs_source.html | 2 +- _combined_topic_response_8cs.html | 2 +- _combined_topic_response_8cs_source.html | 2 +- _command_factory_8cs.html | 2 +- _command_factory_8cs_source.html | 4 +- ...ssion_2_dual_reattach_information_8cs.html | 2 +- ..._dual_reattach_information_8cs_source.html | 2 +- ..._2_session_2_reattach_information_8cs.html | 2 +- ...ion_2_reattach_information_8cs_source.html | 2 +- _components_2_watchdog_2_i_watchdog_8cs.html | 2 +- ...ts_2_watchdog_2_i_watchdog_8cs_source.html | 2 +- ...s_2_watchdog_2_i_watchdog_factory_8cs.html | 2 +- ...chdog_2_i_watchdog_factory_8cs_source.html | 2 +- ...nts_2_watchdog_2_watchdog_factory_8cs.html | 2 +- ...atchdog_2_watchdog_factory_8cs_source.html | 6 +- _configuration_8cs.html | 2 +- _configuration_8cs_source.html | 2 +- _configuration_client_8cs.html | 2 +- _configuration_client_8cs_source.html | 2 +- _configuration_controller_8cs.html | 2 +- _configuration_controller_8cs_source.html | 4 +- _configuration_file_8cs.html | 2 +- _configuration_file_8cs_source.html | 2 +- _configuration_rights_8cs.html | 2 +- _configuration_rights_8cs_source.html | 2 +- _configuration_type_8cs.html | 2 +- _configuration_type_8cs_source.html | 2 +- _conflict_exception_8cs.html | 2 +- _conflict_exception_8cs_source.html | 2 +- _console_2_program_8cs.html | 2 +- _console_2_program_8cs_source.html | 2 +- _console_8cs.html | 2 +- _console_8cs_source.html | 2 +- _control_panel_configuration_8cs.html | 2 +- _control_panel_configuration_8cs_source.html | 2 +- _cryptography_suite_8cs.html | 2 +- _cryptography_suite_8cs_source.html | 2 +- _custom_command_8cs.html | 2 +- _custom_command_8cs_source.html | 2 +- _d_m_api_constants_8cs.html | 2 +- _d_m_api_constants_8cs_source.html | 4 +- _d_m_api_parameters_8cs.html | 2 +- _d_m_api_parameters_8cs_source.html | 2 +- _d_m_api_response_8cs.html | 2 +- _d_m_api_response_8cs_source.html | 2 +- _database_collection_8cs.html | 2 +- _database_collection_8cs_source.html | 10 +- _database_configuration_8cs.html | 4 +- _database_configuration_8cs_source.html | 4 +- _database_connection_factory_8cs.html | 2 +- _database_connection_factory_8cs_source.html | 9 +- _database_context_8cs.html | 4 +- _database_context_8cs_source.html | 48 +- _database_context_extensions_8cs.html | 2 +- _database_context_extensions_8cs_source.html | 6 +- _database_context_factory_8cs.html | 2 +- _database_context_factory_8cs_source.html | 2 +- _database_seeder_8cs.html | 2 +- _database_seeder_8cs_source.html | 13 +- _database_type_8cs.html | 10 +- _database_type_8cs_source.html | 5 +- _dead_session_controller_8cs.html | 2 +- _dead_session_controller_8cs_source.html | 2 +- _default_i_o_manager_8cs.html | 2 +- _default_i_o_manager_8cs_source.html | 2 +- ...n_time_db_context_factory_helpers_8cs.html | 4 +- ...db_context_factory_helpers_8cs_source.html | 8 +- _discord_connection_string_builder_8cs.html | 2 +- ..._connection_string_builder_8cs_source.html | 2 +- _discord_provider_8cs.html | 2 +- _discord_provider_8cs_source.html | 41 +- _dmb_factory_8cs.html | 2 +- _dmb_factory_8cs_source.html | 10 +- _dmb_provider_8cs.html | 2 +- _dmb_provider_8cs_source.html | 10 +- _dream_daemon_8cs.html | 2 +- _dream_daemon_8cs_source.html | 2 +- _dream_daemon_client_8cs.html | 2 +- _dream_daemon_client_8cs_source.html | 2 +- _dream_daemon_controller_8cs.html | 2 +- _dream_daemon_controller_8cs_source.html | 16 +- _dream_daemon_launch_parameters_8cs.html | 2 +- ...m_daemon_launch_parameters_8cs_source.html | 2 +- _dream_daemon_rights_8cs.html | 2 +- _dream_daemon_rights_8cs_source.html | 2 +- _dream_daemon_security_8cs.html | 2 +- _dream_daemon_security_8cs_source.html | 2 +- _dream_maker_client_8cs.html | 2 +- _dream_maker_client_8cs_source.html | 2 +- _dream_maker_controller_8cs.html | 2 +- _dream_maker_controller_8cs_source.html | 22 +- _dream_maker_rights_8cs.html | 2 +- _dream_maker_rights_8cs_source.html | 2 +- _dream_maker_settings_8cs.html | 2 +- _dream_maker_settings_8cs_source.html | 2 +- _dual_reattach_information_base_8cs.html | 2 +- ..._reattach_information_base_8cs_source.html | 2 +- _entity_id_8cs.html | 2 +- _entity_id_8cs_source.html | 2 +- _error_code_8cs.html | 2 +- _error_code_8cs_source.html | 2 +- _error_code_extensions_8cs.html | 2 +- _error_code_extensions_8cs_source.html | 2 +- _error_message_8cs.html | 2 +- _error_message_8cs_source.html | 2 +- _event_consumer_8cs.html | 2 +- _event_consumer_8cs_source.html | 2 +- _event_notification_8cs.html | 2 +- _event_notification_8cs_source.html | 2 +- _event_type_8cs.html | 2 +- _event_type_8cs_source.html | 2 +- _experimental_watchdog_8cs.html | 2 +- _experimental_watchdog_8cs_source.html | 12 +- _features_8dox.html | 2 +- _file_logging_configuration_8cs.html | 2 +- _file_logging_configuration_8cs_source.html | 2 +- _general_configuration_8cs.html | 2 +- _general_configuration_8cs_source.html | 6 +- _git_hub_client_factory_8cs.html | 2 +- _git_hub_client_factory_8cs_source.html | 2 +- _global_suppressions_8cs.html | 2 +- _global_suppressions_8cs_source.html | 2 +- _home_controller_8cs.html | 2 +- _home_controller_8cs_source.html | 10 +- _host_builder_extensions_8cs.html | 2 +- _host_builder_extensions_8cs_source.html | 2 +- _http_client_8cs.html | 2 +- _http_client_8cs_source.html | 2 +- _i_administration_client_8cs.html | 2 +- _i_administration_client_8cs_source.html | 2 +- _i_api_client_8cs.html | 2 +- _i_api_client_8cs_source.html | 2 +- _i_api_client_factory_8cs.html | 2 +- _i_api_client_factory_8cs_source.html | 2 +- _i_assembly_information_provider_8cs.html | 2 +- ...embly_information_provider_8cs_source.html | 2 +- _i_async_delayer_8cs.html | 2 +- _i_async_delayer_8cs_source.html | 2 +- _i_authentication_context_8cs.html | 2 +- _i_authentication_context_8cs_source.html | 2 +- _i_authentication_context_factory_8cs.html | 2 +- ...entication_context_factory_8cs_source.html | 2 +- _i_bridge_dispatcher_8cs.html | 2 +- _i_bridge_dispatcher_8cs_source.html | 2 +- _i_bridge_handler_8cs.html | 2 +- _i_bridge_handler_8cs_source.html | 2 +- _i_bridge_registrar_8cs.html | 2 +- _i_bridge_registrar_8cs_source.html | 2 +- _i_bridge_registration_8cs.html | 2 +- _i_bridge_registration_8cs_source.html | 2 +- _i_byond_client_8cs.html | 2 +- _i_byond_client_8cs_source.html | 2 +- _i_byond_executable_lock_8cs.html | 2 +- _i_byond_executable_lock_8cs_source.html | 2 +- _i_byond_installer_8cs.html | 2 +- _i_byond_installer_8cs_source.html | 2 +- _i_byond_manager_8cs.html | 2 +- _i_byond_manager_8cs_source.html | 2 +- _i_channel_sink_8cs.html | 2 +- _i_channel_sink_8cs_source.html | 2 +- _i_chat_bots_client_8cs.html | 2 +- _i_chat_bots_client_8cs_source.html | 2 +- _i_chat_manager_8cs.html | 2 +- _i_chat_manager_8cs_source.html | 4 +- _i_chat_manager_factory_8cs.html | 2 +- _i_chat_manager_factory_8cs_source.html | 2 +- _i_chat_tracking_context_8cs.html | 2 +- _i_chat_tracking_context_8cs_source.html | 2 +- _i_claims_injector_8cs.html | 2 +- _i_claims_injector_8cs_source.html | 2 +- _i_command_8cs.html | 2 +- _i_command_8cs_source.html | 2 +- _i_command_factory_8cs.html | 2 +- _i_command_factory_8cs_source.html | 2 +- _i_compile_job_sink_8cs.html | 2 +- _i_compile_job_sink_8cs_source.html | 2 +- _i_configuration_8cs.html | 2 +- _i_configuration_8cs_source.html | 2 +- _i_configuration_client_8cs.html | 2 +- _i_configuration_client_8cs_source.html | 2 +- _i_console_8cs.html | 2 +- _i_console_8cs_source.html | 2 +- _i_credentials_provider_8cs.html | 2 +- _i_credentials_provider_8cs_source.html | 2 +- _i_cryptography_suite_8cs.html | 2 +- _i_cryptography_suite_8cs_source.html | 2 +- _i_custom_command_handler_8cs.html | 2 +- _i_custom_command_handler_8cs_source.html | 2 +- _i_database_collection_8cs.html | 2 +- _i_database_collection_8cs_source.html | 7 +- _i_database_connection_factory_8cs.html | 2 +- ...atabase_connection_factory_8cs_source.html | 2 +- _i_database_context_8cs.html | 2 +- _i_database_context_8cs_source.html | 2 +- _i_database_context_factory_8cs.html | 2 +- _i_database_context_factory_8cs_source.html | 2 +- _i_database_seeder_8cs.html | 2 +- _i_database_seeder_8cs_source.html | 2 +- _i_dmb_factory_8cs.html | 2 +- _i_dmb_factory_8cs_source.html | 2 +- _i_dmb_provider_8cs.html | 2 +- _i_dmb_provider_8cs_source.html | 2 +- _i_dream_daemon_client_8cs.html | 2 +- _i_dream_daemon_client_8cs_source.html | 2 +- _i_dream_maker_8cs.html | 2 +- _i_dream_maker_8cs_source.html | 4 +- _i_dream_maker_client_8cs.html | 2 +- _i_dream_maker_client_8cs_source.html | 2 +- _i_event_consumer_8cs.html | 2 +- _i_event_consumer_8cs_source.html | 2 +- _i_git_hub_client_factory_8cs.html | 2 +- _i_git_hub_client_factory_8cs_source.html | 2 +- _i_http_client_8cs.html | 2 +- _i_http_client_8cs_source.html | 2 +- _i_i_o_manager_8cs.html | 2 +- _i_i_o_manager_8cs_source.html | 2 +- _i_identity_cache_8cs.html | 2 +- _i_identity_cache_8cs_source.html | 2 +- _i_instance_8cs.html | 2 +- _i_instance_8cs_source.html | 4 +- _i_instance_client_8cs.html | 2 +- _i_instance_client_8cs_source.html | 2 +- _i_instance_factory_8cs.html | 2 +- _i_instance_factory_8cs_source.html | 2 +- _i_instance_manager_8cs.html | 2 +- _i_instance_manager_8cs_source.html | 2 +- _i_instance_manager_client_8cs.html | 2 +- _i_instance_manager_client_8cs_source.html | 2 +- _i_instance_user_client_8cs.html | 2 +- _i_instance_user_client_8cs_source.html | 2 +- _i_job_manager_8cs.html | 2 +- _i_job_manager_8cs_source.html | 4 +- _i_jobs_client_8cs.html | 2 +- _i_jobs_client_8cs_source.html | 2 +- _i_latest_compile_job_provider_8cs.html | 2 +- ...atest_compile_job_provider_8cs_source.html | 2 +- _i_lib_git2_commands_8cs.html | 2 +- _i_lib_git2_commands_8cs_source.html | 2 +- _i_lib_git2_repository_factory_8cs.html | 2 +- ...ib_git2_repository_factory_8cs_source.html | 2 +- _i_network_prompt_reaper_8cs.html | 2 +- _i_network_prompt_reaper_8cs_source.html | 2 +- _i_platform_identifier_8cs.html | 2 +- _i_platform_identifier_8cs_source.html | 2 +- _i_post_setup_services_8cs.html | 2 +- _i_post_setup_services_8cs_source.html | 4 +- _i_post_write_handler_8cs.html | 2 +- _i_post_write_handler_8cs_source.html | 2 +- _i_process_8cs.html | 2 +- _i_process_8cs_source.html | 2 +- _i_process_base_8cs.html | 2 +- _i_process_base_8cs_source.html | 2 +- _i_process_executor_8cs.html | 2 +- _i_process_executor_8cs_source.html | 2 +- _i_process_features_8cs.html | 2 +- _i_process_features_8cs_source.html | 2 +- _i_provider_8cs.html | 2 +- _i_provider_8cs_source.html | 10 +- _i_provider_factory_8cs.html | 2 +- _i_provider_factory_8cs_source.html | 4 +- _i_reattach_info_handler_8cs.html | 2 +- _i_reattach_info_handler_8cs_source.html | 2 +- _i_repository_8cs.html | 2 +- _i_repository_8cs_source.html | 2 +- _i_repository_client_8cs.html | 2 +- _i_repository_client_8cs_source.html | 2 +- _i_repository_manager_8cs.html | 2 +- _i_repository_manager_8cs_source.html | 2 +- _i_request_logger_8cs.html | 2 +- _i_request_logger_8cs_source.html | 2 +- _i_restart_handler_8cs.html | 2 +- _i_restart_handler_8cs_source.html | 2 +- _i_restart_registration_8cs.html | 2 +- _i_restart_registration_8cs_source.html | 2 +- _i_server_8cs.html | 2 +- _i_server_8cs_source.html | 2 +- _i_server_client_8cs.html | 2 +- _i_server_client_8cs_source.html | 2 +- _i_server_client_factory_8cs.html | 2 +- _i_server_client_factory_8cs_source.html | 2 +- _i_server_control_8cs.html | 2 +- _i_server_control_8cs_source.html | 2 +- _i_server_factory_8cs.html | 2 +- _i_server_factory_8cs_source.html | 2 +- _i_server_port_provider_8cs.html | 2 +- _i_server_port_provider_8cs_source.html | 2 +- _i_session_controller_8cs.html | 2 +- _i_session_controller_8cs_source.html | 2 +- _i_session_controller_factory_8cs.html | 2 +- _i_session_controller_factory_8cs_source.html | 2 +- _i_symlink_factory_8cs.html | 2 +- _i_symlink_factory_8cs_source.html | 2 +- _i_synchronous_i_o_manager_8cs.html | 2 +- _i_synchronous_i_o_manager_8cs_source.html | 2 +- _i_system_identity_8cs.html | 2 +- _i_system_identity_8cs_source.html | 2 +- _i_system_identity_factory_8cs.html | 2 +- _i_system_identity_factory_8cs_source.html | 2 +- _i_token_factory_8cs.html | 2 +- _i_token_factory_8cs_source.html | 2 +- _i_users_client_8cs.html | 2 +- _i_users_client_8cs_source.html | 2 +- _identity_cache_8cs.html | 2 +- _identity_cache_8cs_source.html | 2 +- _identity_cache_object_8cs.html | 2 +- _identity_cache_object_8cs_source.html | 2 +- _instance_client_8cs.html | 2 +- _instance_client_8cs_source.html | 2 +- _instance_controller_8cs.html | 2 +- _instance_controller_8cs_source.html | 28 +- _instance_factory_8cs.html | 2 +- _instance_factory_8cs_source.html | 10 +- _instance_manager_8cs.html | 2 +- _instance_manager_8cs_source.html | 18 +- _instance_manager_client_8cs.html | 2 +- _instance_manager_client_8cs_source.html | 2 +- _instance_manager_rights_8cs.html | 2 +- _instance_manager_rights_8cs_source.html | 2 +- _instance_user_client_8cs.html | 2 +- _instance_user_client_8cs_source.html | 2 +- _instance_user_controller_8cs.html | 2 +- _instance_user_controller_8cs_source.html | 20 +- _instance_user_rights_8cs.html | 2 +- _instance_user_rights_8cs_source.html | 2 +- _insufficient_permissions_exception_8cs.html | 2 +- ...ient_permissions_exception_8cs_source.html | 2 +- _internal_2_server_information_8cs.html | 2 +- ...ernal_2_server_information_8cs_source.html | 2 +- _irc_connection_string_builder_8cs.html | 2 +- ..._connection_string_builder_8cs_source.html | 2 +- _irc_password_type_8cs.html | 2 +- _irc_password_type_8cs_source.html | 2 +- _irc_provider_8cs.html | 2 +- _irc_provider_8cs_source.html | 7 +- _job_controller_8cs.html | 2 +- _job_controller_8cs_source.html | 16 +- _job_exception_8cs.html | 2 +- _job_exception_8cs_source.html | 2 +- _job_handler_8cs.html | 2 +- _job_handler_8cs_source.html | 2 +- _job_manager_8cs.html | 2 +- _job_manager_8cs_source.html | 14 +- _jobs_client_8cs.html | 2 +- _jobs_client_8cs_source.html | 2 +- _kek_command_8cs.html | 2 +- _kek_command_8cs_source.html | 2 +- _launch_result_8cs.html | 2 +- _launch_result_8cs_source.html | 2 +- _lib_git2_commands_8cs.html | 2 +- _lib_git2_commands_8cs_source.html | 2 +- _lib_git2_repository_factory_8cs.html | 2 +- _lib_git2_repository_factory_8cs_source.html | 2 +- _limits_8cs.html | 2 +- _limits_8cs_source.html | 2 +- _message_8cs.html | 2 +- _message_8cs_source.html | 2 +- _method_not_supported_exception_8cs.html | 2 +- ...od_not_supported_exception_8cs_source.html | 2 +- _models_2_dual_reattach_information_8cs.html | 2 +- ..._dual_reattach_information_8cs_source.html | 2 +- _models_2_reattach_information_8cs.html | 2 +- ...els_2_reattach_information_8cs_source.html | 2 +- _monitor_action_8cs.html | 2 +- _monitor_action_8cs_source.html | 2 +- _monitor_activation_reason_8cs.html | 2 +- _monitor_activation_reason_8cs_source.html | 2 +- _monitor_state_8cs.html | 2 +- _monitor_state_8cs_source.html | 2 +- _my_sql_database_context_8cs.html | 4 +- _my_sql_database_context_8cs_source.html | 12 +- ...l_database_context_model_snapshot_8cs.html | 2 +- ...ase_context_model_snapshot_8cs_source.html | 2 +- ...ql_design_time_db_context_factory_8cs.html | 4 +- ...gn_time_db_context_factory_8cs_source.html | 10 +- _native_methods_8cs.html | 2 +- _native_methods_8cs_source.html | 2 +- _open_api_enum_var_names_extension_8cs.html | 2 +- ...i_enum_var_names_extension_8cs_source.html | 2 +- _platform_identifier_8cs.html | 2 +- _platform_identifier_8cs_source.html | 2 +- _posix_byond_installer_8cs.html | 2 +- _posix_byond_installer_8cs_source.html | 2 +- _posix_network_prompt_reaper_8cs.html | 2 +- _posix_network_prompt_reaper_8cs_source.html | 2 +- _posix_post_write_handler_8cs.html | 2 +- _posix_post_write_handler_8cs_source.html | 2 +- _posix_process_features_8cs.html | 2 +- _posix_process_features_8cs_source.html | 2 +- _posix_symlink_factory_8cs.html | 2 +- _posix_symlink_factory_8cs_source.html | 2 +- _posix_system_identity_8cs.html | 2 +- _posix_system_identity_8cs_source.html | 2 +- _posix_system_identity_factory_8cs.html | 2 +- ...ix_system_identity_factory_8cs_source.html | 2 +- _post_setup_services_8cs.html | 2 +- _post_setup_services_8cs_source.html | 4 +- _postgres_sql_database_context_8cs.html | 122 +++ ...tgres_sql_database_context_8cs_source.html | 120 +++ ...l_database_context_model_snapshot_8cs.html | 121 +++ ...ase_context_model_snapshot_8cs_source.html | 117 +++ ...ql_design_time_db_context_factory_8cs.html | 121 +++ ...gn_time_db_context_factory_8cs_source.html | 123 +++ _process_8cs.html | 2 +- _process_8cs_source.html | 2 +- _process_executor_8cs.html | 2 +- _process_executor_8cs_source.html | 2 +- _program_8cs.html | 2 +- _program_8cs_source.html | 4 +- _program_shutdown_token_source_8cs.html | 2 +- ...gram_shutdown_token_source_8cs_source.html | 2 +- _provider_8cs.html | 2 +- _provider_8cs_source.html | 34 +- _provider_factory_8cs.html | 2 +- _provider_factory_8cs_source.html | 8 +- _pull_requests_command_8cs.html | 2 +- _pull_requests_command_8cs_source.html | 6 +- _r_e_a_d_m_e_8md.html | 2 +- _r_e_a_d_m_e_8md_source.html | 2 +- _rate_limit_exception_8cs.html | 2 +- _rate_limit_exception_8cs_source.html | 2 +- _reattach_info_handler_8cs.html | 2 +- _reattach_info_handler_8cs_source.html | 8 +- _reattach_information_base_8cs.html | 2 +- _reattach_information_base_8cs_source.html | 2 +- _reboot_state_8cs.html | 2 +- _reboot_state_8cs_source.html | 2 +- _repository_client_8cs.html | 2 +- _repository_client_8cs_source.html | 2 +- _repository_controller_8cs.html | 2 +- _repository_controller_8cs_source.html | 20 +- _repository_manager_8cs.html | 2 +- _repository_manager_8cs_source.html | 2 +- _repository_rights_8cs.html | 2 +- _repository_rights_8cs_source.html | 2 +- _request_timeout_exception_8cs.html | 2 +- _request_timeout_exception_8cs_source.html | 2 +- _resolving_i_o_manager_8cs.html | 2 +- _resolving_i_o_manager_8cs_source.html | 2 +- _restart_registration_8cs.html | 2 +- _restart_registration_8cs_source.html | 2 +- _rev_info_test_merge_8cs.html | 2 +- _rev_info_test_merge_8cs_source.html | 2 +- _revision_command_8cs.html | 2 +- _revision_command_8cs_source.html | 2 +- _rights_helper_8cs.html | 2 +- _rights_helper_8cs_source.html | 2 +- _rights_type_8cs.html | 2 +- _rights_type_8cs_source.html | 2 +- _routes_8cs.html | 2 +- _routes_8cs_source.html | 2 +- _runtime_information_8cs.html | 2 +- _runtime_information_8cs_source.html | 2 +- _semaphore_slim_context_8cs.html | 2 +- _semaphore_slim_context_8cs_source.html | 2 +- _server_8cs.html | 2 +- _server_8cs_source.html | 6 +- _server_client_8cs.html | 2 +- _server_client_8cs_source.html | 2 +- _server_client_factory_8cs.html | 2 +- _server_client_factory_8cs_source.html | 2 +- _server_error_exception_8cs.html | 2 +- _server_error_exception_8cs_source.html | 2 +- _server_factory_8cs.html | 2 +- _server_factory_8cs_source.html | 12 +- _server_information_8cs.html | 2 +- _server_information_8cs_source.html | 2 +- _server_port_proivder_8cs.html | 2 +- _server_port_proivder_8cs_source.html | 13 +- _server_service_8cs.html | 2 +- _server_service_8cs_source.html | 2 +- _server_side_modifications_8cs.html | 2 +- _server_side_modifications_8cs_source.html | 2 +- _service_2_program_8cs.html | 2 +- _service_2_program_8cs_source.html | 2 +- _service_collection_extensions_8cs.html | 2 +- ...vice_collection_extensions_8cs_source.html | 2 +- _service_unavailable_exception_8cs.html | 2 +- ...vice_unavailable_exception_8cs_source.html | 2 +- _session_controller_8cs.html | 2 +- _session_controller_8cs_source.html | 2 +- _session_controller_factory_8cs.html | 2 +- _session_controller_factory_8cs_source.html | 2 +- _setup_application_8cs.html | 2 +- _setup_application_8cs_source.html | 10 +- _setup_wizard_8cs.html | 2 +- _setup_wizard_8cs_source.html | 59 +- _setup_wizard_mode_8cs.html | 2 +- _setup_wizard_mode_8cs_source.html | 2 +- _sql_server_database_context_8cs.html | 4 +- _sql_server_database_context_8cs_source.html | 12 +- ...r_database_context_model_snapshot_8cs.html | 2 +- ...ase_context_model_snapshot_8cs_source.html | 2 +- ...er_design_time_db_context_factory_8cs.html | 4 +- ...gn_time_db_context_factory_8cs_source.html | 10 +- _sqlite_database_context_8cs.html | 4 +- _sqlite_database_context_8cs_source.html | 12 +- ...e_database_context_model_snapshot_8cs.html | 2 +- ...ase_context_model_snapshot_8cs_source.html | 2 +- ...te_design_time_db_context_factory_8cs.html | 4 +- ...gn_time_db_context_factory_8cs_source.html | 10 +- _swagger_configuration_8cs.html | 2 +- _swagger_configuration_8cs_source.html | 10 +- _synchronous_i_o_manager_8cs.html | 2 +- _synchronous_i_o_manager_8cs_source.html | 2 +- _task_extensions_8cs.html | 2 +- _task_extensions_8cs_source.html | 2 +- _temporary_dmb_provider_8cs.html | 2 +- _temporary_dmb_provider_8cs_source.html | 2 +- _test_merge_base_8cs.html | 2 +- _test_merge_base_8cs_source.html | 2 +- _test_merge_information_8cs.html | 2 +- _test_merge_information_8cs_source.html | 2 +- _test_merge_parameters_8cs.html | 2 +- _test_merge_parameters_8cs_source.html | 2 +- _tgs_authorize_attribute_8cs.html | 2 +- _tgs_authorize_attribute_8cs_source.html | 2 +- ..._server_8_api_2_models_2_chat_bot_8cs.html | 2 +- ..._8_api_2_models_2_chat_bot_8cs_source.html | 2 +- ...ver_8_api_2_models_2_chat_channel_8cs.html | 2 +- ...pi_2_models_2_chat_channel_8cs_source.html | 2 +- ...rver_8_api_2_models_2_compile_job_8cs.html | 2 +- ...api_2_models_2_compile_job_8cs_source.html | 2 +- ...rver_8_api_2_models_2_dream_maker_8cs.html | 2 +- ...api_2_models_2_dream_maker_8cs_source.html | 2 +- ..._server_8_api_2_models_2_instance_8cs.html | 2 +- ..._8_api_2_models_2_instance_8cs_source.html | 2 +- ...er_8_api_2_models_2_instance_user_8cs.html | 2 +- ...i_2_models_2_instance_user_8cs_source.html | 2 +- ...pi_2_models_2_internal_2_chat_bot_8cs.html | 2 +- ...dels_2_internal_2_chat_bot_8cs_source.html | 2 +- ...2_models_2_internal_2_compile_job_8cs.html | 2 +- ...s_2_internal_2_compile_job_8cs_source.html | 2 +- ..._internal_2_dream_daemon_settings_8cs.html | 2 +- ...al_2_dream_daemon_settings_8cs_source.html | 2 +- ...r_8_api_2_models_2_internal_2_job_8cs.html | 2 +- ..._2_models_2_internal_2_job_8cs_source.html | 2 +- ..._2_internal_2_repository_settings_8cs.html | 2 +- ...rnal_2_repository_settings_8cs_source.html | 2 +- ...2_internal_2_revision_information_8cs.html | 2 +- ...nal_2_revision_information_8cs_source.html | 2 +- ..._2_models_2_internal_2_test_merge_8cs.html | 2 +- ...ls_2_internal_2_test_merge_8cs_source.html | 2 +- ..._8_api_2_models_2_internal_2_user_8cs.html | 2 +- ...2_models_2_internal_2_user_8cs_source.html | 2 +- ...ion_8_server_8_api_2_models_2_job_8cs.html | 2 +- ...erver_8_api_2_models_2_job_8cs_source.html | 2 +- ...erver_8_api_2_models_2_repository_8cs.html | 2 +- ..._api_2_models_2_repository_8cs_source.html | 2 +- ...i_2_models_2_revision_information_8cs.html | 2 +- ...els_2_revision_information_8cs_source.html | 4 +- ...erver_8_api_2_models_2_test_merge_8cs.html | 2 +- ..._api_2_models_2_test_merge_8cs_source.html | 2 +- ...on_8_server_8_api_2_models_2_user_8cs.html | 2 +- ...rver_8_api_2_models_2_user_8cs_source.html | 4 +- ...8_server_8_client_2_assembly_info_8cs.html | 2 +- ...r_8_client_2_assembly_info_8cs_source.html | 2 +- ...onents_2_deployment_2_dream_maker_8cs.html | 2 +- ...2_deployment_2_dream_maker_8cs_source.html | 64 +- ...er_8_host_2_components_2_instance_8cs.html | 2 +- ...st_2_components_2_instance_8cs_source.html | 18 +- ...ponents_2_repository_2_repository_8cs.html | 2 +- ..._2_repository_2_repository_8cs_source.html | 2 +- ...server_8_host_2_models_2_chat_bot_8cs.html | 2 +- ...8_host_2_models_2_chat_bot_8cs_source.html | 2 +- ...er_8_host_2_models_2_chat_channel_8cs.html | 2 +- ...st_2_models_2_chat_channel_8cs_source.html | 2 +- ...ver_8_host_2_models_2_compile_job_8cs.html | 2 +- ...ost_2_models_2_compile_job_8cs_source.html | 2 +- ..._2_models_2_dream_daemon_settings_8cs.html | 2 +- ...ls_2_dream_daemon_settings_8cs_source.html | 2 +- ...server_8_host_2_models_2_instance_8cs.html | 2 +- ...8_host_2_models_2_instance_8cs_source.html | 2 +- ...r_8_host_2_models_2_instance_user_8cs.html | 2 +- ...t_2_models_2_instance_user_8cs_source.html | 2 +- ...on_8_server_8_host_2_models_2_job_8cs.html | 2 +- ...rver_8_host_2_models_2_job_8cs_source.html | 2 +- ...st_2_models_2_repository_settings_8cs.html | 2 +- ...dels_2_repository_settings_8cs_source.html | 2 +- ...t_2_models_2_revision_information_8cs.html | 2 +- ...els_2_revision_information_8cs_source.html | 4 +- ...rver_8_host_2_models_2_test_merge_8cs.html | 2 +- ...host_2_models_2_test_merge_8cs_source.html | 2 +- ...n_8_server_8_host_2_models_2_user_8cs.html | 2 +- ...ver_8_host_2_models_2_user_8cs_source.html | 2 +- ...host_2_properties_2_assembly_info_8cs.html | 2 +- ...properties_2_assembly_info_8cs_source.html | 2 +- ..._8_host_8_console_2_assembly_info_8cs.html | 2 +- ..._8_console_2_assembly_info_8cs_source.html | 2 +- ...vice_2_properties_2_assembly_info_8cs.html | 2 +- ...properties_2_assembly_info_8cs_source.html | 2 +- ...8_host_8_watchdog_2_assembly_info_8cs.html | 2 +- ...8_watchdog_2_assembly_info_8cs_source.html | 2 +- _token_8cs.html | 2 +- _token_8cs_source.html | 2 +- _token_factory_8cs.html | 2 +- _token_factory_8cs_source.html | 2 +- _topic_command_type_8cs.html | 2 +- _topic_command_type_8cs_source.html | 2 +- _topic_parameters_8cs.html | 2 +- _topic_parameters_8cs_source.html | 2 +- _topic_response_8cs.html | 2 +- _topic_response_8cs_source.html | 2 +- _unauthorized_exception_8cs.html | 2 +- _unauthorized_exception_8cs_source.html | 2 +- _unrecognized_response_exception_8cs.html | 2 +- ...ognized_response_exception_8cs_source.html | 2 +- _updates_configuration_8cs.html | 2 +- _updates_configuration_8cs_source.html | 2 +- _user_controller_8cs.html | 2 +- _user_controller_8cs_source.html | 16 +- _user_update_8cs.html | 2 +- _user_update_8cs_source.html | 2 +- _users_client_8cs.html | 2 +- _users_client_8cs_source.html | 2 +- _version_command_8cs.html | 2 +- _version_command_8cs_source.html | 2 +- _version_converter_8cs.html | 2 +- _version_converter_8cs_source.html | 2 +- _version_extensions_8cs.html | 2 +- _version_extensions_8cs_source.html | 2 +- _version_mismatch_exception_8cs.html | 2 +- _version_mismatch_exception_8cs_source.html | 2 +- _watchdog_2_i_watchdog_8cs.html | 2 +- _watchdog_2_i_watchdog_8cs_source.html | 2 +- _watchdog_2_i_watchdog_factory_8cs.html | 2 +- ...chdog_2_i_watchdog_factory_8cs_source.html | 2 +- _watchdog_2_watchdog_factory_8cs.html | 2 +- _watchdog_2_watchdog_factory_8cs_source.html | 2 +- _watchdog_8cs.html | 2 +- _watchdog_8cs_source.html | 2 +- _watchdog_base_8cs.html | 2 +- _watchdog_base_8cs_source.html | 64 +- _web_host_builder_extensions_8cs.html | 2 +- _web_host_builder_extensions_8cs_source.html | 2 +- _windows_byond_installer_8cs.html | 2 +- _windows_byond_installer_8cs_source.html | 2 +- _windows_network_prompt_reaper_8cs.html | 2 +- ...dows_network_prompt_reaper_8cs_source.html | 2 +- _windows_post_write_handler_8cs.html | 2 +- _windows_post_write_handler_8cs_source.html | 2 +- _windows_process_features_8cs.html | 2 +- _windows_process_features_8cs_source.html | 2 +- _windows_swappable_dmb_provider_8cs.html | 2 +- ...ows_swappable_dmb_provider_8cs_source.html | 2 +- _windows_symlink_factory_8cs.html | 2 +- _windows_symlink_factory_8cs_source.html | 2 +- _windows_system_identity_8cs.html | 2 +- _windows_system_identity_8cs_source.html | 2 +- _windows_system_identity_factory_8cs.html | 2 +- ...ws_system_identity_factory_8cs_source.html | 2 +- _windows_watchdog_8cs.html | 2 +- _windows_watchdog_8cs_source.html | 8 +- _windows_watchdog_factory_8cs.html | 2 +- _windows_watchdog_factory_8cs_source.html | 4 +- annotated.html | 212 ++--- api.html | 2 +- architecture.html | 4 +- class_authorize_attribute.html | 2 +- class_controller.html | 2 +- class_db_context.html | 8 +- class_db_context__inherit__graph.map | 6 +- class_db_context__inherit__graph.md5 | 2 +- class_db_context__inherit__graph.png | Bin 5027 -> 26487 bytes class_i_async_enumerable.html | 116 +++ class_i_async_enumerable__inherit__graph.map | 4 + class_i_async_enumerable__inherit__graph.md5 | 1 + class_i_async_enumerable__inherit__graph.png | Bin 0 -> 8473 bytes class_i_design_time_db_context_factory.html | 9 +- ...ime_db_context_factory__inherit__graph.map | 7 +- ...ime_db_context_factory__inherit__graph.md5 | 2 +- ...ime_db_context_factory__inherit__graph.png | Bin 26828 -> 27348 bytes class_i_disposable.html | 2 +- class_i_document_filter.html | 2 +- class_i_hosted_service.html | 2 +- class_i_open_api_extension.html | 2 +- class_i_operation_filter.html | 2 +- class_i_queryable.html | 2 +- class_i_schema_filter.html | 2 +- class_json_converter.html | 2 +- class_migration.html | 9 +- class_migration__inherit__graph.map | 7 +- class_migration__inherit__graph.md5 | 2 +- class_migration__inherit__graph.png | Bin 242900 -> 276819 bytes class_model_snapshot.html | 9 +- class_model_snapshot__inherit__graph.map | 7 +- class_model_snapshot__inherit__graph.md5 | 2 +- class_model_snapshot__inherit__graph.png | Bin 17951 -> 21767 bytes class_service_base.html | 2 +- ...erver_1_1_api_1_1_api_headers-members.html | 2 +- ...on_1_1_server_1_1_api_1_1_api_headers.html | 6 +- ...1_1_models_1_1_administration-members.html | 2 +- ...1_1_api_1_1_models_1_1_administration.html | 2 +- ..._1_1_api_1_1_models_1_1_byond-members.html | 2 +- ...1_server_1_1_api_1_1_models_1_1_byond.html | 2 +- ...1_api_1_1_models_1_1_chat_bot-members.html | 2 +- ...erver_1_1_api_1_1_models_1_1_chat_bot.html | 2 +- ...i_1_1_models_1_1_chat_channel-members.html | 2 +- ...r_1_1_api_1_1_models_1_1_chat_channel.html | 2 +- ...pi_1_1_models_1_1_compile_job-members.html | 2 +- ...er_1_1_api_1_1_models_1_1_compile_job.html | 2 +- ...models_1_1_configuration_file-members.html | 2 +- ...api_1_1_models_1_1_configuration_file.html | 2 +- ...ord_connection_string_builder-members.html | 2 +- ...1_1_discord_connection_string_builder.html | 2 +- ...i_1_1_models_1_1_dream_daemon-members.html | 2 +- ...r_1_1_api_1_1_models_1_1_dream_daemon.html | 2 +- ...pi_1_1_models_1_1_dream_maker-members.html | 2 +- ...er_1_1_api_1_1_models_1_1_dream_maker.html | 2 +- ..._api_1_1_models_1_1_entity_id-members.html | 2 +- ...rver_1_1_api_1_1_models_1_1_entity_id.html | 4 +- ...els_1_1_error_code_extensions-members.html | 2 +- ..._1_1_models_1_1_error_code_extensions.html | 2 +- ..._1_1_models_1_1_error_message-members.html | 2 +- ..._1_1_api_1_1_models_1_1_error_message.html | 2 +- ...1_api_1_1_models_1_1_instance-members.html | 2 +- ...erver_1_1_api_1_1_models_1_1_instance.html | 4 +- ..._1_1_models_1_1_instance_user-members.html | 2 +- ..._1_1_api_1_1_models_1_1_instance_user.html | 6 +- ...els_1_1_internal_1_1_chat_bot-members.html | 2 +- ..._1_1_models_1_1_internal_1_1_chat_bot.html | 2 +- ...hat_connection_string_builder-members.html | 2 +- ...al_1_1_chat_connection_string_builder.html | 2 +- ..._1_1_internal_1_1_compile_job-members.html | 2 +- ...1_models_1_1_internal_1_1_compile_job.html | 2 +- ...ream_daemon_launch_parameters-members.html | 2 +- ...al_1_1_dream_daemon_launch_parameters.html | 4 +- ...nal_1_1_dream_daemon_settings-members.html | 2 +- ..._1_internal_1_1_dream_daemon_settings.html | 2 +- ...1_models_1_1_internal_1_1_job-members.html | 2 +- ...1_api_1_1_models_1_1_internal_1_1_job.html | 2 +- ...ernal_1_1_repository_settings-members.html | 2 +- ..._1_1_internal_1_1_repository_settings.html | 2 +- ...rnal_1_1_revision_information-members.html | 2 +- ...1_1_internal_1_1_revision_information.html | 6 +- ...ternal_1_1_server_information-members.html | 2 +- ...s_1_1_internal_1_1_server_information.html | 4 +- ...s_1_1_internal_1_1_test_merge-members.html | 2 +- ..._1_models_1_1_internal_1_1_test_merge.html | 2 +- ..._internal_1_1_test_merge_base-members.html | 2 +- ...dels_1_1_internal_1_1_test_merge_base.html | 2 +- ..._models_1_1_internal_1_1_user-members.html | 2 +- ..._api_1_1_models_1_1_internal_1_1_user.html | 8 +- ...irc_connection_string_builder-members.html | 2 +- ...els_1_1_irc_connection_string_builder.html | 2 +- ...er_1_1_api_1_1_models_1_1_job-members.html | 2 +- ...1_1_server_1_1_api_1_1_models_1_1_job.html | 2 +- ...1_1_api_1_1_models_1_1_limits-members.html | 2 +- ..._server_1_1_api_1_1_models_1_1_limits.html | 4 +- ...api_1_1_models_1_1_repository-members.html | 2 +- ...ver_1_1_api_1_1_models_1_1_repository.html | 2 +- ...dels_1_1_revision_information-members.html | 4 +- ...i_1_1_models_1_1_revision_information.html | 12 +- ...models_1_1_server_information-members.html | 2 +- ...api_1_1_models_1_1_server_information.html | 2 +- ...api_1_1_models_1_1_test_merge-members.html | 2 +- ...ver_1_1_api_1_1_models_1_1_test_merge.html | 2 +- ...els_1_1_test_merge_parameters-members.html | 2 +- ..._1_1_models_1_1_test_merge_parameters.html | 2 +- ..._1_1_api_1_1_models_1_1_token-members.html | 2 +- ...1_server_1_1_api_1_1_models_1_1_token.html | 2 +- ...r_1_1_api_1_1_models_1_1_user-members.html | 4 +- ..._1_server_1_1_api_1_1_models_1_1_user.html | 12 +- ...pi_1_1_models_1_1_user_update-members.html | 4 +- ...er_1_1_api_1_1_models_1_1_user_update.html | 8 +- ..._1_1_rights_1_1_rights_helper-members.html | 2 +- ..._1_1_api_1_1_rights_1_1_rights_helper.html | 4 +- ...1_1_server_1_1_api_1_1_routes-members.html | 2 +- ...station_1_1_server_1_1_api_1_1_routes.html | 6 +- ..._1_api_1_1_version_extensions-members.html | 2 +- ...server_1_1_api_1_1_version_extensions.html | 2 +- ...ent_1_1_administration_client-members.html | 2 +- ..._1_1_client_1_1_administration_client.html | 2 +- ...ver_1_1_client_1_1_api_client-members.html | 2 +- ..._1_1_server_1_1_client_1_1_api_client.html | 15 +- ...client_1_1_api_client_factory-members.html | 2 +- ...ver_1_1_client_1_1_api_client_factory.html | 2 +- ...nt_1_1_api_conflict_exception-members.html | 2 +- ...1_1_client_1_1_api_conflict_exception.html | 2 +- ..._1_1_client_1_1_api_exception-members.html | 2 +- ...1_server_1_1_client_1_1_api_exception.html | 2 +- ...1_client_1_1_client_exception-members.html | 2 +- ...erver_1_1_client_1_1_client_exception.html | 2 +- ...1_components_1_1_byond_client-members.html | 2 +- ...lient_1_1_components_1_1_byond_client.html | 2 +- ...mponents_1_1_chat_bots_client-members.html | 2 +- ...t_1_1_components_1_1_chat_bots_client.html | 2 +- ...ents_1_1_configuration_client-members.html | 2 +- ...1_components_1_1_configuration_client.html | 2 +- ...nents_1_1_dream_daemon_client-members.html | 2 +- ..._1_components_1_1_dream_daemon_client.html | 2 +- ...onents_1_1_dream_maker_client-members.html | 2 +- ...1_1_components_1_1_dream_maker_client.html | 2 +- ...omponents_1_1_instance_client-members.html | 2 +- ...nt_1_1_components_1_1_instance_client.html | 2 +- ...ents_1_1_instance_user_client-members.html | 2 +- ...1_components_1_1_instance_user_client.html | 2 +- ..._1_components_1_1_jobs_client-members.html | 2 +- ...client_1_1_components_1_1_jobs_client.html | 2 +- ...ponents_1_1_repository_client-members.html | 2 +- ..._1_1_components_1_1_repository_client.html | 2 +- ...client_1_1_conflict_exception-members.html | 2 +- ...ver_1_1_client_1_1_conflict_exception.html | 2 +- ...er_1_1_client_1_1_http_client-members.html | 2 +- ...1_1_server_1_1_client_1_1_http_client.html | 2 +- ...t_1_1_instance_manager_client-members.html | 2 +- ..._1_client_1_1_instance_manager_client.html | 2 +- ...ficient_permissions_exception-members.html | 2 +- ..._1_insufficient_permissions_exception.html | 2 +- ...ethod_not_supported_exception-members.html | 2 +- ...nt_1_1_method_not_supported_exception.html | 2 +- ...ient_1_1_rate_limit_exception-members.html | 2 +- ...r_1_1_client_1_1_rate_limit_exception.html | 2 +- ...1_1_request_timeout_exception-members.html | 2 +- ..._client_1_1_request_timeout_exception.html | 2 +- ..._1_1_client_1_1_server_client-members.html | 2 +- ...1_server_1_1_client_1_1_server_client.html | 2 +- ...ent_1_1_server_client_factory-members.html | 2 +- ..._1_1_client_1_1_server_client_factory.html | 2 +- ...nt_1_1_server_error_exception-members.html | 2 +- ...1_1_client_1_1_server_error_exception.html | 2 +- ...service_unavailable_exception-members.html | 2 +- ...ent_1_1_service_unavailable_exception.html | 2 +- ...nt_1_1_unauthorized_exception-members.html | 2 +- ...1_1_client_1_1_unauthorized_exception.html | 2 +- ...recognized_response_exception-members.html | 2 +- ...t_1_1_unrecognized_response_exception.html | 2 +- ...r_1_1_client_1_1_users_client-members.html | 2 +- ..._1_server_1_1_client_1_1_users_client.html | 2 +- ..._1_version_mismatch_exception-members.html | 2 +- ...client_1_1_version_mismatch_exception.html | 2 +- ...ond_1_1_byond_executable_lock-members.html | 2 +- ...s_1_1_byond_1_1_byond_executable_lock.html | 2 +- ...yond_1_1_byond_installer_base-members.html | 2 +- ...ts_1_1_byond_1_1_byond_installer_base.html | 2 +- ...s_1_1_byond_1_1_byond_manager-members.html | 2 +- ...omponents_1_1_byond_1_1_byond_manager.html | 6 +- ...ond_1_1_posix_byond_installer-members.html | 2 +- ...s_1_1_byond_1_1_posix_byond_installer.html | 2 +- ...d_1_1_windows_byond_installer-members.html | 2 +- ...1_1_byond_1_1_windows_byond_installer.html | 2 +- ..._1_1_chat_1_1_channel_mapping-members.html | 2 +- ...mponents_1_1_chat_1_1_channel_mapping.html | 2 +- ...at_1_1_channel_representation-members.html | 2 +- ...s_1_1_chat_1_1_channel_representation.html | 4 +- ...nts_1_1_chat_1_1_chat_manager-members.html | 6 +- ..._components_1_1_chat_1_1_chat_manager.html | 153 ++-- ...chat_1_1_chat_manager_factory-members.html | 2 +- ...nts_1_1_chat_1_1_chat_manager_factory.html | 2 +- ...hat_1_1_chat_tracking_context-members.html | 2 +- ...ts_1_1_chat_1_1_chat_tracking_context.html | 2 +- ...onents_1_1_chat_1_1_chat_user-members.html | 2 +- ...1_1_components_1_1_chat_1_1_chat_user.html | 4 +- ..._1_commands_1_1_byond_command-members.html | 2 +- ...1_chat_1_1_commands_1_1_byond_command.html | 2 +- ..._commands_1_1_command_factory-members.html | 2 +- ...chat_1_1_commands_1_1_command_factory.html | 2 +- ...1_commands_1_1_custom_command-members.html | 2 +- ..._chat_1_1_commands_1_1_custom_command.html | 2 +- ..._1_1_commands_1_1_kek_command-members.html | 2 +- ...1_1_chat_1_1_commands_1_1_kek_command.html | 2 +- ...nds_1_1_pull_requests_command-members.html | 2 +- ..._1_commands_1_1_pull_requests_command.html | 4 +- ...commands_1_1_revision_command-members.html | 2 +- ...hat_1_1_commands_1_1_revision_command.html | 2 +- ..._commands_1_1_version_command-members.html | 2 +- ...chat_1_1_commands_1_1_version_command.html | 2 +- ...roviders_1_1_discord_provider-members.html | 38 +- ...at_1_1_providers_1_1_discord_provider.html | 210 ++++- ...ders_1_1_discord_provider__coll__graph.map | 3 +- ...ders_1_1_discord_provider__coll__graph.md5 | 2 +- ...ders_1_1_discord_provider__coll__graph.png | Bin 58133 -> 70799 bytes ..._1_providers_1_1_irc_provider-members.html | 4 +- ...1_chat_1_1_providers_1_1_irc_provider.html | 78 +- ...hat_1_1_providers_1_1_message-members.html | 2 +- ...ts_1_1_chat_1_1_providers_1_1_message.html | 2 +- ...at_1_1_providers_1_1_provider-members.html | 11 +- ...s_1_1_chat_1_1_providers_1_1_provider.html | 157 +++- ...roviders_1_1_provider_factory-members.html | 2 +- ...at_1_1_providers_1_1_provider_factory.html | 4 +- ..._1_deployment_1_1_dmb_factory-members.html | 2 +- ...onents_1_1_deployment_1_1_dmb_factory.html | 18 +- ...1_deployment_1_1_dmb_provider-members.html | 2 +- ...nents_1_1_deployment_1_1_dmb_provider.html | 2 +- ..._1_deployment_1_1_dream_maker-members.html | 20 +- ...onents_1_1_deployment_1_1_dream_maker.html | 533 +++++------- ...eployment_1_1_dream_maker__coll__graph.map | 28 +- ...eployment_1_1_dream_maker__coll__graph.md5 | 2 +- ...eployment_1_1_dream_maker__coll__graph.png | Bin 152625 -> 154007 bytes ...nt_1_1_temporary_dmb_provider-members.html | 2 +- ...deployment_1_1_temporary_dmb_provider.html | 2 +- ...indows_swappable_dmb_provider-members.html | 2 +- ...nt_1_1_windows_swappable_dmb_provider.html | 8 +- ...9ed564ee9ba8dee124d358a4582a99_icgraph.map | 2 +- ...9ed564ee9ba8dee124d358a4582a99_icgraph.md5 | 2 +- ...9ed564ee9ba8dee124d358a4582a99_icgraph.png | Bin 18364 -> 18585 bytes ...components_1_1_event_consumer-members.html | 2 +- ...ost_1_1_components_1_1_event_consumer.html | 2 +- ...t_1_1_components_1_1_instance-members.html | 2 +- ..._1_1_host_1_1_components_1_1_instance.html | 30 +- ...mponents_1_1_instance_factory-members.html | 2 +- ...t_1_1_components_1_1_instance_factory.html | 10 +- ...mponents_1_1_instance_manager-members.html | 2 +- ...t_1_1_components_1_1_instance_manager.html | 38 +- ...ents_1_1_instance_manager__coll__graph.map | 6 +- ...ents_1_1_instance_manager__coll__graph.md5 | 2 +- ...ents_1_1_instance_manager__coll__graph.png | Bin 206426 -> 207563 bytes ..._bridge_1_1_bridge_parameters-members.html | 2 +- ...erop_1_1_bridge_1_1_bridge_parameters.html | 2 +- ...ridge_1_1_bridge_registration-members.html | 2 +- ...op_1_1_bridge_1_1_bridge_registration.html | 2 +- ..._1_bridge_1_1_bridge_response-members.html | 2 +- ...nterop_1_1_bridge_1_1_bridge_response.html | 2 +- ...ridge_1_1_runtime_information-members.html | 2 +- ...op_1_1_bridge_1_1_runtime_information.html | 2 +- ...ge_1_1_test_merge_information-members.html | 2 +- ...1_1_bridge_1_1_test_merge_information.html | 2 +- ..._1_1_interop_1_1_chat_message-members.html | 2 +- ...mponents_1_1_interop_1_1_chat_message.html | 2 +- ...s_1_1_interop_1_1_chat_update-members.html | 2 +- ...omponents_1_1_interop_1_1_chat_update.html | 2 +- ...converters_1_1_bool_converter-members.html | 2 +- ...rop_1_1_converters_1_1_bool_converter.html | 2 +- ...verters_1_1_version_converter-members.html | 2 +- ..._1_1_converters_1_1_version_converter.html | 2 +- ...interop_1_1_d_m_api_constants-members.html | 2 +- ...nts_1_1_interop_1_1_d_m_api_constants.html | 6 +- ...nterop_1_1_d_m_api_parameters-members.html | 2 +- ...ts_1_1_interop_1_1_d_m_api_parameters.html | 4 +- ..._interop_1_1_d_m_api_response-members.html | 2 +- ...ents_1_1_interop_1_1_d_m_api_response.html | 2 +- ...op_1_1_topic_1_1_chat_command-members.html | 2 +- ..._1_interop_1_1_topic_1_1_chat_command.html | 2 +- ..._topic_1_1_event_notification-members.html | 2 +- ...erop_1_1_topic_1_1_event_notification.html | 2 +- ..._1_topic_1_1_topic_parameters-members.html | 2 +- ...nterop_1_1_topic_1_1_topic_parameters.html | 2 +- ..._1_1_topic_1_1_topic_response-members.html | 2 +- ..._interop_1_1_topic_1_1_topic_response.html | 2 +- ...ository_1_1_lib_git2_commands-members.html | 2 +- ..._1_1_repository_1_1_lib_git2_commands.html | 2 +- ...1_lib_git2_repository_factory-members.html | 2 +- ...itory_1_1_lib_git2_repository_factory.html | 2 +- ...1_1_repository_1_1_repository-members.html | 2 +- ...ponents_1_1_repository_1_1_repository.html | 2 +- ...sitory_1_1_repository_manager-members.html | 2 +- ...1_1_repository_1_1_repository_manager.html | 2 +- ...n_1_1_combined_topic_response-members.html | 2 +- ...1_session_1_1_combined_topic_response.html | 2 +- ...n_1_1_dead_session_controller-members.html | 2 +- ...1_session_1_1_dead_session_controller.html | 2 +- ...1_1_dual_reattach_information-members.html | 2 +- ...session_1_1_dual_reattach_information.html | 6 +- ...1_1_session_1_1_launch_result-members.html | 2 +- ...ponents_1_1_session_1_1_launch_result.html | 2 +- ...ion_1_1_reattach_info_handler-members.html | 2 +- ...1_1_session_1_1_reattach_info_handler.html | 6 +- ...sion_1_1_reattach_information-members.html | 2 +- ..._1_1_session_1_1_reattach_information.html | 4 +- ...ession_1_1_session_controller-members.html | 2 +- ...ts_1_1_session_1_1_session_controller.html | 2 +- ..._1_session_controller_factory-members.html | 2 +- ...ession_1_1_session_controller_factory.html | 8 +- ...tatic_files_1_1_configuration-members.html | 2 +- ...ts_1_1_static_files_1_1_configuration.html | 2 +- ...1_1_server_side_modifications-members.html | 2 +- ...c_files_1_1_server_side_modifications.html | 2 +- ...1_watchdog_1_1_basic_watchdog-members.html | 65 +- ...nents_1_1_watchdog_1_1_basic_watchdog.html | 57 +- ...tchdog_1_1_basic_watchdog__coll__graph.map | 26 +- ...tchdog_1_1_basic_watchdog__coll__graph.md5 | 2 +- ...tchdog_1_1_basic_watchdog__coll__graph.png | Bin 111938 -> 135896 bytes ...dog_1_1_experimental_watchdog-members.html | 57 +- ..._1_watchdog_1_1_experimental_watchdog.html | 69 +- ...1_1_experimental_watchdog__coll__graph.map | 30 +- ...1_1_experimental_watchdog__coll__graph.md5 | 2 +- ...1_1_experimental_watchdog__coll__graph.png | Bin 147045 -> 151483 bytes ..._1_watchdog_1_1_monitor_state-members.html | 2 +- ...onents_1_1_watchdog_1_1_monitor_state.html | 10 +- ..._1_watchdog_1_1_watchdog_base-members.html | 12 +- ...onents_1_1_watchdog_1_1_watchdog_base.html | 246 ++++-- ...atchdog_1_1_watchdog_base__coll__graph.md5 | 2 +- ...atchdog_1_1_watchdog_base__coll__graph.png | Bin 97844 -> 109516 bytes ...watchdog_1_1_watchdog_factory-members.html | 2 +- ...nts_1_1_watchdog_1_1_watchdog_factory.html | 6 +- ...watchdog_1_1_windows_watchdog-members.html | 73 +- ...nts_1_1_watchdog_1_1_windows_watchdog.html | 71 +- ...hdog_1_1_windows_watchdog__coll__graph.map | 44 +- ...hdog_1_1_windows_watchdog__coll__graph.md5 | 2 +- ...hdog_1_1_windows_watchdog__coll__graph.png | Bin 206523 -> 233479 bytes ...39ec5c72078453b2d1787a496013c84_cgraph.map | 3 + ...39ec5c72078453b2d1787a496013c84_cgraph.md5 | 1 + ...39ec5c72078453b2d1787a496013c84_cgraph.png | Bin 0 -> 11558 bytes ..._1_1_windows_watchdog_factory-members.html | 2 +- ...watchdog_1_1_windows_watchdog_factory.html | 2 +- ...1_control_panel_configuration-members.html | 2 +- ...ation_1_1_control_panel_configuration.html | 6 +- ...on_1_1_database_configuration-members.html | 2 +- ...figuration_1_1_database_configuration.html | 22 +- ..._1_file_logging_configuration-members.html | 2 +- ...ration_1_1_file_logging_configuration.html | 4 +- ...ion_1_1_general_configuration-members.html | 6 +- ...nfiguration_1_1_general_configuration.html | 90 +- ...1_1_general_configuration__coll__graph.md5 | 2 +- ...1_1_general_configuration__coll__graph.png | Bin 18099 -> 20532 bytes ...ion_1_1_updates_configuration-members.html | 2 +- ...nfiguration_1_1_updates_configuration.html | 2 +- ..._host_1_1_console_1_1_program-members.html | 2 +- ...rver_1_1_host_1_1_console_1_1_program.html | 2 +- ...1_1_administration_controller-members.html | 2 +- ...rollers_1_1_administration_controller.html | 12 +- ...administration_controller__coll__graph.map | 10 +- ...administration_controller__coll__graph.md5 | 2 +- ...administration_controller__coll__graph.png | Bin 105087 -> 109803 bytes ...ontrollers_1_1_api_controller-members.html | 2 +- ...st_1_1_controllers_1_1_api_controller.html | 6 +- ...rollers_1_1_bridge_controller-members.html | 2 +- ...1_1_controllers_1_1_bridge_controller.html | 2 +- ...trollers_1_1_byond_controller-members.html | 2 +- ..._1_1_controllers_1_1_byond_controller.html | 2 +- ...ntrollers_1_1_chat_controller-members.html | 2 +- ...t_1_1_controllers_1_1_chat_controller.html | 66 +- ...8a75a3f14b37983b1e5a76f2186e0ad_cgraph.map | 2 +- ...8a75a3f14b37983b1e5a76f2186e0ad_cgraph.md5 | 2 +- ...568d3ba39d641f3fb486c779aea92cf_cgraph.map | 2 +- ...568d3ba39d641f3fb486c779aea92cf_cgraph.md5 | 2 +- ..._1_1_configuration_controller-members.html | 2 +- ...trollers_1_1_configuration_controller.html | 2 +- ...s_1_1_dream_daemon_controller-members.html | 2 +- ...ntrollers_1_1_dream_daemon_controller.html | 32 +- ...9ce00c8ac0758a4c2328eae7d7ef2eb_cgraph.map | 2 +- ...9ce00c8ac0758a4c2328eae7d7ef2eb_cgraph.md5 | 2 +- ...rs_1_1_dream_maker_controller-members.html | 2 +- ...ontrollers_1_1_dream_maker_controller.html | 46 +- ...ec0755f8794e6323ba59634c76bd498_cgraph.map | 2 +- ...ec0755f8794e6323ba59634c76bd498_cgraph.md5 | 2 +- ...ntrollers_1_1_home_controller-members.html | 2 +- ...t_1_1_controllers_1_1_home_controller.html | 32 +- ...llers_1_1_home_controller__coll__graph.map | 20 +- ...llers_1_1_home_controller__coll__graph.md5 | 2 +- ...llers_1_1_home_controller__coll__graph.png | Bin 92356 -> 100451 bytes ...e4c43b884ebd0e9c4b60e39d4e1c162_cgraph.map | 2 +- ...e4c43b884ebd0e9c4b60e39d4e1c162_cgraph.md5 | 2 +- ...llers_1_1_instance_controller-members.html | 2 +- ...1_controllers_1_1_instance_controller.html | 72 +- ...s_1_1_instance_controller__coll__graph.map | 6 +- ...s_1_1_instance_controller__coll__graph.md5 | 2 +- ...s_1_1_instance_controller__coll__graph.png | Bin 91666 -> 96768 bytes ...4134d386ac311c0f7a76b7ef312a2d6_cgraph.map | 2 +- ...4134d386ac311c0f7a76b7ef312a2d6_cgraph.md5 | 2 +- ...712ba6603139064d68520e16c076901_cgraph.map | 2 +- ...712ba6603139064d68520e16c076901_cgraph.md5 | 2 +- ..._1_1_instance_user_controller-members.html | 2 +- ...trollers_1_1_instance_user_controller.html | 50 +- ...a26d7b7a1b8c8c8c0ade33893f24b09_cgraph.map | 2 +- ...a26d7b7a1b8c8c8c0ade33893f24b09_cgraph.md5 | 2 +- ...f0cc09f53acd6c87a91a4a01afe8daa_cgraph.map | 2 +- ...f0cc09f53acd6c87a91a4a01afe8daa_cgraph.md5 | 2 +- ...ontrollers_1_1_job_controller-members.html | 2 +- ...st_1_1_controllers_1_1_job_controller.html | 36 +- ...ers_1_1_repository_controller-members.html | 2 +- ...controllers_1_1_repository_controller.html | 64 +- ...1_1_repository_controller__coll__graph.map | 6 +- ...1_1_repository_controller__coll__graph.md5 | 2 +- ...1_1_repository_controller__coll__graph.png | Bin 65264 -> 62462 bytes ...263b7f7bef9a5b23cdc52c3735af3c5_cgraph.map | 2 +- ...263b7f7bef9a5b23cdc52c3735af3c5_cgraph.md5 | 2 +- ...fc3a45ac79e0707e7fe4bb1b625761d_cgraph.map | 2 +- ...fc3a45ac79e0707e7fe4bb1b625761d_cgraph.md5 | 2 +- ...5ffc737feb4385afff13146e336a910_cgraph.map | 2 +- ...5ffc737feb4385afff13146e336a910_cgraph.md5 | 2 +- ...s_1_1_tgs_authorize_attribute-members.html | 2 +- ...ntrollers_1_1_tgs_authorize_attribute.html | 2 +- ...ntrollers_1_1_user_controller-members.html | 2 +- ...t_1_1_controllers_1_1_user_controller.html | 48 +- ...llers_1_1_user_controller__coll__graph.map | 2 +- ...llers_1_1_user_controller__coll__graph.md5 | 2 +- ...llers_1_1_user_controller__coll__graph.png | Bin 55173 -> 52102 bytes ...f795d4042188f30317c6134b75afc70_cgraph.map | 2 +- ...f795d4042188f30317c6134b75afc70_cgraph.md5 | 2 +- ...9457477cad4b6adcedbba47d939f005_cgraph.map | 2 +- ...9457477cad4b6adcedbba47d939f005_cgraph.md5 | 2 +- ...host_1_1_core_1_1_application-members.html | 2 +- ...ver_1_1_host_1_1_core_1_1_application.html | 16 +- ...st_1_1_core_1_1_async_delayer-members.html | 2 +- ...r_1_1_host_1_1_core_1_1_async_delayer.html | 4 +- ...re_1_1_git_hub_client_factory-members.html | 2 +- ...t_1_1_core_1_1_git_hub_client_factory.html | 2 +- ..._api_enum_var_names_extension-members.html | 2 +- ...1_1_open_api_enum_var_names_extension.html | 4 +- ...core_1_1_restart_registration-members.html | 2 +- ...ost_1_1_core_1_1_restart_registration.html | 2 +- ...re_1_1_semaphore_slim_context-members.html | 2 +- ...t_1_1_core_1_1_semaphore_slim_context.html | 4 +- ...core_1_1_server_port_proivder-members.html | 7 +- ...ost_1_1_core_1_1_server_port_proivder.html | 87 +- ..._1_1_server_port_proivder__coll__graph.map | 4 +- ..._1_1_server_port_proivder__coll__graph.md5 | 2 +- ..._1_1_server_port_proivder__coll__graph.png | Bin 4449 -> 42759 bytes ...ore_1_1_swagger_configuration-members.html | 2 +- ...st_1_1_core_1_1_swagger_configuration.html | 16 +- ...abase_1_1_database_collection-members.html | 5 +- ..._1_1_database_1_1_database_collection.html | 76 +- ...e_1_1_database_collection__coll__graph.map | 5 +- ...e_1_1_database_collection__coll__graph.md5 | 2 +- ...e_1_1_database_collection__coll__graph.png | Bin 27200 -> 35420 bytes ..._1_database_collection__inherit__graph.map | 5 +- ..._1_database_collection__inherit__graph.md5 | 2 +- ..._1_database_collection__inherit__graph.png | Bin 7773 -> 10657 bytes ...1_database_connection_factory-members.html | 2 +- ...abase_1_1_database_connection_factory.html | 8 +- ...database_1_1_database_context-members.html | 108 +-- ...ost_1_1_database_1_1_database_context.html | 812 +++++++++--------- ...base_1_1_database_context__coll__graph.map | 34 +- ...base_1_1_database_context__coll__graph.md5 | 2 +- ...base_1_1_database_context__coll__graph.png | Bin 152003 -> 168613 bytes ...e_1_1_database_context__inherit__graph.map | 10 +- ...e_1_1_database_context__inherit__graph.md5 | 2 +- ...e_1_1_database_context__inherit__graph.png | Bin 7335 -> 21147 bytes ...e88d53bf1b6ac87d175d83ed39da7c_icgraph.map | 16 + ...e88d53bf1b6ac87d175d83ed39da7c_icgraph.md5 | 1 + ...e88d53bf1b6ac87d175d83ed39da7c_icgraph.png | Bin 0 -> 91098 bytes ..._1_1_database_context_factory-members.html | 2 +- ...database_1_1_database_context_factory.html | 2 +- ..._database_1_1_database_seeder-members.html | 2 +- ...host_1_1_database_1_1_database_seeder.html | 35 +- ...ed443f053ba4e1098f0e4099d23b774_cgraph.map | 1 - ...ed443f053ba4e1098f0e4099d23b774_cgraph.md5 | 2 +- ...ed443f053ba4e1098f0e4099d23b774_cgraph.png | Bin 9014 -> 5194 bytes ...me_db_context_factory_helpers-members.html | 4 +- ...esign_time_db_context_factory_helpers.html | 27 +- ...4ab9cd41e86e8ce83b3cdd8371b251_icgraph.map | 6 + ...4ab9cd41e86e8ce83b3cdd8371b251_icgraph.md5 | 1 + ...4ab9cd41e86e8ce83b3cdd8371b251_icgraph.png | Bin 0 -> 36242 bytes ...esign_time_db_context_factory-members.html | 2 +- ...my_sql_design_time_db_context_factory.html | 12 +- ...c2173be2b6fa289a8bfcf8da59de797_cgraph.map | 2 +- ...c2173be2b6fa289a8bfcf8da59de797_cgraph.md5 | 2 +- ...c2173be2b6fa289a8bfcf8da59de797_cgraph.png | Bin 12222 -> 12345 bytes ...esign_time_db_context_factory-members.html | 112 +++ ...es_sql_design_time_db_context_factory.html | 178 ++++ ...n_time_db_context_factory__coll__graph.map | 3 + ...n_time_db_context_factory__coll__graph.md5 | 1 + ...n_time_db_context_factory__coll__graph.png | Bin 0 -> 7167 bytes ...ime_db_context_factory__inherit__graph.map | 3 + ...ime_db_context_factory__inherit__graph.md5 | 1 + ...ime_db_context_factory__inherit__graph.png | Bin 0 -> 7167 bytes ...6ff3f3ef52311c0fa45971cc3156bb1_cgraph.map | 3 + ...6ff3f3ef52311c0fa45971cc3156bb1_cgraph.md5 | 1 + ...6ff3f3ef52311c0fa45971cc3156bb1_cgraph.png | Bin 0 -> 12188 bytes ...esign_time_db_context_factory-members.html | 2 +- ...server_design_time_db_context_factory.html | 12 +- ...f56794443746e5d8d0c08389d7cd8a5_cgraph.map | 2 +- ...f56794443746e5d8d0c08389d7cd8a5_cgraph.md5 | 2 +- ...f56794443746e5d8d0c08389d7cd8a5_cgraph.png | Bin 12092 -> 12194 bytes ...esign_time_db_context_factory-members.html | 2 +- ...sqlite_design_time_db_context_factory.html | 12 +- ...8c77d7409d55231d096416130531e93_cgraph.map | 2 +- ...8c77d7409d55231d096416130531e93_cgraph.md5 | 2 +- ...8c77d7409d55231d096416130531e93_cgraph.png | Bin 12077 -> 12197 bytes ...hat_bot_reconnection_interval-members.html | 2 +- ..._s_add_chat_bot_reconnection_interval.html | 2 +- ...d_compile_job_d_m_api_version-members.html | 2 +- ...1_m_s_add_compile_job_d_m_api_version.html | 2 +- ...rations_1_1_m_s_add_heartbeat-members.html | 2 +- ..._1_1_migrations_1_1_m_s_add_heartbeat.html | 2 +- ...s_1_1_m_s_add_job_error_codes-members.html | 2 +- ...igrations_1_1_m_s_add_job_error_codes.html | 2 +- ..._1_1_m_s_add_minimum_security-members.html | 2 +- ...grations_1_1_m_s_add_minimum_security.html | 2 +- ...ns_1_1_m_s_checkpoint_rebuild-members.html | 2 +- ...migrations_1_1_m_s_checkpoint_rebuild.html | 2 +- ..._1_1_m_s_fix_cascading_delete-members.html | 2 +- ...grations_1_1_m_s_fix_cascading_delete.html | 2 +- ...ns_1_1_m_s_fix_rev_info_index-members.html | 2 +- ...migrations_1_1_m_s_fix_rev_info_index.html | 2 +- ...ations_1_1_m_s_initial_create-members.html | 2 +- ...1_1_migrations_1_1_m_s_initial_create.html | 2 +- ...ill_jsons_add_d_m_api_version-members.html | 2 +- ..._1_m_s_kill_jsons_add_d_m_api_version.html | 2 +- ...ations_1_1_m_s_limits_on_chat-members.html | 2 +- ...1_1_migrations_1_1_m_s_limits_on_chat.html | 2 +- ...lable_and_foreign_key_cleanup-members.html | 2 +- ..._m_s_nullable_and_foreign_key_cleanup.html | 2 +- ...reattach_compile_job_required-members.html | 2 +- ...1_1_m_s_reattach_compile_job_required.html | 2 +- ...s_1_1_m_s_remove_soft_columns-members.html | 2 +- ...igrations_1_1_m_s_remove_soft_columns.html | 2 +- ...m_s_toggle_testmerge_comments-members.html | 2 +- ...ons_1_1_m_s_toggle_testmerge_comments.html | 2 +- ...ns_1_1_m_s_tons_of_validation-members.html | 2 +- ...migrations_1_1_m_s_tons_of_validation.html | 2 +- ...hat_bot_reconnection_interval-members.html | 2 +- ..._y_add_chat_bot_reconnection_interval.html | 2 +- ...d_compile_job_d_m_api_version-members.html | 2 +- ...1_m_y_add_compile_job_d_m_api_version.html | 2 +- ...rations_1_1_m_y_add_heartbeat-members.html | 2 +- ..._1_1_migrations_1_1_m_y_add_heartbeat.html | 2 +- ...s_1_1_m_y_add_job_error_codes-members.html | 2 +- ...igrations_1_1_m_y_add_job_error_codes.html | 2 +- ..._1_1_m_y_add_minimum_security-members.html | 2 +- ...grations_1_1_m_y_add_minimum_security.html | 2 +- ...ns_1_1_m_y_checkpoint_rebuild-members.html | 2 +- ...migrations_1_1_m_y_checkpoint_rebuild.html | 2 +- ..._1_1_m_y_fix_cascading_delete-members.html | 2 +- ...grations_1_1_m_y_fix_cascading_delete.html | 2 +- ...ns_1_1_m_y_fix_rev_info_index-members.html | 2 +- ...migrations_1_1_m_y_fix_rev_info_index.html | 2 +- ...ations_1_1_m_y_initial_create-members.html | 2 +- ...1_1_migrations_1_1_m_y_initial_create.html | 2 +- ...ill_jsons_add_d_m_api_version-members.html | 2 +- ..._1_m_y_kill_jsons_add_d_m_api_version.html | 2 +- ...ations_1_1_m_y_limits_on_chat-members.html | 2 +- ...1_1_migrations_1_1_m_y_limits_on_chat.html | 2 +- ...lable_and_foreign_key_cleanup-members.html | 2 +- ..._m_y_nullable_and_foreign_key_cleanup.html | 2 +- ...reattach_compile_job_required-members.html | 2 +- ...1_1_m_y_reattach_compile_job_required.html | 2 +- ...s_1_1_m_y_remove_soft_columns-members.html | 2 +- ...igrations_1_1_m_y_remove_soft_columns.html | 2 +- ...m_y_toggle_testmerge_comments-members.html | 2 +- ...ons_1_1_m_y_toggle_testmerge_comments.html | 2 +- ...ns_1_1_m_y_tons_of_validation-members.html | 2 +- ...migrations_1_1_m_y_tons_of_validation.html | 2 +- ...tabase_context_model_snapshot-members.html | 2 +- ...y_sql_database_context_model_snapshot.html | 2 +- ...1_1_migrations_1_1_p_g_create-members.html | 114 +++ ...atabase_1_1_migrations_1_1_p_g_create.html | 245 ++++++ ...migrations_1_1_p_g_create__coll__graph.map | 3 + ...migrations_1_1_p_g_create__coll__graph.md5 | 1 + ...migrations_1_1_p_g_create__coll__graph.png | Bin 0 -> 4861 bytes ...rations_1_1_p_g_create__inherit__graph.map | 3 + ...rations_1_1_p_g_create__inherit__graph.md5 | 1 + ...rations_1_1_p_g_create__inherit__graph.png | Bin 0 -> 4861 bytes ...rations_1_1_s_l_add_heartbeat-members.html | 2 +- ..._1_1_migrations_1_1_s_l_add_heartbeat.html | 2 +- ..._1_migrations_1_1_s_l_rebuild-members.html | 2 +- ...tabase_1_1_migrations_1_1_s_l_rebuild.html | 2 +- ...s_1_1_s_l_remove_soft_columns-members.html | 2 +- ...igrations_1_1_s_l_remove_soft_columns.html | 2 +- ...tabase_context_model_snapshot-members.html | 2 +- ...erver_database_context_model_snapshot.html | 2 +- ...tabase_context_model_snapshot-members.html | 2 +- ...qlite_database_context_model_snapshot.html | 2 +- ...e_1_1_my_sql_database_context-members.html | 48 +- ..._database_1_1_my_sql_database_context.html | 238 +++-- ...1_my_sql_database_context__coll__graph.map | 18 +- ...1_my_sql_database_context__coll__graph.md5 | 2 +- ...1_my_sql_database_context__coll__graph.png | Bin 5929 -> 152423 bytes ...y_sql_database_context__inherit__graph.map | 4 +- ...y_sql_database_context__inherit__graph.md5 | 2 +- ...y_sql_database_context__inherit__graph.png | Bin 5929 -> 8363 bytes ...postgres_sql_database_context-members.html | 137 +++ ...ase_1_1_postgres_sql_database_context.html | 404 +++++++++ ...gres_sql_database_context__coll__graph.map | 19 + ...gres_sql_database_context__coll__graph.md5 | 1 + ...gres_sql_database_context__coll__graph.png | Bin 0 -> 152471 bytes ...s_sql_database_context__inherit__graph.map | 5 + ...s_sql_database_context__inherit__graph.md5 | 1 + ...s_sql_database_context__inherit__graph.png | Bin 0 -> 8223 bytes ...1_sql_server_database_context-members.html | 48 +- ...abase_1_1_sql_server_database_context.html | 236 +++-- ...l_server_database_context__coll__graph.map | 18 +- ...l_server_database_context__coll__graph.md5 | 2 +- ...l_server_database_context__coll__graph.png | Bin 5315 -> 152282 bytes ...erver_database_context__inherit__graph.map | 4 +- ...erver_database_context__inherit__graph.md5 | 2 +- ...erver_database_context__inherit__graph.png | Bin 5315 -> 8063 bytes ...e_1_1_sqlite_database_context-members.html | 46 +- ..._database_1_1_sqlite_database_context.html | 236 +++-- ...1_sqlite_database_context__coll__graph.map | 18 +- ...1_sqlite_database_context__coll__graph.md5 | 2 +- ...1_sqlite_database_context__coll__graph.png | Bin 5395 -> 152141 bytes ...qlite_database_context__inherit__graph.map | 4 +- ...qlite_database_context__inherit__graph.md5 | 2 +- ...qlite_database_context__inherit__graph.png | Bin 5395 -> 8067 bytes ...pplication_builder_extensions-members.html | 5 +- ...ns_1_1_application_builder_extensions.html | 40 +- ...s_1_1_chat_channel_extensions-members.html | 2 +- ...xtensions_1_1_chat_channel_extensions.html | 2 +- ...1_database_context_extensions-members.html | 2 +- ...sions_1_1_database_context_extensions.html | 4 +- ...s_1_1_host_builder_extensions-members.html | 2 +- ...xtensions_1_1_host_builder_extensions.html | 2 +- ...service_collection_extensions-members.html | 2 +- ...ons_1_1_service_collection_extensions.html | 2 +- ...xtensions_1_1_task_extensions-members.html | 2 +- ...st_1_1_extensions_1_1_task_extensions.html | 2 +- ...1_web_host_builder_extensions-members.html | 2 +- ...sions_1_1_web_host_builder_extensions.html | 2 +- ..._1_1_host_1_1_i_o_1_1_console-members.html | 2 +- ...1_server_1_1_host_1_1_i_o_1_1_console.html | 2 +- ...1_i_o_1_1_default_i_o_manager-members.html | 2 +- ..._host_1_1_i_o_1_1_default_i_o_manager.html | 2 +- ..._1_1_posix_post_write_handler-members.html | 2 +- ..._1_1_i_o_1_1_posix_post_write_handler.html | 2 +- ...i_o_1_1_posix_symlink_factory-members.html | 2 +- ...ost_1_1_i_o_1_1_posix_symlink_factory.html | 2 +- ...i_o_1_1_resolving_i_o_manager-members.html | 2 +- ...ost_1_1_i_o_1_1_resolving_i_o_manager.html | 2 +- ...o_1_1_synchronous_i_o_manager-members.html | 2 +- ...t_1_1_i_o_1_1_synchronous_i_o_manager.html | 2 +- ..._1_windows_post_write_handler-members.html | 2 +- ..._1_i_o_1_1_windows_post_write_handler.html | 2 +- ...o_1_1_windows_symlink_factory-members.html | 2 +- ...t_1_1_i_o_1_1_windows_symlink_factory.html | 2 +- ...st_1_1_jobs_1_1_job_exception-members.html | 2 +- ...r_1_1_host_1_1_jobs_1_1_job_exception.html | 2 +- ...host_1_1_jobs_1_1_job_handler-members.html | 2 +- ...ver_1_1_host_1_1_jobs_1_1_job_handler.html | 6 +- ...host_1_1_jobs_1_1_job_manager-members.html | 2 +- ...ver_1_1_host_1_1_jobs_1_1_job_manager.html | 22 +- ...tabase_context_model_snapshot-members.html | 112 +++ ...s_sql_database_context_model_snapshot.html | 166 ++++ ...se_context_model_snapshot__coll__graph.map | 3 + ...se_context_model_snapshot__coll__graph.md5 | 1 + ...se_context_model_snapshot__coll__graph.png | Bin 0 -> 5895 bytes ...context_model_snapshot__inherit__graph.map | 3 + ...context_model_snapshot__inherit__graph.md5 | 1 + ...context_model_snapshot__inherit__graph.png | Bin 0 -> 5895 bytes ..._host_1_1_models_1_1_chat_bot-members.html | 2 +- ...rver_1_1_host_1_1_models_1_1_chat_bot.html | 2 +- ...t_1_1_models_1_1_chat_channel-members.html | 2 +- ..._1_1_host_1_1_models_1_1_chat_channel.html | 2 +- ...st_1_1_models_1_1_compile_job-members.html | 2 +- ...r_1_1_host_1_1_models_1_1_compile_job.html | 2 +- ...els_1_1_dream_daemon_settings-members.html | 2 +- ..._1_1_models_1_1_dream_daemon_settings.html | 2 +- ...dels_1_1_dream_maker_settings-members.html | 2 +- ...t_1_1_models_1_1_dream_maker_settings.html | 2 +- ...1_1_dual_reattach_information-members.html | 2 +- ..._models_1_1_dual_reattach_information.html | 2 +- ...ual_reattach_information_base-members.html | 2 +- ...ls_1_1_dual_reattach_information_base.html | 4 +- ..._host_1_1_models_1_1_instance-members.html | 2 +- ...rver_1_1_host_1_1_models_1_1_instance.html | 2 +- ..._1_1_models_1_1_instance_user-members.html | 2 +- ...1_1_host_1_1_models_1_1_instance_user.html | 2 +- ...r_1_1_host_1_1_models_1_1_job-members.html | 2 +- ..._1_server_1_1_host_1_1_models_1_1_job.html | 4 +- ...dels_1_1_reattach_information-members.html | 2 +- ...t_1_1_models_1_1_reattach_information.html | 2 +- ...1_1_reattach_information_base-members.html | 2 +- ..._models_1_1_reattach_information_base.html | 2 +- ...odels_1_1_repository_settings-members.html | 2 +- ...st_1_1_models_1_1_repository_settings.html | 2 +- ...odels_1_1_rev_info_test_merge-members.html | 2 +- ...st_1_1_models_1_1_rev_info_test_merge.html | 2 +- ...dels_1_1_revision_information-members.html | 2 +- ...t_1_1_models_1_1_revision_information.html | 4 +- ...ost_1_1_models_1_1_test_merge-members.html | 2 +- ...er_1_1_host_1_1_models_1_1_test_merge.html | 2 +- ..._1_1_host_1_1_models_1_1_user-members.html | 2 +- ...1_server_1_1_host_1_1_models_1_1_user.html | 4 +- ...r_1_1_host_1_1_native_methods-members.html | 2 +- ..._1_server_1_1_host_1_1_native_methods.html | 2 +- ...1_server_1_1_host_1_1_program-members.html | 2 +- ...ation_1_1_server_1_1_host_1_1_program.html | 2 +- ...ty_1_1_authentication_context-members.html | 2 +- ...1_security_1_1_authentication_context.html | 12 +- ...8aeb7dd0a0e480f6f89bd5973703c7_icgraph.map | 4 +- ...8aeb7dd0a0e480f6f89bd5973703c7_icgraph.md5 | 2 +- ...8aeb7dd0a0e480f6f89bd5973703c7_icgraph.png | Bin 76476 -> 76339 bytes ...uthentication_context_factory-members.html | 2 +- ...ty_1_1_authentication_context_factory.html | 4 +- ..._security_1_1_claims_injector-members.html | 2 +- ...host_1_1_security_1_1_claims_injector.html | 2 +- ...curity_1_1_cryptography_suite-members.html | 2 +- ...t_1_1_security_1_1_cryptography_suite.html | 2 +- ...1_security_1_1_identity_cache-members.html | 2 +- ..._host_1_1_security_1_1_identity_cache.html | 2 +- ...ity_1_1_identity_cache_object-members.html | 2 +- ..._1_security_1_1_identity_cache_object.html | 2 +- ...ity_1_1_posix_system_identity-members.html | 2 +- ..._1_security_1_1_posix_system_identity.html | 2 +- ...posix_system_identity_factory-members.html | 2 +- ...ity_1_1_posix_system_identity_factory.html | 2 +- ..._1_security_1_1_token_factory-members.html | 2 +- ...1_host_1_1_security_1_1_token_factory.html | 2 +- ...y_1_1_windows_system_identity-members.html | 2 +- ..._security_1_1_windows_system_identity.html | 2 +- ...ndows_system_identity_factory-members.html | 2 +- ...y_1_1_windows_system_identity_factory.html | 2 +- ..._1_server_1_1_host_1_1_server-members.html | 2 +- ...tation_1_1_server_1_1_host_1_1_server.html | 10 +- ...erver_1_1_host_1_1_server__coll__graph.map | 2 +- ...erver_1_1_host_1_1_server__coll__graph.md5 | 2 +- ...erver_1_1_host_1_1_server__coll__graph.png | Bin 87453 -> 93597 bytes ...r_1_1_host_1_1_server_factory-members.html | 2 +- ..._1_server_1_1_host_1_1_server_factory.html | 17 +- ..._host_1_1_service_1_1_program-members.html | 2 +- ...rver_1_1_host_1_1_service_1_1_program.html | 2 +- ..._1_service_1_1_server_service-members.html | 2 +- ...1_host_1_1_service_1_1_server_service.html | 2 +- ...setup_1_1_post_setup_services-members.html | 2 +- ...ost_1_1_setup_1_1_post_setup_services.html | 14 +- ...p_1_1_post_setup_services__coll__graph.map | 10 +- ...p_1_1_post_setup_services__coll__graph.md5 | 2 +- ...p_1_1_post_setup_services__coll__graph.png | Bin 73763 -> 75844 bytes ...1_setup_1_1_setup_application-members.html | 2 +- ..._host_1_1_setup_1_1_setup_application.html | 6 +- ...st_1_1_setup_1_1_setup_wizard-members.html | 4 +- ...r_1_1_host_1_1_setup_1_1_setup_wizard.html | 205 ++--- ..._1_setup_1_1_setup_wizard__coll__graph.map | 18 +- ..._1_setup_1_1_setup_wizard__coll__graph.md5 | 2 +- ..._1_setup_1_1_setup_wizard__coll__graph.png | Bin 92168 -> 85161 bytes ...assembly_information_provider-members.html | 2 +- ...tem_1_1_assembly_information_provider.html | 2 +- ...ystem_1_1_platform_identifier-members.html | 2 +- ...st_1_1_system_1_1_platform_identifier.html | 2 +- ...1_posix_network_prompt_reaper-members.html | 2 +- ...ystem_1_1_posix_network_prompt_reaper.html | 2 +- ...em_1_1_posix_process_features-members.html | 2 +- ...1_1_system_1_1_posix_process_features.html | 2 +- ...1_host_1_1_system_1_1_process-members.html | 2 +- ...erver_1_1_host_1_1_system_1_1_process.html | 2 +- ...1_system_1_1_process_executor-members.html | 2 +- ..._host_1_1_system_1_1_process_executor.html | 2 +- ...program_shutdown_token_source-members.html | 2 +- ...tem_1_1_program_shutdown_token_source.html | 2 +- ...windows_network_prompt_reaper-members.html | 2 +- ...tem_1_1_windows_network_prompt_reaper.html | 2 +- ..._1_1_windows_process_features-members.html | 2 +- ...1_system_1_1_windows_process_features.html | 2 +- ...ost_1_1_watchdog_1_1_watchdog-members.html | 2 +- ...er_1_1_host_1_1_watchdog_1_1_watchdog.html | 2 +- ...watchdog_1_1_watchdog_factory-members.html | 2 +- ...ost_1_1_watchdog_1_1_watchdog_factory.html | 2 +- classes.html | 205 ++--- dir_02f69155ced6ccca99a24744a5273903.html | 2 +- dir_08963d262fa9e27215dce7b7c6e97f1b.html | 2 +- dir_0de875c2ee7edd4009e892c5045f7b3a.html | 2 +- dir_0f5073360b9ba6d1522457c65eebe36a.html | 2 +- dir_1cebbc1bce26b60bfb2ee26c80f86b02.html | 2 +- dir_222f69c34ae6290dff005cdc719e0a43.html | 2 +- dir_27a421f75b087b80bd0b7a1dbec733b4.html | 2 +- dir_2ea2465064f3ff030c6dd6700a328d91.html | 2 +- dir_34761767c1c3cd212f9d2f80ea0828a8.html | 2 +- dir_38cec1532eaed7aa1de0b599a76e3e80.html | 2 +- dir_3a3ab2f663730d5bed7026cf41734e8a.html | 4 +- dir_3d220027e29e19c608b6d51ce3029c32.html | 2 +- dir_4ddbd20a555bd1f3bf9569744c8989cc.html | 2 +- dir_5ae0a4770cfe9bc511dad7df7deabb23.html | 2 +- dir_5d46d0c3fd1cac5ce1ea23751ad25792.html | 2 +- dir_5e0b464d3b4be546c63c5188ffeb96e4.html | 2 +- dir_61d7974a657c30011f457b376d274ee7.html | 2 +- dir_68267d1309a1af8e8297ef4c3efbcdba.html | 2 +- dir_6a8d19239e99965ecc1391ebd049009a.html | 8 +- dir_6c60df807dd2523776d391746c262b4d.html | 2 +- dir_6d5d151b7baf54e07e6b0a3941fbad55.html | 2 +- dir_7a3fd032a1362a6005a1fc9227a8dd86.html | 2 +- dir_7cd79c49f3850abc2141fa4adfdd1950.html | 2 +- dir_860acef72838dce8d3d54c8f98bc464b.html | 2 +- dir_935188fa6520fafdbdb4920911a885bd.html | 2 +- dir_9826fb2d31585a830723e6d7089b5d0e.html | 2 +- dir_a01d946a2e1b048dc542232ffcdcf572.html | 2 +- dir_a44a562b5a12ef1c8495ac1a63e915ea.html | 2 +- dir_a536e49cdf631f66733cf2c9791144ce.html | 2 +- dir_af336dd70489e812a924e1a8fba73854.html | 4 +- dir_b6324197f7ac56e30d6189c3a45eec56.html | 2 +- dir_be6161819238d55fa660c07ade960e53.html | 2 +- dir_c2c4d4ee47e55233c6ea5db8b96422c6.html | 2 +- dir_c5451d9d233744a5b09a29938f0cda9a.html | 2 +- dir_d822dd0b4e53d3dc95c9c470fa54d3d2.html | 2 +- dir_e2f0549e5ba528b36fd9348e214a8e4f.html | 2 +- dir_f0d41009ce252337feb2b68822c4cfba.html | 2 +- dir_f2a82faa4c4beef364292694cefca86d.html | 2 +- dir_f5938f3ae6ef69fa99e4683082cea79c.html | 2 +- dir_f90d62caf45952f011371f45080cf0bd.html | 2 +- features.html | 2 +- files.html | 21 +- functions.html | 12 +- functions_0x7e.html | 2 +- functions_b.html | 4 +- functions_c.html | 50 +- functions_d.html | 45 +- functions_e.html | 2 +- functions_enum.html | 2 +- functions_f.html | 6 +- functions_func.html | 2 +- functions_func_0x7e.html | 2 +- functions_func_b.html | 4 +- functions_func_c.html | 11 +- functions_func_d.html | 22 +- functions_func_e.html | 2 +- functions_func_f.html | 6 +- functions_func_g.html | 9 +- functions_func_h.html | 2 +- functions_func_i.html | 4 +- functions_func_j.html | 2 +- functions_func_k.html | 2 +- functions_func_l.html | 2 +- functions_func_m.html | 2 +- functions_func_n.html | 2 +- functions_func_o.html | 5 +- functions_func_p.html | 7 +- functions_func_r.html | 2 +- functions_func_s.html | 17 +- functions_func_t.html | 6 +- functions_func_u.html | 8 +- functions_func_v.html | 7 +- functions_func_w.html | 2 +- functions_func_z.html | 2 +- functions_g.html | 10 +- functions_h.html | 2 +- functions_i.html | 12 +- functions_j.html | 6 +- functions_k.html | 2 +- functions_l.html | 4 +- functions_m.html | 2 +- functions_n.html | 6 +- functions_o.html | 5 +- functions_p.html | 7 +- functions_prop.html | 5 +- functions_prop_b.html | 2 +- functions_prop_c.html | 12 +- functions_prop_d.html | 8 +- functions_prop_e.html | 2 +- functions_prop_f.html | 2 +- functions_prop_g.html | 2 +- functions_prop_h.html | 3 +- functions_prop_i.html | 6 +- functions_prop_j.html | 4 +- functions_prop_l.html | 4 +- functions_prop_m.html | 2 +- functions_prop_n.html | 2 +- functions_prop_o.html | 2 +- functions_prop_p.html | 2 +- functions_prop_r.html | 10 +- functions_prop_s.html | 2 +- functions_prop_t.html | 4 +- functions_prop_u.html | 4 +- functions_prop_v.html | 2 +- functions_prop_w.html | 4 +- functions_q.html | 2 +- functions_r.html | 16 +- functions_s.html | 17 +- functions_t.html | 10 +- functions_u.html | 12 +- functions_v.html | 5 +- functions_vars.html | 3 +- functions_vars_b.html | 2 +- functions_vars_c.html | 29 +- functions_vars_d.html | 23 +- functions_vars_e.html | 2 +- functions_vars_f.html | 2 +- functions_vars_g.html | 3 +- functions_vars_h.html | 5 +- functions_vars_i.html | 10 +- functions_vars_j.html | 6 +- functions_vars_k.html | 2 +- functions_vars_l.html | 2 +- functions_vars_m.html | 2 +- functions_vars_n.html | 2 +- functions_vars_o.html | 2 +- functions_vars_p.html | 2 +- functions_vars_q.html | 2 +- functions_vars_r.html | 14 +- functions_vars_s.html | 2 +- functions_vars_t.html | 2 +- functions_vars_u.html | 6 +- functions_vars_v.html | 2 +- functions_vars_w.html | 7 +- functions_w.html | 9 +- functions_z.html | 2 +- graph_legend.html | 2 +- hierarchy.html | 803 ++++++++--------- index.html | 2 +- inherit_graph_100.map | 8 +- inherit_graph_100.md5 | 2 +- inherit_graph_100.png | Bin 3001 -> 29363 bytes inherit_graph_101.map | 3 +- inherit_graph_101.md5 | 2 +- inherit_graph_101.png | Bin 2698 -> 3363 bytes inherit_graph_102.map | 4 +- inherit_graph_102.md5 | 2 +- inherit_graph_102.png | Bin 3193 -> 2901 bytes inherit_graph_103.map | 4 +- inherit_graph_103.md5 | 2 +- inherit_graph_103.png | Bin 8182 -> 2691 bytes inherit_graph_104.map | 3 +- inherit_graph_104.md5 | 2 +- inherit_graph_104.png | Bin 3363 -> 2162 bytes inherit_graph_105.map | 3 +- inherit_graph_105.md5 | 2 +- inherit_graph_105.png | Bin 2901 -> 2496 bytes inherit_graph_106.map | 2 +- inherit_graph_106.md5 | 2 +- inherit_graph_106.png | Bin 2691 -> 2334 bytes inherit_graph_107.map | 2 +- inherit_graph_107.md5 | 2 +- inherit_graph_107.png | Bin 2162 -> 2398 bytes inherit_graph_108.map | 2 +- inherit_graph_108.md5 | 2 +- inherit_graph_108.png | Bin 2496 -> 2169 bytes inherit_graph_109.map | 2 +- inherit_graph_109.md5 | 2 +- inherit_graph_109.png | Bin 2334 -> 2680 bytes inherit_graph_110.map | 4 +- inherit_graph_110.md5 | 2 +- inherit_graph_110.png | Bin 2398 -> 4677 bytes inherit_graph_111.map | 4 +- inherit_graph_111.md5 | 2 +- inherit_graph_111.png | Bin 2169 -> 8392 bytes inherit_graph_112.map | 4 +- inherit_graph_112.md5 | 2 +- inherit_graph_112.png | Bin 2680 -> 10113 bytes inherit_graph_113.map | 5 +- inherit_graph_113.md5 | 2 +- inherit_graph_113.png | Bin 4677 -> 3519 bytes inherit_graph_114.map | 5 +- inherit_graph_114.md5 | 2 +- inherit_graph_114.png | Bin 8392 -> 3129 bytes inherit_graph_115.map | 6 +- inherit_graph_115.md5 | 2 +- inherit_graph_115.png | Bin 10113 -> 10657 bytes inherit_graph_116.map | 3 +- inherit_graph_116.md5 | 2 +- inherit_graph_116.png | Bin 3519 -> 2391 bytes inherit_graph_117.map | 3 +- inherit_graph_117.md5 | 2 +- inherit_graph_117.png | Bin 3129 -> 2213 bytes inherit_graph_118.map | 4 +- inherit_graph_118.md5 | 2 +- inherit_graph_118.png | Bin 10657 -> 1747 bytes inherit_graph_119.map | 3 +- inherit_graph_119.md5 | 2 +- inherit_graph_119.png | Bin 2391 -> 3370 bytes inherit_graph_120.map | 3 +- inherit_graph_120.md5 | 2 +- inherit_graph_120.png | Bin 2213 -> 3538 bytes inherit_graph_121.map | 4 +- inherit_graph_121.md5 | 2 +- inherit_graph_121.png | Bin 1747 -> 8918 bytes inherit_graph_122.map | 4 +- inherit_graph_122.md5 | 2 +- inherit_graph_122.png | Bin 3370 -> 3220 bytes inherit_graph_123.map | 3 +- inherit_graph_123.md5 | 2 +- inherit_graph_123.png | Bin 3538 -> 2202 bytes inherit_graph_124.map | 5 +- inherit_graph_124.md5 | 2 +- inherit_graph_124.png | Bin 8918 -> 2766 bytes inherit_graph_125.map | 4 +- inherit_graph_125.md5 | 2 +- inherit_graph_125.png | Bin 3220 -> 3189 bytes inherit_graph_126.map | 3 +- inherit_graph_126.md5 | 2 +- inherit_graph_126.png | Bin 2202 -> 3556 bytes inherit_graph_127.map | 4 +- inherit_graph_127.md5 | 2 +- inherit_graph_127.png | Bin 2766 -> 3337 bytes inherit_graph_128.map | 4 +- inherit_graph_128.md5 | 2 +- inherit_graph_128.png | Bin 3189 -> 3216 bytes inherit_graph_129.map | 5 +- inherit_graph_129.md5 | 2 +- inherit_graph_129.png | Bin 3556 -> 8455 bytes inherit_graph_130.map | 4 +- inherit_graph_130.md5 | 2 +- inherit_graph_130.png | Bin 3337 -> 3078 bytes inherit_graph_131.map | 4 +- inherit_graph_131.md5 | 2 +- inherit_graph_131.png | Bin 3216 -> 3539 bytes inherit_graph_2.map | 7 +- inherit_graph_2.md5 | 2 +- inherit_graph_2.png | Bin 18624 -> 26278 bytes inherit_graph_4.map | 7 +- inherit_graph_4.md5 | 2 +- inherit_graph_4.png | Bin 6063 -> 7502 bytes inherit_graph_7.map | 9 +- inherit_graph_7.md5 | 2 +- inherit_graph_7.png | Bin 242515 -> 276755 bytes inherit_graph_76.map | 2 +- inherit_graph_76.md5 | 2 +- inherit_graph_8.map | 9 +- inherit_graph_8.md5 | 2 +- inherit_graph_8.png | Bin 17528 -> 21628 bytes inherit_graph_85.map | 3 +- inherit_graph_85.md5 | 2 +- inherit_graph_85.png | Bin 3768 -> 3442 bytes inherit_graph_86.map | 3 +- inherit_graph_86.md5 | 2 +- inherit_graph_86.png | Bin 3526 -> 2761 bytes inherit_graph_87.map | 3 +- inherit_graph_87.md5 | 2 +- inherit_graph_87.png | Bin 5752 -> 2730 bytes inherit_graph_88.map | 2 +- inherit_graph_88.md5 | 2 +- inherit_graph_88.png | Bin 3442 -> 2817 bytes inherit_graph_89.map | 2 +- inherit_graph_89.md5 | 2 +- inherit_graph_89.png | Bin 2761 -> 3009 bytes inherit_graph_90.map | 2 +- inherit_graph_90.md5 | 2 +- inherit_graph_90.png | Bin 2730 -> 3166 bytes inherit_graph_91.map | 2 +- inherit_graph_91.md5 | 2 +- inherit_graph_91.png | Bin 2817 -> 3142 bytes inherit_graph_92.map | 2 +- inherit_graph_92.md5 | 2 +- inherit_graph_92.png | Bin 3009 -> 2661 bytes inherit_graph_93.map | 2 +- inherit_graph_93.md5 | 2 +- inherit_graph_93.png | Bin 3166 -> 2813 bytes inherit_graph_94.map | 2 +- inherit_graph_94.md5 | 2 +- inherit_graph_94.png | Bin 3142 -> 2664 bytes inherit_graph_95.map | 2 +- inherit_graph_95.md5 | 2 +- inherit_graph_95.png | Bin 2661 -> 3238 bytes inherit_graph_96.map | 2 +- inherit_graph_96.md5 | 2 +- inherit_graph_96.png | Bin 2813 -> 3128 bytes inherit_graph_97.map | 2 +- inherit_graph_97.md5 | 2 +- inherit_graph_97.png | Bin 2664 -> 3001 bytes inherit_graph_98.map | 2 +- inherit_graph_98.md5 | 2 +- inherit_graph_98.png | Bin 3238 -> 2698 bytes inherit_graph_99.map | 3 +- inherit_graph_99.md5 | 2 +- inherit_graph_99.png | Bin 3128 -> 3193 bytes inherits.html | 160 ++-- ...components_1_1_i_byond_client-members.html | 2 +- ...ent_1_1_components_1_1_i_byond_client.html | 2 +- ...onents_1_1_i_chat_bots_client-members.html | 2 +- ...1_1_components_1_1_i_chat_bots_client.html | 2 +- ...ts_1_1_i_configuration_client-members.html | 2 +- ...components_1_1_i_configuration_client.html | 2 +- ...nts_1_1_i_dream_daemon_client-members.html | 2 +- ..._components_1_1_i_dream_daemon_client.html | 2 +- ...ents_1_1_i_dream_maker_client-members.html | 2 +- ...1_components_1_1_i_dream_maker_client.html | 2 +- ...ponents_1_1_i_instance_client-members.html | 2 +- ..._1_1_components_1_1_i_instance_client.html | 2 +- ...ts_1_1_i_instance_user_client-members.html | 2 +- ...components_1_1_i_instance_user_client.html | 2 +- ..._components_1_1_i_jobs_client-members.html | 2 +- ...ient_1_1_components_1_1_i_jobs_client.html | 2 +- ...nents_1_1_i_repository_client-members.html | 2 +- ..._1_components_1_1_i_repository_client.html | 2 +- ...t_1_1_i_administration_client-members.html | 2 +- ..._1_client_1_1_i_administration_client.html | 2 +- ...r_1_1_client_1_1_i_api_client-members.html | 2 +- ..._1_server_1_1_client_1_1_i_api_client.html | 2 +- ...ient_1_1_i_api_client_factory-members.html | 2 +- ...r_1_1_client_1_1_i_api_client_factory.html | 2 +- ..._1_1_client_1_1_i_http_client-members.html | 2 +- ...1_server_1_1_client_1_1_i_http_client.html | 2 +- ...1_1_i_instance_manager_client-members.html | 2 +- ..._client_1_1_i_instance_manager_client.html | 2 +- ...1_client_1_1_i_request_logger-members.html | 2 +- ...erver_1_1_client_1_1_i_request_logger.html | 2 +- ..._1_client_1_1_i_server_client-members.html | 2 +- ...server_1_1_client_1_1_i_server_client.html | 2 +- ...t_1_1_i_server_client_factory-members.html | 2 +- ..._1_client_1_1_i_server_client_factory.html | 2 +- ...1_1_client_1_1_i_users_client-members.html | 2 +- ..._server_1_1_client_1_1_i_users_client.html | 2 +- ...d_1_1_i_byond_executable_lock-members.html | 2 +- ...1_1_byond_1_1_i_byond_executable_lock.html | 6 +- ...1_byond_1_1_i_byond_installer-members.html | 2 +- ...nents_1_1_byond_1_1_i_byond_installer.html | 2 +- ...1_1_byond_1_1_i_byond_manager-members.html | 2 +- ...ponents_1_1_byond_1_1_i_byond_manager.html | 2 +- ...at_1_1_commands_1_1_i_command-members.html | 2 +- ...s_1_1_chat_1_1_commands_1_1_i_command.html | 2 +- ...ommands_1_1_i_command_factory-members.html | 2 +- ...at_1_1_commands_1_1_i_command_factory.html | 2 +- ...s_1_1_chat_1_1_i_channel_sink-members.html | 2 +- ...omponents_1_1_chat_1_1_i_channel_sink.html | 2 +- ...s_1_1_chat_1_1_i_chat_manager-members.html | 6 +- ...omponents_1_1_chat_1_1_i_chat_manager.html | 121 ++- ...at_1_1_i_chat_manager_factory-members.html | 2 +- ...s_1_1_chat_1_1_i_chat_manager_factory.html | 2 +- ...t_1_1_i_chat_tracking_context-members.html | 2 +- ..._1_1_chat_1_1_i_chat_tracking_context.html | 2 +- ..._1_1_i_custom_command_handler-members.html | 2 +- ...1_1_chat_1_1_i_custom_command_handler.html | 2 +- ..._1_1_providers_1_1_i_provider-members.html | 5 +- ...1_1_chat_1_1_providers_1_1_i_provider.html | 96 ++- ...4ff61d2fb16b41675498d0d95b67d8_icgraph.map | 3 +- ...4ff61d2fb16b41675498d0d95b67d8_icgraph.md5 | 2 +- ...4ff61d2fb16b41675498d0d95b67d8_icgraph.png | Bin 9942 -> 6221 bytes ...viders_1_1_i_provider_factory-members.html | 2 +- ..._1_1_providers_1_1_i_provider_factory.html | 2 +- ...oyment_1_1_i_compile_job_sink-members.html | 2 +- ...1_1_deployment_1_1_i_compile_job_sink.html | 2 +- ..._deployment_1_1_i_dmb_factory-members.html | 2 +- ...ents_1_1_deployment_1_1_i_dmb_factory.html | 2 +- ...deployment_1_1_i_dmb_provider-members.html | 2 +- ...nts_1_1_deployment_1_1_i_dmb_provider.html | 4 +- ..._deployment_1_1_i_dream_maker-members.html | 2 +- ...ents_1_1_deployment_1_1_i_dream_maker.html | 2 +- ...i_latest_compile_job_provider-members.html | 2 +- ...ent_1_1_i_latest_compile_job_provider.html | 2 +- ...mponents_1_1_i_event_consumer-members.html | 2 +- ...t_1_1_components_1_1_i_event_consumer.html | 2 +- ...1_1_components_1_1_i_instance-members.html | 2 +- ..._1_host_1_1_components_1_1_i_instance.html | 4 +- ...onents_1_1_i_instance_factory-members.html | 2 +- ...1_1_components_1_1_i_instance_factory.html | 2 +- ...onents_1_1_i_instance_manager-members.html | 2 +- ...1_1_components_1_1_i_instance_manager.html | 4 +- ...ridge_1_1_i_bridge_dispatcher-members.html | 2 +- ...op_1_1_bridge_1_1_i_bridge_dispatcher.html | 4 +- ...1_bridge_1_1_i_bridge_handler-members.html | 2 +- ...terop_1_1_bridge_1_1_i_bridge_handler.html | 4 +- ...bridge_1_1_i_bridge_registrar-members.html | 2 +- ...rop_1_1_bridge_1_1_i_bridge_registrar.html | 2 +- ..._1_1_bridge_1_1_i_bridge_registration.html | 2 +- ...ry_1_1_i_credentials_provider-members.html | 2 +- ...repository_1_1_i_credentials_provider.html | 2 +- ...itory_1_1_i_lib_git2_commands-members.html | 2 +- ..._1_repository_1_1_i_lib_git2_commands.html | 2 +- ...i_lib_git2_repository_factory-members.html | 2 +- ...ory_1_1_i_lib_git2_repository_factory.html | 2 +- ...1_repository_1_1_i_repository-members.html | 2 +- ...nents_1_1_repository_1_1_i_repository.html | 14 +- ...tory_1_1_i_repository_manager-members.html | 2 +- ...1_repository_1_1_i_repository_manager.html | 2 +- ...n_1_1_i_reattach_info_handler-members.html | 2 +- ...1_session_1_1_i_reattach_info_handler.html | 2 +- ...sion_1_1_i_session_controller-members.html | 2 +- ..._1_1_session_1_1_i_session_controller.html | 20 +- ..._i_session_controller_factory-members.html | 2 +- ...sion_1_1_i_session_controller_factory.html | 2 +- ...tic_files_1_1_i_configuration-members.html | 2 +- ..._1_1_static_files_1_1_i_configuration.html | 2 +- ...s_1_1_watchdog_1_1_i_watchdog-members.html | 2 +- ...omponents_1_1_watchdog_1_1_i_watchdog.html | 2 +- ...tchdog_1_1_i_watchdog_factory-members.html | 2 +- ...s_1_1_watchdog_1_1_i_watchdog_factory.html | 2 +- ..._1_1_core_1_1_i_async_delayer-members.html | 2 +- ...1_1_host_1_1_core_1_1_i_async_delayer.html | 2 +- ..._1_1_i_git_hub_client_factory-members.html | 2 +- ...1_1_core_1_1_i_git_hub_client_factory.html | 2 +- ..._1_core_1_1_i_restart_handler-members.html | 2 +- ...1_host_1_1_core_1_1_i_restart_handler.html | 2 +- ...t_1_1_core_1_1_i_restart_registration.html | 2 +- ...1_1_core_1_1_i_server_control-members.html | 2 +- ..._1_host_1_1_core_1_1_i_server_control.html | 10 +- ...re_1_1_i_server_port_provider-members.html | 2 +- ...t_1_1_core_1_1_i_server_port_provider.html | 2 +- ...ase_1_1_i_database_collection-members.html | 10 +- ..._1_database_1_1_i_database_collection.html | 98 +-- ...1_1_i_database_collection__coll__graph.map | 3 +- ...1_1_i_database_collection__coll__graph.md5 | 2 +- ...1_1_i_database_collection__coll__graph.png | Bin 5772 -> 8810 bytes ..._i_database_collection__inherit__graph.map | 5 +- ..._i_database_collection__inherit__graph.md5 | 2 +- ..._i_database_collection__inherit__graph.png | Bin 9782 -> 13307 bytes ...i_database_connection_factory-members.html | 2 +- ...ase_1_1_i_database_connection_factory.html | 2 +- ...tabase_1_1_i_database_context-members.html | 2 +- ...t_1_1_database_1_1_i_database_context.html | 20 +- ...1_1_i_database_context__inherit__graph.map | 6 +- ...1_1_i_database_context__inherit__graph.md5 | 2 +- ...1_1_i_database_context__inherit__graph.png | Bin 5726 -> 29298 bytes ..._1_i_database_context_factory-members.html | 2 +- ...tabase_1_1_i_database_context_factory.html | 4 +- ...atabase_1_1_i_database_seeder-members.html | 2 +- ...st_1_1_database_1_1_i_database_seeder.html | 2 +- ..._1_host_1_1_i_o_1_1_i_console-members.html | 2 +- ...server_1_1_host_1_1_i_o_1_1_i_console.html | 2 +- ...ost_1_1_i_o_1_1_i_i_o_manager-members.html | 2 +- ...er_1_1_host_1_1_i_o_1_1_i_i_o_manager.html | 2 +- ..._i_o_1_1_i_post_write_handler-members.html | 2 +- ...host_1_1_i_o_1_1_i_post_write_handler.html | 2 +- ...1_1_i_o_1_1_i_symlink_factory-members.html | 2 +- ..._1_host_1_1_i_o_1_1_i_symlink_factory.html | 2 +- ...1_1_i_synchronous_i_o_manager-members.html | 2 +- ...1_1_i_o_1_1_i_synchronous_i_o_manager.html | 2 +- ..._server_1_1_host_1_1_i_server-members.html | 2 +- ...tion_1_1_server_1_1_host_1_1_i_server.html | 2 +- ...1_1_host_1_1_i_server_factory-members.html | 2 +- ..._server_1_1_host_1_1_i_server_factory.html | 2 +- ...st_1_1_jobs_1_1_i_job_manager-members.html | 2 +- ...r_1_1_host_1_1_jobs_1_1_i_job_manager.html | 2 +- ..._1_1_i_authentication_context-members.html | 2 +- ...security_1_1_i_authentication_context.html | 2 +- ...uthentication_context_factory-members.html | 2 +- ..._1_1_i_authentication_context_factory.html | 2 +- ...ecurity_1_1_i_claims_injector-members.html | 2 +- ...st_1_1_security_1_1_i_claims_injector.html | 2 +- ...rity_1_1_i_cryptography_suite-members.html | 2 +- ...1_1_security_1_1_i_cryptography_suite.html | 2 +- ...security_1_1_i_identity_cache-members.html | 2 +- ...ost_1_1_security_1_1_i_identity_cache.html | 2 +- ...ecurity_1_1_i_system_identity-members.html | 2 +- ...st_1_1_security_1_1_i_system_identity.html | 2 +- ...1_1_i_system_identity_factory-members.html | 2 +- ...ecurity_1_1_i_system_identity_factory.html | 2 +- ..._security_1_1_i_token_factory-members.html | 2 +- ...host_1_1_security_1_1_i_token_factory.html | 2 +- ...tup_1_1_i_post_setup_services-members.html | 2 +- ...t_1_1_setup_1_1_i_post_setup_services.html | 6 +- ...assembly_information_provider-members.html | 2 +- ...m_1_1_i_assembly_information_provider.html | 4 +- ...m_1_1_i_network_prompt_reaper-members.html | 2 +- ..._1_system_1_1_i_network_prompt_reaper.html | 2 +- ...tem_1_1_i_platform_identifier-members.html | 2 +- ..._1_1_system_1_1_i_platform_identifier.html | 2 +- ...host_1_1_system_1_1_i_process-members.html | 2 +- ...ver_1_1_host_1_1_system_1_1_i_process.html | 2 +- ...1_1_system_1_1_i_process_base-members.html | 2 +- ..._1_host_1_1_system_1_1_i_process_base.html | 6 +- ...system_1_1_i_process_executor-members.html | 2 +- ...ost_1_1_system_1_1_i_process_executor.html | 2 +- ...system_1_1_i_process_features-members.html | 2 +- ...ost_1_1_system_1_1_i_process_features.html | 2 +- ...t_1_1_watchdog_1_1_i_watchdog-members.html | 2 +- ..._1_1_host_1_1_watchdog_1_1_i_watchdog.html | 2 +- ...tchdog_1_1_i_watchdog_factory-members.html | 2 +- ...t_1_1_watchdog_1_1_i_watchdog_factory.html | 2 +- namespace_tgstation.html | 2 +- namespace_tgstation_1_1_server.html | 2 +- namespace_tgstation_1_1_server_1_1_api.html | 2 +- ...station_1_1_server_1_1_api_1_1_models.html | 2 +- ...erver_1_1_api_1_1_models_1_1_internal.html | 2 +- ...station_1_1_server_1_1_api_1_1_rights.html | 2 +- ...space_tgstation_1_1_server_1_1_client.html | 2 +- ..._1_1_server_1_1_client_1_1_components.html | 2 +- namespace_tgstation_1_1_server_1_1_host.html | 4 +- ...on_1_1_server_1_1_host_1_1_components.html | 2 +- ...ver_1_1_host_1_1_components_1_1_byond.html | 2 +- ...rver_1_1_host_1_1_components_1_1_chat.html | 2 +- ..._1_1_components_1_1_chat_1_1_commands.html | 2 +- ...1_1_components_1_1_chat_1_1_providers.html | 2 +- ..._1_host_1_1_components_1_1_deployment.html | 2 +- ...r_1_1_host_1_1_components_1_1_interop.html | 2 +- ...1_1_components_1_1_interop_1_1_bridge.html | 2 +- ...components_1_1_interop_1_1_converters.html | 2 +- ..._1_1_components_1_1_interop_1_1_topic.html | 2 +- ..._1_host_1_1_components_1_1_repository.html | 2 +- ...r_1_1_host_1_1_components_1_1_session.html | 2 +- ..._host_1_1_components_1_1_static_files.html | 2 +- ..._1_1_host_1_1_components_1_1_watchdog.html | 2 +- ...1_1_server_1_1_host_1_1_configuration.html | 18 +- ...ation_1_1_server_1_1_host_1_1_console.html | 2 +- ...n_1_1_server_1_1_host_1_1_controllers.html | 2 +- ...gstation_1_1_server_1_1_host_1_1_core.html | 2 +- ...tion_1_1_server_1_1_host_1_1_database.html | 11 +- ...rver_1_1_host_1_1_database_1_1_design.html | 12 +- ..._1_1_host_1_1_database_1_1_migrations.html | 5 +- ...on_1_1_server_1_1_host_1_1_extensions.html | 2 +- ...tgstation_1_1_server_1_1_host_1_1_i_o.html | 2 +- ...gstation_1_1_server_1_1_host_1_1_jobs.html | 2 +- ...on_1_1_server_1_1_host_1_1_migrations.html | 114 +++ ...tation_1_1_server_1_1_host_1_1_models.html | 2 +- ...tion_1_1_server_1_1_host_1_1_security.html | 2 +- ...ation_1_1_server_1_1_host_1_1_service.html | 2 +- ...station_1_1_server_1_1_host_1_1_setup.html | 2 +- ...tation_1_1_server_1_1_host_1_1_system.html | 2 +- ...tion_1_1_server_1_1_host_1_1_watchdog.html | 2 +- namespacemembers.html | 2 +- namespacemembers_enum.html | 2 +- namespaces.html | 15 +- search/all_0.js | 4 +- search/all_1.js | 13 +- search/all_10.js | 27 +- search/all_12.js | 18 +- search/all_13.js | 14 +- search/all_14.js | 14 +- search/all_15.js | 15 +- search/all_16.js | 2 +- search/all_17.js | 8 +- search/all_2.js | 4 +- search/all_3.js | 29 +- search/all_4.js | 55 +- search/all_6.js | 3 +- search/all_7.js | 5 +- search/all_9.js | 35 +- search/all_a.js | 4 +- search/all_c.js | 4 +- search/all_e.js | 2 +- search/all_f.js | 4 +- search/classes_10.js | 4 +- search/classes_11.js | 2 +- search/classes_12.js | 2 +- search/classes_13.js | 2 +- search/classes_3.js | 7 +- search/classes_8.js | 3 +- search/classes_f.js | 4 + search/enumvalues_e.js | 1 + search/files_0.js | 4 +- search/files_10.js | 7 +- search/functions_1.js | 4 +- search/functions_11.js | 10 +- search/functions_12.js | 1 - search/functions_13.js | 3 +- search/functions_14.js | 2 +- search/functions_2.js | 6 +- search/functions_3.js | 11 +- search/functions_5.js | 1 - search/functions_6.js | 3 +- search/functions_8.js | 2 +- search/functions_e.js | 4 +- search/functions_f.js | 3 +- search/namespaces_0.js | 1 + search/properties_0.js | 1 + search/properties_11.js | 2 +- search/properties_12.js | 2 +- search/properties_14.js | 2 +- search/properties_2.js | 8 +- search/properties_3.js | 6 +- search/properties_7.js | 2 +- search/properties_8.js | 4 +- search/properties_9.js | 2 +- search/properties_a.js | 2 +- search/properties_f.js | 8 +- search/variables_0.js | 2 +- search/variables_11.js | 12 +- search/variables_14.js | 4 +- search/variables_16.js | 6 +- search/variables_2.js | 17 +- search/variables_3.js | 15 +- search/variables_6.js | 2 +- search/variables_7.js | 1 + search/variables_8.js | 8 +- search/variables_9.js | 4 +- search/variables_f.js | 2 +- src_2_d_m_a_p_i_2tgs_2_r_e_a_d_m_e_8md.html | 2 +- ...m_a_p_i_2tgs_2_r_e_a_d_m_e_8md_source.html | 2 +- ..._m_a_p_i_2tgs_2core_2_r_e_a_d_m_e_8md.html | 2 +- ...i_2tgs_2core_2_r_e_a_d_m_e_8md_source.html | 2 +- ...m_a_p_i_2tgs_2v3210_2_r_e_a_d_m_e_8md.html | 2 +- ..._2tgs_2v3210_2_r_e_a_d_m_e_8md_source.html | 2 +- ..._d_m_a_p_i_2tgs_2v4_2_r_e_a_d_m_e_8md.html | 2 +- ...p_i_2tgs_2v4_2_r_e_a_d_m_e_8md_source.html | 2 +- src_2_r_e_a_d_m_e_8md.html | 2 +- src_2_r_e_a_d_m_e_8md_source.html | 2 +- ...tion_8_server_8_api_2_r_e_a_d_m_e_8md.html | 2 +- ...server_8_api_2_r_e_a_d_m_e_8md_source.html | 2 +- ...8_host_2_client_app_2_r_e_a_d_m_e_8md.html | 2 +- ...2_client_app_2_r_e_a_d_m_e_8md_source.html | 2 +- ...8_host_2_components_2_r_e_a_d_m_e_8md.html | 2 +- ...2_components_2_r_e_a_d_m_e_8md_source.html | 2 +- ..._host_2_controllers_2_r_e_a_d_m_e_8md.html | 2 +- ..._controllers_2_r_e_a_d_m_e_8md_source.html | 2 +- ...erver_8_host_2_core_2_r_e_a_d_m_e_8md.html | 2 +- ..._host_2_core_2_r_e_a_d_m_e_8md_source.html | 2 +- ...r_8_host_2_database_2_r_e_a_d_m_e_8md.html | 2 +- ...t_2_database_2_r_e_a_d_m_e_8md_source.html | 2 +- ...server_8_host_2_i_o_2_r_e_a_d_m_e_8md.html | 2 +- ...8_host_2_i_o_2_r_e_a_d_m_e_8md_source.html | 2 +- ...erver_8_host_2_jobs_2_r_e_a_d_m_e_8md.html | 2 +- ..._host_2_jobs_2_r_e_a_d_m_e_8md_source.html | 2 +- ...ion_8_server_8_host_2_r_e_a_d_m_e_8md.html | 2 +- ...erver_8_host_2_r_e_a_d_m_e_8md_source.html | 2 +- ...r_8_host_2_security_2_r_e_a_d_m_e_8md.html | 2 +- ...t_2_security_2_r_e_a_d_m_e_8md_source.html | 2 +- ...rver_8_host_2_setup_2_r_e_a_d_m_e_8md.html | 2 +- ...host_2_setup_2_r_e_a_d_m_e_8md_source.html | 2 +- ...ver_8_host_2_system_2_r_e_a_d_m_e_8md.html | 2 +- ...ost_2_system_2_r_e_a_d_m_e_8md_source.html | 2 +- 2299 files changed, 10011 insertions(+), 6125 deletions(-) create mode 100644 20200516111712___p_g_create_8_designer_8cs.html create mode 100644 20200516111712___p_g_create_8_designer_8cs_source.html create mode 100644 20200516111712___p_g_create_8cs.html create mode 100644 20200516111712___p_g_create_8cs_source.html create mode 100644 _postgres_sql_database_context_8cs.html create mode 100644 _postgres_sql_database_context_8cs_source.html create mode 100644 _postgres_sql_database_context_model_snapshot_8cs.html create mode 100644 _postgres_sql_database_context_model_snapshot_8cs_source.html create mode 100644 _postgres_sql_design_time_db_context_factory_8cs.html create mode 100644 _postgres_sql_design_time_db_context_factory_8cs_source.html create mode 100644 class_i_async_enumerable.html create mode 100644 class_i_async_enumerable__inherit__graph.map create mode 100644 class_i_async_enumerable__inherit__graph.md5 create mode 100644 class_i_async_enumerable__inherit__graph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_windows_watchdog_a639ec5c72078453b2d1787a496013c84_cgraph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_windows_watchdog_a639ec5c72078453b2d1787a496013c84_cgraph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_windows_watchdog_a639ec5c72078453b2d1787a496013c84_cgraph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_database_context_ac2e88d53bf1b6ac87d175d83ed39da7c_icgraph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_database_context_ac2e88d53bf1b6ac87d175d83ed39da7c_icgraph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_database_context_ac2e88d53bf1b6ac87d175d83ed39da7c_icgraph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_design_time_db_context_factory_helpers_a184ab9cd41e86e8ce83b3cdd8371b251_icgraph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_design_time_db_context_factory_helpers_a184ab9cd41e86e8ce83b3cdd8371b251_icgraph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_design_time_db_context_factory_helpers_a184ab9cd41e86e8ce83b3cdd8371b251_icgraph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory-members.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory__coll__graph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory__coll__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory__coll__graph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory__inherit__graph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory__inherit__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory__inherit__graph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory_a36ff3f3ef52311c0fa45971cc3156bb1_cgraph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory_a36ff3f3ef52311c0fa45971cc3156bb1_cgraph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_design_1_1_postgres_sql_design_time_db_context_factory_a36ff3f3ef52311c0fa45971cc3156bb1_cgraph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_migrations_1_1_p_g_create-members.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_migrations_1_1_p_g_create.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_migrations_1_1_p_g_create__coll__graph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_migrations_1_1_p_g_create__coll__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_migrations_1_1_p_g_create__coll__graph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_migrations_1_1_p_g_create__inherit__graph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_migrations_1_1_p_g_create__inherit__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_migrations_1_1_p_g_create__inherit__graph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_postgres_sql_database_context-members.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_postgres_sql_database_context.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_postgres_sql_database_context__coll__graph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_postgres_sql_database_context__coll__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_postgres_sql_database_context__coll__graph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_postgres_sql_database_context__inherit__graph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_postgres_sql_database_context__inherit__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_database_1_1_postgres_sql_database_context__inherit__graph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_migrations_1_1_postgres_sql_database_context_model_snapshot-members.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_migrations_1_1_postgres_sql_database_context_model_snapshot.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_migrations_1_1_postgres_sql_database_context_model_snapshot__coll__graph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_migrations_1_1_postgres_sql_database_context_model_snapshot__coll__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_migrations_1_1_postgres_sql_database_context_model_snapshot__coll__graph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_migrations_1_1_postgres_sql_database_context_model_snapshot__inherit__graph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_migrations_1_1_postgres_sql_database_context_model_snapshot__inherit__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_migrations_1_1_postgres_sql_database_context_model_snapshot__inherit__graph.png create mode 100644 namespace_tgstation_1_1_server_1_1_host_1_1_migrations.html diff --git a/20180906135553___m_s_initial_create_8_designer_8cs.html b/20180906135553___m_s_initial_create_8_designer_8cs.html index 325669a877..f07670f5fb 100644 --- a/20180906135553___m_s_initial_create_8_designer_8cs.html +++ b/20180906135553___m_s_initial_create_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180906135553___m_s_initial_create_8_designer_8cs_source.html b/20180906135553___m_s_initial_create_8_designer_8cs_source.html index ee0d064e59..35ae68d28b 100644 --- a/20180906135553___m_s_initial_create_8_designer_8cs_source.html +++ b/20180906135553___m_s_initial_create_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180906135553___m_s_initial_create_8cs.html b/20180906135553___m_s_initial_create_8cs.html index 77204cd32f..884ac4a190 100644 --- a/20180906135553___m_s_initial_create_8cs.html +++ b/20180906135553___m_s_initial_create_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180906135553___m_s_initial_create_8cs_source.html b/20180906135553___m_s_initial_create_8cs_source.html index 8b7da0d9fd..127b8443d9 100644 --- a/20180906135553___m_s_initial_create_8cs_source.html +++ b/20180906135553___m_s_initial_create_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180906143029___m_y_initial_create_8_designer_8cs.html b/20180906143029___m_y_initial_create_8_designer_8cs.html index 09e90e626b..d5b7df7d56 100644 --- a/20180906143029___m_y_initial_create_8_designer_8cs.html +++ b/20180906143029___m_y_initial_create_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180906143029___m_y_initial_create_8_designer_8cs_source.html b/20180906143029___m_y_initial_create_8_designer_8cs_source.html index 16a79f54bb..a967e20c4e 100644 --- a/20180906143029___m_y_initial_create_8_designer_8cs_source.html +++ b/20180906143029___m_y_initial_create_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180906143029___m_y_initial_create_8cs.html b/20180906143029___m_y_initial_create_8cs.html index ffcd4a8964..65cafcc7e9 100644 --- a/20180906143029___m_y_initial_create_8cs.html +++ b/20180906143029___m_y_initial_create_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180906143029___m_y_initial_create_8cs_source.html b/20180906143029___m_y_initial_create_8cs_source.html index 75c49e918f..50b7a6ff33 100644 --- a/20180906143029___m_y_initial_create_8cs_source.html +++ b/20180906143029___m_y_initial_create_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918020726___m_y_add_minimum_security_8_designer_8cs.html b/20180918020726___m_y_add_minimum_security_8_designer_8cs.html index 184c712d4f..c293fbfb3e 100644 --- a/20180918020726___m_y_add_minimum_security_8_designer_8cs.html +++ b/20180918020726___m_y_add_minimum_security_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918020726___m_y_add_minimum_security_8_designer_8cs_source.html b/20180918020726___m_y_add_minimum_security_8_designer_8cs_source.html index 046912b464..3968417ea5 100644 --- a/20180918020726___m_y_add_minimum_security_8_designer_8cs_source.html +++ b/20180918020726___m_y_add_minimum_security_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918020726___m_y_add_minimum_security_8cs.html b/20180918020726___m_y_add_minimum_security_8cs.html index 479edfa83a..c156cdb85c 100644 --- a/20180918020726___m_y_add_minimum_security_8cs.html +++ b/20180918020726___m_y_add_minimum_security_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918020726___m_y_add_minimum_security_8cs_source.html b/20180918020726___m_y_add_minimum_security_8cs_source.html index ac24a3e130..00d2a4947b 100644 --- a/20180918020726___m_y_add_minimum_security_8cs_source.html +++ b/20180918020726___m_y_add_minimum_security_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918021228___m_s_add_minimum_security_8_designer_8cs.html b/20180918021228___m_s_add_minimum_security_8_designer_8cs.html index 365a70fc92..788a7f1eaf 100644 --- a/20180918021228___m_s_add_minimum_security_8_designer_8cs.html +++ b/20180918021228___m_s_add_minimum_security_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918021228___m_s_add_minimum_security_8_designer_8cs_source.html b/20180918021228___m_s_add_minimum_security_8_designer_8cs_source.html index 64deaac121..6325d7089c 100644 --- a/20180918021228___m_s_add_minimum_security_8_designer_8cs_source.html +++ b/20180918021228___m_s_add_minimum_security_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918021228___m_s_add_minimum_security_8cs.html b/20180918021228___m_s_add_minimum_security_8cs.html index ce476ef9d4..1ee9393632 100644 --- a/20180918021228___m_s_add_minimum_security_8cs.html +++ b/20180918021228___m_s_add_minimum_security_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918021228___m_s_add_minimum_security_8cs_source.html b/20180918021228___m_s_add_minimum_security_8cs_source.html index 1aad8f276e..dbbcd1cc9a 100644 --- a/20180918021228___m_s_add_minimum_security_8cs_source.html +++ b/20180918021228___m_s_add_minimum_security_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918204520___m_y_nullable_and_foreign_key_cleanup_8_designer_8cs.html b/20180918204520___m_y_nullable_and_foreign_key_cleanup_8_designer_8cs.html index c80629f8c0..dd8d40694c 100644 --- a/20180918204520___m_y_nullable_and_foreign_key_cleanup_8_designer_8cs.html +++ b/20180918204520___m_y_nullable_and_foreign_key_cleanup_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918204520___m_y_nullable_and_foreign_key_cleanup_8_designer_8cs_source.html b/20180918204520___m_y_nullable_and_foreign_key_cleanup_8_designer_8cs_source.html index 9fbd0f5fa7..9fc58f62b4 100644 --- a/20180918204520___m_y_nullable_and_foreign_key_cleanup_8_designer_8cs_source.html +++ b/20180918204520___m_y_nullable_and_foreign_key_cleanup_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918204520___m_y_nullable_and_foreign_key_cleanup_8cs.html b/20180918204520___m_y_nullable_and_foreign_key_cleanup_8cs.html index fc539968db..6d0ff5f0b0 100644 --- a/20180918204520___m_y_nullable_and_foreign_key_cleanup_8cs.html +++ b/20180918204520___m_y_nullable_and_foreign_key_cleanup_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918204520___m_y_nullable_and_foreign_key_cleanup_8cs_source.html b/20180918204520___m_y_nullable_and_foreign_key_cleanup_8cs_source.html index 0ac5dd6b2d..4032d5b079 100644 --- a/20180918204520___m_y_nullable_and_foreign_key_cleanup_8cs_source.html +++ b/20180918204520___m_y_nullable_and_foreign_key_cleanup_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918205224___m_s_nullable_and_foreign_key_cleanup_8_designer_8cs.html b/20180918205224___m_s_nullable_and_foreign_key_cleanup_8_designer_8cs.html index 0ba4954d30..60b154dde8 100644 --- a/20180918205224___m_s_nullable_and_foreign_key_cleanup_8_designer_8cs.html +++ b/20180918205224___m_s_nullable_and_foreign_key_cleanup_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918205224___m_s_nullable_and_foreign_key_cleanup_8_designer_8cs_source.html b/20180918205224___m_s_nullable_and_foreign_key_cleanup_8_designer_8cs_source.html index d64e976c19..ff92939025 100644 --- a/20180918205224___m_s_nullable_and_foreign_key_cleanup_8_designer_8cs_source.html +++ b/20180918205224___m_s_nullable_and_foreign_key_cleanup_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918205224___m_s_nullable_and_foreign_key_cleanup_8cs.html b/20180918205224___m_s_nullable_and_foreign_key_cleanup_8cs.html index b34b24f915..5320512e19 100644 --- a/20180918205224___m_s_nullable_and_foreign_key_cleanup_8cs.html +++ b/20180918205224___m_s_nullable_and_foreign_key_cleanup_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180918205224___m_s_nullable_and_foreign_key_cleanup_8cs_source.html b/20180918205224___m_s_nullable_and_foreign_key_cleanup_8cs_source.html index 69b2e5c082..6752dd1f76 100644 --- a/20180918205224___m_s_nullable_and_foreign_key_cleanup_8cs_source.html +++ b/20180918205224___m_s_nullable_and_foreign_key_cleanup_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180926033145___m_s_reattach_compile_job_required_8_designer_8cs.html b/20180926033145___m_s_reattach_compile_job_required_8_designer_8cs.html index 7515b7fc6b..8938864382 100644 --- a/20180926033145___m_s_reattach_compile_job_required_8_designer_8cs.html +++ b/20180926033145___m_s_reattach_compile_job_required_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180926033145___m_s_reattach_compile_job_required_8_designer_8cs_source.html b/20180926033145___m_s_reattach_compile_job_required_8_designer_8cs_source.html index 4ecaecec72..4b9b7f783e 100644 --- a/20180926033145___m_s_reattach_compile_job_required_8_designer_8cs_source.html +++ b/20180926033145___m_s_reattach_compile_job_required_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180926033145___m_s_reattach_compile_job_required_8cs.html b/20180926033145___m_s_reattach_compile_job_required_8cs.html index 2405df8ecb..1d3f04350d 100644 --- a/20180926033145___m_s_reattach_compile_job_required_8cs.html +++ b/20180926033145___m_s_reattach_compile_job_required_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180926033145___m_s_reattach_compile_job_required_8cs_source.html b/20180926033145___m_s_reattach_compile_job_required_8cs_source.html index ce8ef3aa6a..68924b24a1 100644 --- a/20180926033145___m_s_reattach_compile_job_required_8cs_source.html +++ b/20180926033145___m_s_reattach_compile_job_required_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180926034014___m_y_reattach_compile_job_required_8_designer_8cs.html b/20180926034014___m_y_reattach_compile_job_required_8_designer_8cs.html index 916d0bec75..f84a5fc973 100644 --- a/20180926034014___m_y_reattach_compile_job_required_8_designer_8cs.html +++ b/20180926034014___m_y_reattach_compile_job_required_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180926034014___m_y_reattach_compile_job_required_8_designer_8cs_source.html b/20180926034014___m_y_reattach_compile_job_required_8_designer_8cs_source.html index 65035dd5cd..993116e29a 100644 --- a/20180926034014___m_y_reattach_compile_job_required_8_designer_8cs_source.html +++ b/20180926034014___m_y_reattach_compile_job_required_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180926034014___m_y_reattach_compile_job_required_8cs.html b/20180926034014___m_y_reattach_compile_job_required_8cs.html index c7d240aae5..5a71f52d11 100644 --- a/20180926034014___m_y_reattach_compile_job_required_8cs.html +++ b/20180926034014___m_y_reattach_compile_job_required_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20180926034014___m_y_reattach_compile_job_required_8cs_source.html b/20180926034014___m_y_reattach_compile_job_required_8cs_source.html index 5d8f732acf..9934399196 100644 --- a/20180926034014___m_y_reattach_compile_job_required_8cs_source.html +++ b/20180926034014___m_y_reattach_compile_job_required_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20181124231534___m_s_toggle_testmerge_comments_8_designer_8cs.html b/20181124231534___m_s_toggle_testmerge_comments_8_designer_8cs.html index 74027841c8..8be1ab94ec 100644 --- a/20181124231534___m_s_toggle_testmerge_comments_8_designer_8cs.html +++ b/20181124231534___m_s_toggle_testmerge_comments_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20181124231534___m_s_toggle_testmerge_comments_8_designer_8cs_source.html b/20181124231534___m_s_toggle_testmerge_comments_8_designer_8cs_source.html index d510867a6d..c8a9b4107b 100644 --- a/20181124231534___m_s_toggle_testmerge_comments_8_designer_8cs_source.html +++ b/20181124231534___m_s_toggle_testmerge_comments_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20181124231534___m_s_toggle_testmerge_comments_8cs.html b/20181124231534___m_s_toggle_testmerge_comments_8cs.html index c7b9287b8c..ccf825e96e 100644 --- a/20181124231534___m_s_toggle_testmerge_comments_8cs.html +++ b/20181124231534___m_s_toggle_testmerge_comments_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20181124231534___m_s_toggle_testmerge_comments_8cs_source.html b/20181124231534___m_s_toggle_testmerge_comments_8cs_source.html index d72faeebfa..3174dab896 100644 --- a/20181124231534___m_s_toggle_testmerge_comments_8cs_source.html +++ b/20181124231534___m_s_toggle_testmerge_comments_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20181124231549___m_y_toggle_testmerge_comments_8_designer_8cs.html b/20181124231549___m_y_toggle_testmerge_comments_8_designer_8cs.html index 96e21e78e1..e3e6bbb15c 100644 --- a/20181124231549___m_y_toggle_testmerge_comments_8_designer_8cs.html +++ b/20181124231549___m_y_toggle_testmerge_comments_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20181124231549___m_y_toggle_testmerge_comments_8_designer_8cs_source.html b/20181124231549___m_y_toggle_testmerge_comments_8_designer_8cs_source.html index 6dc1527a7e..c98d011223 100644 --- a/20181124231549___m_y_toggle_testmerge_comments_8_designer_8cs_source.html +++ b/20181124231549___m_y_toggle_testmerge_comments_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20181124231549___m_y_toggle_testmerge_comments_8cs.html b/20181124231549___m_y_toggle_testmerge_comments_8cs.html index 17166c9274..ca730783c5 100644 --- a/20181124231549___m_y_toggle_testmerge_comments_8cs.html +++ b/20181124231549___m_y_toggle_testmerge_comments_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20181124231549___m_y_toggle_testmerge_comments_8cs_source.html b/20181124231549___m_y_toggle_testmerge_comments_8cs_source.html index 4f1eece372..c9c89e9074 100644 --- a/20181124231549___m_y_toggle_testmerge_comments_8cs_source.html +++ b/20181124231549___m_y_toggle_testmerge_comments_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20190219041825___m_s_fix_rev_info_index_8_designer_8cs.html b/20190219041825___m_s_fix_rev_info_index_8_designer_8cs.html index bde5e11136..e8f60081a2 100644 --- a/20190219041825___m_s_fix_rev_info_index_8_designer_8cs.html +++ b/20190219041825___m_s_fix_rev_info_index_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20190219041825___m_s_fix_rev_info_index_8_designer_8cs_source.html b/20190219041825___m_s_fix_rev_info_index_8_designer_8cs_source.html index ad86618fd9..d82bcf4825 100644 --- a/20190219041825___m_s_fix_rev_info_index_8_designer_8cs_source.html +++ b/20190219041825___m_s_fix_rev_info_index_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20190219041825___m_s_fix_rev_info_index_8cs.html b/20190219041825___m_s_fix_rev_info_index_8cs.html index 8fb3d13d15..122ecaec88 100644 --- a/20190219041825___m_s_fix_rev_info_index_8cs.html +++ b/20190219041825___m_s_fix_rev_info_index_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20190219041825___m_s_fix_rev_info_index_8cs_source.html b/20190219041825___m_s_fix_rev_info_index_8cs_source.html index cee0bf27ed..b8462b67f5 100644 --- a/20190219041825___m_s_fix_rev_info_index_8cs_source.html +++ b/20190219041825___m_s_fix_rev_info_index_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20190219042440___m_y_fix_rev_info_index_8_designer_8cs.html b/20190219042440___m_y_fix_rev_info_index_8_designer_8cs.html index 3f6a4ea7ee..4a3eee8c4d 100644 --- a/20190219042440___m_y_fix_rev_info_index_8_designer_8cs.html +++ b/20190219042440___m_y_fix_rev_info_index_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20190219042440___m_y_fix_rev_info_index_8_designer_8cs_source.html b/20190219042440___m_y_fix_rev_info_index_8_designer_8cs_source.html index ac7c61954c..f7aef6de3f 100644 --- a/20190219042440___m_y_fix_rev_info_index_8_designer_8cs_source.html +++ b/20190219042440___m_y_fix_rev_info_index_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20190219042440___m_y_fix_rev_info_index_8cs.html b/20190219042440___m_y_fix_rev_info_index_8cs.html index efb5a4f622..7f728e4ddd 100644 --- a/20190219042440___m_y_fix_rev_info_index_8cs.html +++ b/20190219042440___m_y_fix_rev_info_index_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20190219042440___m_y_fix_rev_info_index_8cs_source.html b/20190219042440___m_y_fix_rev_info_index_8cs_source.html index f2ad0fcff7..c8fd2f5db6 100644 --- a/20190219042440___m_y_fix_rev_info_index_8cs_source.html +++ b/20190219042440___m_y_fix_rev_info_index_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200121170252___m_s_add_chat_bot_reconnection_interval_8_designer_8cs.html b/20200121170252___m_s_add_chat_bot_reconnection_interval_8_designer_8cs.html index f10c345eb7..d4a7813ff5 100644 --- a/20200121170252___m_s_add_chat_bot_reconnection_interval_8_designer_8cs.html +++ b/20200121170252___m_s_add_chat_bot_reconnection_interval_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200121170252___m_s_add_chat_bot_reconnection_interval_8_designer_8cs_source.html b/20200121170252___m_s_add_chat_bot_reconnection_interval_8_designer_8cs_source.html index 808bd63b22..6fc451b542 100644 --- a/20200121170252___m_s_add_chat_bot_reconnection_interval_8_designer_8cs_source.html +++ b/20200121170252___m_s_add_chat_bot_reconnection_interval_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200121170252___m_s_add_chat_bot_reconnection_interval_8cs.html b/20200121170252___m_s_add_chat_bot_reconnection_interval_8cs.html index 798508ba6d..35bb4e1112 100644 --- a/20200121170252___m_s_add_chat_bot_reconnection_interval_8cs.html +++ b/20200121170252___m_s_add_chat_bot_reconnection_interval_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200121170252___m_s_add_chat_bot_reconnection_interval_8cs_source.html b/20200121170252___m_s_add_chat_bot_reconnection_interval_8cs_source.html index 919a8c639f..412de89864 100644 --- a/20200121170252___m_s_add_chat_bot_reconnection_interval_8cs_source.html +++ b/20200121170252___m_s_add_chat_bot_reconnection_interval_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200121171159___m_y_add_chat_bot_reconnection_interval_8_designer_8cs.html b/20200121171159___m_y_add_chat_bot_reconnection_interval_8_designer_8cs.html index 9244093100..611b584e58 100644 --- a/20200121171159___m_y_add_chat_bot_reconnection_interval_8_designer_8cs.html +++ b/20200121171159___m_y_add_chat_bot_reconnection_interval_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200121171159___m_y_add_chat_bot_reconnection_interval_8_designer_8cs_source.html b/20200121171159___m_y_add_chat_bot_reconnection_interval_8_designer_8cs_source.html index b14990ba15..af26715989 100644 --- a/20200121171159___m_y_add_chat_bot_reconnection_interval_8_designer_8cs_source.html +++ b/20200121171159___m_y_add_chat_bot_reconnection_interval_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200121171159___m_y_add_chat_bot_reconnection_interval_8cs.html b/20200121171159___m_y_add_chat_bot_reconnection_interval_8cs.html index dce7488dff..87a1b5ae0a 100644 --- a/20200121171159___m_y_add_chat_bot_reconnection_interval_8cs.html +++ b/20200121171159___m_y_add_chat_bot_reconnection_interval_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200121171159___m_y_add_chat_bot_reconnection_interval_8cs_source.html b/20200121171159___m_y_add_chat_bot_reconnection_interval_8cs_source.html index 969d597f29..b27541b074 100644 --- a/20200121171159___m_y_add_chat_bot_reconnection_interval_8cs_source.html +++ b/20200121171159___m_y_add_chat_bot_reconnection_interval_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200411164808___m_s_tons_of_validation_8_designer_8cs.html b/20200411164808___m_s_tons_of_validation_8_designer_8cs.html index 11808a13e2..211eaa1dee 100644 --- a/20200411164808___m_s_tons_of_validation_8_designer_8cs.html +++ b/20200411164808___m_s_tons_of_validation_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200411164808___m_s_tons_of_validation_8_designer_8cs_source.html b/20200411164808___m_s_tons_of_validation_8_designer_8cs_source.html index 56e6b0eecd..449c7ad807 100644 --- a/20200411164808___m_s_tons_of_validation_8_designer_8cs_source.html +++ b/20200411164808___m_s_tons_of_validation_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200411164808___m_s_tons_of_validation_8cs.html b/20200411164808___m_s_tons_of_validation_8cs.html index 50bbc93bc2..0281a27798 100644 --- a/20200411164808___m_s_tons_of_validation_8cs.html +++ b/20200411164808___m_s_tons_of_validation_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200411164808___m_s_tons_of_validation_8cs_source.html b/20200411164808___m_s_tons_of_validation_8cs_source.html index 08b420b910..5701ca610e 100644 --- a/20200411164808___m_s_tons_of_validation_8cs_source.html +++ b/20200411164808___m_s_tons_of_validation_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200411170010___m_y_tons_of_validation_8_designer_8cs.html b/20200411170010___m_y_tons_of_validation_8_designer_8cs.html index 014814d991..9479ad5cea 100644 --- a/20200411170010___m_y_tons_of_validation_8_designer_8cs.html +++ b/20200411170010___m_y_tons_of_validation_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200411170010___m_y_tons_of_validation_8_designer_8cs_source.html b/20200411170010___m_y_tons_of_validation_8_designer_8cs_source.html index 3abe35e934..99c617d3ac 100644 --- a/20200411170010___m_y_tons_of_validation_8_designer_8cs_source.html +++ b/20200411170010___m_y_tons_of_validation_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200411170010___m_y_tons_of_validation_8cs.html b/20200411170010___m_y_tons_of_validation_8cs.html index 8e93fbae83..363a33b20a 100644 --- a/20200411170010___m_y_tons_of_validation_8cs.html +++ b/20200411170010___m_y_tons_of_validation_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200411170010___m_y_tons_of_validation_8cs_source.html b/20200411170010___m_y_tons_of_validation_8cs_source.html index 03c9482168..f4659d4f78 100644 --- a/20200411170010___m_y_tons_of_validation_8cs_source.html +++ b/20200411170010___m_y_tons_of_validation_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200420175359___m_s_limits_on_chat_8_designer_8cs.html b/20200420175359___m_s_limits_on_chat_8_designer_8cs.html index babc79a6ae..3a209ebeca 100644 --- a/20200420175359___m_s_limits_on_chat_8_designer_8cs.html +++ b/20200420175359___m_s_limits_on_chat_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200420175359___m_s_limits_on_chat_8_designer_8cs_source.html b/20200420175359___m_s_limits_on_chat_8_designer_8cs_source.html index 029f5812dc..5486a28d46 100644 --- a/20200420175359___m_s_limits_on_chat_8_designer_8cs_source.html +++ b/20200420175359___m_s_limits_on_chat_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200420175359___m_s_limits_on_chat_8cs.html b/20200420175359___m_s_limits_on_chat_8cs.html index 3af385ad7e..9ef8df6838 100644 --- a/20200420175359___m_s_limits_on_chat_8cs.html +++ b/20200420175359___m_s_limits_on_chat_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200420175359___m_s_limits_on_chat_8cs_source.html b/20200420175359___m_s_limits_on_chat_8cs_source.html index 3654a05cd8..526c299de3 100644 --- a/20200420175359___m_s_limits_on_chat_8cs_source.html +++ b/20200420175359___m_s_limits_on_chat_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200420181015___m_y_limits_on_chat_8_designer_8cs.html b/20200420181015___m_y_limits_on_chat_8_designer_8cs.html index 441a85476d..b5c0bbbf1b 100644 --- a/20200420181015___m_y_limits_on_chat_8_designer_8cs.html +++ b/20200420181015___m_y_limits_on_chat_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200420181015___m_y_limits_on_chat_8_designer_8cs_source.html b/20200420181015___m_y_limits_on_chat_8_designer_8cs_source.html index 5adab39f7d..14138ea629 100644 --- a/20200420181015___m_y_limits_on_chat_8_designer_8cs_source.html +++ b/20200420181015___m_y_limits_on_chat_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200420181015___m_y_limits_on_chat_8cs.html b/20200420181015___m_y_limits_on_chat_8cs.html index 04e21d9c98..975c9df094 100644 --- a/20200420181015___m_y_limits_on_chat_8cs.html +++ b/20200420181015___m_y_limits_on_chat_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200420181015___m_y_limits_on_chat_8cs_source.html b/20200420181015___m_y_limits_on_chat_8cs_source.html index 31f5b1a7c7..4ac70dea65 100644 --- a/20200420181015___m_y_limits_on_chat_8cs_source.html +++ b/20200420181015___m_y_limits_on_chat_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200422001202___m_s_add_compile_job_d_m_api_version_8_designer_8cs.html b/20200422001202___m_s_add_compile_job_d_m_api_version_8_designer_8cs.html index 4b591d7063..c2e4737104 100644 --- a/20200422001202___m_s_add_compile_job_d_m_api_version_8_designer_8cs.html +++ b/20200422001202___m_s_add_compile_job_d_m_api_version_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200422001202___m_s_add_compile_job_d_m_api_version_8_designer_8cs_source.html b/20200422001202___m_s_add_compile_job_d_m_api_version_8_designer_8cs_source.html index f7852f76bd..a258cce9df 100644 --- a/20200422001202___m_s_add_compile_job_d_m_api_version_8_designer_8cs_source.html +++ b/20200422001202___m_s_add_compile_job_d_m_api_version_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200422001202___m_s_add_compile_job_d_m_api_version_8cs.html b/20200422001202___m_s_add_compile_job_d_m_api_version_8cs.html index c3904f7d18..8a65e16dff 100644 --- a/20200422001202___m_s_add_compile_job_d_m_api_version_8cs.html +++ b/20200422001202___m_s_add_compile_job_d_m_api_version_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200422001202___m_s_add_compile_job_d_m_api_version_8cs_source.html b/20200422001202___m_s_add_compile_job_d_m_api_version_8cs_source.html index 49c8c164d0..ab0b30b73e 100644 --- a/20200422001202___m_s_add_compile_job_d_m_api_version_8cs_source.html +++ b/20200422001202___m_s_add_compile_job_d_m_api_version_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200422010115___m_y_add_compile_job_d_m_api_version_8_designer_8cs.html b/20200422010115___m_y_add_compile_job_d_m_api_version_8_designer_8cs.html index 92601d7e7b..e7faf43764 100644 --- a/20200422010115___m_y_add_compile_job_d_m_api_version_8_designer_8cs.html +++ b/20200422010115___m_y_add_compile_job_d_m_api_version_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200422010115___m_y_add_compile_job_d_m_api_version_8_designer_8cs_source.html b/20200422010115___m_y_add_compile_job_d_m_api_version_8_designer_8cs_source.html index 8bfc548165..ab3fa87422 100644 --- a/20200422010115___m_y_add_compile_job_d_m_api_version_8_designer_8cs_source.html +++ b/20200422010115___m_y_add_compile_job_d_m_api_version_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200422010115___m_y_add_compile_job_d_m_api_version_8cs.html b/20200422010115___m_y_add_compile_job_d_m_api_version_8cs.html index 8fa6aa2c97..3cd3df340d 100644 --- a/20200422010115___m_y_add_compile_job_d_m_api_version_8cs.html +++ b/20200422010115___m_y_add_compile_job_d_m_api_version_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200422010115___m_y_add_compile_job_d_m_api_version_8cs_source.html b/20200422010115___m_y_add_compile_job_d_m_api_version_8cs_source.html index 74c6abc513..b6ee01025d 100644 --- a/20200422010115___m_y_add_compile_job_d_m_api_version_8cs_source.html +++ b/20200422010115___m_y_add_compile_job_d_m_api_version_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423003841___m_y_checkpoint_rebuild_8_designer_8cs.html b/20200423003841___m_y_checkpoint_rebuild_8_designer_8cs.html index 95f0562a33..a47c8b53be 100644 --- a/20200423003841___m_y_checkpoint_rebuild_8_designer_8cs.html +++ b/20200423003841___m_y_checkpoint_rebuild_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423003841___m_y_checkpoint_rebuild_8_designer_8cs_source.html b/20200423003841___m_y_checkpoint_rebuild_8_designer_8cs_source.html index f02a110771..5b0d223018 100644 --- a/20200423003841___m_y_checkpoint_rebuild_8_designer_8cs_source.html +++ b/20200423003841___m_y_checkpoint_rebuild_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423003841___m_y_checkpoint_rebuild_8cs.html b/20200423003841___m_y_checkpoint_rebuild_8cs.html index da4d22723b..5c4354916b 100644 --- a/20200423003841___m_y_checkpoint_rebuild_8cs.html +++ b/20200423003841___m_y_checkpoint_rebuild_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423003841___m_y_checkpoint_rebuild_8cs_source.html b/20200423003841___m_y_checkpoint_rebuild_8cs_source.html index bd5892a4e0..49d667bfae 100644 --- a/20200423003841___m_y_checkpoint_rebuild_8cs_source.html +++ b/20200423003841___m_y_checkpoint_rebuild_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423004015___m_s_checkpoint_rebuild_8_designer_8cs.html b/20200423004015___m_s_checkpoint_rebuild_8_designer_8cs.html index 9353dc8767..243ad47b60 100644 --- a/20200423004015___m_s_checkpoint_rebuild_8_designer_8cs.html +++ b/20200423004015___m_s_checkpoint_rebuild_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423004015___m_s_checkpoint_rebuild_8_designer_8cs_source.html b/20200423004015___m_s_checkpoint_rebuild_8_designer_8cs_source.html index efb98f222b..05de792ab7 100644 --- a/20200423004015___m_s_checkpoint_rebuild_8_designer_8cs_source.html +++ b/20200423004015___m_s_checkpoint_rebuild_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423004015___m_s_checkpoint_rebuild_8cs.html b/20200423004015___m_s_checkpoint_rebuild_8cs.html index 0b20332c57..91599d95af 100644 --- a/20200423004015___m_s_checkpoint_rebuild_8cs.html +++ b/20200423004015___m_s_checkpoint_rebuild_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423004015___m_s_checkpoint_rebuild_8cs_source.html b/20200423004015___m_s_checkpoint_rebuild_8cs_source.html index 7480afc00e..34920eb635 100644 --- a/20200423004015___m_s_checkpoint_rebuild_8cs_source.html +++ b/20200423004015___m_s_checkpoint_rebuild_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423052142___m_s_kill_jsons_add_d_m_api_version_8_designer_8cs.html b/20200423052142___m_s_kill_jsons_add_d_m_api_version_8_designer_8cs.html index 9a66bc8d47..96b6dd0625 100644 --- a/20200423052142___m_s_kill_jsons_add_d_m_api_version_8_designer_8cs.html +++ b/20200423052142___m_s_kill_jsons_add_d_m_api_version_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423052142___m_s_kill_jsons_add_d_m_api_version_8_designer_8cs_source.html b/20200423052142___m_s_kill_jsons_add_d_m_api_version_8_designer_8cs_source.html index bef54e7c93..d39d0e2958 100644 --- a/20200423052142___m_s_kill_jsons_add_d_m_api_version_8_designer_8cs_source.html +++ b/20200423052142___m_s_kill_jsons_add_d_m_api_version_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423052142___m_s_kill_jsons_add_d_m_api_version_8cs.html b/20200423052142___m_s_kill_jsons_add_d_m_api_version_8cs.html index ecdabd1498..a4b78e8041 100644 --- a/20200423052142___m_s_kill_jsons_add_d_m_api_version_8cs.html +++ b/20200423052142___m_s_kill_jsons_add_d_m_api_version_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423052142___m_s_kill_jsons_add_d_m_api_version_8cs_source.html b/20200423052142___m_s_kill_jsons_add_d_m_api_version_8cs_source.html index 5d0ace81a6..67c44c11a5 100644 --- a/20200423052142___m_s_kill_jsons_add_d_m_api_version_8cs_source.html +++ b/20200423052142___m_s_kill_jsons_add_d_m_api_version_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423052709___m_y_kill_jsons_add_d_m_api_version_8_designer_8cs.html b/20200423052709___m_y_kill_jsons_add_d_m_api_version_8_designer_8cs.html index f5fbff2ee8..7ad77d04e7 100644 --- a/20200423052709___m_y_kill_jsons_add_d_m_api_version_8_designer_8cs.html +++ b/20200423052709___m_y_kill_jsons_add_d_m_api_version_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423052709___m_y_kill_jsons_add_d_m_api_version_8_designer_8cs_source.html b/20200423052709___m_y_kill_jsons_add_d_m_api_version_8_designer_8cs_source.html index 18ca2bbee7..dd6af59d51 100644 --- a/20200423052709___m_y_kill_jsons_add_d_m_api_version_8_designer_8cs_source.html +++ b/20200423052709___m_y_kill_jsons_add_d_m_api_version_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423052709___m_y_kill_jsons_add_d_m_api_version_8cs.html b/20200423052709___m_y_kill_jsons_add_d_m_api_version_8cs.html index a8f0ea72ef..d1e36abdd9 100644 --- a/20200423052709___m_y_kill_jsons_add_d_m_api_version_8cs.html +++ b/20200423052709___m_y_kill_jsons_add_d_m_api_version_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200423052709___m_y_kill_jsons_add_d_m_api_version_8cs_source.html b/20200423052709___m_y_kill_jsons_add_d_m_api_version_8cs_source.html index 09d4b265b0..d4311d31f1 100644 --- a/20200423052709___m_y_kill_jsons_add_d_m_api_version_8cs_source.html +++ b/20200423052709___m_y_kill_jsons_add_d_m_api_version_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200425024433___m_s_add_job_error_codes_8_designer_8cs.html b/20200425024433___m_s_add_job_error_codes_8_designer_8cs.html index e38ab8cae2..ea716f2c62 100644 --- a/20200425024433___m_s_add_job_error_codes_8_designer_8cs.html +++ b/20200425024433___m_s_add_job_error_codes_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200425024433___m_s_add_job_error_codes_8_designer_8cs_source.html b/20200425024433___m_s_add_job_error_codes_8_designer_8cs_source.html index b21f1e4e5b..21966e0b19 100644 --- a/20200425024433___m_s_add_job_error_codes_8_designer_8cs_source.html +++ b/20200425024433___m_s_add_job_error_codes_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200425024433___m_s_add_job_error_codes_8cs.html b/20200425024433___m_s_add_job_error_codes_8cs.html index 48a7199d9a..43d18105d4 100644 --- a/20200425024433___m_s_add_job_error_codes_8cs.html +++ b/20200425024433___m_s_add_job_error_codes_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200425024433___m_s_add_job_error_codes_8cs_source.html b/20200425024433___m_s_add_job_error_codes_8cs_source.html index 02eb9671cd..727d47ffdd 100644 --- a/20200425024433___m_s_add_job_error_codes_8cs_source.html +++ b/20200425024433___m_s_add_job_error_codes_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200425024953___m_y_add_job_error_codes_8_designer_8cs.html b/20200425024953___m_y_add_job_error_codes_8_designer_8cs.html index 5cad697620..284b67ef55 100644 --- a/20200425024953___m_y_add_job_error_codes_8_designer_8cs.html +++ b/20200425024953___m_y_add_job_error_codes_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200425024953___m_y_add_job_error_codes_8_designer_8cs_source.html b/20200425024953___m_y_add_job_error_codes_8_designer_8cs_source.html index 5f5753e921..d7030239f4 100644 --- a/20200425024953___m_y_add_job_error_codes_8_designer_8cs_source.html +++ b/20200425024953___m_y_add_job_error_codes_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200425024953___m_y_add_job_error_codes_8cs.html b/20200425024953___m_y_add_job_error_codes_8cs.html index f0efaf5120..fb2de6eb2d 100644 --- a/20200425024953___m_y_add_job_error_codes_8cs.html +++ b/20200425024953___m_y_add_job_error_codes_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200425024953___m_y_add_job_error_codes_8cs_source.html b/20200425024953___m_y_add_job_error_codes_8cs_source.html index b21f5109ae..50786c184a 100644 --- a/20200425024953___m_y_add_job_error_codes_8cs_source.html +++ b/20200425024953___m_y_add_job_error_codes_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427173852___m_s_fix_cascading_delete_8_designer_8cs.html b/20200427173852___m_s_fix_cascading_delete_8_designer_8cs.html index 594cd6d58b..03d0378e54 100644 --- a/20200427173852___m_s_fix_cascading_delete_8_designer_8cs.html +++ b/20200427173852___m_s_fix_cascading_delete_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427173852___m_s_fix_cascading_delete_8_designer_8cs_source.html b/20200427173852___m_s_fix_cascading_delete_8_designer_8cs_source.html index 689ad3d0bd..a8a7ca00e6 100644 --- a/20200427173852___m_s_fix_cascading_delete_8_designer_8cs_source.html +++ b/20200427173852___m_s_fix_cascading_delete_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427173852___m_s_fix_cascading_delete_8cs.html b/20200427173852___m_s_fix_cascading_delete_8cs.html index 9bbd1d9ca1..577a915ba7 100644 --- a/20200427173852___m_s_fix_cascading_delete_8cs.html +++ b/20200427173852___m_s_fix_cascading_delete_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427173852___m_s_fix_cascading_delete_8cs_source.html b/20200427173852___m_s_fix_cascading_delete_8cs_source.html index 778c9ecfc2..97d71a7e32 100644 --- a/20200427173852___m_s_fix_cascading_delete_8cs_source.html +++ b/20200427173852___m_s_fix_cascading_delete_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427180159___m_y_fix_cascading_delete_8_designer_8cs.html b/20200427180159___m_y_fix_cascading_delete_8_designer_8cs.html index 2cbf95c23c..eabe474429 100644 --- a/20200427180159___m_y_fix_cascading_delete_8_designer_8cs.html +++ b/20200427180159___m_y_fix_cascading_delete_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427180159___m_y_fix_cascading_delete_8_designer_8cs_source.html b/20200427180159___m_y_fix_cascading_delete_8_designer_8cs_source.html index 249c031003..865d66cc97 100644 --- a/20200427180159___m_y_fix_cascading_delete_8_designer_8cs_source.html +++ b/20200427180159___m_y_fix_cascading_delete_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427180159___m_y_fix_cascading_delete_8cs.html b/20200427180159___m_y_fix_cascading_delete_8cs.html index 045f269fb1..41cbfff7a1 100644 --- a/20200427180159___m_y_fix_cascading_delete_8cs.html +++ b/20200427180159___m_y_fix_cascading_delete_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427180159___m_y_fix_cascading_delete_8cs_source.html b/20200427180159___m_y_fix_cascading_delete_8cs_source.html index 0dc5cc3504..c18d10df0c 100644 --- a/20200427180159___m_y_fix_cascading_delete_8cs_source.html +++ b/20200427180159___m_y_fix_cascading_delete_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427180904___s_l_rebuild_8_designer_8cs.html b/20200427180904___s_l_rebuild_8_designer_8cs.html index d3ab9e6329..433a23c6af 100644 --- a/20200427180904___s_l_rebuild_8_designer_8cs.html +++ b/20200427180904___s_l_rebuild_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427180904___s_l_rebuild_8_designer_8cs_source.html b/20200427180904___s_l_rebuild_8_designer_8cs_source.html index 279b355df9..6c55dc9eb7 100644 --- a/20200427180904___s_l_rebuild_8_designer_8cs_source.html +++ b/20200427180904___s_l_rebuild_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427180904___s_l_rebuild_8cs.html b/20200427180904___s_l_rebuild_8cs.html index 6e40f43d52..1e096cdcbf 100644 --- a/20200427180904___s_l_rebuild_8cs.html +++ b/20200427180904___s_l_rebuild_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200427180904___s_l_rebuild_8cs_source.html b/20200427180904___s_l_rebuild_8cs_source.html index 2de8413681..d4ce000d62 100644 --- a/20200427180904___s_l_rebuild_8cs_source.html +++ b/20200427180904___s_l_rebuild_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511005957___m_s_add_heartbeat_8_designer_8cs.html b/20200511005957___m_s_add_heartbeat_8_designer_8cs.html index 488e29848a..92da70693a 100644 --- a/20200511005957___m_s_add_heartbeat_8_designer_8cs.html +++ b/20200511005957___m_s_add_heartbeat_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511005957___m_s_add_heartbeat_8_designer_8cs_source.html b/20200511005957___m_s_add_heartbeat_8_designer_8cs_source.html index 4b5cd68f8e..925709eafd 100644 --- a/20200511005957___m_s_add_heartbeat_8_designer_8cs_source.html +++ b/20200511005957___m_s_add_heartbeat_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511005957___m_s_add_heartbeat_8cs.html b/20200511005957___m_s_add_heartbeat_8cs.html index 3a99f71124..527b78a2ae 100644 --- a/20200511005957___m_s_add_heartbeat_8cs.html +++ b/20200511005957___m_s_add_heartbeat_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511005957___m_s_add_heartbeat_8cs_source.html b/20200511005957___m_s_add_heartbeat_8cs_source.html index 0f5aab5b0f..0f33257ebc 100644 --- a/20200511005957___m_s_add_heartbeat_8cs_source.html +++ b/20200511005957___m_s_add_heartbeat_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511011048___m_y_add_heartbeat_8_designer_8cs.html b/20200511011048___m_y_add_heartbeat_8_designer_8cs.html index 5c65d0a466..66c608b217 100644 --- a/20200511011048___m_y_add_heartbeat_8_designer_8cs.html +++ b/20200511011048___m_y_add_heartbeat_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511011048___m_y_add_heartbeat_8_designer_8cs_source.html b/20200511011048___m_y_add_heartbeat_8_designer_8cs_source.html index 5aa06e9933..ee5af64b67 100644 --- a/20200511011048___m_y_add_heartbeat_8_designer_8cs_source.html +++ b/20200511011048___m_y_add_heartbeat_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511011048___m_y_add_heartbeat_8cs.html b/20200511011048___m_y_add_heartbeat_8cs.html index d0d97db9f4..88753e80c1 100644 --- a/20200511011048___m_y_add_heartbeat_8cs.html +++ b/20200511011048___m_y_add_heartbeat_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511011048___m_y_add_heartbeat_8cs_source.html b/20200511011048___m_y_add_heartbeat_8cs_source.html index 082da14b3c..0ef2a07dff 100644 --- a/20200511011048___m_y_add_heartbeat_8cs_source.html +++ b/20200511011048___m_y_add_heartbeat_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511012117___s_l_add_heartbeat_8_designer_8cs.html b/20200511012117___s_l_add_heartbeat_8_designer_8cs.html index d52771f567..33c1597391 100644 --- a/20200511012117___s_l_add_heartbeat_8_designer_8cs.html +++ b/20200511012117___s_l_add_heartbeat_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511012117___s_l_add_heartbeat_8_designer_8cs_source.html b/20200511012117___s_l_add_heartbeat_8_designer_8cs_source.html index f2f7845093..f25e841593 100644 --- a/20200511012117___s_l_add_heartbeat_8_designer_8cs_source.html +++ b/20200511012117___s_l_add_heartbeat_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511012117___s_l_add_heartbeat_8cs.html b/20200511012117___s_l_add_heartbeat_8cs.html index 0a799682bb..46d06ca82c 100644 --- a/20200511012117___s_l_add_heartbeat_8cs.html +++ b/20200511012117___s_l_add_heartbeat_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511012117___s_l_add_heartbeat_8cs_source.html b/20200511012117___s_l_add_heartbeat_8cs_source.html index b0cb847cc2..718a15cc60 100644 --- a/20200511012117___s_l_add_heartbeat_8cs_source.html +++ b/20200511012117___s_l_add_heartbeat_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511162912___s_l_remove_soft_columns_8_designer_8cs.html b/20200511162912___s_l_remove_soft_columns_8_designer_8cs.html index f266da61ce..f6f6c8c6af 100644 --- a/20200511162912___s_l_remove_soft_columns_8_designer_8cs.html +++ b/20200511162912___s_l_remove_soft_columns_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511162912___s_l_remove_soft_columns_8_designer_8cs_source.html b/20200511162912___s_l_remove_soft_columns_8_designer_8cs_source.html index 6df00fdce6..27b4e71c3a 100644 --- a/20200511162912___s_l_remove_soft_columns_8_designer_8cs_source.html +++ b/20200511162912___s_l_remove_soft_columns_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511162912___s_l_remove_soft_columns_8cs.html b/20200511162912___s_l_remove_soft_columns_8cs.html index 7c2b52d626..4e34ebf391 100644 --- a/20200511162912___s_l_remove_soft_columns_8cs.html +++ b/20200511162912___s_l_remove_soft_columns_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200511162912___s_l_remove_soft_columns_8cs_source.html b/20200511162912___s_l_remove_soft_columns_8cs_source.html index 3326a9d539..bccba59301 100644 --- a/20200511162912___s_l_remove_soft_columns_8cs_source.html +++ b/20200511162912___s_l_remove_soft_columns_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200512181801___m_s_remove_soft_columns_8_designer_8cs.html b/20200512181801___m_s_remove_soft_columns_8_designer_8cs.html index 6c66914c43..26052a8252 100644 --- a/20200512181801___m_s_remove_soft_columns_8_designer_8cs.html +++ b/20200512181801___m_s_remove_soft_columns_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200512181801___m_s_remove_soft_columns_8_designer_8cs_source.html b/20200512181801___m_s_remove_soft_columns_8_designer_8cs_source.html index 9dfabf6ca3..3f48ace937 100644 --- a/20200512181801___m_s_remove_soft_columns_8_designer_8cs_source.html +++ b/20200512181801___m_s_remove_soft_columns_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200512181801___m_s_remove_soft_columns_8cs.html b/20200512181801___m_s_remove_soft_columns_8cs.html index b01e2f7270..885d410bb2 100644 --- a/20200512181801___m_s_remove_soft_columns_8cs.html +++ b/20200512181801___m_s_remove_soft_columns_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200512181801___m_s_remove_soft_columns_8cs_source.html b/20200512181801___m_s_remove_soft_columns_8cs_source.html index 752015bc21..2121c0954e 100644 --- a/20200512181801___m_s_remove_soft_columns_8cs_source.html +++ b/20200512181801___m_s_remove_soft_columns_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200512182535___m_y_remove_soft_columns_8_designer_8cs.html b/20200512182535___m_y_remove_soft_columns_8_designer_8cs.html index e48839410f..460490c281 100644 --- a/20200512182535___m_y_remove_soft_columns_8_designer_8cs.html +++ b/20200512182535___m_y_remove_soft_columns_8_designer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200512182535___m_y_remove_soft_columns_8_designer_8cs_source.html b/20200512182535___m_y_remove_soft_columns_8_designer_8cs_source.html index 5cc2b8b4c1..a0a038c275 100644 --- a/20200512182535___m_y_remove_soft_columns_8_designer_8cs_source.html +++ b/20200512182535___m_y_remove_soft_columns_8_designer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200512182535___m_y_remove_soft_columns_8cs.html b/20200512182535___m_y_remove_soft_columns_8cs.html index 8dbed82619..698b3befd4 100644 --- a/20200512182535___m_y_remove_soft_columns_8cs.html +++ b/20200512182535___m_y_remove_soft_columns_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200512182535___m_y_remove_soft_columns_8cs_source.html b/20200512182535___m_y_remove_soft_columns_8cs_source.html index 32b2768675..1cad4a50cc 100644 --- a/20200512182535___m_y_remove_soft_columns_8cs_source.html +++ b/20200512182535___m_y_remove_soft_columns_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/20200516111712___p_g_create_8_designer_8cs.html b/20200516111712___p_g_create_8_designer_8cs.html new file mode 100644 index 0000000000..223022b8f7 --- /dev/null +++ b/20200516111712___p_g_create_8_designer_8cs.html @@ -0,0 +1,122 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.Designer.cs File Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
20200516111712_PGCreate.Designer.cs File Reference
+
+
+ +

Go to the source code of this file.

+ + + + + +

+Classes

class  Tgstation.Server.Host.Database.Migrations.PGCreate
 Create initial schema for PostgreSQL. More...
 
+ + + +

+Namespaces

namespace  Tgstation.Server.Host.Database.Migrations
 
+
+ + + + diff --git a/20200516111712___p_g_create_8_designer_8cs_source.html b/20200516111712___p_g_create_8_designer_8cs_source.html new file mode 100644 index 0000000000..c5d5ce1bdf --- /dev/null +++ b/20200516111712___p_g_create_8_designer_8cs_source.html @@ -0,0 +1,112 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.Designer.cs Source File + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
20200516111712_PGCreate.Designer.cs
+
+
+Go to the documentation of this file.
1 // <auto-generated />
2 using System;
3 using Microsoft.EntityFrameworkCore;
4 using Microsoft.EntityFrameworkCore.Infrastructure;
5 using Microsoft.EntityFrameworkCore.Migrations;
6 using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
7 
8 namespace Tgstation.Server.Host.Database.Migrations
9 {
10  [DbContext(typeof(PostgresSqlDatabaseContext))]
11  [Migration("20200516111712_PGCreate")]
12  partial class PGCreate
13  {
15  protected override void BuildTargetModel(ModelBuilder modelBuilder)
16  {
17 #pragma warning disable 612, 618
18  modelBuilder
19  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
20  .HasAnnotation("ProductVersion", "3.1.4")
21  .HasAnnotation("Relational:MaxIdentifierLength", 63);
22 
23  modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
24  {
25  b.Property<long>("Id")
26  .ValueGeneratedOnAdd()
27  .HasColumnType("bigint")
28  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
29 
30  b.Property<int>("ChannelLimit")
31  .HasColumnType("integer");
32 
33  b.Property<string>("ConnectionString")
34  .IsRequired()
35  .HasColumnType("character varying(10000)")
36  .HasMaxLength(10000);
37 
38  b.Property<bool?>("Enabled")
39  .HasColumnType("boolean");
40 
41  b.Property<long>("InstanceId")
42  .HasColumnType("bigint");
43 
44  b.Property<string>("Name")
45  .IsRequired()
46  .HasColumnType("character varying(100)")
47  .HasMaxLength(100);
48 
49  b.Property<int>("Provider")
50  .HasColumnType("integer");
51 
52  b.Property<long>("ReconnectionInterval")
53  .HasColumnType("bigint");
54 
55  b.HasKey("Id");
56 
57  b.HasIndex("InstanceId", "Name")
58  .IsUnique();
59 
60  b.ToTable("ChatBots");
61  });
62 
63  modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
64  {
65  b.Property<long>("Id")
66  .ValueGeneratedOnAdd()
67  .HasColumnType("bigint")
68  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
69 
70  b.Property<long>("ChatSettingsId")
71  .HasColumnType("bigint");
72 
73  b.Property<decimal?>("DiscordChannelId")
74  .HasColumnType("numeric(20,0)");
75 
76  b.Property<string>("IrcChannel")
77  .HasColumnType("character varying(100)")
78  .HasMaxLength(100);
79 
80  b.Property<bool?>("IsAdminChannel")
81  .IsRequired()
82  .HasColumnType("boolean");
83 
84  b.Property<bool?>("IsUpdatesChannel")
85  .IsRequired()
86  .HasColumnType("boolean");
87 
88  b.Property<bool?>("IsWatchdogChannel")
89  .IsRequired()
90  .HasColumnType("boolean");
91 
92  b.Property<string>("Tag")
93  .HasColumnType("character varying(10000)")
94  .HasMaxLength(10000);
95 
96  b.HasKey("Id");
97 
98  b.HasIndex("ChatSettingsId", "DiscordChannelId")
99  .IsUnique();
100 
101  b.HasIndex("ChatSettingsId", "IrcChannel")
102  .IsUnique();
103 
104  b.ToTable("ChatChannels");
105  });
106 
107  modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
108  {
109  b.Property<long>("Id")
110  .ValueGeneratedOnAdd()
111  .HasColumnType("bigint")
112  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
113 
114  b.Property<string>("ByondVersion")
115  .IsRequired()
116  .HasColumnType("text");
117 
118  b.Property<int?>("DMApiMajorVersion")
119  .HasColumnType("integer");
120 
121  b.Property<int?>("DMApiMinorVersion")
122  .HasColumnType("integer");
123 
124  b.Property<int?>("DMApiPatchVersion")
125  .HasColumnType("integer");
126 
127  b.Property<Guid?>("DirectoryName")
128  .IsRequired()
129  .HasColumnType("uuid");
130 
131  b.Property<string>("DmeName")
132  .IsRequired()
133  .HasColumnType("text");
134 
135  b.Property<long>("JobId")
136  .HasColumnType("bigint");
137 
138  b.Property<int>("MinimumSecurityLevel")
139  .HasColumnType("integer");
140 
141  b.Property<string>("Output")
142  .IsRequired()
143  .HasColumnType("text");
144 
145  b.Property<long>("RevisionInformationId")
146  .HasColumnType("bigint");
147 
148  b.HasKey("Id");
149 
150  b.HasIndex("DirectoryName");
151 
152  b.HasIndex("JobId")
153  .IsUnique();
154 
155  b.HasIndex("RevisionInformationId");
156 
157  b.ToTable("CompileJobs");
158  });
159 
160  modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
161  {
162  b.Property<long>("Id")
163  .ValueGeneratedOnAdd()
164  .HasColumnType("bigint")
165  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
166 
167  b.Property<bool?>("AllowWebClient")
168  .IsRequired()
169  .HasColumnType("boolean");
170 
171  b.Property<bool?>("AutoStart")
172  .IsRequired()
173  .HasColumnType("boolean");
174 
175  b.Property<long>("HeartbeatSeconds")
176  .HasColumnType("bigint");
177 
178  b.Property<long>("InstanceId")
179  .HasColumnType("bigint");
180 
181  b.Property<int>("PrimaryPort")
182  .HasColumnType("integer");
183 
184  b.Property<int>("SecondaryPort")
185  .HasColumnType("integer");
186 
187  b.Property<int>("SecurityLevel")
188  .HasColumnType("integer");
189 
190  b.Property<long>("StartupTimeout")
191  .HasColumnType("bigint");
192 
193  b.HasKey("Id");
194 
195  b.HasIndex("InstanceId")
196  .IsUnique();
197 
198  b.ToTable("DreamDaemonSettings");
199  });
200 
201  modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
202  {
203  b.Property<long>("Id")
204  .ValueGeneratedOnAdd()
205  .HasColumnType("bigint")
206  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
207 
208  b.Property<int>("ApiValidationPort")
209  .HasColumnType("integer");
210 
211  b.Property<int>("ApiValidationSecurityLevel")
212  .HasColumnType("integer");
213 
214  b.Property<long>("InstanceId")
215  .HasColumnType("bigint");
216 
217  b.Property<string>("ProjectName")
218  .HasColumnType("character varying(10000)")
219  .HasMaxLength(10000);
220 
221  b.HasKey("Id");
222 
223  b.HasIndex("InstanceId")
224  .IsUnique();
225 
226  b.ToTable("DreamMakerSettings");
227  });
228 
229  modelBuilder.Entity("Tgstation.Server.Host.Models.DualReattachInformation", b =>
230  {
231  b.Property<long>("Id")
232  .ValueGeneratedOnAdd()
233  .HasColumnType("bigint")
234  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
235 
236  b.Property<long?>("AlphaId")
237  .HasColumnType("bigint");
238 
239  b.Property<bool>("AlphaIsActive")
240  .HasColumnType("boolean");
241 
242  b.Property<long?>("BravoId")
243  .HasColumnType("bigint");
244 
245  b.Property<long>("InstanceId")
246  .HasColumnType("bigint");
247 
248  b.HasKey("Id");
249 
250  b.HasIndex("AlphaId");
251 
252  b.HasIndex("BravoId");
253 
254  b.HasIndex("InstanceId")
255  .IsUnique();
256 
257  b.ToTable("WatchdogReattachInformations");
258  });
259 
260  modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
261  {
262  b.Property<long>("Id")
263  .ValueGeneratedOnAdd()
264  .HasColumnType("bigint")
265  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
266 
267  b.Property<long>("AutoUpdateInterval")
268  .HasColumnType("bigint");
269 
270  b.Property<int>("ChatBotLimit")
271  .HasColumnType("integer");
272 
273  b.Property<int>("ConfigurationType")
274  .HasColumnType("integer");
275 
276  b.Property<string>("Name")
277  .IsRequired()
278  .HasColumnType("character varying(10000)")
279  .HasMaxLength(10000);
280 
281  b.Property<bool?>("Online")
282  .IsRequired()
283  .HasColumnType("boolean");
284 
285  b.Property<string>("Path")
286  .IsRequired()
287  .HasColumnType("text");
288 
289  b.HasKey("Id");
290 
291  b.HasIndex("Path")
292  .IsUnique();
293 
294  b.ToTable("Instances");
295  });
296 
297  modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
298  {
299  b.Property<long>("Id")
300  .ValueGeneratedOnAdd()
301  .HasColumnType("bigint")
302  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
303 
304  b.Property<decimal>("ByondRights")
305  .HasColumnType("numeric(20,0)");
306 
307  b.Property<decimal>("ChatBotRights")
308  .HasColumnType("numeric(20,0)");
309 
310  b.Property<decimal>("ConfigurationRights")
311  .HasColumnType("numeric(20,0)");
312 
313  b.Property<decimal>("DreamDaemonRights")
314  .HasColumnType("numeric(20,0)");
315 
316  b.Property<decimal>("DreamMakerRights")
317  .HasColumnType("numeric(20,0)");
318 
319  b.Property<long>("InstanceId")
320  .HasColumnType("bigint");
321 
322  b.Property<decimal>("InstanceUserRights")
323  .HasColumnType("numeric(20,0)");
324 
325  b.Property<decimal>("RepositoryRights")
326  .HasColumnType("numeric(20,0)");
327 
328  b.Property<long?>("UserId")
329  .IsRequired()
330  .HasColumnType("bigint");
331 
332  b.HasKey("Id");
333 
334  b.HasIndex("InstanceId");
335 
336  b.HasIndex("UserId", "InstanceId")
337  .IsUnique();
338 
339  b.ToTable("InstanceUsers");
340  });
341 
342  modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
343  {
344  b.Property<long>("Id")
345  .ValueGeneratedOnAdd()
346  .HasColumnType("bigint")
347  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
348 
349  b.Property<decimal?>("CancelRight")
350  .HasColumnType("numeric(20,0)");
351 
352  b.Property<decimal?>("CancelRightsType")
353  .HasColumnType("numeric(20,0)");
354 
355  b.Property<bool?>("Cancelled")
356  .IsRequired()
357  .HasColumnType("boolean");
358 
359  b.Property<long?>("CancelledById")
360  .HasColumnType("bigint");
361 
362  b.Property<string>("Description")
363  .IsRequired()
364  .HasColumnType("text");
365 
366  b.Property<long?>("ErrorCode")
367  .HasColumnType("bigint");
368 
369  b.Property<string>("ExceptionDetails")
370  .HasColumnType("text");
371 
372  b.Property<long>("InstanceId")
373  .HasColumnType("bigint");
374 
375  b.Property<DateTimeOffset?>("StartedAt")
376  .IsRequired()
377  .HasColumnType("timestamp with time zone");
378 
379  b.Property<long>("StartedById")
380  .HasColumnType("bigint");
381 
382  b.Property<DateTimeOffset?>("StoppedAt")
383  .HasColumnType("timestamp with time zone");
384 
385  b.HasKey("Id");
386 
387  b.HasIndex("CancelledById");
388 
389  b.HasIndex("InstanceId");
390 
391  b.HasIndex("StartedById");
392 
393  b.ToTable("Jobs");
394  });
395 
396  modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
397  {
398  b.Property<long>("Id")
399  .ValueGeneratedOnAdd()
400  .HasColumnType("bigint")
401  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
402 
403  b.Property<string>("AccessIdentifier")
404  .IsRequired()
405  .HasColumnType("text");
406 
407  b.Property<long>("CompileJobId")
408  .HasColumnType("bigint");
409 
410  b.Property<bool>("IsPrimary")
411  .HasColumnType("boolean");
412 
413  b.Property<int>("LaunchSecurityLevel")
414  .HasColumnType("integer");
415 
416  b.Property<int>("Port")
417  .HasColumnType("integer");
418 
419  b.Property<int>("ProcessId")
420  .HasColumnType("integer");
421 
422  b.Property<int>("RebootState")
423  .HasColumnType("integer");
424 
425  b.HasKey("Id");
426 
427  b.HasIndex("CompileJobId");
428 
429  b.ToTable("ReattachInformations");
430  });
431 
432  modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
433  {
434  b.Property<long>("Id")
435  .ValueGeneratedOnAdd()
436  .HasColumnType("bigint")
437  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
438 
439  b.Property<string>("AccessToken")
440  .HasColumnType("character varying(10000)")
441  .HasMaxLength(10000);
442 
443  b.Property<string>("AccessUser")
444  .HasColumnType("character varying(10000)")
445  .HasMaxLength(10000);
446 
447  b.Property<bool?>("AutoUpdatesKeepTestMerges")
448  .IsRequired()
449  .HasColumnType("boolean");
450 
451  b.Property<bool?>("AutoUpdatesSynchronize")
452  .IsRequired()
453  .HasColumnType("boolean");
454 
455  b.Property<string>("CommitterEmail")
456  .IsRequired()
457  .HasColumnType("character varying(10000)")
458  .HasMaxLength(10000);
459 
460  b.Property<string>("CommitterName")
461  .IsRequired()
462  .HasColumnType("character varying(10000)")
463  .HasMaxLength(10000);
464 
465  b.Property<long>("InstanceId")
466  .HasColumnType("bigint");
467 
468  b.Property<bool?>("PostTestMergeComment")
469  .IsRequired()
470  .HasColumnType("boolean");
471 
472  b.Property<bool?>("PushTestMergeCommits")
473  .IsRequired()
474  .HasColumnType("boolean");
475 
476  b.Property<bool?>("ShowTestMergeCommitters")
477  .IsRequired()
478  .HasColumnType("boolean");
479 
480  b.HasKey("Id");
481 
482  b.HasIndex("InstanceId")
483  .IsUnique();
484 
485  b.ToTable("RepositorySettings");
486  });
487 
488  modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
489  {
490  b.Property<long>("Id")
491  .ValueGeneratedOnAdd()
492  .HasColumnType("bigint")
493  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
494 
495  b.Property<long>("RevisionInformationId")
496  .HasColumnType("bigint");
497 
498  b.Property<long>("TestMergeId")
499  .HasColumnType("bigint");
500 
501  b.HasKey("Id");
502 
503  b.HasIndex("RevisionInformationId");
504 
505  b.HasIndex("TestMergeId");
506 
507  b.ToTable("RevInfoTestMerges");
508  });
509 
510  modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
511  {
512  b.Property<long>("Id")
513  .ValueGeneratedOnAdd()
514  .HasColumnType("bigint")
515  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
516 
517  b.Property<string>("CommitSha")
518  .IsRequired()
519  .HasColumnType("character varying(40)")
520  .HasMaxLength(40);
521 
522  b.Property<long>("InstanceId")
523  .HasColumnType("bigint");
524 
525  b.Property<string>("OriginCommitSha")
526  .IsRequired()
527  .HasColumnType("character varying(40)")
528  .HasMaxLength(40);
529 
530  b.HasKey("Id");
531 
532  b.HasIndex("InstanceId", "CommitSha")
533  .IsUnique();
534 
535  b.ToTable("RevisionInformations");
536  });
537 
538  modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
539  {
540  b.Property<long>("Id")
541  .ValueGeneratedOnAdd()
542  .HasColumnType("bigint")
543  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
544 
545  b.Property<string>("Author")
546  .IsRequired()
547  .HasColumnType("text");
548 
549  b.Property<string>("BodyAtMerge")
550  .IsRequired()
551  .HasColumnType("text");
552 
553  b.Property<string>("Comment")
554  .HasColumnType("character varying(10000)")
555  .HasMaxLength(10000);
556 
557  b.Property<DateTimeOffset>("MergedAt")
558  .HasColumnType("timestamp with time zone");
559 
560  b.Property<long>("MergedById")
561  .HasColumnType("bigint");
562 
563  b.Property<int>("Number")
564  .HasColumnType("integer");
565 
566  b.Property<long?>("PrimaryRevisionInformationId")
567  .IsRequired()
568  .HasColumnType("bigint");
569 
570  b.Property<string>("PullRequestRevision")
571  .IsRequired()
572  .HasColumnType("character varying(40)")
573  .HasMaxLength(40);
574 
575  b.Property<string>("TitleAtMerge")
576  .IsRequired()
577  .HasColumnType("text");
578 
579  b.Property<string>("Url")
580  .IsRequired()
581  .HasColumnType("text");
582 
583  b.HasKey("Id");
584 
585  b.HasIndex("MergedById");
586 
587  b.HasIndex("PrimaryRevisionInformationId")
588  .IsUnique();
589 
590  b.ToTable("TestMerges");
591  });
592 
593  modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
594  {
595  b.Property<long?>("Id")
596  .ValueGeneratedOnAdd()
597  .HasColumnType("bigint")
598  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
599 
600  b.Property<decimal>("AdministrationRights")
601  .HasColumnType("numeric(20,0)");
602 
603  b.Property<string>("CanonicalName")
604  .IsRequired()
605  .HasColumnType("text");
606 
607  b.Property<DateTimeOffset?>("CreatedAt")
608  .IsRequired()
609  .HasColumnType("timestamp with time zone");
610 
611  b.Property<long?>("CreatedById")
612  .HasColumnType("bigint");
613 
614  b.Property<bool?>("Enabled")
615  .IsRequired()
616  .HasColumnType("boolean");
617 
618  b.Property<decimal>("InstanceManagerRights")
619  .HasColumnType("numeric(20,0)");
620 
621  b.Property<DateTimeOffset?>("LastPasswordUpdate")
622  .HasColumnType("timestamp with time zone");
623 
624  b.Property<string>("Name")
625  .IsRequired()
626  .HasColumnType("character varying(10000)")
627  .HasMaxLength(10000);
628 
629  b.Property<string>("PasswordHash")
630  .HasColumnType("text");
631 
632  b.Property<string>("SystemIdentifier")
633  .HasColumnType("text");
634 
635  b.HasKey("Id");
636 
637  b.HasIndex("CanonicalName")
638  .IsUnique();
639 
640  b.HasIndex("CreatedById");
641 
642  b.HasIndex("SystemIdentifier")
643  .IsUnique();
644 
645  b.ToTable("Users");
646  });
647 
648  modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
649  {
650  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
651  .WithMany("ChatSettings")
652  .HasForeignKey("InstanceId")
653  .OnDelete(DeleteBehavior.Cascade)
654  .IsRequired();
655  });
656 
657  modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
658  {
659  b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings")
660  .WithMany("Channels")
661  .HasForeignKey("ChatSettingsId")
662  .OnDelete(DeleteBehavior.Cascade)
663  .IsRequired();
664  });
665 
666  modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
667  {
668  b.HasOne("Tgstation.Server.Host.Models.Job", "Job")
669  .WithOne()
670  .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId")
671  .OnDelete(DeleteBehavior.Cascade)
672  .IsRequired();
673 
674  b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
675  .WithMany("CompileJobs")
676  .HasForeignKey("RevisionInformationId")
677  .OnDelete(DeleteBehavior.ClientNoAction)
678  .IsRequired();
679  });
680 
681  modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
682  {
683  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
684  .WithOne("DreamDaemonSettings")
685  .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId")
686  .OnDelete(DeleteBehavior.Cascade)
687  .IsRequired();
688  });
689 
690  modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
691  {
692  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
693  .WithOne("DreamMakerSettings")
694  .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId")
695  .OnDelete(DeleteBehavior.Cascade)
696  .IsRequired();
697  });
698 
699  modelBuilder.Entity("Tgstation.Server.Host.Models.DualReattachInformation", b =>
700  {
701  b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Alpha")
702  .WithMany()
703  .HasForeignKey("AlphaId");
704 
705  b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo")
706  .WithMany()
707  .HasForeignKey("BravoId");
708 
709  b.HasOne("Tgstation.Server.Host.Models.Instance", null)
710  .WithOne("WatchdogReattachInformation")
711  .HasForeignKey("Tgstation.Server.Host.Models.DualReattachInformation", "InstanceId")
712  .OnDelete(DeleteBehavior.Cascade)
713  .IsRequired();
714  });
715 
716  modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
717  {
718  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
719  .WithMany("InstanceUsers")
720  .HasForeignKey("InstanceId")
721  .OnDelete(DeleteBehavior.Cascade)
722  .IsRequired();
723 
724  b.HasOne("Tgstation.Server.Host.Models.User", null)
725  .WithMany("InstanceUsers")
726  .HasForeignKey("UserId")
727  .OnDelete(DeleteBehavior.Cascade)
728  .IsRequired();
729  });
730 
731  modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
732  {
733  b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy")
734  .WithMany()
735  .HasForeignKey("CancelledById");
736 
737  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
738  .WithMany("Jobs")
739  .HasForeignKey("InstanceId")
740  .OnDelete(DeleteBehavior.Cascade)
741  .IsRequired();
742 
743  b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy")
744  .WithMany()
745  .HasForeignKey("StartedById")
746  .OnDelete(DeleteBehavior.Cascade)
747  .IsRequired();
748  });
749 
750  modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
751  {
752  b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
753  .WithMany()
754  .HasForeignKey("CompileJobId")
755  .OnDelete(DeleteBehavior.Cascade)
756  .IsRequired();
757  });
758 
759  modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
760  {
761  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
762  .WithOne("RepositorySettings")
763  .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId")
764  .OnDelete(DeleteBehavior.Cascade)
765  .IsRequired();
766  });
767 
768  modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
769  {
770  b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
771  .WithMany("ActiveTestMerges")
772  .HasForeignKey("RevisionInformationId")
773  .OnDelete(DeleteBehavior.Cascade)
774  .IsRequired();
775 
776  b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge")
777  .WithMany("RevisonInformations")
778  .HasForeignKey("TestMergeId")
779  .OnDelete(DeleteBehavior.ClientNoAction)
780  .IsRequired();
781  });
782 
783  modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
784  {
785  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
786  .WithMany("RevisionInformations")
787  .HasForeignKey("InstanceId")
788  .OnDelete(DeleteBehavior.Cascade)
789  .IsRequired();
790  });
791 
792  modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
793  {
794  b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy")
795  .WithMany("TestMerges")
796  .HasForeignKey("MergedById")
797  .OnDelete(DeleteBehavior.Restrict)
798  .IsRequired();
799 
800  b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation")
801  .WithOne("PrimaryTestMerge")
802  .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId")
803  .OnDelete(DeleteBehavior.Cascade)
804  .IsRequired();
805  });
806 
807  modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
808  {
809  b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy")
810  .WithMany("CreatedUsers")
811  .HasForeignKey("CreatedById");
812  });
813 #pragma warning restore 612, 618
814  }
815  }
816 }
+ + + + + + +
+ + + + diff --git a/20200516111712___p_g_create_8cs.html b/20200516111712___p_g_create_8cs.html new file mode 100644 index 0000000000..aef7b0a276 --- /dev/null +++ b/20200516111712___p_g_create_8cs.html @@ -0,0 +1,122 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.cs File Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
20200516111712_PGCreate.cs File Reference
+
+
+ +

Go to the source code of this file.

+ + + + + +

+Classes

class  Tgstation.Server.Host.Database.Migrations.PGCreate
 Create initial schema for PostgreSQL. More...
 
+ + + +

+Namespaces

namespace  Tgstation.Server.Host.Database.Migrations
 
+
+ + + + diff --git a/20200516111712___p_g_create_8cs_source.html b/20200516111712___p_g_create_8cs_source.html new file mode 100644 index 0000000000..f967fb9161 --- /dev/null +++ b/20200516111712___p_g_create_8cs_source.html @@ -0,0 +1,125 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/Migrations/20200516111712_PGCreate.cs Source File + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
20200516111712_PGCreate.cs
+
+
+Go to the documentation of this file.
1 using System;
2 using Microsoft.EntityFrameworkCore.Migrations;
3 using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
4 
5 namespace Tgstation.Server.Host.Database.Migrations
6 {
10  public partial class PGCreate : Migration
11  {
13  protected override void Up(MigrationBuilder migrationBuilder)
14  {
15  if (migrationBuilder == null)
16  throw new ArgumentNullException(nameof(migrationBuilder));
17 
18  migrationBuilder.CreateTable(
19  name: "Instances",
20  columns: table => new
21  {
22  Id = table.Column<long>(nullable: false)
23  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
24  Name = table.Column<string>(maxLength: 10000, nullable: false),
25  Path = table.Column<string>(nullable: false),
26  Online = table.Column<bool>(nullable: false),
27  ConfigurationType = table.Column<int>(nullable: false),
28  AutoUpdateInterval = table.Column<long>(nullable: false),
29  ChatBotLimit = table.Column<int>(nullable: false)
30  },
31  constraints: table =>
32  {
33  table.PrimaryKey("PK_Instances", x => x.Id);
34  });
35 
36  migrationBuilder.CreateTable(
37  name: "Users",
38  columns: table => new
39  {
40  Id = table.Column<long>(nullable: false)
41  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
42  Enabled = table.Column<bool>(nullable: false),
43  CreatedAt = table.Column<DateTimeOffset>(nullable: false),
44  SystemIdentifier = table.Column<string>(nullable: true),
45  Name = table.Column<string>(maxLength: 10000, nullable: false),
46  AdministrationRights = table.Column<decimal>(nullable: false),
47  InstanceManagerRights = table.Column<decimal>(nullable: false),
48  PasswordHash = table.Column<string>(nullable: true),
49  CreatedById = table.Column<long>(nullable: true),
50  CanonicalName = table.Column<string>(nullable: false),
51  LastPasswordUpdate = table.Column<DateTimeOffset>(nullable: true)
52  },
53  constraints: table =>
54  {
55  table.PrimaryKey("PK_Users", x => x.Id);
56  table.ForeignKey(
57  name: "FK_Users_Users_CreatedById",
58  column: x => x.CreatedById,
59  principalTable: "Users",
60  principalColumn: "Id",
61  onDelete: ReferentialAction.Restrict);
62  });
63 
64  migrationBuilder.CreateTable(
65  name: "ChatBots",
66  columns: table => new
67  {
68  Id = table.Column<long>(nullable: false)
69  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
70  Name = table.Column<string>(maxLength: 100, nullable: false),
71  Enabled = table.Column<bool>(nullable: true),
72  ReconnectionInterval = table.Column<long>(nullable: false),
73  ChannelLimit = table.Column<int>(nullable: false),
74  Provider = table.Column<int>(nullable: false),
75  ConnectionString = table.Column<string>(maxLength: 10000, nullable: false),
76  InstanceId = table.Column<long>(nullable: false)
77  },
78  constraints: table =>
79  {
80  table.PrimaryKey("PK_ChatBots", x => x.Id);
81  table.ForeignKey(
82  name: "FK_ChatBots_Instances_InstanceId",
83  column: x => x.InstanceId,
84  principalTable: "Instances",
85  principalColumn: "Id",
86  onDelete: ReferentialAction.Cascade);
87  });
88 
89  migrationBuilder.CreateTable(
90  name: "DreamDaemonSettings",
91  columns: table => new
92  {
93  Id = table.Column<long>(nullable: false)
94  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
95  AllowWebClient = table.Column<bool>(nullable: false),
96  SecurityLevel = table.Column<int>(nullable: false),
97  PrimaryPort = table.Column<int>(nullable: false),
98  SecondaryPort = table.Column<int>(nullable: false),
99  StartupTimeout = table.Column<long>(nullable: false),
100  HeartbeatSeconds = table.Column<long>(nullable: false),
101  AutoStart = table.Column<bool>(nullable: false),
102  InstanceId = table.Column<long>(nullable: false)
103  },
104  constraints: table =>
105  {
106  table.PrimaryKey("PK_DreamDaemonSettings", x => x.Id);
107  table.ForeignKey(
108  name: "FK_DreamDaemonSettings_Instances_InstanceId",
109  column: x => x.InstanceId,
110  principalTable: "Instances",
111  principalColumn: "Id",
112  onDelete: ReferentialAction.Cascade);
113  });
114 
115  migrationBuilder.CreateTable(
116  name: "DreamMakerSettings",
117  columns: table => new
118  {
119  Id = table.Column<long>(nullable: false)
120  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
121  ProjectName = table.Column<string>(maxLength: 10000, nullable: true),
122  ApiValidationPort = table.Column<int>(nullable: false),
123  ApiValidationSecurityLevel = table.Column<int>(nullable: false),
124  InstanceId = table.Column<long>(nullable: false)
125  },
126  constraints: table =>
127  {
128  table.PrimaryKey("PK_DreamMakerSettings", x => x.Id);
129  table.ForeignKey(
130  name: "FK_DreamMakerSettings_Instances_InstanceId",
131  column: x => x.InstanceId,
132  principalTable: "Instances",
133  principalColumn: "Id",
134  onDelete: ReferentialAction.Cascade);
135  });
136 
137  migrationBuilder.CreateTable(
138  name: "RepositorySettings",
139  columns: table => new
140  {
141  Id = table.Column<long>(nullable: false)
142  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
143  CommitterName = table.Column<string>(maxLength: 10000, nullable: false),
144  CommitterEmail = table.Column<string>(maxLength: 10000, nullable: false),
145  AccessUser = table.Column<string>(maxLength: 10000, nullable: true),
146  AccessToken = table.Column<string>(maxLength: 10000, nullable: true),
147  PushTestMergeCommits = table.Column<bool>(nullable: false),
148  ShowTestMergeCommitters = table.Column<bool>(nullable: false),
149  AutoUpdatesKeepTestMerges = table.Column<bool>(nullable: false),
150  AutoUpdatesSynchronize = table.Column<bool>(nullable: false),
151  PostTestMergeComment = table.Column<bool>(nullable: false),
152  InstanceId = table.Column<long>(nullable: false)
153  },
154  constraints: table =>
155  {
156  table.PrimaryKey("PK_RepositorySettings", x => x.Id);
157  table.ForeignKey(
158  name: "FK_RepositorySettings_Instances_InstanceId",
159  column: x => x.InstanceId,
160  principalTable: "Instances",
161  principalColumn: "Id",
162  onDelete: ReferentialAction.Cascade);
163  });
164 
165  migrationBuilder.CreateTable(
166  name: "RevisionInformations",
167  columns: table => new
168  {
169  Id = table.Column<long>(nullable: false)
170  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
171  CommitSha = table.Column<string>(maxLength: 40, nullable: false),
172  OriginCommitSha = table.Column<string>(maxLength: 40, nullable: false),
173  InstanceId = table.Column<long>(nullable: false)
174  },
175  constraints: table =>
176  {
177  table.PrimaryKey("PK_RevisionInformations", x => x.Id);
178  table.ForeignKey(
179  name: "FK_RevisionInformations_Instances_InstanceId",
180  column: x => x.InstanceId,
181  principalTable: "Instances",
182  principalColumn: "Id",
183  onDelete: ReferentialAction.Cascade);
184  });
185 
186  migrationBuilder.CreateTable(
187  name: "InstanceUsers",
188  columns: table => new
189  {
190  Id = table.Column<long>(nullable: false)
191  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
192  UserId = table.Column<long>(nullable: false),
193  InstanceUserRights = table.Column<decimal>(nullable: false),
194  ByondRights = table.Column<decimal>(nullable: false),
195  DreamDaemonRights = table.Column<decimal>(nullable: false),
196  DreamMakerRights = table.Column<decimal>(nullable: false),
197  RepositoryRights = table.Column<decimal>(nullable: false),
198  ChatBotRights = table.Column<decimal>(nullable: false),
199  ConfigurationRights = table.Column<decimal>(nullable: false),
200  InstanceId = table.Column<long>(nullable: false)
201  },
202  constraints: table =>
203  {
204  table.PrimaryKey("PK_InstanceUsers", x => x.Id);
205  table.ForeignKey(
206  name: "FK_InstanceUsers_Instances_InstanceId",
207  column: x => x.InstanceId,
208  principalTable: "Instances",
209  principalColumn: "Id",
210  onDelete: ReferentialAction.Cascade);
211  table.ForeignKey(
212  name: "FK_InstanceUsers_Users_UserId",
213  column: x => x.UserId,
214  principalTable: "Users",
215  principalColumn: "Id",
216  onDelete: ReferentialAction.Cascade);
217  });
218 
219  migrationBuilder.CreateTable(
220  name: "Jobs",
221  columns: table => new
222  {
223  Id = table.Column<long>(nullable: false)
224  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
225  Description = table.Column<string>(nullable: false),
226  ErrorCode = table.Column<long>(nullable: true),
227  ExceptionDetails = table.Column<string>(nullable: true),
228  StartedAt = table.Column<DateTimeOffset>(nullable: false),
229  StoppedAt = table.Column<DateTimeOffset>(nullable: true),
230  Cancelled = table.Column<bool>(nullable: false),
231  CancelRightsType = table.Column<decimal>(nullable: true),
232  CancelRight = table.Column<decimal>(nullable: true),
233  StartedById = table.Column<long>(nullable: false),
234  CancelledById = table.Column<long>(nullable: true),
235  InstanceId = table.Column<long>(nullable: false)
236  },
237  constraints: table =>
238  {
239  table.PrimaryKey("PK_Jobs", x => x.Id);
240  table.ForeignKey(
241  name: "FK_Jobs_Users_CancelledById",
242  column: x => x.CancelledById,
243  principalTable: "Users",
244  principalColumn: "Id",
245  onDelete: ReferentialAction.Restrict);
246  table.ForeignKey(
247  name: "FK_Jobs_Instances_InstanceId",
248  column: x => x.InstanceId,
249  principalTable: "Instances",
250  principalColumn: "Id",
251  onDelete: ReferentialAction.Cascade);
252  table.ForeignKey(
253  name: "FK_Jobs_Users_StartedById",
254  column: x => x.StartedById,
255  principalTable: "Users",
256  principalColumn: "Id",
257  onDelete: ReferentialAction.Cascade);
258  });
259 
260  migrationBuilder.CreateTable(
261  name: "ChatChannels",
262  columns: table => new
263  {
264  Id = table.Column<long>(nullable: false)
265  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
266  IrcChannel = table.Column<string>(maxLength: 100, nullable: true),
267  DiscordChannelId = table.Column<decimal>(nullable: true),
268  IsAdminChannel = table.Column<bool>(nullable: false),
269  IsWatchdogChannel = table.Column<bool>(nullable: false),
270  IsUpdatesChannel = table.Column<bool>(nullable: false),
271  Tag = table.Column<string>(maxLength: 10000, nullable: true),
272  ChatSettingsId = table.Column<long>(nullable: false)
273  },
274  constraints: table =>
275  {
276  table.PrimaryKey("PK_ChatChannels", x => x.Id);
277  table.ForeignKey(
278  name: "FK_ChatChannels_ChatBots_ChatSettingsId",
279  column: x => x.ChatSettingsId,
280  principalTable: "ChatBots",
281  principalColumn: "Id",
282  onDelete: ReferentialAction.Cascade);
283  });
284 
285  migrationBuilder.CreateTable(
286  name: "TestMerges",
287  columns: table => new
288  {
289  Id = table.Column<long>(nullable: false)
290  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
291  Number = table.Column<int>(nullable: false),
292  PullRequestRevision = table.Column<string>(maxLength: 40, nullable: false),
293  Comment = table.Column<string>(maxLength: 10000, nullable: true),
294  TitleAtMerge = table.Column<string>(nullable: false),
295  BodyAtMerge = table.Column<string>(nullable: false),
296  Url = table.Column<string>(nullable: false),
297  Author = table.Column<string>(nullable: false),
298  MergedAt = table.Column<DateTimeOffset>(nullable: false),
299  MergedById = table.Column<long>(nullable: false),
300  PrimaryRevisionInformationId = table.Column<long>(nullable: false)
301  },
302  constraints: table =>
303  {
304  table.PrimaryKey("PK_TestMerges", x => x.Id);
305  table.ForeignKey(
306  name: "FK_TestMerges_Users_MergedById",
307  column: x => x.MergedById,
308  principalTable: "Users",
309  principalColumn: "Id",
310  onDelete: ReferentialAction.Restrict);
311  table.ForeignKey(
312  name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId",
313  column: x => x.PrimaryRevisionInformationId,
314  principalTable: "RevisionInformations",
315  principalColumn: "Id",
316  onDelete: ReferentialAction.Cascade);
317  });
318 
319  migrationBuilder.CreateTable(
320  name: "CompileJobs",
321  columns: table => new
322  {
323  Id = table.Column<long>(nullable: false)
324  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
325  DmeName = table.Column<string>(nullable: false),
326  Output = table.Column<string>(nullable: false),
327  DirectoryName = table.Column<Guid>(nullable: false),
328  MinimumSecurityLevel = table.Column<int>(nullable: false),
329  JobId = table.Column<long>(nullable: false),
330  RevisionInformationId = table.Column<long>(nullable: false),
331  ByondVersion = table.Column<string>(nullable: false),
332  DMApiMajorVersion = table.Column<int>(nullable: true),
333  DMApiMinorVersion = table.Column<int>(nullable: true),
334  DMApiPatchVersion = table.Column<int>(nullable: true)
335  },
336  constraints: table =>
337  {
338  table.PrimaryKey("PK_CompileJobs", x => x.Id);
339  table.ForeignKey(
340  name: "FK_CompileJobs_Jobs_JobId",
341  column: x => x.JobId,
342  principalTable: "Jobs",
343  principalColumn: "Id",
344  onDelete: ReferentialAction.Cascade);
345  table.ForeignKey(
346  name: "FK_CompileJobs_RevisionInformations_RevisionInformationId",
347  column: x => x.RevisionInformationId,
348  principalTable: "RevisionInformations",
349  principalColumn: "Id");
350  });
351 
352  migrationBuilder.CreateTable(
353  name: "RevInfoTestMerges",
354  columns: table => new
355  {
356  Id = table.Column<long>(nullable: false)
357  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
358  TestMergeId = table.Column<long>(nullable: false),
359  RevisionInformationId = table.Column<long>(nullable: false)
360  },
361  constraints: table =>
362  {
363  table.PrimaryKey("PK_RevInfoTestMerges", x => x.Id);
364  table.ForeignKey(
365  name: "FK_RevInfoTestMerges_RevisionInformations_RevisionInformationId",
366  column: x => x.RevisionInformationId,
367  principalTable: "RevisionInformations",
368  principalColumn: "Id",
369  onDelete: ReferentialAction.Cascade);
370  table.ForeignKey(
371  name: "FK_RevInfoTestMerges_TestMerges_TestMergeId",
372  column: x => x.TestMergeId,
373  principalTable: "TestMerges",
374  principalColumn: "Id");
375  });
376 
377  migrationBuilder.CreateTable(
378  name: "ReattachInformations",
379  columns: table => new
380  {
381  Id = table.Column<long>(nullable: false)
382  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
383  AccessIdentifier = table.Column<string>(nullable: false),
384  ProcessId = table.Column<int>(nullable: false),
385  IsPrimary = table.Column<bool>(nullable: false),
386  Port = table.Column<int>(nullable: false),
387  RebootState = table.Column<int>(nullable: false),
388  LaunchSecurityLevel = table.Column<int>(nullable: false),
389  CompileJobId = table.Column<long>(nullable: false)
390  },
391  constraints: table =>
392  {
393  table.PrimaryKey("PK_ReattachInformations", x => x.Id);
394  table.ForeignKey(
395  name: "FK_ReattachInformations_CompileJobs_CompileJobId",
396  column: x => x.CompileJobId,
397  principalTable: "CompileJobs",
398  principalColumn: "Id",
399  onDelete: ReferentialAction.Cascade);
400  });
401 
402  migrationBuilder.CreateTable(
403  name: "WatchdogReattachInformations",
404  columns: table => new
405  {
406  Id = table.Column<long>(nullable: false)
407  .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
408  AlphaIsActive = table.Column<bool>(nullable: false),
409  InstanceId = table.Column<long>(nullable: false),
410  AlphaId = table.Column<long>(nullable: true),
411  BravoId = table.Column<long>(nullable: true)
412  },
413  constraints: table =>
414  {
415  table.PrimaryKey("PK_WatchdogReattachInformations", x => x.Id);
416  table.ForeignKey(
417  name: "FK_WatchdogReattachInformations_ReattachInformations_AlphaId",
418  column: x => x.AlphaId,
419  principalTable: "ReattachInformations",
420  principalColumn: "Id",
421  onDelete: ReferentialAction.Restrict);
422  table.ForeignKey(
423  name: "FK_WatchdogReattachInformations_ReattachInformations_BravoId",
424  column: x => x.BravoId,
425  principalTable: "ReattachInformations",
426  principalColumn: "Id",
427  onDelete: ReferentialAction.Restrict);
428  table.ForeignKey(
429  name: "FK_WatchdogReattachInformations_Instances_InstanceId",
430  column: x => x.InstanceId,
431  principalTable: "Instances",
432  principalColumn: "Id",
433  onDelete: ReferentialAction.Cascade);
434  });
435 
436  migrationBuilder.CreateIndex(
437  name: "IX_ChatBots_InstanceId_Name",
438  table: "ChatBots",
439  columns: new[] { "InstanceId", "Name" },
440  unique: true);
441 
442  migrationBuilder.CreateIndex(
443  name: "IX_ChatChannels_ChatSettingsId_DiscordChannelId",
444  table: "ChatChannels",
445  columns: new[] { "ChatSettingsId", "DiscordChannelId" },
446  unique: true);
447 
448  migrationBuilder.CreateIndex(
449  name: "IX_ChatChannels_ChatSettingsId_IrcChannel",
450  table: "ChatChannels",
451  columns: new[] { "ChatSettingsId", "IrcChannel" },
452  unique: true);
453 
454  migrationBuilder.CreateIndex(
455  name: "IX_CompileJobs_DirectoryName",
456  table: "CompileJobs",
457  column: "DirectoryName");
458 
459  migrationBuilder.CreateIndex(
460  name: "IX_CompileJobs_JobId",
461  table: "CompileJobs",
462  column: "JobId",
463  unique: true);
464 
465  migrationBuilder.CreateIndex(
466  name: "IX_CompileJobs_RevisionInformationId",
467  table: "CompileJobs",
468  column: "RevisionInformationId");
469 
470  migrationBuilder.CreateIndex(
471  name: "IX_DreamDaemonSettings_InstanceId",
472  table: "DreamDaemonSettings",
473  column: "InstanceId",
474  unique: true);
475 
476  migrationBuilder.CreateIndex(
477  name: "IX_DreamMakerSettings_InstanceId",
478  table: "DreamMakerSettings",
479  column: "InstanceId",
480  unique: true);
481 
482  migrationBuilder.CreateIndex(
483  name: "IX_Instances_Path",
484  table: "Instances",
485  column: "Path",
486  unique: true);
487 
488  migrationBuilder.CreateIndex(
489  name: "IX_InstanceUsers_InstanceId",
490  table: "InstanceUsers",
491  column: "InstanceId");
492 
493  migrationBuilder.CreateIndex(
494  name: "IX_InstanceUsers_UserId_InstanceId",
495  table: "InstanceUsers",
496  columns: new[] { "UserId", "InstanceId" },
497  unique: true);
498 
499  migrationBuilder.CreateIndex(
500  name: "IX_Jobs_CancelledById",
501  table: "Jobs",
502  column: "CancelledById");
503 
504  migrationBuilder.CreateIndex(
505  name: "IX_Jobs_InstanceId",
506  table: "Jobs",
507  column: "InstanceId");
508 
509  migrationBuilder.CreateIndex(
510  name: "IX_Jobs_StartedById",
511  table: "Jobs",
512  column: "StartedById");
513 
514  migrationBuilder.CreateIndex(
515  name: "IX_ReattachInformations_CompileJobId",
516  table: "ReattachInformations",
517  column: "CompileJobId");
518 
519  migrationBuilder.CreateIndex(
520  name: "IX_RepositorySettings_InstanceId",
521  table: "RepositorySettings",
522  column: "InstanceId",
523  unique: true);
524 
525  migrationBuilder.CreateIndex(
526  name: "IX_RevInfoTestMerges_RevisionInformationId",
527  table: "RevInfoTestMerges",
528  column: "RevisionInformationId");
529 
530  migrationBuilder.CreateIndex(
531  name: "IX_RevInfoTestMerges_TestMergeId",
532  table: "RevInfoTestMerges",
533  column: "TestMergeId");
534 
535  migrationBuilder.CreateIndex(
536  name: "IX_RevisionInformations_InstanceId_CommitSha",
537  table: "RevisionInformations",
538  columns: new[] { "InstanceId", "CommitSha" },
539  unique: true);
540 
541  migrationBuilder.CreateIndex(
542  name: "IX_TestMerges_MergedById",
543  table: "TestMerges",
544  column: "MergedById");
545 
546  migrationBuilder.CreateIndex(
547  name: "IX_TestMerges_PrimaryRevisionInformationId",
548  table: "TestMerges",
549  column: "PrimaryRevisionInformationId",
550  unique: true);
551 
552  migrationBuilder.CreateIndex(
553  name: "IX_Users_CanonicalName",
554  table: "Users",
555  column: "CanonicalName",
556  unique: true);
557 
558  migrationBuilder.CreateIndex(
559  name: "IX_Users_CreatedById",
560  table: "Users",
561  column: "CreatedById");
562 
563  migrationBuilder.CreateIndex(
564  name: "IX_Users_SystemIdentifier",
565  table: "Users",
566  column: "SystemIdentifier",
567  unique: true);
568 
569  migrationBuilder.CreateIndex(
570  name: "IX_WatchdogReattachInformations_AlphaId",
571  table: "WatchdogReattachInformations",
572  column: "AlphaId");
573 
574  migrationBuilder.CreateIndex(
575  name: "IX_WatchdogReattachInformations_BravoId",
576  table: "WatchdogReattachInformations",
577  column: "BravoId");
578 
579  migrationBuilder.CreateIndex(
580  name: "IX_WatchdogReattachInformations_InstanceId",
581  table: "WatchdogReattachInformations",
582  column: "InstanceId",
583  unique: true);
584  }
585 
587  protected override void Down(MigrationBuilder migrationBuilder)
588  {
589  if (migrationBuilder == null)
590  throw new ArgumentNullException(nameof(migrationBuilder));
591 
592  migrationBuilder.DropTable(
593  name: "ChatChannels");
594 
595  migrationBuilder.DropTable(
596  name: "DreamDaemonSettings");
597 
598  migrationBuilder.DropTable(
599  name: "DreamMakerSettings");
600 
601  migrationBuilder.DropTable(
602  name: "InstanceUsers");
603 
604  migrationBuilder.DropTable(
605  name: "RepositorySettings");
606 
607  migrationBuilder.DropTable(
608  name: "RevInfoTestMerges");
609 
610  migrationBuilder.DropTable(
611  name: "WatchdogReattachInformations");
612 
613  migrationBuilder.DropTable(
614  name: "ChatBots");
615 
616  migrationBuilder.DropTable(
617  name: "TestMerges");
618 
619  migrationBuilder.DropTable(
620  name: "ReattachInformations");
621 
622  migrationBuilder.DropTable(
623  name: "CompileJobs");
624 
625  migrationBuilder.DropTable(
626  name: "Jobs");
627 
628  migrationBuilder.DropTable(
629  name: "RevisionInformations");
630 
631  migrationBuilder.DropTable(
632  name: "Users");
633 
634  migrationBuilder.DropTable(
635  name: "Instances");
636  }
637  }
638 }
ConfigurationRights
Rights for Models.ConfigurationFile
+ +
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
+
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
+
InstanceUserRights
Rights for an Models.Instance
+ + +
AdministrationRights
Rights for Models.Administration
+
ConfigurationType
The type of configuration allowed on an Instance
+ +
override void Up(MigrationBuilder migrationBuilder)
+
DreamDaemonRights
Rights for Models.DreamDaemon
+
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
+
InstanceManagerRights
Rights for managing Models.Instances
+
override void Down(MigrationBuilder migrationBuilder)
+
DreamMakerRights
Rights for Models.DreamMaker
+ +
RepositoryRights
Rights for a Models.Repository
+
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
+ +
+ + + + diff --git a/_a_p_i_8dox.html b/_a_p_i_8dox.html index a46942d0b8..884a86b078 100644 --- a/_a_p_i_8dox.html +++ b/_a_p_i_8dox.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_administration_8cs.html b/_administration_8cs.html index eed5a5c358..120e09fae9 100644 --- a/_administration_8cs.html +++ b/_administration_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_administration_8cs_source.html b/_administration_8cs_source.html index abd0e205b8..746256aa66 100644 --- a/_administration_8cs_source.html +++ b/_administration_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_administration_client_8cs.html b/_administration_client_8cs.html index a62919c447..89374c0c99 100644 --- a/_administration_client_8cs.html +++ b/_administration_client_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_administration_client_8cs_source.html b/_administration_client_8cs_source.html index a062d0a2da..f81b802ed3 100644 --- a/_administration_client_8cs_source.html +++ b/_administration_client_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_administration_controller_8cs.html b/_administration_controller_8cs.html index ec14d10374..cc4fc498c5 100644 --- a/_administration_controller_8cs.html +++ b/_administration_controller_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_administration_controller_8cs_source.html b/_administration_controller_8cs_source.html index 01cdbdf510..a4b1bd9a71 100644 --- a/_administration_controller_8cs_source.html +++ b/_administration_controller_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -136,7 +136,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IServerControl serverUpdater
The IServerControl for the AdministrationController
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the AdministrationController
-
+
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
Definition: Console.cs:6
diff --git a/_administration_rights_8cs.html b/_administration_rights_8cs.html index 92c1d160b6..94e74c57dd 100644 --- a/_administration_rights_8cs.html +++ b/_administration_rights_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_administration_rights_8cs_source.html b/_administration_rights_8cs_source.html index 21d4d31b16..bb127a210e 100644 --- a/_administration_rights_8cs_source.html +++ b/_administration_rights_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_client_8cs.html b/_api_client_8cs.html index 37253e5768..f5e6798805 100644 --- a/_api_client_8cs.html +++ b/_api_client_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_client_8cs_source.html b/_api_client_8cs_source.html index ae6a905f9e..4783694adb 100644 --- a/_api_client_8cs_source.html +++ b/_api_client_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiClient.cs
-Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Newtonsoft.Json.Converters;
3 using Newtonsoft.Json.Serialization;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Net.Http;
9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Client
16 {
18  sealed class ApiClient : IApiClient
19  {
21  public Uri Url { get; }
22 
24  public ApiHeaders Headers
25  {
26  get => headers;
27  set => headers = value ?? throw new InvalidOperationException("Cannot set null headers!");
28  }
29 
31  public TimeSpan Timeout
32  {
33  get => httpClient.Timeout;
34  set => httpClient.Timeout = value;
35  }
36 
41 
45  readonly List<IRequestLogger> requestLoggers;
46 
51 
55  readonly SemaphoreSlim semaphoreSlim;
56 
61 
62  static JsonSerializerSettings GetSerializerSettings() => new JsonSerializerSettings
63  {
64  ContractResolver = new CamelCasePropertyNamesContractResolver(),
65  Converters = new[] { new VersionConverter() }
66  };
67 
68  static void HandleBadResponse(HttpResponseMessage response, string json)
69  {
70  ErrorMessage? errorMessage = null;
71  try
72  {
73  // check if json serializes to an error message
74  errorMessage = JsonConvert.DeserializeObject<ErrorMessage>(json, GetSerializerSettings());
75  }
76  catch (JsonException) { }
77 
78 #pragma warning disable IDE0010 // Add missing cases
79  switch (response.StatusCode)
80 #pragma warning restore IDE0010 // Add missing cases
81  {
82  case HttpStatusCode.UpgradeRequired:
83  throw new VersionMismatchException(errorMessage, response);
84  case HttpStatusCode.Unauthorized:
85  throw new UnauthorizedException(errorMessage, response);
86  case HttpStatusCode.InternalServerError:
87  throw new ServerErrorException(errorMessage, response);
88  case HttpStatusCode.NotImplemented:
89  // unprocessable entity
90  case (HttpStatusCode)422:
91  throw new MethodNotSupportedException(errorMessage, response);
92  case HttpStatusCode.NotFound:
93  case HttpStatusCode.Gone:
94  case HttpStatusCode.Conflict:
95  throw new ConflictException(errorMessage, response);
96  case HttpStatusCode.Forbidden:
97  throw new InsufficientPermissionsException(response);
98  case HttpStatusCode.ServiceUnavailable:
99  throw new ServiceUnavailableException(response);
100  case HttpStatusCode.RequestTimeout:
101  throw new RequestTimeoutException(response);
102  case (HttpStatusCode)429:
103  throw new RateLimitException(errorMessage, response);
104  default:
105  throw new ApiConflictException(errorMessage, response);
106  }
107  }
108 
116  public ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders)
117  {
118  this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
119  Url = url ?? throw new ArgumentNullException(nameof(url));
120  headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders));
121  this.tokenRefreshHeaders = tokenRefreshHeaders;
122 
123  requestLoggers = new List<IRequestLogger>();
124  semaphoreSlim = new SemaphoreSlim(1);
125  }
126 
128  public void Dispose()
129  {
130  httpClient.Dispose();
131  semaphoreSlim.Dispose();
132  }
133 
145  async Task<TResult> RunRequest<TResult>(string route, object? body, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
146  {
147  if (route == null)
148  throw new ArgumentNullException(nameof(route));
149  if (method == null)
150  throw new ArgumentNullException(nameof(method));
151  if (body == null && (method == HttpMethod.Post || method == HttpMethod.Put))
152  throw new InvalidOperationException("Body cannot be null for POST or PUT!");
153 
154  HttpResponseMessage response;
155  var fullUri = new Uri(Url, route);
156  var serializerSettings = GetSerializerSettings();
157  using (var request = new HttpRequestMessage(method, fullUri))
158  {
159  if (body != null)
160  request.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
161 
162  var headersToUse = tokenRefresh ? tokenRefreshHeaders! : headers;
163  headersToUse.SetRequestHeaders(request.Headers, instanceId);
164 
165  // This is meant to be a gate against token refresh operations
166  await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
167  if(!tokenRefresh)
168  semaphoreSlim.Release();
169 
170  try
171  {
172  await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false);
173 
174  response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
175  }
176  finally
177  {
178  if (tokenRefresh)
179  semaphoreSlim.Release();
180  }
181  }
182 
183  using (response)
184  {
185  await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false);
186 
187  var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
188 
189  if (!response.IsSuccessStatusCode)
190  {
191  if (!tokenRefresh
192  && response.StatusCode == HttpStatusCode.Unauthorized
193  && await RefreshToken(cancellationToken).ConfigureAwait(false))
194  return await RunRequest<TResult>(route, body, method, instanceId, false, cancellationToken).ConfigureAwait(false);
195  HandleBadResponse(response, json);
196  }
197 
198  if (String.IsNullOrWhiteSpace(json))
199  json = JsonConvert.SerializeObject(new object());
200 
201  try
202  {
203  return JsonConvert.DeserializeObject<TResult>(json, serializerSettings) !;
204  }
205  catch (JsonException)
206  {
207  throw new UnrecognizedResponseException(response);
208  }
209  }
210  }
211 
212  async Task<bool> RefreshToken(CancellationToken cancellationToken)
213  {
214  if (tokenRefreshHeaders == null)
215  return false;
216 
217  try
218  {
219  var token = await RunRequest<Token>(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken);
220  headers = new ApiHeaders(headers.UserAgent!, token.Bearer!);
221  }
222  catch (ClientException)
223  {
224  return false;
225  }
226 
227  return true;
228  }
229 
231  public Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, null, false, cancellationToken);
232 
234  public Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, null, false, cancellationToken);
235 
237  public Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Post, null, false, cancellationToken);
238 
240  public Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, null, false, cancellationToken);
241 
243  public Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Post, null, false, cancellationToken);
244 
246  public Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, null, false, cancellationToken);
247 
249  public Task Delete(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, null, false, cancellationToken);
250 
252  public Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, instanceId, false, cancellationToken);
253 
255  public Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, instanceId, false, cancellationToken);
256 
258  public Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, instanceId, false, cancellationToken);
259 
261  public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, instanceId, false, cancellationToken);
262 
264  public Task Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Delete, instanceId, false, cancellationToken);
265 
267  public Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Delete, instanceId, false, cancellationToken);
268 
270  public Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken);
271 
273  public Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Patch, instanceId, false, cancellationToken);
274 
276  public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger)));
277  }
278 }
Occurs when a response is received that did not deserialize to one of the expected Api...
+Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Newtonsoft.Json.Converters;
3 using Newtonsoft.Json.Serialization;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Net.Http;
9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Client
16 {
18  sealed class ApiClient : IApiClient
19  {
21  public Uri Url { get; }
22 
24  public ApiHeaders Headers
25  {
26  get => headers;
27  set => headers = value ?? throw new InvalidOperationException("Cannot set null headers!");
28  }
29 
31  public TimeSpan Timeout
32  {
33  get => httpClient.Timeout;
34  set => httpClient.Timeout = value;
35  }
36 
41 
45  readonly List<IRequestLogger> requestLoggers;
46 
51 
55  readonly SemaphoreSlim semaphoreSlim;
56 
61 
62  static JsonSerializerSettings GetSerializerSettings() => new JsonSerializerSettings
63  {
64  ContractResolver = new CamelCasePropertyNamesContractResolver(),
65  Converters = new[] { new VersionConverter() }
66  };
67 
68  static void HandleBadResponse(HttpResponseMessage response, string json)
69  {
70  ErrorMessage? errorMessage = null;
71  try
72  {
73  // check if json serializes to an error message
74  errorMessage = JsonConvert.DeserializeObject<ErrorMessage>(json, GetSerializerSettings());
75  }
76  catch (JsonException) { }
77 
78 #pragma warning disable IDE0010 // Add missing cases
79  switch (response.StatusCode)
80 #pragma warning restore IDE0010 // Add missing cases
81  {
82  case HttpStatusCode.UpgradeRequired:
83  throw new VersionMismatchException(errorMessage, response);
84  case HttpStatusCode.Unauthorized:
85  throw new UnauthorizedException(errorMessage, response);
86  case HttpStatusCode.InternalServerError:
87  throw new ServerErrorException(errorMessage, response);
88  case HttpStatusCode.NotImplemented:
89  // unprocessable entity
90  case (HttpStatusCode)422:
91  throw new MethodNotSupportedException(errorMessage, response);
92  case HttpStatusCode.NotFound:
93  case HttpStatusCode.Gone:
94  case HttpStatusCode.Conflict:
95  throw new ConflictException(errorMessage, response);
96  case HttpStatusCode.Forbidden:
97  throw new InsufficientPermissionsException(response);
98  case HttpStatusCode.ServiceUnavailable:
99  throw new ServiceUnavailableException(response);
100  case HttpStatusCode.RequestTimeout:
101  throw new RequestTimeoutException(response);
102  case (HttpStatusCode)429:
103  throw new RateLimitException(errorMessage, response);
104  default:
105  throw new ApiConflictException(errorMessage, response);
106  }
107  }
108 
116  public ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders)
117  {
118  this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
119  Url = url ?? throw new ArgumentNullException(nameof(url));
120  headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders));
121  this.tokenRefreshHeaders = tokenRefreshHeaders;
122 
123  requestLoggers = new List<IRequestLogger>();
124  semaphoreSlim = new SemaphoreSlim(1);
125  }
126 
128  public void Dispose()
129  {
130  httpClient.Dispose();
131  semaphoreSlim.Dispose();
132  }
133 
145  async Task<TResult> RunRequest<TResult>(string route, object? body, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
146  {
147  if (route == null)
148  throw new ArgumentNullException(nameof(route));
149  if (method == null)
150  throw new ArgumentNullException(nameof(method));
151  if (body == null && (method == HttpMethod.Post || method == HttpMethod.Put))
152  throw new InvalidOperationException("Body cannot be null for POST or PUT!");
153 
154  HttpResponseMessage response;
155  var fullUri = new Uri(Url, route);
156  var serializerSettings = GetSerializerSettings();
157  using (var request = new HttpRequestMessage(method, fullUri))
158  {
159  if (body != null)
160  request.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
161 
162  var headersToUse = tokenRefresh ? tokenRefreshHeaders! : headers;
163  headersToUse.SetRequestHeaders(request.Headers, instanceId);
164 
165  await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false);
166 
167  response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
168  }
169 
170  using (response)
171  {
172  await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false);
173 
174  var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
175 
176  if (!response.IsSuccessStatusCode)
177  {
178  if (!tokenRefresh
179  && response.StatusCode == HttpStatusCode.Unauthorized
180  && await RefreshToken(cancellationToken).ConfigureAwait(false))
181  return await RunRequest<TResult>(route, body, method, instanceId, false, cancellationToken).ConfigureAwait(false);
182  HandleBadResponse(response, json);
183  }
184 
185  if (String.IsNullOrWhiteSpace(json))
186  json = JsonConvert.SerializeObject(new object());
187 
188  try
189  {
190  return JsonConvert.DeserializeObject<TResult>(json, serializerSettings) !;
191  }
192  catch (JsonException)
193  {
194  throw new UnrecognizedResponseException(response);
195  }
196  }
197  }
198 
199  async Task<bool> RefreshToken(CancellationToken cancellationToken)
200  {
201  if (tokenRefreshHeaders == null)
202  return false;
203 
204  var startingToken = headers.Token;
205  await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
206  try
207  {
208  if (startingToken != headers.Token)
209  return true;
210 
211  var token = await RunRequest<Token>(Routes.Root, new object(), HttpMethod.Post, null, true, cancellationToken);
212  headers = new ApiHeaders(headers.UserAgent!, token.Bearer!);
213  }
214  catch (ClientException)
215  {
216  return false;
217  }
218  finally
219  {
220  semaphoreSlim.Release();
221  }
222 
223  return true;
224  }
225 
227  public Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, null, false, cancellationToken);
228 
230  public Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, null, false, cancellationToken);
231 
233  public Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Post, null, false, cancellationToken);
234 
236  public Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, null, false, cancellationToken);
237 
239  public Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Post, null, false, cancellationToken);
240 
242  public Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, null, false, cancellationToken);
243 
245  public Task Delete(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, null, false, cancellationToken);
246 
248  public Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, instanceId, false, cancellationToken);
249 
251  public Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, instanceId, false, cancellationToken);
252 
254  public Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, instanceId, false, cancellationToken);
255 
257  public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, instanceId, false, cancellationToken);
258 
260  public Task Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Delete, instanceId, false, cancellationToken);
261 
263  public Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Delete, instanceId, false, cancellationToken);
264 
266  public Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken);
267 
269  public Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Patch, instanceId, false, cancellationToken);
270 
272  public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger)));
273  }
274 }
Occurs when a response is received that did not deserialize to one of the expected Api...
@@ -113,7 +113,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly ApiHeaders tokenRefreshHeaders
Backing field for Headers
Definition: ApiClient.cs:50
For sending HTTP requests
Definition: IHttpClient.cs:11
-
async Task< bool > RefreshToken(CancellationToken cancellationToken)
Definition: ApiClient.cs:212
+
async Task< bool > RefreshToken(CancellationToken cancellationToken)
Definition: ApiClient.cs:199
const string Root
The root controller
Definition: Routes.cs:14
readonly List< IRequestLogger > requestLoggers
The IRequestLoggers used by the ApiClient
Definition: ApiClient.cs:45
readonly SemaphoreSlim semaphoreSlim
The SemaphoreSlim for Token refreshes.
Definition: ApiClient.cs:55
@@ -126,6 +126,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Web interface for the API
Definition: IApiClient.cs:11
ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders?tokenRefreshHeaders)
Construct an ApiClient
Definition: ApiClient.cs:116
+
string Token
The client&#39;s JWT
Definition: ApiHeaders.cs:87
Occurs when the client tries to use a currently unsupported API
readonly IHttpClient httpClient
The HttpClient for the ApiClient
Definition: ApiClient.cs:40
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
diff --git a/_api_client_factory_8cs.html b/_api_client_factory_8cs.html index bb6ca869bf..32059db5c7 100644 --- a/_api_client_factory_8cs.html +++ b/_api_client_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_client_factory_8cs_source.html b/_api_client_factory_8cs_source.html index 27201e1034..0b456f93a1 100644 --- a/_api_client_factory_8cs_source.html +++ b/_api_client_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_conflict_exception_8cs.html b/_api_conflict_exception_8cs.html index df477b8d70..a84b7ef856 100644 --- a/_api_conflict_exception_8cs.html +++ b/_api_conflict_exception_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_conflict_exception_8cs_source.html b/_api_conflict_exception_8cs_source.html index 973ed01f7c..f8c15f5e61 100644 --- a/_api_conflict_exception_8cs_source.html +++ b/_api_conflict_exception_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_controller_8cs.html b/_api_controller_8cs.html index 2bdd19cca5..40258a6ea5 100644 --- a/_api_controller_8cs.html +++ b/_api_controller_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_controller_8cs_source.html b/_api_controller_8cs_source.html index 18f7842875..31d530d19f 100644 --- a/_api_controller_8cs_source.html +++ b/_api_controller_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.AspNetCore.Mvc.Filters;
4 using Microsoft.Extensions.Logging;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Controllers
16 {
20  [Produces(ApiHeaders.ApplicationJson)]
21  [ApiController]
22  public abstract class ApiController : Controller
23  {
27  protected ApiHeaders ApiHeaders { get; private set; }
28 
32  protected IDatabaseContext DatabaseContext { get; }
33 
38 
42  protected ILogger Logger { get; }
43 
47  protected Models.Instance Instance { get; }
48 
52  readonly bool requireInstance;
53 
57  readonly bool requireHeaders;
58 
67  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
68  {
69  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
70  if (authenticationContextFactory == null)
71  throw new ArgumentNullException(nameof(authenticationContextFactory));
72  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
75  this.requireInstance = requireInstance;
76  this.requireHeaders = requireHeaders;
77  }
78 
80  #pragma warning disable CA1506 // TODO: Decomplexify
81  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
82  {
83  // ALL valid token and login requests that match a route go through this function
84  // 404 is returned before
85  if (AuthenticationContext != null && AuthenticationContext.User == null)
86  {
87  // valid token, expired password
88  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
89  return;
90  }
91 
92  // validate the headers
93  try
94  {
95  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
96 
97  if (!ApiHeaders.Compatible())
98  {
99  await StatusCode(
100  (int)HttpStatusCode.UpgradeRequired,
101  new ErrorMessage(ErrorCode.ApiMismatch))
102  .ExecuteResultAsync(context)
103  .ConfigureAwait(false);
104  return;
105  }
106 
107  if (requireInstance)
108  {
109  if (!ApiHeaders.InstanceId.HasValue)
110  {
111  await BadRequest(new ErrorMessage(ErrorCode.InstanceHeaderRequired)).ExecuteResultAsync(context).ConfigureAwait(false);
112  return;
113  }
114 
116  {
117  // accessing an instance they don't have access to or one that's disabled
118  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
119  return;
120  }
121  }
122  }
123  catch (InvalidOperationException e)
124  {
125  if (requireHeaders)
126  {
127  await BadRequest(
128  new ErrorMessage(ErrorCode.BadHeaders)
129  {
130  AdditionalData = e.Message
131  })
132  .ExecuteResultAsync(context)
133  .ConfigureAwait(false);
134  return;
135  }
136  }
137 
138  if (ModelState?.IsValid == false)
139  {
140  var errorMessages = ModelState
141  .SelectMany(x => x.Value.Errors)
142  .Select(x => x.ErrorMessage)
143 
144  // We use RequiredAttributes purely for preventing properties from becoming nullable in the databases
145  // We validate missing required fields in controllers
146  // Unfortunately, we can't remove the whole validator for that as it checks other things like StringLength
147  // This is the best way to deal with it unfortunately
148  .Where(x => !x.EndsWith(" field is required.", StringComparison.Ordinal));
149 
150  if (errorMessages.Any())
151  {
152  await BadRequest(
153  new ErrorMessage(ErrorCode.ModelValidationFailure)
154  {
155  AdditionalData = String.Join(Environment.NewLine, errorMessages)
156  })
157  .ExecuteResultAsync(context).ConfigureAwait(false);
158  return;
159  }
160 
161  ModelState.Clear();
162  }
163 
164  if (ApiHeaders != null)
165  Logger.LogDebug(
166  "Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}",
167  AuthenticationContext?.User.Id.Value.ToString(CultureInfo.InvariantCulture),
168  ApiHeaders.ApiVersion.Semver(),
170  Request.Method,
171  Request.Path,
172  Request.QueryString,
174 
175  try
176  {
177  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
178  }
179  catch (OperationCanceledException e)
180  {
181  Logger.LogDebug("Request cancelled! Exception: {0}", e);
182  throw;
183  }
184  }
185  #pragma warning restore CA1506
186  }
187 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.AspNetCore.Mvc.Filters;
4 using Microsoft.Extensions.Logging;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Controllers
16 {
20  [Produces(ApiHeaders.ApplicationJson)]
21  [ApiController]
22  public abstract class ApiController : Controller
23  {
27  protected ApiHeaders ApiHeaders { get; private set; }
28 
32  protected IDatabaseContext DatabaseContext { get; }
33 
38 
42  protected ILogger Logger { get; }
43 
47  protected Models.Instance Instance { get; }
48 
52  readonly bool requireInstance;
53 
57  readonly bool requireHeaders;
58 
67  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
68  {
69  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
70  if (authenticationContextFactory == null)
71  throw new ArgumentNullException(nameof(authenticationContextFactory));
72  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
75  this.requireInstance = requireInstance;
76  this.requireHeaders = requireHeaders;
77  }
78 
80  #pragma warning disable CA1506 // TODO: Decomplexify
81  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
82  {
83  // ALL valid token and login requests that match a route go through this function
84  // 404 is returned before
85  if (AuthenticationContext != null && AuthenticationContext.User == null)
86  {
87  // valid token, expired password
88  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
89  return;
90  }
91 
92  // validate the headers
93  try
94  {
95  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
96 
97  if (!ApiHeaders.Compatible())
98  {
99  await StatusCode(
100  (int)HttpStatusCode.UpgradeRequired,
101  new ErrorMessage(ErrorCode.ApiMismatch))
102  .ExecuteResultAsync(context)
103  .ConfigureAwait(false);
104  return;
105  }
106 
107  if (requireInstance)
108  {
109  if (!ApiHeaders.InstanceId.HasValue)
110  {
111  await BadRequest(new ErrorMessage(ErrorCode.InstanceHeaderRequired)).ExecuteResultAsync(context).ConfigureAwait(false);
112  return;
113  }
114 
116  {
117  // accessing an instance they don't have access to or one that's disabled
118  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
119  return;
120  }
121  }
122  }
123  catch (InvalidOperationException e)
124  {
125  if (requireHeaders)
126  {
127  await BadRequest(
128  new ErrorMessage(ErrorCode.BadHeaders)
129  {
130  AdditionalData = e.Message
131  })
132  .ExecuteResultAsync(context)
133  .ConfigureAwait(false);
134  return;
135  }
136  }
137 
138  if (ModelState?.IsValid == false)
139  {
140  var errorMessages = ModelState
141  .SelectMany(x => x.Value.Errors)
142  .Select(x => x.ErrorMessage)
143 
144  // We use RequiredAttributes purely for preventing properties from becoming nullable in the databases
145  // We validate missing required fields in controllers
146  // Unfortunately, we can't remove the whole validator for that as it checks other things like StringLength
147  // This is the best way to deal with it unfortunately
148  .Where(x => !x.EndsWith(" field is required.", StringComparison.Ordinal));
149 
150  if (errorMessages.Any())
151  {
152  await BadRequest(
153  new ErrorMessage(ErrorCode.ModelValidationFailure)
154  {
155  AdditionalData = String.Join(Environment.NewLine, errorMessages)
156  })
157  .ExecuteResultAsync(context).ConfigureAwait(false);
158  return;
159  }
160 
161  ModelState.Clear();
162  }
163 
164  if (ApiHeaders != null)
165  Logger.LogDebug(
166  "Request details: User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}",
167  AuthenticationContext?.User.Id.Value.ToString(CultureInfo.InvariantCulture),
168  ApiHeaders.ApiVersion.Semver(),
170  Request.Method,
171  Request.Path,
172  Request.QueryString,
174 
175  try
176  {
177  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
178  }
179  catch (OperationCanceledException e)
180  {
181  Logger.LogDebug("Request cancelled! Exception: {0}", e);
182  throw;
183  }
184  }
185  #pragma warning restore CA1506
186  }
187 }
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
Construct an ApiController
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
@@ -107,7 +107,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
bool Compatible()
Checks if the ApiVersion is compatible with Version
Represents the currently authenticated Api.Models.User
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:17
@@ -126,7 +126,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
- +
diff --git a/_api_exception_8cs.html b/_api_exception_8cs.html index 5c8363f27b..1d7164448d 100644 --- a/_api_exception_8cs.html +++ b/_api_exception_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_exception_8cs_source.html b/_api_exception_8cs_source.html index fd9eb18b6b..79db0fbcec 100644 --- a/_api_exception_8cs_source.html +++ b/_api_exception_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_headers_8cs.html b/_api_headers_8cs.html index dac47f83dd..70bbf5d045 100644 --- a/_api_headers_8cs.html +++ b/_api_headers_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_headers_8cs_source.html b/_api_headers_8cs_source.html index 788d29bc8d..f1977a34b4 100644 --- a/_api_headers_8cs_source.html +++ b/_api_headers_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_validation_status_8cs.html b/_api_validation_status_8cs.html index b9fcb70b43..d71f43839a 100644 --- a/_api_validation_status_8cs.html +++ b/_api_validation_status_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_api_validation_status_8cs_source.html b/_api_validation_status_8cs_source.html index 8a8fa1d81f..c77fc0802d 100644 --- a/_api_validation_status_8cs_source.html +++ b/_api_validation_status_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_application_8cs.html b/_application_8cs.html index 618fc9b2a9..1beebca9e3 100644 --- a/_application_8cs.html +++ b/_application_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_application_8cs_source.html b/_application_8cs_source.html index c0c4eced4b..9def760522 100644 --- a/_application_8cs_source.html +++ b/_application_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Application.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Cyberboss.AspNetCore.AsyncInitializer;
3 using Microsoft.AspNetCore.Authentication.JwtBearer;
4 using Microsoft.AspNetCore.Builder;
5 using Microsoft.AspNetCore.Cors.Infrastructure;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.AspNetCore.Identity;
8 using Microsoft.Extensions.Configuration;
9 using Microsoft.Extensions.DependencyInjection;
10 using Microsoft.Extensions.Hosting;
11 using Microsoft.Extensions.Logging;
12 using Microsoft.Extensions.Options;
13 using Microsoft.Extensions.Primitives;
14 using Newtonsoft.Json;
15 using Serilog;
16 using Serilog.Events;
17 using Serilog.Formatting.Display;
18 using System;
19 using System.Globalization;
20 using System.IdentityModel.Tokens.Jwt;
21 using System.Threading.Tasks;
22 using Tgstation.Server.Api;
35 using Tgstation.Server.Host.IO;
40 
41 namespace Tgstation.Server.Host.Core
42 {
46 #pragma warning disable CA1506
48  {
52  readonly IWebHostEnvironment hostingEnvironment;
53 
58 
64  public Application(
65  IConfiguration configuration,
66  IWebHostEnvironment hostingEnvironment)
67  : base(configuration)
68  {
69  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
70  }
71 
76  public static IServerFactory CreateDefaultServerFactory()
77  => new ServerFactory(
79  IOManager);
80 
86  public void ConfigureServices(IServiceCollection services, IPostSetupServices postSetupServices)
87  {
88  ConfigureServices(services);
89 
90  if (postSetupServices == null)
91  throw new ArgumentNullException(nameof(postSetupServices));
92 
93  // configure configuration
94  services.UseStandardConfig<UpdatesConfiguration>(Configuration);
95  services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
96 
97  // enable options which give us config reloading
98  services.AddOptions();
99 
100  static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) =>
101  logLevel switch
102  {
103  LogLevel.Critical => LogEventLevel.Fatal,
104  LogLevel.Debug => LogEventLevel.Debug,
105  LogLevel.Error => LogEventLevel.Error,
106  LogLevel.Information => LogEventLevel.Information,
107  LogLevel.Trace => LogEventLevel.Verbose,
108  LogLevel.Warning => LogEventLevel.Warning,
109  LogLevel.None => null,
110  _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel)),
111  };
112 
113  var microsoftEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.MicrosoftLogLevel);
114  services.SetupLogging(
115  config =>
116  {
117  if (microsoftEventLevel.HasValue)
118  config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
119  },
120  sinkConfig =>
121  {
122  if (postSetupServices.FileLoggingConfiguration.Disable)
123  return;
124 
125  // common app data is C:/ProgramData on windows, else /usr/share
126  var logPath = !String.IsNullOrEmpty(postSetupServices.FileLoggingConfiguration.Directory)
127  ? postSetupServices.FileLoggingConfiguration.Directory
128  : IOManager.ConcatPath(
129  Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
131  "Logs");
132 
133  var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
134 
135  var formatter = new MessageTemplateTextFormatter(
136  "{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
137  null);
138 
139  logPath = IOManager.ConcatPath(logPath, "tgs-{Date}.log");
140  var rollingFileConfig = sinkConfig.File(
141  formatter,
142  logPath,
143  logEventLevel ?? LogEventLevel.Verbose,
144  50 * 1024 * 1024, // 50MB max size
145  flushToDiskInterval: TimeSpan.FromSeconds(2),
146  rollingInterval: RollingInterval.Day,
147  rollOnFileSizeLimit: true);
148  });
149 
150  // configure bearer token validation
151  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
152  {
153  // this line isn't actually run until the first request is made
154  // at that point tokenFactory will be populated
155  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
156  jwtBearerOptions.Events = new JwtBearerEvents
157  {
158  // Application is our composition root so this monstrosity of a line is okay
159  // At least, that's what I tell myself to sleep at night
160  OnTokenValidated = ctx => ctx
161  .HttpContext
162  .RequestServices
163  .GetRequiredService<IClaimsInjector>()
164  .InjectClaimsIntoContext(
165  ctx,
166  ctx.HttpContext.RequestAborted)
167  };
168  });
169 
170  // fucking prevents converting 'sub' to M$ bs
171  // can't be done in the above lambda, that's too late
172  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
173 
174  // add mvc, configure the json serializer settings
175  services
176  .AddMvc(options =>
177  {
178  options.EnableEndpointRouting = false;
179  })
180  .AddNewtonsoftJson(options =>
181  {
182  options.AllowInputFormatterExceptionMessages = true;
183  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
184  options.SerializerSettings.CheckAdditionalContent = true;
185  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
186  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
187  options.SerializerSettings.Converters = new[] { new VersionConverter() };
188  });
189 
190  if (hostingEnvironment.IsDevelopment())
191  {
192  static string GetDocumentationFilePath(string assemblyLocation) => IOManager.ConcatPath(IOManager.GetDirectoryName(assemblyLocation), String.Concat(IOManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
193  var assemblyDocumentationPath = GetDocumentationFilePath(typeof(Application).Assembly.Location);
194  var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location);
195  services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath));
196  services.AddSwaggerGenNewtonsoftSupport();
197  }
198 
199  // enable browser detection
200  services.AddDetectionCore().AddBrowser();
201 
202  // CORS conditionally enabled later
203  services.AddCors();
204 
205  void AddTypedContext<TContext>() where TContext : DatabaseContext<TContext>
206  {
207  services.AddDbContext<TContext>(builder =>
208  {
209  if (hostingEnvironment.IsDevelopment())
210  builder.EnableSensitiveDataLogging();
211  });
212  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
213  }
214 
215  // add the correct database context type
216  var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
217  switch (dbType)
218  {
219  case DatabaseType.MySql:
220  case DatabaseType.MariaDB:
221  AddTypedContext<MySqlDatabaseContext>();
222  break;
223  case DatabaseType.SqlServer:
224  AddTypedContext<SqlServerDatabaseContext>();
225  break;
226  case DatabaseType.Sqlite:
227  AddTypedContext<SqliteDatabaseContext>();
228  break;
229  default:
230  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
231  }
232 
233  // configure other database services
234  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
235  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
236 
237  // configure security services
239  services.AddScoped<IClaimsInjector, ClaimsInjector>();
240  services.AddSingleton<IIdentityCache, IdentityCache>();
241  services.AddSingleton<ICryptographySuite, CryptographySuite>();
242  services.AddSingleton<ITokenFactory, TokenFactory>();
243  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
244 
245  // configure platform specific services
246  if (postSetupServices.PlatformIdentifier.IsWindows)
247  {
248  if (postSetupServices.GeneralConfiguration.UseBasicWatchdogOnWindows)
249  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
250  else
251  services.AddSingleton<IWatchdogFactory, WindowsWatchdogFactory>();
252 
253  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
254  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
255  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
256  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
257  services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
258 
259  services.AddSingleton<WindowsNetworkPromptReaper>();
260  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
261  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
262  }
263  else
264  {
265  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
266  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
267  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
268  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
269  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
270  services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
271  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
272  }
273 
274  // configure misc services
275  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
276  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
277  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
278  services.AddSingleton<IServerPortProvider, ServerPortProivder>();
279  services.AddSingleton<ITopicClient, TopicClient>();
280  services.AddSingleton(new SocketParameters
281  {
282  ReceiveTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
283  SendTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
284  ConnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
285  DisconnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout)
286  });
287 
288  // configure component services
289  services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
290  services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
291  services.AddSingleton<IProviderFactory, ProviderFactory>();
292  services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
293  services.AddSingleton<IInstanceFactory, InstanceFactory>();
294 
295  // configure root services
296  services.AddSingleton<IJobManager, JobManager>();
297 
298  services.AddSingleton<InstanceManager>();
299  services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
300  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
301  }
302 
304  protected override void ConfigureHostedService(IServiceCollection services)
305  => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
306 
317  public void Configure(
318  IApplicationBuilder applicationBuilder,
319  IServerControl serverControl,
320  ITokenFactory tokenFactory,
321  IInstanceManager instanceManager,
322  IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
323  IOptions<GeneralConfiguration> generalConfigurationOptions,
324  ILogger<Application> logger)
325  {
326  if (applicationBuilder == null)
327  throw new ArgumentNullException(nameof(applicationBuilder));
328  if (serverControl == null)
329  throw new ArgumentNullException(nameof(serverControl));
330 
331  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
332 
333  var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
334  var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
335 
336  if (logger == null)
337  throw new ArgumentNullException(nameof(logger));
338 
339  logger.LogDebug("Content Root: {0}", hostingEnvironment.ContentRootPath);
340  logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
341 
342  if (generalConfiguration.MinimumPasswordLength > Limits.MaximumStringLength)
343  {
344  logger.LogCritical("Configured minimum password length ({0}) is greater than the maximum database string length ({1})!");
345  serverControl.Die(new InvalidOperationException("Minimum password length greater than database limit!"));
346  return;
347  }
348 
349  // attempt to restart the server if the configuration changes
350  if (serverControl.WatchdogPresent)
351  ChangeToken.OnChange(Configuration.GetReloadToken, () => serverControl.Restart());
352 
353  // setup the HTTP request pipeline
354  // Final point where we wrap exceptions in a 500 (ErrorMessage) response
355  applicationBuilder.UseServerErrorHandling();
356 
357  // 503 requests made while the application is starting
358  applicationBuilder.UseAsyncInitialization(async (cancellationToken) =>
359  {
360  var tcs = new TaskCompletionSource<object>();
361  using (cancellationToken.Register(() => tcs.SetCanceled()))
362  await Task.WhenAny(tcs.Task, instanceManager.Ready).ConfigureAwait(false);
363  });
364 
365  // suppress OperationCancelledExceptions, they are just aborted HTTP requests
366  applicationBuilder.UseCancelledRequestSuppression();
367 
368  if (hostingEnvironment.IsDevelopment())
369  {
370  applicationBuilder.UseSwagger();
371  applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4"));
372  logger.LogTrace("Swagger API generation enabled");
373  }
374 
375  // Set up CORS based on configuration if necessary
376  Action<CorsPolicyBuilder> corsBuilder = null;
377  if (controlPanelConfiguration.AllowAnyOrigin)
378  {
379  logger.LogTrace("Access-Control-Allow-Origin: *");
380  corsBuilder = builder => builder.AllowAnyOrigin();
381  }
382  else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
383  {
384  logger.LogTrace("Access-Control-Allow-Origin: ", String.Join(',', controlPanelConfiguration.AllowedOrigins));
385  corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
386  }
387 
388  var originalBuilder = corsBuilder;
389  corsBuilder = builder =>
390  {
391  builder.AllowAnyHeader().AllowAnyMethod();
392  originalBuilder?.Invoke(builder);
393  };
394  applicationBuilder.UseCors(corsBuilder);
395 
396  // spa loading if necessary
397  if (controlPanelConfiguration.Enable)
398  {
399  logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
400  applicationBuilder.UseStaticFiles();
401  }
402  else
403  logger.LogDebug("Web control panel disabled!");
404 
405  // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
406  applicationBuilder.UseAuthentication();
407 
408  // suppress and log database exceptions
409  applicationBuilder.UseDbConflictHandling();
410 
411  // majority of handling is done in the controllers
412  applicationBuilder.UseMvc();
413 
414  // 404 anything that gets this far
415  }
416  }
417 }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Cyberboss.AspNetCore.AsyncInitializer;
3 using Microsoft.AspNetCore.Authentication.JwtBearer;
4 using Microsoft.AspNetCore.Builder;
5 using Microsoft.AspNetCore.Cors.Infrastructure;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.AspNetCore.Identity;
8 using Microsoft.Extensions.Configuration;
9 using Microsoft.Extensions.DependencyInjection;
10 using Microsoft.Extensions.Hosting;
11 using Microsoft.Extensions.Logging;
12 using Microsoft.Extensions.Options;
13 using Microsoft.Extensions.Primitives;
14 using Newtonsoft.Json;
15 using Serilog;
16 using Serilog.Events;
17 using Serilog.Formatting.Display;
18 using System;
19 using System.Globalization;
20 using System.IdentityModel.Tokens.Jwt;
21 using System.Threading.Tasks;
22 using Tgstation.Server.Api;
35 using Tgstation.Server.Host.IO;
40 
41 namespace Tgstation.Server.Host.Core
42 {
46 #pragma warning disable CA1506
48  {
52  readonly IWebHostEnvironment hostingEnvironment;
53 
58 
64  public Application(
65  IConfiguration configuration,
66  IWebHostEnvironment hostingEnvironment)
67  : base(configuration)
68  {
69  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
70  }
71 
76  public static IServerFactory CreateDefaultServerFactory()
77  => new ServerFactory(
79  IOManager);
80 
86  public void ConfigureServices(IServiceCollection services, IPostSetupServices postSetupServices)
87  {
88  ConfigureServices(services);
89 
90  if (postSetupServices == null)
91  throw new ArgumentNullException(nameof(postSetupServices));
92 
93  // configure configuration
94  services.UseStandardConfig<UpdatesConfiguration>(Configuration);
95  services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
96 
97  // enable options which give us config reloading
98  services.AddOptions();
99 
100  static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) =>
101  logLevel switch
102  {
103  LogLevel.Critical => LogEventLevel.Fatal,
104  LogLevel.Debug => LogEventLevel.Debug,
105  LogLevel.Error => LogEventLevel.Error,
106  LogLevel.Information => LogEventLevel.Information,
107  LogLevel.Trace => LogEventLevel.Verbose,
108  LogLevel.Warning => LogEventLevel.Warning,
109  LogLevel.None => null,
110  _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel)),
111  };
112 
113  var microsoftEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.MicrosoftLogLevel);
114  services.SetupLogging(
115  config =>
116  {
117  if (microsoftEventLevel.HasValue)
118  config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
119  },
120  sinkConfig =>
121  {
122  if (postSetupServices.FileLoggingConfiguration.Disable)
123  return;
124 
125  // common app data is C:/ProgramData on windows, else /usr/share
126  var logPath = !String.IsNullOrEmpty(postSetupServices.FileLoggingConfiguration.Directory)
127  ? postSetupServices.FileLoggingConfiguration.Directory
128  : IOManager.ConcatPath(
129  Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
131  "Logs");
132 
133  var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
134 
135  var formatter = new MessageTemplateTextFormatter(
136  "{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
137  null);
138 
139  logPath = IOManager.ConcatPath(logPath, "tgs-{Date}.log");
140  var rollingFileConfig = sinkConfig.File(
141  formatter,
142  logPath,
143  logEventLevel ?? LogEventLevel.Verbose,
144  50 * 1024 * 1024, // 50MB max size
145  flushToDiskInterval: TimeSpan.FromSeconds(2),
146  rollingInterval: RollingInterval.Day,
147  rollOnFileSizeLimit: true);
148  });
149 
150  // configure bearer token validation
151  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
152  {
153  // this line isn't actually run until the first request is made
154  // at that point tokenFactory will be populated
155  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
156  jwtBearerOptions.Events = new JwtBearerEvents
157  {
158  // Application is our composition root so this monstrosity of a line is okay
159  // At least, that's what I tell myself to sleep at night
160  OnTokenValidated = ctx => ctx
161  .HttpContext
162  .RequestServices
163  .GetRequiredService<IClaimsInjector>()
164  .InjectClaimsIntoContext(
165  ctx,
166  ctx.HttpContext.RequestAborted)
167  };
168  });
169 
170  // fucking prevents converting 'sub' to M$ bs
171  // can't be done in the above lambda, that's too late
172  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
173 
174  // add mvc, configure the json serializer settings
175  services
176  .AddMvc(options =>
177  {
178  options.EnableEndpointRouting = false;
179  })
180  .AddNewtonsoftJson(options =>
181  {
182  options.AllowInputFormatterExceptionMessages = true;
183  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
184  options.SerializerSettings.CheckAdditionalContent = true;
185  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
186  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
187  options.SerializerSettings.Converters = new[] { new VersionConverter() };
188  });
189 
190  if (hostingEnvironment.IsDevelopment())
191  {
192  static string GetDocumentationFilePath(string assemblyLocation) => IOManager.ConcatPath(IOManager.GetDirectoryName(assemblyLocation), String.Concat(IOManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
193  var assemblyDocumentationPath = GetDocumentationFilePath(typeof(Application).Assembly.Location);
194  var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location);
195  services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath));
196  services.AddSwaggerGenNewtonsoftSupport();
197  }
198 
199  // enable browser detection
200  services.AddDetectionCore().AddBrowser();
201 
202  // CORS conditionally enabled later
203  services.AddCors();
204 
205  void AddTypedContext<TContext>() where TContext : DatabaseContext
206  {
207  services.AddDbContext<TContext>(builder =>
208  {
209  if (hostingEnvironment.IsDevelopment())
210  builder.EnableSensitiveDataLogging();
211  });
212  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
213  }
214 
215  // add the correct database context type
216  var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
217  switch (dbType)
218  {
219  case DatabaseType.MySql:
220  case DatabaseType.MariaDB:
221  AddTypedContext<MySqlDatabaseContext>();
222  break;
223  case DatabaseType.SqlServer:
224  AddTypedContext<SqlServerDatabaseContext>();
225  break;
226  case DatabaseType.Sqlite:
227  AddTypedContext<SqliteDatabaseContext>();
228  break;
229  case DatabaseType.PostgresSql:
230  AddTypedContext<PostgresSqlDatabaseContext>();
231  break;
232  default:
233  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
234  }
235 
236  // configure other database services
237  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
238  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
239 
240  // configure security services
242  services.AddScoped<IClaimsInjector, ClaimsInjector>();
243  services.AddSingleton<IIdentityCache, IdentityCache>();
244  services.AddSingleton<ICryptographySuite, CryptographySuite>();
245  services.AddSingleton<ITokenFactory, TokenFactory>();
246  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
247 
248  // configure platform specific services
249  if (postSetupServices.PlatformIdentifier.IsWindows)
250  {
251  if (postSetupServices.GeneralConfiguration.UseBasicWatchdogOnWindows)
252  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
253  else
254  services.AddSingleton<IWatchdogFactory, WindowsWatchdogFactory>();
255 
256  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
257  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
258  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
259  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
260  services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
261 
262  services.AddSingleton<WindowsNetworkPromptReaper>();
263  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
264  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
265  }
266  else
267  {
268  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
269  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
270  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
271  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
272  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
273  services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
274  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
275  }
276 
277  // configure misc services
278  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
279  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
280  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
281  services.AddSingleton<IServerPortProvider, ServerPortProivder>();
282  services.AddSingleton<ITopicClient, TopicClient>();
283  services.AddSingleton(new SocketParameters
284  {
285  ReceiveTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
286  SendTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
287  ConnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
288  DisconnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout)
289  });
290 
291  // configure component services
292  services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
293  services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
294  services.AddSingleton<IProviderFactory, ProviderFactory>();
295  services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
296  services.AddSingleton<IInstanceFactory, InstanceFactory>();
297 
298  // configure root services
299  services.AddSingleton<IJobManager, JobManager>();
300 
301  services.AddSingleton<InstanceManager>();
302  services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
303  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
304  }
305 
307  protected override void ConfigureHostedService(IServiceCollection services)
308  => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
309 
320  public void Configure(
321  IApplicationBuilder applicationBuilder,
322  IServerControl serverControl,
323  ITokenFactory tokenFactory,
324  IInstanceManager instanceManager,
325  IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
326  IOptions<GeneralConfiguration> generalConfigurationOptions,
327  ILogger<Application> logger)
328  {
329  if (applicationBuilder == null)
330  throw new ArgumentNullException(nameof(applicationBuilder));
331  if (serverControl == null)
332  throw new ArgumentNullException(nameof(serverControl));
333 
334  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
335 
336  var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
337  var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
338 
339  if (logger == null)
340  throw new ArgumentNullException(nameof(logger));
341 
342  logger.LogDebug("Content Root: {0}", hostingEnvironment.ContentRootPath);
343  logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
344 
345  if (generalConfiguration.MinimumPasswordLength > Limits.MaximumStringLength)
346  {
347  logger.LogCritical("Configured minimum password length ({0}) is greater than the maximum database string length ({1})!");
348  serverControl.Die(new InvalidOperationException("Minimum password length greater than database limit!"));
349  return;
350  }
351 
352  // attempt to restart the server if the configuration changes
353  if (serverControl.WatchdogPresent)
354  ChangeToken.OnChange(Configuration.GetReloadToken, () => serverControl.Restart());
355 
356  // setup the HTTP request pipeline
357  // Final point where we wrap exceptions in a 500 (ErrorMessage) response
358  applicationBuilder.UseServerErrorHandling();
359 
360  applicationBuilder.UseRequestCounting();
361 
362  // 503 requests made while the application is starting
363  applicationBuilder.UseAsyncInitialization(async (cancellationToken) =>
364  {
365  var tcs = new TaskCompletionSource<object>();
366  using (cancellationToken.Register(() => tcs.SetCanceled()))
367  await Task.WhenAny(tcs.Task, instanceManager.Ready).ConfigureAwait(false);
368  });
369 
370  // suppress OperationCancelledExceptions, they are just aborted HTTP requests
371  applicationBuilder.UseCancelledRequestSuppression();
372 
373  if (hostingEnvironment.IsDevelopment())
374  {
375  applicationBuilder.UseSwagger();
376  applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4"));
377  logger.LogTrace("Swagger API generation enabled");
378  }
379 
380  // Set up CORS based on configuration if necessary
381  Action<CorsPolicyBuilder> corsBuilder = null;
382  if (controlPanelConfiguration.AllowAnyOrigin)
383  {
384  logger.LogTrace("Access-Control-Allow-Origin: *");
385  corsBuilder = builder => builder.AllowAnyOrigin();
386  }
387  else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
388  {
389  logger.LogTrace("Access-Control-Allow-Origin: ", String.Join(',', controlPanelConfiguration.AllowedOrigins));
390  corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
391  }
392 
393  var originalBuilder = corsBuilder;
394  corsBuilder = builder =>
395  {
396  builder.AllowAnyHeader().AllowAnyMethod();
397  originalBuilder?.Invoke(builder);
398  };
399  applicationBuilder.UseCors(corsBuilder);
400 
401  // spa loading if necessary
402  if (controlPanelConfiguration.Enable)
403  {
404  logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
405  applicationBuilder.UseStaticFiles();
406  }
407  else
408  logger.LogDebug("Web control panel disabled!");
409 
410  // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
411  applicationBuilder.UseAuthentication();
412 
413  // suppress and log database exceptions
414  applicationBuilder.UseDbConflictHandling();
415 
416  // majority of handling is done in the controllers
417  applicationBuilder.UseMvc();
418 
419  // 404 anything that gets this far
420  }
421  }
422 }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
@@ -131,7 +131,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Sets up dependency injection.
Definition: Application.cs:47
Implements various filters for Swashbuckle.
-
Implementation of IServerFactory.
+
Implementation of IServerFactory.
ISystemIdentityFactory for windows systems. Uses long running tasks due to potential networked domain...
@@ -139,7 +139,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Contains various cryptographic functions
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
Application(IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
Construct an Application
Definition: Application.cs:64
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:17
@@ -148,9 +148,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseConfiguration DatabaseConfiguration
The Configuration.DatabaseConfiguration.
Configuration for the automatic update system
- +
For downloading and installing BYOND extractions for a given system
-
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IInstanceManager instanceManager, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:317
+
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IInstanceManager instanceManager, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:320
For accessing the disk in a synchronous manner
DatabaseType DatabaseType
The Configuration.DatabaseType to create
@@ -163,7 +163,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
bool UseBasicWatchdogOnWindows
If the Components.Watchdog.WindowsWatchdog should not be used if it is available. ...
+
bool UseBasicWatchdogOnWindows
If the Components.Watchdog.WindowsWatchdog should not be used if it is available. ...
IPostWriteHandler for Windows systems
DI root for configuring a SetupWizard.
@@ -189,7 +189,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Manages the runtime of Jobs
Definition: IJobManager.cs:13
-
int ByondTopicTimeout
The timeout in milliseconds for sending and receiving topics to/from DreamDaemon. Note that a single ...
+
int ByondTopicTimeout
The timeout in milliseconds for sending and receiving topics to/from DreamDaemon. Note that a single ...
On Windows, DreamDaemon will show an unskippable prompt when using /world/proc/OpenPort(). This looks out for those prompts and immediately clicks "Yes" if the owning process has registered for it
@@ -211,7 +211,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_application_builder_extensions_8cs.html b/_application_builder_extensions_8cs.html index ad1f64fb11..e4e3bde21d 100644 --- a/_application_builder_extensions_8cs.html +++ b/_application_builder_extensions_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_application_builder_extensions_8cs_source.html b/_application_builder_extensions_8cs_source.html index 6ff4171d83..f2aa0e09b1 100644 --- a/_application_builder_extensions_8cs_source.html +++ b/_application_builder_extensions_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,11 +94,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApplicationBuilderExtensions.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Builder;
2 using Microsoft.AspNetCore.Http;
3 using Microsoft.AspNetCore.Mvc;
4 using Microsoft.EntityFrameworkCore;
5 using Microsoft.Extensions.DependencyInjection;
6 using Microsoft.Extensions.Logging;
7 using System;
8 using System.Globalization;
9 using System.Net;
11 
12 namespace Tgstation.Server.Host.Extensions
13 {
18  {
24  static ILogger GetLogger(HttpContext httpContext) => httpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(typeof(ApplicationBuilderExtensions));
25 
30  public static void UseDbConflictHandling(this IApplicationBuilder applicationBuilder)
31  {
32  if (applicationBuilder == null)
33  throw new ArgumentNullException(nameof(applicationBuilder));
34  applicationBuilder.Use(async (context, next) =>
35  {
36  var logger = GetLogger(context);
37  try
38  {
39  await next().ConfigureAwait(false);
40  }
41  catch (DbUpdateException e)
42  {
43  logger.LogDebug("Database conflict: {0}", e.Message);
44  await new ConflictObjectResult(new ErrorMessage(ErrorCode.DatabaseIntegrityConflict)
45  {
46  AdditionalData = String.Format(CultureInfo.InvariantCulture, (e.InnerException ?? e).Message)
47  }).ExecuteResultAsync(new ActionContext
48  {
49  HttpContext = context
50  }).ConfigureAwait(false);
51  }
52  });
53  }
54 
59  public static void UseCancelledRequestSuppression(this IApplicationBuilder applicationBuilder)
60  {
61  if (applicationBuilder == null)
62  throw new ArgumentNullException(nameof(applicationBuilder));
63  applicationBuilder.Use(async (context, next) =>
64  {
65  var logger = GetLogger(context);
66  try
67  {
68  await next().ConfigureAwait(false);
69  }
70  catch (OperationCanceledException)
71  {
72  logger.LogDebug("Request cancelled!");
73  }
74  });
75  }
76 
81  public static void UseServerErrorHandling(this IApplicationBuilder applicationBuilder)
82  {
83  if (applicationBuilder == null)
84  throw new ArgumentNullException(nameof(applicationBuilder));
85  applicationBuilder.Use(async (context, next) =>
86  {
87  var logger = GetLogger(context);
88  try
89  {
90  await next().ConfigureAwait(false);
91  }
92  catch (Exception e)
93  {
94  logger.LogError("Failed request: {0}", e);
95  await new ObjectResult(
96  new ErrorMessage(ErrorCode.InternalServerError)
97  {
98  AdditionalData = e.ToString()
99  })
100  {
101  StatusCode = (int)HttpStatusCode.InternalServerError
102  }
103  .ExecuteResultAsync(new ActionContext
104  {
105  HttpContext = context
106  }).ConfigureAwait(false);
107  }
108  });
109  }
110  }
111 }
static void UseDbConflictHandling(this IApplicationBuilder applicationBuilder)
Return a ConflictObjectResult for DbUpdateExceptions
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Builder;
2 using Microsoft.AspNetCore.Http;
3 using Microsoft.AspNetCore.Mvc;
4 using Microsoft.EntityFrameworkCore;
5 using Microsoft.Extensions.DependencyInjection;
6 using Microsoft.Extensions.Logging;
7 using System;
8 using System.Globalization;
9 using System.Net;
11 
12 namespace Tgstation.Server.Host.Extensions
13 {
18  {
24  static ILogger GetLogger(HttpContext httpContext) => httpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger(typeof(ApplicationBuilderExtensions));
25 
30  public static void UseDbConflictHandling(this IApplicationBuilder applicationBuilder)
31  {
32  if (applicationBuilder == null)
33  throw new ArgumentNullException(nameof(applicationBuilder));
34  applicationBuilder.Use(async (context, next) =>
35  {
36  var logger = GetLogger(context);
37  try
38  {
39  await next().ConfigureAwait(false);
40  }
41  catch (DbUpdateException e)
42  {
43  logger.LogDebug("Database conflict: {0}", e.Message);
44  await new ConflictObjectResult(new ErrorMessage(ErrorCode.DatabaseIntegrityConflict)
45  {
46  AdditionalData = String.Format(CultureInfo.InvariantCulture, (e.InnerException ?? e).Message)
47  }).ExecuteResultAsync(new ActionContext
48  {
49  HttpContext = context
50  }).ConfigureAwait(false);
51  }
52  });
53  }
54 
59  public static void UseCancelledRequestSuppression(this IApplicationBuilder applicationBuilder)
60  {
61  if (applicationBuilder == null)
62  throw new ArgumentNullException(nameof(applicationBuilder));
63  applicationBuilder.Use(async (context, next) =>
64  {
65  var logger = GetLogger(context);
66  try
67  {
68  await next().ConfigureAwait(false);
69  }
70  catch (OperationCanceledException)
71  {
72  logger.LogDebug("Request cancelled!");
73  }
74  });
75  }
76 
81  public static void UseServerErrorHandling(this IApplicationBuilder applicationBuilder)
82  {
83  if (applicationBuilder == null)
84  throw new ArgumentNullException(nameof(applicationBuilder));
85  applicationBuilder.Use(async (context, next) =>
86  {
87  var logger = GetLogger(context);
88  try
89  {
90  await next().ConfigureAwait(false);
91  }
92  catch (Exception e)
93  {
94  logger.LogError("Failed request: {0}", e);
95  await new ObjectResult(
96  new ErrorMessage(ErrorCode.InternalServerError)
97  {
98  AdditionalData = e.ToString()
99  })
100  {
101  StatusCode = (int)HttpStatusCode.InternalServerError
102  }
103  .ExecuteResultAsync(new ActionContext
104  {
105  HttpContext = context
106  }).ConfigureAwait(false);
107  }
108  });
109  }
110 
115  public static void UseRequestCounting(this IApplicationBuilder applicationBuilder)
116  {
117  if (applicationBuilder == null)
118  throw new ArgumentNullException(nameof(applicationBuilder));
119 
120  ulong requestCounter = 0;
121 
122  applicationBuilder.Use(async (context, next) =>
123  {
124  var logger = GetLogger(context);
125  var requestNumber = ++requestCounter;
126  logger.LogTrace("Starting request #{0}...", requestNumber);
127  try
128  {
129  await next().ConfigureAwait(false);
130  }
131  finally
132  {
133  logger.LogTrace("Finished request #{0}", requestNumber);
134  }
135  });
136  }
137  }
138 }
static void UseDbConflictHandling(this IApplicationBuilder applicationBuilder)
Return a ConflictObjectResult for DbUpdateExceptions
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
+
static void UseRequestCounting(this IApplicationBuilder applicationBuilder)
Add middleware for logging the request number.
static void UseServerErrorHandling(this IApplicationBuilder applicationBuilder)
Suppress all in flight exceptions with error 500.
static void UseCancelledRequestSuppression(this IApplicationBuilder applicationBuilder)
Suppress TaskCanceledException warnings when a user aborts a request
diff --git a/_architecture_8dox.html b/_architecture_8dox.html index 4a9b6e6e7a..256b91705e 100644 --- a/_architecture_8dox.html +++ b/_architecture_8dox.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_assembly_information_provider_8cs.html b/_assembly_information_provider_8cs.html index 888c1dacdb..f9474f6fb9 100644 --- a/_assembly_information_provider_8cs.html +++ b/_assembly_information_provider_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_assembly_information_provider_8cs_source.html b/_assembly_information_provider_8cs_source.html index 5e1f820e21..2ba2113897 100644 --- a/_assembly_information_provider_8cs_source.html +++ b/_assembly_information_provider_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_async_delayer_8cs.html b/_async_delayer_8cs.html index 446ce3c13d..0d7e5cfce3 100644 --- a/_async_delayer_8cs.html +++ b/_async_delayer_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_async_delayer_8cs_source.html b/_async_delayer_8cs_source.html index 5f0ea4f355..e2e146cf2b 100644 --- a/_async_delayer_8cs_source.html +++ b/_async_delayer_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_authentication_context_8cs.html b/_authentication_context_8cs.html index 309aa26304..0a755a462d 100644 --- a/_authentication_context_8cs.html +++ b/_authentication_context_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_authentication_context_8cs_source.html b/_authentication_context_8cs_source.html index 356b8c8c40..0d790d8a19 100644 --- a/_authentication_context_8cs_source.html +++ b/_authentication_context_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_authentication_context_factory_8cs.html b/_authentication_context_factory_8cs.html index 901e47862c..d896259500 100644 --- a/_authentication_context_factory_8cs.html +++ b/_authentication_context_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_authentication_context_factory_8cs_source.html b/_authentication_context_factory_8cs_source.html index 7972804356..89523d0685 100644 --- a/_authentication_context_factory_8cs_source.html +++ b/_authentication_context_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
AuthenticationContextFactory.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Linq;
5 using System.Threading;
6 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Security
11 {
14  {
16  public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
17 
22 
27 
31  readonly ILogger<AuthenticationContextFactory> logger;
32 
40  IDatabaseContext databaseContext,
41  IIdentityCache identityCache,
42  ILogger<AuthenticationContextFactory> logger)
43  {
44  this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
45  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
46  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
47  }
48 
50  public void Dispose() => CurrentAuthenticationContext?.Dispose();
51 
53  public async Task CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset validAfter, CancellationToken cancellationToken)
54  {
55  if (CurrentAuthenticationContext != null)
56  throw new InvalidOperationException("Authentication context has already been loaded");
57 
58  var user = await databaseContext.Users.Where(x => x.Id == userId)
59  .Include(x => x.CreatedBy)
60  .FirstOrDefaultAsync(cancellationToken)
61  .ConfigureAwait(false);
62  if (user == default)
63  {
64  CurrentAuthenticationContext = new AuthenticationContext();
65  return;
66  }
67 
68  ISystemIdentity systemIdentity;
69  if (user.SystemIdentifier != null)
70  systemIdentity = identityCache.LoadCachedIdentity(user);
71  else
72  {
73  if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > validAfter)
74  {
75  CurrentAuthenticationContext = new AuthenticationContext();
76  return;
77  }
78 
79  systemIdentity = null;
80  }
81 
82  try
83  {
84  InstanceUser instanceUser = null;
85  if (instanceId.HasValue)
86  {
87  instanceUser = await databaseContext.InstanceUsers
88  .Where(x => x.UserId == userId && x.InstanceId == instanceId && x.Instance.Online.Value)
89  .Include(x => x.Instance)
90  .FirstOrDefaultAsync(cancellationToken)
91  .ConfigureAwait(false);
92 
93  if (instanceUser == null)
94  logger.LogDebug("User {0} does not have permissions on instance {1}!", userId, instanceId.Value);
95  }
96 
97  CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instanceUser);
98  }
99  catch
100  {
101  systemIdentity?.Dispose();
102  throw;
103  }
104  }
105  }
106 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Linq;
5 using System.Threading;
6 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Security
11 {
14  {
16  public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
17 
22 
27 
31  readonly ILogger<AuthenticationContextFactory> logger;
32 
40  IDatabaseContext databaseContext,
41  IIdentityCache identityCache,
42  ILogger<AuthenticationContextFactory> logger)
43  {
44  this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
45  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
46  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
47  }
48 
50  public void Dispose() => CurrentAuthenticationContext?.Dispose();
51 
53  public async Task CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset validAfter, CancellationToken cancellationToken)
54  {
55  if (CurrentAuthenticationContext != null)
56  throw new InvalidOperationException("Authentication context has already been loaded");
57 
58  var user = await databaseContext
59  .Users
60  .AsQueryable()
61  .Where(x => x.Id == userId)
62  .Include(x => x.CreatedBy)
63  .FirstOrDefaultAsync(cancellationToken)
64  .ConfigureAwait(false);
65  if (user == default)
66  {
67  CurrentAuthenticationContext = new AuthenticationContext();
68  return;
69  }
70 
71  ISystemIdentity systemIdentity;
72  if (user.SystemIdentifier != null)
73  systemIdentity = identityCache.LoadCachedIdentity(user);
74  else
75  {
76  if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > validAfter)
77  {
78  CurrentAuthenticationContext = new AuthenticationContext();
79  return;
80  }
81 
82  systemIdentity = null;
83  }
84 
85  try
86  {
87  InstanceUser instanceUser = null;
88  if (instanceId.HasValue)
89  {
90  instanceUser = await databaseContext.InstanceUsers
91  .AsQueryable()
92  .Where(x => x.UserId == userId && x.InstanceId == instanceId && x.Instance.Online.Value)
93  .Include(x => x.Instance)
94  .FirstOrDefaultAsync(cancellationToken)
95  .ConfigureAwait(false);
96 
97  if (instanceUser == null)
98  logger.LogDebug("User {0} does not have permissions on instance {1}!", userId, instanceId.Value);
99  }
100 
101  CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instanceUser);
102  }
103  catch
104  {
105  systemIdentity?.Dispose();
106  throw;
107  }
108  }
109  }
110 }
Represents a user on the current global::System.Runtime.InteropServices.OSPlatform ...
@@ -112,7 +112,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
AuthenticationContextFactory(IDatabaseContext databaseContext, IIdentityCache identityCache, ILogger< AuthenticationContextFactory > logger)
Construct an AuthenticationContextFactory
- +
diff --git a/_basic_watchdog_8cs.html b/_basic_watchdog_8cs.html index 29007c996f..9bf7c14e08 100644 --- a/_basic_watchdog_8cs.html +++ b/_basic_watchdog_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_basic_watchdog_8cs_source.html b/_basic_watchdog_8cs_source.html index e38e14c9ed..ef937af8de 100644 --- a/_basic_watchdog_8cs_source.html +++ b/_basic_watchdog_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
BasicWatchdog.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Threading;
6 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Components.Watchdog
16 {
21  {
23  public sealed override bool AlphaIsActive => true;
24 
26  public sealed override Models.CompileJob ActiveCompileJob => Server?.Dmb.CompileJob;
27 
29  public sealed override RebootState? RebootState => Server?.RebootState;
30 
34  protected ISessionController Server { get; private set; }
35 
40 
56  public BasicWatchdog(
57  IChatManager chat,
58  ISessionControllerFactory sessionControllerFactory,
59  IDmbFactory dmbFactory,
60  IReattachInfoHandler reattachInfoHandler,
61  IDatabaseContextFactory databaseContextFactory,
62  IJobManager jobManager,
63  IServerControl serverControl,
64  IAsyncDelayer asyncDelayer,
65  ILogger<BasicWatchdog> logger,
66  DreamDaemonLaunchParameters initialLaunchParameters,
67  Api.Models.Instance instance,
68  bool autoStart)
69  : base(
70  chat,
71  sessionControllerFactory,
72  dmbFactory,
73  reattachInfoHandler,
74  databaseContextFactory,
75  jobManager,
76  serverControl,
77  asyncDelayer,
78  logger,
79  initialLaunchParameters,
80  instance,
81  autoStart)
82  { }
83 
85  protected override IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState)
86  {
87  if (Server == null)
88  return null;
89 
90  monitorState.ActiveServer = Server;
91 
92  return new Dictionary<MonitorActivationReason, Task>
93  {
94  { MonitorActivationReason.ActiveServerCrashed, Server.Lifetime },
95  { MonitorActivationReason.ActiveServerRebooted, Server.OnReboot },
96  { MonitorActivationReason.InactiveServerCrashed, Extensions.TaskExtensions.InfiniteTask() },
97  { MonitorActivationReason.InactiveServerRebooted, Extensions.TaskExtensions.InfiniteTask() },
98  { MonitorActivationReason.InactiveServerStartupComplete, Extensions.TaskExtensions.InfiniteTask() }
99  };
100  }
101 
103  protected override async Task HandleMonitorWakeup(MonitorActivationReason reason, MonitorState monitorState, CancellationToken cancellationToken)
104  {
105  switch (reason)
106  {
107  case MonitorActivationReason.ActiveServerCrashed:
108  string exitWord = Server.TerminationWasRequested ? "exited" : "crashed";
109  if (Server.RebootState == Session.RebootState.Shutdown)
110  {
111  // the time for graceful shutdown is now
112  await Chat.SendWatchdogMessage(
113  String.Format(
114  CultureInfo.InvariantCulture,
115  "Server {0}! Shutting down due to graceful termination request...",
116  exitWord),
117  false,
118  cancellationToken)
119  .ConfigureAwait(false);
120  monitorState.NextAction = MonitorAction.Exit;
121  }
122  else
123  {
124  await Chat.SendWatchdogMessage(
125  String.Format(
126  CultureInfo.InvariantCulture,
127  "Server {0}! Rebooting...",
128  exitWord),
129  false,
130  cancellationToken)
131  .ConfigureAwait(false);
132  monitorState.NextAction = MonitorAction.Restart;
133  }
134 
135  break;
136  case MonitorActivationReason.ActiveServerRebooted:
137  var rebootState = Server.RebootState;
138  if (gracefulRebootRequired && rebootState == Session.RebootState.Normal)
139  {
140  Logger.LogError("Watchdog reached normal reboot state with gracefulRebootRequired set!");
141  rebootState = Session.RebootState.Restart;
142  }
143 
144  gracefulRebootRequired = false;
145  Server.ResetRebootState();
146 
147  switch (rebootState)
148  {
149  case Session.RebootState.Normal:
150  monitorState.NextAction = HandleNormalReboot();
151  break;
152  case Session.RebootState.Restart:
153  monitorState.NextAction = MonitorAction.Restart;
154  break;
155  case Session.RebootState.Shutdown:
156  // graceful shutdown time
157  await Chat.SendWatchdogMessage(
158  "Active server rebooted! Shutting down due to graceful termination request...",
159  false,
160  cancellationToken)
161  .ConfigureAwait(false);
162  monitorState.NextAction = MonitorAction.Exit;
163  break;
164  default:
165  throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
166  }
167 
168  break;
169  case MonitorActivationReason.ActiveLaunchParametersUpdated:
170  await Server.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false);
171  gracefulRebootRequired = true;
172  break;
173  case MonitorActivationReason.NewDmbAvailable:
174  await HandleNewDmbAvailable(cancellationToken).ConfigureAwait(false);
175  break;
176  case MonitorActivationReason.InactiveServerCrashed:
177  case MonitorActivationReason.InactiveServerRebooted:
178  case MonitorActivationReason.InactiveServerStartupComplete:
179  throw new NotSupportedException($"Unsupported activation reason: {reason}");
180  case MonitorActivationReason.Heartbeat:
181  default:
182  throw new InvalidOperationException($"Invalid activation reason: {reason}");
183  }
184  }
185 
187  protected sealed override DualReattachInformation CreateReattachInformation()
189  {
190  AlphaIsActive = true,
191  Alpha = Server?.Release()
192  };
193 
195  protected override void DisposeAndNullControllers()
196  {
197  Server?.Dispose();
198  Server = null;
199  Running = false;
200  gracefulRebootRequired = false;
201  }
202 
204  protected sealed override ISessionController GetActiveController() => Server;
205 
207  protected sealed override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
208  {
209  var serverToReattach = reattachInfo?.Alpha ?? reattachInfo?.Bravo;
210  var serverToKill = reattachInfo?.Bravo ?? reattachInfo?.Alpha;
211 
212  // vice versa
213  if (serverToKill == serverToReattach)
214  serverToKill = null;
215 
216  if (reattachInfo?.AlphaIsActive == false)
217  {
218  var temp = serverToReattach;
219  serverToReattach = serverToKill;
220  serverToKill = temp;
221  }
222 
223  // don't need a new dmb if reattaching
224  var doesntNeedNewDmb = serverToReattach != null;
225  var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(1);
226 
227  // if this try catches something, both servers are killed
228  bool inactiveServerWasKilled = false;
229  try
230  {
231  // start the alpha server task, either by launch a new process or attaching to an existing one
232  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
233  // The tasks pertaining to server startup times are in the ISessionControllers
234  Task<ISessionController> serverLaunchTask, inactiveReattachTask;
235  if (!doesntNeedNewDmb)
236  {
237  dmbToUse = await PrepServerForLaunch(dmbToUse, cancellationToken).ConfigureAwait(false);
238  serverLaunchTask = SessionControllerFactory.LaunchNew(
239  dmbToUse,
240  null,
241  ActiveLaunchParameters,
242  true,
243  true,
244  false,
245  cancellationToken);
246  }
247  else
248  serverLaunchTask = SessionControllerFactory.Reattach(serverToReattach, cancellationToken);
249 
250  bool thereIsAnInactiveServerToKill = serverToKill != null;
251  if (thereIsAnInactiveServerToKill)
252  inactiveReattachTask = SessionControllerFactory.Reattach(serverToKill, cancellationToken);
253  else
254  inactiveReattachTask = Task.FromResult<ISessionController>(null);
255 
256  // retrieve the session controller
257  Server = await serverLaunchTask.ConfigureAwait(false);
258 
259  // failed reattaches will return null
260  Server?.SetHighPriority();
261 
262  var inactiveServerController = await inactiveReattachTask.ConfigureAwait(false);
263  inactiveServerController?.Dispose();
264  inactiveServerWasKilled = inactiveServerController != null;
265 
266  // possiblity of null servers due to failed reattaches
267  if (Server == null)
268  {
269  callBeforeRecurse();
270  await NotifyOfFailedReattach(thereIsAnInactiveServerToKill && !inactiveServerWasKilled, cancellationToken).ConfigureAwait(false);
271  return;
272  }
273 
274  await CheckLaunchResult(Server, "Server", cancellationToken).ConfigureAwait(false);
275 
276  Server.EnableCustomChatCommands();
277  }
278  catch
279  {
280  // kill the controllers
281  bool serverWasActive = Server != null;
282  DisposeAndNullControllers();
283 
284  // server didn't get control of this dmb
285  if (dmbToUse != null && !serverWasActive)
286  dmbToUse.Dispose();
287 
288  if (serverToKill != null && !inactiveServerWasKilled)
289  serverToKill.Dmb.Dispose();
290  throw;
291  }
292  }
293 
298  protected virtual MonitorAction HandleNormalReboot()
299  {
300  bool dmbUpdatePending = ActiveLaunchParameters != LastLaunchParameters;
301  return dmbUpdatePending ? MonitorAction.Restart : MonitorAction.Continue;
302  }
303 
309  protected virtual Task HandleNewDmbAvailable(CancellationToken cancellationToken)
310  {
311  gracefulRebootRequired = true;
312  return Server.SetRebootState(Session.RebootState.Restart, cancellationToken);
313  }
314 
321  protected virtual Task<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken) => Task.FromResult(dmbToUse);
322 
324  public override Task ResetRebootState(CancellationToken cancellationToken)
325  {
326  if (!gracefulRebootRequired)
327  return base.ResetRebootState(cancellationToken);
328 
329  return Restart(true, cancellationToken);
330  }
331  }
332 }
BasicWatchdog(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger< BasicWatchdog > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the BasicWatchdog .
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Threading;
6 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Components.Watchdog
16 {
21  {
23  public sealed override bool AlphaIsActive => true;
24 
26  public sealed override Models.CompileJob ActiveCompileJob => Server?.Dmb.CompileJob;
27 
29  public sealed override RebootState? RebootState => Server?.RebootState;
30 
34  protected ISessionController Server { get; private set; }
35 
40 
56  public BasicWatchdog(
57  IChatManager chat,
58  ISessionControllerFactory sessionControllerFactory,
59  IDmbFactory dmbFactory,
60  IReattachInfoHandler reattachInfoHandler,
61  IDatabaseContextFactory databaseContextFactory,
62  IJobManager jobManager,
63  IServerControl serverControl,
64  IAsyncDelayer asyncDelayer,
65  ILogger<BasicWatchdog> logger,
66  DreamDaemonLaunchParameters initialLaunchParameters,
67  Api.Models.Instance instance,
68  bool autoStart)
69  : base(
70  chat,
71  sessionControllerFactory,
72  dmbFactory,
73  reattachInfoHandler,
74  databaseContextFactory,
75  jobManager,
76  serverControl,
77  asyncDelayer,
78  logger,
79  initialLaunchParameters,
80  instance,
81  autoStart)
82  { }
83 
85  protected override IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState)
86  {
87  if (Server == null)
88  return null;
89 
90  monitorState.ActiveServer = Server;
91 
92  return new Dictionary<MonitorActivationReason, Task>
93  {
94  { MonitorActivationReason.ActiveServerCrashed, Server.Lifetime },
95  { MonitorActivationReason.ActiveServerRebooted, Server.OnReboot },
96  { MonitorActivationReason.InactiveServerCrashed, Extensions.TaskExtensions.InfiniteTask() },
97  { MonitorActivationReason.InactiveServerRebooted, Extensions.TaskExtensions.InfiniteTask() },
98  { MonitorActivationReason.InactiveServerStartupComplete, Extensions.TaskExtensions.InfiniteTask() }
99  };
100  }
101 
103  protected override async Task HandleMonitorWakeup(MonitorActivationReason reason, MonitorState monitorState, CancellationToken cancellationToken)
104  {
105  switch (reason)
106  {
107  case MonitorActivationReason.ActiveServerCrashed:
108  string exitWord = Server.TerminationWasRequested ? "exited" : "crashed";
109  if (Server.RebootState == Session.RebootState.Shutdown)
110  {
111  // the time for graceful shutdown is now
112  await Chat.SendWatchdogMessage(
113  String.Format(
114  CultureInfo.InvariantCulture,
115  "Server {0}! Shutting down due to graceful termination request...",
116  exitWord),
117  false,
118  cancellationToken)
119  .ConfigureAwait(false);
120  monitorState.NextAction = MonitorAction.Exit;
121  }
122  else
123  {
124  await Chat.SendWatchdogMessage(
125  String.Format(
126  CultureInfo.InvariantCulture,
127  "Server {0}! Rebooting...",
128  exitWord),
129  false,
130  cancellationToken)
131  .ConfigureAwait(false);
132  monitorState.NextAction = MonitorAction.Restart;
133  }
134 
135  break;
136  case MonitorActivationReason.ActiveServerRebooted:
137  var rebootState = Server.RebootState;
138  if (gracefulRebootRequired && rebootState == Session.RebootState.Normal)
139  {
140  Logger.LogError("Watchdog reached normal reboot state with gracefulRebootRequired set!");
141  rebootState = Session.RebootState.Restart;
142  }
143 
144  gracefulRebootRequired = false;
145  Server.ResetRebootState();
146 
147  switch (rebootState)
148  {
149  case Session.RebootState.Normal:
150  monitorState.NextAction = HandleNormalReboot();
151  break;
152  case Session.RebootState.Restart:
153  monitorState.NextAction = MonitorAction.Restart;
154  break;
155  case Session.RebootState.Shutdown:
156  // graceful shutdown time
157  await Chat.SendWatchdogMessage(
158  "Active server rebooted! Shutting down due to graceful termination request...",
159  false,
160  cancellationToken)
161  .ConfigureAwait(false);
162  monitorState.NextAction = MonitorAction.Exit;
163  break;
164  default:
165  throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
166  }
167 
168  break;
169  case MonitorActivationReason.ActiveLaunchParametersUpdated:
170  await Server.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false);
171  gracefulRebootRequired = true;
172  break;
173  case MonitorActivationReason.NewDmbAvailable:
174  await HandleNewDmbAvailable(cancellationToken).ConfigureAwait(false);
175  break;
176  case MonitorActivationReason.InactiveServerCrashed:
177  case MonitorActivationReason.InactiveServerRebooted:
178  case MonitorActivationReason.InactiveServerStartupComplete:
179  throw new NotSupportedException($"Unsupported activation reason: {reason}");
180  case MonitorActivationReason.Heartbeat:
181  default:
182  throw new InvalidOperationException($"Invalid activation reason: {reason}");
183  }
184  }
185 
187  protected sealed override DualReattachInformation CreateReattachInformation()
189  {
190  AlphaIsActive = true,
191  Alpha = Server?.Release()
192  };
193 
195  protected override void DisposeAndNullControllersImpl()
196  {
197  Server?.Dispose();
198  Server = null;
199  Running = false;
200  gracefulRebootRequired = false;
201  }
202 
204  protected sealed override ISessionController GetActiveController() => Server;
205 
207  protected sealed override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
208  {
209  var serverToReattach = reattachInfo?.Alpha ?? reattachInfo?.Bravo;
210  var serverToKill = reattachInfo?.Bravo ?? reattachInfo?.Alpha;
211 
212  // vice versa
213  if (serverToKill == serverToReattach)
214  serverToKill = null;
215 
216  if (reattachInfo?.AlphaIsActive == false)
217  {
218  var temp = serverToReattach;
219  serverToReattach = serverToKill;
220  serverToKill = temp;
221  }
222 
223  // don't need a new dmb if reattaching
224  var doesntNeedNewDmb = serverToReattach != null;
225  var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(1);
226 
227  // if this try catches something, both servers are killed
228  bool inactiveServerWasKilled = false;
229  try
230  {
231  // start the alpha server task, either by launch a new process or attaching to an existing one
232  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
233  // The tasks pertaining to server startup times are in the ISessionControllers
234  Task<ISessionController> serverLaunchTask, inactiveReattachTask;
235  if (!doesntNeedNewDmb)
236  {
237  dmbToUse = await PrepServerForLaunch(dmbToUse, cancellationToken).ConfigureAwait(false);
238  serverLaunchTask = SessionControllerFactory.LaunchNew(
239  dmbToUse,
240  null,
241  ActiveLaunchParameters,
242  true,
243  true,
244  false,
245  cancellationToken);
246  }
247  else
248  serverLaunchTask = SessionControllerFactory.Reattach(serverToReattach, cancellationToken);
249 
250  bool thereIsAnInactiveServerToKill = serverToKill != null;
251  if (thereIsAnInactiveServerToKill)
252  inactiveReattachTask = SessionControllerFactory.Reattach(serverToKill, cancellationToken);
253  else
254  inactiveReattachTask = Task.FromResult<ISessionController>(null);
255 
256  // retrieve the session controller
257  Server = await serverLaunchTask.ConfigureAwait(false);
258 
259  // failed reattaches will return null
260  Server?.SetHighPriority();
261 
262  var inactiveServerController = await inactiveReattachTask.ConfigureAwait(false);
263  inactiveServerController?.Dispose();
264  inactiveServerWasKilled = inactiveServerController != null;
265 
266  // possiblity of null servers due to failed reattaches
267  if (Server == null)
268  {
269  callBeforeRecurse();
270  await NotifyOfFailedReattach(thereIsAnInactiveServerToKill && !inactiveServerWasKilled, cancellationToken).ConfigureAwait(false);
271  return;
272  }
273 
274  await CheckLaunchResult(Server, "Server", cancellationToken).ConfigureAwait(false);
275 
276  Server.EnableCustomChatCommands();
277  }
278  catch
279  {
280  // kill the controllers
281  bool serverWasActive = Server != null;
282  DisposeAndNullControllers();
283 
284  // server didn't get control of this dmb
285  if (dmbToUse != null && !serverWasActive)
286  dmbToUse.Dispose();
287 
288  if (serverToKill != null && !inactiveServerWasKilled)
289  serverToKill.Dmb.Dispose();
290  throw;
291  }
292  }
293 
298  protected virtual MonitorAction HandleNormalReboot()
299  {
300  bool dmbUpdatePending = ActiveLaunchParameters != LastLaunchParameters;
301  return dmbUpdatePending ? MonitorAction.Restart : MonitorAction.Continue;
302  }
303 
309  protected virtual Task HandleNewDmbAvailable(CancellationToken cancellationToken)
310  {
311  gracefulRebootRequired = true;
312  return Server.SetRebootState(Session.RebootState.Restart, cancellationToken);
313  }
314 
321  protected virtual Task<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken) => Task.FromResult(dmbToUse);
322 
324  public override Task ResetRebootState(CancellationToken cancellationToken)
325  {
326  if (!gracefulRebootRequired)
327  return base.ResetRebootState(cancellationToken);
328 
329  return Restart(true, cancellationToken);
330  }
331  }
332 }
BasicWatchdog(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger< BasicWatchdog > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the BasicWatchdog .
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
void EnableCustomChatCommands()
Enables the reading of custom chat commands from the ISessionController
@@ -113,6 +113,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
virtual MonitorAction HandleNormalReboot()
Handler for MonitorActivationReason.ActiveServerRebooted when the RebootState is RebootState.Normal.
ReattachInformation Bravo
ReattachInformation for the Bravo session
+
override void DisposeAndNullControllersImpl()
Call IDisposable.Dispose and null the fields for all ISessionControllers and set Running to ...
For managing connected chat services
Definition: IChatManager.cs:13
@@ -144,9 +145,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
The (absolute) state of the ExperimentalWatchdog
Definition: MonitorState.cs:9
- + -
override void DisposeAndNullControllers()
Call IDisposable.Dispose and null the fields for all ISessionControllers and set Running to ...
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_bool_converter_8cs.html b/_bool_converter_8cs.html index 32095a00d9..7d3b780af0 100644 --- a/_bool_converter_8cs.html +++ b/_bool_converter_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bool_converter_8cs_source.html b/_bool_converter_8cs_source.html index 7342bccd70..fee55a80bd 100644 --- a/_bool_converter_8cs_source.html +++ b/_bool_converter_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_command_type_8cs.html b/_bridge_command_type_8cs.html index 19306ebe97..76bcb07eee 100644 --- a/_bridge_command_type_8cs.html +++ b/_bridge_command_type_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_command_type_8cs_source.html b/_bridge_command_type_8cs_source.html index 60a4357b2b..a09f1f2a53 100644 --- a/_bridge_command_type_8cs_source.html +++ b/_bridge_command_type_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_controller_8cs.html b/_bridge_controller_8cs.html index a5d797ec4d..7a4af319a4 100644 --- a/_bridge_controller_8cs.html +++ b/_bridge_controller_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_controller_8cs_source.html b/_bridge_controller_8cs_source.html index 3caacdf102..4dab0f9b1e 100644 --- a/_bridge_controller_8cs_source.html +++ b/_bridge_controller_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_parameters_8cs.html b/_bridge_parameters_8cs.html index 11d06ecf76..bb55731aeb 100644 --- a/_bridge_parameters_8cs.html +++ b/_bridge_parameters_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_parameters_8cs_source.html b/_bridge_parameters_8cs_source.html index 586f130298..af3ae639cd 100644 --- a/_bridge_parameters_8cs_source.html +++ b/_bridge_parameters_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_registration_8cs.html b/_bridge_registration_8cs.html index 098301201c..fe45b46740 100644 --- a/_bridge_registration_8cs.html +++ b/_bridge_registration_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_registration_8cs_source.html b/_bridge_registration_8cs_source.html index f744aea560..46ba5e61ea 100644 --- a/_bridge_registration_8cs_source.html +++ b/_bridge_registration_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_response_8cs.html b/_bridge_response_8cs.html index 8062510270..9fbb059820 100644 --- a/_bridge_response_8cs.html +++ b/_bridge_response_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_bridge_response_8cs_source.html b/_bridge_response_8cs_source.html index aac029473f..943b15314c 100644 --- a/_bridge_response_8cs_source.html +++ b/_bridge_response_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_8cs.html b/_byond_8cs.html index becd1a3cc1..1994289ceb 100644 --- a/_byond_8cs.html +++ b/_byond_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_8cs_source.html b/_byond_8cs_source.html index c387a66228..9d5e3fb6b0 100644 --- a/_byond_8cs_source.html +++ b/_byond_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_client_8cs.html b/_byond_client_8cs.html index ec32c8e8c2..fe44c6c228 100644 --- a/_byond_client_8cs.html +++ b/_byond_client_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_client_8cs_source.html b/_byond_client_8cs_source.html index 3440276eb6..e5736510ca 100644 --- a/_byond_client_8cs_source.html +++ b/_byond_client_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_command_8cs.html b/_byond_command_8cs.html index 693020b43d..1b61b381f1 100644 --- a/_byond_command_8cs.html +++ b/_byond_command_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_command_8cs_source.html b/_byond_command_8cs_source.html index ca2f526b4f..504dda1c1e 100644 --- a/_byond_command_8cs_source.html +++ b/_byond_command_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_controller_8cs.html b/_byond_controller_8cs.html index bb230f146b..c6bc9b7fac 100644 --- a/_byond_controller_8cs.html +++ b/_byond_controller_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_controller_8cs_source.html b/_byond_controller_8cs_source.html index bd65f52b15..1b9167dd75 100644 --- a/_byond_controller_8cs_source.html +++ b/_byond_controller_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -128,7 +128,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task< IActionResult > Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
Changes the active BYOND version to the one specified in a given model .
- +
diff --git a/_byond_executable_lock_8cs.html b/_byond_executable_lock_8cs.html index 3402fbf652..689f744a83 100644 --- a/_byond_executable_lock_8cs.html +++ b/_byond_executable_lock_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_executable_lock_8cs_source.html b/_byond_executable_lock_8cs_source.html index 15cee64622..fb6f48f52d 100644 --- a/_byond_executable_lock_8cs_source.html +++ b/_byond_executable_lock_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_installer_base_8cs.html b/_byond_installer_base_8cs.html index 81d9be1d0d..ed73b1d66d 100644 --- a/_byond_installer_base_8cs.html +++ b/_byond_installer_base_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_installer_base_8cs_source.html b/_byond_installer_base_8cs_source.html index 84c3c1a8cf..5eaf60577f 100644 --- a/_byond_installer_base_8cs_source.html +++ b/_byond_installer_base_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_manager_8cs.html b/_byond_manager_8cs.html index 7a17952f07..2fc4138990 100644 --- a/_byond_manager_8cs.html +++ b/_byond_manager_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_manager_8cs_source.html b/_byond_manager_8cs_source.html index f0fad01d32..eac97b5aa8 100644 --- a/_byond_manager_8cs_source.html +++ b/_byond_manager_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_rights_8cs.html b/_byond_rights_8cs.html index 0b22f00093..32a7679680 100644 --- a/_byond_rights_8cs.html +++ b/_byond_rights_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_byond_rights_8cs_source.html b/_byond_rights_8cs_source.html index 0dbda3b428..d5f79ca4f9 100644 --- a/_byond_rights_8cs_source.html +++ b/_byond_rights_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_channel_mapping_8cs.html b/_channel_mapping_8cs.html index 1707ca3571..59bfe9a2f5 100644 --- a/_channel_mapping_8cs.html +++ b/_channel_mapping_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_channel_mapping_8cs_source.html b/_channel_mapping_8cs_source.html index 8832a2754a..80a72f9fba 100644 --- a/_channel_mapping_8cs_source.html +++ b/_channel_mapping_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_channel_representation_8cs.html b/_channel_representation_8cs.html index f8c3a76b06..c7aaeb083c 100644 --- a/_channel_representation_8cs.html +++ b/_channel_representation_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_channel_representation_8cs_source.html b/_channel_representation_8cs_source.html index db74a11638..2e307d58c3 100644 --- a/_channel_representation_8cs_source.html +++ b/_channel_representation_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_bot_rights_8cs.html b/_chat_bot_rights_8cs.html index d6de3eb411..94e68f7dd1 100644 --- a/_chat_bot_rights_8cs.html +++ b/_chat_bot_rights_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_bot_rights_8cs_source.html b/_chat_bot_rights_8cs_source.html index 5b89f6297b..80f9e45833 100644 --- a/_chat_bot_rights_8cs_source.html +++ b/_chat_bot_rights_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_bots_client_8cs.html b/_chat_bots_client_8cs.html index 4139181938..a3bda2a04c 100644 --- a/_chat_bots_client_8cs.html +++ b/_chat_bots_client_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_bots_client_8cs_source.html b/_chat_bots_client_8cs_source.html index a238593866..0c1ef3dd77 100644 --- a/_chat_bots_client_8cs_source.html +++ b/_chat_bots_client_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_channel_extensions_8cs.html b/_chat_channel_extensions_8cs.html index fa1f476811..c8ae47663e 100644 --- a/_chat_channel_extensions_8cs.html +++ b/_chat_channel_extensions_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_channel_extensions_8cs_source.html b/_chat_channel_extensions_8cs_source.html index 8ff97a1590..4ee820d12c 100644 --- a/_chat_channel_extensions_8cs_source.html +++ b/_chat_channel_extensions_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_command_8cs.html b/_chat_command_8cs.html index bd9fb11e6d..ab6b6193fa 100644 --- a/_chat_command_8cs.html +++ b/_chat_command_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_command_8cs_source.html b/_chat_command_8cs_source.html index b2488cc70c..666b88e89e 100644 --- a/_chat_command_8cs_source.html +++ b/_chat_command_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_connection_string_builder_8cs.html b/_chat_connection_string_builder_8cs.html index f584f529f7..2a453e3b0a 100644 --- a/_chat_connection_string_builder_8cs.html +++ b/_chat_connection_string_builder_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_connection_string_builder_8cs_source.html b/_chat_connection_string_builder_8cs_source.html index 9d1e9c5c9f..8bcb5bc770 100644 --- a/_chat_connection_string_builder_8cs_source.html +++ b/_chat_connection_string_builder_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_controller_8cs.html b/_chat_controller_8cs.html index 08cba13d22..4a32dc8433 100644 --- a/_chat_controller_8cs.html +++ b/_chat_controller_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_controller_8cs_source.html b/_chat_controller_8cs_source.html index cb8713cada..bba8856265 100644 --- a/_chat_controller_8cs_source.html +++ b/_chat_controller_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ChatController.cs
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Linq.Expressions;
5 using System.Net;
6 using System.Reflection;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Microsoft.AspNetCore.Mvc;
10 using Microsoft.EntityFrameworkCore;
11 using Microsoft.Extensions.Logging;
12 using Tgstation.Server.Api;
19 using Z.EntityFramework.Plus;
20 
21 namespace Tgstation.Server.Host.Controllers
22 {
26  [Route(Routes.Chat)]
27  #pragma warning disable CA1506 // TODO: Decomplexify
28  public sealed class ChatController : ApiController
29  {
34 
42  public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
43  {
44  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
45  }
46 
52  static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api) => new Models.ChatChannel
53  {
54  DiscordChannelId = api.DiscordChannelId,
55  IrcChannel = api.IrcChannel,
56  IsAdminChannel = api.IsAdminChannel ?? false,
57  IsWatchdogChannel = api.IsWatchdogChannel ?? false,
58  IsUpdatesChannel = api.IsUpdatesChannel ?? false,
59  Tag = api.Tag
60  };
61 
69  [HttpPut]
70  [TgsAuthorize(ChatBotRights.Create)]
71  [ProducesResponseType(typeof(Api.Models.ChatBot), 201)]
72  public async Task<IActionResult> Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
73  {
74  if (model == null)
75  throw new ArgumentNullException(nameof(model));
76 
77  var earlyOut = StandardModelChecks(model, true);
78  if (earlyOut != null)
79  return earlyOut;
80 
81  var countOfExistingBotsInInstance = await DatabaseContext
82  .ChatBots
83  .Where(x => x.InstanceId == Instance.Id)
84  .CountAsync(cancellationToken)
85  .ConfigureAwait(false);
86 
87  if (countOfExistingBotsInInstance >= Instance.ChatBotLimit.Value)
88  return Conflict(new ErrorMessage(ErrorCode.ChatBotMax));
89 
90  model.Enabled ??= false;
91  model.ReconnectionInterval ??= 1;
92 
93  // try to update das db first
94  var dbModel = new Models.ChatBot
95  {
96  Name = model.Name,
97  ConnectionString = model.ConnectionString,
98  Enabled = model.Enabled,
99  Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).ToList() ?? new List<Models.ChatChannel>(), // important that this isn't null
100  InstanceId = Instance.Id,
101  Provider = model.Provider,
102  ReconnectionInterval = model.ReconnectionInterval,
103  ChannelLimit = model.ChannelLimit
104  };
105 
106  DatabaseContext.ChatBots.Add(dbModel);
107 
108  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
109  var instance = instanceManager.GetInstance(Instance);
110  try
111  {
112  // try to create it
113  await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false);
114 
115  if (dbModel.Channels.Count > 0)
116  await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false);
117  }
118  catch
119  {
120  // undo the add
121  DatabaseContext.ChatBots.Remove(dbModel);
122  await DatabaseContext.Save(default).ConfigureAwait(false);
123  await instance.Chat.DeleteConnection(dbModel.Id, cancellationToken).ConfigureAwait(false);
124  throw;
125  }
126 
127  return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
128  }
129 
137  [HttpDelete("{id}")]
138  [TgsAuthorize(ChatBotRights.Delete)]
139  [ProducesResponseType(204)]
140  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
141  {
142  var instance = instanceManager.GetInstance(Instance);
143  await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatBots.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false);
144 
145  return Ok();
146  }
147 
154  [HttpGet(Routes.List)]
155  [TgsAuthorize(ChatBotRights.Read)]
156  [ProducesResponseType(typeof(IEnumerable<Api.Models.ChatBot>), 200)]
157  public async Task<IActionResult> List(CancellationToken cancellationToken)
158  {
159  var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels);
160 
161  var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
162 
163  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
164 
165  if (!connectionStrings)
166  foreach (var I in results)
167  I.ConnectionString = null;
168 
169  return Json(results.Select(x => x.ToApi()));
170  }
171 
180  [HttpGet("{id}")]
181  [TgsAuthorize(ChatBotRights.Read)]
182  [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
183  [ProducesResponseType(410)]
184  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
185  {
186  var query = DatabaseContext.ChatBots.Where(x => x.Id == id).Include(x => x.Channels);
187 
188  var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
189  if (results == default)
190  return StatusCode((int)HttpStatusCode.Gone);
191 
192  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
193 
194  if (!connectionStrings)
195  results.ConnectionString = null;
196 
197  return Json(results.ToApi());
198  }
199 
207  [HttpPost]
208  [TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)]
209  [ProducesResponseType(200)]
210  [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
211 #pragma warning disable CA1502 // TODO: Decomplexify
212 #pragma warning disable CA1506
213  public async Task<IActionResult> Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
214  #pragma warning restore CA1502
215  #pragma warning restore CA1506
216  {
217  if (model == null)
218  throw new ArgumentNullException(nameof(model));
219 
220  var earlyOut = StandardModelChecks(model, false);
221  if (earlyOut != null)
222  return earlyOut;
223 
224  var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id && x.Id == model.Id).Include(x => x.Channels);
225 
226  var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
227 
228  if (current == default)
229  return StatusCode((int)HttpStatusCode.Gone);
230 
231  if ((model.Channels?.Count ?? current.Channels.Count) > (model.ChannelLimit ?? current.ChannelLimit.Value))
232  {
233  // 400 or 409 depends on if the client sent both
234  var errorMessage = new ErrorMessage(ErrorCode.ChatBotMaxChannels);
235  if (model.Channels != null && model.ChannelLimit.HasValue)
236  return BadRequest(errorMessage);
237  return Conflict(errorMessage);
238  }
239 
240  var userRights = (ChatBotRights)AuthenticationContext.GetRight(RightsType.ChatBots);
241 
242  bool anySettingsModified = false;
243 
244  bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatBot, T>> expression, ChatBotRights requiredRight)
245  {
246  var memberSelectorExpression = (MemberExpression)expression.Body;
247  var property = (PropertyInfo)memberSelectorExpression.Member;
248 
249  var newVal = property.GetValue(model);
250  if (newVal == null)
251  return false;
252  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
253  return true;
254 
255  property.SetValue(current, newVal);
256  anySettingsModified = true;
257  return false;
258  }
259 
260  var oldProvider = current.Provider;
261 
262  if (CheckModified(x => x.ConnectionString, ChatBotRights.WriteConnectionString)
263  || CheckModified(x => x.Enabled, ChatBotRights.WriteEnabled)
264  || CheckModified(x => x.Name, ChatBotRights.WriteName)
265  || CheckModified(x => x.Provider, ChatBotRights.WriteProvider)
266  || CheckModified(x => x.ReconnectionInterval, ChatBotRights.WriteReconnectionInterval)
267  || CheckModified(x => x.ChannelLimit, ChatBotRights.WriteChannelLimit)
268  || (model.Channels != null && !userRights.HasFlag(ChatBotRights.WriteChannels)))
269  return Forbid();
270 
271  var hasChannels = model.Channels != null;
272  if (hasChannels || (model.Provider.HasValue && model.Provider != oldProvider))
273  {
274  DatabaseContext.ChatChannels.RemoveRange(current.Channels);
275  if (hasChannels)
276  {
277  var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList();
278  DatabaseContext.ChatChannels.AddRange(dbChannels);
279  current.Channels = dbChannels;
280  }
281  else
282  current.Channels.Clear();
283  }
284 
285  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
286 
287  var chat = instanceManager.GetInstance(Instance).Chat;
288 
289  if (anySettingsModified)
290  await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false); // have to rebuild the thing first
291 
292  if (model.Channels != null || anySettingsModified)
293  await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false);
294 
295  if (userRights.HasFlag(ChatBotRights.Read))
296  {
297  if (!userRights.HasFlag(ChatBotRights.ReadConnectionString))
298  current.ConnectionString = null;
299  return Json(current.ToApi());
300  }
301 
302  return Ok();
303  }
304 
311  private IActionResult StandardModelChecks(Api.Models.ChatBot model, bool forCreation)
312  {
313  if (model.ReconnectionInterval == 0)
314  throw new InvalidOperationException("RecconnectionInterval cannot be zero!");
315 
316  if (forCreation && !model.Provider.HasValue)
317  return BadRequest(new ErrorMessage(ErrorCode.ChatBotProviderMissing));
318 
319  if (model.Name != null && String.IsNullOrWhiteSpace(model.Name))
320  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWhitespaceName));
321 
322  if (model.ConnectionString != null && String.IsNullOrWhiteSpace(model.ConnectionString))
323  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWhitespaceConnectionString));
324 
325  if (!model.ValidateProviderChannelTypes())
326  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWrongChannelType));
327 
328  var defaultMaxChannels = (ulong)Math.Max(Models.ChatBot.DefaultChannelLimit, model.Channels?.Count ?? 0);
329  if (defaultMaxChannels > UInt16.MaxValue)
330  return BadRequest(new ErrorMessage(ErrorCode.ChatBotMaxChannels));
331 
332  if (forCreation)
333  model.ChannelLimit ??= (ushort)defaultMaxChannels;
334 
335  return null;
336  }
337  }
338  #pragma warning restore CA1506
339 }
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Linq.Expressions;
5 using System.Net;
6 using System.Reflection;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Microsoft.AspNetCore.Mvc;
10 using Microsoft.EntityFrameworkCore;
11 using Microsoft.Extensions.Logging;
12 using Tgstation.Server.Api;
19 using Z.EntityFramework.Plus;
20 
21 namespace Tgstation.Server.Host.Controllers
22 {
26  [Route(Routes.Chat)]
27  #pragma warning disable CA1506 // TODO: Decomplexify
28  public sealed class ChatController : ApiController
29  {
34 
42  public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
43  {
44  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
45  }
46 
52  static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api) => new Models.ChatChannel
53  {
54  DiscordChannelId = api.DiscordChannelId,
55  IrcChannel = api.IrcChannel,
56  IsAdminChannel = api.IsAdminChannel ?? false,
57  IsWatchdogChannel = api.IsWatchdogChannel ?? false,
58  IsUpdatesChannel = api.IsUpdatesChannel ?? false,
59  Tag = api.Tag
60  };
61 
69  [HttpPut]
70  [TgsAuthorize(ChatBotRights.Create)]
71  [ProducesResponseType(typeof(Api.Models.ChatBot), 201)]
72  public async Task<IActionResult> Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
73  {
74  if (model == null)
75  throw new ArgumentNullException(nameof(model));
76 
77  var earlyOut = StandardModelChecks(model, true);
78  if (earlyOut != null)
79  return earlyOut;
80 
81  var countOfExistingBotsInInstance = await DatabaseContext
82  .ChatBots
83  .AsQueryable()
84  .Where(x => x.InstanceId == Instance.Id)
85  .CountAsync(cancellationToken)
86  .ConfigureAwait(false);
87 
88  if (countOfExistingBotsInInstance >= Instance.ChatBotLimit.Value)
89  return Conflict(new ErrorMessage(ErrorCode.ChatBotMax));
90 
91  model.Enabled ??= false;
92  model.ReconnectionInterval ??= 1;
93 
94  // try to update das db first
95  var dbModel = new Models.ChatBot
96  {
97  Name = model.Name,
98  ConnectionString = model.ConnectionString,
99  Enabled = model.Enabled,
100  Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).ToList() ?? new List<Models.ChatChannel>(), // important that this isn't null
101  InstanceId = Instance.Id,
102  Provider = model.Provider,
103  ReconnectionInterval = model.ReconnectionInterval,
104  ChannelLimit = model.ChannelLimit
105  };
106 
107  DatabaseContext.ChatBots.Add(dbModel);
108 
109  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
110  var instance = instanceManager.GetInstance(Instance);
111  try
112  {
113  // try to create it
114  await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false);
115 
116  if (dbModel.Channels.Count > 0)
117  await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false);
118  }
119  catch
120  {
121  // undo the add
122  DatabaseContext.ChatBots.Remove(dbModel);
123  await DatabaseContext.Save(default).ConfigureAwait(false);
124  await instance.Chat.DeleteConnection(dbModel.Id, cancellationToken).ConfigureAwait(false);
125  throw;
126  }
127 
128  return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
129  }
130 
138  [HttpDelete("{id}")]
139  [TgsAuthorize(ChatBotRights.Delete)]
140  [ProducesResponseType(204)]
141  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
142  {
143  var instance = instanceManager.GetInstance(Instance);
144  await Task.WhenAll(
145  instance.Chat.DeleteConnection(id, cancellationToken),
147  .ChatBots
148  .AsQueryable()
149  .Where(x => x.Id == id)
150  .DeleteAsync(cancellationToken))
151  .ConfigureAwait(false);
152 
153  return Ok();
154  }
155 
162  [HttpGet(Routes.List)]
163  [TgsAuthorize(ChatBotRights.Read)]
164  [ProducesResponseType(typeof(IEnumerable<Api.Models.ChatBot>), 200)]
165  public async Task<IActionResult> List(CancellationToken cancellationToken)
166  {
167  var query = DatabaseContext
168  .ChatBots
169  .AsQueryable()
170  .Where(x => x.InstanceId == Instance.Id)
171  .Include(x => x.Channels);
172 
173  var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
174 
175  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
176 
177  if (!connectionStrings)
178  foreach (var I in results)
179  I.ConnectionString = null;
180 
181  return Json(results.Select(x => x.ToApi()));
182  }
183 
192  [HttpGet("{id}")]
193  [TgsAuthorize(ChatBotRights.Read)]
194  [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
195  [ProducesResponseType(410)]
196  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
197  {
198  var query = DatabaseContext.ChatBots
199  .AsQueryable()
200  .Where(x => x.Id == id)
201  .Include(x => x.Channels);
202 
203  var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
204  if (results == default)
205  return StatusCode((int)HttpStatusCode.Gone);
206 
207  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
208 
209  if (!connectionStrings)
210  results.ConnectionString = null;
211 
212  return Json(results.ToApi());
213  }
214 
222  [HttpPost]
223  [TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)]
224  [ProducesResponseType(200)]
225  [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
226 #pragma warning disable CA1502 // TODO: Decomplexify
227 #pragma warning disable CA1506
228  public async Task<IActionResult> Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
229  #pragma warning restore CA1502
230  #pragma warning restore CA1506
231  {
232  if (model == null)
233  throw new ArgumentNullException(nameof(model));
234 
235  var earlyOut = StandardModelChecks(model, false);
236  if (earlyOut != null)
237  return earlyOut;
238 
239  var query = DatabaseContext
240  .ChatBots
241  .AsQueryable()
242  .Where(x => x.InstanceId == Instance.Id && x.Id == model.Id)
243  .Include(x => x.Channels);
244 
245  var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
246 
247  if (current == default)
248  return StatusCode((int)HttpStatusCode.Gone);
249 
250  if ((model.Channels?.Count ?? current.Channels.Count) > (model.ChannelLimit ?? current.ChannelLimit.Value))
251  {
252  // 400 or 409 depends on if the client sent both
253  var errorMessage = new ErrorMessage(ErrorCode.ChatBotMaxChannels);
254  if (model.Channels != null && model.ChannelLimit.HasValue)
255  return BadRequest(errorMessage);
256  return Conflict(errorMessage);
257  }
258 
259  var userRights = (ChatBotRights)AuthenticationContext.GetRight(RightsType.ChatBots);
260 
261  bool anySettingsModified = false;
262 
263  bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatBot, T>> expression, ChatBotRights requiredRight)
264  {
265  var memberSelectorExpression = (MemberExpression)expression.Body;
266  var property = (PropertyInfo)memberSelectorExpression.Member;
267 
268  var newVal = property.GetValue(model);
269  if (newVal == null)
270  return false;
271  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
272  return true;
273 
274  property.SetValue(current, newVal);
275  anySettingsModified = true;
276  return false;
277  }
278 
279  var oldProvider = current.Provider;
280 
281  if (CheckModified(x => x.ConnectionString, ChatBotRights.WriteConnectionString)
282  || CheckModified(x => x.Enabled, ChatBotRights.WriteEnabled)
283  || CheckModified(x => x.Name, ChatBotRights.WriteName)
284  || CheckModified(x => x.Provider, ChatBotRights.WriteProvider)
285  || CheckModified(x => x.ReconnectionInterval, ChatBotRights.WriteReconnectionInterval)
286  || CheckModified(x => x.ChannelLimit, ChatBotRights.WriteChannelLimit)
287  || (model.Channels != null && !userRights.HasFlag(ChatBotRights.WriteChannels)))
288  return Forbid();
289 
290  var hasChannels = model.Channels != null;
291  if (hasChannels || (model.Provider.HasValue && model.Provider != oldProvider))
292  {
293  DatabaseContext.ChatChannels.RemoveRange(current.Channels);
294  if (hasChannels)
295  {
296  var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList();
297  DatabaseContext.ChatChannels.AddRange(dbChannels);
298  current.Channels = dbChannels;
299  }
300  else
301  current.Channels.Clear();
302  }
303 
304  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
305 
306  var chat = instanceManager.GetInstance(Instance).Chat;
307 
308  if (anySettingsModified)
309  await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false); // have to rebuild the thing first
310 
311  if (model.Channels != null || anySettingsModified)
312  await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false);
313 
314  if (userRights.HasFlag(ChatBotRights.Read))
315  {
316  if (!userRights.HasFlag(ChatBotRights.ReadConnectionString))
317  current.ConnectionString = null;
318  return Json(current.ToApi());
319  }
320 
321  return Ok();
322  }
323 
330  private IActionResult StandardModelChecks(Api.Models.ChatBot model, bool forCreation)
331  {
332  if (model.ReconnectionInterval == 0)
333  throw new InvalidOperationException("RecconnectionInterval cannot be zero!");
334 
335  if (forCreation && !model.Provider.HasValue)
336  return BadRequest(new ErrorMessage(ErrorCode.ChatBotProviderMissing));
337 
338  if (model.Name != null && String.IsNullOrWhiteSpace(model.Name))
339  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWhitespaceName));
340 
341  if (model.ConnectionString != null && String.IsNullOrWhiteSpace(model.ConnectionString))
342  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWhitespaceConnectionString));
343 
344  if (!model.ValidateProviderChannelTypes())
345  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWrongChannelType));
346 
347  var defaultMaxChannels = (ulong)Math.Max(Models.ChatBot.DefaultChannelLimit, model.Channels?.Count ?? 0);
348  if (defaultMaxChannels > UInt16.MaxValue)
349  return BadRequest(new ErrorMessage(ErrorCode.ChatBotMaxChannels));
350 
351  if (forCreation)
352  model.ChannelLimit ??= (ushort)defaultMaxChannels;
353 
354  return null;
355  }
356  }
357  #pragma warning restore CA1506
358 }
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext.
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
@@ -103,18 +103,20 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); +
DbSet< ChatChannel > ChatChannels
The ChatChannels in the DatabaseContext.
-
IActionResult StandardModelChecks(Api.Models.ChatBot model, bool forCreation)
Perform some basic validation of a given model .
+
IActionResult StandardModelChecks(Api.Models.ChatBot model, bool forCreation)
Perform some basic validation of a given model .
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
+
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
ushort ChatBotLimit
The maximum number of ChatBots the Instance may contain.
Definition: Instance.cs:52
Routes to a server actions
Definition: Routes.cs:9
-
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Delete a Api.Models.ChatBot.
+
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Delete a Api.Models.ChatBot.
ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger< ChatController > logger)
Construct a ChatController
-
async Task< IActionResult > List(CancellationToken cancellationToken)
List Api.Models.ChatBots.
-
async Task< IActionResult > Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
Updates a chat bot model .
+
async Task< IActionResult > List(CancellationToken cancellationToken)
List Api.Models.ChatBots.
+
async Task< IActionResult > Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
Updates a chat bot model .
const string Chat
The Models.ChatBot controller
Definition: Routes.cs:69
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
@@ -126,15 +128,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiController for managing Api.Models.ChatBots
readonly IInstanceManager instanceManager
The IInstanceManager for the ChatController
-
DbSet< ChatChannel > ChatChannels
The ChatChannels in the DatabaseContext<TParentContext>.
const string List
The postfix for list operations
Definition: Routes.cs:84
async Task< IActionResult > Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
Create a new chat bot model .
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
-
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.ChatBot.
+
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.ChatBot.
-
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext<TParentContext>.
- +
diff --git a/_chat_manager_8cs.html b/_chat_manager_8cs.html index 0f7b6268e8..029811ae44 100644 --- a/_chat_manager_8cs.html +++ b/_chat_manager_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_manager_8cs_source.html b/_chat_manager_8cs_source.html index bbd7785293..ca4b1288d8 100644 --- a/_chat_manager_8cs_source.html +++ b/_chat_manager_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,14 +94,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ChatManager.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host.Components.Chat
16 {
18  // TODO: Decomplexify
19  #pragma warning disable CA1506
21  {
22  const string CommonMention = "!tgs";
23 
28 
33 
38 
43 
48 
52  readonly ILoggerFactory loggerFactory;
53 
57  readonly ILogger<ChatManager> logger;
58 
62  readonly IDictionary<string, ICommand> builtinCommands;
63 
67  readonly IDictionary<long, IProvider> providers;
68 
72  readonly IDictionary<ulong, ChannelMapping> mappedChannels;
73 
77  readonly IList<IChatTrackingContext> trackingContexts;
78 
82  readonly CancellationTokenSource handlerCts;
83 
87  readonly List<Models.ChatBot> activeChatBots;
88 
92  readonly object synchronizationLock;
93 
98 
103 
107  TaskCompletionSource<object> connectionsUpdated;
108 
113 
117  bool started;
118 
130  public ChatManager(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<ChatManager> logger, IEnumerable<Models.ChatBot> initialChatBots)
131  {
132  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
133  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
134  this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
135  if (serverControl == null)
136  throw new ArgumentNullException(nameof(serverControl));
137  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
138  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
139  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
140  activeChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
141 
142  restartRegistration = serverControl.RegisterForRestart(this);
143 
144  synchronizationLock = new object();
145 
146  builtinCommands = new Dictionary<string, ICommand>();
147  providers = new Dictionary<long, IProvider>();
148  mappedChannels = new Dictionary<ulong, ChannelMapping>();
149  trackingContexts = new List<IChatTrackingContext>();
150  handlerCts = new CancellationTokenSource();
151  connectionsUpdated = new TaskCompletionSource<object>();
152  channelIdCounter = 1;
153  }
154 
156  public void Dispose()
157  {
158  restartRegistration.Dispose();
159  handlerCts.Dispose();
160  foreach (var I in providers)
161  I.Value.Dispose();
162  }
163 
171  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
172  {
173  IProvider provider;
174  lock (providers)
175  if (!providers.TryGetValue(connectionId, out provider))
176  return null;
177 
178  Task trackingContextsUpdateTask;
179  lock (mappedChannels)
180  {
181  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
182  mappedChannels.Remove(I);
183 
184  var newMappedChannels = mappedChannels.Select(y => y.Value.Channel).ToList();
185 
186  if (updateTrackings)
187  lock (trackingContexts)
188  trackingContextsUpdateTask = Task.WhenAll(trackingContexts.Select(x => x.UpdateChannels(newMappedChannels, cancellationToken)));
189  else
190  trackingContextsUpdateTask = Task.CompletedTask;
191  }
192 
193  await trackingContextsUpdateTask.ConfigureAwait(false);
194 
195  return provider;
196  }
197 
205  #pragma warning disable CA1502
206  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
207  #pragma warning restore CA1502
208  {
209  // provider reconnected, remap channels.
210  if (message == null)
211  {
212  IEnumerable<Api.Models.ChatChannel> channelsToMap;
213  lock (activeChatBots)
214  channelsToMap = activeChatBots.FirstOrDefault()?.Channels;
215 
216  if (channelsToMap?.Any() ?? false)
217  {
218  long providerId;
219  lock (providers)
220  providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
221  await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false);
222  }
223 
224  return;
225  }
226 
227  // map the channel if it's private and we haven't seen it
228  lock (providers)
229  {
230  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
231  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
232  if (message.User.Channel.IsPrivateChannel)
233  lock (mappedChannels)
234  {
235  if (!provider.Connected)
236  return;
237  if (!enumerable.Any())
238  {
239  ulong newId;
240  lock (synchronizationLock)
241  newId = channelIdCounter++;
242  logger.LogTrace(
243  "Mapping private channel {0}:{1} as {2}",
244  message.User.Channel.ConnectionName,
245  message.User.FriendlyName,
246  newId);
247  mappedChannels.Add(newId, new ChannelMapping
248  {
249  IsWatchdogChannel = false,
250  ProviderChannelId = message.User.Channel.RealId,
251  ProviderId = providerId,
252  Channel = message.User.Channel
253  });
254  message.User.Channel.RealId = newId;
255  }
256  else
257  message.User.Channel.RealId = enumerable.First().Key;
258  }
259  else
260  {
261  // need to add tag and isAdminChannel
262  var mapping = enumerable.First().Value;
263  message.User.Channel.Id = mapping.Channel.Id;
264  message.User.Channel.Tag = mapping.Channel.Tag;
265  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
266  }
267  }
268 
269  var splits = new List<string>(message.Content.Trim().Split(' '));
270  var address = splits[0];
271  if (address.Length > 1 && (address.Last() == ':' || address.Last() == ','))
272  address = address[0..^1];
273 
274  address = address.ToUpperInvariant();
275 
276  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
277 
278  // no mention
279  if (!addressed && !message.User.Channel.IsPrivateChannel)
280  return;
281 
282  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
283 
284  if (addressed)
285  splits.RemoveAt(0);
286 
287  if (splits.Count == 0)
288  {
289  // just a mention
290  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
291  return;
292  }
293 
294  var command = splits[0].ToUpperInvariant();
295  splits.RemoveAt(0);
296  var arguments = String.Join(" ", splits);
297 
298  try
299  {
300  ICommand GetCommand(string commandName)
301  {
302  if (!builtinCommands.TryGetValue(commandName, out var handler))
303  {
304  handler = trackingContexts
305  .Where(x => x.CustomCommands != null)
306  .SelectMany(x => x.CustomCommands)
307  .Where(x => x.Name.ToUpperInvariant() == commandName)
308  .FirstOrDefault();
309  }
310 
311  return handler;
312  }
313 
314  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
315 
316  if (command == "HELP" || command == "?")
317  {
318  string helpText;
319  if (splits.Count == 0)
320  {
321  var allCommands = builtinCommands.Select(x => x.Value).ToList();
322  allCommands.AddRange(
323  trackingContexts
324  .Where(x => x.CustomCommands != null)
325  .SelectMany(
326  x => x.CustomCommands));
327  helpText = String.Format(CultureInfo.InvariantCulture, "Available commands (Type '?' or 'help' and then a command name for more details): {0}", String.Join(", ", allCommands.Select(x => x.Name)));
328  }
329  else
330  {
331  var helpHandler = GetCommand(splits[0].ToUpperInvariant());
332  if (helpHandler != default)
333  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
334  else
335  helpText = UnknownCommandMessage;
336  }
337 
338  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
339  return;
340  }
341 
342  var commandHandler = GetCommand(command);
343 
344  if (commandHandler == default)
345  {
346  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
347  return;
348  }
349 
350  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
351  {
352  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
353  return;
354  }
355 
356  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
357  if (result != null)
358  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
359  }
360  catch (OperationCanceledException)
361  {
362  throw;
363  }
364  catch (Exception e)
365  {
366  // error bc custom commands should reply about why it failed
367  logger.LogError("Error processing chat command: {0}", e);
368  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
369  }
370  }
371 
377  async Task MonitorMessages(CancellationToken cancellationToken)
378  {
379  var messageTasks = new Dictionary<IProvider, Task<Message>>();
380  try
381  {
382  while (!cancellationToken.IsCancellationRequested)
383  {
384  // prune disconnected providers
385  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
386  messageTasks.Remove(I.Key);
387 
388  // add new ones
389  Task updatedTask;
390  lock (synchronizationLock)
391  updatedTask = connectionsUpdated.Task;
392  lock (providers)
393  foreach (var I in providers)
394  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
395  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
396 
397  if (messageTasks.Count == 0)
398  {
399  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
400  continue;
401  }
402 
403  // wait for a message
404  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
405 
406  // process completed ones
407  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
408  {
409  var message = await I.Value.ConfigureAwait(false);
410  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
411  messageTasks.Remove(I.Key);
412  }
413  }
414  }
415  catch (OperationCanceledException)
416  {
417  logger.LogTrace("Message processing loop cancelled!");
418  }
419  catch (Exception e)
420  {
421  logger.LogError("Message monitor crashed! Exception: {0}", e);
422  }
423  }
424 
426  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
427  {
428  if (newChannels == null)
429  throw new ArgumentNullException(nameof(newChannels));
430  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
431  if (provider == null)
432  return;
433  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
434  lock (activeChatBots)
435  {
436  var botToUpdate = activeChatBots.FirstOrDefault(bot => bot.Id == connectionId);
437  if (botToUpdate != null)
438  botToUpdate.Channels = newChannels
439  .Select(apiModel => new Models.ChatChannel
440  {
441  DiscordChannelId = apiModel.DiscordChannelId,
442  IrcChannel = apiModel.IrcChannel,
443  IsAdminChannel = apiModel.IsAdminChannel,
444  IsUpdatesChannel = apiModel.IsUpdatesChannel,
445  IsWatchdogChannel = apiModel.IsWatchdogChannel,
446  Tag = apiModel.Tag
447  })
448  .ToList();
449  }
450 
451  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
452  {
453  IsWatchdogChannel = x.IsWatchdogChannel == true,
454  IsUpdatesChannel = x.IsUpdatesChannel == true,
455  IsAdminChannel = x.IsAdminChannel == true,
456  ProviderChannelId = y.RealId,
457  ProviderId = connectionId,
458  Channel = y
459  });
460 
461  ulong baseId;
462  lock (synchronizationLock)
463  {
464  baseId = channelIdCounter;
465  channelIdCounter += (ulong)results.Count;
466  }
467 
468  Task trackingContextUpdateTask;
469  lock (mappedChannels)
470  {
471  lock (providers)
472  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) // aborted again
473  return;
474  foreach (var I in mappings)
475  {
476  var newId = baseId++;
477  logger.LogTrace("Mapping channel {0}:{1} as {2}", I.Channel.ConnectionName, I.Channel.FriendlyName, newId);
478  mappedChannels.Add(newId, I);
479  I.Channel.RealId = newId;
480  }
481 
482  lock (trackingContexts)
483  trackingContextUpdateTask = Task.WhenAll(
484  trackingContexts.Select(
485  x => x.UpdateChannels(
486  mappedChannels.Select(y => y.Value.Channel).ToList(),
487  cancellationToken)));
488  }
489 
490  await trackingContextUpdateTask.ConfigureAwait(false);
491  }
492 
494  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
495  {
496  if (newSettings == null)
497  throw new ArgumentNullException(nameof(newSettings));
498  IProvider provider;
499 
500  async Task DisconnectProvider(IProvider p)
501  {
502  try
503  {
504  await p.Disconnect(cancellationToken).ConfigureAwait(false);
505  }
506  finally
507  {
508  p.Dispose();
509  }
510  }
511 
512  Task disconnectTask;
513  lock (providers)
514  {
515  // raw settings changes forces a rebuild of the provider
516  if (providers.TryGetValue(newSettings.Id, out provider))
517  {
518  providers.Remove(newSettings.Id);
519  disconnectTask = DisconnectProvider(provider);
520  }
521  else
522  disconnectTask = Task.CompletedTask;
523  if (newSettings.Enabled.Value)
524  {
525  provider = providerFactory.CreateProvider(newSettings);
526  providers.Add(newSettings.Id, provider);
527  }
528  }
529 
530  lock (mappedChannels)
531  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
532  mappedChannels.Remove(I);
533 
534  await disconnectTask.ConfigureAwait(false);
535 
536  if (started)
537  {
538  if (newSettings.Enabled.Value)
539  await provider.Connect(cancellationToken).ConfigureAwait(false);
540  lock (synchronizationLock)
541  {
542  // same thread shennanigans
543  var oldOne = connectionsUpdated;
544  connectionsUpdated = new TaskCompletionSource<object>();
545  oldOne.SetResult(null);
546  }
547  }
548 
549  Task reconnectionUpdateTask = Task.CompletedTask;
550  lock (activeChatBots)
551  {
552  var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
553  if (originalChatBot != null)
554  {
555  if (originalChatBot.ReconnectionInterval != newSettings.ReconnectionInterval)
556  reconnectionUpdateTask = provider.SetReconnectInterval(newSettings.ReconnectionInterval.Value);
557 
558  activeChatBots.Remove(originalChatBot);
559  }
560 
561  activeChatBots.Add(new Models.ChatBot
562  {
563  Id = newSettings.Id,
564  ConnectionString = newSettings.ConnectionString,
565  Enabled = newSettings.Enabled,
566  Name = newSettings.Name,
567  ReconnectionInterval = newSettings.ReconnectionInterval,
568  Provider = newSettings.Provider
569  });
570  }
571 
572  await reconnectionUpdateTask.ConfigureAwait(false);
573  }
574 
576  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
577  {
578  if (message == null)
579  throw new ArgumentNullException(nameof(message));
580  if (channelIds == null)
581  throw new ArgumentNullException(nameof(channelIds));
582 
583  logger.LogTrace("Chat send \"{0}\" to channels: {1}", message, String.Join(", ", channelIds));
584 
585  return Task.WhenAll(channelIds.Select(x =>
586  {
587  ChannelMapping channelMapping;
588  lock (mappedChannels)
589  if (!mappedChannels.TryGetValue(x, out channelMapping))
590  return Task.CompletedTask;
591  IProvider provider;
592  lock (providers)
593  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
594  return Task.CompletedTask;
595  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
596  }));
597  }
598 
600  public Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
601  {
602  List<ulong> wdChannels = null;
603  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
604 
605  // so it doesn't change while we're using it
606  lock (mappedChannels)
607  {
608  if (adminOnly)
609  {
610  wdChannels = mappedChannels.Where(x => x.Value.IsAdminChannel).Select(x => x.Key).ToList();
611  if (wdChannels.Count == 0)
612  adminOnly = false;
613  }
614 
615  if (!adminOnly)
616  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
617  }
618 
619  return SendMessage(message, wdChannels, cancellationToken);
620  }
621 
623  public Task SendUpdateMessage(string message, CancellationToken cancellationToken)
624  {
625  List<ulong> wdChannels;
626  message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message);
627  lock (mappedChannels) // so it doesn't change while we're using it
628  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
629  return SendMessage(message, wdChannels, cancellationToken);
630  }
631 
633  public async Task StartAsync(CancellationToken cancellationToken)
634  {
635  foreach (var I in commandFactory.GenerateCommands())
636  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
637  var initialChatBots = activeChatBots.ToList();
638  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
639  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
640  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
641  chatHandler = MonitorMessages(handlerCts.Token);
642  started = true;
643  }
644 
646  public async Task StopAsync(CancellationToken cancellationToken)
647  {
648  handlerCts.Cancel();
649  if (chatHandler != null)
650  await chatHandler.ConfigureAwait(false);
651  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
652  }
653 
656  {
657  if (customCommandHandler == null)
658  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
659 
660  IChatTrackingContext context = null;
661  lock (mappedChannels)
662  context = new ChatTrackingContext(
663  customCommandHandler,
664  mappedChannels.Select(y => y.Value.Channel),
665  loggerFactory.CreateLogger<ChatTrackingContext>(),
666  () =>
667  {
668  lock (trackingContexts)
669  trackingContexts.Remove(context);
670  });
671 
672  lock (trackingContexts)
673  trackingContexts.Add(context);
674 
675  return context;
676  }
677 
679  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
680  {
681  if (this.customCommandHandler != null)
682  throw new InvalidOperationException("RegisterCommandHandler() already called!");
683  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
684  }
685 
687  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
688  {
689  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
690  if (provider != null)
691  try
692  {
693  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
694  }
695  finally
696  {
697  provider.Dispose();
698  }
699  }
700 
702  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
703  {
704  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
705  List<ulong> wdChannels;
706  lock (mappedChannels) // so it doesn't change while we're using it
707  wdChannels = mappedChannels.Select(x => x.Key).ToList();
708  return SendMessage(message, wdChannels, cancellationToken);
709  }
710  }
711 }
bool IsWatchdogChannel
If Channel is a watchdog channel
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host.Components.Chat
16 {
18  // TODO: Decomplexify
19  #pragma warning disable CA1506
21  {
22  const string CommonMention = "!tgs";
23 
28 
33 
38 
43 
48 
52  readonly ILoggerFactory loggerFactory;
53 
57  readonly ILogger<ChatManager> logger;
58 
62  readonly IDictionary<string, ICommand> builtinCommands;
63 
67  readonly IDictionary<long, IProvider> providers;
68 
72  readonly IDictionary<ulong, ChannelMapping> mappedChannels;
73 
77  readonly IList<IChatTrackingContext> trackingContexts;
78 
82  readonly CancellationTokenSource handlerCts;
83 
87  readonly List<Models.ChatBot> activeChatBots;
88 
92  readonly object synchronizationLock;
93 
98 
103 
107  TaskCompletionSource<object> connectionsUpdated;
108 
113 
117  bool started;
118 
130  public ChatManager(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<ChatManager> logger, IEnumerable<Models.ChatBot> initialChatBots)
131  {
132  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
133  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
134  this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
135  if (serverControl == null)
136  throw new ArgumentNullException(nameof(serverControl));
137  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
138  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
139  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
140  activeChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
141 
142  restartRegistration = serverControl.RegisterForRestart(this);
143 
144  synchronizationLock = new object();
145 
146  builtinCommands = new Dictionary<string, ICommand>();
147  providers = new Dictionary<long, IProvider>();
148  mappedChannels = new Dictionary<ulong, ChannelMapping>();
149  trackingContexts = new List<IChatTrackingContext>();
150  handlerCts = new CancellationTokenSource();
151  connectionsUpdated = new TaskCompletionSource<object>();
152  channelIdCounter = 1;
153  }
154 
156  public void Dispose()
157  {
158  restartRegistration.Dispose();
159  handlerCts.Dispose();
160  foreach (var I in providers)
161  I.Value.Dispose();
162  }
163 
171  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
172  {
173  IProvider provider;
174  lock (providers)
175  if (!providers.TryGetValue(connectionId, out provider))
176  return null;
177 
178  Task trackingContextsUpdateTask;
179  lock (mappedChannels)
180  {
181  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
182  mappedChannels.Remove(I);
183 
184  var newMappedChannels = mappedChannels.Select(y => y.Value.Channel).ToList();
185 
186  if (updateTrackings)
187  lock (trackingContexts)
188  trackingContextsUpdateTask = Task.WhenAll(trackingContexts.Select(x => x.UpdateChannels(newMappedChannels, cancellationToken)));
189  else
190  trackingContextsUpdateTask = Task.CompletedTask;
191  }
192 
193  await trackingContextsUpdateTask.ConfigureAwait(false);
194 
195  return provider;
196  }
197 
205  #pragma warning disable CA1502
206  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
207  #pragma warning restore CA1502
208  {
209  // provider reconnected, remap channels.
210  if (message == null)
211  {
212  IEnumerable<Api.Models.ChatChannel> channelsToMap;
213  lock (activeChatBots)
214  channelsToMap = activeChatBots.FirstOrDefault()?.Channels;
215 
216  if (channelsToMap?.Any() ?? false)
217  {
218  long providerId;
219  lock (providers)
220  providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
221  await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false);
222  }
223 
224  return;
225  }
226 
227  // map the channel if it's private and we haven't seen it
228  lock (providers)
229  {
230  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
231  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
232  if (message.User.Channel.IsPrivateChannel)
233  lock (mappedChannels)
234  {
235  if (!provider.Connected)
236  return;
237  if (!enumerable.Any())
238  {
239  ulong newId;
240  lock (synchronizationLock)
241  newId = channelIdCounter++;
242  logger.LogTrace(
243  "Mapping private channel {0}:{1} as {2}",
244  message.User.Channel.ConnectionName,
245  message.User.FriendlyName,
246  newId);
247  mappedChannels.Add(newId, new ChannelMapping
248  {
249  IsWatchdogChannel = false,
250  ProviderChannelId = message.User.Channel.RealId,
251  ProviderId = providerId,
252  Channel = message.User.Channel
253  });
254  message.User.Channel.RealId = newId;
255  }
256  else
257  message.User.Channel.RealId = enumerable.First().Key;
258  }
259  else
260  {
261  // need to add tag and isAdminChannel
262  var mapping = enumerable.First().Value;
263  message.User.Channel.Id = mapping.Channel.Id;
264  message.User.Channel.Tag = mapping.Channel.Tag;
265  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
266  }
267  }
268 
269  var splits = new List<string>(message.Content.Trim().Split(' '));
270  var address = splits[0];
271  if (address.Length > 1 && (address.Last() == ':' || address.Last() == ','))
272  address = address[0..^1];
273 
274  address = address.ToUpperInvariant();
275 
276  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
277 
278  // no mention
279  if (!addressed && !message.User.Channel.IsPrivateChannel)
280  return;
281 
282  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
283 
284  if (addressed)
285  splits.RemoveAt(0);
286 
287  if (splits.Count == 0)
288  {
289  // just a mention
290  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
291  return;
292  }
293 
294  var command = splits[0].ToUpperInvariant();
295  splits.RemoveAt(0);
296  var arguments = String.Join(" ", splits);
297 
298  try
299  {
300  ICommand GetCommand(string commandName)
301  {
302  if (!builtinCommands.TryGetValue(commandName, out var handler))
303  {
304  handler = trackingContexts
305  .Where(x => x.CustomCommands != null)
306  .SelectMany(x => x.CustomCommands)
307  .Where(x => x.Name.ToUpperInvariant() == commandName)
308  .FirstOrDefault();
309  }
310 
311  return handler;
312  }
313 
314  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
315 
316  if (command == "HELP" || command == "?")
317  {
318  string helpText;
319  if (splits.Count == 0)
320  {
321  var allCommands = builtinCommands.Select(x => x.Value).ToList();
322  allCommands.AddRange(
323  trackingContexts
324  .Where(x => x.CustomCommands != null)
325  .SelectMany(
326  x => x.CustomCommands));
327  helpText = String.Format(CultureInfo.InvariantCulture, "Available commands (Type '?' or 'help' and then a command name for more details): {0}", String.Join(", ", allCommands.Select(x => x.Name)));
328  }
329  else
330  {
331  var helpHandler = GetCommand(splits[0].ToUpperInvariant());
332  if (helpHandler != default)
333  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
334  else
335  helpText = UnknownCommandMessage;
336  }
337 
338  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
339  return;
340  }
341 
342  var commandHandler = GetCommand(command);
343 
344  if (commandHandler == default)
345  {
346  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
347  return;
348  }
349 
350  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
351  {
352  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
353  return;
354  }
355 
356  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
357  if (result != null)
358  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
359  }
360  catch (OperationCanceledException)
361  {
362  throw;
363  }
364  catch (Exception e)
365  {
366  // error bc custom commands should reply about why it failed
367  logger.LogError("Error processing chat command: {0}", e);
368  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
369  }
370  }
371 
377  async Task MonitorMessages(CancellationToken cancellationToken)
378  {
379  var messageTasks = new Dictionary<IProvider, Task<Message>>();
380  try
381  {
382  while (!cancellationToken.IsCancellationRequested)
383  {
384  // prune disconnected providers
385  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
386  messageTasks.Remove(I.Key);
387 
388  // add new ones
389  Task updatedTask;
390  lock (synchronizationLock)
391  updatedTask = connectionsUpdated.Task;
392  lock (providers)
393  foreach (var I in providers)
394  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
395  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
396 
397  if (messageTasks.Count == 0)
398  {
399  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
400  continue;
401  }
402 
403  // wait for a message
404  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
405 
406  // process completed ones
407  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
408  {
409  var message = await I.Value.ConfigureAwait(false);
410  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
411  messageTasks.Remove(I.Key);
412  }
413  }
414  }
415  catch (OperationCanceledException)
416  {
417  logger.LogTrace("Message processing loop cancelled!");
418  }
419  catch (Exception e)
420  {
421  logger.LogError("Message monitor crashed! Exception: {0}", e);
422  }
423  }
424 
426  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
427  {
428  if (newChannels == null)
429  throw new ArgumentNullException(nameof(newChannels));
430  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
431  if (provider == null)
432  return;
433  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
434  lock (activeChatBots)
435  {
436  var botToUpdate = activeChatBots.FirstOrDefault(bot => bot.Id == connectionId);
437  if (botToUpdate != null)
438  botToUpdate.Channels = newChannels
439  .Select(apiModel => new Models.ChatChannel
440  {
441  DiscordChannelId = apiModel.DiscordChannelId,
442  IrcChannel = apiModel.IrcChannel,
443  IsAdminChannel = apiModel.IsAdminChannel,
444  IsUpdatesChannel = apiModel.IsUpdatesChannel,
445  IsWatchdogChannel = apiModel.IsWatchdogChannel,
446  Tag = apiModel.Tag
447  })
448  .ToList();
449  }
450 
451  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
452  {
453  IsWatchdogChannel = x.IsWatchdogChannel == true,
454  IsUpdatesChannel = x.IsUpdatesChannel == true,
455  IsAdminChannel = x.IsAdminChannel == true,
456  ProviderChannelId = y.RealId,
457  ProviderId = connectionId,
458  Channel = y
459  });
460 
461  ulong baseId;
462  lock (synchronizationLock)
463  {
464  baseId = channelIdCounter;
465  channelIdCounter += (ulong)results.Count;
466  }
467 
468  Task trackingContextUpdateTask;
469  lock (mappedChannels)
470  {
471  lock (providers)
472  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) // aborted again
473  return;
474  foreach (var I in mappings)
475  {
476  var newId = baseId++;
477  logger.LogTrace("Mapping channel {0}:{1} as {2}", I.Channel.ConnectionName, I.Channel.FriendlyName, newId);
478  mappedChannels.Add(newId, I);
479  I.Channel.RealId = newId;
480  }
481 
482  lock (trackingContexts)
483  trackingContextUpdateTask = Task.WhenAll(
484  trackingContexts.Select(
485  x => x.UpdateChannels(
486  mappedChannels.Select(y => y.Value.Channel).ToList(),
487  cancellationToken)));
488  }
489 
490  await trackingContextUpdateTask.ConfigureAwait(false);
491  }
492 
494  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
495  {
496  if (newSettings == null)
497  throw new ArgumentNullException(nameof(newSettings));
498  IProvider provider;
499 
500  async Task DisconnectProvider(IProvider p)
501  {
502  try
503  {
504  await p.Disconnect(cancellationToken).ConfigureAwait(false);
505  }
506  finally
507  {
508  p.Dispose();
509  }
510  }
511 
512  Task disconnectTask;
513  lock (providers)
514  {
515  // raw settings changes forces a rebuild of the provider
516  if (providers.TryGetValue(newSettings.Id, out provider))
517  {
518  providers.Remove(newSettings.Id);
519  disconnectTask = DisconnectProvider(provider);
520  }
521  else
522  disconnectTask = Task.CompletedTask;
523  if (newSettings.Enabled.Value)
524  {
525  provider = providerFactory.CreateProvider(newSettings);
526  providers.Add(newSettings.Id, provider);
527  }
528  }
529 
530  lock (mappedChannels)
531  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
532  mappedChannels.Remove(I);
533 
534  await disconnectTask.ConfigureAwait(false);
535 
536  if (started)
537  {
538  if (newSettings.Enabled.Value)
539  await provider.Connect(cancellationToken).ConfigureAwait(false);
540  lock (synchronizationLock)
541  {
542  // same thread shennanigans
543  var oldOne = connectionsUpdated;
544  connectionsUpdated = new TaskCompletionSource<object>();
545  oldOne.SetResult(null);
546  }
547  }
548 
549  Task reconnectionUpdateTask = Task.CompletedTask;
550  lock (activeChatBots)
551  {
552  var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
553  if (originalChatBot != null)
554  {
555  if (originalChatBot.ReconnectionInterval != newSettings.ReconnectionInterval)
556  reconnectionUpdateTask = provider.SetReconnectInterval(newSettings.ReconnectionInterval.Value);
557 
558  activeChatBots.Remove(originalChatBot);
559  }
560 
561  activeChatBots.Add(new Models.ChatBot
562  {
563  Id = newSettings.Id,
564  ConnectionString = newSettings.ConnectionString,
565  Enabled = newSettings.Enabled,
566  Name = newSettings.Name,
567  ReconnectionInterval = newSettings.ReconnectionInterval,
568  Provider = newSettings.Provider
569  });
570  }
571 
572  await reconnectionUpdateTask.ConfigureAwait(false);
573  }
574 
576  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
577  {
578  if (message == null)
579  throw new ArgumentNullException(nameof(message));
580  if (channelIds == null)
581  throw new ArgumentNullException(nameof(channelIds));
582 
583  logger.LogTrace("Chat send \"{0}\" to channels: {1}", message, String.Join(", ", channelIds));
584 
585  return Task.WhenAll(channelIds.Select(x =>
586  {
587  ChannelMapping channelMapping;
588  lock (mappedChannels)
589  if (!mappedChannels.TryGetValue(x, out channelMapping))
590  return Task.CompletedTask;
591  IProvider provider;
592  lock (providers)
593  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
594  return Task.CompletedTask;
595  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
596  }));
597  }
598 
600  public Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
601  {
602  List<ulong> wdChannels = null;
603  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
604 
605  // so it doesn't change while we're using it
606  lock (mappedChannels)
607  {
608  if (adminOnly)
609  {
610  wdChannels = mappedChannels.Where(x => x.Value.IsAdminChannel).Select(x => x.Key).ToList();
611  if (wdChannels.Count == 0)
612  adminOnly = false;
613  }
614 
615  if (!adminOnly)
616  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
617  }
618 
619  return SendMessage(message, wdChannels, cancellationToken);
620  }
621 
623  public async Task<Func<string, string, Task>> SendDeploymentMessage(
624  Models.RevisionInformation revisionInformation,
625  Version byondVersion,
626  DateTimeOffset? estimatedCompletionTime,
627  string gitHubOwner,
628  string gitHubRepo,
629  bool localCommitPushed,
630  CancellationToken cancellationToken)
631  {
632  List<ulong> wdChannels;
633  lock (mappedChannels) // so it doesn't change while we're using it
634  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
635 
636  logger.LogTrace("Sending deployment message for RevisionInformation: {0}", revisionInformation.Id);
637 
638  var callbacks = new List<Func<string, string, Task>>();
639 
640  await Task.WhenAll(
641  wdChannels.Select(
642  async x =>
643  {
644  ChannelMapping channelMapping;
645  lock (mappedChannels)
646  if (!mappedChannels.TryGetValue(x, out channelMapping))
647  return;
648  IProvider provider;
649  lock (providers)
650  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
651  return;
652  try
653  {
654  var callback = await provider.SendUpdateMessage(
655  revisionInformation,
656  byondVersion,
657  estimatedCompletionTime,
658  gitHubOwner,
659  gitHubRepo,
660  channelMapping.ProviderChannelId,
661  localCommitPushed,
662  cancellationToken)
663  .ConfigureAwait(false);
664 
665  callbacks.Add(callback);
666  }
667  catch (Exception ex)
668  {
669  logger.LogWarning(
670  "Error sending deploy message to provider {0}! Exception: {1}",
671  channelMapping.ProviderId,
672  ex);
673  }
674  }))
675  .ConfigureAwait(false);
676 
677  return (errorMessage, dreamMakerOutput) => Task.WhenAll(callbacks.Select(x => x(errorMessage, dreamMakerOutput)));
678  }
679 
681  public async Task StartAsync(CancellationToken cancellationToken)
682  {
683  foreach (var I in commandFactory.GenerateCommands())
684  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
685  var initialChatBots = activeChatBots.ToList();
686  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
687  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
688  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
689  chatHandler = MonitorMessages(handlerCts.Token);
690  started = true;
691  }
692 
694  public async Task StopAsync(CancellationToken cancellationToken)
695  {
696  handlerCts.Cancel();
697  if (chatHandler != null)
698  await chatHandler.ConfigureAwait(false);
699  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
700  }
701 
704  {
705  if (customCommandHandler == null)
706  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
707 
708  IChatTrackingContext context = null;
709  lock (mappedChannels)
710  context = new ChatTrackingContext(
711  customCommandHandler,
712  mappedChannels.Select(y => y.Value.Channel),
713  loggerFactory.CreateLogger<ChatTrackingContext>(),
714  () =>
715  {
716  lock (trackingContexts)
717  trackingContexts.Remove(context);
718  });
719 
720  lock (trackingContexts)
721  trackingContexts.Add(context);
722 
723  return context;
724  }
725 
727  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
728  {
729  if (this.customCommandHandler != null)
730  throw new InvalidOperationException("RegisterCommandHandler() already called!");
731  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
732  }
733 
735  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
736  {
737  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
738  if (provider != null)
739  try
740  {
741  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
742  }
743  finally
744  {
745  provider.Dispose();
746  }
747  }
748 
750  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
751  {
752  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
753  List<ulong> wdChannels;
754  lock (mappedChannels) // so it doesn't change while we're using it
755  wdChannels = mappedChannels.Select(x => x.Key).ToList();
756  return SendMessage(message, wdChannels, cancellationToken);
757  }
758  }
759 }
bool IsWatchdogChannel
If Channel is a watchdog channel
Represents a mapping of a ChannelRepresentation.RealId
ulong channelIdCounter
Used for remapping ChannelRepresentation.RealIds
Definition: ChatManager.cs:112
Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
Definition: ChatManager.cs:600
async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
Change chat settings. If the ChatBot.Id is not currently in use, a new connection will be made instea...
Definition: ChatManager.cs:494
-
Task SendUpdateMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured update channels
Definition: ChatManager.cs:623
readonly ICommandFactory commandFactory
The ICommandFactory for the ChatManager
Definition: ChatManager.cs:37
ChatUser User
The Components.Chat.ChatUser who sent the Message
Definition: Message.cs:16
@@ -116,10 +115,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
bool IsAdminChannel
If this is considered a channel for admin commands
async Task< IProvider > RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
Remove a IProvider from providers and mappedChannels optionally updating the trackingContexts as well...
Definition: ChatManager.cs:171
-
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: ChatManager.cs:702
+
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: ChatManager.cs:750
Represents a message recieved by a IProvider
Definition: Message.cs:6
-
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection
Definition: ChatManager.cs:687
-
For interacting with a chat service
Definition: IProvider.cs:11
+
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection
Definition: ChatManager.cs:735
+
For interacting with a chat service
Definition: IProvider.cs:12
Task chatHandler
The Task that monitors incoming chat messages
Definition: ChatManager.cs:102
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the ChatManager
Definition: ChatManager.cs:42
@@ -131,8 +130,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For managing connected chat services
Definition: IChatManager.cs:13
string Id
Backing field for RealId. Represented as a string to avoid BYOND percision loss
+
async Task< Func< string, string, Task > > SendDeploymentMessage(Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, bool localCommitPushed, CancellationToken cancellationToken)
Send the message for a deployment to configured deployment channels.
Definition: ChatManager.cs:623
string FriendlyName
The friendly name of the user
Definition: ChatUser.cs:30
-
IChatTrackingContext CreateTrackingContext()
Start tracking Commands.CustomCommands and ChannelRepresentations.
Definition: ChatManager.cs:655
+
IChatTrackingContext CreateTrackingContext()
Start tracking Commands.CustomCommands and ChannelRepresentations.
Definition: ChatManager.cs:703
string ConnectionName
The name of the connection the ChannelRepresentation belongs to
@@ -146,15 +146,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
bool started
If StartAsync(CancellationToken) has been called
Definition: ChatManager.cs:117
readonly IIOManager ioManager
The IIOManager for the ChatManager
Definition: ChatManager.cs:32
- +
async Task ChangeChannels(long connectionId, IEnumerable< Api.Models.ChatChannel > newChannels, CancellationToken cancellationToken)
Change chat channels
Definition: ChatManager.cs:426
-
string BotMention
The string that indicates the IProvider was mentioned
Definition: IProvider.cs:21
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:646
-
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: ChatManager.cs:679
+
string BotMention
The string that indicates the IProvider was mentioned
Definition: IProvider.cs:22
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:694
+
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: ChatManager.cs:727
TaskCompletionSource< object > connectionsUpdated
The TaskCompletionSource<TResult> that completes when ChatBots change
Definition: ChatManager.cs:107
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:633
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:681
readonly List< Models.ChatBot > activeChatBots
The active Models.ChatBot for the ChatManager
Definition: ChatManager.cs:87
Handles Commands.ICommands that map to those defined in a IChatTrackingContext
diff --git a/_chat_manager_factory_8cs.html b/_chat_manager_factory_8cs.html index 5b00d398aa..bce10dc310 100644 --- a/_chat_manager_factory_8cs.html +++ b/_chat_manager_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_manager_factory_8cs_source.html b/_chat_manager_factory_8cs_source.html index d7211871ea..103abf141e 100644 --- a/_chat_manager_factory_8cs_source.html +++ b/_chat_manager_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_message_8cs.html b/_chat_message_8cs.html index c714255fad..19a3f0f2f8 100644 --- a/_chat_message_8cs.html +++ b/_chat_message_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_message_8cs_source.html b/_chat_message_8cs_source.html index 84c593a510..80e24c97e8 100644 --- a/_chat_message_8cs_source.html +++ b/_chat_message_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_provider_8cs.html b/_chat_provider_8cs.html index 99c8a0daaf..c7ae5c8597 100644 --- a/_chat_provider_8cs.html +++ b/_chat_provider_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_provider_8cs_source.html b/_chat_provider_8cs_source.html index 4e2b250858..2301361c20 100644 --- a/_chat_provider_8cs_source.html +++ b/_chat_provider_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_tracking_context_8cs.html b/_chat_tracking_context_8cs.html index 4bafac7efc..f569f1b54b 100644 --- a/_chat_tracking_context_8cs.html +++ b/_chat_tracking_context_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_tracking_context_8cs_source.html b/_chat_tracking_context_8cs_source.html index cc814fb77e..8dcbcd2382 100644 --- a/_chat_tracking_context_8cs_source.html +++ b/_chat_tracking_context_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_update_8cs.html b/_chat_update_8cs.html index c30faea65e..dbd3d5e9d8 100644 --- a/_chat_update_8cs.html +++ b/_chat_update_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_update_8cs_source.html b/_chat_update_8cs_source.html index 7fb6b7fc4d..dd587ba1c2 100644 --- a/_chat_update_8cs_source.html +++ b/_chat_update_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_user_8cs.html b/_chat_user_8cs.html index 3803ddec2c..f7cd3d4516 100644 --- a/_chat_user_8cs.html +++ b/_chat_user_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_chat_user_8cs_source.html b/_chat_user_8cs_source.html index 242dbac3c9..91ac2d84c2 100644 --- a/_chat_user_8cs_source.html +++ b/_chat_user_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_claims_injector_8cs.html b/_claims_injector_8cs.html index 194412c26d..5f77a587de 100644 --- a/_claims_injector_8cs.html +++ b/_claims_injector_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_claims_injector_8cs_source.html b/_claims_injector_8cs_source.html index ac77b2044f..16bb915e42 100644 --- a/_claims_injector_8cs_source.html +++ b/_claims_injector_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_client_exception_8cs.html b/_client_exception_8cs.html index e9294f2d9a..a16dd73351 100644 --- a/_client_exception_8cs.html +++ b/_client_exception_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_client_exception_8cs_source.html b/_client_exception_8cs_source.html index e7a8d48c8a..5d92fb8980 100644 --- a/_client_exception_8cs_source.html +++ b/_client_exception_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_combined_topic_response_8cs.html b/_combined_topic_response_8cs.html index 67643abffd..a06ae181d1 100644 --- a/_combined_topic_response_8cs.html +++ b/_combined_topic_response_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_combined_topic_response_8cs_source.html b/_combined_topic_response_8cs_source.html index 92cad38549..056c4b6ca6 100644 --- a/_combined_topic_response_8cs_source.html +++ b/_combined_topic_response_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_command_factory_8cs.html b/_command_factory_8cs.html index 773918c8a8..d8e8f451e1 100644 --- a/_command_factory_8cs.html +++ b/_command_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_command_factory_8cs_source.html b/_command_factory_8cs_source.html index a3073df571..fe5c6b0f31 100644 --- a/_command_factory_8cs_source.html +++ b/_command_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -121,7 +121,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
CommandFactory(IAssemblyInformationProvider assemblyInformationProvider, IByondManager byondManager, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
Construct a CommandFactory
- +
readonly IRepositoryManager repositoryManager
The IRepositoryManager for the CommandFactory
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:13
diff --git a/_components_2_session_2_dual_reattach_information_8cs.html b/_components_2_session_2_dual_reattach_information_8cs.html index e37a268b98..7b8e22d23d 100644 --- a/_components_2_session_2_dual_reattach_information_8cs.html +++ b/_components_2_session_2_dual_reattach_information_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_components_2_session_2_dual_reattach_information_8cs_source.html b/_components_2_session_2_dual_reattach_information_8cs_source.html index 6fb96a19f9..664cd47b9a 100644 --- a/_components_2_session_2_dual_reattach_information_8cs_source.html +++ b/_components_2_session_2_dual_reattach_information_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_components_2_session_2_reattach_information_8cs.html b/_components_2_session_2_reattach_information_8cs.html index 02cf0d7e86..009163fe8f 100644 --- a/_components_2_session_2_reattach_information_8cs.html +++ b/_components_2_session_2_reattach_information_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_components_2_session_2_reattach_information_8cs_source.html b/_components_2_session_2_reattach_information_8cs_source.html index 6c5684335a..f12ffd7bae 100644 --- a/_components_2_session_2_reattach_information_8cs_source.html +++ b/_components_2_session_2_reattach_information_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_components_2_watchdog_2_i_watchdog_8cs.html b/_components_2_watchdog_2_i_watchdog_8cs.html index 9e3d485725..9302d9d260 100644 --- a/_components_2_watchdog_2_i_watchdog_8cs.html +++ b/_components_2_watchdog_2_i_watchdog_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_components_2_watchdog_2_i_watchdog_8cs_source.html b/_components_2_watchdog_2_i_watchdog_8cs_source.html index 66a2e32248..b0b4c407d6 100644 --- a/_components_2_watchdog_2_i_watchdog_8cs_source.html +++ b/_components_2_watchdog_2_i_watchdog_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_components_2_watchdog_2_i_watchdog_factory_8cs.html b/_components_2_watchdog_2_i_watchdog_factory_8cs.html index a2b3d9e1d3..96c5608dd5 100644 --- a/_components_2_watchdog_2_i_watchdog_factory_8cs.html +++ b/_components_2_watchdog_2_i_watchdog_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_components_2_watchdog_2_i_watchdog_factory_8cs_source.html b/_components_2_watchdog_2_i_watchdog_factory_8cs_source.html index 630120a81d..2353eff9b8 100644 --- a/_components_2_watchdog_2_i_watchdog_factory_8cs_source.html +++ b/_components_2_watchdog_2_i_watchdog_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_components_2_watchdog_2_watchdog_factory_8cs.html b/_components_2_watchdog_2_watchdog_factory_8cs.html index eeb0c0c5b0..6f87e7469e 100644 --- a/_components_2_watchdog_2_watchdog_factory_8cs.html +++ b/_components_2_watchdog_2_watchdog_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_components_2_watchdog_2_watchdog_factory_8cs_source.html b/_components_2_watchdog_2_watchdog_factory_8cs_source.html index 92a8924f63..de94bb825f 100644 --- a/_components_2_watchdog_2_watchdog_factory_8cs_source.html +++ b/_components_2_watchdog_2_watchdog_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -116,7 +116,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IWatchdog CreateWatchdog(IChatManager chat, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, ISessionControllerFactory sessionControllerFactory, IIOManager ioManager, Api.Models.Instance instance, DreamDaemonSettings settings)
Creates a IWatchdog
-
bool UseExperimentalWatchdog
If the Components.Watchdog.ExperimentalWatchdog should be used.
+
bool UseExperimentalWatchdog
If the Components.Watchdog.ExperimentalWatchdog should be used.
Manages the runtime of Jobs
Definition: IJobManager.cs:13
@@ -129,7 +129,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:13
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_configuration_8cs.html b/_configuration_8cs.html index 7d2bc40e75..d049764894 100644 --- a/_configuration_8cs.html +++ b/_configuration_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_8cs_source.html b/_configuration_8cs_source.html index 0952053fa5..8af446cd7a 100644 --- a/_configuration_8cs_source.html +++ b/_configuration_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_client_8cs.html b/_configuration_client_8cs.html index 8e29e5af8f..009459e37c 100644 --- a/_configuration_client_8cs.html +++ b/_configuration_client_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_client_8cs_source.html b/_configuration_client_8cs_source.html index 36c18f087c..5944fb9ae1 100644 --- a/_configuration_client_8cs_source.html +++ b/_configuration_client_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_controller_8cs.html b/_configuration_controller_8cs.html index d16eaea4cb..904336076e 100644 --- a/_configuration_controller_8cs.html +++ b/_configuration_controller_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_controller_8cs_source.html b/_configuration_controller_8cs_source.html index 9dd17e273a..1c03a62aa3 100644 --- a/_configuration_controller_8cs_source.html +++ b/_configuration_controller_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -133,7 +133,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task< IActionResult > Directory(string directoryPath, CancellationToken cancellationToken)
Get the contents of a directory at a directoryPath
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
- +
ConfigurationType ConfigurationType
If ConfigurationFile can be used on the Instance
Definition: Instance.cs:40
diff --git a/_configuration_file_8cs.html b/_configuration_file_8cs.html index 6fa9b4aa54..8a974568e0 100644 --- a/_configuration_file_8cs.html +++ b/_configuration_file_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_file_8cs_source.html b/_configuration_file_8cs_source.html index 27eb0925a0..d470540a1b 100644 --- a/_configuration_file_8cs_source.html +++ b/_configuration_file_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_rights_8cs.html b/_configuration_rights_8cs.html index afef0efceb..88c73f546c 100644 --- a/_configuration_rights_8cs.html +++ b/_configuration_rights_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_rights_8cs_source.html b/_configuration_rights_8cs_source.html index 60e2e50df3..0a021ad6e6 100644 --- a/_configuration_rights_8cs_source.html +++ b/_configuration_rights_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_type_8cs.html b/_configuration_type_8cs.html index e89af9908b..28088b3627 100644 --- a/_configuration_type_8cs.html +++ b/_configuration_type_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_configuration_type_8cs_source.html b/_configuration_type_8cs_source.html index 4a5be51ac8..b39b6b23a6 100644 --- a/_configuration_type_8cs_source.html +++ b/_configuration_type_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_conflict_exception_8cs.html b/_conflict_exception_8cs.html index a3f48cc45a..1463008b0c 100644 --- a/_conflict_exception_8cs.html +++ b/_conflict_exception_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_conflict_exception_8cs_source.html b/_conflict_exception_8cs_source.html index c422f4a110..7dd324c89b 100644 --- a/_conflict_exception_8cs_source.html +++ b/_conflict_exception_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_console_2_program_8cs.html b/_console_2_program_8cs.html index a5a8bfac37..ebd4acd9f5 100644 --- a/_console_2_program_8cs.html +++ b/_console_2_program_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_console_2_program_8cs_source.html b/_console_2_program_8cs_source.html index fe006fb16d..0542af964e 100644 --- a/_console_2_program_8cs_source.html +++ b/_console_2_program_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_console_8cs.html b/_console_8cs.html index ff6e03f7c8..f017ab925f 100644 --- a/_console_8cs.html +++ b/_console_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_console_8cs_source.html b/_console_8cs_source.html index 3784d4a32c..4bdbb56ede 100644 --- a/_console_8cs_source.html +++ b/_console_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_control_panel_configuration_8cs.html b/_control_panel_configuration_8cs.html index 732f9efc1b..0dc873bbd7 100644 --- a/_control_panel_configuration_8cs.html +++ b/_control_panel_configuration_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_control_panel_configuration_8cs_source.html b/_control_panel_configuration_8cs_source.html index 652b795df0..f656e034ae 100644 --- a/_control_panel_configuration_8cs_source.html +++ b/_control_panel_configuration_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_cryptography_suite_8cs.html b/_cryptography_suite_8cs.html index cbf4db57f7..e203c94e9f 100644 --- a/_cryptography_suite_8cs.html +++ b/_cryptography_suite_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_cryptography_suite_8cs_source.html b/_cryptography_suite_8cs_source.html index c56ac11b50..70b2f6a640 100644 --- a/_cryptography_suite_8cs_source.html +++ b/_cryptography_suite_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_custom_command_8cs.html b/_custom_command_8cs.html index 73699e2b16..83b9ed6040 100644 --- a/_custom_command_8cs.html +++ b/_custom_command_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_custom_command_8cs_source.html b/_custom_command_8cs_source.html index ddff44444b..9f329a8d73 100644 --- a/_custom_command_8cs_source.html +++ b/_custom_command_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_d_m_api_constants_8cs.html b/_d_m_api_constants_8cs.html index c3edea00ff..55de727f21 100644 --- a/_d_m_api_constants_8cs.html +++ b/_d_m_api_constants_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_d_m_api_constants_8cs_source.html b/_d_m_api_constants_8cs_source.html index 33bee997b4..e8c732361b 100644 --- a/_d_m_api_constants_8cs_source.html +++ b/_d_m_api_constants_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DMApiConstants.cs
-Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Newtonsoft.Json.Serialization;
3 using System;
5 
6 namespace Tgstation.Server.Host.Components.Interop
7 {
11  static class DMApiConstants
12  {
16  public const string ParamApiVersion = "server_service_version";
17 
21  public const string ParamServerPort = "tgs_port";
22 
26  public const string ParamAccessIdentifier = "tgs_key";
27 
31  public const string TopicData = "tgs_data";
32 
36  public static readonly Version Version = new Version(5, 2, 1);
37 
41  public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
42  {
43  ContractResolver = new DefaultContractResolver
44  {
45  NamingStrategy = new CamelCaseNamingStrategy()
46  },
47  Converters = new JsonConverter[]
48  {
49  new VersionConverter(),
50  new BoolConverter()
51  },
52  DefaultValueHandling = DefaultValueHandling.Ignore,
53  ReferenceLoopHandling = ReferenceLoopHandling.Ignore
54  };
55  }
56 }
+Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Newtonsoft.Json.Serialization;
3 using System;
5 
6 namespace Tgstation.Server.Host.Components.Interop
7 {
11  static class DMApiConstants
12  {
16  public const string ParamApiVersion = "server_service_version";
17 
21  public const string ParamServerPort = "tgs_port";
22 
26  public const string ParamAccessIdentifier = "tgs_key";
27 
31  public const string TopicData = "tgs_data";
32 
36  public static readonly Version Version = new Version(5, 2, 2);
37 
41  public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings
42  {
43  ContractResolver = new DefaultContractResolver
44  {
45  NamingStrategy = new CamelCaseNamingStrategy()
46  },
47  Converters = new JsonConverter[]
48  {
49  new VersionConverter(),
50  new BoolConverter()
51  },
52  DefaultValueHandling = DefaultValueHandling.Ignore,
53  ReferenceLoopHandling = ReferenceLoopHandling.Ignore
54  };
55  }
56 }
Constants used for communication with the DMAPI
diff --git a/_d_m_api_parameters_8cs.html b/_d_m_api_parameters_8cs.html index 63711f231f..308bb09b99 100644 --- a/_d_m_api_parameters_8cs.html +++ b/_d_m_api_parameters_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_d_m_api_parameters_8cs_source.html b/_d_m_api_parameters_8cs_source.html index 119a5d77a5..88dceb62ab 100644 --- a/_d_m_api_parameters_8cs_source.html +++ b/_d_m_api_parameters_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_d_m_api_response_8cs.html b/_d_m_api_response_8cs.html index 69dd3e67b8..1206ca3a34 100644 --- a/_d_m_api_response_8cs.html +++ b/_d_m_api_response_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_d_m_api_response_8cs_source.html b/_d_m_api_response_8cs_source.html index 04ddbc3662..0745214e02 100644 --- a/_d_m_api_response_8cs_source.html +++ b/_d_m_api_response_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_database_collection_8cs.html b/_database_collection_8cs.html index 2ee6e50704..3229bb4117 100644 --- a/_database_collection_8cs.html +++ b/_database_collection_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_database_collection_8cs_source.html b/_database_collection_8cs_source.html index fe0241d1ba..709d1de3ba 100644 --- a/_database_collection_8cs_source.html +++ b/_database_collection_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,12 +94,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseCollection.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Collections;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Linq.Expressions;
7 using System.Threading;
8 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Database
11 {
13  sealed class DatabaseCollection<TModel> : IDatabaseCollection<TModel> where TModel : class
14  {
18  readonly DbSet<TModel> dbSet;
19 
24  public DatabaseCollection(DbSet<TModel> dbSet)
25  {
26  this.dbSet = dbSet ?? throw new ArgumentNullException(nameof(dbSet));
27  }
28 
30  public IEnumerable<TModel> Local => dbSet.Local;
31 
33  public Type ElementType => dbSet.AsQueryable().ElementType;
34 
36  public Expression Expression => dbSet.AsQueryable().Expression;
37 
39  public IQueryProvider Provider => dbSet.AsQueryable().Provider;
40 
42  public void Add(TModel model) => dbSet.Add(model);
43 
45  public void AddRange(IEnumerable<TModel> models) => dbSet.AddRange(models);
46 
48  public void Attach(TModel model) => dbSet.Attach(model);
49 
51  public Task ForEachAsync(Action<TModel> action, CancellationToken cancellationToken) => dbSet
52  .AsAsyncEnumerable()
53  .ForEachAsync(action, cancellationToken);
54 
56  public IEnumerator<TModel> GetEnumerator() => dbSet.AsQueryable().GetEnumerator();
57 
59  public void Remove(TModel model) => dbSet.Remove(model);
60 
62  public void RemoveRange(IEnumerable<TModel> models) => dbSet.RemoveRange(models);
63 
65  public Task<List<TModel>> ToListAsync(CancellationToken cancellationToken) => dbSet.AsQueryable().ToListAsync(cancellationToken);
66 
68  IEnumerator IEnumerable.GetEnumerator() => dbSet.AsQueryable().GetEnumerator();
69  }
70 }
DatabaseCollection(DbSet< TModel > dbSet)
Initializes a new instance of the DatabaseCollection<TModel> .
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Collections;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Linq.Expressions;
7 using System.Threading;
8 
9 namespace Tgstation.Server.Host.Database
10 {
12  sealed class DatabaseCollection<TModel> : IDatabaseCollection<TModel> where TModel : class
13  {
17  readonly DbSet<TModel> dbSet;
18 
23  public DatabaseCollection(DbSet<TModel> dbSet)
24  {
25  this.dbSet = dbSet ?? throw new ArgumentNullException(nameof(dbSet));
26  }
27 
29  public IEnumerable<TModel> Local => dbSet.Local;
30 
32  public Type ElementType => dbSet.AsQueryable().ElementType;
33 
35  public Expression Expression => dbSet.AsQueryable().Expression;
36 
38  public IQueryProvider Provider => dbSet.AsQueryable().Provider;
39 
41  public void Add(TModel model) => dbSet.Add(model);
42 
44  public void AddRange(IEnumerable<TModel> models) => dbSet.AddRange(models);
45 
47  public void Attach(TModel model) => dbSet.Attach(model);
48 
50  public IAsyncEnumerator<TModel> GetAsyncEnumerator(CancellationToken cancellationToken = default) => dbSet.AsAsyncEnumerable().GetAsyncEnumerator();
51 
53  public IEnumerator<TModel> GetEnumerator() => dbSet.AsQueryable().GetEnumerator();
54 
56  public void Remove(TModel model) => dbSet.Remove(model);
57 
59  public void RemoveRange(IEnumerable<TModel> models) => dbSet.RemoveRange(models);
60 
62  IEnumerator IEnumerable.GetEnumerator() => dbSet.AsQueryable().GetEnumerator();
63  }
64 }
DatabaseCollection(DbSet< TModel > dbSet)
Initializes a new instance of the DatabaseCollection<TModel> .
- -
readonly DbSet< TModel > dbSet
The backing DbSet<TEntity>.
+ +
readonly DbSet< TModel > dbSet
The backing DbSet<TEntity>.
- +
diff --git a/_database_configuration_8cs.html b/_database_configuration_8cs.html index 0e0d624ac2..e073eadad2 100644 --- a/_database_configuration_8cs.html +++ b/_database_configuration_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

Classes

class  Tgstation.Server.Host.Configuration.DatabaseConfigurationConfiguration options for the Database.DatabaseContext<TParentContext> More...
Configuration options for the Database.DatabaseContext More...
  @@ -97,7 +97,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Newtonsoft.Json.Converters;
3 
4 namespace Tgstation.Server.Host.Configuration
5 {
9  sealed class DatabaseConfiguration
10  {
14  public const string Section = "Database";
15 
19  [JsonConverter(typeof(StringEnumConverter))]
20  public DatabaseType DatabaseType { get; set; }
21 
25  public bool ResetAdminPassword { get; set; }
26 
30  public string ConnectionString { get; set; }
31 
35  public bool DropDatabase { get; set; }
36 
40  public bool DesignTime { get; set; }
41 
45  public string MySqlServerVersion { get; set; }
46  }
47 }
-
Configuration options for the Database.DatabaseContext<TParentContext>
+
Configuration options for the Database.DatabaseContext
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
diff --git a/_database_connection_factory_8cs.html b/_database_connection_factory_8cs.html index 224de0fa5c..1dc4414867 100644 --- a/_database_connection_factory_8cs.html +++ b/_database_connection_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_database_connection_factory_8cs_source.html b/_database_connection_factory_8cs_source.html index 9c10da277b..0cf2f4a1b1 100644 --- a/_database_connection_factory_8cs_source.html +++ b/_database_connection_factory_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,17 +94,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseConnectionFactory.cs
-Go to the documentation of this file.
1 using System;
2 using System.Data.Common;
3 using System.Data.SqlClient;
4 using Microsoft.Data.Sqlite;
5 using MySql.Data.MySqlClient;
7 
8 namespace Tgstation.Server.Host.Database
9 {
12  {
14  public DbConnection CreateConnection(string connectionString, DatabaseType databaseType)
15  {
16  if (connectionString == null)
17  throw new ArgumentNullException(nameof(connectionString));
18 
19  switch (databaseType)
20  {
21  case DatabaseType.MariaDB:
22  case DatabaseType.MySql:
23  return new MySqlConnection
24  {
25  ConnectionString = connectionString
26  };
27  case DatabaseType.SqlServer:
28  return new SqlConnection
29  {
30  ConnectionString = connectionString
31  };
32  case DatabaseType.Sqlite:
33  return new SqliteConnection
34  {
35  ConnectionString = connectionString
36  };
37  default:
38  throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType, "Invalid DatabaseType!");
39  }
40  }
41  }
42 }
+Go to the documentation of this file.
1 using System;
2 using System.Data.Common;
3 using System.Data.SqlClient;
4 using Microsoft.Data.Sqlite;
5 using MySql.Data.MySqlClient;
6 using Npgsql;
8 
9 namespace Tgstation.Server.Host.Database
10 {
13  {
15  public DbConnection CreateConnection(string connectionString, DatabaseType databaseType)
16  {
17  if (connectionString == null)
18  throw new ArgumentNullException(nameof(connectionString));
19 
20  switch (databaseType)
21  {
22  case DatabaseType.MariaDB:
23  case DatabaseType.MySql:
24  return new MySqlConnection
25  {
26  ConnectionString = connectionString
27  };
28  case DatabaseType.SqlServer:
29  return new SqlConnection
30  {
31  ConnectionString = connectionString
32  };
33  case DatabaseType.Sqlite:
34  return new SqliteConnection
35  {
36  ConnectionString = connectionString
37  };
38  case DatabaseType.PostgresSql:
39  return new NpgsqlConnection
40  {
41  ConnectionString = connectionString
42  };
43  default:
44  throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType, "Invalid DatabaseType!");
45  }
46  }
47  }
48 }
+ -
DbConnection CreateConnection(string connectionString, DatabaseType databaseType)
Create a DbConnection
+
DbConnection CreateConnection(string connectionString, DatabaseType databaseType)
Create a DbConnection
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
- +
@@ -102,7 +102,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

diff --git a/_database_configuration_8cs_source.html b/_database_configuration_8cs_source.html index 6a72a00620..55d003a498 100644 --- a/_database_configuration_8cs_source.html +++ b/_database_configuration_8cs_source.html @@ -25,7 +25,7 @@

tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
- +

Classes

class  Tgstation.Server.Host.Database.DatabaseContext< TParentContext >
class  Tgstation.Server.Host.Database.DatabaseContext
 Backend abstract implementation of IDatabaseContext More...
 
diff --git a/_database_context_8cs_source.html b/_database_context_8cs_source.html index 9db746af6b..ea41ebbeae 100644 --- a/_database_context_8cs_source.html +++ b/_database_context_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,70 +94,70 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseContext.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.EntityFrameworkCore.Infrastructure;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Logging;
6 using Microsoft.Extensions.Options;
7 using System;
8 using System.Globalization;
9 using System.Linq;
10 using System.Threading;
11 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Database
17 {
22 #pragma warning disable CA1506 // TODO: Decomplexify
23  abstract class DatabaseContext<TParentContext> : DbContext, IDatabaseContext where TParentContext : DbContext
24  {
27 
31  public DbSet<User> Users { get; set; }
32 
36  public DbSet<Instance> Instances { get; set; }
37 
41  public DbSet<CompileJob> CompileJobs { get; set; }
42 
46  public DbSet<RevisionInformation> RevisionInformations { get; set; }
47 
51  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
52 
56  public DbSet<ChatBot> ChatBots { get; set; }
57 
61  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
62 
66  public DbSet<RepositorySettings> RepositorySettings { get; set; }
67 
71  public DbSet<InstanceUser> InstanceUsers { get; set; }
72 
76  public DbSet<ChatChannel> ChatChannels { get; set; }
77 
81  public DbSet<Job> Jobs { get; set; }
82 
86  public DbSet<ReattachInformation> ReattachInformations { get; set; }
87 
91  public DbSet<DualReattachInformation> WatchdogReattachInformations { get; set; }
92 
96  public DbSet<TestMerge> TestMerges { get; set; }
97 
101  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
102 
106  protected ILogger Logger { get; }
107 
112 
115 
118 
121 
124 
127 
130 
133 
136 
139 
142 
145 
148 
151 
156 
161 
166 
171 
176 
181 
186 
191 
196 
201 
206 
211 
216 
221 
229  public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
230  {
231  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
232  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
233  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
234 
235  usersCollection = new DatabaseCollection<User>(Users);
236  instancesCollection = new DatabaseCollection<Instance>(Instances);
237  instanceUsersCollection = new DatabaseCollection<InstanceUser>(InstanceUsers);
238  compileJobsCollection = new DatabaseCollection<CompileJob>(CompileJobs);
239  repositorySettingsCollection = new DatabaseCollection<RepositorySettings>(RepositorySettings);
240  dreamMakerSettingsCollection = new DatabaseCollection<DreamMakerSettings>(DreamMakerSettings);
241  dreamDaemonSettingsCollection = new DatabaseCollection<DreamDaemonSettings>(DreamDaemonSettings);
242  chatBotsCollection = new DatabaseCollection<ChatBot>(ChatBots);
243  chatChannelsCollection = new DatabaseCollection<ChatChannel>(ChatChannels);
244  revisionInformationsCollection = new DatabaseCollection<RevisionInformation>(RevisionInformations);
245  jobsCollection = new DatabaseCollection<Job>(Jobs);
246  reattachInformationsCollection = new DatabaseCollection<ReattachInformation>(ReattachInformations);
247  watchdogReattachInformationsCollection = new DatabaseCollection<DualReattachInformation>(WatchdogReattachInformations);
248  }
249 
251  protected override void OnModelCreating(ModelBuilder modelBuilder)
252  {
253  // Setup our more complex database relations
254  Logger.LogTrace("Building entity framework context...");
255  base.OnModelCreating(modelBuilder);
256 
257  var userModel = modelBuilder.Entity<User>();
258  userModel.HasIndex(x => x.CanonicalName).IsUnique();
259  userModel.HasIndex(x => x.SystemIdentifier).IsUnique();
260  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
261 
262  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
263 
264  var revInfo = modelBuilder.Entity<RevisionInformation>();
265  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
266  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Cascade);
267  revInfo.HasIndex(x => new { x.InstanceId, x.CommitSha }).IsUnique();
268 
269  // IMPORTANT: When an instance is deleted (detached) it cascades into the maze of revinfo/testmerge/ritm/compilejob/job/ri relations
270  // This maze starts at revInfo and jobs
271  // jobs takes care of deleting compile jobs and ris
272  // rev info takes care of the rest
273  // Break the link here so the db doesn't shit itself complaining about cascading deletes
274  // EF will handle making the right query to destroy everything
275  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.ClientNoAction);
276 
277  // Also break the link between ritm and testmerge so it doesn't cycle in a triangle with rev info
278  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.ClientNoAction);
279 
280  var compileJob = modelBuilder.Entity<CompileJob>();
281  compileJob.HasIndex(x => x.DirectoryName);
282  compileJob.HasOne(x => x.Job).WithOne().OnDelete(DeleteBehavior.Cascade);
283 
284  var chatChannel = modelBuilder.Entity<ChatChannel>();
285  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
286  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
287  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
288 
289  modelBuilder.Entity<ChatBot>().HasIndex(x => new { x.InstanceId, x.Name }).IsUnique();
290 
291  var instanceModel = modelBuilder.Entity<Instance>();
292  instanceModel.HasIndex(x => x.Path).IsUnique();
293  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
294  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
295  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
296  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
297  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
298  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
299  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
300  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
301  }
302 
304  public async Task Initialize(CancellationToken cancellationToken)
305  {
306  ValidateDatabaseType();
307 
309  {
310  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
311  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
312  }
313 
314  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
315  var wasEmpty = !migrations.Any();
316 
317  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
318  {
319  Logger.LogInformation("Migrating database...");
320  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
321  }
322  else
323  Logger.LogDebug("No migrations to apply.");
324 
325  wasEmpty |= (await Users.AsQueryable().CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
326 
327  if (wasEmpty)
328  {
329  Logger.LogInformation("Seeding database...");
330  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
331  }
332  else
333  {
335  {
336  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
337  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
338  }
339 
340  await databaseSeeder.SanitizeDatabase(this, cancellationToken).ConfigureAwait(false);
341  }
342  }
343 
345  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
346 
348  public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
349  {
350  if (version == null)
351  throw new ArgumentNullException(nameof(version));
352  if (version < new Version(4, 0))
353  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
354 
355  // Update this with new migrations as they are made
356  string targetMigration = null;
357  if (version < new Version(4, 1, 0))
358  throw new NotSupportedException("Cannot migrate below version 4.1.0!");
359  else if (version < new Version(4, 2, 0))
360  targetMigration = DatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete);
361 
362  if (targetMigration == null)
363  {
364  Logger.LogDebug("No down migration required.");
365  return;
366  }
367 
368  string migrationSubstitution;
369  switch (DatabaseType)
370  {
371  case DatabaseType.SqlServer:
372  // already setup
373  migrationSubstitution = null;
374  break;
375  case DatabaseType.MySql:
376  case DatabaseType.MariaDB:
377  migrationSubstitution = "MY{0}";
378  break;
379  case DatabaseType.Sqlite:
380  migrationSubstitution = "SL{0}";
381  break;
382  default:
383  throw new InvalidOperationException($"Invalid DatabaseType: {DatabaseType}");
384  }
385 
386  if (migrationSubstitution != null)
387  targetMigration = String.Format(CultureInfo.InvariantCulture, migrationSubstitution, targetMigration.Substring(2));
388 
389  // even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
390  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
391  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
392 
393  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
394  try
395  {
396  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
397  }
398  catch (Exception e)
399  {
400  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
401  }
402  }
403 
407  protected abstract void ValidateDatabaseType();
408  }
409 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.EntityFrameworkCore.Infrastructure;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Logging;
6 using Microsoft.Extensions.Options;
7 using System;
8 using System.Globalization;
9 using System.Linq;
10 using System.Threading;
11 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Database
17 {
21 #pragma warning disable CA1506 // TODO: Decomplexify
23  {
26 
30  public DbSet<User> Users { get; set; }
31 
35  public DbSet<Instance> Instances { get; set; }
36 
40  public DbSet<CompileJob> CompileJobs { get; set; }
41 
45  public DbSet<RevisionInformation> RevisionInformations { get; set; }
46 
50  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
51 
55  public DbSet<ChatBot> ChatBots { get; set; }
56 
60  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
61 
65  public DbSet<RepositorySettings> RepositorySettings { get; set; }
66 
70  public DbSet<InstanceUser> InstanceUsers { get; set; }
71 
75  public DbSet<ChatChannel> ChatChannels { get; set; }
76 
80  public DbSet<Job> Jobs { get; set; }
81 
85  public DbSet<ReattachInformation> ReattachInformations { get; set; }
86 
90  public DbSet<DualReattachInformation> WatchdogReattachInformations { get; set; }
91 
95  public DbSet<TestMerge> TestMerges { get; set; }
96 
100  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
101 
105  protected ILogger Logger { get; }
106 
111 
114 
117 
120 
123 
126 
129 
132 
135 
138 
141 
144 
147 
150 
155 
160 
165 
170 
175 
180 
185 
190 
195 
200 
205 
210 
215 
220 
228  public DatabaseContext(DbContextOptions dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
229  {
230  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
231  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
232  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
233 
234  usersCollection = new DatabaseCollection<User>(Users);
235  instancesCollection = new DatabaseCollection<Instance>(Instances);
236  instanceUsersCollection = new DatabaseCollection<InstanceUser>(InstanceUsers);
237  compileJobsCollection = new DatabaseCollection<CompileJob>(CompileJobs);
238  repositorySettingsCollection = new DatabaseCollection<RepositorySettings>(RepositorySettings);
239  dreamMakerSettingsCollection = new DatabaseCollection<DreamMakerSettings>(DreamMakerSettings);
240  dreamDaemonSettingsCollection = new DatabaseCollection<DreamDaemonSettings>(DreamDaemonSettings);
241  chatBotsCollection = new DatabaseCollection<ChatBot>(ChatBots);
242  chatChannelsCollection = new DatabaseCollection<ChatChannel>(ChatChannels);
243  revisionInformationsCollection = new DatabaseCollection<RevisionInformation>(RevisionInformations);
244  jobsCollection = new DatabaseCollection<Job>(Jobs);
245  reattachInformationsCollection = new DatabaseCollection<ReattachInformation>(ReattachInformations);
246  watchdogReattachInformationsCollection = new DatabaseCollection<DualReattachInformation>(WatchdogReattachInformations);
247  }
248 
250  protected override void OnModelCreating(ModelBuilder modelBuilder)
251  {
252  // Setup our more complex database relations
253  Logger.LogTrace("Building entity framework context...");
254  base.OnModelCreating(modelBuilder);
255 
256  var userModel = modelBuilder.Entity<User>();
257  userModel.HasIndex(x => x.CanonicalName).IsUnique();
258  userModel.HasIndex(x => x.SystemIdentifier).IsUnique();
259  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
260 
261  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
262 
263  var revInfo = modelBuilder.Entity<RevisionInformation>();
264  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
265  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Cascade);
266  revInfo.HasIndex(x => new { x.InstanceId, x.CommitSha }).IsUnique();
267 
268  // IMPORTANT: When an instance is deleted (detached) it cascades into the maze of revinfo/testmerge/ritm/compilejob/job/ri relations
269  // This maze starts at revInfo and jobs
270  // jobs takes care of deleting compile jobs and ris
271  // rev info takes care of the rest
272  // Break the link here so the db doesn't shit itself complaining about cascading deletes
273  // EF will handle making the right query to destroy everything
274  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.ClientNoAction);
275 
276  // Also break the link between ritm and testmerge so it doesn't cycle in a triangle with rev info
277  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.ClientNoAction);
278 
279  var compileJob = modelBuilder.Entity<CompileJob>();
280  compileJob.HasIndex(x => x.DirectoryName);
281  compileJob.HasOne(x => x.Job).WithOne().OnDelete(DeleteBehavior.Cascade);
282 
283  var chatChannel = modelBuilder.Entity<ChatChannel>();
284  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
285  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
286  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
287 
288  modelBuilder.Entity<ChatBot>().HasIndex(x => new { x.InstanceId, x.Name }).IsUnique();
289 
290  var instanceModel = modelBuilder.Entity<Instance>();
291  instanceModel.HasIndex(x => x.Path).IsUnique();
292  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
293  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
294  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
295  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
296  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
297  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
298  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
299  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
300  }
301 
303  public async Task Initialize(CancellationToken cancellationToken)
304  {
305  ValidateDatabaseType();
306 
308  {
309  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
310  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
311  }
312 
313  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
314  var wasEmpty = !migrations.Any();
315 
316  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
317  {
318  Logger.LogInformation("Migrating database...");
319  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
320  }
321  else
322  Logger.LogDebug("No migrations to apply.");
323 
324  wasEmpty |= (await Users.AsQueryable().CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
325 
326  if (wasEmpty)
327  {
328  Logger.LogInformation("Seeding database...");
329  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
330  }
331  else
332  {
334  {
335  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
336  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
337  }
338 
339  await databaseSeeder.SanitizeDatabase(this, cancellationToken).ConfigureAwait(false);
340  }
341  }
342 
344  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
345 
347  public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
348  {
349  if (version == null)
350  throw new ArgumentNullException(nameof(version));
351  if (version < new Version(4, 0))
352  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
353 
354  // Update this with new migrations as they are made
355  string targetMigration = null;
356 
357  if (DatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0))
358  throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!");
359 
360  if (version < new Version(4, 1, 0))
361  throw new NotSupportedException("Cannot migrate below version 4.1.0!");
362 
363  if (version < new Version(4, 2, 0))
364  targetMigration = DatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete);
365 
366  if (targetMigration == null)
367  {
368  Logger.LogDebug("No down migration required.");
369  return;
370  }
371 
372  string migrationSubstitution;
373  switch (DatabaseType)
374  {
375  case DatabaseType.SqlServer:
376  // already setup
377  migrationSubstitution = null;
378  break;
379  case DatabaseType.MySql:
380  case DatabaseType.MariaDB:
381  migrationSubstitution = "MY{0}";
382  break;
383  case DatabaseType.Sqlite:
384  migrationSubstitution = "SL{0}";
385  break;
386  case DatabaseType.PostgresSql:
387  migrationSubstitution = "PG{0}";
388  break;
389  default:
390  throw new InvalidOperationException($"Invalid DatabaseType: {DatabaseType}");
391  }
392 
393  if (migrationSubstitution != null)
394  targetMigration = String.Format(CultureInfo.InvariantCulture, migrationSubstitution, targetMigration.Substring(2));
395 
396  // even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
397  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
398  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
399 
400  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
401  try
402  {
403  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
404  }
405  catch (Exception e)
406  {
407  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
408  }
409  }
410 
414  protected abstract void ValidateDatabaseType();
415  }
416 }
IDatabaseCollection< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
IDatabaseCollection< InstanceUser > InstanceUsers
The InstanceUsers in the IDatabaseContext
-
readonly IDatabaseCollection< ChatBot > chatBotsCollection
Backing field for IDatabaseContext.ChatBots.
IDatabaseCollection< ChatChannel > ChatChannels
The ChatChannel in the IDatabaseContext
-
readonly IDatabaseCollection< CompileJob > compileJobsCollection
Backing field for IDatabaseContext.CompileJobs.
-
readonly IDatabaseCollection< RepositorySettings > repositorySettingsCollection
Backing field for IDatabaseContext.RepositorySettings.
+
readonly IDatabaseCollection< DualReattachInformation > watchdogReattachInformationsCollection
Backing field for IDatabaseContext.WatchdogReattachInformations.
-
readonly IDatabaseCollection< RevisionInformation > revisionInformationsCollection
Backing field for IDatabaseContext.RevisionInformations.
+
readonly IDatabaseCollection< ChatChannel > chatChannelsCollection
Backing field for IDatabaseContext.ChatChannels.
For initially seeding a database
-
readonly IDatabaseCollection< ChatChannel > chatChannelsCollection
Backing field for IDatabaseContext.ChatChannels.
Fix cascading data deletes for Models.Instances on MSSQL.
IDatabaseCollection< CompileJob > CompileJobs
The CompileJobs in the IDatabaseContext
-
DatabaseContext(DbContextOptions< TParentContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger)
Construct a DatabaseContext<TParentContext>
-
readonly IDatabaseCollection< InstanceUser > instanceUsersCollection
Backing field for IDatabaseContext.InstanceUsers.
- -
readonly IDatabaseCollection< DreamMakerSettings > dreamMakerSettingsCollection
Backing field for IDatabaseContext.DreamMakerSettings.
-
Backend abstract implementation of IDatabaseContext
+ +
Backend abstract implementation of IDatabaseContext
IDatabaseCollection< Instance > Instances
The Instances in the IDatabaseContext
-
readonly IDatabaseCollection< ReattachInformation > reattachInformationsCollection
Backing field for IDatabaseContext.ReattachInformations.
IDatabaseCollection< ChatBot > ChatBots
The ChatBots in the IDatabaseContext
DatabaseType DatabaseType
The Configuration.DatabaseType to create
IDatabaseCollection< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the IDatabaseContext
-
async Task Initialize(CancellationToken cancellationToken)
Creates and migrates the IDatabaseContext
+
readonly IDatabaseCollection< Instance > instancesCollection
Backing field for IDatabaseContext.Instances.
+
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext
+
readonly IDatabaseCollection< DreamMakerSettings > dreamMakerSettingsCollection
Backing field for IDatabaseContext.DreamMakerSettings.
bool ResetAdminPassword
If the admin user should be enabled and have it&#39;s password reset
IDatabaseCollection< DreamMakerSettings > DreamMakerSettings
The Models.DreamMakerSettings in the IDatabaseContext
+
readonly IDatabaseCollection< User > usersCollection
Backing field for IDatabaseContext.Users.
+
readonly IDatabaseCollection< RevisionInformation > revisionInformationsCollection
Backing field for IDatabaseContext.RevisionInformations.
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
-
async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
Attempt to downgrade the schema to the migration used for a given server version ...
+
async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
Attempt to downgrade the schema to the migration used for a given server version ...
+
DatabaseContext(DbContextOptions dbContextOptions, IOptions< DatabaseConfiguration > databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger)
Construct a DatabaseContext
+
readonly IDatabaseCollection< CompileJob > compileJobsCollection
Backing field for IDatabaseContext.CompileJobs.
IDatabaseCollection< DualReattachInformation > WatchdogReattachInformations
The DbSet<TEntity> for DualReattachInformations
-
Configuration options for the Database.DatabaseContext<TParentContext>
-
readonly IDatabaseCollection< DreamDaemonSettings > dreamDaemonSettingsCollection
Backing field for IDatabaseContext.DreamDaemonSettings.
+
Configuration options for the Database.DatabaseContext
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
+
readonly IDatabaseCollection< InstanceUser > instanceUsersCollection
Backing field for IDatabaseContext.InstanceUsers.
bool DropDatabase
If the database should be deleted on application startup. Should not be used in production! ...
-
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
+
override void OnModelCreating(ModelBuilder modelBuilder)
+
readonly IDatabaseCollection< DreamDaemonSettings > dreamDaemonSettingsCollection
Backing field for IDatabaseContext.DreamDaemonSettings.
+
readonly IDatabaseCollection< ChatBot > chatBotsCollection
Backing field for IDatabaseContext.ChatBots.
+
readonly IDatabaseCollection< RepositorySettings > repositorySettingsCollection
Backing field for IDatabaseContext.RepositorySettings.
IDatabaseCollection< DreamDaemonSettings > DreamDaemonSettings
The Models.DreamDaemonSettings in the IDatabaseContext
IDatabaseCollection< ReattachInformation > ReattachInformations
The DbSet<TEntity> for ReattachInformations
IDatabaseCollection< Job > Jobs
The Jobs in the IDatabaseContext
-
readonly IDatabaseCollection< Job > jobsCollection
Backing field for IDatabaseContext.Jobs.
-
readonly IDatabaseCollection< Instance > instancesCollection
Backing field for IDatabaseContext.Instances.
+
async Task Initialize(CancellationToken cancellationToken)
Creates and migrates the IDatabaseContext
+
readonly IDatabaseCollection< ReattachInformation > reattachInformationsCollection
Backing field for IDatabaseContext.ReattachInformations.
-
override void OnModelCreating(ModelBuilder modelBuilder)
IDatabaseCollection< User > Users
The Users in the IDatabaseContext
+
readonly IDatabaseCollection< Job > jobsCollection
Backing field for IDatabaseContext.Jobs.
- + -
readonly IDatabaseCollection< User > usersCollection
Backing field for IDatabaseContext.Users.
-
readonly IDatabaseCollection< DualReattachInformation > watchdogReattachInformationsCollection
Backing field for IDatabaseContext.WatchdogReattachInformations.
diff --git a/_database_context_extensions_8cs_source.html b/_database_context_extensions_8cs_source.html index bc8f539ce2..8f879dabb1 100644 --- a/_database_context_extensions_8cs_source.html +++ b/_database_context_extensions_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseContextExtensions.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
8 
9 namespace Tgstation.Server.Host.Extensions
10 {
15  {
23  public static Task<CompileJob> MostRecentCompletedCompileJobOrDefault(
24  this IDatabaseContext databaseContext,
25  Api.Models.Instance instance,
26  CancellationToken cancellationToken)
27  {
28  if (databaseContext == null)
29  throw new ArgumentNullException(nameof(databaseContext));
30 
31  return databaseContext
33  .Where(x => x.Job.Instance.Id == instance.Id)
34  .OrderByDescending(x => x.Job.StoppedAt)
35  .FirstOrDefaultAsync(cancellationToken);
36  }
37  }
38 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
8 
9 namespace Tgstation.Server.Host.Extensions
10 {
15  {
23  public static Task<CompileJob> MostRecentCompletedCompileJobOrDefault(
24  this IDatabaseContext databaseContext,
25  Api.Models.Instance instance,
26  CancellationToken cancellationToken)
27  {
28  if (databaseContext == null)
29  throw new ArgumentNullException(nameof(databaseContext));
30 
31  return databaseContext
33  .AsQueryable()
34  .Where(x => x.Job.Instance.Id == instance.Id)
35  .OrderByDescending(x => x.Job.StoppedAt)
36  .FirstOrDefaultAsync(cancellationToken);
37  }
38  }
39 }
static Task< CompileJob > MostRecentCompletedCompileJobOrDefault(this IDatabaseContext databaseContext, Api.Models.Instance instance, CancellationToken cancellationToken)
Get the most recent CompileJob for a given instance from a given databaseContext ...
@@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
diff --git a/_database_context_factory_8cs.html b/_database_context_factory_8cs.html index f877e6edb1..a9cf8d8d6d 100644 --- a/_database_context_factory_8cs.html +++ b/_database_context_factory_8cs.html @@ -25,7 +25,7 @@
diff --git a/_database_context_factory_8cs_source.html b/_database_context_factory_8cs_source.html index 1aa037cbc4..e2f4fb087f 100644 --- a/_database_context_factory_8cs_source.html +++ b/_database_context_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_database_seeder_8cs.html b/_database_seeder_8cs.html index f801b853c1..cdb512b7b4 100644 --- a/_database_seeder_8cs.html +++ b/_database_seeder_8cs.html @@ -25,7 +25,7 @@ diff --git a/_database_seeder_8cs_source.html b/_database_seeder_8cs_source.html index 795b500ea4..6a4e74a75b 100644 --- a/_database_seeder_8cs_source.html +++ b/_database_seeder_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,10 +94,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseSeeder.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Database
12 {
15  {
20 
25 
31  public DatabaseSeeder(ICryptographySuite cryptographySuite, IPlatformIdentifier platformIdentifier)
32  {
33  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
34  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
35  }
36 
41  void SeedAdminUser(IDatabaseContext databaseContext)
42  {
43  var admin = new User
44  {
46  CreatedAt = DateTimeOffset.Now,
48  Name = Api.Models.User.AdminName,
49  CanonicalName = User.CanonicalizeName(Api.Models.User.AdminName),
50  Enabled = true,
51  };
52  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, true);
53  databaseContext.Users.Add(admin);
54  }
55 
57  public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
58  {
59  SeedAdminUser(databaseContext);
60  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
61  }
62 
64  public async Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
65  {
66  var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false);
67  if (admin != null)
68  {
69  // Fix the issue with ulong enums
70  // https://github.com/tgstation/tgstation-server/commit/db341d43b3dab74fe3681f5172ca9bfeaafa6b6d#diff-09f06ec4584665cf89bb77b97f5ccfb9R36-R39
71  // https://github.com/JamesNK/Newtonsoft.Json/issues/2301
72  admin.AdministrationRights &= RightsHelper.AllRights<AdministrationRights>();
73  admin.InstanceManagerRights &= RightsHelper.AllRights<InstanceManagerRights>();
74  }
75 
76  if (platformIdentifier.IsWindows)
77  {
78  // normalize backslashes to forward slashes
79  var allInstances = await databaseContext.Instances.ToListAsync(cancellationToken).ConfigureAwait(false);
80  foreach (var instance in allInstances)
81  instance.Path = instance.Path.Replace('\\', '/');
82  }
83 
84  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
85  }
86 
88  public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
89  {
90  var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false);
91  if (admin != null)
92  {
93  admin.Enabled = true;
94  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, false);
95  }
96 
97  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
98  }
99 
106  async Task<User> GetAdminUser(IDatabaseContext databaseContext, CancellationToken cancellationToken)
107  {
108  var admin = await databaseContext
109  .Users
110  .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName))
111  .FirstOrDefaultAsync(cancellationToken)
112  .ConfigureAwait(false);
113  if (admin == default)
114  SeedAdminUser(databaseContext);
115 
116  return admin;
117  }
118  }
119 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Database
12 {
15  {
20 
25 
31  public DatabaseSeeder(ICryptographySuite cryptographySuite, IPlatformIdentifier platformIdentifier)
32  {
33  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
34  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
35  }
36 
41  void SeedAdminUser(IDatabaseContext databaseContext)
42  {
43  var admin = new User
44  {
46  CreatedAt = DateTimeOffset.Now,
48  Name = Api.Models.User.AdminName,
49  CanonicalName = User.CanonicalizeName(Api.Models.User.AdminName),
50  Enabled = true,
51  };
52  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, true);
53  databaseContext.Users.Add(admin);
54  }
55 
57  public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
58  {
59  SeedAdminUser(databaseContext);
60  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
61  }
62 
64  public async Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
65  {
66  var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false);
67  if (admin != null)
68  {
69  // Fix the issue with ulong enums
70  // https://github.com/tgstation/tgstation-server/commit/db341d43b3dab74fe3681f5172ca9bfeaafa6b6d#diff-09f06ec4584665cf89bb77b97f5ccfb9R36-R39
71  // https://github.com/JamesNK/Newtonsoft.Json/issues/2301
72  admin.AdministrationRights &= RightsHelper.AllRights<AdministrationRights>();
73  admin.InstanceManagerRights &= RightsHelper.AllRights<InstanceManagerRights>();
74  }
75 
76  if (platformIdentifier.IsWindows)
77  {
78  // normalize backslashes to forward slashes
79  var allInstances = await databaseContext
80  .Instances
81  .AsQueryable()
82  .ToListAsync(cancellationToken)
83  .ConfigureAwait(false);
84  foreach (var instance in allInstances)
85  instance.Path = instance.Path.Replace('\\', '/');
86  }
87 
88  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
89  }
90 
92  public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
93  {
94  var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false);
95  if (admin != null)
96  {
97  admin.Enabled = true;
98  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, false);
99  }
100 
101  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
102  }
103 
110  async Task<User> GetAdminUser(IDatabaseContext databaseContext, CancellationToken cancellationToken)
111  {
112  var admin = await databaseContext
113  .Users
114  .AsQueryable()
115  .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName))
116  .FirstOrDefaultAsync(cancellationToken)
117  .ConfigureAwait(false);
118  if (admin == default)
119  SeedAdminUser(databaseContext);
120 
121  return admin;
122  }
123  }
124 }
-
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the DatabaseContext<TParentContext>
+
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the DatabaseSeeder
@@ -111,17 +111,16 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IDatabaseCollection< Instance > Instances
The Instances in the IDatabaseContext
-
async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Changes the admin password in IDatabaseContext back to it&#39;s default and enables the account ...
+
async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Changes the admin password in IDatabaseContext back to it&#39;s default and enables the account ...
InstanceManagerRights
Rights for managing Models.Instances
static string CanonicalizeName(string name)
Change a Api.Models.Internal.User.Name into a CanonicalName.
DatabaseSeeder(ICryptographySuite cryptographySuite, IPlatformIdentifier platformIdentifier)
Construct a DatabaseSeeder
-
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the DatabaseContext<TParentContext>.
+
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the DatabaseSeeder.
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
-
Task< List< TModel > > ToListAsync(CancellationToken cancellationToken)
Retrieve all the TModel s in the table.
-
async Task< User > GetAdminUser(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Get or create the admin User.
+
async Task< User > GetAdminUser(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Get or create the admin User.
void SeedAdminUser(IDatabaseContext databaseContext)
Add a default admin User to a given databaseContext
IDatabaseCollection< User > Users
The Users in the IDatabaseContext
For identifying the current platform
diff --git a/_database_type_8cs.html b/_database_type_8cs.html index 589ac27343..8017c9886b 100644 --- a/_database_type_8cs.html +++ b/_database_type_8cs.html @@ -25,7 +25,7 @@
@@ -107,10 +107,14 @@ Namespaces
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
- diff --git a/_database_type_8cs_source.html b/_database_type_8cs_source.html index 5d31b58e3b..d4dedbdfc5 100644 --- a/_database_type_8cs_source.html +++ b/_database_type_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,13 +94,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseType.cs
-Go to the documentation of this file. diff --git a/_dead_session_controller_8cs.html b/_dead_session_controller_8cs.html index 4bb2f9ff73..df9838ce1f 100644 --- a/_dead_session_controller_8cs.html +++ b/_dead_session_controller_8cs.html @@ -25,7 +25,7 @@
diff --git a/_dead_session_controller_8cs_source.html b/_dead_session_controller_8cs_source.html index 59c14e97c4..a0d9527b42 100644 --- a/_dead_session_controller_8cs_source.html +++ b/_dead_session_controller_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_default_i_o_manager_8cs.html b/_default_i_o_manager_8cs.html index 1e1bc2c62c..d896deae1c 100644 --- a/_default_i_o_manager_8cs.html +++ b/_default_i_o_manager_8cs.html @@ -25,7 +25,7 @@ diff --git a/_default_i_o_manager_8cs_source.html b/_default_i_o_manager_8cs_source.html index 5be9e41ece..a7e8409077 100644 --- a/_default_i_o_manager_8cs_source.html +++ b/_default_i_o_manager_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_design_time_db_context_factory_helpers_8cs.html b/_design_time_db_context_factory_helpers_8cs.html index 822cdf76ee..3c2f603e55 100644 --- a/_design_time_db_context_factory_helpers_8cs.html +++ b/_design_time_db_context_factory_helpers_8cs.html @@ -25,7 +25,7 @@ @@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Enumerations

enum  Tgstation.Server.Host.Configuration.DatabaseType { Tgstation.Server.Host.Configuration.DatabaseType.SqlServer, +
enum  Tgstation.Server.Host.Configuration.DatabaseType {
+  Tgstation.Server.Host.Configuration.DatabaseType.SqlServer, Tgstation.Server.Host.Configuration.DatabaseType.MySql, Tgstation.Server.Host.Configuration.DatabaseType.MariaDB, -Tgstation.Server.Host.Configuration.DatabaseType.Sqlite +Tgstation.Server.Host.Configuration.DatabaseType.Sqlite, +
+  Tgstation.Server.Host.Configuration.DatabaseType.PostgresSql +
}
 Type of database to user More...
 
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite

Classes

class  Tgstation.Server.Host.Database.Design.DesignTimeDbContextFactoryHelpers
 Contains helpers for creating design time DatabaseContext<TParentContext>s More...
 Contains helpers for creating design time DatabaseContexts More...
 
@@ -94,11 +94,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DesignTimeDbContextFactoryHelpers.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Options;
3 
4 namespace Tgstation.Server.Host.Database.Design
5 {
10  {
17  public static IOptions<DatabaseConfiguration> GetDbContextOptions(DatabaseType databaseType, string connectionString)
18  {
19  var dbConfig = new DatabaseConfiguration
20  {
21  DesignTime = true,
22  DatabaseType = databaseType,
23  ConnectionString = connectionString
24  };
25 
26  return Options.Create(dbConfig);
27  }
28  }
29 }
static IOptions< DatabaseConfiguration > GetDbContextOptions(DatabaseType databaseType, string connectionString)
Get the IOptions<TOptions> for the DatabaseConfiguration
+Go to the documentation of this file.
1 using Microsoft.Extensions.Options;
3 
4 namespace Tgstation.Server.Host.Database.Design
5 {
10  {
17  public static IOptions<DatabaseConfiguration> GetDatabaseConfiguration(DatabaseType databaseType, string connectionString)
18  {
19  var dbConfig = new DatabaseConfiguration
20  {
21  DesignTime = true,
22  DatabaseType = databaseType,
23  ConnectionString = connectionString
24  };
25 
26  return Options.Create(dbConfig);
27  }
28  }
29 }
static IOptions< DatabaseConfiguration > GetDatabaseConfiguration(DatabaseType databaseType, string connectionString)
Get the IOptions<TOptions> for the DatabaseConfiguration
-
Configuration options for the Database.DatabaseContext<TParentContext>
+
Configuration options for the Database.DatabaseContext
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
Contains helpers for creating design time DatabaseContext<TParentContext>s
+ diff --git a/_discord_connection_string_builder_8cs.html b/_discord_connection_string_builder_8cs.html index 487fdd6bc0..23bda11679 100644 --- a/_discord_connection_string_builder_8cs.html +++ b/_discord_connection_string_builder_8cs.html @@ -25,7 +25,7 @@
diff --git a/_discord_connection_string_builder_8cs_source.html b/_discord_connection_string_builder_8cs_source.html index 779c5a44e8..eb30429364 100644 --- a/_discord_connection_string_builder_8cs_source.html +++ b/_discord_connection_string_builder_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_discord_provider_8cs.html b/_discord_provider_8cs.html index 684f67c314..19f0f6d07d 100644 --- a/_discord_provider_8cs.html +++ b/_discord_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_discord_provider_8cs_source.html b/_discord_provider_8cs_source.html index 115779b145..9346930331 100644 --- a/_discord_provider_8cs_source.html +++ b/_discord_provider_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,26 +94,39 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DiscordProvider.cs
-Go to the documentation of this file.
1 using Discord;
2 using Discord.WebSocket;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Components.Chat.Providers
11 {
15  sealed class DiscordProvider : Provider
16  {
18  public override bool Connected => client.ConnectionState == ConnectionState.Connected;
19 
21  public override string BotMention
22  {
23  get
24  {
25  if (!Connected)
26  throw new InvalidOperationException("Provider not connected");
27  return NormalizeMentions(client.CurrentUser.Mention);
28  }
29  }
30 
34  readonly DiscordSocketClient client;
35 
39  readonly string botToken;
40 
44  readonly List<ulong> mappedChannels;
45 
51  static string NormalizeMentions(string fromDiscord) => fromDiscord.Replace("<!@", "<@", StringComparison.Ordinal);
52 
59  public DiscordProvider(ILogger<DiscordProvider> logger, string botToken, uint reconnectInterval)
60  : base(logger, reconnectInterval)
61  {
62  this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
63  client = new DiscordSocketClient();
64  client.MessageReceived += Client_MessageReceived;
65  mappedChannels = new List<ulong>();
66  }
67 
69  public override void Dispose()
70  {
71  client.Dispose();
72 
73  base.Dispose();
74  }
75 
81  async Task Client_MessageReceived(SocketMessage e)
82  {
83  if (e.Author.Id == client.CurrentUser.Id)
84  return;
85 
86  var pm = e.Channel is IPrivateChannel;
87 
88  if (!pm && !mappedChannels.Contains(e.Channel.Id))
89  {
90  var mentionedUs = e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id);
91  if (mentionedUs)
92  {
93  Logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username);
94  await SendMessage(e.Channel.Id, "I do not respond to this channel!", default).ConfigureAwait(false);
95  }
96 
97  return;
98  }
99 
100  var result = new Message
101  {
102  Content = NormalizeMentions(e.Content),
103  User = new ChatUser
104  {
105  RealId = e.Author.Id,
106  Channel = new ChannelRepresentation
107  {
108  RealId = e.Channel.Id,
109  IsPrivateChannel = pm,
110  ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
111  FriendlyName = e.Channel.Name
112 
113  // isAdmin and Tag populated by manager
114  },
115  FriendlyName = e.Author.Username,
116  Mention = NormalizeMentions(e.Author.Mention)
117  }
118  };
119  EnqueueMessage(result);
120  }
121 
123  public override async Task<bool> Connect(CancellationToken cancellationToken)
124  {
125  Logger.LogTrace("Connecting...");
126  if (Connected)
127  {
128  Logger.LogTrace("Already connected not doing connection attempt!");
129  return true;
130  }
131 
132  try
133  {
134  await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
135 
136  Logger.LogTrace("Logged in.");
137  cancellationToken.ThrowIfCancellationRequested();
138 
139  await client.StartAsync().ConfigureAwait(false);
140 
141  Logger.LogTrace("Started.");
142 
143  var channelsAvailable = new TaskCompletionSource<object>();
144  client.Ready += () =>
145  {
146  channelsAvailable.TrySetResult(null);
147  return Task.CompletedTask;
148  };
149  using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
150  await channelsAvailable.Task.ConfigureAwait(false);
151  Logger.LogDebug("Connection established!");
152  }
153  catch (OperationCanceledException)
154  {
155  throw;
156  }
157  catch (Exception e)
158  {
159  Logger.LogWarning("Error connecting to Discord: {0}", e);
160  return false;
161  }
162 
163  return true;
164  }
165 
167  public override async Task Disconnect(CancellationToken cancellationToken)
168  {
169  Logger.LogTrace("Disconnecting...");
170  if (!Connected)
171  {
172  Logger.LogTrace("Already disconnected not doing disconnection attempt!");
173  return;
174  }
175 
176  try
177  {
178  await client.StopAsync().ConfigureAwait(false);
179  Logger.LogTrace("Stopped.");
180  cancellationToken.ThrowIfCancellationRequested();
181  await client.LogoutAsync().ConfigureAwait(false);
182  Logger.LogDebug("Disconnected!");
183  }
184  catch (OperationCanceledException)
185  {
186  throw;
187  }
188  catch (Exception e)
189  {
190  Logger.LogWarning("Error disconnecting from discord: {0}", e);
191  }
192  }
193 
195  public override Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken)
196  {
197  if (channels == null)
198  throw new ArgumentNullException(nameof(channels));
199 
200  if (!Connected)
201  {
202  Logger.LogWarning("Cannot map channels, provider disconnected!");
203  return Task.FromResult<IReadOnlyCollection<ChannelRepresentation>>(Array.Empty<ChannelRepresentation>());
204  }
205 
206  ChannelRepresentation GetModelChannelFromDBChannel(Api.Models.ChatChannel channelFromDB)
207  {
208  if (!channelFromDB.DiscordChannelId.HasValue)
209  throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
210 
211  var channelId = channelFromDB.DiscordChannelId.Value;
212  var discordChannel = client.GetChannel(channelId);
213  if (discordChannel is ITextChannel textChannel)
214  {
215  var channelModel = new ChannelRepresentation
216  {
217  RealId = discordChannel.Id,
218  IsAdminChannel = channelFromDB.IsAdminChannel == true,
219  ConnectionName = textChannel.Guild.Name,
220  FriendlyName = textChannel.Name,
221  IsPrivateChannel = false,
222  Tag = channelFromDB.Tag
223  };
224  Logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName);
225  return channelModel;
226  }
227 
228  Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel?.GetType());
229  return null;
230  }
231 
232  var enumerator = channels.Select(x => GetModelChannelFromDBChannel(x)).Where(x => x != null).ToList();
233 
234  lock (client)
235  {
236  mappedChannels.Clear();
237  mappedChannels.AddRange(enumerator.Select(x => x.RealId));
238  }
239 
240  return Task.FromResult<IReadOnlyCollection<ChannelRepresentation>>(enumerator);
241  }
242 
244  public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
245  {
246  try
247  {
248  var channel = client.GetChannel(channelId) as IMessageChannel;
249  await (channel?.SendMessageAsync(message, false, null, new RequestOptions
250  {
251  CancelToken = cancellationToken
252  }) ?? Task.CompletedTask).ConfigureAwait(false);
253  }
254  catch (OperationCanceledException)
255  {
256  throw;
257  }
258  catch (Exception e)
259  {
260  Logger.LogWarning("Error sending discord message: {0}", e);
261  }
262  }
263  }
264 }
async Task Client_MessageReceived(SocketMessage e)
Handle a message recieved from Discord
- - +Go to the documentation of this file.
1 using Discord;
2 using Discord.WebSocket;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Components.Chat.Providers
13 {
17  sealed class DiscordProvider : Provider
18  {
20  public override bool Connected => client.ConnectionState == ConnectionState.Connected;
21 
23  public override string BotMention
24  {
25  get
26  {
27  if (!Connected)
28  throw new InvalidOperationException("Provider not connected");
29  return NormalizeMentions(client.CurrentUser.Mention);
30  }
31  }
32 
37 
41  readonly DiscordSocketClient client;
42 
46  readonly string botToken;
47 
51  readonly List<ulong> mappedChannels;
52 
58  static string NormalizeMentions(string fromDiscord) => fromDiscord.Replace("<!@", "<@", StringComparison.Ordinal);
59 
68  IAssemblyInformationProvider assemblyInformationProvider,
69  ILogger<DiscordProvider> logger,
70  string botToken,
71  uint reconnectInterval)
72  : base(logger, reconnectInterval)
73  {
74  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
75  this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
76  client = new DiscordSocketClient();
77  client.MessageReceived += Client_MessageReceived;
78  mappedChannels = new List<ulong>();
79  }
80 
82  public override void Dispose()
83  {
84  client.Dispose();
85 
86  base.Dispose();
87  }
88 
94  async Task Client_MessageReceived(SocketMessage e)
95  {
96  if (e.Author.Id == client.CurrentUser.Id)
97  return;
98 
99  var pm = e.Channel is IPrivateChannel;
100 
101  if (!pm && !mappedChannels.Contains(e.Channel.Id))
102  {
103  var mentionedUs = e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id);
104  if (mentionedUs)
105  {
106  Logger.LogTrace("Ignoring mention from {0} ({1}) by {2} ({3}). Channel not mapped!", e.Channel.Id, e.Channel.Name, e.Author.Id, e.Author.Username);
107  await SendMessage(e.Channel.Id, "I do not respond to this channel!", default).ConfigureAwait(false);
108  }
109 
110  return;
111  }
112 
113  var result = new Message
114  {
115  Content = NormalizeMentions(e.Content),
116  User = new ChatUser
117  {
118  RealId = e.Author.Id,
119  Channel = new ChannelRepresentation
120  {
121  RealId = e.Channel.Id,
122  IsPrivateChannel = pm,
123  ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
124  FriendlyName = e.Channel.Name
125 
126  // isAdmin and Tag populated by manager
127  },
128  FriendlyName = e.Author.Username,
129  Mention = NormalizeMentions(e.Author.Mention)
130  }
131  };
132  EnqueueMessage(result);
133  }
134 
136  public override async Task<bool> Connect(CancellationToken cancellationToken)
137  {
138  Logger.LogTrace("Connecting...");
139  if (Connected)
140  {
141  Logger.LogTrace("Already connected not doing connection attempt!");
142  return true;
143  }
144 
145  try
146  {
147  await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
148 
149  Logger.LogTrace("Logged in.");
150  cancellationToken.ThrowIfCancellationRequested();
151 
152  await client.StartAsync().ConfigureAwait(false);
153 
154  Logger.LogTrace("Started.");
155 
156  var channelsAvailable = new TaskCompletionSource<object>();
157  client.Ready += () =>
158  {
159  channelsAvailable.TrySetResult(null);
160  return Task.CompletedTask;
161  };
162  using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
163  await channelsAvailable.Task.ConfigureAwait(false);
164  Logger.LogDebug("Connection established!");
165  }
166  catch (OperationCanceledException)
167  {
168  throw;
169  }
170  catch (Exception e)
171  {
172  Logger.LogWarning("Error connecting to Discord: {0}", e);
173  return false;
174  }
175 
176  return true;
177  }
178 
180  public override async Task Disconnect(CancellationToken cancellationToken)
181  {
182  Logger.LogTrace("Disconnecting...");
183  if (!Connected)
184  {
185  Logger.LogTrace("Already disconnected not doing disconnection attempt!");
186  return;
187  }
188 
189  try
190  {
191  await client.StopAsync().ConfigureAwait(false);
192  Logger.LogTrace("Stopped.");
193  cancellationToken.ThrowIfCancellationRequested();
194  await client.LogoutAsync().ConfigureAwait(false);
195  Logger.LogDebug("Disconnected!");
196  }
197  catch (OperationCanceledException)
198  {
199  throw;
200  }
201  catch (Exception e)
202  {
203  Logger.LogWarning("Error disconnecting from discord: {0}", e);
204  }
205  }
206 
208  public override Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken)
209  {
210  if (channels == null)
211  throw new ArgumentNullException(nameof(channels));
212 
213  if (!Connected)
214  {
215  Logger.LogWarning("Cannot map channels, provider disconnected!");
216  return Task.FromResult<IReadOnlyCollection<ChannelRepresentation>>(Array.Empty<ChannelRepresentation>());
217  }
218 
219  ChannelRepresentation GetModelChannelFromDBChannel(Api.Models.ChatChannel channelFromDB)
220  {
221  if (!channelFromDB.DiscordChannelId.HasValue)
222  throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
223 
224  var channelId = channelFromDB.DiscordChannelId.Value;
225  var discordChannel = client.GetChannel(channelId);
226  if (discordChannel is ITextChannel textChannel)
227  {
228  var channelModel = new ChannelRepresentation
229  {
230  RealId = discordChannel.Id,
231  IsAdminChannel = channelFromDB.IsAdminChannel == true,
232  ConnectionName = textChannel.Guild.Name,
233  FriendlyName = textChannel.Name,
234  IsPrivateChannel = false,
235  Tag = channelFromDB.Tag
236  };
237  Logger.LogTrace("Mapped channel {0}: {1}", channelModel.RealId, channelModel.FriendlyName);
238  return channelModel;
239  }
240 
241  Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel?.GetType());
242  return null;
243  }
244 
245  var enumerator = channels.Select(x => GetModelChannelFromDBChannel(x)).Where(x => x != null).ToList();
246 
247  lock (client)
248  {
249  mappedChannels.Clear();
250  mappedChannels.AddRange(enumerator.Select(x => x.RealId));
251  }
252 
253  return Task.FromResult<IReadOnlyCollection<ChannelRepresentation>>(enumerator);
254  }
255 
257  public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
258  {
259  try
260  {
261  var channel = client.GetChannel(channelId) as IMessageChannel;
262  await (channel?.SendMessageAsync(message, false, null, new RequestOptions
263  {
264  CancelToken = cancellationToken
265  }) ?? Task.CompletedTask).ConfigureAwait(false);
266  }
267  catch (OperationCanceledException)
268  {
269  throw;
270  }
271  catch (Exception e)
272  {
273  Logger.LogWarning("Error sending discord message: {0}", e);
274  }
275  }
276 
278  public override async Task<Func<string, string, Task>> SendUpdateMessage(
279  RevisionInformation revisionInformation,
280  Version byondVersion,
281  DateTimeOffset? estimatedCompletionTime,
282  string gitHubOwner,
283  string gitHubRepo,
284  ulong channelId,
285  bool localCommitPushed,
286  CancellationToken cancellationToken)
287  {
288  bool gitHub = gitHubOwner != null && gitHubRepo != null;
289 
290  localCommitPushed |= revisionInformation.CommitSha == revisionInformation.OriginCommitSha;
291 
292  var fields = new List<EmbedFieldBuilder>
293  {
294  new EmbedFieldBuilder
295  {
296  Name = "BYOND Version",
297  Value = $"{byondVersion.Major}.{byondVersion.Minor}",
298  IsInline = true
299  },
300  new EmbedFieldBuilder
301  {
302  Name = "Local Commit",
303  Value = localCommitPushed && gitHub
304  ? $"[{revisionInformation.CommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.CommitSha})"
305  : revisionInformation.CommitSha.Substring(0, 7),
306  IsInline = true
307  },
308  new EmbedFieldBuilder
309  {
310  Name = "Branch Commit",
311  Value = gitHub
312  ? $"[{revisionInformation.OriginCommitSha.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{revisionInformation.OriginCommitSha})"
313  : revisionInformation.OriginCommitSha.Substring(0, 7),
314  IsInline = true
315  }
316  };
317 
318  fields.AddRange((revisionInformation.ActiveTestMerges ?? Enumerable.Empty<RevInfoTestMerge>())
319  .Select(x => x.TestMerge)
320  .Select(x => new EmbedFieldBuilder
321  {
322  Name = $"#{x.Number}",
323  Value = $"[{x.TitleAtMerge}]({x.Url}) by _[@{x.Author}](https://github.com/{x.Author})_{Environment.NewLine}Commit: [{x.PullRequestRevision.Substring(0, 7)}](https://github.com/{gitHubOwner}/{gitHubRepo}/commit/{x.PullRequestRevision}){(String.IsNullOrWhiteSpace(x.Comment) ? String.Empty : $"{Environment.NewLine}_**{x.Comment}**_")}"
324  }));
325 
326  var builder = new EmbedBuilder
327  {
328  Author = new EmbedAuthorBuilder
329  {
330  Name = assemblyInformationProvider.VersionPrefix,
331  Url = "https://github.com/tgstation/tgstation-server",
332  IconUrl = "https://avatars0.githubusercontent.com/u/1363778?s=280&v=4"
333  },
334  Color = Color.Gold,
335  Description = "TGS has begun deploying active repository code to production.",
336  Fields = fields,
337  Title = "Code Deployment",
338  Footer = new EmbedFooterBuilder
339  {
340  Text = "In progress... ETA"
341  },
342  Timestamp = estimatedCompletionTime
343  };
344 
345  Logger.LogTrace("Attempting to post deploy embed to channel {0}...", channelId);
346  if (!(client.GetChannel(channelId) is IMessageChannel channel))
347  {
348  Logger.LogTrace("Channel ID {0} does not exist or is not an IMessageChannel!", channelId);
349  return (errorMessage, dreamMakerOutput) => Task.CompletedTask;
350  }
351 
352  var message = await channel.SendMessageAsync(
353  String.Empty,
354  false,
355  builder.Build(),
356  new RequestOptions
357  {
358  CancelToken = cancellationToken
359  })
360  .ConfigureAwait(false);
361 
362  return async (errorMessage, dreamMakerOutput) =>
363  {
364  builder.Footer.Text = errorMessage == null ? "Succeeded" : "Failed";
365  builder.Color = errorMessage == null ? Color.Green : Color.Red;
366  builder.Timestamp = DateTimeOffset.Now;
367  builder.Description = errorMessage == null
368  ? "The deployment completed successfully and will be available at the next server reboot."
369  : "The deployment failed.";
370 
371  if (dreamMakerOutput != null)
372  builder.AddField(new EmbedFieldBuilder
373  {
374  Name = "DreamMaker Output",
375  Value = $"```{Environment.NewLine}{dreamMakerOutput}{Environment.NewLine}```"
376  });
377 
378  if (errorMessage != null)
379  builder.AddField(new EmbedFieldBuilder
380  {
381  Name = "Error Message",
382  Value = errorMessage
383  });
384 
385  try
386  {
387  await message.ModifyAsync(
388  props => props.Embed = builder.Build())
389  .ConfigureAwait(false);
390  }
391  catch (Exception ex)
392  {
393  Logger.LogWarning("Updating deploy embed {0} failed, attempting new post! Exception: {1}", message.Id, ex);
394  try
395  {
396  await channel.SendMessageAsync(
397  String.Empty,
398  false,
399  builder.Build())
400  .ConfigureAwait(false);
401  }
402  catch (Exception ex2)
403  {
404  Logger.LogWarning("Posting completion deploy embed failed! Exception: {0}", ex2);
405  }
406  }
407  };
408  }
409  }
410 }
async Task Client_MessageReceived(SocketMessage e)
Handle a message recieved from Discord
+ + + -
override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
Send a message to the IProvider
+ +
override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
Send a message to the IProvider
- + +
Many to many relationship for Models.RevisionInformation and Models.TestMerge
Represents a message recieved by a IProvider
Definition: Message.cs:6
-
override async Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
-
override Task< IReadOnlyCollection< ChannelRepresentation > > MapChannels(IEnumerable< Api.Models.ChatChannel > channels, CancellationToken cancellationToken)
Get the ChannelRepresentations for given channels
-
override async Task< bool > Connect(CancellationToken cancellationToken)
Attempt to connect the IProvider
+
override async Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
+
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the DiscordProvider.
+
override Task< IReadOnlyCollection< ChannelRepresentation > > MapChannels(IEnumerable< Api.Models.ChatChannel > channels, CancellationToken cancellationToken)
Get the ChannelRepresentations for given channels
+
override async Task< bool > Connect(CancellationToken cancellationToken)
Attempt to connect the IProvider
+
string Id
Backing field for RealId. Represented as a string to avoid BYOND percision loss
-
readonly string botToken
The token used for connecting to discord
-
readonly DiscordSocketClient client
The DiscordSocketClient for the DiscordProvider
+ +
readonly string botToken
The token used for connecting to discord
+ +
readonly DiscordSocketClient client
The DiscordSocketClient for the DiscordProvider
Represents a tgs_chat_user datum
Definition: ChatUser.cs:10
-
readonly List< ulong > mappedChannels
List<T> of mapped ITextChannel IEntity<TId>.Ids
+
readonly List< ulong > mappedChannels
List<T> of mapped ITextChannel IEntity<TId>.Ids
string Id
Backing field for RealId. Represented as a string to avoid BYOND percision loss
Definition: ChatUser.cs:15
+
override async Task< Func< string, string, Task > > SendUpdateMessage(RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken)
Send the message for a deployment.
- -
DiscordProvider(ILogger< DiscordProvider > logger, string botToken, uint reconnectInterval)
Construct a DiscordProvider
+ + +
DiscordProvider(IAssemblyInformationProvider assemblyInformationProvider, ILogger< DiscordProvider > logger, string botToken, uint reconnectInterval)
Construct a DiscordProvider
+ +
List< RevInfoTestMerge > ActiveTestMerges
See Api.Models.RevisionInformation.ActiveTestMerges
+ +
string OriginCommitSha
The sha of the most recent remote commit
diff --git a/_dmb_factory_8cs.html b/_dmb_factory_8cs.html index b628062428..bec5403cd4 100644 --- a/_dmb_factory_8cs.html +++ b/_dmb_factory_8cs.html @@ -25,7 +25,7 @@
diff --git a/_dmb_factory_8cs_source.html b/_dmb_factory_8cs_source.html index c859d6d08e..87dfa1e785 100644 --- a/_dmb_factory_8cs_source.html +++ b/_dmb_factory_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DmbFactory.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
10 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Deployment
14 {
19  {
21  public Task OnNewerDmb
22  {
23  get
24  {
25  lock (jobLockCounts)
26  return newerDmbTcs.Task;
27  }
28  }
29 
31  public bool DmbAvailable => nextDmbProvider != null;
32 
37 
42 
46  readonly ILogger<DmbFactory> logger;
47 
51  readonly Api.Models.Instance instance;
52 
56  readonly CancellationTokenSource cleanupCts;
57 
61  readonly IDictionary<long, int> jobLockCounts;
62 
67 
71  TaskCompletionSource<object> newerDmbTcs;
72 
77 
85  public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger<DmbFactory> logger, Api.Models.Instance instance)
86  {
87  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
88  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
89  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
90  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
91 
92  cleanupTask = Task.CompletedTask;
93  newerDmbTcs = new TaskCompletionSource<object>();
94  cleanupCts = new CancellationTokenSource();
95  jobLockCounts = new Dictionary<long, int>();
96  }
97 
99  public void Dispose() => cleanupCts.Dispose(); // we don't dispose nextDmbProvider here, since it might be the only thing we have
100 
106  {
107  async Task HandleCleanup()
108  {
109  var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
110  Task otherTask;
111 
112  // lock (this) //already locked below
113  otherTask = cleanupTask;
114  await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
115  }
116 
117  lock (jobLockCounts)
118  if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
119  {
120  jobLockCounts.Remove(job.Id);
121  logger.LogDebug("Cleaning lock-free compile job {0} => {1}", job.Id, job.DirectoryName);
122  cleanupTask = HandleCleanup();
123  }
124  else
125  {
126  var decremented = --jobLockCounts[job.Id];
127  logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented);
128  }
129  }
130 
132  public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
133  {
134  if (job == null)
135  throw new ArgumentNullException(nameof(job));
136 
137  var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
138  if (newProvider == null)
139  return;
140  lock (jobLockCounts)
141  {
142  nextDmbProvider?.Dispose();
143  nextDmbProvider = newProvider;
144  newerDmbTcs.SetResult(nextDmbProvider);
145  newerDmbTcs = new TaskCompletionSource<object>();
146  }
147  }
148 
150  public IDmbProvider LockNextDmb(int lockCount)
151  {
152  if (!DmbAvailable)
153  throw new InvalidOperationException("No .dmb available!");
154  if (lockCount < 0)
155  throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
156  lock (jobLockCounts)
157  {
158  var jobId = nextDmbProvider.CompileJob.Id;
159  var incremented = jobLockCounts[jobId] += lockCount;
160  logger.LogTrace("Compile job {0} lock count now: {1}", jobId, incremented);
161  return nextDmbProvider;
162  }
163  }
164 
166  public async Task StartAsync(CancellationToken cancellationToken)
167  {
168  CompileJob cj = null;
169  await databaseContextFactory.UseContext(async (db) =>
170  {
171  cj = await db
172  .MostRecentCompletedCompileJobOrDefault(instance, cancellationToken)
173  .ConfigureAwait(false);
174  })
175  .ConfigureAwait(false);
176 
177  if (cj == default(CompileJob))
178  return;
179  await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
180 
181  // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
182  }
183 
185  public async Task StopAsync(CancellationToken cancellationToken)
186  {
187  using (cancellationToken.Register(() => cleanupCts.Cancel()))
188  await cleanupTask.ConfigureAwait(false);
189  }
190 
192  #pragma warning disable CA1506 // TODO: Decomplexify
193  public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
194  {
195  if (compileJob == null)
196  throw new ArgumentNullException(nameof(compileJob));
197 
198  // ensure we have the entire compile job tree
199  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
200  await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id)
201  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
202  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
203  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
204  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); // can't wait to see that query
205 
206  if (!compileJob.Job.StoppedAt.HasValue)
207  {
208  // This happens if we're told to load the compile job that is currently finished up
209  // It can constitute an API violation if it's returned by the DreamDaemonController so just set it here
210  // Bit of a hack, but it should work out to be the same value
211  logger.LogTrace("Setting missing StoppedAt for CompileJob job...");
212  compileJob.Job.StoppedAt = DateTimeOffset.Now;
213  }
214 
215  var providerSubmitted = false;
216  var newProvider = new DmbProvider(compileJob, ioManager, () =>
217  {
218  if (providerSubmitted)
219  CleanJob(compileJob);
220  });
221 
222  try
223  {
224  var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
225  var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
226 
227  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
228  {
229  logger.LogWarning("Error loading compile job, .dmb missing!");
230  return null; // omae wa mou shinderu
231  }
232 
233  lock (jobLockCounts)
234  {
235  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
236  {
237  value = 1;
238  jobLockCounts.Add(compileJob.Id, 1);
239  }
240  else
241  jobLockCounts[compileJob.Id] = ++value;
242 
243  logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
244 
245  providerSubmitted = true;
246  return newProvider;
247  }
248  }
249  finally
250  {
251  if (!providerSubmitted)
252  newProvider.Dispose();
253  }
254  }
255  #pragma warning restore CA1506
256 
258  #pragma warning disable CA1506 // TODO: Decomplexify
259  public async Task CleanUnusedCompileJobs(CancellationToken cancellationToken)
260  {
261  List<long> jobIdsToSkip;
262 
263  // don't clean locked directories
264  lock (jobLockCounts)
265  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
266 
267  List<string> jobUidsToNotErase = null;
268 
269  // find the uids of locked directories
270  await databaseContextFactory.UseContext(async db =>
271  {
272  jobUidsToNotErase = (await db.CompileJobs.Where(
273  x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id))
274  .Select(x => x.DirectoryName.Value)
275  .ToListAsync(cancellationToken)
276  .ConfigureAwait(false))
277  .Select(x => x.ToString())
278  .ToList();
279  }).ConfigureAwait(false);
280 
281  jobUidsToNotErase.Add(WindowsSwappableDmbProvider.LiveGameDirectory);
282 
283  logger.LogTrace("We will not clean the following directories: {0}", String.Join(", ", jobUidsToNotErase));
284 
285  // cleanup
286  var gameDirectory = ioManager.ResolvePath();
287  await ioManager.CreateDirectory(gameDirectory, cancellationToken).ConfigureAwait(false);
288  var directories = await ioManager.GetDirectories(gameDirectory, cancellationToken).ConfigureAwait(false);
289  int deleting = 0;
290  var tasks = directories.Select(async x =>
291  {
292  var nameOnly = ioManager.GetFileName(x);
293  if (jobUidsToNotErase.Contains(nameOnly))
294  return;
295  logger.LogDebug("Cleaning unused game folder: {0}...", nameOnly);
296  try
297  {
298  ++deleting;
299  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
300  }
301  catch (OperationCanceledException)
302  {
303  throw;
304  }
305  catch (Exception e)
306  {
307  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
308  }
309  }).ToList();
310  if (deleting > 0)
311  await Task.WhenAll(tasks).ConfigureAwait(false);
312  }
313  #pragma warning restore CA1506
314 
317  {
318  if (!DmbAvailable)
319  return null;
320  return LockNextDmb(0)?.CompileJob;
321  }
322  }
323 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
10 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Deployment
14 {
19  {
21  public Task OnNewerDmb
22  {
23  get
24  {
25  lock (jobLockCounts)
26  return newerDmbTcs.Task;
27  }
28  }
29 
31  public bool DmbAvailable => nextDmbProvider != null;
32 
37 
42 
46  readonly ILogger<DmbFactory> logger;
47 
51  readonly Api.Models.Instance instance;
52 
56  readonly CancellationTokenSource cleanupCts;
57 
61  readonly IDictionary<long, int> jobLockCounts;
62 
67 
71  TaskCompletionSource<object> newerDmbTcs;
72 
77 
85  public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger<DmbFactory> logger, Api.Models.Instance instance)
86  {
87  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
88  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
89  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
90  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
91 
92  cleanupTask = Task.CompletedTask;
93  newerDmbTcs = new TaskCompletionSource<object>();
94  cleanupCts = new CancellationTokenSource();
95  jobLockCounts = new Dictionary<long, int>();
96  }
97 
99  public void Dispose() => cleanupCts.Dispose(); // we don't dispose nextDmbProvider here, since it might be the only thing we have
100 
106  {
107  async Task HandleCleanup()
108  {
109  var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
110  Task otherTask;
111 
112  // lock (this) //already locked below
113  otherTask = cleanupTask;
114  await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
115  }
116 
117  lock (jobLockCounts)
118  if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
119  {
120  jobLockCounts.Remove(job.Id);
121  logger.LogDebug("Cleaning lock-free compile job {0} => {1}", job.Id, job.DirectoryName);
122  cleanupTask = HandleCleanup();
123  }
124  else
125  {
126  var decremented = --jobLockCounts[job.Id];
127  logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented);
128  }
129  }
130 
132  public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
133  {
134  if (job == null)
135  throw new ArgumentNullException(nameof(job));
136 
137  var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
138  if (newProvider == null)
139  return;
140  lock (jobLockCounts)
141  {
142  nextDmbProvider?.Dispose();
143  nextDmbProvider = newProvider;
144  newerDmbTcs.SetResult(nextDmbProvider);
145  newerDmbTcs = new TaskCompletionSource<object>();
146  }
147  }
148 
150  public IDmbProvider LockNextDmb(int lockCount)
151  {
152  if (!DmbAvailable)
153  throw new InvalidOperationException("No .dmb available!");
154  if (lockCount < 0)
155  throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
156  lock (jobLockCounts)
157  {
158  var jobId = nextDmbProvider.CompileJob.Id;
159  var incremented = jobLockCounts[jobId] += lockCount;
160  logger.LogTrace("Compile job {0} lock count now: {1}", jobId, incremented);
161  return nextDmbProvider;
162  }
163  }
164 
166  public async Task StartAsync(CancellationToken cancellationToken)
167  {
168  CompileJob cj = null;
169  await databaseContextFactory.UseContext(async (db) =>
170  {
171  cj = await db
172  .MostRecentCompletedCompileJobOrDefault(instance, cancellationToken)
173  .ConfigureAwait(false);
174  })
175  .ConfigureAwait(false);
176 
177  if (cj == default(CompileJob))
178  return;
179  await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
180 
181  // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
182  }
183 
185  public async Task StopAsync(CancellationToken cancellationToken)
186  {
187  using (cancellationToken.Register(() => cleanupCts.Cancel()))
188  await cleanupTask.ConfigureAwait(false);
189  }
190 
192  #pragma warning disable CA1506 // TODO: Decomplexify
193  public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
194  {
195  if (compileJob == null)
196  throw new ArgumentNullException(nameof(compileJob));
197 
198  // ensure we have the entire compile job tree
199  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
200  await databaseContextFactory.UseContext(
201  async db => compileJob = await db
202  .CompileJobs
203  .AsQueryable()
204  .Where(x => x.Id == compileJob.Id)
205  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
206  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
207  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
208  .FirstAsync(cancellationToken)
209  .ConfigureAwait(false))
210  .ConfigureAwait(false); // can't wait to see that query
211 
212  if (!compileJob.Job.StoppedAt.HasValue)
213  {
214  // This happens if we're told to load the compile job that is currently finished up
215  // It can constitute an API violation if it's returned by the DreamDaemonController so just set it here
216  // Bit of a hack, but it should work out to be the same value
217  logger.LogTrace("Setting missing StoppedAt for CompileJob job...");
218  compileJob.Job.StoppedAt = DateTimeOffset.Now;
219  }
220 
221  var providerSubmitted = false;
222  var newProvider = new DmbProvider(compileJob, ioManager, () =>
223  {
224  if (providerSubmitted)
225  CleanJob(compileJob);
226  });
227 
228  try
229  {
230  var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
231  var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
232 
233  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
234  {
235  logger.LogWarning("Error loading compile job, .dmb missing!");
236  return null; // omae wa mou shinderu
237  }
238 
239  lock (jobLockCounts)
240  {
241  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
242  {
243  value = 1;
244  jobLockCounts.Add(compileJob.Id, 1);
245  }
246  else
247  jobLockCounts[compileJob.Id] = ++value;
248 
249  logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
250 
251  providerSubmitted = true;
252  return newProvider;
253  }
254  }
255  finally
256  {
257  if (!providerSubmitted)
258  newProvider.Dispose();
259  }
260  }
261  #pragma warning restore CA1506
262 
264  #pragma warning disable CA1506 // TODO: Decomplexify
265  public async Task CleanUnusedCompileJobs(CancellationToken cancellationToken)
266  {
267  List<long> jobIdsToSkip;
268 
269  // don't clean locked directories
270  lock (jobLockCounts)
271  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
272 
273  List<string> jobUidsToNotErase = null;
274 
275  // find the uids of locked directories
276  await databaseContextFactory.UseContext(async db =>
277  {
278  jobUidsToNotErase = (await db
279  .CompileJobs
280  .AsQueryable()
281  .Where(
282  x => x.Job.Instance.Id == instance.Id
283  && jobIdsToSkip.Contains(x.Id))
284  .Select(x => x.DirectoryName.Value)
285  .ToListAsync(cancellationToken)
286  .ConfigureAwait(false))
287  .Select(x => x.ToString())
288  .ToList();
289  }).ConfigureAwait(false);
290 
291  jobUidsToNotErase.Add(WindowsSwappableDmbProvider.LiveGameDirectory);
292 
293  logger.LogTrace("We will not clean the following directories: {0}", String.Join(", ", jobUidsToNotErase));
294 
295  // cleanup
296  var gameDirectory = ioManager.ResolvePath();
297  await ioManager.CreateDirectory(gameDirectory, cancellationToken).ConfigureAwait(false);
298  var directories = await ioManager.GetDirectories(gameDirectory, cancellationToken).ConfigureAwait(false);
299  int deleting = 0;
300  var tasks = directories.Select(async x =>
301  {
302  var nameOnly = ioManager.GetFileName(x);
303  if (jobUidsToNotErase.Contains(nameOnly))
304  return;
305  logger.LogDebug("Cleaning unused game folder: {0}...", nameOnly);
306  try
307  {
308  ++deleting;
309  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
310  }
311  catch (OperationCanceledException)
312  {
313  throw;
314  }
315  catch (Exception e)
316  {
317  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
318  }
319  }).ToList();
320  if (deleting > 0)
321  await Task.WhenAll(tasks).ConfigureAwait(false);
322  }
323  #pragma warning restore CA1506
324 
327  {
328  if (!DmbAvailable)
329  return null;
330  return LockNextDmb(0)?.CompileJob;
331  }
332  }
333 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
async Task StartAsync(CancellationToken cancellationToken)
Definition: DmbFactory.cs:166
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:76
@@ -106,7 +106,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
async Task CleanUnusedCompileJobs(CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder.
Definition: DmbFactory.cs:259
+
async Task CleanUnusedCompileJobs(CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder.
Definition: DmbFactory.cs:265
DateTimeOffset StoppedAt
When the Job stopped
Definition: Job.cs:37
void CleanJob(CompileJob job)
Delete the Api.Models.Internal.CompileJob.DirectoryName of job
Definition: DmbFactory.cs:105
Job Job
See Api.Models.CompileJob.Job
Definition: CompileJob.cs:13
@@ -115,7 +115,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IDictionary< long, int > jobLockCounts
Map of CompileJob.JobIds to locks on them.
Definition: DmbFactory.cs:61
readonly IIOManager ioManager
The IIOManager for the DmbFactory
Definition: DmbFactory.cs:41
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask
Definition: DmbFactory.cs:56
-
CompileJob LatestCompileJob()
Gets the latest CompileJob.
Definition: DmbFactory.cs:316
+
CompileJob LatestCompileJob()
Gets the latest CompileJob.
Definition: DmbFactory.cs:326
@@ -131,7 +131,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IDmbProvider LockNextDmb(int lockCount)
Gets the next IDmbProvider
Definition: DmbFactory.cs:150
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory
Definition: DmbFactory.cs:46
- +
readonly Api.Models.Instance instance
The Api.Models.Instance for the DmbFactory
Definition: DmbFactory.cs:51
diff --git a/_dmb_provider_8cs.html b/_dmb_provider_8cs.html index 236fe453ce..0ba96e0bfc 100644 --- a/_dmb_provider_8cs.html +++ b/_dmb_provider_8cs.html @@ -25,7 +25,7 @@
diff --git a/_dmb_provider_8cs_source.html b/_dmb_provider_8cs_source.html index 0b3ae51073..969cb680f4 100644 --- a/_dmb_provider_8cs_source.html +++ b/_dmb_provider_8cs_source.html @@ -25,7 +25,7 @@ @@ -98,11 +98,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
DmbProvider(CompileJob compileJob, IIOManager ioManager, Action onDispose)
Construct a DmbProvider
Definition: DmbProvider.cs:40
-
const string ADirectoryName
Name of the primary directory used for compilation
Definition: DreamMaker.cs:33
+
const string ADirectoryName
Name of the primary directory used for compilation
Definition: DreamMaker.cs:32
readonly IIOManager ioManager
The IIOManager for the DmbProvider
Definition: DmbProvider.cs:27
-
const string DmbExtension
Extension for .dmbs
Definition: DreamMaker.cs:43
+
const string DmbExtension
Extension for .dmbs
Definition: DreamMaker.cs:42
Guid DirectoryName
The Game folder the results were compiled into
Definition: CompileJob.cs:28
Action onDispose
The Action to run when Dispose is called
Definition: DmbProvider.cs:32
@@ -112,7 +112,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
const string BDirectoryName
Name of the secondary directory used for compilation
Definition: DreamMaker.cs:38
+
const string BDirectoryName
Name of the secondary directory used for compilation
Definition: DreamMaker.cs:37
diff --git a/_dream_daemon_8cs.html b/_dream_daemon_8cs.html index ab183b7d20..7bb6e5201e 100644 --- a/_dream_daemon_8cs.html +++ b/_dream_daemon_8cs.html @@ -25,7 +25,7 @@
diff --git a/_dream_daemon_8cs_source.html b/_dream_daemon_8cs_source.html index 49861f6cc9..3b57ad2f43 100644 --- a/_dream_daemon_8cs_source.html +++ b/_dream_daemon_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_dream_daemon_client_8cs.html b/_dream_daemon_client_8cs.html index 772406b76a..0c70cf4176 100644 --- a/_dream_daemon_client_8cs.html +++ b/_dream_daemon_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_dream_daemon_client_8cs_source.html b/_dream_daemon_client_8cs_source.html index 436df15cb6..003aaa8918 100644 --- a/_dream_daemon_client_8cs_source.html +++ b/_dream_daemon_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_dream_daemon_controller_8cs.html b/_dream_daemon_controller_8cs.html index 5edf06c492..e27b22832b 100644 --- a/_dream_daemon_controller_8cs.html +++ b/_dream_daemon_controller_8cs.html @@ -25,7 +25,7 @@ diff --git a/_dream_daemon_controller_8cs_source.html b/_dream_daemon_controller_8cs_source.html index 181ed134a3..d93ade2fa6 100644 --- a/_dream_daemon_controller_8cs_source.html +++ b/_dream_daemon_controller_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,8 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Linq.Expressions;
7 using System.Net;
8 using System.Reflection;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
20 
21 namespace Tgstation.Server.Host.Controllers
22 {
26  [Route(Routes.DreamDaemon)]
27  public sealed class DreamDaemonController : ApiController
28  {
33 
38 
47  public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamDaemonController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
48  {
49  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
50  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
51  }
52 
60  [HttpPut]
61  [TgsAuthorize(DreamDaemonRights.Start)]
62  [ProducesResponseType(typeof(Api.Models.Job), 202)]
63  [ProducesResponseType(410)]
64  public async Task<IActionResult> Create(CancellationToken cancellationToken)
65  {
66  // alias for launching DD
67  var instance = instanceManager.GetInstance(Instance);
68 
69  if (instance.Watchdog.Running)
70  return StatusCode((int)HttpStatusCode.Gone);
71 
72  var job = new Models.Job
73  {
74  Description = "Launch DreamDaemon",
75  CancelRight = (ulong)DreamDaemonRights.Shutdown,
76  CancelRightsType = RightsType.DreamDaemon,
78  StartedBy = AuthenticationContext.User
79  };
80  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false);
81  return Accepted(job.ToApi());
82  }
83 
90  [HttpGet]
91  [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)]
92  [ProducesResponseType(typeof(DreamDaemon), 200)]
93  [ProducesResponseType(410)]
94  public Task<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
95 
102  async Task<IActionResult> ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
103  {
104  var instance = instanceManager.GetInstance(Instance);
105  var dd = instance.Watchdog;
106 
107  var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0;
108  var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0;
109 
110  if (settings == null)
111  {
112  settings = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
113  if (settings == default)
114  return StatusCode((int)HttpStatusCode.Gone);
115  }
116 
117  var result = new DreamDaemon();
118  if (metadata)
119  {
120  var alphaActive = dd.AlphaIsActive;
121  var llp = dd.LastLaunchParameters;
122  var rstate = dd.RebootState;
123  result.AutoStart = settings.AutoStart.Value;
124  result.CurrentPort = alphaActive ? llp?.PrimaryPort.Value : llp?.SecondaryPort.Value;
125  result.CurrentSecurity = llp?.SecurityLevel.Value;
126  result.CurrentAllowWebclient = llp?.AllowWebClient.Value;
127  result.PrimaryPort = settings.PrimaryPort.Value;
128  result.AllowWebClient = settings.AllowWebClient.Value;
129  result.Running = dd.Running;
130  result.SecondaryPort = settings.SecondaryPort.Value;
131  result.SecurityLevel = settings.SecurityLevel.Value;
132  result.SoftRestart = rstate == RebootState.Restart;
133  result.SoftShutdown = rstate == RebootState.Shutdown;
134  result.StartupTimeout = settings.StartupTimeout.Value;
135  result.HeartbeatSeconds = settings.HeartbeatSeconds.Value;
136  }
137 
138  if (revision)
139  {
140  var latestCompileJob = instance.LatestCompileJob();
141  result.ActiveCompileJob = ((dd.Running ? dd.ActiveCompileJob : latestCompileJob) ?? latestCompileJob)?.ToApi();
142  if (latestCompileJob?.Id != result.ActiveCompileJob?.Id)
143  result.StagedCompileJob = latestCompileJob?.ToApi();
144  }
145 
146  return Json(result);
147  }
148 
155  [HttpDelete]
156  [TgsAuthorize(DreamDaemonRights.Shutdown)]
157  [ProducesResponseType(204)]
158  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
159  {
160  var instance = instanceManager.GetInstance(Instance);
161  await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false);
162  return NoContent();
163  }
164 
173  [HttpPost]
174  [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout | DreamDaemonRights.SetHeartbeatInterval)]
175  [ProducesResponseType(typeof(DreamDaemon), 200)]
176  [ProducesResponseType(410)]
177  #pragma warning disable CA1502 // TODO: Decomplexify
178  #pragma warning disable CA1506
179  public async Task<IActionResult> Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
180  {
181  if (model == null)
182  throw new ArgumentNullException(nameof(model));
183 
184  if (model.PrimaryPort == 0)
185  throw new InvalidOperationException("Primary port cannot be 0!");
186 
187  if (model.SecondaryPort == 0)
188  throw new InvalidOperationException("Secondary port cannot be 0!");
189 
190  if (model.SoftShutdown == true && model.SoftRestart == true)
191  return BadRequest(new ErrorMessage(ErrorCode.DreamDaemonDoubleSoft));
192 
193  // alias for changing DD settings
194  var current = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
195 
196  if (current == default)
197  return StatusCode((int)HttpStatusCode.Gone);
198 
199  var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon);
200 
201  bool CheckModified<T>(Expression<Func<Api.Models.Internal.DreamDaemonSettings, T>> expression, DreamDaemonRights requiredRight)
202  {
203  var memberSelectorExpression = (MemberExpression)expression.Body;
204  var property = (PropertyInfo)memberSelectorExpression.Member;
205 
206  var newVal = property.GetValue(model);
207  if (newVal == null)
208  return false;
209  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
210  return true;
211 
212  property.SetValue(current, newVal);
213  return false;
214  }
215 
216  var instance = instanceManager.GetInstance(Instance);
217  var dd = instance.Watchdog;
218  var rebootState = dd.RebootState;
219  var oldSoftRestart = rebootState == RebootState.Restart;
220  var oldSoftShutdown = rebootState == RebootState.Shutdown;
221 
222  if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient)
223  || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart)
224  || CheckModified(x => x.PrimaryPort, DreamDaemonRights.SetPorts)
225  || CheckModified(x => x.SecondaryPort, DreamDaemonRights.SetPorts)
226  || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity)
227  || (model.SoftRestart.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart))
228  || (model.SoftShutdown.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown))
229  || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout)
230  || CheckModified(x => x.HeartbeatSeconds, DreamDaemonRights.SetHeartbeatInterval))
231  return Forbid();
232 
233  if (current.PrimaryPort == current.SecondaryPort)
234  return BadRequest(new ErrorMessage(ErrorCode.DreamDaemonDuplicatePorts));
235 
236  var wd = instanceManager.GetInstance(Instance).Watchdog;
237 
238  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
239 
240  // run this second because current may be modified by it
241  await wd.ChangeSettings(current, cancellationToken).ConfigureAwait(false);
242 
243  if (!oldSoftRestart && model.SoftRestart == true)
244  await wd.Restart(true, cancellationToken).ConfigureAwait(false);
245  else if (!oldSoftShutdown && model.SoftShutdown == true)
246  await wd.Terminate(true, cancellationToken).ConfigureAwait(false);
247  else if ((oldSoftRestart && model.SoftRestart == false) || (oldSoftShutdown && model.SoftShutdown == false))
248  await wd.ResetRebootState(cancellationToken).ConfigureAwait(false);
249 
250  return await ReadImpl(current, cancellationToken).ConfigureAwait(false);
251  }
252  #pragma warning restore CA1506
253  #pragma warning restore CA1502
254 
261  [HttpPatch]
262  [TgsAuthorize(DreamDaemonRights.Restart)]
263  [ProducesResponseType(typeof(Api.Models.Job), 202)]
264  public async Task<IActionResult> Restart(CancellationToken cancellationToken)
265  {
266  var job = new Models.Job
267  {
268  Instance = Instance,
269  CancelRightsType = RightsType.DreamDaemon,
270  CancelRight = (ulong)DreamDaemonRights.Shutdown,
271  StartedBy = AuthenticationContext.User,
272  Description = "Restart Watchdog"
273  };
274 
275  var watchdog = instanceManager.GetInstance(Instance).Watchdog;
276 
277  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false);
278  return Accepted(job.ToApi());
279  }
280  }
281 }
async Task< IActionResult > Delete(CancellationToken cancellationToken)
Stops the Watchdog if it&#39;s running.
-
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Linq.Expressions;
7 using System.Net;
8 using System.Reflection;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
20 
21 namespace Tgstation.Server.Host.Controllers
22 {
26  [Route(Routes.DreamDaemon)]
27  public sealed class DreamDaemonController : ApiController
28  {
33 
38 
47  public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamDaemonController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
48  {
49  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
50  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
51  }
52 
60  [HttpPut]
61  [TgsAuthorize(DreamDaemonRights.Start)]
62  [ProducesResponseType(typeof(Api.Models.Job), 202)]
63  [ProducesResponseType(410)]
64  public async Task<IActionResult> Create(CancellationToken cancellationToken)
65  {
66  // alias for launching DD
67  var instance = instanceManager.GetInstance(Instance);
68 
69  if (instance.Watchdog.Running)
70  return StatusCode((int)HttpStatusCode.Gone);
71 
72  var job = new Models.Job
73  {
74  Description = "Launch DreamDaemon",
75  CancelRight = (ulong)DreamDaemonRights.Shutdown,
76  CancelRightsType = RightsType.DreamDaemon,
78  StartedBy = AuthenticationContext.User
79  };
80  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false);
81  return Accepted(job.ToApi());
82  }
83 
90  [HttpGet]
91  [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)]
92  [ProducesResponseType(typeof(DreamDaemon), 200)]
93  [ProducesResponseType(410)]
94  public Task<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
95 
102  async Task<IActionResult> ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
103  {
104  var instance = instanceManager.GetInstance(Instance);
105  var dd = instance.Watchdog;
106 
107  var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0;
108  var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0;
109 
110  if (settings == null)
111  {
112  settings = await DatabaseContext
113  .Instances
114  .AsQueryable()
115  .Where(x => x.Id == Instance.Id)
116  .Select(x => x.DreamDaemonSettings)
117  .FirstOrDefaultAsync(cancellationToken)
118  .ConfigureAwait(false);
119  if (settings == default)
120  return StatusCode((int)HttpStatusCode.Gone);
121  }
122 
123  var result = new DreamDaemon();
124  if (metadata)
125  {
126  var alphaActive = dd.AlphaIsActive;
127  var llp = dd.LastLaunchParameters;
128  var rstate = dd.RebootState;
129  result.AutoStart = settings.AutoStart.Value;
130  result.CurrentPort = alphaActive ? llp?.PrimaryPort.Value : llp?.SecondaryPort.Value;
131  result.CurrentSecurity = llp?.SecurityLevel.Value;
132  result.CurrentAllowWebclient = llp?.AllowWebClient.Value;
133  result.PrimaryPort = settings.PrimaryPort.Value;
134  result.AllowWebClient = settings.AllowWebClient.Value;
135  result.Running = dd.Running;
136  result.SecondaryPort = settings.SecondaryPort.Value;
137  result.SecurityLevel = settings.SecurityLevel.Value;
138  result.SoftRestart = rstate == RebootState.Restart;
139  result.SoftShutdown = rstate == RebootState.Shutdown;
140  result.StartupTimeout = settings.StartupTimeout.Value;
141  result.HeartbeatSeconds = settings.HeartbeatSeconds.Value;
142  }
143 
144  if (revision)
145  {
146  var latestCompileJob = instance.LatestCompileJob();
147  result.ActiveCompileJob = ((dd.Running ? dd.ActiveCompileJob : latestCompileJob) ?? latestCompileJob)?.ToApi();
148  if (latestCompileJob?.Id != result.ActiveCompileJob?.Id)
149  result.StagedCompileJob = latestCompileJob?.ToApi();
150  }
151 
152  return Json(result);
153  }
154 
161  [HttpDelete]
162  [TgsAuthorize(DreamDaemonRights.Shutdown)]
163  [ProducesResponseType(204)]
164  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
165  {
166  var instance = instanceManager.GetInstance(Instance);
167  await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false);
168  return NoContent();
169  }
170 
179  [HttpPost]
180  [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout | DreamDaemonRights.SetHeartbeatInterval)]
181  [ProducesResponseType(typeof(DreamDaemon), 200)]
182  [ProducesResponseType(410)]
183  #pragma warning disable CA1502 // TODO: Decomplexify
184  #pragma warning disable CA1506
185  public async Task<IActionResult> Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
186  {
187  if (model == null)
188  throw new ArgumentNullException(nameof(model));
189 
190  if (model.PrimaryPort == 0)
191  throw new InvalidOperationException("Primary port cannot be 0!");
192 
193  if (model.SecondaryPort == 0)
194  throw new InvalidOperationException("Secondary port cannot be 0!");
195 
196  if (model.SoftShutdown == true && model.SoftRestart == true)
197  return BadRequest(new ErrorMessage(ErrorCode.DreamDaemonDoubleSoft));
198 
199  // alias for changing DD settings
200  var current = await DatabaseContext
201  .Instances
202  .AsQueryable()
203  .Where(x => x.Id == Instance.Id)
204  .Select(x => x.DreamDaemonSettings)
205  .FirstOrDefaultAsync(cancellationToken)
206  .ConfigureAwait(false);
207 
208  if (current == default)
209  return StatusCode((int)HttpStatusCode.Gone);
210 
211  var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon);
212 
213  bool CheckModified<T>(Expression<Func<Api.Models.Internal.DreamDaemonSettings, T>> expression, DreamDaemonRights requiredRight)
214  {
215  var memberSelectorExpression = (MemberExpression)expression.Body;
216  var property = (PropertyInfo)memberSelectorExpression.Member;
217 
218  var newVal = property.GetValue(model);
219  if (newVal == null)
220  return false;
221  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
222  return true;
223 
224  property.SetValue(current, newVal);
225  return false;
226  }
227 
228  var instance = instanceManager.GetInstance(Instance);
229  var dd = instance.Watchdog;
230  var rebootState = dd.RebootState;
231  var oldSoftRestart = rebootState == RebootState.Restart;
232  var oldSoftShutdown = rebootState == RebootState.Shutdown;
233 
234  if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient)
235  || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart)
236  || CheckModified(x => x.PrimaryPort, DreamDaemonRights.SetPorts)
237  || CheckModified(x => x.SecondaryPort, DreamDaemonRights.SetPorts)
238  || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity)
239  || (model.SoftRestart.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart))
240  || (model.SoftShutdown.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown))
241  || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout)
242  || CheckModified(x => x.HeartbeatSeconds, DreamDaemonRights.SetHeartbeatInterval))
243  return Forbid();
244 
245  if (current.PrimaryPort == current.SecondaryPort)
246  return BadRequest(new ErrorMessage(ErrorCode.DreamDaemonDuplicatePorts));
247 
248  var wd = instanceManager.GetInstance(Instance).Watchdog;
249 
250  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
251 
252  // run this second because current may be modified by it
253  await wd.ChangeSettings(current, cancellationToken).ConfigureAwait(false);
254 
255  if (!oldSoftRestart && model.SoftRestart == true)
256  await wd.Restart(true, cancellationToken).ConfigureAwait(false);
257  else if (!oldSoftShutdown && model.SoftShutdown == true)
258  await wd.Terminate(true, cancellationToken).ConfigureAwait(false);
259  else if ((oldSoftRestart && model.SoftRestart == false) || (oldSoftShutdown && model.SoftShutdown == false))
260  await wd.ResetRebootState(cancellationToken).ConfigureAwait(false);
261 
262  return await ReadImpl(current, cancellationToken).ConfigureAwait(false);
263  }
264  #pragma warning restore CA1506
265  #pragma warning restore CA1502
266 
273  [HttpPatch]
274  [TgsAuthorize(DreamDaemonRights.Restart)]
275  [ProducesResponseType(typeof(Api.Models.Job), 202)]
276  public async Task<IActionResult> Restart(CancellationToken cancellationToken)
277  {
278  var job = new Models.Job
279  {
280  Instance = Instance,
281  CancelRightsType = RightsType.DreamDaemon,
282  CancelRight = (ulong)DreamDaemonRights.Shutdown,
283  StartedBy = AuthenticationContext.User,
284  Description = "Restart Watchdog"
285  };
286 
287  var watchdog = instanceManager.GetInstance(Instance).Watchdog;
288 
289  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false);
290  return Accepted(job.ToApi());
291  }
292  }
293 }
async Task< IActionResult > Delete(CancellationToken cancellationToken)
Stops the Watchdog if it&#39;s running.
@@ -106,22 +105,23 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonSecurity SecurityLevel
The DreamDaemonSecurity level of DreamDaemon
-
async Task< IActionResult > Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
Update watchdog settings to be applied at next server reboot.
+
async Task< IActionResult > Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
Update watchdog settings to be applied at next server reboot.
bool AutoStart
If DreamDaemon starts when it&#39;s Instance starts
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
+
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
async Task< IActionResult > ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
Implementation of Read(CancellationToken)
-
async Task< IActionResult > Restart(CancellationToken cancellationToken)
Creates a Api.Models.Job to restart the Watchdog. It will start if it wasn&#39;t already running...
+
async Task< IActionResult > Restart(CancellationToken cancellationToken)
Creates a Api.Models.Job to restart the Watchdog. It will start if it wasn&#39;t already running...
Routes to a server actions
Definition: Routes.cs:9
DreamDaemonRights
Rights for Models.DreamDaemon
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
DreamDaemonRights DreamDaemonRights
The Rights.DreamDaemonRights of the InstanceUser
Definition: InstanceUser.cs:33
-
DbSet< Instance > Instances
The Instances in the DatabaseContext<TParentContext>.
async Task< IActionResult > Create(CancellationToken cancellationToken)
Launches the watchdog.
+
DbSet< Instance > Instances
The Instances in the DatabaseContext.
DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger< DreamDaemonController > logger)
Construct a DreamDaemonController
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
@@ -145,7 +145,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ushort PrimaryPort
The first port DreamDaemon uses. This should be the publically advertised port
- +
bool AllowWebClient
If the BYOND web client can be used to connect to the game server
diff --git a/_dream_daemon_launch_parameters_8cs.html b/_dream_daemon_launch_parameters_8cs.html index 8f7307e235..b224cde920 100644 --- a/_dream_daemon_launch_parameters_8cs.html +++ b/_dream_daemon_launch_parameters_8cs.html @@ -25,7 +25,7 @@
diff --git a/_dream_daemon_launch_parameters_8cs_source.html b/_dream_daemon_launch_parameters_8cs_source.html index 51ece73d97..8c85d3cc85 100644 --- a/_dream_daemon_launch_parameters_8cs_source.html +++ b/_dream_daemon_launch_parameters_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_dream_daemon_rights_8cs.html b/_dream_daemon_rights_8cs.html index e35dc747d7..a165068adb 100644 --- a/_dream_daemon_rights_8cs.html +++ b/_dream_daemon_rights_8cs.html @@ -25,7 +25,7 @@ diff --git a/_dream_daemon_rights_8cs_source.html b/_dream_daemon_rights_8cs_source.html index edcfac4b54..6437cdb004 100644 --- a/_dream_daemon_rights_8cs_source.html +++ b/_dream_daemon_rights_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_dream_daemon_security_8cs.html b/_dream_daemon_security_8cs.html index f19d76abdc..07ebfc2d0c 100644 --- a/_dream_daemon_security_8cs.html +++ b/_dream_daemon_security_8cs.html @@ -25,7 +25,7 @@ diff --git a/_dream_daemon_security_8cs_source.html b/_dream_daemon_security_8cs_source.html index b6bea22dd7..070f3ba18e 100644 --- a/_dream_daemon_security_8cs_source.html +++ b/_dream_daemon_security_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_dream_maker_client_8cs.html b/_dream_maker_client_8cs.html index d13b591f95..88252c0cc8 100644 --- a/_dream_maker_client_8cs.html +++ b/_dream_maker_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_dream_maker_client_8cs_source.html b/_dream_maker_client_8cs_source.html index be005ffc6d..a0f718cb27 100644 --- a/_dream_maker_client_8cs_source.html +++ b/_dream_maker_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_dream_maker_controller_8cs.html b/_dream_maker_controller_8cs.html index 72f41a2d5d..e156618be8 100644 --- a/_dream_maker_controller_8cs.html +++ b/_dream_maker_controller_8cs.html @@ -25,7 +25,7 @@ diff --git a/_dream_maker_controller_8cs_source.html b/_dream_maker_controller_8cs_source.html index bc124fc91c..f6f0815b7f 100644 --- a/_dream_maker_controller_8cs_source.html +++ b/_dream_maker_controller_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,30 +94,31 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamMakerController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
18 
19 namespace Tgstation.Server.Host.Controllers
20 {
24  [Route(Routes.DreamMaker)]
25  #pragma warning disable CA1506 // TODO: Decomplexify
26  public sealed class DreamMakerController : ApiController
27  {
32 
37 
46  public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamMakerController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
47  {
48  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
49  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
50  }
51 
58  [HttpGet]
59  [TgsAuthorize(DreamMakerRights.Read)]
60  [ProducesResponseType(typeof(DreamMaker), 200)]
61  public async Task<IActionResult> Read(CancellationToken cancellationToken)
62  {
63  var instance = instanceManager.GetInstance(Instance);
64  var dreamMakerSettings = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
65  return Json(dreamMakerSettings.ToApi());
66  }
67 
76  [HttpGet("{id}")]
77  [TgsAuthorize(DreamMakerRights.CompileJobs)]
78  [ProducesResponseType(typeof(Api.Models.CompileJob), 200)]
79  [ProducesResponseType(404)]
80  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
81  {
82  var compileJob = await DatabaseContext.CompileJobs
83  .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
84  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
85  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
86  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
87  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
88  if (compileJob == default)
89  return NotFound();
90  return Json(compileJob.ToApi());
91  }
92 
99  [HttpGet(Routes.List)]
100  [TgsAuthorize(DreamMakerRights.CompileJobs)]
101  [ProducesResponseType(typeof(List<EntityId>), 200)]
102  public async Task<IActionResult> List(CancellationToken cancellationToken)
103  {
104  var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new EntityId
105  {
106  Id = x.Id
107  }).ToListAsync(cancellationToken).ConfigureAwait(false);
108  return Json(compileJobs);
109  }
110 
117  [HttpPut]
118  [TgsAuthorize(DreamMakerRights.Compile)]
119  [ProducesResponseType(typeof(Api.Models.Job), 202)]
120  public async Task<IActionResult> Create(CancellationToken cancellationToken)
121  {
122  var job = new Models.Job
123  {
124  Description = "Compile active repository code",
125  StartedBy = AuthenticationContext.User,
126  CancelRightsType = RightsType.DreamMaker,
127  CancelRight = (ulong)DreamMakerRights.CancelCompile,
129  };
130 
131  IInstance instance = instanceManager.GetInstance(Instance);
132  await jobManager.RegisterOperation(
133  job,
134  instance.DreamMaker.DeploymentProcess,
135  cancellationToken)
136  .ConfigureAwait(false);
137  return Accepted(job.ToApi());
138  }
139 
149  [HttpPost]
150  [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)]
151  [ProducesResponseType(typeof(DreamMaker), 200)]
152  [ProducesResponseType(204)]
153  [ProducesResponseType(410)]
154  public async Task<IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
155  {
156  if (model == null)
157  throw new ArgumentNullException(nameof(model));
158 
159  if (model.ApiValidationPort == 0)
160  throw new InvalidOperationException("ApiValidationPort cannot be 0!");
161 
162  var hostModel = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
163  if (hostModel == null)
164  return StatusCode((int)HttpStatusCode.Gone);
165 
166  if (model.ProjectName != null)
167  {
169  return Forbid();
170  if (model.ProjectName.Length == 0)
171  hostModel.ProjectName = null;
172  else
173  hostModel.ProjectName = model.ProjectName;
174  }
175 
176  if (model.ApiValidationPort.HasValue)
177  {
178  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort))
179  return Forbid();
180  hostModel.ApiValidationPort = model.ApiValidationPort;
181  }
182 
183  if (model.ApiValidationSecurityLevel.HasValue)
184  {
185  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel))
186  return Forbid();
187  hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
188  }
189 
190  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
191 
192  if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0)
193  return NoContent();
194 
195  return await Read(cancellationToken).ConfigureAwait(false);
196  }
197  }
198 }
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
- +Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
18 
19 namespace Tgstation.Server.Host.Controllers
20 {
24  [Route(Routes.DreamMaker)]
25  #pragma warning disable CA1506 // TODO: Decomplexify
26  public sealed class DreamMakerController : ApiController
27  {
32 
37 
46  public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamMakerController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
47  {
48  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
49  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
50  }
51 
58  [HttpGet]
59  [TgsAuthorize(DreamMakerRights.Read)]
60  [ProducesResponseType(typeof(DreamMaker), 200)]
61  public async Task<IActionResult> Read(CancellationToken cancellationToken)
62  {
63  var instance = instanceManager.GetInstance(Instance);
64  var dreamMakerSettings = await DatabaseContext
66  .AsQueryable()
67  .Where(x => x.InstanceId == Instance.Id)
68  .FirstOrDefaultAsync(cancellationToken)
69  .ConfigureAwait(false);
70  return Json(dreamMakerSettings.ToApi());
71  }
72 
81  [HttpGet("{id}")]
82  [TgsAuthorize(DreamMakerRights.CompileJobs)]
83  [ProducesResponseType(typeof(Api.Models.CompileJob), 200)]
84  [ProducesResponseType(404)]
85  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
86  {
87  var compileJob = await DatabaseContext
89  .AsQueryable()
90  .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
91  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
92  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
93  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
94  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
95  if (compileJob == default)
96  return NotFound();
97  return Json(compileJob.ToApi());
98  }
99 
106  [HttpGet(Routes.List)]
107  [TgsAuthorize(DreamMakerRights.CompileJobs)]
108  [ProducesResponseType(typeof(List<EntityId>), 200)]
109  public async Task<IActionResult> List(CancellationToken cancellationToken)
110  {
111  var compileJobs = await DatabaseContext
112  .CompileJobs
113  .AsQueryable()
114  .Where(x => x.Job.Instance.Id == Instance.Id)
115  .OrderByDescending(x => x.Job.StoppedAt)
116  .Select(x => new EntityId
117  {
118  Id = x.Id
119  })
120  .ToListAsync(cancellationToken)
121  .ConfigureAwait(false);
122  return Json(compileJobs);
123  }
124 
131  [HttpPut]
132  [TgsAuthorize(DreamMakerRights.Compile)]
133  [ProducesResponseType(typeof(Api.Models.Job), 202)]
134  public async Task<IActionResult> Create(CancellationToken cancellationToken)
135  {
136  var job = new Models.Job
137  {
138  Description = "Compile active repository code",
139  StartedBy = AuthenticationContext.User,
140  CancelRightsType = RightsType.DreamMaker,
141  CancelRight = (ulong)DreamMakerRights.CancelCompile,
143  };
144 
145  IInstance instance = instanceManager.GetInstance(Instance);
146  await jobManager.RegisterOperation(
147  job,
148  instance.DreamMaker.DeploymentProcess,
149  cancellationToken)
150  .ConfigureAwait(false);
151  return Accepted(job.ToApi());
152  }
153 
163  [HttpPost]
164  [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)]
165  [ProducesResponseType(typeof(DreamMaker), 200)]
166  [ProducesResponseType(204)]
167  [ProducesResponseType(410)]
168  public async Task<IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
169  {
170  if (model == null)
171  throw new ArgumentNullException(nameof(model));
172 
173  if (model.ApiValidationPort == 0)
174  throw new InvalidOperationException("ApiValidationPort cannot be 0!");
175 
176  var hostModel = await DatabaseContext
178  .AsQueryable()
179  .Where(x => x.InstanceId == Instance.Id)
180  .FirstOrDefaultAsync(cancellationToken)
181  .ConfigureAwait(false);
182  if (hostModel == null)
183  return StatusCode((int)HttpStatusCode.Gone);
184 
185  if (model.ProjectName != null)
186  {
188  return Forbid();
189  if (model.ProjectName.Length == 0)
190  hostModel.ProjectName = null;
191  else
192  hostModel.ProjectName = model.ProjectName;
193  }
194 
195  if (model.ApiValidationPort.HasValue)
196  {
197  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort))
198  return Forbid();
199  hostModel.ApiValidationPort = model.ApiValidationPort;
200  }
201 
202  if (model.ApiValidationSecurityLevel.HasValue)
203  {
204  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel))
205  return Forbid();
206  hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
207  }
208 
209  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
210 
211  if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0)
212  return NoContent();
213 
214  return await Read(cancellationToken).ConfigureAwait(false);
215  }
216  }
217 }
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
Represents the state of the DreamMaker compiler. Create action starts a new compile. Delete action cancels the current compile
Definition: DreamMaker.cs:8
long Id
The ID of the entity.
Definition: EntityId.cs:11
-
async Task< IActionResult > List(CancellationToken cancellationToken)
List all Api.Models.CompileJob EntityIds for the instance.
+
async Task< IActionResult > List(CancellationToken cancellationToken)
List all Api.Models.CompileJob EntityIds for the instance.
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
ApiController for managing the deployment system.
-
async Task< IActionResult > Create(CancellationToken cancellationToken)
Begin deploying repository code.
+
async Task< IActionResult > Create(CancellationToken cancellationToken)
Begin deploying repository code.
-
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a Api.Models.CompileJob specified by a given id .
+
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a Api.Models.CompileJob specified by a given id .
+
DbSet< DreamMakerSettings > DreamMakerSettings
The Models.DreamMakerSettings in the DatabaseContext.
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
+
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
async Task< IActionResult > Read(CancellationToken cancellationToken)
Read current DreamMaker status.
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
+
DbSet< CompileJob > CompileJobs
The CompileJobs in the DatabaseContext.
Routes to a server actions
Definition: Routes.cs:9
-
DbSet< DreamMakerSettings > DreamMakerSettings
The Models.DreamMakerSettings in the DatabaseContext<TParentContext>.
-
async Task< IActionResult > Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
Update deployment settings.
+
async Task< IActionResult > Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
Update deployment settings.
IDreamMaker DreamMaker
The IDreamMaker for the IInstance.
Definition: IInstance.cs:31
const string DreamMaker
The Models.DreamMaker controller
Definition: Routes.cs:74
@@ -137,11 +138,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
const string List
The postfix for list operations
Definition: Routes.cs:84
-
DbSet< CompileJob > CompileJobs
The CompileJobs in the DatabaseContext<TParentContext>.
User can read all chat settings except Models.Internal.ChatBot.ConnectionString
- +
diff --git a/_dream_maker_rights_8cs.html b/_dream_maker_rights_8cs.html index cd53052231..7b4dcd8e67 100644 --- a/_dream_maker_rights_8cs.html +++ b/_dream_maker_rights_8cs.html @@ -25,7 +25,7 @@
diff --git a/_dream_maker_rights_8cs_source.html b/_dream_maker_rights_8cs_source.html index c15051a2cc..bf6c50a116 100644 --- a/_dream_maker_rights_8cs_source.html +++ b/_dream_maker_rights_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_dream_maker_settings_8cs.html b/_dream_maker_settings_8cs.html index e439a7d8f0..ee3af2af81 100644 --- a/_dream_maker_settings_8cs.html +++ b/_dream_maker_settings_8cs.html @@ -25,7 +25,7 @@ diff --git a/_dream_maker_settings_8cs_source.html b/_dream_maker_settings_8cs_source.html index 11ba384adc..16b4f5e603 100644 --- a/_dream_maker_settings_8cs_source.html +++ b/_dream_maker_settings_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_dual_reattach_information_base_8cs.html b/_dual_reattach_information_base_8cs.html index 85a4f805d3..7427a2a40e 100644 --- a/_dual_reattach_information_base_8cs.html +++ b/_dual_reattach_information_base_8cs.html @@ -25,7 +25,7 @@ diff --git a/_dual_reattach_information_base_8cs_source.html b/_dual_reattach_information_base_8cs_source.html index 8c844833b1..2e2a96bd3c 100644 --- a/_dual_reattach_information_base_8cs_source.html +++ b/_dual_reattach_information_base_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_entity_id_8cs.html b/_entity_id_8cs.html index b877641ac8..b7a4d2a68b 100644 --- a/_entity_id_8cs.html +++ b/_entity_id_8cs.html @@ -25,7 +25,7 @@ diff --git a/_entity_id_8cs_source.html b/_entity_id_8cs_source.html index c796dcf1af..60fe80ca9c 100644 --- a/_entity_id_8cs_source.html +++ b/_entity_id_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_error_code_8cs.html b/_error_code_8cs.html index b3a6fcc240..eecdeed057 100644 --- a/_error_code_8cs.html +++ b/_error_code_8cs.html @@ -25,7 +25,7 @@ diff --git a/_error_code_8cs_source.html b/_error_code_8cs_source.html index 3b77c4acda..6fd0a536b6 100644 --- a/_error_code_8cs_source.html +++ b/_error_code_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_error_code_extensions_8cs.html b/_error_code_extensions_8cs.html index b6a74f78cb..858349b5b5 100644 --- a/_error_code_extensions_8cs.html +++ b/_error_code_extensions_8cs.html @@ -25,7 +25,7 @@ diff --git a/_error_code_extensions_8cs_source.html b/_error_code_extensions_8cs_source.html index e890bdb304..b6d416c0ce 100644 --- a/_error_code_extensions_8cs_source.html +++ b/_error_code_extensions_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_error_message_8cs.html b/_error_message_8cs.html index 95d3ee70c1..32618d94e3 100644 --- a/_error_message_8cs.html +++ b/_error_message_8cs.html @@ -25,7 +25,7 @@ diff --git a/_error_message_8cs_source.html b/_error_message_8cs_source.html index be93a385ba..d890225c8f 100644 --- a/_error_message_8cs_source.html +++ b/_error_message_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_event_consumer_8cs.html b/_event_consumer_8cs.html index a9bac8dc30..df6dd23b2e 100644 --- a/_event_consumer_8cs.html +++ b/_event_consumer_8cs.html @@ -25,7 +25,7 @@ diff --git a/_event_consumer_8cs_source.html b/_event_consumer_8cs_source.html index fc74d9d79e..ea4501dde9 100644 --- a/_event_consumer_8cs_source.html +++ b/_event_consumer_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_event_notification_8cs.html b/_event_notification_8cs.html index 29da1f992f..f87ce5cc36 100644 --- a/_event_notification_8cs.html +++ b/_event_notification_8cs.html @@ -25,7 +25,7 @@ diff --git a/_event_notification_8cs_source.html b/_event_notification_8cs_source.html index 8a7ae4bd8e..0790d79bf2 100644 --- a/_event_notification_8cs_source.html +++ b/_event_notification_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_event_type_8cs.html b/_event_type_8cs.html index 6b30726c51..243c6bbeaf 100644 --- a/_event_type_8cs.html +++ b/_event_type_8cs.html @@ -25,7 +25,7 @@ diff --git a/_event_type_8cs_source.html b/_event_type_8cs_source.html index 8570ef9d2f..d0295c96d8 100644 --- a/_event_type_8cs_source.html +++ b/_event_type_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_experimental_watchdog_8cs.html b/_experimental_watchdog_8cs.html index 7f37169cef..2fea944383 100644 --- a/_experimental_watchdog_8cs.html +++ b/_experimental_watchdog_8cs.html @@ -25,7 +25,7 @@ diff --git a/_experimental_watchdog_8cs_source.html b/_experimental_watchdog_8cs_source.html index 335b62e27e..6c15b1de3a 100644 --- a/_experimental_watchdog_8cs_source.html +++ b/_experimental_watchdog_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ExperimentalWatchdog.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.Globalization;
6 using System.Threading;
7 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Components.Watchdog
17 {
22  {
26  const int AlphaBravoStartupSeperationInterval = 10; // TODO: Make this configurable
27 
29  public override bool AlphaIsActive => alphaIsActive;
30 
32  public override Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
33 
35  public override RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
36 
41 
46 
51 
68  IChatManager chat,
69  ISessionControllerFactory sessionControllerFactory,
70  IDmbFactory dmbFactory,
71  IReattachInfoHandler reattachInfoHandler,
72  IDatabaseContextFactory databaseContextFactory,
73  IJobManager jobManager,
74  IServerControl serverControl,
75  IAsyncDelayer asyncDelayer,
76  ILogger<ExperimentalWatchdog> logger,
77  DreamDaemonLaunchParameters initialLaunchParameters,
78  Api.Models.Instance instance, bool autoStart)
79  : base(
80  chat,
81  sessionControllerFactory,
82  dmbFactory,
83  reattachInfoHandler,
84  databaseContextFactory,
85  jobManager,
86  serverControl,
87  asyncDelayer,
88  logger,
89  initialLaunchParameters,
90  instance,
91  autoStart)
92  {
93  alphaIsActive = true;
94  }
95 
97  #pragma warning disable CA1502 // TODO: Decomplexify
98  protected override async Task HandleMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
99  {
100  Logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
101 
102  // this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
103  // I'll do my best to walk you through it
104 
105  // returns true if the inactive server can't be used immediately
106  // also sets monitor to restart if the above holds
107  bool FullRestartDeadInactive()
108  {
109  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
110  {
111  Logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
112  monitorState.NextAction = MonitorAction.Restart; // will dispose server
113  return true;
114  }
115 
116  return false;
117  }
118 
119  // trys to set inactive server's port to the public game port
120  // doesn't handle closing active server's port
121  // returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
122  // on failure, sets monitor to restart
123  async Task<bool> MakeInactiveActive()
124  {
125  Logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
126  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
127 
128  if (!result)
129  {
130  Logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
131  monitorState.NextAction = MonitorAction.Restart; // will dispose server
132  return false;
133  }
134 
135  // inactive server should always be using active launch parameters
136  LastLaunchParameters = ActiveLaunchParameters;
137 
138  var tmp = monitorState.ActiveServer;
139  monitorState.ActiveServer = monitorState.InactiveServer;
140  monitorState.InactiveServer = tmp;
141  alphaIsActive = !AlphaIsActive;
142  monitorState.ActiveServer.EnableCustomChatCommands();
143  return true;
144  }
145 
146  // Kills and tries to launch inactive server with the latest dmb
147  // falls back to current dmb on failure
148  // Sets critfail on inactive server failing that
149  // returns false if the backup dmb was used successfully, true otherwise
150  async Task UpdateAndRestartInactiveServer(bool breakAfter)
151  {
152  ActiveParametersUpdated = new TaskCompletionSource<object>();
153  monitorState.InactiveServer.Dispose(); // kill or recycle it
154  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
155  monitorState.NextAction = desiredNextAction;
156 
157  Logger.LogInformation("Rebooting inactive server...");
158  var newDmb = DmbFactory.LockNextDmb(1);
159  try
160  {
162  newDmb,
163  null,
164  ActiveLaunchParameters,
165  false,
166  !monitorState.ActiveServer.IsPrimary,
167  false,
168  cancellationToken)
169  .ConfigureAwait(false);
170  monitorState.InactiveServer.SetHighPriority();
171  }
172  catch (OperationCanceledException)
173  {
174  throw;
175  }
176  catch (Exception e)
177  {
178  Logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
179 
180  // ahh jeez, what do we do here?
181  // this is our fault, so it should never happen but
182  // idk maybe a database error while handling the newest dmb?
183  // either way try to start it using the active server's dmb as a backup
184  try
185  {
186  var dmbBackup = await DmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
187 
188  if (dmbBackup == null) // NANI!?
189  throw new InvalidOperationException("Watchdog double crit-fail!"); // just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
190 
192  dmbBackup,
193  null,
194  ActiveLaunchParameters,
195  false,
196  !monitorState.ActiveServer.IsPrimary,
197  false,
198  cancellationToken)
199  .ConfigureAwait(false);
200  monitorState.InactiveServer.SetHighPriority();
201  await Chat.SendWatchdogMessage(
202  "Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...",
203  false,
204  cancellationToken).ConfigureAwait(false);
205  }
206  catch (OperationCanceledException)
207  {
208  throw;
209  }
210  catch (Exception e2)
211  {
212  // fuuuuucckkk
213  Logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
214  monitorState.InactiveServerCritFail = true;
215  await Chat.SendWatchdogMessage(
216  "Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits",
217  false,
218  cancellationToken).ConfigureAwait(false);
219  return;
220  }
221  }
222 
223  Logger.LogInformation("Successfully relaunched inactive server!");
224  monitorState.RebootingInactiveServer = true;
225  }
226 
227  static string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
228 
229  // reason handling
230  switch (activationReason)
231  {
232  case MonitorActivationReason.ActiveServerCrashed:
233  if (monitorState.ActiveServer.RebootState == Session.RebootState.Shutdown)
234  {
235  // the time for graceful shutdown is now
236  await Chat.SendWatchdogMessage(
237  String.Format(
238  CultureInfo.InvariantCulture,
239  "Active server {0}! Shutting down due to graceful termination request...",
240  ExitWord(monitorState.ActiveServer)),
241  false,
242  cancellationToken).ConfigureAwait(false);
243  monitorState.NextAction = MonitorAction.Exit;
244  break;
245  }
246 
247  if (FullRestartDeadInactive())
248  {
249  // tell chat about it and go ahead
250  await Chat.SendWatchdogMessage(
251  String.Format(
252  CultureInfo.InvariantCulture,
253  "Active server {0}! Inactive server unable to online!",
254  ExitWord(monitorState.ActiveServer)),
255  false,
256  cancellationToken).ConfigureAwait(false);
257 
258  // we've already been set to restart
259  break;
260  }
261 
262  // tell chat about it
263  await Chat.SendWatchdogMessage(
264  String.Format(
265  CultureInfo.InvariantCulture,
266  "Active server {0}! Onlining inactive server...",
267  ExitWord(monitorState.ActiveServer)),
268  false,
269  cancellationToken)
270  .ConfigureAwait(false);
271 
272  // try to activate the inactive server
273  if (!await MakeInactiveActive().ConfigureAwait(false))
274  break; // failing that, we've already been set to restart
275 
276  // bring up another inactive server
277  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
278  break;
279  case MonitorActivationReason.InactiveServerCrashed:
280  // just announce and try to bring it back
281  await Chat.SendWatchdogMessage(
282  String.Format(
283  CultureInfo.InvariantCulture,
284  "Inactive server {0}! Rebooting...",
285  ExitWord(monitorState.InactiveServer)),
286  false,
287  cancellationToken)
288  .ConfigureAwait(false);
289  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
290  break;
291  case MonitorActivationReason.ActiveServerRebooted:
292  // ideal goal: active server just closed its port
293  // tell inactive server to open it's port and that's now the active server
294  var rebootState = monitorState.ActiveServer.RebootState;
295  monitorState.ActiveServer.ResetRebootState(); // the DMAPI has already done this internally
296 
297  if (FullRestartDeadInactive() && rebootState != Session.RebootState.Shutdown)
298  break; // full restart if the inactive server is being fucky
299 
300  // what matters here is the RebootState
301  var restartOnceSwapped = false;
302 
303  switch (rebootState)
304  {
305  case Session.RebootState.Normal:
306  // life as normal
307  break;
308  case Session.RebootState.Restart:
309  // reboot the current active server once the inactive one activates
310  restartOnceSwapped = true;
311  break;
312  case Session.RebootState.Shutdown:
313  // graceful shutdown time
314  await Chat.SendWatchdogMessage(
315  "Active server rebooted! Shutting down due to graceful termination request...",
316  false,
317  cancellationToken)
318  .ConfigureAwait(false);
319  monitorState.NextAction = MonitorAction.Exit;
320  return;
321  default:
322  throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
323  }
324 
325  // are both servers now running the same CompileJob?
326  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
327 
328  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
329  restartOnceSwapped = true; // need a new launch to update either settings or compile job
330 
331  if (restartOnceSwapped)
332  /*
333  * we need to manually restart active server
334  * just kill it here, easier that way
335  */
336  monitorState.ActiveServer.Dispose();
337 
338  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
339 
340  if (activeServerStillHasPortOpen)
341  /* we didn't want active server to swap for some reason and it still has it's port open
342  * just continue as normal
343  */
344  break;
345 
346  if (!await MakeInactiveActive().ConfigureAwait(false))
347  break; // monitor will restart
348 
349  // servers now swapped
350  // enable this now if inactive server is not still valid
351  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
352 
353  if (!restartOnceSwapped)
354  /*
355  * now try to reopen it on the private port
356  * failing that, just reboot it
357  */
358  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
359 
360  // break either way because any issues past this point would be solved by the reboot
361  if (restartOnceSwapped) // for one reason or another
362  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); // update and reboot
363  else
364  monitorState.NextAction = MonitorAction.Skip; // only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
365  break;
366  case MonitorActivationReason.InactiveServerRebooted:
367  // just don't let the active server close it's port if the inactive server isn't ready
368  monitorState.RebootingInactiveServer = true;
369  monitorState.InactiveServer.ResetRebootState();
370  monitorState.ActiveServer.ClosePortOnReboot = false;
371  monitorState.NextAction = MonitorAction.Continue;
372  break;
373  case MonitorActivationReason.InactiveServerStartupComplete:
374  // opposite of above case
375  monitorState.RebootingInactiveServer = false;
376  monitorState.ActiveServer.ClosePortOnReboot = true;
377  monitorState.NextAction = MonitorAction.Continue;
378  break;
379  case MonitorActivationReason.NewDmbAvailable:
380  case MonitorActivationReason.ActiveLaunchParametersUpdated:
381  // just reload the inactive server and wait for a swap to apply the changes
382  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
383  break;
384  case MonitorActivationReason.Heartbeat:
385  default:
386  throw new InvalidOperationException(
387  String.Format(
388  CultureInfo.InvariantCulture,
389  "Invalid monitor activation reason: {0}!",
390  activationReason));
391  }
392  }
393 #pragma warning restore CA1502
394 
398  protected override void DisposeAndNullControllers()
399  {
400  alphaServer?.Dispose();
401  alphaServer = null;
402  bravoServer?.Dispose();
403  bravoServer = null;
404  Running = false;
405  }
406 
408  protected override IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState)
409  {
410  if (AlphaIsActive)
411  Logger.LogDebug("Alpha is the active server");
412  else
413  Logger.LogDebug("Bravo is the active server");
414 
415  if (monitorState.RebootingInactiveServer)
416  Logger.LogDebug("Inactive server is rebooting");
417 
418  // update the monitor state with the inactive/active servers
419  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
420  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
421 
422  if (monitorState.ActiveServer.ClosePortOnReboot)
423  Logger.LogDebug("Active server will close port on reboot");
424  if (monitorState.InactiveServer.ClosePortOnReboot)
425  Logger.LogDebug("Inactive server will close port on reboot");
426 
427  Logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
428  Logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
429 
430  return new Dictionary<MonitorActivationReason, Task>
431  {
432  { MonitorActivationReason.ActiveServerCrashed, monitorState.ActiveServer.Lifetime },
433  { MonitorActivationReason.ActiveServerRebooted, monitorState.ActiveServer.OnReboot },
434  { MonitorActivationReason.InactiveServerCrashed, monitorState.InactiveServer.Lifetime },
435  { MonitorActivationReason.InactiveServerRebooted, monitorState.InactiveServer.OnReboot },
436  { MonitorActivationReason.InactiveServerStartupComplete, monitorState.InactiveServer.OnPrime }
437  };
438  }
439 
441  #pragma warning disable CA1502 // TODO: Decomplexify
442  protected override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
443  {
444  Debug.Assert(alphaServer == null && bravoServer == null, "Entered LaunchNoLock with one or more of the servers not being null!");
445 
446  // don't need a new dmb if reattaching
447  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
448  var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(2);
449 
450  // if this try catches something, both servers are killed
451  try
452  {
453  // start the alpha server task, either by launch a new process or attaching to an existing one
454  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
455  // The tasks pertaining to server startup times are in the ISessionControllers
456  Task<ISessionController> alphaServerTask;
457  if (!doesntNeedNewDmb)
458  alphaServerTask = SessionControllerFactory.LaunchNew(
459  dmbToUse,
460  null,
461  ActiveLaunchParameters,
462  true,
463  true,
464  false,
465  cancellationToken);
466  else
467  alphaServerTask = SessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
468 
469  // retrieve the session controller
470  var startTime = DateTimeOffset.Now;
471  alphaServer = await alphaServerTask.ConfigureAwait(false);
472 
473  // failed reattaches will return null
474  alphaServer?.SetHighPriority();
475 
476  // extra delay for total ordering
477  var now = DateTimeOffset.Now;
478  var delay = now - startTime;
479 
480  // definitely not if reattaching though
481  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
482  await AsyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
483 
484  // now bring bravo up
485  if (!doesntNeedNewDmb)
486  bravoServer = await SessionControllerFactory.LaunchNew(
487  dmbToUse,
488  null,
489  ActiveLaunchParameters,
490  false,
491  false,
492  false,
493  cancellationToken)
494  .ConfigureAwait(false);
495  else
496  bravoServer = await SessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
497 
498  // failed reattaches will return null
499  bravoServer?.SetHighPriority();
500 
501  // possiblity of null servers due to failed reattaches
502  if (alphaServer == null || bravoServer == null)
503  {
504  await chatTask.ConfigureAwait(false);
505  var bothServersDead = alphaServer == null && bravoServer == null;
506  if (bothServersDead
507  || (alphaServer == null && reattachInfo.AlphaIsActive)
508  || (bravoServer == null && !reattachInfo.AlphaIsActive))
509  {
510  // we lost the active server, just restart entirely
511  DisposeAndNullControllers();
512  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
513  Logger.LogWarning(FailReattachMessage);
514  Logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
515  chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken);
516  callBeforeRecurse();
517  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
518  await chatTask.ConfigureAwait(false);
519  return;
520  }
521 
522  // we still have the active server but the other one is dead to us, hand it off to the monitor to restart
523  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
524  chatTask = Chat.SendWatchdogMessage(InactiveReattachFailureMessage, false, cancellationToken);
525  Logger.LogWarning(InactiveReattachFailureMessage);
526 
527  if (reattachInfo.AlphaIsActive)
528  bravoServer = SessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
529  else
530  alphaServer = SessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
531  }
532 
533  var alphaLrt = CheckLaunchResult(alphaServer, "Alpha", cancellationToken);
534  var bravoLrt = CheckLaunchResult(bravoServer, "Bravo", cancellationToken);
535 
536  // this task completes when both serers have finished booting
537  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
538 
539  await allTask.ConfigureAwait(false);
540 
541  // both servers are now running, alpha is the active server(unless reattach), huzzah
542  alphaIsActive = reattachInfo?.AlphaIsActive ?? true;
543 
544  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
545  activeServer.EnableCustomChatCommands();
546  activeServer.ClosePortOnReboot = true;
547  }
548  catch
549  {
550  if (dmbToUse != null)
551  {
552  // we locked 2 dmbs
553  if (bravoServer == null)
554  {
555  // bravo didn't get control of his
556  dmbToUse.Dispose();
557  if (alphaServer == null)
558  dmbToUse.Dispose(); // alpha didn't get control of his
559  }
560  }
561  else if (doesntNeedNewDmb) // we have reattachInfo
562  if (bravoServer == null)
563  {
564  // bravo didn't get control of his
565  reattachInfo.Bravo?.Dmb.Dispose();
566  if (alphaServer == null)
567  reattachInfo.Alpha?.Dmb.Dispose(); // alpha didn't get control of his
568  }
569 
570  // kill the controllers
571  DisposeAndNullControllers();
572  throw;
573  }
574  }
575  #pragma warning restore CA1502
576 
578  protected override ISessionController GetActiveController() => AlphaIsActive ? alphaServer : bravoServer;
579 
581  protected override DualReattachInformation CreateReattachInformation()
583  {
584  AlphaIsActive = AlphaIsActive,
585  Alpha = alphaServer?.Release(),
586  Bravo = bravoServer?.Release()
587  };
588  }
589 }
bool InactiveServerCritFail
If the inactive server is in an unrecoverable state
Definition: MonitorState.cs:19
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.Globalization;
6 using System.Threading;
7 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Components.Watchdog
17 {
22  {
26  const int AlphaBravoStartupSeperationInterval = 10; // TODO: Make this configurable
27 
29  public override bool AlphaIsActive => alphaIsActive;
30 
32  public override Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
33 
35  public override RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
36 
41 
46 
51 
68  IChatManager chat,
69  ISessionControllerFactory sessionControllerFactory,
70  IDmbFactory dmbFactory,
71  IReattachInfoHandler reattachInfoHandler,
72  IDatabaseContextFactory databaseContextFactory,
73  IJobManager jobManager,
74  IServerControl serverControl,
75  IAsyncDelayer asyncDelayer,
76  ILogger<ExperimentalWatchdog> logger,
77  DreamDaemonLaunchParameters initialLaunchParameters,
78  Api.Models.Instance instance, bool autoStart)
79  : base(
80  chat,
81  sessionControllerFactory,
82  dmbFactory,
83  reattachInfoHandler,
84  databaseContextFactory,
85  jobManager,
86  serverControl,
87  asyncDelayer,
88  logger,
89  initialLaunchParameters,
90  instance,
91  autoStart)
92  {
93  alphaIsActive = true;
94  }
95 
97  #pragma warning disable CA1502 // TODO: Decomplexify
98  protected override async Task HandleMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
99  {
100  Logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
101 
102  // this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
103  // I'll do my best to walk you through it
104 
105  // returns true if the inactive server can't be used immediately
106  // also sets monitor to restart if the above holds
107  bool FullRestartDeadInactive()
108  {
109  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
110  {
111  Logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
112  monitorState.NextAction = MonitorAction.Restart; // will dispose server
113  return true;
114  }
115 
116  return false;
117  }
118 
119  // trys to set inactive server's port to the public game port
120  // doesn't handle closing active server's port
121  // returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
122  // on failure, sets monitor to restart
123  async Task<bool> MakeInactiveActive()
124  {
125  Logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
126  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
127 
128  if (!result)
129  {
130  Logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
131  monitorState.NextAction = MonitorAction.Restart; // will dispose server
132  return false;
133  }
134 
135  // inactive server should always be using active launch parameters
136  LastLaunchParameters = ActiveLaunchParameters;
137 
138  var tmp = monitorState.ActiveServer;
139  monitorState.ActiveServer = monitorState.InactiveServer;
140  monitorState.InactiveServer = tmp;
141  alphaIsActive = !AlphaIsActive;
142  monitorState.ActiveServer.EnableCustomChatCommands();
143  return true;
144  }
145 
146  // Kills and tries to launch inactive server with the latest dmb
147  // falls back to current dmb on failure
148  // Sets critfail on inactive server failing that
149  // returns false if the backup dmb was used successfully, true otherwise
150  async Task UpdateAndRestartInactiveServer(bool breakAfter)
151  {
152  ActiveParametersUpdated = new TaskCompletionSource<object>();
153  monitorState.InactiveServer.Dispose(); // kill or recycle it
154  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
155  monitorState.NextAction = desiredNextAction;
156 
157  Logger.LogInformation("Rebooting inactive server...");
158  var newDmb = DmbFactory.LockNextDmb(1);
159  try
160  {
162  newDmb,
163  null,
164  ActiveLaunchParameters,
165  false,
166  !monitorState.ActiveServer.IsPrimary,
167  false,
168  cancellationToken)
169  .ConfigureAwait(false);
170  monitorState.InactiveServer.SetHighPriority();
171  }
172  catch (OperationCanceledException)
173  {
174  throw;
175  }
176  catch (Exception e)
177  {
178  Logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
179 
180  // ahh jeez, what do we do here?
181  // this is our fault, so it should never happen but
182  // idk maybe a database error while handling the newest dmb?
183  // either way try to start it using the active server's dmb as a backup
184  try
185  {
186  var dmbBackup = await DmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
187 
188  if (dmbBackup == null) // NANI!?
189  throw new InvalidOperationException("Watchdog double crit-fail!"); // just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
190 
192  dmbBackup,
193  null,
194  ActiveLaunchParameters,
195  false,
196  !monitorState.ActiveServer.IsPrimary,
197  false,
198  cancellationToken)
199  .ConfigureAwait(false);
200  monitorState.InactiveServer.SetHighPriority();
201  await Chat.SendWatchdogMessage(
202  "Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...",
203  false,
204  cancellationToken).ConfigureAwait(false);
205  }
206  catch (OperationCanceledException)
207  {
208  throw;
209  }
210  catch (Exception e2)
211  {
212  // fuuuuucckkk
213  Logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
214  monitorState.InactiveServerCritFail = true;
215  await Chat.SendWatchdogMessage(
216  "Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits",
217  false,
218  cancellationToken).ConfigureAwait(false);
219  return;
220  }
221  }
222 
223  Logger.LogInformation("Successfully relaunched inactive server!");
224  monitorState.RebootingInactiveServer = true;
225  }
226 
227  static string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
228 
229  // reason handling
230  switch (activationReason)
231  {
232  case MonitorActivationReason.ActiveServerCrashed:
233  if (monitorState.ActiveServer.RebootState == Session.RebootState.Shutdown)
234  {
235  // the time for graceful shutdown is now
236  await Chat.SendWatchdogMessage(
237  String.Format(
238  CultureInfo.InvariantCulture,
239  "Active server {0}! Shutting down due to graceful termination request...",
240  ExitWord(monitorState.ActiveServer)),
241  false,
242  cancellationToken).ConfigureAwait(false);
243  monitorState.NextAction = MonitorAction.Exit;
244  break;
245  }
246 
247  if (FullRestartDeadInactive())
248  {
249  // tell chat about it and go ahead
250  await Chat.SendWatchdogMessage(
251  String.Format(
252  CultureInfo.InvariantCulture,
253  "Active server {0}! Inactive server unable to online!",
254  ExitWord(monitorState.ActiveServer)),
255  false,
256  cancellationToken).ConfigureAwait(false);
257 
258  // we've already been set to restart
259  break;
260  }
261 
262  // tell chat about it
263  await Chat.SendWatchdogMessage(
264  String.Format(
265  CultureInfo.InvariantCulture,
266  "Active server {0}! Onlining inactive server...",
267  ExitWord(monitorState.ActiveServer)),
268  false,
269  cancellationToken)
270  .ConfigureAwait(false);
271 
272  // try to activate the inactive server
273  if (!await MakeInactiveActive().ConfigureAwait(false))
274  break; // failing that, we've already been set to restart
275 
276  // bring up another inactive server
277  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
278  break;
279  case MonitorActivationReason.InactiveServerCrashed:
280  // just announce and try to bring it back
281  await Chat.SendWatchdogMessage(
282  String.Format(
283  CultureInfo.InvariantCulture,
284  "Inactive server {0}! Rebooting...",
285  ExitWord(monitorState.InactiveServer)),
286  false,
287  cancellationToken)
288  .ConfigureAwait(false);
289  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
290  break;
291  case MonitorActivationReason.ActiveServerRebooted:
292  // ideal goal: active server just closed its port
293  // tell inactive server to open it's port and that's now the active server
294  var rebootState = monitorState.ActiveServer.RebootState;
295  monitorState.ActiveServer.ResetRebootState(); // the DMAPI has already done this internally
296 
297  if (FullRestartDeadInactive() && rebootState != Session.RebootState.Shutdown)
298  break; // full restart if the inactive server is being fucky
299 
300  // what matters here is the RebootState
301  var restartOnceSwapped = false;
302 
303  switch (rebootState)
304  {
305  case Session.RebootState.Normal:
306  // life as normal
307  break;
308  case Session.RebootState.Restart:
309  // reboot the current active server once the inactive one activates
310  restartOnceSwapped = true;
311  break;
312  case Session.RebootState.Shutdown:
313  // graceful shutdown time
314  await Chat.SendWatchdogMessage(
315  "Active server rebooted! Shutting down due to graceful termination request...",
316  false,
317  cancellationToken)
318  .ConfigureAwait(false);
319  monitorState.NextAction = MonitorAction.Exit;
320  return;
321  default:
322  throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
323  }
324 
325  // are both servers now running the same CompileJob?
326  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
327 
328  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
329  restartOnceSwapped = true; // need a new launch to update either settings or compile job
330 
331  if (restartOnceSwapped)
332  /*
333  * we need to manually restart active server
334  * just kill it here, easier that way
335  */
336  monitorState.ActiveServer.Dispose();
337 
338  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
339 
340  if (activeServerStillHasPortOpen)
341  /* we didn't want active server to swap for some reason and it still has it's port open
342  * just continue as normal
343  */
344  break;
345 
346  if (!await MakeInactiveActive().ConfigureAwait(false))
347  break; // monitor will restart
348 
349  // servers now swapped
350  // enable this now if inactive server is not still valid
351  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
352 
353  if (!restartOnceSwapped)
354  /*
355  * now try to reopen it on the private port
356  * failing that, just reboot it
357  */
358  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
359 
360  // break either way because any issues past this point would be solved by the reboot
361  if (restartOnceSwapped) // for one reason or another
362  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); // update and reboot
363  else
364  monitorState.NextAction = MonitorAction.Skip; // only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
365  break;
366  case MonitorActivationReason.InactiveServerRebooted:
367  // just don't let the active server close it's port if the inactive server isn't ready
368  monitorState.RebootingInactiveServer = true;
369  monitorState.InactiveServer.ResetRebootState();
370  monitorState.ActiveServer.ClosePortOnReboot = false;
371  monitorState.NextAction = MonitorAction.Continue;
372  break;
373  case MonitorActivationReason.InactiveServerStartupComplete:
374  // opposite of above case
375  monitorState.RebootingInactiveServer = false;
376  monitorState.ActiveServer.ClosePortOnReboot = true;
377  monitorState.NextAction = MonitorAction.Continue;
378  break;
379  case MonitorActivationReason.NewDmbAvailable:
380  case MonitorActivationReason.ActiveLaunchParametersUpdated:
381  // just reload the inactive server and wait for a swap to apply the changes
382  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
383  break;
384  case MonitorActivationReason.Heartbeat:
385  default:
386  throw new InvalidOperationException(
387  String.Format(
388  CultureInfo.InvariantCulture,
389  "Invalid monitor activation reason: {0}!",
390  activationReason));
391  }
392  }
393 #pragma warning restore CA1502
394 
396  protected override void DisposeAndNullControllersImpl()
397  {
398  alphaServer?.Dispose();
399  alphaServer = null;
400  bravoServer?.Dispose();
401  bravoServer = null;
402  Running = false;
403  }
404 
406  protected override IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState)
407  {
408  if (AlphaIsActive)
409  Logger.LogDebug("Alpha is the active server");
410  else
411  Logger.LogDebug("Bravo is the active server");
412 
413  if (monitorState.RebootingInactiveServer)
414  Logger.LogDebug("Inactive server is rebooting");
415 
416  // update the monitor state with the inactive/active servers
417  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
418  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
419 
420  if (monitorState.ActiveServer.ClosePortOnReboot)
421  Logger.LogDebug("Active server will close port on reboot");
422  if (monitorState.InactiveServer.ClosePortOnReboot)
423  Logger.LogDebug("Inactive server will close port on reboot");
424 
425  Logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
426  Logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
427 
428  return new Dictionary<MonitorActivationReason, Task>
429  {
430  { MonitorActivationReason.ActiveServerCrashed, monitorState.ActiveServer.Lifetime },
431  { MonitorActivationReason.ActiveServerRebooted, monitorState.ActiveServer.OnReboot },
432  { MonitorActivationReason.InactiveServerCrashed, monitorState.InactiveServer.Lifetime },
433  { MonitorActivationReason.InactiveServerRebooted, monitorState.InactiveServer.OnReboot },
434  { MonitorActivationReason.InactiveServerStartupComplete, monitorState.InactiveServer.OnPrime }
435  };
436  }
437 
439  #pragma warning disable CA1502 // TODO: Decomplexify
440  protected override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
441  {
442  Debug.Assert(alphaServer == null && bravoServer == null, "Entered LaunchNoLock with one or more of the servers not being null!");
443 
444  // don't need a new dmb if reattaching
445  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
446  var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(2);
447 
448  // if this try catches something, both servers are killed
449  try
450  {
451  // start the alpha server task, either by launch a new process or attaching to an existing one
452  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
453  // The tasks pertaining to server startup times are in the ISessionControllers
454  Task<ISessionController> alphaServerTask;
455  if (!doesntNeedNewDmb)
456  alphaServerTask = SessionControllerFactory.LaunchNew(
457  dmbToUse,
458  null,
459  ActiveLaunchParameters,
460  true,
461  true,
462  false,
463  cancellationToken);
464  else
465  alphaServerTask = SessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
466 
467  // retrieve the session controller
468  var startTime = DateTimeOffset.Now;
469  alphaServer = await alphaServerTask.ConfigureAwait(false);
470 
471  // failed reattaches will return null
472  alphaServer?.SetHighPriority();
473 
474  // extra delay for total ordering
475  var now = DateTimeOffset.Now;
476  var delay = now - startTime;
477 
478  // definitely not if reattaching though
479  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
480  await AsyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
481 
482  // now bring bravo up
483  if (!doesntNeedNewDmb)
484  bravoServer = await SessionControllerFactory.LaunchNew(
485  dmbToUse,
486  null,
487  ActiveLaunchParameters,
488  false,
489  false,
490  false,
491  cancellationToken)
492  .ConfigureAwait(false);
493  else
494  bravoServer = await SessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
495 
496  // failed reattaches will return null
497  bravoServer?.SetHighPriority();
498 
499  // possiblity of null servers due to failed reattaches
500  if (alphaServer == null || bravoServer == null)
501  {
502  await chatTask.ConfigureAwait(false);
503  var bothServersDead = alphaServer == null && bravoServer == null;
504  if (bothServersDead
505  || (alphaServer == null && reattachInfo.AlphaIsActive)
506  || (bravoServer == null && !reattachInfo.AlphaIsActive))
507  {
508  // we lost the active server, just restart entirely
509  DisposeAndNullControllers();
510  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
511  Logger.LogWarning(FailReattachMessage);
512  Logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
513  chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken);
514  callBeforeRecurse();
515  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
516  await chatTask.ConfigureAwait(false);
517  return;
518  }
519 
520  // we still have the active server but the other one is dead to us, hand it off to the monitor to restart
521  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
522  chatTask = Chat.SendWatchdogMessage(InactiveReattachFailureMessage, false, cancellationToken);
523  Logger.LogWarning(InactiveReattachFailureMessage);
524 
525  if (reattachInfo.AlphaIsActive)
526  bravoServer = SessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
527  else
528  alphaServer = SessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
529  }
530 
531  var alphaLrt = CheckLaunchResult(alphaServer, "Alpha", cancellationToken);
532  var bravoLrt = CheckLaunchResult(bravoServer, "Bravo", cancellationToken);
533 
534  // this task completes when both serers have finished booting
535  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
536 
537  await allTask.ConfigureAwait(false);
538 
539  // both servers are now running, alpha is the active server(unless reattach), huzzah
540  alphaIsActive = reattachInfo?.AlphaIsActive ?? true;
541 
542  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
543  activeServer.EnableCustomChatCommands();
544  activeServer.ClosePortOnReboot = true;
545  }
546  catch
547  {
548  if (dmbToUse != null)
549  {
550  // we locked 2 dmbs
551  if (bravoServer == null)
552  {
553  // bravo didn't get control of his
554  dmbToUse.Dispose();
555  if (alphaServer == null)
556  dmbToUse.Dispose(); // alpha didn't get control of his
557  }
558  }
559  else if (doesntNeedNewDmb) // we have reattachInfo
560  if (bravoServer == null)
561  {
562  // bravo didn't get control of his
563  reattachInfo.Bravo?.Dmb.Dispose();
564  if (alphaServer == null)
565  reattachInfo.Alpha?.Dmb.Dispose(); // alpha didn't get control of his
566  }
567 
568  // kill the controllers
569  DisposeAndNullControllers();
570  throw;
571  }
572  }
573  #pragma warning restore CA1502
574 
576  protected override ISessionController GetActiveController() => AlphaIsActive ? alphaServer : bravoServer;
577 
579  protected override DualReattachInformation CreateReattachInformation()
581  {
582  AlphaIsActive = AlphaIsActive,
583  Alpha = alphaServer?.Release(),
584  Bravo = bravoServer?.Release()
585  };
586  }
587 }
bool InactiveServerCritFail
If the inactive server is in an unrecoverable state
Definition: MonitorState.cs:19
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
@@ -110,7 +110,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ReattachInformation Alpha
ReattachInformation for the Alpha session
-
override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
Starts all ISessionControllers.
+
override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
Starts all ISessionControllers.
+
override void DisposeAndNullControllersImpl()
Call IDisposable.Dispose and null the fields for all ISessionControllers and set Running to ...
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
Task< bool > SetPort(ushort newPort, CancellationToken cancellatonToken)
Causes the world to start listening on a newPort
@@ -137,7 +138,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
MonitorActivationReason
Reasons for the monitor to wake up
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon
-
override IReadOnlyDictionary< MonitorActivationReason, Task > GetMonitoredServerTasks(MonitorState monitorState)
Gets the tasks for the following MonitorActivationReasons: MonitorActivationReason.ActiveServerCrashed, MonitorActivationReason.ActiveServerRebooted, MonitorActivationReason.InactiveServerCrashed, MonitorActivationReason.InactiveServerRebooted, MonitorActivationReason.InactiveServerStartupComplete.
+
override IReadOnlyDictionary< MonitorActivationReason, Task > GetMonitoredServerTasks(MonitorState monitorState)
Gets the tasks for the following MonitorActivationReasons: MonitorActivationReason.ActiveServerCrashed, MonitorActivationReason.ActiveServerRebooted, MonitorActivationReason.InactiveServerCrashed, MonitorActivationReason.InactiveServerRebooted, MonitorActivationReason.InactiveServerStartupComplete.
ISessionController ActiveServer
The active ISessionController
Definition: MonitorState.cs:30
@@ -163,10 +164,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
bool ClosePortOnReboot
If the port should be rotated off when the world reboots
- +
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
-
override void DisposeAndNullControllers()
Call IDisposable.Dispose on alphaServer and bravoServer and set them to
diff --git a/_file_logging_configuration_8cs.html b/_file_logging_configuration_8cs.html index 294d6fe58f..7b653d4f1d 100644 --- a/_file_logging_configuration_8cs.html +++ b/_file_logging_configuration_8cs.html @@ -25,7 +25,7 @@ diff --git a/_file_logging_configuration_8cs_source.html b/_file_logging_configuration_8cs_source.html index 4142096750..4595bf9f63 100644 --- a/_file_logging_configuration_8cs_source.html +++ b/_file_logging_configuration_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_general_configuration_8cs.html b/_general_configuration_8cs.html index 4234b5e7ed..3399592805 100644 --- a/_general_configuration_8cs.html +++ b/_general_configuration_8cs.html @@ -25,7 +25,7 @@ diff --git a/_general_configuration_8cs_source.html b/_general_configuration_8cs_source.html index b30a5b4cd4..4055e5e773 100644 --- a/_general_configuration_8cs_source.html +++ b/_general_configuration_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,13 +94,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
GeneralConfiguration.cs
-Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Newtonsoft.Json.Converters;
4 
5 namespace Tgstation.Server.Host.Configuration
6 {
11  {
15  public const string Section = "General";
16 
20  const uint DefaultMinimumPasswordLength = 15;
21 
25  const uint DefaultInstanceLimit = 10;
26 
30  const uint DefaultUserLimit = 100;
31 
35  const int DefaultByondTopicTimeout = 5000;
36 
40  const int DefaultRestartTimeout = 10000;
41 
45  public string GitHubAccessToken { get; set; }
46 
50  [JsonConverter(typeof(StringEnumConverter))]
51  public SetupWizardMode SetupWizardMode { get; set; }
52 
56  public int ByondTopicTimeout { get; set; } = DefaultByondTopicTimeout;
57 
61  public int RestartTimeout { get; set; } = DefaultRestartTimeout;
62 
66  public bool UseExperimentalWatchdog { get; set; }
67 
71  public bool UseBasicWatchdogOnWindows { get; set; }
72 
77  {
78  MinimumPasswordLength = DefaultMinimumPasswordLength;
79  InstanceLimit = DefaultInstanceLimit;
80  UserLimit = DefaultUserLimit;
81  }
82  }
83 }
Base class for Models.ServerInformation.
+Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Newtonsoft.Json.Converters;
4 
5 namespace Tgstation.Server.Host.Configuration
6 {
11  {
15  public const string Section = "General";
16 
20  public const ushort DefaultApiPort = 5000;
21 
25  const uint DefaultMinimumPasswordLength = 15;
26 
30  const uint DefaultInstanceLimit = 10;
31 
35  const uint DefaultUserLimit = 100;
36 
40  const int DefaultByondTopicTimeout = 5000;
41 
45  const int DefaultRestartTimeout = 60000;
46 
50  public ushort ApiPort { get; set; }
51 
55  public string GitHubAccessToken { get; set; }
56 
60  [JsonConverter(typeof(StringEnumConverter))]
61  public SetupWizardMode SetupWizardMode { get; set; }
62 
66  public int ByondTopicTimeout { get; set; } = DefaultByondTopicTimeout;
67 
71  public int RestartTimeout { get; set; } = DefaultRestartTimeout;
72 
76  public bool UseExperimentalWatchdog { get; set; }
77 
81  public bool UseBasicWatchdogOnWindows { get; set; }
82 
87  {
88  MinimumPasswordLength = DefaultMinimumPasswordLength;
89  InstanceLimit = DefaultInstanceLimit;
90  UserLimit = DefaultUserLimit;
91  }
92  }
93 }
Base class for Models.ServerInformation.
SetupWizardMode
Determines if the Setup.SetupWizard will run
-
GeneralConfiguration()
Initializes a new instance of the GeneralConfiguration .
+
GeneralConfiguration()
Initializes a new instance of the GeneralConfiguration .
diff --git a/_git_hub_client_factory_8cs.html b/_git_hub_client_factory_8cs.html index 548ce39b8c..59c9e3c4bf 100644 --- a/_git_hub_client_factory_8cs.html +++ b/_git_hub_client_factory_8cs.html @@ -25,7 +25,7 @@
diff --git a/_git_hub_client_factory_8cs_source.html b/_git_hub_client_factory_8cs_source.html index 137bae4388..d1cc58040f 100644 --- a/_git_hub_client_factory_8cs_source.html +++ b/_git_hub_client_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_global_suppressions_8cs.html b/_global_suppressions_8cs.html index c321cf3b54..13efa1eaa5 100644 --- a/_global_suppressions_8cs.html +++ b/_global_suppressions_8cs.html @@ -25,7 +25,7 @@ diff --git a/_global_suppressions_8cs_source.html b/_global_suppressions_8cs_source.html index f6542b6633..f6a93e6bc3 100644 --- a/_global_suppressions_8cs_source.html +++ b/_global_suppressions_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_home_controller_8cs.html b/_home_controller_8cs.html index d0884e9700..1d44a41d36 100644 --- a/_home_controller_8cs.html +++ b/_home_controller_8cs.html @@ -25,7 +25,7 @@ diff --git a/_home_controller_8cs_source.html b/_home_controller_8cs_source.html index 422451b4e3..f2ae495b90 100644 --- a/_home_controller_8cs_source.html +++ b/_home_controller_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
HomeController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authorization;
2 using Microsoft.AspNetCore.Http;
3 using Microsoft.AspNetCore.Mvc;
4 using Microsoft.EntityFrameworkCore;
5 using Microsoft.Extensions.Logging;
6 using Microsoft.Extensions.Options;
7 using Microsoft.Extensions.Primitives;
8 using Microsoft.Net.Http.Headers;
9 using System;
10 using System.Linq;
11 using System.Net.Mime;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
22 using Wangkanai.Detection;
23 
24 namespace Tgstation.Server.Host.Controllers
25 {
29  [Route(Routes.Root)]
30  public sealed class HomeController : ApiController
31  {
36 
41 
46 
51 
56 
60  readonly IBrowserResolver browserResolver;
61 
66 
71 
87  IDatabaseContext databaseContext,
88  IAuthenticationContextFactory authenticationContextFactory,
89  ITokenFactory tokenFactory,
90  ISystemIdentityFactory systemIdentityFactory,
91  ICryptographySuite cryptographySuite,
92  IAssemblyInformationProvider assemblyInformationProvider,
93  IIdentityCache identityCache,
94  IBrowserResolver browserResolver,
95  IOptions<GeneralConfiguration> generalConfigurationOptions,
96  IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
97  ILogger<HomeController> logger)
98  : base(databaseContext, authenticationContextFactory, logger, false, false)
99  {
100  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
101  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
102  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
103  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
104  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
105  this.browserResolver = browserResolver ?? throw new ArgumentNullException(nameof(browserResolver));
106  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
107  controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
108  }
109 
117  [HttpGet]
118  [TgsAuthorize]
119  [AllowAnonymous]
120  [ProducesResponseType(typeof(Api.Models.ServerInformation), 200)]
121  public IActionResult Home()
122  {
123  if (AuthenticationContext != null)
124  return Json(new Api.Models.ServerInformation
125  {
126  Version = assemblyInformationProvider.Version,
127  ApiVersion = ApiHeaders.Version,
128  DMApiVersion = DMApiConstants.Version,
129  MinimumPasswordLength = generalConfiguration.MinimumPasswordLength,
130  InstanceLimit = generalConfiguration.InstanceLimit,
131  UserLimit = generalConfiguration.UserLimit,
132  ValidInstancePaths = generalConfiguration.ValidInstancePaths
133  });
134 
135  // if we are using a browser and the control panel, soft redirect to the app page
136  if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic)
137  {
138  Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), loading control panel...", browserResolver.UserAgent);
139  return File("~/index.html", MediaTypeNames.Text.Html);
140  }
141 
142  return Unauthorized();
143  }
144 
153  [HttpPost]
154  [ProducesResponseType(typeof(Api.Models.Token), 200)]
155  [ProducesResponseType(401)]
156  [ProducesResponseType(403)]
157  #pragma warning disable CA1506 // TODO: Decomplexify
158  public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
159  {
160  if (ApiHeaders == null)
161  {
162  // Get the exact error
163  var errorMessage = "Missing API headers!";
164  try
165  {
166  var _ = new ApiHeaders(Request.GetTypedHeaders());
167  }
168  catch (InvalidOperationException ex)
169  {
170  errorMessage = ex.Message;
171  }
172 
173  Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\""));
174 
175  return BadRequest(
176  new Api.Models.ErrorMessage(Api.Models.ErrorCode.BadHeaders)
177  {
178  AdditionalData = errorMessage
179  });
180  }
181 
183  return BadRequest(new Api.Models.ErrorMessage(Api.Models.ErrorCode.TokenWithToken));
184 
185  ISystemIdentity systemIdentity;
186  try
187  {
188  // trust the system over the database because a user's name can change while still having the same SID
189  systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
190  }
191  catch (NotImplementedException)
192  {
193  systemIdentity = null;
194  }
195 
196  using (systemIdentity)
197  {
198  // Get the user from the database
199  IQueryable<User> query;
200  string canonicalName = Models.User.CanonicalizeName(ApiHeaders.Username);
201  if (systemIdentity == null)
202  query = DatabaseContext.Users.Where(x => x.CanonicalName == canonicalName);
203  else
204  query = DatabaseContext.Users.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid);
205  var users = await query.Select(x => new User
206  {
207  Id = x.Id,
208  PasswordHash = x.PasswordHash,
209  Enabled = x.Enabled,
210  Name = x.Name
211  }).ToListAsync(cancellationToken).ConfigureAwait(false);
212 
213  // Pick the DB user first
214  var user = users
215  .OrderByDescending(dbUser => dbUser.PasswordHash != null)
216  .FirstOrDefault();
217 
218  // No user? You're not allowed
219  if (user == null)
220  return Unauthorized();
221 
222  // A system user may have had their name AND password changed to one in our DB...
223  // Or a DB user was created that had the same user/pass as a system user
224  // Dumb admins...
225  // FALLBACK TO THE DB USER HERE, DO NOT REVEAL A SYSTEM LOGIN!!!
226  // This of course, allows system users to discover TGS users in this (HIGHLY IMPROBABLE) case but that is not our fault
227  var originalHash = user.PasswordHash;
228  var isDbUser = originalHash != null;
229  bool usingSystemIdentity = systemIdentity != null && !isDbUser;
230  if (!usingSystemIdentity)
231  {
232  // DB User password check and update
233  if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
234  return Unauthorized();
235  if (user.PasswordHash != originalHash)
236  {
237  Logger.LogDebug("User ID {0}'s password hash needs a refresh, updating database.", user.Id);
238  var updatedUser = new User
239  {
240  Id = user.Id
241  };
242  DatabaseContext.Users.Attach(updatedUser);
243  updatedUser.PasswordHash = user.PasswordHash;
244  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
245  }
246  }
247  else if (systemIdentity.Username != user.Name)
248  {
249  // System identity username change update
250  Logger.LogDebug("User ID {0}'s system identity needs a refresh, updating database.", user.Id);
251  DatabaseContext.Users.Attach(user);
252  user.Name = systemIdentity.Username;
253  user.CanonicalName = Models.User.CanonicalizeName(user.Name);
254  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
255  }
256 
257  // Now that the bookeeping is done, tell them to fuck off if necessary
258  if (!user.Enabled.Value)
259  {
260  Logger.LogTrace("Not logging in disabled user {0}.", user.Id);
261  return Forbid();
262  }
263 
264  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
265  if (usingSystemIdentity)
266  {
267  // expire the identity slightly after the auth token in case of lag
268  var identExpiry = token.ExpiresAt;
269  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
270  identExpiry += TimeSpan.FromSeconds(15);
271  identityCache.CacheSystemIdentity(user, systemIdentity, identExpiry);
272  }
273 
274  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
275 
276  return Json(token);
277  }
278  }
279  #pragma warning restore CA1506
280  }
281 }
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authorization;
2 using Microsoft.AspNetCore.Http;
3 using Microsoft.AspNetCore.Mvc;
4 using Microsoft.EntityFrameworkCore;
5 using Microsoft.Extensions.Logging;
6 using Microsoft.Extensions.Options;
7 using Microsoft.Extensions.Primitives;
8 using Microsoft.Net.Http.Headers;
9 using System;
10 using System.Linq;
11 using System.Net.Mime;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
22 using Wangkanai.Detection;
23 
24 namespace Tgstation.Server.Host.Controllers
25 {
29  [Route(Routes.Root)]
30  public sealed class HomeController : ApiController
31  {
36 
41 
46 
51 
56 
60  readonly IBrowserResolver browserResolver;
61 
66 
71 
87  IDatabaseContext databaseContext,
88  IAuthenticationContextFactory authenticationContextFactory,
89  ITokenFactory tokenFactory,
90  ISystemIdentityFactory systemIdentityFactory,
91  ICryptographySuite cryptographySuite,
92  IAssemblyInformationProvider assemblyInformationProvider,
93  IIdentityCache identityCache,
94  IBrowserResolver browserResolver,
95  IOptions<GeneralConfiguration> generalConfigurationOptions,
96  IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
97  ILogger<HomeController> logger)
98  : base(databaseContext, authenticationContextFactory, logger, false, false)
99  {
100  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
101  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
102  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
103  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
104  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
105  this.browserResolver = browserResolver ?? throw new ArgumentNullException(nameof(browserResolver));
106  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
107  controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
108  }
109 
117  [HttpGet]
118  [TgsAuthorize]
119  [AllowAnonymous]
120  [ProducesResponseType(typeof(Api.Models.ServerInformation), 200)]
121  public IActionResult Home()
122  {
123  if (AuthenticationContext != null)
124  return Json(new Api.Models.ServerInformation
125  {
126  Version = assemblyInformationProvider.Version,
127  ApiVersion = ApiHeaders.Version,
128  DMApiVersion = DMApiConstants.Version,
129  MinimumPasswordLength = generalConfiguration.MinimumPasswordLength,
130  InstanceLimit = generalConfiguration.InstanceLimit,
131  UserLimit = generalConfiguration.UserLimit,
132  ValidInstancePaths = generalConfiguration.ValidInstancePaths
133  });
134 
135  // if we are using a browser and the control panel, soft redirect to the app page
136  if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic)
137  {
138  Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), loading control panel...", browserResolver.UserAgent);
139  return File("~/index.html", MediaTypeNames.Text.Html);
140  }
141 
142  return Unauthorized();
143  }
144 
153  [HttpPost]
154  [ProducesResponseType(typeof(Api.Models.Token), 200)]
155  [ProducesResponseType(401)]
156  [ProducesResponseType(403)]
157  #pragma warning disable CA1506 // TODO: Decomplexify
158  public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
159  {
160  if (ApiHeaders == null)
161  {
162  // Get the exact error
163  var errorMessage = "Missing API headers!";
164  try
165  {
166  var _ = new ApiHeaders(Request.GetTypedHeaders());
167  }
168  catch (InvalidOperationException ex)
169  {
170  errorMessage = ex.Message;
171  }
172 
173  Response.Headers.Add(HeaderNames.WWWAuthenticate, new StringValues("basic realm=\"Create TGS4 bearer token\""));
174 
175  return BadRequest(
176  new Api.Models.ErrorMessage(Api.Models.ErrorCode.BadHeaders)
177  {
178  AdditionalData = errorMessage
179  });
180  }
181 
183  return BadRequest(new Api.Models.ErrorMessage(Api.Models.ErrorCode.TokenWithToken));
184 
185  ISystemIdentity systemIdentity;
186  try
187  {
188  // trust the system over the database because a user's name can change while still having the same SID
189  systemIdentity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
190  }
191  catch (NotImplementedException)
192  {
193  systemIdentity = null;
194  }
195 
196  using (systemIdentity)
197  {
198  // Get the user from the database
199  IQueryable<User> query = DatabaseContext.Users.AsQueryable();
200  string canonicalName = Models.User.CanonicalizeName(ApiHeaders.Username);
201  if (systemIdentity == null)
202  query = query.Where(x => x.CanonicalName == canonicalName);
203  else
204  query = query.Where(x => x.CanonicalName == canonicalName || x.SystemIdentifier == systemIdentity.Uid);
205  var users = await query.Select(x => new User
206  {
207  Id = x.Id,
208  PasswordHash = x.PasswordHash,
209  Enabled = x.Enabled,
210  Name = x.Name
211  }).ToListAsync(cancellationToken).ConfigureAwait(false);
212 
213  // Pick the DB user first
214  var user = users
215  .OrderByDescending(dbUser => dbUser.PasswordHash != null)
216  .FirstOrDefault();
217 
218  // No user? You're not allowed
219  if (user == null)
220  return Unauthorized();
221 
222  // A system user may have had their name AND password changed to one in our DB...
223  // Or a DB user was created that had the same user/pass as a system user
224  // Dumb admins...
225  // FALLBACK TO THE DB USER HERE, DO NOT REVEAL A SYSTEM LOGIN!!!
226  // This of course, allows system users to discover TGS users in this (HIGHLY IMPROBABLE) case but that is not our fault
227  var originalHash = user.PasswordHash;
228  var isDbUser = originalHash != null;
229  bool usingSystemIdentity = systemIdentity != null && !isDbUser;
230  if (!usingSystemIdentity)
231  {
232  // DB User password check and update
233  if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
234  return Unauthorized();
235  if (user.PasswordHash != originalHash)
236  {
237  Logger.LogDebug("User ID {0}'s password hash needs a refresh, updating database.", user.Id);
238  var updatedUser = new User
239  {
240  Id = user.Id
241  };
242  DatabaseContext.Users.Attach(updatedUser);
243  updatedUser.PasswordHash = user.PasswordHash;
244  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
245  }
246  }
247  else if (systemIdentity.Username != user.Name)
248  {
249  // System identity username change update
250  Logger.LogDebug("User ID {0}'s system identity needs a refresh, updating database.", user.Id);
251  DatabaseContext.Users.Attach(user);
252  user.Name = systemIdentity.Username;
253  user.CanonicalName = Models.User.CanonicalizeName(user.Name);
254  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
255  }
256 
257  // Now that the bookeeping is done, tell them to fuck off if necessary
258  if (!user.Enabled.Value)
259  {
260  Logger.LogTrace("Not logging in disabled user {0}.", user.Id);
261  return Forbid();
262  }
263 
264  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
265  if (usingSystemIdentity)
266  {
267  // expire the identity slightly after the auth token in case of lag
268  var identExpiry = token.ExpiresAt;
269  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
270  identExpiry += TimeSpan.FromSeconds(15);
271  identityCache.CacheSystemIdentity(user, systemIdentity, identExpiry);
272  }
273 
274  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
275 
276  return Json(token);
277  }
278  }
279  #pragma warning restore CA1506
280  }
281 }
DbSet< User > Users
The Users in the DatabaseContext.
async Task< IActionResult > CreateToken(CancellationToken cancellationToken)
Attempt to authenticate a User using ApiController.ApiHeaders
@@ -107,13 +107,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the HomeController.
IActionResult Home()
Main page of the Application
string Username
The client&#39;s username
Definition: ApiHeaders.cs:92
+
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
Contains various cryptographic functions
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the HomeController
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:17
readonly ControlPanelConfiguration controlPanelConfiguration
The ControlPanelConfiguration for the HomeController
-
DbSet< User > Users
The Users in the DatabaseContext<TParentContext>.
string Password
The client&#39;s password
Definition: ApiHeaders.cs:97
@@ -141,7 +141,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the HomeController
readonly ITokenFactory tokenFactory
The ITokenFactory for the HomeController
- +
diff --git a/_host_builder_extensions_8cs.html b/_host_builder_extensions_8cs.html index 0452cd034b..67dcb6939b 100644 --- a/_host_builder_extensions_8cs.html +++ b/_host_builder_extensions_8cs.html @@ -25,7 +25,7 @@
diff --git a/_host_builder_extensions_8cs_source.html b/_host_builder_extensions_8cs_source.html index 58a77f4995..8f015b2c6f 100644 --- a/_host_builder_extensions_8cs_source.html +++ b/_host_builder_extensions_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_http_client_8cs.html b/_http_client_8cs.html index 40f11c3a37..d6cf06aaa0 100644 --- a/_http_client_8cs.html +++ b/_http_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_http_client_8cs_source.html b/_http_client_8cs_source.html index 87c64753a1..78a2093e39 100644 --- a/_http_client_8cs_source.html +++ b/_http_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_administration_client_8cs.html b/_i_administration_client_8cs.html index d6035a74ba..d2933c9890 100644 --- a/_i_administration_client_8cs.html +++ b/_i_administration_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_administration_client_8cs_source.html b/_i_administration_client_8cs_source.html index 8bb7af14e3..d00a24d34b 100644 --- a/_i_administration_client_8cs_source.html +++ b/_i_administration_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_api_client_8cs.html b/_i_api_client_8cs.html index e68922a64c..2777b0d6f8 100644 --- a/_i_api_client_8cs.html +++ b/_i_api_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_api_client_8cs_source.html b/_i_api_client_8cs_source.html index cfa66ede10..fc0c55a045 100644 --- a/_i_api_client_8cs_source.html +++ b/_i_api_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_api_client_factory_8cs.html b/_i_api_client_factory_8cs.html index 2a10d135c7..1dedfb957a 100644 --- a/_i_api_client_factory_8cs.html +++ b/_i_api_client_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_api_client_factory_8cs_source.html b/_i_api_client_factory_8cs_source.html index 76299d69d3..157fa0c25e 100644 --- a/_i_api_client_factory_8cs_source.html +++ b/_i_api_client_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_assembly_information_provider_8cs.html b/_i_assembly_information_provider_8cs.html index 859909479e..cd5bf09048 100644 --- a/_i_assembly_information_provider_8cs.html +++ b/_i_assembly_information_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_assembly_information_provider_8cs_source.html b/_i_assembly_information_provider_8cs_source.html index 31d75f7cf1..85c2c6bec9 100644 --- a/_i_assembly_information_provider_8cs_source.html +++ b/_i_assembly_information_provider_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_async_delayer_8cs.html b/_i_async_delayer_8cs.html index 4b88e5c4fc..1f2a8d60ba 100644 --- a/_i_async_delayer_8cs.html +++ b/_i_async_delayer_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_async_delayer_8cs_source.html b/_i_async_delayer_8cs_source.html index 68ee2328d3..0df61ed454 100644 --- a/_i_async_delayer_8cs_source.html +++ b/_i_async_delayer_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_authentication_context_8cs.html b/_i_authentication_context_8cs.html index bd2f8f2070..07578433bb 100644 --- a/_i_authentication_context_8cs.html +++ b/_i_authentication_context_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_authentication_context_8cs_source.html b/_i_authentication_context_8cs_source.html index 635b00df6f..fa81e2889e 100644 --- a/_i_authentication_context_8cs_source.html +++ b/_i_authentication_context_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_authentication_context_factory_8cs.html b/_i_authentication_context_factory_8cs.html index 8d220c10db..26c95e8bb7 100644 --- a/_i_authentication_context_factory_8cs.html +++ b/_i_authentication_context_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_authentication_context_factory_8cs_source.html b/_i_authentication_context_factory_8cs_source.html index 3fb6e42763..1f46ad17c1 100644 --- a/_i_authentication_context_factory_8cs_source.html +++ b/_i_authentication_context_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_bridge_dispatcher_8cs.html b/_i_bridge_dispatcher_8cs.html index 2e7a9fe5f6..308a4254e8 100644 --- a/_i_bridge_dispatcher_8cs.html +++ b/_i_bridge_dispatcher_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_bridge_dispatcher_8cs_source.html b/_i_bridge_dispatcher_8cs_source.html index 65a0b8092a..f7c09044e4 100644 --- a/_i_bridge_dispatcher_8cs_source.html +++ b/_i_bridge_dispatcher_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_bridge_handler_8cs.html b/_i_bridge_handler_8cs.html index c4c73c4b2d..8dad942516 100644 --- a/_i_bridge_handler_8cs.html +++ b/_i_bridge_handler_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_bridge_handler_8cs_source.html b/_i_bridge_handler_8cs_source.html index c0ea20e85a..52ad819393 100644 --- a/_i_bridge_handler_8cs_source.html +++ b/_i_bridge_handler_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_bridge_registrar_8cs.html b/_i_bridge_registrar_8cs.html index 542affd729..740e612c2f 100644 --- a/_i_bridge_registrar_8cs.html +++ b/_i_bridge_registrar_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_bridge_registrar_8cs_source.html b/_i_bridge_registrar_8cs_source.html index 2d03013bf2..b5709e72b1 100644 --- a/_i_bridge_registrar_8cs_source.html +++ b/_i_bridge_registrar_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_bridge_registration_8cs.html b/_i_bridge_registration_8cs.html index f33c34f0ac..5cf99d9c04 100644 --- a/_i_bridge_registration_8cs.html +++ b/_i_bridge_registration_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_bridge_registration_8cs_source.html b/_i_bridge_registration_8cs_source.html index 31fcace44a..cd20b6e1a3 100644 --- a/_i_bridge_registration_8cs_source.html +++ b/_i_bridge_registration_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_byond_client_8cs.html b/_i_byond_client_8cs.html index a4f2fcbab9..794f3b5de1 100644 --- a/_i_byond_client_8cs.html +++ b/_i_byond_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_byond_client_8cs_source.html b/_i_byond_client_8cs_source.html index 917e571fd6..fad0a5e396 100644 --- a/_i_byond_client_8cs_source.html +++ b/_i_byond_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_byond_executable_lock_8cs.html b/_i_byond_executable_lock_8cs.html index 47892e3d08..a318968b44 100644 --- a/_i_byond_executable_lock_8cs.html +++ b/_i_byond_executable_lock_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_byond_executable_lock_8cs_source.html b/_i_byond_executable_lock_8cs_source.html index 6ab82d25d8..a902f8bd98 100644 --- a/_i_byond_executable_lock_8cs_source.html +++ b/_i_byond_executable_lock_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_byond_installer_8cs.html b/_i_byond_installer_8cs.html index 8525021fed..b1e5bc438e 100644 --- a/_i_byond_installer_8cs.html +++ b/_i_byond_installer_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_byond_installer_8cs_source.html b/_i_byond_installer_8cs_source.html index 57ca712697..bce0242a77 100644 --- a/_i_byond_installer_8cs_source.html +++ b/_i_byond_installer_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_byond_manager_8cs.html b/_i_byond_manager_8cs.html index 04dcb4a047..6e9a45d905 100644 --- a/_i_byond_manager_8cs.html +++ b/_i_byond_manager_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_byond_manager_8cs_source.html b/_i_byond_manager_8cs_source.html index 56f47c2fba..e4c0d55dc9 100644 --- a/_i_byond_manager_8cs_source.html +++ b/_i_byond_manager_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_channel_sink_8cs.html b/_i_channel_sink_8cs.html index 813b5464f8..df1035d6dc 100644 --- a/_i_channel_sink_8cs.html +++ b/_i_channel_sink_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_channel_sink_8cs_source.html b/_i_channel_sink_8cs_source.html index e793d9861a..baf0ba57f3 100644 --- a/_i_channel_sink_8cs_source.html +++ b/_i_channel_sink_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_chat_bots_client_8cs.html b/_i_chat_bots_client_8cs.html index 5cb97f09bc..6a67152f18 100644 --- a/_i_chat_bots_client_8cs.html +++ b/_i_chat_bots_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_chat_bots_client_8cs_source.html b/_i_chat_bots_client_8cs_source.html index f130232d70..03c7a129b1 100644 --- a/_i_chat_bots_client_8cs_source.html +++ b/_i_chat_bots_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_chat_manager_8cs.html b/_i_chat_manager_8cs.html index dd7279d0aa..11a35bc6c5 100644 --- a/_i_chat_manager_8cs.html +++ b/_i_chat_manager_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_chat_manager_8cs_source.html b/_i_chat_manager_8cs_source.html index 5ee3c5a106..f3ad998e43 100644 --- a/_i_chat_manager_8cs_source.html +++ b/_i_chat_manager_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IChatManager.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Collections.Generic;
4 using System.Threading;
5 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Components.Chat
9 {
14  {
19  void RegisterCommandHandler(ICustomCommandHandler customCommandHandler);
20 
27  Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken);
28 
35  Task DeleteConnection(long connectionId, CancellationToken cancellationToken);
36 
44  Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken);
45 
53  Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken);
54 
62  Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken);
63 
70  Task SendUpdateMessage(string message, CancellationToken cancellationToken);
71 
76  IChatTrackingContext CreateTrackingContext();
77  }
78 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Collections.Generic;
4 using System.Threading;
5 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Components.Chat
9 {
14  {
19  void RegisterCommandHandler(ICustomCommandHandler customCommandHandler);
20 
27  Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken);
28 
35  Task DeleteConnection(long connectionId, CancellationToken cancellationToken);
36 
44  Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken);
45 
53  Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken);
54 
62  Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken);
63 
75  Task<Func<string, string, Task>> SendDeploymentMessage(
76  Models.RevisionInformation revisionInformation,
77  Version byondVersion,
78  DateTimeOffset? estimatedCompletionTime,
79  string gitHubOwner,
80  string gitHubRepo,
81  bool localCommitPushed,
82  CancellationToken cancellationToken);
83 
88  IChatTrackingContext CreateTrackingContext();
89  }
90 }
Manage the server chat bots
Definition: ChatBot.cs:9
diff --git a/_i_chat_manager_factory_8cs.html b/_i_chat_manager_factory_8cs.html index 71caa0ce48..9c21878cee 100644 --- a/_i_chat_manager_factory_8cs.html +++ b/_i_chat_manager_factory_8cs.html @@ -25,7 +25,7 @@
diff --git a/_i_chat_manager_factory_8cs_source.html b/_i_chat_manager_factory_8cs_source.html index a7ad731f1a..66c2ef8a0d 100644 --- a/_i_chat_manager_factory_8cs_source.html +++ b/_i_chat_manager_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_chat_tracking_context_8cs.html b/_i_chat_tracking_context_8cs.html index b73fef3fee..fdf8744e9c 100644 --- a/_i_chat_tracking_context_8cs.html +++ b/_i_chat_tracking_context_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_chat_tracking_context_8cs_source.html b/_i_chat_tracking_context_8cs_source.html index 2f39e5d2ae..b2ca1a3314 100644 --- a/_i_chat_tracking_context_8cs_source.html +++ b/_i_chat_tracking_context_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_claims_injector_8cs.html b/_i_claims_injector_8cs.html index 2b034b4870..d2ca864f2e 100644 --- a/_i_claims_injector_8cs.html +++ b/_i_claims_injector_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_claims_injector_8cs_source.html b/_i_claims_injector_8cs_source.html index de391ec61b..a6daea51b3 100644 --- a/_i_claims_injector_8cs_source.html +++ b/_i_claims_injector_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_command_8cs.html b/_i_command_8cs.html index a655f5b732..e365722c1a 100644 --- a/_i_command_8cs.html +++ b/_i_command_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_command_8cs_source.html b/_i_command_8cs_source.html index 30ac6aa338..abe6f26e89 100644 --- a/_i_command_8cs_source.html +++ b/_i_command_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_command_factory_8cs.html b/_i_command_factory_8cs.html index 0f60b4b1b5..3e7be0662b 100644 --- a/_i_command_factory_8cs.html +++ b/_i_command_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_command_factory_8cs_source.html b/_i_command_factory_8cs_source.html index 9aae529470..ead14ef4bd 100644 --- a/_i_command_factory_8cs_source.html +++ b/_i_command_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_compile_job_sink_8cs.html b/_i_compile_job_sink_8cs.html index 23a8460a1d..de838503cc 100644 --- a/_i_compile_job_sink_8cs.html +++ b/_i_compile_job_sink_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_compile_job_sink_8cs_source.html b/_i_compile_job_sink_8cs_source.html index c2d8079b90..1897ecaf91 100644 --- a/_i_compile_job_sink_8cs_source.html +++ b/_i_compile_job_sink_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_configuration_8cs.html b/_i_configuration_8cs.html index b1ac7bdbca..80cb721dd8 100644 --- a/_i_configuration_8cs.html +++ b/_i_configuration_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_configuration_8cs_source.html b/_i_configuration_8cs_source.html index 3fd13dbb70..80075c5c9f 100644 --- a/_i_configuration_8cs_source.html +++ b/_i_configuration_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_configuration_client_8cs.html b/_i_configuration_client_8cs.html index 4f423af34d..0f9c4caecd 100644 --- a/_i_configuration_client_8cs.html +++ b/_i_configuration_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_configuration_client_8cs_source.html b/_i_configuration_client_8cs_source.html index b9f2605eef..bffb464c1e 100644 --- a/_i_configuration_client_8cs_source.html +++ b/_i_configuration_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_console_8cs.html b/_i_console_8cs.html index 1f1a6ef2e5..0461532b4a 100644 --- a/_i_console_8cs.html +++ b/_i_console_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_console_8cs_source.html b/_i_console_8cs_source.html index 922a326c63..f1582c71bd 100644 --- a/_i_console_8cs_source.html +++ b/_i_console_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_credentials_provider_8cs.html b/_i_credentials_provider_8cs.html index 1f9b8245c8..40161f823f 100644 --- a/_i_credentials_provider_8cs.html +++ b/_i_credentials_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_credentials_provider_8cs_source.html b/_i_credentials_provider_8cs_source.html index 58a8c3fe7d..bc9c406f84 100644 --- a/_i_credentials_provider_8cs_source.html +++ b/_i_credentials_provider_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_cryptography_suite_8cs.html b/_i_cryptography_suite_8cs.html index ca50b101f1..8db5dc9160 100644 --- a/_i_cryptography_suite_8cs.html +++ b/_i_cryptography_suite_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_cryptography_suite_8cs_source.html b/_i_cryptography_suite_8cs_source.html index 9567c39706..b0e3110c3c 100644 --- a/_i_cryptography_suite_8cs_source.html +++ b/_i_cryptography_suite_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_custom_command_handler_8cs.html b/_i_custom_command_handler_8cs.html index 6c7eadfd54..57f396803c 100644 --- a/_i_custom_command_handler_8cs.html +++ b/_i_custom_command_handler_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_custom_command_handler_8cs_source.html b/_i_custom_command_handler_8cs_source.html index 5d971301df..48ff9987f6 100644 --- a/_i_custom_command_handler_8cs_source.html +++ b/_i_custom_command_handler_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_database_collection_8cs.html b/_i_database_collection_8cs.html index ac79b2427b..53bb5be750 100644 --- a/_i_database_collection_8cs.html +++ b/_i_database_collection_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_database_collection_8cs_source.html b/_i_database_collection_8cs_source.html index 402b6ea7ff..96f03f02aa 100644 --- a/_i_database_collection_8cs_source.html +++ b/_i_database_collection_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,10 +94,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IDatabaseCollection.cs
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Database
8 {
13  public interface IDatabaseCollection<TModel> : IQueryable<TModel>
14  {
18  IEnumerable<TModel> Local { get; }
19 
24  void Add(TModel model);
25 
30  void Remove(TModel model);
31 
36  void Attach(TModel model);
37 
42  void AddRange(IEnumerable<TModel> models);
43 
48  void RemoveRange(IEnumerable<TModel> models);
49 
56  Task ForEachAsync(Action<TModel> action, CancellationToken cancellationToken);
57 
63  Task<List<TModel>> ToListAsync(CancellationToken cancellationToken);
64  }
65 }
+Go to the documentation of this file.
1 using System.Collections.Generic;
2 using System.Linq;
3 
4 namespace Tgstation.Server.Host.Database
5 {
10  public interface IDatabaseCollection<TModel> : IQueryable<TModel>, IAsyncEnumerable<TModel>
11  {
15  IEnumerable<TModel> Local { get; }
16 
21  void Add(TModel model);
22 
27  void Remove(TModel model);
28 
33  void Attach(TModel model);
34 
39  void AddRange(IEnumerable<TModel> models);
40 
45  void RemoveRange(IEnumerable<TModel> models);
46  }
47 }
- + +
diff --git a/_i_database_connection_factory_8cs.html b/_i_database_connection_factory_8cs.html index 53b83eb306..0d57c9c3ec 100644 --- a/_i_database_connection_factory_8cs.html +++ b/_i_database_connection_factory_8cs.html @@ -25,7 +25,7 @@
diff --git a/_i_database_connection_factory_8cs_source.html b/_i_database_connection_factory_8cs_source.html index c8a8ab1071..98f42ebfba 100644 --- a/_i_database_connection_factory_8cs_source.html +++ b/_i_database_connection_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_database_context_8cs.html b/_i_database_context_8cs.html index 6352406d70..1508241ef7 100644 --- a/_i_database_context_8cs.html +++ b/_i_database_context_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_database_context_8cs_source.html b/_i_database_context_8cs_source.html index 09cac99911..b1b89ea091 100644 --- a/_i_database_context_8cs_source.html +++ b/_i_database_context_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_database_context_factory_8cs.html b/_i_database_context_factory_8cs.html index 45d2470ff9..3e8268836e 100644 --- a/_i_database_context_factory_8cs.html +++ b/_i_database_context_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_database_context_factory_8cs_source.html b/_i_database_context_factory_8cs_source.html index e2816ecb20..7340571ad1 100644 --- a/_i_database_context_factory_8cs_source.html +++ b/_i_database_context_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_database_seeder_8cs.html b/_i_database_seeder_8cs.html index 3967eaeb23..404436a4a8 100644 --- a/_i_database_seeder_8cs.html +++ b/_i_database_seeder_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_database_seeder_8cs_source.html b/_i_database_seeder_8cs_source.html index b5525e7852..3eda961ad0 100644 --- a/_i_database_seeder_8cs_source.html +++ b/_i_database_seeder_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_dmb_factory_8cs.html b/_i_dmb_factory_8cs.html index 636fd7dcd9..03c43ff20f 100644 --- a/_i_dmb_factory_8cs.html +++ b/_i_dmb_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_dmb_factory_8cs_source.html b/_i_dmb_factory_8cs_source.html index 557a0c2d47..d667c1051a 100644 --- a/_i_dmb_factory_8cs_source.html +++ b/_i_dmb_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_dmb_provider_8cs.html b/_i_dmb_provider_8cs.html index 020d1b2cd8..06ded9245a 100644 --- a/_i_dmb_provider_8cs.html +++ b/_i_dmb_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_dmb_provider_8cs_source.html b/_i_dmb_provider_8cs_source.html index 14c41edcf6..0193c25f59 100644 --- a/_i_dmb_provider_8cs_source.html +++ b/_i_dmb_provider_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_dream_daemon_client_8cs.html b/_i_dream_daemon_client_8cs.html index fce6734062..7c05edc211 100644 --- a/_i_dream_daemon_client_8cs.html +++ b/_i_dream_daemon_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_dream_daemon_client_8cs_source.html b/_i_dream_daemon_client_8cs_source.html index a765468a09..c6c2cfc364 100644 --- a/_i_dream_daemon_client_8cs_source.html +++ b/_i_dream_daemon_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_dream_maker_8cs.html b/_i_dream_maker_8cs.html index 4ac7edf994..8b2fed1067 100644 --- a/_i_dream_maker_8cs.html +++ b/_i_dream_maker_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_dream_maker_8cs_source.html b/_i_dream_maker_8cs_source.html index f518558db9..7b6c10dcc9 100644 --- a/_i_dream_maker_8cs_source.html +++ b/_i_dream_maker_8cs_source.html @@ -25,7 +25,7 @@ @@ -102,7 +102,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - + diff --git a/_i_dream_maker_client_8cs.html b/_i_dream_maker_client_8cs.html index 533ab943db..9eee0a3c8c 100644 --- a/_i_dream_maker_client_8cs.html +++ b/_i_dream_maker_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_dream_maker_client_8cs_source.html b/_i_dream_maker_client_8cs_source.html index 6fb977257c..936fa603af 100644 --- a/_i_dream_maker_client_8cs_source.html +++ b/_i_dream_maker_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_event_consumer_8cs.html b/_i_event_consumer_8cs.html index 0ccbf265d2..1dd8db9a44 100644 --- a/_i_event_consumer_8cs.html +++ b/_i_event_consumer_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_event_consumer_8cs_source.html b/_i_event_consumer_8cs_source.html index 2236c6d391..315a551b6c 100644 --- a/_i_event_consumer_8cs_source.html +++ b/_i_event_consumer_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_git_hub_client_factory_8cs.html b/_i_git_hub_client_factory_8cs.html index 8ede9fdd7c..c2aed3994b 100644 --- a/_i_git_hub_client_factory_8cs.html +++ b/_i_git_hub_client_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_git_hub_client_factory_8cs_source.html b/_i_git_hub_client_factory_8cs_source.html index cff7a2c3db..c792e99789 100644 --- a/_i_git_hub_client_factory_8cs_source.html +++ b/_i_git_hub_client_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_http_client_8cs.html b/_i_http_client_8cs.html index 58d0c15955..8e6a31d355 100644 --- a/_i_http_client_8cs.html +++ b/_i_http_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_http_client_8cs_source.html b/_i_http_client_8cs_source.html index a0dba3ec90..db3958d405 100644 --- a/_i_http_client_8cs_source.html +++ b/_i_http_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_i_o_manager_8cs.html b/_i_i_o_manager_8cs.html index cdb181d8cd..4c53491169 100644 --- a/_i_i_o_manager_8cs.html +++ b/_i_i_o_manager_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_i_o_manager_8cs_source.html b/_i_i_o_manager_8cs_source.html index 4cbf8d26a0..c5a5399b95 100644 --- a/_i_i_o_manager_8cs_source.html +++ b/_i_i_o_manager_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_identity_cache_8cs.html b/_i_identity_cache_8cs.html index 518ba0fddb..6155f2c2af 100644 --- a/_i_identity_cache_8cs.html +++ b/_i_identity_cache_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_identity_cache_8cs_source.html b/_i_identity_cache_8cs_source.html index e249576c93..efa94126fd 100644 --- a/_i_identity_cache_8cs_source.html +++ b/_i_identity_cache_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_8cs.html b/_i_instance_8cs.html index a68dac4e09..f022cb785f 100644 --- a/_i_instance_8cs.html +++ b/_i_instance_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_8cs_source.html b/_i_instance_8cs_source.html index 875be4b47b..a9ff465237 100644 --- a/_i_instance_8cs_source.html +++ b/_i_instance_8cs_source.html @@ -25,7 +25,7 @@ @@ -101,7 +101,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
For managing connected chat services
Definition: IChatManager.cs:13
diff --git a/_i_instance_client_8cs.html b/_i_instance_client_8cs.html index 7dc0c81a15..6ce9aba3f4 100644 --- a/_i_instance_client_8cs.html +++ b/_i_instance_client_8cs.html @@ -25,7 +25,7 @@
diff --git a/_i_instance_client_8cs_source.html b/_i_instance_client_8cs_source.html index 7b3aebdea4..769cec7c10 100644 --- a/_i_instance_client_8cs_source.html +++ b/_i_instance_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_factory_8cs.html b/_i_instance_factory_8cs.html index 24dc3d5a72..fb92eda818 100644 --- a/_i_instance_factory_8cs.html +++ b/_i_instance_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_factory_8cs_source.html b/_i_instance_factory_8cs_source.html index 70412ef597..23069424bc 100644 --- a/_i_instance_factory_8cs_source.html +++ b/_i_instance_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_manager_8cs.html b/_i_instance_manager_8cs.html index 7bfa1b3abb..9d29567545 100644 --- a/_i_instance_manager_8cs.html +++ b/_i_instance_manager_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_manager_8cs_source.html b/_i_instance_manager_8cs_source.html index 7781dd0d67..8490584f3b 100644 --- a/_i_instance_manager_8cs_source.html +++ b/_i_instance_manager_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_manager_client_8cs.html b/_i_instance_manager_client_8cs.html index d20fbe0cd2..ffef4ae82f 100644 --- a/_i_instance_manager_client_8cs.html +++ b/_i_instance_manager_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_manager_client_8cs_source.html b/_i_instance_manager_client_8cs_source.html index 59cb2cf187..393d6ebb07 100644 --- a/_i_instance_manager_client_8cs_source.html +++ b/_i_instance_manager_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_user_client_8cs.html b/_i_instance_user_client_8cs.html index 8feb7702a1..c098445bca 100644 --- a/_i_instance_user_client_8cs.html +++ b/_i_instance_user_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_instance_user_client_8cs_source.html b/_i_instance_user_client_8cs_source.html index 36f101aa71..b32c644f78 100644 --- a/_i_instance_user_client_8cs_source.html +++ b/_i_instance_user_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_job_manager_8cs.html b/_i_job_manager_8cs.html index 809417d50a..6284d142c3 100644 --- a/_i_job_manager_8cs.html +++ b/_i_job_manager_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_job_manager_8cs_source.html b/_i_job_manager_8cs_source.html index 81f9d01e3f..107c3b2426 100644 --- a/_i_job_manager_8cs_source.html +++ b/_i_job_manager_8cs_source.html @@ -25,7 +25,7 @@ @@ -104,7 +104,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - + diff --git a/_i_jobs_client_8cs.html b/_i_jobs_client_8cs.html index 2ca1e3b930..3a651f4d24 100644 --- a/_i_jobs_client_8cs.html +++ b/_i_jobs_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_jobs_client_8cs_source.html b/_i_jobs_client_8cs_source.html index 5d5b863748..8da49a0c8a 100644 --- a/_i_jobs_client_8cs_source.html +++ b/_i_jobs_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_latest_compile_job_provider_8cs.html b/_i_latest_compile_job_provider_8cs.html index 2ed8fab23b..e14124bfe9 100644 --- a/_i_latest_compile_job_provider_8cs.html +++ b/_i_latest_compile_job_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_latest_compile_job_provider_8cs_source.html b/_i_latest_compile_job_provider_8cs_source.html index 0a462364d6..29bcdb2d2f 100644 --- a/_i_latest_compile_job_provider_8cs_source.html +++ b/_i_latest_compile_job_provider_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_lib_git2_commands_8cs.html b/_i_lib_git2_commands_8cs.html index a3c92e77ce..6d24de2ea0 100644 --- a/_i_lib_git2_commands_8cs.html +++ b/_i_lib_git2_commands_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_lib_git2_commands_8cs_source.html b/_i_lib_git2_commands_8cs_source.html index b524a81fd2..eb215156f6 100644 --- a/_i_lib_git2_commands_8cs_source.html +++ b/_i_lib_git2_commands_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_lib_git2_repository_factory_8cs.html b/_i_lib_git2_repository_factory_8cs.html index 55433005b5..1dcd0904b0 100644 --- a/_i_lib_git2_repository_factory_8cs.html +++ b/_i_lib_git2_repository_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_lib_git2_repository_factory_8cs_source.html b/_i_lib_git2_repository_factory_8cs_source.html index 2b689fd19d..25bd173fd7 100644 --- a/_i_lib_git2_repository_factory_8cs_source.html +++ b/_i_lib_git2_repository_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_network_prompt_reaper_8cs.html b/_i_network_prompt_reaper_8cs.html index c2147dfc3e..6aedf5d1b9 100644 --- a/_i_network_prompt_reaper_8cs.html +++ b/_i_network_prompt_reaper_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_network_prompt_reaper_8cs_source.html b/_i_network_prompt_reaper_8cs_source.html index 464cafdafe..ebe769d988 100644 --- a/_i_network_prompt_reaper_8cs_source.html +++ b/_i_network_prompt_reaper_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_platform_identifier_8cs.html b/_i_platform_identifier_8cs.html index a3954c882e..1f3080867f 100644 --- a/_i_platform_identifier_8cs.html +++ b/_i_platform_identifier_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_platform_identifier_8cs_source.html b/_i_platform_identifier_8cs_source.html index 59a8b25157..0a9ce4cbeb 100644 --- a/_i_platform_identifier_8cs_source.html +++ b/_i_platform_identifier_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_post_setup_services_8cs.html b/_i_post_setup_services_8cs.html index f38fc00cc5..ebd3c58a3e 100644 --- a/_i_post_setup_services_8cs.html +++ b/_i_post_setup_services_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_post_setup_services_8cs_source.html b/_i_post_setup_services_8cs_source.html index e1cf335b92..b2ace34af2 100644 --- a/_i_post_setup_services_8cs_source.html +++ b/_i_post_setup_services_8cs_source.html @@ -25,7 +25,7 @@ @@ -99,7 +99,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Set of objects needed to configure an Core.Application.
-
Configuration options for the Database.DatabaseContext<TParentContext>
+
Configuration options for the Database.DatabaseContext
diff --git a/_i_post_write_handler_8cs.html b/_i_post_write_handler_8cs.html index 5fd1c19eee..1492396542 100644 --- a/_i_post_write_handler_8cs.html +++ b/_i_post_write_handler_8cs.html @@ -25,7 +25,7 @@
diff --git a/_i_post_write_handler_8cs_source.html b/_i_post_write_handler_8cs_source.html index af3ec8d0b3..4b0ccc1e59 100644 --- a/_i_post_write_handler_8cs_source.html +++ b/_i_post_write_handler_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_process_8cs.html b/_i_process_8cs.html index 4db3564028..c6213c0e28 100644 --- a/_i_process_8cs.html +++ b/_i_process_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_process_8cs_source.html b/_i_process_8cs_source.html index a321dc8c39..3898580a30 100644 --- a/_i_process_8cs_source.html +++ b/_i_process_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_process_base_8cs.html b/_i_process_base_8cs.html index 855f605bdf..8e2b6bb1c6 100644 --- a/_i_process_base_8cs.html +++ b/_i_process_base_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_process_base_8cs_source.html b/_i_process_base_8cs_source.html index 2ea8e69f30..30c897ccd8 100644 --- a/_i_process_base_8cs_source.html +++ b/_i_process_base_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_process_executor_8cs.html b/_i_process_executor_8cs.html index 550f0a60b9..308f0674db 100644 --- a/_i_process_executor_8cs.html +++ b/_i_process_executor_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_process_executor_8cs_source.html b/_i_process_executor_8cs_source.html index 480fe655f7..3392883c3a 100644 --- a/_i_process_executor_8cs_source.html +++ b/_i_process_executor_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_process_features_8cs.html b/_i_process_features_8cs.html index b0b5c831d8..eb08ddb45c 100644 --- a/_i_process_features_8cs.html +++ b/_i_process_features_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_process_features_8cs_source.html b/_i_process_features_8cs_source.html index 4568838327..f3eac01872 100644 --- a/_i_process_features_8cs_source.html +++ b/_i_process_features_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_provider_8cs.html b/_i_provider_8cs.html index cd9ef07ace..23cf838274 100644 --- a/_i_provider_8cs.html +++ b/_i_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_provider_8cs_source.html b/_i_provider_8cs_source.html index 01f3d86d14..2dfb3d10db 100644 --- a/_i_provider_8cs_source.html +++ b/_i_provider_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,10 +94,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IProvider.cs
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Threading;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Components.Chat.Providers
7 {
11  interface IProvider : IDisposable
12  {
16  bool Connected { get; }
17 
21  string BotMention { get; }
22 
29  Task<Message> NextMessage(CancellationToken cancellationToken);
30 
36  Task<bool> Connect(CancellationToken cancellationToken);
37 
43  Task Disconnect(CancellationToken cancellationToken);
44 
51  Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken);
52 
60  Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
61 
67  Task SetReconnectInterval(uint reconnectInterval);
68  }
69 }
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Threading;
4 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Components.Chat.Providers
8 {
12  interface IProvider : IDisposable
13  {
17  bool Connected { get; }
18 
22  string BotMention { get; }
23 
30  Task<Message> NextMessage(CancellationToken cancellationToken);
31 
37  Task<bool> Connect(CancellationToken cancellationToken);
38 
44  Task Disconnect(CancellationToken cancellationToken);
45 
52  Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken);
53 
61  Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
62 
68  Task SetReconnectInterval(uint reconnectInterval);
69 
82  Task<Func<string, string, Task>> SendUpdateMessage(
83  RevisionInformation revisionInformation,
84  Version byondVersion,
85  DateTimeOffset? estimatedCompletionTime,
86  string gitHubOwner,
87  string gitHubRepo,
88  ulong channelId,
89  bool localCommitPushed,
90  CancellationToken cancellationToken);
91  }
92 }
+ -
For interacting with a chat service
Definition: IProvider.cs:11
+
For interacting with a chat service
Definition: IProvider.cs:12
+ + +
diff --git a/_i_provider_factory_8cs.html b/_i_provider_factory_8cs.html index 54d8a0673e..dca7d6657c 100644 --- a/_i_provider_factory_8cs.html +++ b/_i_provider_factory_8cs.html @@ -25,7 +25,7 @@
diff --git a/_i_provider_factory_8cs_source.html b/_i_provider_factory_8cs_source.html index e31d2d1fc5..d53929b56d 100644 --- a/_i_provider_factory_8cs_source.html +++ b/_i_provider_factory_8cs_source.html @@ -25,7 +25,7 @@ @@ -96,7 +96,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Go to the documentation of this file.
2 
3 namespace Tgstation.Server.Host.Components.Chat.Providers
4 {
8  interface IProviderFactory
9  {
15  IProvider CreateProvider(ChatBot settings);
16  }
17 }
-
For interacting with a chat service
Definition: IProvider.cs:11
+
For interacting with a chat service
Definition: IProvider.cs:12
Manage the server chat bots
Definition: ChatBot.cs:9
diff --git a/_i_reattach_info_handler_8cs.html b/_i_reattach_info_handler_8cs.html index fe778efc52..43eaa5b019 100644 --- a/_i_reattach_info_handler_8cs.html +++ b/_i_reattach_info_handler_8cs.html @@ -25,7 +25,7 @@
diff --git a/_i_reattach_info_handler_8cs_source.html b/_i_reattach_info_handler_8cs_source.html index 1ac8804fb2..2918100226 100644 --- a/_i_reattach_info_handler_8cs_source.html +++ b/_i_reattach_info_handler_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_repository_8cs.html b/_i_repository_8cs.html index e238a8bf89..59466dfdcc 100644 --- a/_i_repository_8cs.html +++ b/_i_repository_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_repository_8cs_source.html b/_i_repository_8cs_source.html index a549dc4217..4751eceab8 100644 --- a/_i_repository_8cs_source.html +++ b/_i_repository_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_repository_client_8cs.html b/_i_repository_client_8cs.html index eff485edee..5498150b75 100644 --- a/_i_repository_client_8cs.html +++ b/_i_repository_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_repository_client_8cs_source.html b/_i_repository_client_8cs_source.html index f0082a8db8..451cc7e88b 100644 --- a/_i_repository_client_8cs_source.html +++ b/_i_repository_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_repository_manager_8cs.html b/_i_repository_manager_8cs.html index 7f95ca0fcd..1fab2ef5a9 100644 --- a/_i_repository_manager_8cs.html +++ b/_i_repository_manager_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_repository_manager_8cs_source.html b/_i_repository_manager_8cs_source.html index 2c704890ca..4ad31a58fd 100644 --- a/_i_repository_manager_8cs_source.html +++ b/_i_repository_manager_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_request_logger_8cs.html b/_i_request_logger_8cs.html index b02c32c42d..a4f6882c0e 100644 --- a/_i_request_logger_8cs.html +++ b/_i_request_logger_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_request_logger_8cs_source.html b/_i_request_logger_8cs_source.html index aa665b7c97..f8120d4009 100644 --- a/_i_request_logger_8cs_source.html +++ b/_i_request_logger_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_restart_handler_8cs.html b/_i_restart_handler_8cs.html index 06443587c9..7d6a25846f 100644 --- a/_i_restart_handler_8cs.html +++ b/_i_restart_handler_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_restart_handler_8cs_source.html b/_i_restart_handler_8cs_source.html index a40ef7407b..517e4823ed 100644 --- a/_i_restart_handler_8cs_source.html +++ b/_i_restart_handler_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_restart_registration_8cs.html b/_i_restart_registration_8cs.html index 2250904f10..b4d8928634 100644 --- a/_i_restart_registration_8cs.html +++ b/_i_restart_registration_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_restart_registration_8cs_source.html b/_i_restart_registration_8cs_source.html index 0973eeec8d..5b68f93dc6 100644 --- a/_i_restart_registration_8cs_source.html +++ b/_i_restart_registration_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_server_8cs.html b/_i_server_8cs.html index fdc7a5b9fb..8702aaf10d 100644 --- a/_i_server_8cs.html +++ b/_i_server_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_server_8cs_source.html b/_i_server_8cs_source.html index 53769ba5df..72481a20e7 100644 --- a/_i_server_8cs_source.html +++ b/_i_server_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_server_client_8cs.html b/_i_server_client_8cs.html index 1e655a6f48..cf59a4ac22 100644 --- a/_i_server_client_8cs.html +++ b/_i_server_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_server_client_8cs_source.html b/_i_server_client_8cs_source.html index 5e62a2afee..7d01030126 100644 --- a/_i_server_client_8cs_source.html +++ b/_i_server_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_server_client_factory_8cs.html b/_i_server_client_factory_8cs.html index bba6c4cfe6..69192dc86b 100644 --- a/_i_server_client_factory_8cs.html +++ b/_i_server_client_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_server_client_factory_8cs_source.html b/_i_server_client_factory_8cs_source.html index d2a3736acf..6a7c264acc 100644 --- a/_i_server_client_factory_8cs_source.html +++ b/_i_server_client_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_server_control_8cs.html b/_i_server_control_8cs.html index f7a5776580..bd841010c4 100644 --- a/_i_server_control_8cs.html +++ b/_i_server_control_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_server_control_8cs_source.html b/_i_server_control_8cs_source.html index d7c319a58f..0e3aa08a77 100644 --- a/_i_server_control_8cs_source.html +++ b/_i_server_control_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_server_factory_8cs.html b/_i_server_factory_8cs.html index f057762ffe..7ed4bdede1 100644 --- a/_i_server_factory_8cs.html +++ b/_i_server_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_server_factory_8cs_source.html b/_i_server_factory_8cs_source.html index 645ff5b798..bfd568495f 100644 --- a/_i_server_factory_8cs_source.html +++ b/_i_server_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_server_port_provider_8cs.html b/_i_server_port_provider_8cs.html index be18ffea80..8bb5d5a03e 100644 --- a/_i_server_port_provider_8cs.html +++ b/_i_server_port_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_server_port_provider_8cs_source.html b/_i_server_port_provider_8cs_source.html index 1733642972..7de84c78cc 100644 --- a/_i_server_port_provider_8cs_source.html +++ b/_i_server_port_provider_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_session_controller_8cs.html b/_i_session_controller_8cs.html index 3130ce76cd..eeecfa35a1 100644 --- a/_i_session_controller_8cs.html +++ b/_i_session_controller_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_session_controller_8cs_source.html b/_i_session_controller_8cs_source.html index 23f6b9bcc5..a9e6422e7c 100644 --- a/_i_session_controller_8cs_source.html +++ b/_i_session_controller_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_session_controller_factory_8cs.html b/_i_session_controller_factory_8cs.html index 163e243c2f..c2ecd431c9 100644 --- a/_i_session_controller_factory_8cs.html +++ b/_i_session_controller_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_session_controller_factory_8cs_source.html b/_i_session_controller_factory_8cs_source.html index 1368443bd3..a67a9c898a 100644 --- a/_i_session_controller_factory_8cs_source.html +++ b/_i_session_controller_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_symlink_factory_8cs.html b/_i_symlink_factory_8cs.html index 10931ca13b..96c6a9e779 100644 --- a/_i_symlink_factory_8cs.html +++ b/_i_symlink_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_symlink_factory_8cs_source.html b/_i_symlink_factory_8cs_source.html index ed275bf260..1a6bcebb32 100644 --- a/_i_symlink_factory_8cs_source.html +++ b/_i_symlink_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_synchronous_i_o_manager_8cs.html b/_i_synchronous_i_o_manager_8cs.html index aac2248d64..8b9b52f74b 100644 --- a/_i_synchronous_i_o_manager_8cs.html +++ b/_i_synchronous_i_o_manager_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_synchronous_i_o_manager_8cs_source.html b/_i_synchronous_i_o_manager_8cs_source.html index 4c406089be..c156fa8feb 100644 --- a/_i_synchronous_i_o_manager_8cs_source.html +++ b/_i_synchronous_i_o_manager_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_system_identity_8cs.html b/_i_system_identity_8cs.html index 1d634f1cdc..dc3a907c58 100644 --- a/_i_system_identity_8cs.html +++ b/_i_system_identity_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_system_identity_8cs_source.html b/_i_system_identity_8cs_source.html index 4b9091c619..3da98ef8f6 100644 --- a/_i_system_identity_8cs_source.html +++ b/_i_system_identity_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_system_identity_factory_8cs.html b/_i_system_identity_factory_8cs.html index affe3f3620..5b02809b88 100644 --- a/_i_system_identity_factory_8cs.html +++ b/_i_system_identity_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_system_identity_factory_8cs_source.html b/_i_system_identity_factory_8cs_source.html index 2d22cef0ad..aee28e3cc5 100644 --- a/_i_system_identity_factory_8cs_source.html +++ b/_i_system_identity_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_token_factory_8cs.html b/_i_token_factory_8cs.html index 2695423bd5..5d650cff59 100644 --- a/_i_token_factory_8cs.html +++ b/_i_token_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_token_factory_8cs_source.html b/_i_token_factory_8cs_source.html index 7ee646cd01..4003668ed5 100644 --- a/_i_token_factory_8cs_source.html +++ b/_i_token_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_i_users_client_8cs.html b/_i_users_client_8cs.html index 777f2c952b..a9d21a9227 100644 --- a/_i_users_client_8cs.html +++ b/_i_users_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_i_users_client_8cs_source.html b/_i_users_client_8cs_source.html index a75e9c2dbe..10793881d4 100644 --- a/_i_users_client_8cs_source.html +++ b/_i_users_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_identity_cache_8cs.html b/_identity_cache_8cs.html index d9fa173c53..36f72db241 100644 --- a/_identity_cache_8cs.html +++ b/_identity_cache_8cs.html @@ -25,7 +25,7 @@ diff --git a/_identity_cache_8cs_source.html b/_identity_cache_8cs_source.html index 18038ff9bf..e22cf5e49d 100644 --- a/_identity_cache_8cs_source.html +++ b/_identity_cache_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_identity_cache_object_8cs.html b/_identity_cache_object_8cs.html index 2d07224a67..27640b218e 100644 --- a/_identity_cache_object_8cs.html +++ b/_identity_cache_object_8cs.html @@ -25,7 +25,7 @@ diff --git a/_identity_cache_object_8cs_source.html b/_identity_cache_object_8cs_source.html index 31245b4038..be7b97d1c5 100644 --- a/_identity_cache_object_8cs_source.html +++ b/_identity_cache_object_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_instance_client_8cs.html b/_instance_client_8cs.html index 46b8f245a5..4e5dcf703f 100644 --- a/_instance_client_8cs.html +++ b/_instance_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_instance_client_8cs_source.html b/_instance_client_8cs_source.html index a4a2cadab3..fe23bcd21b 100644 --- a/_instance_client_8cs_source.html +++ b/_instance_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_instance_controller_8cs.html b/_instance_controller_8cs.html index f33e2bb2db..8e2a2198cd 100644 --- a/_instance_controller_8cs.html +++ b/_instance_controller_8cs.html @@ -25,7 +25,7 @@ diff --git a/_instance_controller_8cs_source.html b/_instance_controller_8cs_source.html index f7acdc4e8a..4522370cfe 100644 --- a/_instance_controller_8cs_source.html +++ b/_instance_controller_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,12 +94,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Linq.Expressions;
11 using System.Net;
12 using System.Reflection;
13 using System.Threading;
14 using System.Threading.Tasks;
15 using Tgstation.Server.Api;
21 using Tgstation.Server.Host.IO;
26 
27 namespace Tgstation.Server.Host.Controllers
28 {
32  [Route(Routes.InstanceManager)]
33  #pragma warning disable CA1506 // TODO: Decomplexify
34  public sealed class InstanceController : ApiController
35  {
39  const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
40 
41  const string MoveInstanceJobPrefix = "Move instance ID ";
42 
47 
52 
57 
62 
67 
72 
86  IDatabaseContext databaseContext,
87  IAuthenticationContextFactory authenticationContextFactory,
88  IJobManager jobManager,
89  IInstanceManager instanceManager,
90  IIOManager ioManager,
91  IAssemblyInformationProvider assemblyInformationProvider,
92  IPlatformIdentifier platformIdentifier,
93  IOptions<GeneralConfiguration> generalConfigurationOptions,
94  ILogger<InstanceController> logger)
95  : base(databaseContext, authenticationContextFactory, logger, false, true)
96  {
97  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
98  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
99  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
100  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
101  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
102  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
103  }
104 
105  string NormalizePath(string path)
106  {
107  if (path == null)
108  return null;
109 
110  path = ioManager.ResolvePath(path);
111  if (platformIdentifier.IsWindows)
112  path = path.ToUpperInvariant().Replace('\\', '/');
113 
114  return path;
115  }
116 
117  Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
118  {
119  ByondRights = (ByondRights)~0U,
126  UserId = AuthenticationContext.User.Id
127  };
128 
137  [HttpPut]
138  [TgsAuthorize(InstanceManagerRights.Create)]
139  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
140  [ProducesResponseType(typeof(Api.Models.Instance), 201)]
141  public async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
142  {
143  if (model == null)
144  throw new ArgumentNullException(nameof(model));
145 
146  if (String.IsNullOrWhiteSpace(model.Name))
147  return BadRequest(new ErrorMessage(ErrorCode.InstanceWhitespaceName));
148 
149  var targetInstancePath = NormalizePath(model.Path);
150  model.Path = targetInstancePath;
151 
152  var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory);
153 
154  bool InstanceIsChildOf(string otherPath)
155  {
156  if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
157  return false;
158 
159  bool sameLength = targetInstancePath.Length == otherPath.Length;
160  char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
161  return sameLength
162  || dirSeparatorChar == Path.DirectorySeparatorChar
163  || dirSeparatorChar == Path.AltDirectorySeparatorChar;
164  }
165 
166  if (InstanceIsChildOf(installationDirectoryPath))
167  return Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
168 
169  // Validate it's not a child of any other instance
170  IActionResult earlyOut = null;
171  ulong countOfOtherInstances = 0;
172  using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
173  {
174  var newCancellationToken = cts.Token;
175  try
176  {
177  await DatabaseContext.Instances.ForEachAsync(
178  otherInstance =>
179  {
180  if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
181  earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached));
182  else if (InstanceIsChildOf(otherInstance.Path))
183  earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
184 
185  if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
186  cts.Cancel();
187  },
188  newCancellationToken)
189  .ConfigureAwait(false);
190  }
191  catch (OperationCanceledException)
192  {
193  cancellationToken.ThrowIfCancellationRequested();
194  }
195  }
196 
197  if (earlyOut != null)
198  return earlyOut;
199 
200  // Last test, ensure it's in the list of valid paths
201  if (!(generalConfiguration.ValidInstancePaths?
202  .Select(path => NormalizePath(path))
203  .Any(path => InstanceIsChildOf(path)) ?? true))
204  return BadRequest(new ErrorMessage(ErrorCode.InstanceNotAtWhitelistedPath));
205 
206  async Task<bool> DirExistsAndIsNotEmpty()
207  {
208  if (!await ioManager.DirectoryExists(model.Path, cancellationToken).ConfigureAwait(false))
209  return false;
210 
211  var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
212  var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken);
213 
214  var files = await filesTask.ConfigureAwait(false);
215  var dirs = await dirsTask.ConfigureAwait(false);
216 
217  return files.Concat(dirs).Any();
218  }
219 
220  var dirExistsTask = DirExistsAndIsNotEmpty();
221  bool attached = false;
222  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
223  if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
224  return Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath));
225  else
226  attached = true;
227 
228  var newInstance = new Models.Instance
229  {
230  ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
232  {
233  AllowWebClient = false,
234  AutoStart = false,
235  PrimaryPort = 1337,
236  SecondaryPort = 1338,
237  SecurityLevel = DreamDaemonSecurity.Safe,
238  StartupTimeout = 60,
239  HeartbeatSeconds = 60
240  },
242  {
243  ApiValidationPort = 1339,
244  ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
245  },
246  Name = model.Name,
247  Online = false,
248  Path = model.Path,
249  AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
250  ChatBotLimit = model.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
252  {
253  CommitterEmail = "tgstation-server@users.noreply.github.com",
254  CommitterName = assemblyInformationProvider.VersionPrefix,
255  PushTestMergeCommits = false,
256  ShowTestMergeCommitters = false,
257  AutoUpdatesKeepTestMerges = false,
258  AutoUpdatesSynchronize = false,
259  PostTestMergeComment = false
260  },
261  InstanceUsers = new List<Models.InstanceUser> // give this user full privileges on the instance
262  {
263  InstanceAdminUser()
264  }
265  };
266 
267  DatabaseContext.Instances.Add(newInstance);
268  try
269  {
270  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
271 
272  try
273  {
274  // actually reserve it now
275  await ioManager.CreateDirectory(targetInstancePath, cancellationToken).ConfigureAwait(false);
276  await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
277  }
278  catch
279  {
280  // oh shit delete the model
281  DatabaseContext.Instances.Remove(newInstance);
282 
283  await DatabaseContext.Save(default).ConfigureAwait(false);
284  throw;
285  }
286  }
287  catch (IOException e)
288  {
289  return Conflict(new ErrorMessage(ErrorCode.IOError)
290  {
291  AdditionalData = e.Message
292  });
293  }
294 
295  Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
296 
297  var api = newInstance.ToApi();
298  return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
299  }
300 
309  [HttpDelete("{id}")]
310  [TgsAuthorize(InstanceManagerRights.Delete)]
311  [ProducesResponseType(204)]
312  [ProducesResponseType(410)]
313  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
314  {
315  var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id)
316  .Include(x => x.WatchdogReattachInformation)
317  .Include(x => x.WatchdogReattachInformation.Alpha)
318  .Include(x => x.WatchdogReattachInformation.Bravo)
319  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
320  if (originalModel == default)
321  return StatusCode((int)HttpStatusCode.Gone);
322  if (originalModel.Online.Value)
323  return Conflict(new ErrorMessage(ErrorCode.InstanceDetachOnline));
324 
325  if (originalModel.WatchdogReattachInformation != null)
326  {
327  DatabaseContext.WatchdogReattachInformations.Remove(originalModel.WatchdogReattachInformation);
328  if (originalModel.WatchdogReattachInformation.Alpha != null)
329  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Alpha);
330  if (originalModel.WatchdogReattachInformation.Bravo != null)
331  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Bravo);
332  }
333 
334  DatabaseContext.Instances.Remove(originalModel);
335 
336  var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
337  await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), default).ConfigureAwait(false);
338  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // cascades everything
339  return NoContent();
340  }
341 
350  [HttpPost]
351  [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline | InstanceManagerRights.SetChatBotLimit)]
352  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
353  [ProducesResponseType(typeof(Api.Models.Instance), 202)]
354  [ProducesResponseType(410)]
355 #pragma warning disable CA1502 // TODO: Decomplexify
356  public async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
357  {
358  if (model == null)
359  throw new ArgumentNullException(nameof(model));
360 
361  IQueryable<Models.Instance> InstanceQuery() => DatabaseContext.Instances.Where(x => x.Id == model.Id);
362 
363  var moveJob = await InstanceQuery()
364  .SelectMany(x => x.Jobs).
365 #pragma warning disable CA1307 // Specify StringComparison
366  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
367 #pragma warning restore CA1307 // Specify StringComparison
368  .Select(x => new Models.Job
369  {
370  Id = x.Id
371  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
372 
373  if (moveJob != default)
374  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false); // cancel it now
375 
376  var originalModel = await InstanceQuery()
377  .Include(x => x.RepositorySettings)
378  .Include(x => x.ChatSettings)
379  .ThenInclude(x => x.Channels)
380  .Include(x => x.DreamDaemonSettings) // need these for onlining
381  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
382  if (originalModel == default(Models.Instance))
383  return StatusCode((int)HttpStatusCode.Gone);
384 
385  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
386  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
387  {
388  var memberSelectorExpression = (MemberExpression)expression.Body;
389  var property = (PropertyInfo)memberSelectorExpression.Member;
390 
391  var newVal = property.GetValue(model);
392  if (newVal == null)
393  return false;
394  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
395  return true;
396 
397  property.SetValue(originalModel, newVal);
398  return false;
399  }
400 
401  string originalModelPath = null;
402  string rawPath = null;
403  if (model.Path != null)
404  {
405  rawPath = NormalizePath(model.Path);
406 
407  if (model.Path != originalModel.Path)
408  {
409  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
410  return Forbid();
411  if (originalModel.Online.Value && model.Online != true)
412  return Conflict(new ErrorMessage(ErrorCode.InstanceRelocateOnline));
413 
414  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
415  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
416  return Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath));
417 
418  originalModelPath = originalModel.Path;
419  originalModel.Path = model.Path;
420  }
421  }
422 
423  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
424  var originalOnline = originalModel.Online.Value;
425  var renamed = model.Name != null && originalModel.Name != model.Name;
426 
427  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
428  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
429  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
430  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline)
431  || CheckModified(x => x.ChatBotLimit, InstanceManagerRights.SetChatBotLimit))
432  return Forbid();
433 
434  if (model.ChatBotLimit.HasValue)
435  {
436  var countOfExistingChatBots = await DatabaseContext
437  .ChatBots
438  .Where(x => x.InstanceId == originalModel.Id)
439  .CountAsync(cancellationToken)
440  .ConfigureAwait(false);
441 
442  if (countOfExistingChatBots > model.ChatBotLimit.Value)
443  return Conflict(new ErrorMessage(ErrorCode.ChatBotMax));
444  }
445 
446  // ensure the current user has write privilege on the instance
447  var usersInstanceUser = await InstanceQuery()
448  .SelectMany(x => x.InstanceUsers)
449  .Where(x => x.UserId == AuthenticationContext.User.Id)
450  .FirstOrDefaultAsync(cancellationToken)
451  .ConfigureAwait(false);
452  if (usersInstanceUser == default)
453  {
454  var instanceAdminUser = InstanceAdminUser();
455  instanceAdminUser.InstanceId = originalModel.Id;
456  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
457  }
458  else
459  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
460 
461  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
462 
463  if (renamed)
464  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
465 
466  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
467  try
468  {
469  if (originalOnline && model.Online == false)
470  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
471  else if (!originalOnline && model.Online == true)
472  {
473  // force autostart false here because we don't want any long running jobs right now
474  // remember to document this
475  originalModel.DreamDaemonSettings.AutoStart = false;
476  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
477  }
478  }
479  catch (Exception e)
480  {
481  if(!(e is OperationCanceledException))
482  Logger.LogError("Error changing instance online state! Exception: {0}", e);
483  originalModel.Online = originalOnline;
484  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
485  if (originalModelPath != null)
486  originalModel.Path = originalModelPath;
487  await DatabaseContext.Save(default).ConfigureAwait(false);
488  throw;
489  }
490 
491  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
492  {
493  Id = originalModel.Id
494  };
495 
496  var moving = originalModelPath != null;
497  if (moving)
498  {
499  var job = new Models.Job
500  {
501  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
502  Instance = originalModel,
503  CancelRightsType = RightsType.InstanceManager,
504  CancelRight = (ulong)InstanceManagerRights.Relocate,
505  StartedBy = AuthenticationContext.User
506  };
507 
508  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
509  api.MoveJob = job.ToApi();
510  }
511 
512  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
513  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
514 
515  return moving ? (IActionResult)Accepted(api) : Json(api);
516  }
517 #pragma warning restore CA1502
518 
525  [HttpGet(Routes.List)]
526  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
527  [ProducesResponseType(typeof(IEnumerable<Api.Models.Instance>), 200)]
528  public async Task<IActionResult> List(CancellationToken cancellationToken)
529  {
530  IQueryable<Models.Instance> GetBaseQuery()
531  {
532  IQueryable<Models.Instance> query = DatabaseContext.Instances;
534  query = query
535  .Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id))
536  .Where(x => x.InstanceUsers.Any(instanceUser =>
537  instanceUser.ByondRights != ByondRights.None ||
538  instanceUser.ChatBotRights != ChatBotRights.None ||
539  instanceUser.ConfigurationRights != ConfigurationRights.None ||
540  instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
541  instanceUser.DreamMakerRights != DreamMakerRights.None ||
542  instanceUser.InstanceUserRights != InstanceUserRights.None));
543 
544  // Hack for EF IAsyncEnumerable BS
545  return query.Select(x => x);
546  }
547 
548  var moveJobs = await GetBaseQuery()
549  .SelectMany(x => x.Jobs)
550 #pragma warning disable CA1307 // Specify StringComparison
551  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
552 #pragma warning restore CA1307 // Specify StringComparison
553  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
554  .Include(x => x.Instance)
555  .ToListAsync(cancellationToken)
556  .ConfigureAwait(false);
557 
558  var instances = await GetBaseQuery()
559  .ToListAsync(cancellationToken)
560  .ConfigureAwait(false);
561 
562  var apis = instances.Select(x => x.ToApi());
563  foreach(var I in moveJobs)
564  apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi(); // if this .First() fails i will personally murder kevinz000 because I just know he is somehow responsible
565  return Json(apis);
566  }
567 
576  [HttpGet("{id}")]
577  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
578  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
579  [ProducesResponseType(410)]
580  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
581  {
582  var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
583  IQueryable<Models.Instance> QueryForUser()
584  {
585  var query = DatabaseContext.Instances.Where(x => x.Id == id);
586 
587  if (cantList)
588  query = query.Include(x => x.InstanceUsers);
589  return query;
590  }
591 
592  var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
593 
594  if (instance == null)
595  return StatusCode((int)HttpStatusCode.Gone);
596 
597  if (cantList && !instance.InstanceUsers.Any(instanceUser => instanceUser.UserId == AuthenticationContext.User.Id &&
598  (instanceUser.ByondRights != ByondRights.None ||
599  instanceUser.ChatBotRights != ChatBotRights.None ||
600  instanceUser.ConfigurationRights != ConfigurationRights.None ||
601  instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
602  instanceUser.DreamMakerRights != DreamMakerRights.None ||
603  instanceUser.InstanceUserRights != InstanceUserRights.None)))
604  return Forbid();
605 
606  var api = instance.ToApi();
607 
608  var moveJob = await QueryForUser()
609  .SelectMany(x => x.Jobs)
610 #pragma warning disable CA1307 // Specify StringComparison
611  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
612 #pragma warning restore CA1307 // Specify StringComparison
613  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
614  .FirstOrDefaultAsync(cancellationToken)
615  .ConfigureAwait(false);
616  api.MoveJob = moveJob?.ToApi();
617  return Json(api);
618  }
619  }
620 }
string Name
The name of the User
Definition: User.cs:41
-
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Linq.Expressions;
11 using System.Net;
12 using System.Reflection;
13 using System.Threading;
14 using System.Threading.Tasks;
15 using Tgstation.Server.Api;
21 using Tgstation.Server.Host.IO;
26 
27 namespace Tgstation.Server.Host.Controllers
28 {
32  [Route(Routes.InstanceManager)]
33  #pragma warning disable CA1506 // TODO: Decomplexify
34  public sealed class InstanceController : ApiController
35  {
39  const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
40 
41  const string MoveInstanceJobPrefix = "Move instance ID ";
42 
47 
52 
57 
62 
67 
72 
86  IDatabaseContext databaseContext,
87  IAuthenticationContextFactory authenticationContextFactory,
88  IJobManager jobManager,
89  IInstanceManager instanceManager,
90  IIOManager ioManager,
91  IAssemblyInformationProvider assemblyInformationProvider,
92  IPlatformIdentifier platformIdentifier,
93  IOptions<GeneralConfiguration> generalConfigurationOptions,
94  ILogger<InstanceController> logger)
95  : base(databaseContext, authenticationContextFactory, logger, false, true)
96  {
97  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
98  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
99  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
100  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
101  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
102  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
103  }
104 
105  string NormalizePath(string path)
106  {
107  if (path == null)
108  return null;
109 
110  path = ioManager.ResolvePath(path);
111  if (platformIdentifier.IsWindows)
112  path = path.ToUpperInvariant().Replace('\\', '/');
113 
114  return path;
115  }
116 
117  Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
118  {
119  ByondRights = (ByondRights)~0U,
126  UserId = AuthenticationContext.User.Id
127  };
128 
137  [HttpPut]
138  [TgsAuthorize(InstanceManagerRights.Create)]
139  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
140  [ProducesResponseType(typeof(Api.Models.Instance), 201)]
141  public async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
142  {
143  if (model == null)
144  throw new ArgumentNullException(nameof(model));
145 
146  if (String.IsNullOrWhiteSpace(model.Name))
147  return BadRequest(new ErrorMessage(ErrorCode.InstanceWhitespaceName));
148 
149  var targetInstancePath = NormalizePath(model.Path);
150  model.Path = targetInstancePath;
151 
152  var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory);
153 
154  bool InstanceIsChildOf(string otherPath)
155  {
156  if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
157  return false;
158 
159  bool sameLength = targetInstancePath.Length == otherPath.Length;
160  char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
161  return sameLength
162  || dirSeparatorChar == Path.DirectorySeparatorChar
163  || dirSeparatorChar == Path.AltDirectorySeparatorChar;
164  }
165 
166  if (InstanceIsChildOf(installationDirectoryPath))
167  return Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
168 
169  // Validate it's not a child of any other instance
170  IActionResult earlyOut = null;
171  ulong countOfOtherInstances = 0;
172  using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
173  {
174  var newCancellationToken = cts.Token;
175  try
176  {
177  await DatabaseContext
178  .Instances
179  .AsQueryable()
180  .Select(x => new Models.Instance
181  {
182  Path = x.Path
183  })
184  .ForEachAsync(
185  otherInstance =>
186  {
187  if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
188  earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached));
189  else if (InstanceIsChildOf(otherInstance.Path))
190  earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
191 
192  if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
193  cts.Cancel();
194  },
195  newCancellationToken)
196  .ConfigureAwait(false);
197  }
198  catch (OperationCanceledException)
199  {
200  cancellationToken.ThrowIfCancellationRequested();
201  }
202  }
203 
204  if (earlyOut != null)
205  return earlyOut;
206 
207  // Last test, ensure it's in the list of valid paths
208  if (!(generalConfiguration.ValidInstancePaths?
209  .Select(path => NormalizePath(path))
210  .Any(path => InstanceIsChildOf(path)) ?? true))
211  return BadRequest(new ErrorMessage(ErrorCode.InstanceNotAtWhitelistedPath));
212 
213  async Task<bool> DirExistsAndIsNotEmpty()
214  {
215  if (!await ioManager.DirectoryExists(model.Path, cancellationToken).ConfigureAwait(false))
216  return false;
217 
218  var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
219  var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken);
220 
221  var files = await filesTask.ConfigureAwait(false);
222  var dirs = await dirsTask.ConfigureAwait(false);
223 
224  return files.Concat(dirs).Any();
225  }
226 
227  var dirExistsTask = DirExistsAndIsNotEmpty();
228  bool attached = false;
229  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
230  if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
231  return Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath));
232  else
233  attached = true;
234 
235  var newInstance = new Models.Instance
236  {
237  ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
239  {
240  AllowWebClient = false,
241  AutoStart = false,
242  PrimaryPort = 1337,
243  SecondaryPort = 1338,
244  SecurityLevel = DreamDaemonSecurity.Safe,
245  StartupTimeout = 60,
246  HeartbeatSeconds = 60
247  },
249  {
250  ApiValidationPort = 1339,
251  ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
252  },
253  Name = model.Name,
254  Online = false,
255  Path = model.Path,
256  AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
257  ChatBotLimit = model.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
259  {
260  CommitterEmail = "tgstation-server@users.noreply.github.com",
261  CommitterName = assemblyInformationProvider.VersionPrefix,
262  PushTestMergeCommits = false,
263  ShowTestMergeCommitters = false,
264  AutoUpdatesKeepTestMerges = false,
265  AutoUpdatesSynchronize = false,
266  PostTestMergeComment = false
267  },
268  InstanceUsers = new List<Models.InstanceUser> // give this user full privileges on the instance
269  {
270  InstanceAdminUser()
271  }
272  };
273 
274  DatabaseContext.Instances.Add(newInstance);
275  try
276  {
277  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
278 
279  try
280  {
281  // actually reserve it now
282  await ioManager.CreateDirectory(targetInstancePath, cancellationToken).ConfigureAwait(false);
283  await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
284  }
285  catch
286  {
287  // oh shit delete the model
288  DatabaseContext.Instances.Remove(newInstance);
289 
290  await DatabaseContext.Save(default).ConfigureAwait(false);
291  throw;
292  }
293  }
294  catch (IOException e)
295  {
296  return Conflict(new ErrorMessage(ErrorCode.IOError)
297  {
298  AdditionalData = e.Message
299  });
300  }
301 
302  Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
303 
304  var api = newInstance.ToApi();
305  return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
306  }
307 
316  [HttpDelete("{id}")]
317  [TgsAuthorize(InstanceManagerRights.Delete)]
318  [ProducesResponseType(204)]
319  [ProducesResponseType(410)]
320  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
321  {
322  var originalModel = await DatabaseContext
323  .Instances
324  .AsQueryable()
325  .Where(x => x.Id == id)
326  .Include(x => x.WatchdogReattachInformation)
327  .Include(x => x.WatchdogReattachInformation.Alpha)
328  .Include(x => x.WatchdogReattachInformation.Bravo)
329  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
330  if (originalModel == default)
331  return StatusCode((int)HttpStatusCode.Gone);
332  if (originalModel.Online.Value)
333  return Conflict(new ErrorMessage(ErrorCode.InstanceDetachOnline));
334 
335  if (originalModel.WatchdogReattachInformation != null)
336  {
337  DatabaseContext.WatchdogReattachInformations.Remove(originalModel.WatchdogReattachInformation);
338  if (originalModel.WatchdogReattachInformation.Alpha != null)
339  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Alpha);
340  if (originalModel.WatchdogReattachInformation.Bravo != null)
341  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Bravo);
342  }
343 
344  DatabaseContext.Instances.Remove(originalModel);
345 
346  var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
347  await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), default).ConfigureAwait(false);
348  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // cascades everything
349  return NoContent();
350  }
351 
360  [HttpPost]
361  [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline | InstanceManagerRights.SetChatBotLimit)]
362  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
363  [ProducesResponseType(typeof(Api.Models.Instance), 202)]
364  [ProducesResponseType(410)]
365 #pragma warning disable CA1502 // TODO: Decomplexify
366  public async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
367  {
368  if (model == null)
369  throw new ArgumentNullException(nameof(model));
370 
371  IQueryable<Models.Instance> InstanceQuery() => DatabaseContext
372  .Instances
373  .AsQueryable()
374  .Where(x => x.Id == model.Id);
375 
376  var moveJob = await InstanceQuery()
377  .SelectMany(x => x.Jobs).
378 #pragma warning disable CA1307 // Specify StringComparison
379  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
380 #pragma warning restore CA1307 // Specify StringComparison
381  .Select(x => new Models.Job
382  {
383  Id = x.Id
384  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
385 
386  if (moveJob != default)
387  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false); // cancel it now
388 
389  var originalModel = await InstanceQuery()
390  .Include(x => x.RepositorySettings)
391  .Include(x => x.ChatSettings)
392  .ThenInclude(x => x.Channels)
393  .Include(x => x.DreamDaemonSettings) // need these for onlining
394  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
395  if (originalModel == default(Models.Instance))
396  return StatusCode((int)HttpStatusCode.Gone);
397 
398  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
399  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
400  {
401  var memberSelectorExpression = (MemberExpression)expression.Body;
402  var property = (PropertyInfo)memberSelectorExpression.Member;
403 
404  var newVal = property.GetValue(model);
405  if (newVal == null)
406  return false;
407  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
408  return true;
409 
410  property.SetValue(originalModel, newVal);
411  return false;
412  }
413 
414  string originalModelPath = null;
415  string rawPath = null;
416  if (model.Path != null)
417  {
418  rawPath = NormalizePath(model.Path);
419 
420  if (model.Path != originalModel.Path)
421  {
422  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
423  return Forbid();
424  if (originalModel.Online.Value && model.Online != true)
425  return Conflict(new ErrorMessage(ErrorCode.InstanceRelocateOnline));
426 
427  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
428  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
429  return Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath));
430 
431  originalModelPath = originalModel.Path;
432  originalModel.Path = model.Path;
433  }
434  }
435 
436  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
437  var originalOnline = originalModel.Online.Value;
438  var renamed = model.Name != null && originalModel.Name != model.Name;
439 
440  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
441  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
442  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
443  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline)
444  || CheckModified(x => x.ChatBotLimit, InstanceManagerRights.SetChatBotLimit))
445  return Forbid();
446 
447  if (model.ChatBotLimit.HasValue)
448  {
449  var countOfExistingChatBots = await DatabaseContext
450  .ChatBots
451  .AsQueryable()
452  .Where(x => x.InstanceId == originalModel.Id)
453  .CountAsync(cancellationToken)
454  .ConfigureAwait(false);
455 
456  if (countOfExistingChatBots > model.ChatBotLimit.Value)
457  return Conflict(new ErrorMessage(ErrorCode.ChatBotMax));
458  }
459 
460  // ensure the current user has write privilege on the instance
461  var usersInstanceUser = await InstanceQuery()
462  .SelectMany(x => x.InstanceUsers)
463  .Where(x => x.UserId == AuthenticationContext.User.Id)
464  .FirstOrDefaultAsync(cancellationToken)
465  .ConfigureAwait(false);
466  if (usersInstanceUser == default)
467  {
468  var instanceAdminUser = InstanceAdminUser();
469  instanceAdminUser.InstanceId = originalModel.Id;
470  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
471  }
472  else
473  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
474 
475  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
476 
477  if (renamed)
478  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
479 
480  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
481  try
482  {
483  if (originalOnline && model.Online == false)
484  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
485  else if (!originalOnline && model.Online == true)
486  {
487  // force autostart false here because we don't want any long running jobs right now
488  // remember to document this
489  originalModel.DreamDaemonSettings.AutoStart = false;
490  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
491  }
492  }
493  catch (Exception e)
494  {
495  if(!(e is OperationCanceledException))
496  Logger.LogError("Error changing instance online state! Exception: {0}", e);
497  originalModel.Online = originalOnline;
498  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
499  if (originalModelPath != null)
500  originalModel.Path = originalModelPath;
501  await DatabaseContext.Save(default).ConfigureAwait(false);
502  throw;
503  }
504 
505  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
506  {
507  Id = originalModel.Id
508  };
509 
510  var moving = originalModelPath != null;
511  if (moving)
512  {
513  var job = new Models.Job
514  {
515  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
516  Instance = originalModel,
517  CancelRightsType = RightsType.InstanceManager,
518  CancelRight = (ulong)InstanceManagerRights.Relocate,
519  StartedBy = AuthenticationContext.User
520  };
521 
522  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
523  api.MoveJob = job.ToApi();
524  }
525 
526  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
527  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
528 
529  return moving ? (IActionResult)Accepted(api) : Json(api);
530  }
531 #pragma warning restore CA1502
532 
539  [HttpGet(Routes.List)]
540  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
541  [ProducesResponseType(typeof(IEnumerable<Api.Models.Instance>), 200)]
542  public async Task<IActionResult> List(CancellationToken cancellationToken)
543  {
544  IQueryable<Models.Instance> GetBaseQuery()
545  {
546  IQueryable<Models.Instance> query = DatabaseContext.Instances;
548  query = query
549  .Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id))
550  .Where(x => x.InstanceUsers.Any(instanceUser =>
551  instanceUser.ByondRights != ByondRights.None ||
552  instanceUser.ChatBotRights != ChatBotRights.None ||
553  instanceUser.ConfigurationRights != ConfigurationRights.None ||
554  instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
555  instanceUser.DreamMakerRights != DreamMakerRights.None ||
556  instanceUser.InstanceUserRights != InstanceUserRights.None));
557 
558  // Hack for EF IAsyncEnumerable BS
559  return query.Select(x => x);
560  }
561 
562  var moveJobs = await GetBaseQuery()
563  .SelectMany(x => x.Jobs)
564 #pragma warning disable CA1307 // Specify StringComparison
565  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
566 #pragma warning restore CA1307 // Specify StringComparison
567  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
568  .Include(x => x.Instance)
569  .ToListAsync(cancellationToken)
570  .ConfigureAwait(false);
571 
572  var instances = await GetBaseQuery()
573  .ToListAsync(cancellationToken)
574  .ConfigureAwait(false);
575 
576  var apis = instances.Select(x => x.ToApi());
577  foreach(var I in moveJobs)
578  apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi(); // if this .First() fails i will personally murder kevinz000 because I just know he is somehow responsible
579  return Json(apis);
580  }
581 
590  [HttpGet("{id}")]
591  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
592  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
593  [ProducesResponseType(410)]
594  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
595  {
596  var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
597  IQueryable<Models.Instance> QueryForUser()
598  {
599  var query = DatabaseContext
600  .Instances
601  .AsQueryable()
602  .Where(x => x.Id == id);
603 
604  if (cantList)
605  query = query.Include(x => x.InstanceUsers);
606  return query;
607  }
608 
609  var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
610 
611  if (instance == null)
612  return StatusCode((int)HttpStatusCode.Gone);
613 
614  if (cantList && !instance.InstanceUsers.Any(instanceUser => instanceUser.UserId == AuthenticationContext.User.Id &&
615  (instanceUser.ByondRights != ByondRights.None ||
616  instanceUser.ChatBotRights != ChatBotRights.None ||
617  instanceUser.ConfigurationRights != ConfigurationRights.None ||
618  instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
619  instanceUser.DreamMakerRights != DreamMakerRights.None ||
620  instanceUser.InstanceUserRights != InstanceUserRights.None)))
621  return Forbid();
622 
623  var api = instance.ToApi();
624 
625  var moveJob = await QueryForUser()
626  .SelectMany(x => x.Jobs)
627 #pragma warning disable CA1307 // Specify StringComparison
628  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
629 #pragma warning restore CA1307 // Specify StringComparison
630  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
631  .FirstOrDefaultAsync(cancellationToken)
632  .ConfigureAwait(false);
633  api.MoveJob = moveJob?.ToApi();
634  return Json(api);
635  }
636  }
637 }
string Name
The name of the User
Definition: User.cs:41
+
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext.
ConfigurationRights
Rights for Models.ConfigurationFile
+
DbSet< DualReattachInformation > WatchdogReattachInformations
The DualReattachInformations in the DatabaseContext.
-
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.Instance.
+
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.Instance.
@@ -115,33 +116,34 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
async Task< IActionResult > List(CancellationToken cancellationToken)
List Api.Models.Instances.
+
async Task< IActionResult > List(CancellationToken cancellationToken)
List Api.Models.Instances.
ConfigurationType
The type of configuration allowed on an Instance
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceController.
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
+
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ApiController for managing Components.Instances
readonly IIOManager ioManager
The IIOManager for the InstanceController
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
readonly IJobManager jobManager
The IJobManager for the InstanceController
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the InstanceController
-
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Detach an Api.Models.Instance with the given id .
-
DbSet< ReattachInformation > ReattachInformations
The ReattachInformations in the DatabaseContext<TParentContext>.
+
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Detach an Api.Models.Instance with the given id .
Routes to a server actions
Definition: Routes.cs:9
DreamDaemonRights
Rights for Models.DreamDaemon
const string CurrentDirectory
Path to the current working directory for the IIOManager.
-
DbSet< Instance > Instances
The Instances in the DatabaseContext<TParentContext>.
InstanceManagerRights
Rights for managing Models.Instances
-
async Task< IActionResult > Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
Modify an Api.Models.Instance&#39;s settings.
+
DbSet< Instance > Instances
The Instances in the DatabaseContext.
+
async Task< IActionResult > Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
Modify an Api.Models.Instance&#39;s settings.
+
DbSet< ReattachInformation > ReattachInformations
The ReattachInformations in the DatabaseContext.
DreamMakerRights
Rights for Models.DreamMaker
+
DbSet< InstanceUser > InstanceUsers
The InstanceUsers in the DatabaseContext.
RepositoryRights
Rights for a Models.Repository
InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IAssemblyInformationProvider assemblyInformationProvider, IPlatformIdentifier platformIdentifier, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< InstanceController > logger)
Construct a InstanceController
-
DbSet< InstanceUser > InstanceUsers
The InstanceUsers in the DatabaseContext<TParentContext>.
const string InstanceManager
The Models.Instance controller
Definition: Routes.cs:29
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the InstanceController
@@ -158,7 +160,6 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IIOManager that resolves paths to Environment.CurrentDirectory
DreamDaemonSecurity
DreamDaemon&#39;s security level
-
DbSet< DualReattachInformation > WatchdogReattachInformations
The DualReattachInformations in the DatabaseContext<TParentContext>.
const string List
The postfix for list operations
Definition: Routes.cs:84
Interface for using filesystems
Definition: IIOManager.cs:11
async Task< IActionResult > Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
Create or attach an Api.Models.Instance.
@@ -167,8 +168,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
-
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext<TParentContext>.
- +
const ushort DefaultChatBotLimit
Default for Api.Models.Instance.ChatBotLimit.
Definition: Instance.cs:13
diff --git a/_instance_factory_8cs.html b/_instance_factory_8cs.html index cc9ba9bbd0..a8e1b7c3e9 100644 --- a/_instance_factory_8cs.html +++ b/_instance_factory_8cs.html @@ -25,7 +25,7 @@
diff --git a/_instance_factory_8cs_source.html b/_instance_factory_8cs_source.html index 8805eebb6b..73d58e6308 100644 --- a/_instance_factory_8cs_source.html +++ b/_instance_factory_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,11 +94,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceFactory.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Threading;
5 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
20 
21 namespace Tgstation.Server.Host.Components
22 {
25  {
30 
35 
40 
44  readonly ILoggerFactory loggerFactory;
45 
49  readonly ITopicClient byondTopicSender;
50 
55 
60 
65 
70 
75 
80 
85 
90 
95 
100 
105 
110 
115 
120 
125 
150  IIOManager ioManager,
151  IDatabaseContextFactory databaseContextFactory,
152  IAssemblyInformationProvider assemblyInformationProvider,
153  ILoggerFactory loggerFactory,
154  ITopicClient byondTopicSender,
155  ICryptographySuite cryptographySuite,
156  ISynchronousIOManager synchronousIOManager,
157  ISymlinkFactory symlinkFactory,
158  IByondInstaller byondInstaller,
159  IChatManagerFactory chatFactory,
160  IProcessExecutor processExecutor,
161  IPostWriteHandler postWriteHandler,
162  IWatchdogFactory watchdogFactory,
163  IJobManager jobManager,
164  INetworkPromptReaper networkPromptReaper,
165  IGitHubClientFactory gitHubClientFactory,
166  IPlatformIdentifier platformIdentifier,
167  ILibGit2RepositoryFactory repositoryFactory,
168  ILibGit2Commands repositoryCommands,
169  IServerPortProvider serverPortProvider)
170  {
171  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
172  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
173  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
174  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
175  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
176  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
177  this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
178  this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
179  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
180  this.chatFactory = chatFactory ?? throw new ArgumentNullException(nameof(chatFactory));
181  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
182  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
183  this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory));
184  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
185  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
186  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
187  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
188  this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory));
189  this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands));
190  this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
191  }
192 
194 #pragma warning disable CA1506 // TODO: Decomplexify
195  public IInstance CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata)
196  {
197  // Create the ioManager for the instance
198  var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path);
199 
200  // various other ioManagers
201  var repoIoManager = new ResolvingIOManager(instanceIoManager, "Repository");
202  var byondIOManager = new ResolvingIOManager(instanceIoManager, "Byond");
203  var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game");
204  var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");
205 
206  var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, platformIdentifier, loggerFactory.CreateLogger<StaticFiles.Configuration>());
207  var eventConsumer = new EventConsumer(configuration);
208  var repoManager = new RepositoryManager(
209  repositoryFactory,
210  repositoryCommands,
211  repoIoManager,
212  eventConsumer,
213  loggerFactory.CreateLogger<Repository.Repository>(),
214  loggerFactory.CreateLogger<RepositoryManager>());
215  try
216  {
217  var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger<ByondManager>());
218 
219  var commandFactory = new CommandFactory(assemblyInformationProvider, byond, repoManager, databaseContextFactory, metadata);
220 
221  var chatManager = chatFactory.CreateChatManager(instanceIoManager, commandFactory, metadata.ChatSettings);
222  try
223  {
224  var sessionControllerFactory = new SessionControllerFactory(
225  processExecutor,
226  byond,
227  byondTopicSender,
228  cryptographySuite,
229  assemblyInformationProvider,
230  gameIoManager,
231  chatManager,
232  networkPromptReaper,
233  platformIdentifier,
234  bridgeRegistrar,
235  serverPortProvider,
236  loggerFactory,
237  loggerFactory.CreateLogger<SessionControllerFactory>(),
238  metadata.CloneMetadata());
239 
240  var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger<DmbFactory>(), metadata.CloneMetadata());
241  try
242  {
243  var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, loggerFactory.CreateLogger<ReattachInfoHandler>(), metadata.CloneMetadata());
244  var watchdog = watchdogFactory.CreateWatchdog(
245  chatManager,
246  dmbFactory,
247  reattachInfoHandler,
248  sessionControllerFactory,
249  gameIoManager,
250  metadata.CloneMetadata(),
251  metadata.DreamDaemonSettings);
252  eventConsumer.SetWatchdog(watchdog);
253  commandFactory.SetWatchdog(watchdog);
254  try
255  {
256  Instance instance = null;
257  var dreamMaker = new DreamMaker(
258  byond,
259  gameIoManager,
260  configuration,
261  sessionControllerFactory,
262  eventConsumer,
263  chatManager,
264  processExecutor,
265  watchdog,
266  gitHubClientFactory,
267  dmbFactory,
268  repoManager,
269  loggerFactory.CreateLogger<DreamMaker>(),
270  metadata.CloneMetadata());
271 
272  instance = new Instance(
273  metadata.CloneMetadata(),
274  repoManager,
275  byond,
276  dreamMaker,
277  watchdog,
278  chatManager,
279  configuration,
280  databaseContextFactory,
281  dmbFactory,
282  jobManager,
283  eventConsumer,
284  loggerFactory.CreateLogger<Instance>());
285 
286  return instance;
287  }
288  catch
289  {
290  watchdog.Dispose();
291  throw;
292  }
293  }
294  catch
295  {
296  dmbFactory.Dispose();
297  throw;
298  }
299  }
300  catch
301  {
302  chatManager.Dispose();
303  throw;
304  }
305  }
306  catch
307  {
308  repoManager.Dispose();
309  throw;
310  }
311  }
312 #pragma warning restore CA1506
313 
315  public Task StartAsync(CancellationToken cancellationToken)
316  {
317  CheckSystemCompatibility();
318  return byondInstaller.CleanCache(cancellationToken);
319  }
320 
322  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
323 
327  private void CheckSystemCompatibility() => repositoryFactory.CreateInMemory().Dispose();
328  }
329 }
readonly ITopicClient byondTopicSender
The ITopicClient for the InstanceFactory
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Threading;
5 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
20 
21 namespace Tgstation.Server.Host.Components
22 {
25  {
30 
35 
40 
44  readonly ILoggerFactory loggerFactory;
45 
49  readonly ITopicClient byondTopicSender;
50 
55 
60 
65 
70 
75 
80 
85 
90 
95 
100 
105 
110 
115 
120 
125 
150  IIOManager ioManager,
151  IDatabaseContextFactory databaseContextFactory,
152  IAssemblyInformationProvider assemblyInformationProvider,
153  ILoggerFactory loggerFactory,
154  ITopicClient byondTopicSender,
155  ICryptographySuite cryptographySuite,
156  ISynchronousIOManager synchronousIOManager,
157  ISymlinkFactory symlinkFactory,
158  IByondInstaller byondInstaller,
159  IChatManagerFactory chatFactory,
160  IProcessExecutor processExecutor,
161  IPostWriteHandler postWriteHandler,
162  IWatchdogFactory watchdogFactory,
163  IJobManager jobManager,
164  INetworkPromptReaper networkPromptReaper,
165  IGitHubClientFactory gitHubClientFactory,
166  IPlatformIdentifier platformIdentifier,
167  ILibGit2RepositoryFactory repositoryFactory,
168  ILibGit2Commands repositoryCommands,
169  IServerPortProvider serverPortProvider)
170  {
171  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
172  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
173  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
174  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
175  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
176  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
177  this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
178  this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
179  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
180  this.chatFactory = chatFactory ?? throw new ArgumentNullException(nameof(chatFactory));
181  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
182  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
183  this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory));
184  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
185  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
186  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
187  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
188  this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory));
189  this.repositoryCommands = repositoryCommands ?? throw new ArgumentNullException(nameof(repositoryCommands));
190  this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
191  }
192 
194 #pragma warning disable CA1506 // TODO: Decomplexify
195  public IInstance CreateInstance(IBridgeRegistrar bridgeRegistrar, Models.Instance metadata)
196  {
197  // Create the ioManager for the instance
198  var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path);
199 
200  // various other ioManagers
201  var repoIoManager = new ResolvingIOManager(instanceIoManager, "Repository");
202  var byondIOManager = new ResolvingIOManager(instanceIoManager, "Byond");
203  var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game");
204  var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");
205 
206  var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, platformIdentifier, loggerFactory.CreateLogger<StaticFiles.Configuration>());
207  var eventConsumer = new EventConsumer(configuration);
208  var repoManager = new RepositoryManager(
209  repositoryFactory,
210  repositoryCommands,
211  repoIoManager,
212  eventConsumer,
213  loggerFactory.CreateLogger<Repository.Repository>(),
214  loggerFactory.CreateLogger<RepositoryManager>());
215  try
216  {
217  var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger<ByondManager>());
218 
219  var commandFactory = new CommandFactory(assemblyInformationProvider, byond, repoManager, databaseContextFactory, metadata);
220 
221  var chatManager = chatFactory.CreateChatManager(instanceIoManager, commandFactory, metadata.ChatSettings);
222  try
223  {
224  var sessionControllerFactory = new SessionControllerFactory(
225  processExecutor,
226  byond,
227  byondTopicSender,
228  cryptographySuite,
229  assemblyInformationProvider,
230  gameIoManager,
231  chatManager,
232  networkPromptReaper,
233  platformIdentifier,
234  bridgeRegistrar,
235  serverPortProvider,
236  loggerFactory,
237  loggerFactory.CreateLogger<SessionControllerFactory>(),
238  metadata.CloneMetadata());
239 
240  var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger<DmbFactory>(), metadata.CloneMetadata());
241  try
242  {
243  var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, loggerFactory.CreateLogger<ReattachInfoHandler>(), metadata.CloneMetadata());
244  var watchdog = watchdogFactory.CreateWatchdog(
245  chatManager,
246  dmbFactory,
247  reattachInfoHandler,
248  sessionControllerFactory,
249  gameIoManager,
250  metadata.CloneMetadata(),
251  metadata.DreamDaemonSettings);
252  eventConsumer.SetWatchdog(watchdog);
253  commandFactory.SetWatchdog(watchdog);
254  try
255  {
256  Instance instance = null;
257  var dreamMaker = new DreamMaker(
258  byond,
259  gameIoManager,
260  configuration,
261  sessionControllerFactory,
262  eventConsumer,
263  chatManager,
264  processExecutor,
265  gitHubClientFactory,
266  dmbFactory,
267  repoManager,
268  loggerFactory.CreateLogger<DreamMaker>(),
269  metadata.CloneMetadata());
270 
271  instance = new Instance(
272  metadata.CloneMetadata(),
273  repoManager,
274  byond,
275  dreamMaker,
276  watchdog,
277  chatManager,
278  configuration,
279  databaseContextFactory,
280  dmbFactory,
281  jobManager,
282  eventConsumer,
283  loggerFactory.CreateLogger<Instance>());
284 
285  return instance;
286  }
287  catch
288  {
289  watchdog.Dispose();
290  throw;
291  }
292  }
293  catch
294  {
295  dmbFactory.Dispose();
296  throw;
297  }
298  }
299  catch
300  {
301  chatManager.Dispose();
302  throw;
303  }
304  }
305  catch
306  {
307  repoManager.Dispose();
308  throw;
309  }
310  }
311 #pragma warning restore CA1506
312 
314  public Task StartAsync(CancellationToken cancellationToken)
315  {
316  CheckSystemCompatibility();
317  return byondInstaller.CleanCache(cancellationToken);
318  }
319 
321  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
322 
326  private void CheckSystemCompatibility() => repositoryFactory.CreateInMemory().Dispose();
327  }
328 }
readonly ITopicClient byondTopicSender
The ITopicClient for the InstanceFactory
readonly ISynchronousIOManager synchronousIOManager
The ISynchronousIOManager for the InstanceFactory
readonly IByondInstaller byondInstaller
The IByondInstaller for the InstanceFactory
-
Task StartAsync(CancellationToken cancellationToken)
+
Task StartAsync(CancellationToken cancellationToken)
@@ -111,7 +111,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For low level interactions with a LibGit2Sharp.IRepository.
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the InstanceFactory
- +
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the InstanceFactory
@@ -170,7 +170,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For identifying the current platform
- +
diff --git a/_instance_manager_8cs.html b/_instance_manager_8cs.html index 9cb553b0d8..8a421d9d55 100644 --- a/_instance_manager_8cs.html +++ b/_instance_manager_8cs.html @@ -25,7 +25,7 @@
diff --git a/_instance_manager_8cs_source.html b/_instance_manager_8cs_source.html index c6b2ba1f38..a14d43ae37 100644 --- a/_instance_manager_8cs_source.html +++ b/_instance_manager_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,25 +94,25 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManager.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
20 
21 namespace Tgstation.Server.Host.Components
22 {
25  {
27  public Task Ready => readyTcs.Task;
28 
33 
38 
43 
48 
53 
58 
63 
68 
72  readonly ILogger<InstanceManager> logger;
73 
77  readonly IDictionary<long, IInstance> instances;
78 
82  readonly IDictionary<string, IBridgeHandler> bridgeHandlers;
83 
88 
92  readonly TaskCompletionSource<object> readyTcs;
93 
98 
102  bool disposed;
103 
118  IInstanceFactory instanceFactory,
119  IIOManager ioManager,
120  IDatabaseContextFactory databaseContextFactory,
121  IAssemblyInformationProvider assemblyInformationProvider,
122  IJobManager jobManager,
123  IServerControl serverControl,
124  ISystemIdentityFactory systemIdentityFactory,
125  IAsyncDelayer asyncDelayer,
126  IOptions<GeneralConfiguration> generalConfigurationOptions,
127  ILogger<InstanceManager> logger)
128  {
129  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
130  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
131  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
132  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
133  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
134  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
135  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
136  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
137  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
138  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
139 
140  serverControl.RegisterForRestart(this);
141 
142  instances = new Dictionary<long, IInstance>();
143  bridgeHandlers = new Dictionary<string, IBridgeHandler>();
144  readyTcs = new TaskCompletionSource<object>();
145  }
146 
148  public void Dispose()
149  {
150  lock (instances)
151  {
152  if (disposed)
153  return;
154  disposed = true;
155  }
156 
157  foreach (var I in instances)
158  I.Value.Dispose();
159  }
160 
162  public IInstance GetInstance(Models.Instance metadata)
163  {
164  if (metadata == null)
165  throw new ArgumentNullException(nameof(metadata));
166  lock (instances)
167  {
168  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
169  throw new InvalidOperationException("Instance not online!");
170  return instance;
171  }
172  }
173 
175  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
176  {
177  if (newPath == null)
178  throw new ArgumentNullException(nameof(newPath));
179  if (instance.Online.Value)
180  throw new InvalidOperationException("Cannot move an online instance!");
181  var oldPath = instance.Path;
182  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
183  await databaseContextFactory.UseContext(db =>
184  {
185  var targetInstance = new Models.Instance
186  {
187  Id = instance.Id
188  };
189  db.Instances.Attach(targetInstance);
190  targetInstance.Path = newPath;
191  return db.Save(cancellationToken);
192  }).ConfigureAwait(false);
193  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
194  }
195 
197  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
198  {
199  if (metadata == null)
200  throw new ArgumentNullException(nameof(metadata));
201  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
202  IInstance instance;
203  lock (instances)
204  {
205  if (!instances.TryGetValue(metadata.Id, out instance))
206  throw new InvalidOperationException("Instance not online!");
207  instances.Remove(metadata.Id);
208  }
209 
210  try
211  {
212  // we are the one responsible for cancelling his jobs
213  var tasks = new List<Task>();
214  await databaseContextFactory.UseContext(async db =>
215  {
216  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
217  {
218  Id = x.Id
219  }).ToAsyncEnumerable();
220  await jobs.ForEachAsync(job =>
221  {
222  lock (tasks)
223  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
224  }, cancellationToken).ConfigureAwait(false);
225  }).ConfigureAwait(false);
226 
227  await Task.WhenAll(tasks).ConfigureAwait(false);
228 
229  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
230  }
231  finally
232  {
233  instance.Dispose();
234  }
235  }
236 
238  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
239  {
240  if (metadata == null)
241  throw new ArgumentNullException(nameof(metadata));
242  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
243  var instance = instanceFactory.CreateInstance(this, metadata);
244  try
245  {
246  lock (instances)
247  {
248  if (instances.ContainsKey(metadata.Id))
249  throw new InvalidOperationException("Instance already online!");
250  instances.Add(metadata.Id, instance);
251  }
252  }
253  catch
254  {
255  instance.Dispose();
256  throw;
257  }
258 
259  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
260  }
261 
263  #pragma warning disable CA1506 // TODO: Decomplexify
264  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
265  {
266  logger.LogInformation(assemblyInformationProvider.VersionString);
267 
268  try
269  {
270  CheckSystemCompatibility();
271  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
272  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
273  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
274  var dbInstances = databaseContext.Instances.Where(x => x.Online.Value)
275  .Include(x => x.RepositorySettings)
276  .Include(x => x.ChatSettings)
277  .ThenInclude(x => x.Channels)
278  .Include(x => x.DreamDaemonSettings)
279  .ToAsyncEnumerable();
280  var tasks = new List<Task>();
281  await factoryStartup.ConfigureAwait(false);
282  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
283  await Task.WhenAll(tasks).ConfigureAwait(false);
284  logger.LogInformation("Server ready!");
285  readyTcs.SetResult(null);
286  }
287  catch (OperationCanceledException)
288  {
289  logger.LogInformation("Cancelled instance manager initialization!");
290  }
291  catch (Exception e)
292  {
293  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
294  try
295  {
296  await serverControl.Die(e).ConfigureAwait(false);
297  return;
298  }
299  catch (Exception e2)
300  {
301  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
302  }
303 
304  throw;
305  }
306  });
307  #pragma warning restore CA1506 // TODO: Decomplexify
308 
310  public async Task StopAsync(CancellationToken cancellationToken)
311  {
312  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
313  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
314  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
315 
316  // downgrade the db if necessary
317  if (downgradeVersion != null)
318  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
319  }
320 
322  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
323  {
324  downgradeVersion = updateVersion != null && updateVersion < assemblyInformationProvider.Version ? updateVersion : null;
325  return Task.CompletedTask;
326  }
327 
332  {
333  if (generalConfiguration.UseExperimentalWatchdog && !Debugger.IsAttached)
334  throw new InvalidOperationException(
335  "The experimental watchdog is currently non-functional! Please disable the '{nameof(generalConfiguration.UseExperimentalWatchdog)}' option in your configuration!");
336 
337  using var systemIdentity = systemIdentityFactory.GetCurrent();
338  if (!systemIdentity.CanCreateSymlinks)
339  throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!");
340  }
341 
343  public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
344  {
345  if (parameters == null)
346  throw new ArgumentNullException(nameof(parameters));
347 
348  IBridgeHandler bridgeHandler = null;
349  for (var i = 0; bridgeHandler == null && i < 30; ++i)
350  {
351  // There's a miniscule time period where we could potentially receive a bridge request and not have the registration ready when we launch DD
352  // This is a stopgap
353  Task delayTask = Task.CompletedTask;
354  lock (bridgeHandlers)
355  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
356  delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
357 
358  await delayTask.ConfigureAwait(false);
359  }
360 
361  if (bridgeHandler == null)
362  lock (bridgeHandlers)
363  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
364  {
365  logger.LogWarning("Recieved invalid bridge request with accees identifier: {0}", parameters.AccessIdentifier);
366  return null;
367  }
368 
369  return await bridgeHandler.ProcessBridgeRequest(parameters, cancellationToken).ConfigureAwait(false);
370  }
371 
374  {
375  if (bridgeHandler == null)
376  throw new ArgumentNullException(nameof(bridgeHandler));
377 
378  var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier;
379  lock (bridgeHandlers)
380  {
381  bridgeHandlers.Add(accessIdentifier, bridgeHandler);
382  logger.LogTrace("Registered bridge handler: {0}", accessIdentifier);
383  }
384 
385  return new BridgeRegistration(() =>
386  {
387  lock (bridgeHandlers)
388  {
389  bridgeHandlers.Remove(accessIdentifier);
390  logger.LogTrace("Unregistered bridge handler: {0}", accessIdentifier);
391  }
392  });
393  }
394  }
395 }
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the InstanceManager
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
20 
21 namespace Tgstation.Server.Host.Components
22 {
25  {
27  public Task Ready => readyTcs.Task;
28 
33 
38 
43 
48 
53 
58 
63 
68 
72  readonly ILogger<InstanceManager> logger;
73 
77  readonly IDictionary<long, IInstance> instances;
78 
82  readonly IDictionary<string, IBridgeHandler> bridgeHandlers;
83 
88 
92  readonly TaskCompletionSource<object> readyTcs;
93 
98 
102  bool disposed;
103 
118  IInstanceFactory instanceFactory,
119  IIOManager ioManager,
120  IDatabaseContextFactory databaseContextFactory,
121  IAssemblyInformationProvider assemblyInformationProvider,
122  IJobManager jobManager,
123  IServerControl serverControl,
124  ISystemIdentityFactory systemIdentityFactory,
125  IAsyncDelayer asyncDelayer,
126  IOptions<GeneralConfiguration> generalConfigurationOptions,
127  ILogger<InstanceManager> logger)
128  {
129  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
130  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
131  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
132  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
133  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
134  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
135  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
136  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
137  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
138  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
139 
140  serverControl.RegisterForRestart(this);
141 
142  instances = new Dictionary<long, IInstance>();
143  bridgeHandlers = new Dictionary<string, IBridgeHandler>();
144  readyTcs = new TaskCompletionSource<object>();
145  }
146 
148  public void Dispose()
149  {
150  lock (instances)
151  {
152  if (disposed)
153  return;
154  disposed = true;
155  }
156 
157  foreach (var I in instances)
158  I.Value.Dispose();
159  }
160 
162  public IInstance GetInstance(Models.Instance metadata)
163  {
164  if (metadata == null)
165  throw new ArgumentNullException(nameof(metadata));
166  lock (instances)
167  {
168  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
169  throw new InvalidOperationException("Instance not online!");
170  return instance;
171  }
172  }
173 
175  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
176  {
177  if (newPath == null)
178  throw new ArgumentNullException(nameof(newPath));
179  if (instance.Online.Value)
180  throw new InvalidOperationException("Cannot move an online instance!");
181  var oldPath = instance.Path;
182  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
183  await databaseContextFactory.UseContext(db =>
184  {
185  var targetInstance = new Models.Instance
186  {
187  Id = instance.Id
188  };
189  db.Instances.Attach(targetInstance);
190  targetInstance.Path = newPath;
191  return db.Save(cancellationToken);
192  }).ConfigureAwait(false);
193  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
194  }
195 
197  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
198  {
199  if (metadata == null)
200  throw new ArgumentNullException(nameof(metadata));
201  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
202  IInstance instance;
203  lock (instances)
204  {
205  if (!instances.TryGetValue(metadata.Id, out instance))
206  throw new InvalidOperationException("Instance not online!");
207  instances.Remove(metadata.Id);
208  }
209 
210  try
211  {
212  // we are the one responsible for cancelling his jobs
213  var tasks = new List<Task>();
214  await databaseContextFactory.UseContext(async db =>
215  {
216  var jobs = db
217  .Jobs
218  .AsQueryable()
219  .Where(x => x.Instance.Id == metadata.Id)
220  .Select(x => new Models.Job
221  {
222  Id = x.Id
223  });
224  await jobs.ForEachAsync(job =>
225  {
226  lock (tasks)
227  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
228  }, cancellationToken).ConfigureAwait(false);
229  }).ConfigureAwait(false);
230 
231  await Task.WhenAll(tasks).ConfigureAwait(false);
232 
233  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
234  }
235  finally
236  {
237  instance.Dispose();
238  }
239  }
240 
242  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
243  {
244  if (metadata == null)
245  throw new ArgumentNullException(nameof(metadata));
246  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
247  var instance = instanceFactory.CreateInstance(this, metadata);
248  try
249  {
250  lock (instances)
251  {
252  if (instances.ContainsKey(metadata.Id))
253  throw new InvalidOperationException("Instance already online!");
254  instances.Add(metadata.Id, instance);
255  }
256  }
257  catch
258  {
259  instance.Dispose();
260  throw;
261  }
262 
263  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
264  }
265 
267  #pragma warning disable CA1506 // TODO: Decomplexify
268  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
269  {
270  logger.LogInformation(assemblyInformationProvider.VersionString);
271 
272  try
273  {
274  CheckSystemCompatibility();
275  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
276  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
277  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
278  var dbInstances = databaseContext
279  .Instances
280  .AsQueryable()
281  .Where(x => x.Online.Value)
282  .Include(x => x.RepositorySettings)
283  .Include(x => x.ChatSettings)
284  .ThenInclude(x => x.Channels)
285  .Include(x => x.DreamDaemonSettings)
286  .ToAsyncEnumerable();
287  var tasks = new List<Task>();
288  await factoryStartup.ConfigureAwait(false);
289  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
290  await Task.WhenAll(tasks).ConfigureAwait(false);
291  logger.LogInformation("Server ready!");
292  readyTcs.SetResult(null);
293  }
294  catch (OperationCanceledException)
295  {
296  logger.LogInformation("Cancelled instance manager initialization!");
297  }
298  catch (Exception e)
299  {
300  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
301  try
302  {
303  await serverControl.Die(e).ConfigureAwait(false);
304  return;
305  }
306  catch (Exception e2)
307  {
308  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
309  }
310 
311  throw;
312  }
313  });
314  #pragma warning restore CA1506 // TODO: Decomplexify
315 
317  public async Task StopAsync(CancellationToken cancellationToken)
318  {
319  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
320  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
321  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
322 
323  // downgrade the db if necessary
324  if (downgradeVersion != null)
325  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
326  }
327 
329  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
330  {
331  downgradeVersion = updateVersion != null && updateVersion < assemblyInformationProvider.Version ? updateVersion : null;
332  return Task.CompletedTask;
333  }
334 
339  {
340  if (generalConfiguration.UseExperimentalWatchdog && !Debugger.IsAttached)
341  throw new InvalidOperationException(
342  "The experimental watchdog is currently non-functional! Please disable the '{nameof(generalConfiguration.UseExperimentalWatchdog)}' option in your configuration!");
343 
344  using var systemIdentity = systemIdentityFactory.GetCurrent();
345  if (!systemIdentity.CanCreateSymlinks)
346  throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!");
347  }
348 
350  public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
351  {
352  if (parameters == null)
353  throw new ArgumentNullException(nameof(parameters));
354 
355  IBridgeHandler bridgeHandler = null;
356  for (var i = 0; bridgeHandler == null && i < 30; ++i)
357  {
358  // There's a miniscule time period where we could potentially receive a bridge request and not have the registration ready when we launch DD
359  // This is a stopgap
360  Task delayTask = Task.CompletedTask;
361  lock (bridgeHandlers)
362  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
363  delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
364 
365  await delayTask.ConfigureAwait(false);
366  }
367 
368  if (bridgeHandler == null)
369  lock (bridgeHandlers)
370  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
371  {
372  logger.LogWarning("Recieved invalid bridge request with accees identifier: {0}", parameters.AccessIdentifier);
373  return null;
374  }
375 
376  return await bridgeHandler.ProcessBridgeRequest(parameters, cancellationToken).ConfigureAwait(false);
377  }
378 
381  {
382  if (bridgeHandler == null)
383  throw new ArgumentNullException(nameof(bridgeHandler));
384 
385  var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier;
386  lock (bridgeHandlers)
387  {
388  bridgeHandlers.Add(accessIdentifier, bridgeHandler);
389  logger.LogTrace("Registered bridge handler: {0}", accessIdentifier);
390  }
391 
392  return new BridgeRegistration(() =>
393  {
394  lock (bridgeHandlers)
395  {
396  bridgeHandlers.Remove(accessIdentifier);
397  logger.LogTrace("Unregistered bridge handler: {0}", accessIdentifier);
398  }
399  });
400  }
401  }
402 }
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the InstanceManager
readonly IServerControl serverControl
The IServerControl for the InstanceManager
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
readonly IDictionary< string, IBridgeHandler > bridgeHandlers
Map of DMApiParameters.AccessIdentifiers to their respective IBridgeHandlers.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
+
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the InstanceManager
bool disposed
If the InstanceManager has been Disposed
-
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
+
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the InstanceManager
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
-
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
+
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
-
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
+
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceManager.
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
@@ -126,7 +126,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
For interacting with the instance services
Definition: IInstance.cs:16
-
async Task StopAsync(CancellationToken cancellationToken)
+
async Task StopAsync(CancellationToken cancellationToken)
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance
@@ -147,14 +147,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IIOManager ioManager
The IIOManager for the InstanceManager
Interface for using filesystems
Definition: IIOManager.cs:11
-
void CheckSystemCompatibility()
Check we have a valid system identity.
+
void CheckSystemCompatibility()
Check we have a valid system identity.
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IAssemblyInformationProvider assemblyInformationProvider, IJobManager jobManager, IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< InstanceManager > logger)
Construct an InstanceManager
async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
Move an IInstance
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
Represents a registration of an interop session.
- +
DMApiParameters DMApiParameters
The DMApiParameters for the IBridgeHandler.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_instance_manager_client_8cs.html b/_instance_manager_client_8cs.html index 6b6c5b2a43..9764fef102 100644 --- a/_instance_manager_client_8cs.html +++ b/_instance_manager_client_8cs.html @@ -25,7 +25,7 @@
diff --git a/_instance_manager_client_8cs_source.html b/_instance_manager_client_8cs_source.html index d770429b2d..574776d983 100644 --- a/_instance_manager_client_8cs_source.html +++ b/_instance_manager_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_instance_manager_rights_8cs.html b/_instance_manager_rights_8cs.html index 98d8780aa7..a583137507 100644 --- a/_instance_manager_rights_8cs.html +++ b/_instance_manager_rights_8cs.html @@ -25,7 +25,7 @@ diff --git a/_instance_manager_rights_8cs_source.html b/_instance_manager_rights_8cs_source.html index d609d2ba84..4987dbf776 100644 --- a/_instance_manager_rights_8cs_source.html +++ b/_instance_manager_rights_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_instance_user_client_8cs.html b/_instance_user_client_8cs.html index 3f124887e0..21c4cb7d1c 100644 --- a/_instance_user_client_8cs.html +++ b/_instance_user_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_instance_user_client_8cs_source.html b/_instance_user_client_8cs_source.html index 18277183b6..2d43de87da 100644 --- a/_instance_user_client_8cs_source.html +++ b/_instance_user_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_instance_user_controller_8cs.html b/_instance_user_controller_8cs.html index d74df8dadd..538afd1fb9 100644 --- a/_instance_user_controller_8cs.html +++ b/_instance_user_controller_8cs.html @@ -25,7 +25,7 @@ diff --git a/_instance_user_controller_8cs_source.html b/_instance_user_controller_8cs_source.html index 7d22389e1a..2964d4a2b1 100644 --- a/_instance_user_controller_8cs_source.html +++ b/_instance_user_controller_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,8 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceUserController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
16 using Z.EntityFramework.Plus;
17 
18 namespace Tgstation.Server.Host.Controllers
19 {
23  [Route(Routes.InstanceUser)]
24  public sealed class InstanceUserController : ApiController
25  {
32  public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
33  { }
34 
40  IActionResult StandardModelChecks(Api.Models.InstanceUser model)
41  {
42  if (model == null)
43  throw new ArgumentNullException(nameof(model));
44 
45  if (!model.UserId.HasValue)
46  return BadRequest(new ErrorMessage(ErrorCode.UserMissingId));
47 
48  return null;
49  }
50 
58  [HttpPut]
59  [TgsAuthorize(InstanceUserRights.CreateUsers)]
60  [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)]
61  public async Task<IActionResult> Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
62  {
63  // Don't check the result as how can a new user have an ID
64  StandardModelChecks(model);
65 
66  var dbUser = new Models.InstanceUser
67  {
68  ByondRights = RightsHelper.Clamp(model.ByondRights ?? ByondRights.None),
69  ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? ChatBotRights.None),
70  ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? ConfigurationRights.None),
71  DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? DreamDaemonRights.None),
72  DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? DreamMakerRights.None),
73  RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? RepositoryRights.None),
74  InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? InstanceUserRights.None),
75  UserId = model.UserId,
76  InstanceId = Instance.Id
77  };
78 
79  DatabaseContext.InstanceUsers.Add(dbUser);
80 
81  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
82  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
83  }
84 
93  [HttpPost]
94  [TgsAuthorize(InstanceUserRights.WriteUsers)]
95  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
96  [ProducesResponseType(410)]
97  #pragma warning disable CA1506 // TODO: Decomplexify
98  public async Task<IActionResult> Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
99  {
100  var earlyOut = StandardModelChecks(model);
101  if (earlyOut != null)
102  return earlyOut;
103 
104  var originalUser = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == model.UserId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
105  if (originalUser == null)
106  return StatusCode((int)HttpStatusCode.Gone);
107 
108  originalUser.ByondRights = RightsHelper.Clamp(model.ByondRights ?? originalUser.ByondRights.Value);
109  originalUser.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalUser.RepositoryRights.Value);
110  originalUser.InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? originalUser.InstanceUserRights.Value);
111  originalUser.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalUser.ChatBotRights.Value);
112  originalUser.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalUser.ConfigurationRights.Value);
113  originalUser.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalUser.DreamDaemonRights.Value);
114  originalUser.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalUser.DreamMakerRights.Value);
115 
116  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
117  return Json(originalUser.UserId == AuthenticationContext.User.Id || (AuthenticationContext.GetRight(RightsType.InstanceUser) & (ulong)InstanceUserRights.ReadUsers) != 0 ? originalUser.ToApi() : new Api.Models.InstanceUser
118  {
119  UserId = originalUser.UserId
120  });
121  }
122 #pragma warning restore CA1506
123  [HttpGet]
129  [TgsAuthorize]
130  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
131  public IActionResult Read() => Json(AuthenticationContext.InstanceUser.ToApi());
132 
139  [HttpGet(Routes.List)]
140  [TgsAuthorize(InstanceUserRights.ReadUsers)]
141  [ProducesResponseType(typeof(IEnumerable<Api.Models.InstanceUser>), 200)]
142  public async Task<IActionResult> List(CancellationToken cancellationToken)
143  {
144  var users = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).ToListAsync(cancellationToken).ConfigureAwait(false);
145  return Json(users.Select(x => x.ToApi()));
146  }
147 
156  [HttpGet("{id}")]
157  [TgsAuthorize(InstanceUserRights.ReadUsers)]
158  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
159  [ProducesResponseType(410)]
160  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
161  {
162  // this functions as userId
163  var user = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
164  if (user == default)
165  return StatusCode((int)HttpStatusCode.Gone);
166  return Json(user.ToApi());
167  }
168 
176  [HttpDelete("{id}")]
177  [TgsAuthorize(InstanceUserRights.WriteUsers)]
178  [ProducesResponseType(204)]
179  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
180  {
181  await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).DeleteAsync(cancellationToken).ConfigureAwait(false);
182  return NoContent();
183  }
184  }
185 }
InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger< InstanceUserController > logger)
Construct a UserController
-
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
16 using Z.EntityFramework.Plus;
17 
18 namespace Tgstation.Server.Host.Controllers
19 {
23  [Route(Routes.InstanceUser)]
24  public sealed class InstanceUserController : ApiController
25  {
32  public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
33  { }
34 
40  IActionResult StandardModelChecks(Api.Models.InstanceUser model)
41  {
42  if (model == null)
43  throw new ArgumentNullException(nameof(model));
44 
45  if (!model.UserId.HasValue)
46  return BadRequest(new ErrorMessage(ErrorCode.UserMissingId));
47 
48  return null;
49  }
50 
58  [HttpPut]
59  [TgsAuthorize(InstanceUserRights.CreateUsers)]
60  [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)]
61  public async Task<IActionResult> Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
62  {
63  // Don't check the result as how can a new user have an ID
64  StandardModelChecks(model);
65 
66  var dbUser = new Models.InstanceUser
67  {
68  ByondRights = RightsHelper.Clamp(model.ByondRights ?? ByondRights.None),
69  ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? ChatBotRights.None),
70  ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? ConfigurationRights.None),
71  DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? DreamDaemonRights.None),
72  DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? DreamMakerRights.None),
73  RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? RepositoryRights.None),
74  InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? InstanceUserRights.None),
75  UserId = model.UserId,
76  InstanceId = Instance.Id
77  };
78 
79  DatabaseContext.InstanceUsers.Add(dbUser);
80 
81  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
82  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
83  }
84 
93  [HttpPost]
94  [TgsAuthorize(InstanceUserRights.WriteUsers)]
95  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
96  [ProducesResponseType(410)]
97  #pragma warning disable CA1506 // TODO: Decomplexify
98  public async Task<IActionResult> Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
99  {
100  var earlyOut = StandardModelChecks(model);
101  if (earlyOut != null)
102  return earlyOut;
103 
104  var originalUser = await DatabaseContext
105  .Instances
106  .AsQueryable()
107  .Where(x => x.Id == Instance.Id)
108  .SelectMany(x => x.InstanceUsers)
109  .Where(x => x.UserId == model.UserId)
110  .FirstOrDefaultAsync(cancellationToken)
111  .ConfigureAwait(false);
112  if (originalUser == null)
113  return StatusCode((int)HttpStatusCode.Gone);
114 
115  originalUser.ByondRights = RightsHelper.Clamp(model.ByondRights ?? originalUser.ByondRights.Value);
116  originalUser.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalUser.RepositoryRights.Value);
117  originalUser.InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? originalUser.InstanceUserRights.Value);
118  originalUser.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalUser.ChatBotRights.Value);
119  originalUser.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalUser.ConfigurationRights.Value);
120  originalUser.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalUser.DreamDaemonRights.Value);
121  originalUser.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalUser.DreamMakerRights.Value);
122 
123  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
124  return Json(originalUser.UserId == AuthenticationContext.User.Id || (AuthenticationContext.GetRight(RightsType.InstanceUser) & (ulong)InstanceUserRights.ReadUsers) != 0 ? originalUser.ToApi() : new Api.Models.InstanceUser
125  {
126  UserId = originalUser.UserId
127  });
128  }
129 #pragma warning restore CA1506
130  [HttpGet]
136  [TgsAuthorize]
137  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
138  public IActionResult Read() => Json(AuthenticationContext.InstanceUser.ToApi());
139 
146  [HttpGet(Routes.List)]
147  [TgsAuthorize(InstanceUserRights.ReadUsers)]
148  [ProducesResponseType(typeof(IEnumerable<Api.Models.InstanceUser>), 200)]
149  public async Task<IActionResult> List(CancellationToken cancellationToken)
150  {
151  var users = await DatabaseContext
152  .Instances
153  .AsQueryable()
154  .Where(x => x.Id == Instance.Id)
155  .SelectMany(x => x.InstanceUsers)
156  .ToListAsync(cancellationToken)
157  .ConfigureAwait(false);
158  return Json(users.Select(x => x.ToApi()));
159  }
160 
169  [HttpGet("{id}")]
170  [TgsAuthorize(InstanceUserRights.ReadUsers)]
171  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
172  [ProducesResponseType(410)]
173  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
174  {
175  // this functions as userId
176  var user = await DatabaseContext
177  .Instances
178  .AsQueryable()
179  .Where(x => x.Id == Instance.Id)
180  .SelectMany(x => x.InstanceUsers)
181  .Where(x => x.UserId == id)
182  .FirstOrDefaultAsync(cancellationToken)
183  .ConfigureAwait(false);
184  if (user == default)
185  return StatusCode((int)HttpStatusCode.Gone);
186  return Json(user.ToApi());
187  }
188 
196  [HttpDelete("{id}")]
197  [TgsAuthorize(InstanceUserRights.WriteUsers)]
198  [ProducesResponseType(204)]
199  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
200  {
201  await DatabaseContext
202  .Instances
203  .AsQueryable()
204  .Where(x => x.Id == Instance.Id)
205  .SelectMany(x => x.InstanceUsers)
206  .Where(x => x.UserId == id)
207  .DeleteAsync(cancellationToken)
208  .ConfigureAwait(false);
209  return NoContent();
210  }
211  }
212 }
InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger< InstanceUserController > logger)
Construct a UserController
ConfigurationRights
Rights for Models.ConfigurationFile
@@ -104,28 +103,29 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
-
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Delete an Api.Models.InstanceUser.
+
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Delete an Api.Models.InstanceUser.
InstanceUserRights
Rights for an Models.Instance
const string InstanceUser
The Models.InstanceUser controller
Definition: Routes.cs:64
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
+
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
async Task< IActionResult > Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
Create an Api.Models.InstanceUser.
Routes to a server actions
Definition: Routes.cs:9
DreamDaemonRights
Rights for Models.DreamDaemon
-
DbSet< Instance > Instances
The Instances in the DatabaseContext<TParentContext>.
+
DbSet< Instance > Instances
The Instances in the DatabaseContext.
DreamMakerRights
Rights for Models.DreamMaker
+
DbSet< InstanceUser > InstanceUsers
The InstanceUsers in the DatabaseContext.
RepositoryRights
Rights for a Models.Repository
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
-
DbSet< InstanceUser > InstanceUsers
The InstanceUsers in the DatabaseContext<TParentContext>.
long Id
The ID of the User
Definition: User.cs:16
Metadata about a server instance
Definition: Instance.cs:9
@@ -133,15 +133,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
const string List
The postfix for list operations
Definition: Routes.cs:84
User can read all chat settings except Models.Internal.ChatBot.ConnectionString
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
-
async Task< IActionResult > List(CancellationToken cancellationToken)
Lists Api.Models.InstanceUsers for the instance.
+
async Task< IActionResult > List(CancellationToken cancellationToken)
Lists Api.Models.InstanceUsers for the instance.
async Task< IActionResult > Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
Update the permissions for an Api.Models.InstanceUser.
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
IActionResult StandardModelChecks(Api.Models.InstanceUser model)
Checks a model for errors.
- + -
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Gets a specific Api.Models.InstanceUser.
+
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Gets a specific Api.Models.InstanceUser.
diff --git a/_instance_user_rights_8cs_source.html b/_instance_user_rights_8cs_source.html index 10aab1ffe1..f7e4d1167a 100644 --- a/_instance_user_rights_8cs_source.html +++ b/_instance_user_rights_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_insufficient_permissions_exception_8cs.html b/_insufficient_permissions_exception_8cs.html index 581a2a4870..a13ae64e99 100644 --- a/_insufficient_permissions_exception_8cs.html +++ b/_insufficient_permissions_exception_8cs.html @@ -25,7 +25,7 @@ diff --git a/_insufficient_permissions_exception_8cs_source.html b/_insufficient_permissions_exception_8cs_source.html index e563654575..eda3d10a88 100644 --- a/_insufficient_permissions_exception_8cs_source.html +++ b/_insufficient_permissions_exception_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_internal_2_server_information_8cs.html b/_internal_2_server_information_8cs.html index 7bba7dd3a0..572877d40c 100644 --- a/_internal_2_server_information_8cs.html +++ b/_internal_2_server_information_8cs.html @@ -25,7 +25,7 @@ diff --git a/_internal_2_server_information_8cs_source.html b/_internal_2_server_information_8cs_source.html index 719d3f2a35..d78cab5169 100644 --- a/_internal_2_server_information_8cs_source.html +++ b/_internal_2_server_information_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_irc_connection_string_builder_8cs.html b/_irc_connection_string_builder_8cs.html index 2e6618fe9a..6ff370ffe6 100644 --- a/_irc_connection_string_builder_8cs.html +++ b/_irc_connection_string_builder_8cs.html @@ -25,7 +25,7 @@ diff --git a/_irc_connection_string_builder_8cs_source.html b/_irc_connection_string_builder_8cs_source.html index 194926d950..4097ad6b55 100644 --- a/_irc_connection_string_builder_8cs_source.html +++ b/_irc_connection_string_builder_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_irc_password_type_8cs.html b/_irc_password_type_8cs.html index 64c0aa1e80..94112952fd 100644 --- a/_irc_password_type_8cs.html +++ b/_irc_password_type_8cs.html @@ -25,7 +25,7 @@ diff --git a/_irc_password_type_8cs_source.html b/_irc_password_type_8cs_source.html index 2e36d929da..4235f61b16 100644 --- a/_irc_password_type_8cs_source.html +++ b/_irc_password_type_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_irc_provider_8cs.html b/_irc_provider_8cs.html index 9412d7fb86..e6dfd8b073 100644 --- a/_irc_provider_8cs.html +++ b/_irc_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_irc_provider_8cs_source.html b/_irc_provider_8cs_source.html index 448027aefb..a78ea3d584 100644 --- a/_irc_provider_8cs_source.html +++ b/_irc_provider_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IrcProvider.cs
-Go to the documentation of this file.
1 using Meebey.SmartIrc4net;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Components.Chat.Providers
16 {
20  sealed class IrcProvider : Provider
21  {
22  const int TimeoutSeconds = 5;
23 
25  public override bool Connected => client.IsConnected;
26 
28  public override string BotMention => client.Nickname;
29 
34 
38  readonly IrcFeatures client;
39 
43  readonly string address;
44 
48  readonly ushort port;
49 
53  readonly string nickname;
54 
58  readonly string password;
59 
64 
68  readonly Dictionary<ulong, string> channelIdMap;
69 
73  readonly Dictionary<ulong, string> queryChannelIdMap;
74 
79 
83  Task listenTask;
84 
89 
103  public IrcProvider(
104  IAssemblyInformationProvider assemblyInformationProvider,
105  IAsyncDelayer asyncDelayer,
106  ILogger<IrcProvider> logger,
107  string address,
108  ushort port,
109  string nickname,
110  string password,
111  IrcPasswordType? passwordType,
112  uint reconnectInterval,
113  bool useSsl)
114  : base(logger, reconnectInterval)
115  {
116  if (assemblyInformationProvider == null)
117  throw new ArgumentNullException(nameof(assemblyInformationProvider));
118  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
119 
120  this.address = address ?? throw new ArgumentNullException(nameof(address));
121  this.port = port;
122  this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
123 
124  if (passwordType.HasValue && password == null)
125  throw new ArgumentNullException(nameof(password));
126 
127  if (password != null && !passwordType.HasValue)
128  throw new ArgumentNullException(nameof(passwordType));
129 
130  this.password = password;
131  this.passwordType = passwordType;
132 
133  client = new IrcFeatures
134  {
135  SupportNonRfc = true,
136  CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
137  AutoRejoin = true,
138  AutoRejoinOnKick = true,
139  AutoRelogin = true,
140  AutoRetry = true,
141  AutoRetryLimit = TimeoutSeconds,
142  AutoRetryDelay = TimeoutSeconds,
143  ActiveChannelSyncing = true,
144  AutoNickHandling = true,
145  CtcpVersion = assemblyInformationProvider.VersionString,
146  UseSsl = useSsl
147  };
148  if (useSsl)
149  client.ValidateServerCertificate = true; // dunno if it defaults to that or what
150 
151  client.OnChannelMessage += Client_OnChannelMessage;
152  client.OnQueryMessage += Client_OnQueryMessage;
153 
154  channelIdMap = new Dictionary<ulong, string>();
155  queryChannelIdMap = new Dictionary<ulong, string>();
156  channelIdCounter = 1;
157  disconnecting = false;
158  }
159 
161  public override void Dispose()
162  {
163  if (Connected)
164  {
165  disconnecting = true;
166  client.Disconnect(); // just closes the socket
167  }
168 
169  base.Dispose();
170  }
171 
177  void HandleMessage(IrcEventArgs e, bool isPrivate)
178  {
179  if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
180  return;
181 
182  var username = e.Data.Nick;
183  var channelName = isPrivate ? username : e.Data.Channel;
184 
185  ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
186  {
187  ulong? resultId = null;
188  if (!dicToCheck.Any(x =>
189  {
190  if (x.Value != channelName)
191  return false;
192  resultId = x.Key;
193  return true;
194  }))
195  {
196  resultId = channelIdCounter++;
197  dicToCheck.Add(resultId.Value, channelName);
198  if (dicToCheck == queryChannelIdMap)
199  channelIdMap.Add(resultId.Value, null);
200  }
201 
202  return resultId.Value;
203  }
204 
205  ulong userId, channelId;
206  lock (client)
207  {
208  userId = MapAndGetChannelId(queryChannelIdMap);
209  channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
210  }
211 
212  var message = new Message
213  {
214  Content = e.Data.Message,
215  User = new ChatUser
216  {
217  Channel = new ChannelRepresentation
218  {
219  ConnectionName = address,
220  FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
221  RealId = channelId,
222  IsPrivateChannel = isPrivate
223 
224  // isAdmin and Tag populated by manager
225  },
226  FriendlyName = username,
227  RealId = userId,
228  Mention = username
229  }
230  };
231 
232  EnqueueMessage(message);
233  }
234 
240  void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
241 
247  void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
248 
250  public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
251  {
252  disconnecting = false;
253  lock (client)
254  try
255  {
256  client.Connect(address, port);
257 
258  cancellationToken.ThrowIfCancellationRequested();
259 
260  if (passwordType == IrcPasswordType.Server)
261  client.Login(nickname, nickname, 0, nickname, password);
262  else
263  {
264  if (passwordType == IrcPasswordType.Sasl)
265  {
266  client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else
267  cancellationToken.ThrowIfCancellationRequested();
268  }
269 
270  client.Login(nickname, nickname, 0, nickname);
271  }
272 
273  if (passwordType == IrcPasswordType.NickServ)
274  {
275  cancellationToken.ThrowIfCancellationRequested();
276  client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
277  }
278  else if (passwordType == IrcPasswordType.Sasl)
279  {
280  // wait for the sasl ack or timeout
281  var recievedAck = false;
282  var recievedPlus = false;
283  client.OnReadLine += (sender, e) =>
284  {
285  if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal))
286  recievedAck = true;
287  else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal))
288  recievedPlus = true;
289  };
290 
291  var startTime = DateTimeOffset.Now;
292  var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
293  cancellationToken.ThrowIfCancellationRequested();
294 
295  var listenTimeSpan = TimeSpan.FromMilliseconds(10);
296  for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
297  client.Listen(false);
298 
299  client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
300  cancellationToken.ThrowIfCancellationRequested();
301 
302  for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
303  client.Listen(false);
304 
305  // Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
306  var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password);
307  var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
308  var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64);
309  var chars = authLine.ToCharArray();
310  client.WriteLine(authLine, Priority.Critical);
311 
312  cancellationToken.ThrowIfCancellationRequested();
313  client.WriteLine("CAP END", Priority.Critical);
314  }
315 
316  client.Listen(false);
317 
318  listenTask = Task.Factory.StartNew(() =>
319  {
320  while (!disconnecting && client.IsConnected && client.Nickname != nickname)
321  {
322  client.ListenOnce(true);
323  if (disconnecting || !client.IsConnected)
324  break;
325  client.Listen(false);
326 
327  // ensure we have the correct nick
328  if (client.GetIrcUser(nickname) == null)
329  client.RfcNick(nickname);
330  }
331 
332  client.Listen();
333  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
334  }
335  catch (OperationCanceledException)
336  {
337  throw;
338  }
339  catch (Exception e)
340  {
341  Logger.LogWarning("Unable to connect to IRC: {0}", e);
342  return false;
343  }
344 
345  return true;
346  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
347 
349  public override async Task Disconnect(CancellationToken cancellationToken)
350  {
351  if (!Connected)
352  return;
353  try
354  {
355  await Task.Factory.StartNew(() =>
356  {
357  try
358  {
359  client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through
360  }
361  catch (Exception e)
362  {
363  Logger.LogWarning("Error quitting IRC: {0}", e);
364  }
365  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
366  Dispose();
367  await listenTask.ConfigureAwait(false);
368  }
369  catch (OperationCanceledException)
370  {
371  throw;
372  }
373  catch (Exception e)
374  {
375  Logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
376  }
377  }
378 
380  public override Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(
381  IEnumerable<ChatChannel> channels,
382  CancellationToken cancellationToken)
383  => Task.Factory.StartNew(() =>
384  {
385  if (channels.Any(x => x.IrcChannel == null))
386  throw new InvalidOperationException("ChatChannel missing IrcChannel!");
387  lock (client)
388  {
389  var channelsWithKeys = new Dictionary<string, string>();
390  var hs = new HashSet<string>(); // for unique inserts
391  foreach (var channel in channels)
392  {
393  var name = channel.GetIrcChannelName();
394  var key = channel.GetIrcChannelKey();
395  if (hs.Add(name) && key != null)
396  channelsWithKeys.Add(name, key);
397  }
398 
399  var toPart = new List<string>();
400  foreach (var activeChannel in client.JoinedChannels)
401  if (!hs.Remove(activeChannel))
402  toPart.Add(activeChannel);
403 
404  foreach (var channelToLeave in toPart)
405  client.RfcPart(channelToLeave, "Pretty nice abscond!");
406  foreach (var channelToJoin in hs)
407  if (channelsWithKeys.TryGetValue(channelToJoin, out var key))
408  client.RfcJoin(channelToJoin, key);
409  else
410  client.RfcJoin(channelToJoin);
411 
412  return (IReadOnlyCollection<ChannelRepresentation>)channels
413  .Select(x =>
414  {
415  var channelName = x.GetIrcChannelName();
416  ulong? id = null;
417  if (!channelIdMap.Any(y =>
418  {
419  if (y.Value != channelName)
420  return false;
421  id = y.Key;
422  return true;
423  }))
424  {
425  id = channelIdCounter++;
426  channelIdMap.Add(id.Value, channelName);
427  }
428 
429  return new ChannelRepresentation
430  {
431  RealId = id.Value,
432  IsAdminChannel = x.IsAdminChannel == true,
433  ConnectionName = address,
434  FriendlyName = channelIdMap[id.Value],
435  IsPrivateChannel = false,
436  Tag = x.Tag
437  };
438  })
439  .ToList();
440  }
441  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
442 
444  public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
445  {
446  // IRC doesn't allow newlines
447  message = String.Concat(
448  message
449  .Where(x => x != '\r')
450  .Select(x => x == '\n' ? '|' : x));
451 
452  var channelName = channelIdMap[channelId];
453  SendType sendType;
454  if (channelName == null)
455  {
456  channelName = queryChannelIdMap[channelId];
457  sendType = SendType.Notice;
458  }
459  else
460  sendType = SendType.Message;
461  try
462  {
463  client.SendMessage(sendType, channelName, message);
464  }
465  catch (Exception e)
466  {
467  Logger.LogWarning("Unable to send to channel: {0}", e);
468  }
469  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
470  }
471 }
ulong channelIdCounter
Id counter for channelIdMap
Definition: IrcProvider.cs:78
+Go to the documentation of this file.
1 using Meebey.SmartIrc4net;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Components.Chat.Providers
16 {
20  sealed class IrcProvider : Provider
21  {
22  const int TimeoutSeconds = 5;
23 
25  public override bool Connected => client.IsConnected;
26 
28  public override string BotMention => client.Nickname;
29 
34 
38  readonly IrcFeatures client;
39 
43  readonly string address;
44 
48  readonly ushort port;
49 
53  readonly string nickname;
54 
58  readonly string password;
59 
64 
68  readonly Dictionary<ulong, string> channelIdMap;
69 
73  readonly Dictionary<ulong, string> queryChannelIdMap;
74 
79 
83  Task listenTask;
84 
89 
103  public IrcProvider(
104  IAssemblyInformationProvider assemblyInformationProvider,
105  IAsyncDelayer asyncDelayer,
106  ILogger<IrcProvider> logger,
107  string address,
108  ushort port,
109  string nickname,
110  string password,
111  IrcPasswordType? passwordType,
112  uint reconnectInterval,
113  bool useSsl)
114  : base(logger, reconnectInterval)
115  {
116  if (assemblyInformationProvider == null)
117  throw new ArgumentNullException(nameof(assemblyInformationProvider));
118  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
119 
120  this.address = address ?? throw new ArgumentNullException(nameof(address));
121  this.port = port;
122  this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
123 
124  if (passwordType.HasValue && password == null)
125  throw new ArgumentNullException(nameof(password));
126 
127  if (password != null && !passwordType.HasValue)
128  throw new ArgumentNullException(nameof(passwordType));
129 
130  this.password = password;
131  this.passwordType = passwordType;
132 
133  client = new IrcFeatures
134  {
135  SupportNonRfc = true,
136  CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
137  AutoRejoin = true,
138  AutoRejoinOnKick = true,
139  AutoRelogin = true,
140  AutoRetry = true,
141  AutoRetryLimit = TimeoutSeconds,
142  AutoRetryDelay = TimeoutSeconds,
143  ActiveChannelSyncing = true,
144  AutoNickHandling = true,
145  CtcpVersion = assemblyInformationProvider.VersionString,
146  UseSsl = useSsl
147  };
148  if (useSsl)
149  client.ValidateServerCertificate = true; // dunno if it defaults to that or what
150 
151  client.OnChannelMessage += Client_OnChannelMessage;
152  client.OnQueryMessage += Client_OnQueryMessage;
153 
154  channelIdMap = new Dictionary<ulong, string>();
155  queryChannelIdMap = new Dictionary<ulong, string>();
156  channelIdCounter = 1;
157  disconnecting = false;
158  }
159 
161  public override void Dispose()
162  {
163  if (Connected)
164  {
165  disconnecting = true;
166  client.Disconnect(); // just closes the socket
167  }
168 
169  base.Dispose();
170  }
171 
177  void HandleMessage(IrcEventArgs e, bool isPrivate)
178  {
179  if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
180  return;
181 
182  var username = e.Data.Nick;
183  var channelName = isPrivate ? username : e.Data.Channel;
184 
185  ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
186  {
187  ulong? resultId = null;
188  if (!dicToCheck.Any(x =>
189  {
190  if (x.Value != channelName)
191  return false;
192  resultId = x.Key;
193  return true;
194  }))
195  {
196  resultId = channelIdCounter++;
197  dicToCheck.Add(resultId.Value, channelName);
198  if (dicToCheck == queryChannelIdMap)
199  channelIdMap.Add(resultId.Value, null);
200  }
201 
202  return resultId.Value;
203  }
204 
205  ulong userId, channelId;
206  lock (client)
207  {
208  userId = MapAndGetChannelId(queryChannelIdMap);
209  channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
210  }
211 
212  var message = new Message
213  {
214  Content = e.Data.Message,
215  User = new ChatUser
216  {
217  Channel = new ChannelRepresentation
218  {
219  ConnectionName = address,
220  FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
221  RealId = channelId,
222  IsPrivateChannel = isPrivate
223 
224  // isAdmin and Tag populated by manager
225  },
226  FriendlyName = username,
227  RealId = userId,
228  Mention = username
229  }
230  };
231 
232  EnqueueMessage(message);
233  }
234 
240  void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
241 
247  void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
248 
250  public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
251  {
252  disconnecting = false;
253  lock (client)
254  try
255  {
256  client.Connect(address, port);
257 
258  cancellationToken.ThrowIfCancellationRequested();
259 
260  if (passwordType == IrcPasswordType.Server)
261  client.Login(nickname, nickname, 0, nickname, password);
262  else
263  {
264  if (passwordType == IrcPasswordType.Sasl)
265  {
266  client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else
267  cancellationToken.ThrowIfCancellationRequested();
268  }
269 
270  client.Login(nickname, nickname, 0, nickname);
271  }
272 
273  if (passwordType == IrcPasswordType.NickServ)
274  {
275  cancellationToken.ThrowIfCancellationRequested();
276  client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
277  }
278  else if (passwordType == IrcPasswordType.Sasl)
279  {
280  // wait for the sasl ack or timeout
281  var recievedAck = false;
282  var recievedPlus = false;
283  client.OnReadLine += (sender, e) =>
284  {
285  if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal))
286  recievedAck = true;
287  else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal))
288  recievedPlus = true;
289  };
290 
291  var startTime = DateTimeOffset.Now;
292  var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
293  cancellationToken.ThrowIfCancellationRequested();
294 
295  var listenTimeSpan = TimeSpan.FromMilliseconds(10);
296  for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
297  client.Listen(false);
298 
299  client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
300  cancellationToken.ThrowIfCancellationRequested();
301 
302  for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
303  client.Listen(false);
304 
305  // Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
306  var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password);
307  var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
308  var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64);
309  var chars = authLine.ToCharArray();
310  client.WriteLine(authLine, Priority.Critical);
311 
312  cancellationToken.ThrowIfCancellationRequested();
313  client.WriteLine("CAP END", Priority.Critical);
314  }
315 
316  client.Listen(false);
317 
318  listenTask = Task.Factory.StartNew(() =>
319  {
320  while (!disconnecting && client.IsConnected && client.Nickname != nickname)
321  {
322  client.ListenOnce(true);
323  if (disconnecting || !client.IsConnected)
324  break;
325  client.Listen(false);
326 
327  // ensure we have the correct nick
328  if (client.GetIrcUser(nickname) == null)
329  client.RfcNick(nickname);
330  }
331 
332  client.Listen();
333  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
334  }
335  catch (OperationCanceledException)
336  {
337  throw;
338  }
339  catch (Exception e)
340  {
341  Logger.LogWarning("Unable to connect to IRC: {0}", e);
342  return false;
343  }
344 
345  return true;
346  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
347 
349  public override async Task Disconnect(CancellationToken cancellationToken)
350  {
351  if (!Connected)
352  return;
353  try
354  {
355  await Task.Factory.StartNew(() =>
356  {
357  try
358  {
359  client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through
360  }
361  catch (Exception e)
362  {
363  Logger.LogWarning("Error quitting IRC: {0}", e);
364  }
365  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
366  Dispose();
367  await listenTask.ConfigureAwait(false);
368  }
369  catch (OperationCanceledException)
370  {
371  throw;
372  }
373  catch (Exception e)
374  {
375  Logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
376  }
377  }
378 
380  public override Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(
381  IEnumerable<ChatChannel> channels,
382  CancellationToken cancellationToken)
383  => Task.Factory.StartNew(() =>
384  {
385  if (channels.Any(x => x.IrcChannel == null))
386  throw new InvalidOperationException("ChatChannel missing IrcChannel!");
387  lock (client)
388  {
389  var channelsWithKeys = new Dictionary<string, string>();
390  var hs = new HashSet<string>(); // for unique inserts
391  foreach (var channel in channels)
392  {
393  var name = channel.GetIrcChannelName();
394  var key = channel.GetIrcChannelKey();
395  if (hs.Add(name) && key != null)
396  channelsWithKeys.Add(name, key);
397  }
398 
399  var toPart = new List<string>();
400  foreach (var activeChannel in client.JoinedChannels)
401  if (!hs.Remove(activeChannel))
402  toPart.Add(activeChannel);
403 
404  foreach (var channelToLeave in toPart)
405  client.RfcPart(channelToLeave, "Pretty nice abscond!");
406  foreach (var channelToJoin in hs)
407  if (channelsWithKeys.TryGetValue(channelToJoin, out var key))
408  client.RfcJoin(channelToJoin, key);
409  else
410  client.RfcJoin(channelToJoin);
411 
412  return (IReadOnlyCollection<ChannelRepresentation>)channels
413  .Select(x =>
414  {
415  var channelName = x.GetIrcChannelName();
416  ulong? id = null;
417  if (!channelIdMap.Any(y =>
418  {
419  if (y.Value != channelName)
420  return false;
421  id = y.Key;
422  return true;
423  }))
424  {
425  id = channelIdCounter++;
426  channelIdMap.Add(id.Value, channelName);
427  }
428 
429  return new ChannelRepresentation
430  {
431  RealId = id.Value,
432  IsAdminChannel = x.IsAdminChannel == true,
433  ConnectionName = address,
434  FriendlyName = channelIdMap[id.Value],
435  IsPrivateChannel = false,
436  Tag = x.Tag
437  };
438  })
439  .ToList();
440  }
441  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
442 
444  public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
445  {
446  // IRC doesn't allow newlines
447  message = String.Concat(
448  message
449  .Where(x => x != '\r')
450  .Select(x => x == '\n' ? '|' : x));
451 
452  var channelName = channelIdMap[channelId];
453  SendType sendType;
454  if (channelName == null)
455  {
456  channelName = queryChannelIdMap[channelId];
457  sendType = SendType.Notice;
458  }
459  else
460  sendType = SendType.Message;
461  try
462  {
463  client.SendMessage(sendType, channelName, message);
464  }
465  catch (Exception e)
466  {
467  Logger.LogWarning("Unable to send to channel: {0}", e);
468  }
469  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
470 
472  public override async Task<Func<string, string, Task>> SendUpdateMessage(
473  Models.RevisionInformation revisionInformation,
474  Version byondVersion,
475  DateTimeOffset? estimatedCompletionTime,
476  string gitHubOwner,
477  string gitHubRepo,
478  ulong channelId,
479  bool localCommitPushed,
480  CancellationToken cancellationToken)
481  {
482  var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
483  string remoteCommitInsert;
484  if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
485  {
486  commitInsert = String.Format(CultureInfo.InvariantCulture, localCommitPushed ? "^{0}" : "{0}", commitInsert);
487  remoteCommitInsert = String.Empty;
488  }
489  else
490  remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
491 
492  var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
493  String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
494  {
495  var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
496  if (x.Comment != null)
497  result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
498  return result;
499  })));
500 
501  await SendMessage(
502  channelId,
503  String.Format(
504  CultureInfo.InvariantCulture,
505  "DM: Deploying revision: {0}{1}{2} BYOND Version: {3}{4}",
506  commitInsert,
507  testmergeInsert,
508  remoteCommitInsert,
509  byondVersion,
510  estimatedCompletionTime.HasValue
511  ? $" ETA: {estimatedCompletionTime - DateTimeOffset.Now}"
512  : String.Empty),
513  cancellationToken).ConfigureAwait(false);
514 
515  return (errorMessage, dreamMakerOutput) => SendMessage(
516  channelId,
517  $"DM: Deployment {(errorMessage == null ? "complete" : "failed")}!",
518  cancellationToken);
519  }
520  }
521 }
ulong channelIdCounter
Id counter for channelIdMap
Definition: IrcProvider.cs:78
IrcProvider(IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, ILogger< IrcProvider > logger, string address, ushort port, string nickname, string password, IrcPasswordType?passwordType, uint reconnectInterval, bool useSsl)
Construct an IrcProvider
Definition: IrcProvider.cs:103
readonly Dictionary< ulong, string > queryChannelIdMap
Map of ChannelRepresentation.RealIds to query users
Definition: IrcProvider.cs:73
@@ -105,7 +105,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
bool IsAdminChannel
If this is considered a channel for admin commands
readonly string address
Address of the server to connect to
Definition: IrcProvider.cs:43
override async Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
Definition: IrcProvider.cs:349
@@ -124,6 +124,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); +
override async Task< Func< string, string, Task > > SendUpdateMessage(Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, ulong channelId, bool localCommitPushed, CancellationToken cancellationToken)
Definition: IrcProvider.cs:472
readonly IrcPasswordType passwordType
The IrcPasswordType of password
Definition: IrcProvider.cs:63
IrcPasswordType
Represents the type of a password for a ChatProvider.Irc
diff --git a/_job_controller_8cs.html b/_job_controller_8cs.html index d182d21bf6..915b3df607 100644 --- a/_job_controller_8cs.html +++ b/_job_controller_8cs.html @@ -25,7 +25,7 @@
diff --git a/_job_controller_8cs_source.html b/_job_controller_8cs_source.html index 125d863da6..2d628177fb 100644 --- a/_job_controller_8cs_source.html +++ b/_job_controller_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,21 +94,21 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JobController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.Jobs)]
22  public sealed class JobController : ApiController
23  {
28 
36  public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger<JobController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
37  {
38  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
39  }
40 
47  [HttpGet]
48  [TgsAuthorize]
49  [ProducesResponseType(typeof(IEnumerable<Api.Models.Job>), 200)]
50  public async Task<IActionResult> Read(CancellationToken cancellationToken)
51  {
52  var result = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue).OrderByDescending(x => x.StartedAt).ToListAsync(cancellationToken).ConfigureAwait(false);
53  return Json(result.Select(x => x.ToApi()));
54  }
55 
62  [HttpGet(Routes.List)]
63  [TgsAuthorize]
64  [ProducesResponseType(typeof(List<Api.Models.EntityId>), 200)]
65  public async Task<IActionResult> List(CancellationToken cancellationToken)
66  {
67  // you KNOW this will need pagination eventually right?
68  var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.EntityId
69  {
70  Id = x.Id
71  }).ToListAsync(cancellationToken).ConfigureAwait(false);
72  return Json(jobs);
73  }
74 
84  [HttpDelete("{id}")]
85  [TgsAuthorize]
86  [ProducesResponseType(typeof(Api.Models.Job), 202)]
87  [ProducesResponseType(404)]
88  [ProducesResponseType(410)]
89  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
90  {
91  // don't care if an instance post or not at this point
92  var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
93  if (job == default(Job))
94  return NotFound();
95 
96  if (job.StoppedAt != null)
97  return StatusCode((int)HttpStatusCode.Gone);
98 
99  if (job.CancelRight.HasValue && job.CancelRightsType.HasValue && (AuthenticationContext.GetRight(job.CancelRightsType.Value) & job.CancelRight.Value) == 0)
100  return Forbid();
101 
102  var updatedJob = await jobManager.CancelJob(job, AuthenticationContext.User, false, cancellationToken).ConfigureAwait(false);
103  return updatedJob != null ? (IActionResult)Accepted(updatedJob.ToApi()) : StatusCode((int)HttpStatusCode.Gone);
104  }
105 
114  [HttpGet("{id}")]
115  [TgsAuthorize]
116  [ProducesResponseType(typeof(Api.Models.Job), 200)]
117  [ProducesResponseType(404)]
118  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
119  {
120  var job = await DatabaseContext.Jobs.Where(x => x.Id == id && x.Instance.Id == Instance.Id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
121  if (job == default)
122  return NotFound();
123  var api = job.ToApi();
124  api.Progress = jobManager.JobProgress(job);
125  return Json(api);
126  }
127  }
128 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.Jobs)]
22  public sealed class JobController : ApiController
23  {
28 
36  public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger<JobController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
37  {
38  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
39  }
40 
47  [HttpGet]
48  [TgsAuthorize]
49  [ProducesResponseType(typeof(IEnumerable<Api.Models.Job>), 200)]
50  public async Task<IActionResult> Read(CancellationToken cancellationToken)
51  {
52  var result = await DatabaseContext
53  .Jobs
54  .AsQueryable()
55  .Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue)
56  .OrderByDescending(x => x.StartedAt)
57  .ToListAsync(cancellationToken)
58  .ConfigureAwait(false);
59  return Json(result.Select(x => x.ToApi()));
60  }
61 
68  [HttpGet(Routes.List)]
69  [TgsAuthorize]
70  [ProducesResponseType(typeof(List<Api.Models.EntityId>), 200)]
71  public async Task<IActionResult> List(CancellationToken cancellationToken)
72  {
73  // you KNOW this will need pagination eventually right?
74  var jobs = await DatabaseContext
75  .Jobs
76  .AsQueryable()
77  .Where(x => x.Instance.Id == Instance.Id)
78  .OrderByDescending(x => x.StartedAt)
79  .Select(x => new Api.Models.EntityId
80  {
81  Id = x.Id
82  })
83  .ToListAsync(cancellationToken)
84  .ConfigureAwait(false);
85  return Json(jobs);
86  }
87 
97  [HttpDelete("{id}")]
98  [TgsAuthorize]
99  [ProducesResponseType(typeof(Api.Models.Job), 202)]
100  [ProducesResponseType(404)]
101  [ProducesResponseType(410)]
102  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
103  {
104  // don't care if an instance post or not at this point
105  var job = await DatabaseContext
106  .Jobs
107  .AsQueryable()
108  .Where(x => x.Id == id && x.Instance.Id == Instance.Id)
109  .FirstOrDefaultAsync(cancellationToken)
110  .ConfigureAwait(false);
111  if (job == default(Job))
112  return NotFound();
113 
114  if (job.StoppedAt != null)
115  return StatusCode((int)HttpStatusCode.Gone);
116 
117  if (job.CancelRight.HasValue && job.CancelRightsType.HasValue && (AuthenticationContext.GetRight(job.CancelRightsType.Value) & job.CancelRight.Value) == 0)
118  return Forbid();
119 
120  var updatedJob = await jobManager.CancelJob(job, AuthenticationContext.User, false, cancellationToken).ConfigureAwait(false);
121  return updatedJob != null ? (IActionResult)Accepted(updatedJob.ToApi()) : StatusCode((int)HttpStatusCode.Gone);
122  }
123 
132  [HttpGet("{id}")]
133  [TgsAuthorize]
134  [ProducesResponseType(typeof(Api.Models.Job), 200)]
135  [ProducesResponseType(404)]
136  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
137  {
138  var job = await DatabaseContext
139  .Jobs
140  .AsQueryable()
141  .Where(x => x.Id == id && x.Instance.Id == Instance.Id)
142  .Include(x => x.StartedBy)
143  .FirstOrDefaultAsync(cancellationToken)
144  .ConfigureAwait(false);
145  if (job == default)
146  return NotFound();
147  var api = job.ToApi();
148  api.Progress = jobManager.JobProgress(job);
149  return Json(api);
150  }
151  }
152 }
JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger< JobController > logger)
Construct a HomeController
+
DbSet< Job > Jobs
The Jobs in the DatabaseContext.
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
-
Backend abstract implementation of IDatabaseContext
-
DbSet< Job > Jobs
The Jobs in the DatabaseContext<TParentContext>.
+
Backend abstract implementation of IDatabaseContext
Routes to a server actions
Definition: Routes.cs:9
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
-
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.Job.
+
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.Job.
const string Jobs
The Models.Job controller
Definition: Routes.cs:79
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
@@ -117,14 +117,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Manages the runtime of Jobs
Definition: IJobManager.cs:13
-
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Cancel a running Api.Models.Job.
+
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Cancel a running Api.Models.Job.
const string List
The postfix for list operations
Definition: Routes.cs:84
readonly IJobManager jobManager
The IJobManager for the JobController
ApiController for Api.Models.Jobs
-
async Task< IActionResult > List(CancellationToken cancellationToken)
List all Api.Models.Job Api.Models.EntityIds for the instance in reverse creation order...
- +
async Task< IActionResult > List(CancellationToken cancellationToken)
List all Api.Models.Job Api.Models.EntityIds for the instance in reverse creation order...
+
diff --git a/_job_exception_8cs.html b/_job_exception_8cs.html index eb37de7138..568e62dcc9 100644 --- a/_job_exception_8cs.html +++ b/_job_exception_8cs.html @@ -25,7 +25,7 @@
diff --git a/_job_exception_8cs_source.html b/_job_exception_8cs_source.html index 6f7a34f136..185c441de7 100644 --- a/_job_exception_8cs_source.html +++ b/_job_exception_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_job_handler_8cs.html b/_job_handler_8cs.html index 73ae26bf02..63b831a1fe 100644 --- a/_job_handler_8cs.html +++ b/_job_handler_8cs.html @@ -25,7 +25,7 @@ diff --git a/_job_handler_8cs_source.html b/_job_handler_8cs_source.html index 1589f06d9e..cb3c79a89e 100644 --- a/_job_handler_8cs_source.html +++ b/_job_handler_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_job_manager_8cs.html b/_job_manager_8cs.html index 6f682a2fd9..ba14529e0b 100644 --- a/_job_manager_8cs.html +++ b/_job_manager_8cs.html @@ -25,7 +25,7 @@ diff --git a/_job_manager_8cs_source.html b/_job_manager_8cs_source.html index 250d671c50..6803bb6791 100644 --- a/_job_manager_8cs_source.html +++ b/_job_manager_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JobManager.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Jobs
12 {
15  {
20 
24  readonly ILogger<JobManager> logger;
25 
29  readonly Dictionary<long, JobHandler> jobs;
30 
34  readonly object synchronizationLock;
35 
41  public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger<JobManager> logger)
42  {
43  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
44  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
45  jobs = new Dictionary<long, JobHandler>();
46  synchronizationLock = new object();
47  }
48 
50  public void Dispose()
51  {
52  foreach (var job in jobs)
53  job.Value.Dispose();
54  }
55 
62  {
63  lock (synchronizationLock)
64  {
65  if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler))
66  throw new InvalidOperationException("Job not running!");
67  return jobHandler;
68  }
69  }
70 
78  async Task RunJob(Job job, Func<Job, IDatabaseContextFactory, CancellationToken, Task> operation, CancellationToken cancellationToken)
79  {
80  try
81  {
82  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
83  try
84  {
85  var oldJob = job;
86  job = new Job { Id = oldJob.Id };
87 
88  await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false);
89 
90  logger.LogDebug("Job {0} completed!", job.Id);
91  }
92  catch (OperationCanceledException)
93  {
94  logger.LogDebug("Job {0} cancelled!", job.Id);
95  job.Cancelled = true;
96  }
97  catch (JobException e)
98  {
99  job.ErrorCode = e.ErrorCode;
100  job.ExceptionDetails = e.Message;
101  LogRegularException();
102  if (e.InnerException != null)
103  logger.LogDebug(
104  "Inner exception for job {0}: {1}",
105  job.Id,
106  e.InnerException is JobException
107  ? e.InnerException.Message
108  : e.InnerException.ToString());
109  }
110  catch (Exception e)
111  {
112  job.ExceptionDetails = e.ToString();
113  LogRegularException();
114  }
115 
116  await databaseContextFactory.UseContext(async databaseContext =>
117  {
118  var attachedJob = new Job
119  {
120  Id = job.Id
121  };
122 
123  databaseContext.Jobs.Attach(attachedJob);
124  attachedJob.StoppedAt = DateTimeOffset.Now;
125  attachedJob.ExceptionDetails = job.ExceptionDetails;
126  attachedJob.ErrorCode = job.ErrorCode;
127  attachedJob.Cancelled = job.Cancelled;
128 
129  await databaseContext.Save(default).ConfigureAwait(false);
130  }).ConfigureAwait(false);
131  }
132  finally
133  {
134  lock (synchronizationLock)
135  {
136  var handler = jobs[job.Id];
137  jobs.Remove(job.Id);
138  handler.Dispose();
139  }
140  }
141  }
142 
144  public Task RegisterOperation(Job job, Func<Job, IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
145  {
146  if (job == null)
147  throw new ArgumentNullException(nameof(job));
148  if (operation == null)
149  throw new ArgumentNullException(nameof(operation));
150 
151  job.StartedAt = DateTimeOffset.Now;
152  job.Cancelled = false;
153 
154  job.Instance = new Instance
155  {
156  Id = job.Instance.Id
157  };
158  databaseContext.Instances.Attach(job.Instance);
159 
160  job.StartedBy = new User
161  {
162  Id = job.StartedBy.Id
163  };
164  databaseContext.Users.Attach(job.StartedBy);
165 
166  databaseContext.Jobs.Add(job);
167 
168  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
169  logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
170  var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
171  operation(jobParam, serviceProvider, y =>
172  {
173  lock (synchronizationLock)
174  if (jobs.TryGetValue(job.Id, out var handler))
175  handler.Progress = y;
176  }, ct),
177  x));
178  lock (synchronizationLock)
179  jobs.Add(job.Id, jobHandler);
180  });
181 
183  public async Task StartAsync(CancellationToken cancellationToken)
184  {
185  logger.LogTrace("Starting job manager...");
186  await databaseContextFactory.UseContext(async databaseContext =>
187  {
188  // mark all jobs as cancelled
189  var badJobs = await databaseContext.Jobs.Where(y => !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false);
190  if (badJobs.Count > 0)
191  {
192  logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
193  foreach (var I in badJobs)
194  {
195  var job = new Job { Id = I };
196  databaseContext.Jobs.Attach(job);
197  job.Cancelled = true;
198  job.StoppedAt = DateTimeOffset.Now;
199  }
200 
201  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
202  }
203  }).ConfigureAwait(false);
204  logger.LogDebug("Job manager started!");
205  }
206 
208  public async Task StopAsync(CancellationToken cancellationToken)
209  {
210  var joinTasks = jobs.Select(x =>
211  {
212  x.Value.Cancel();
213  return x.Value.Wait(cancellationToken);
214  });
215  await Task.WhenAll(joinTasks).ConfigureAwait(false);
216  }
217 
219  public async Task<Job> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
220  {
221  if (job == null)
222  throw new ArgumentNullException(nameof(job));
223  if (user == null)
224  throw new ArgumentNullException(nameof(user));
225  JobHandler handler;
226  try
227  {
228  handler = CheckGetJob(job);
229  }
230  catch (InvalidOperationException)
231  {
232  // this is fine
233  return null;
234  }
235 
236  handler.Cancel(); // this will ensure the db update is only done once
237  await databaseContextFactory.UseContext(async databaseContext =>
238  {
239  var updatedJob = new Job { Id = job.Id };
240  databaseContext.Jobs.Attach(job);
241  var attachedUser = new User { Id = user.Id };
242  databaseContext.Users.Attach(user);
243  updatedJob.CancelledBy = attachedUser;
244 
245  // let either startup or cancellation set job.cancelled
246  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
247  job.CancelledBy = user;
248  }).ConfigureAwait(false);
249  if (blocking)
250  await handler.Wait(cancellationToken).ConfigureAwait(false);
251  return job;
252  }
253 
255  public int? JobProgress(Job job)
256  {
257  if (job == null)
258  throw new ArgumentNullException(nameof(job));
259  lock (synchronizationLock)
260  {
261  if (!jobs.TryGetValue(job.Id, out var handler))
262  return null;
263  return handler.Progress;
264  }
265  }
266 
268  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
269  {
270  if (job == null)
271  throw new ArgumentNullException(nameof(job));
272  if (canceller == null)
273  throw new ArgumentNullException(nameof(canceller));
274  JobHandler handler;
275  lock (synchronizationLock)
276  {
277  if (!jobs.TryGetValue(job.Id, out handler))
278  return;
279  }
280 
281  Task cancelTask = null;
282  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
283  await handler.Wait(cancellationToken).ConfigureAwait(false);
284 
285  if (cancelTask != null)
286  await cancelTask.ConfigureAwait(false);
287  }
288  }
289 }
long Id
The ID of the entity.
Definition: EntityId.cs:11
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Jobs
12 {
15  {
20 
24  readonly ILogger<JobManager> logger;
25 
29  readonly Dictionary<long, JobHandler> jobs;
30 
34  readonly object synchronizationLock;
35 
41  public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger<JobManager> logger)
42  {
43  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
44  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
45  jobs = new Dictionary<long, JobHandler>();
46  synchronizationLock = new object();
47  }
48 
50  public void Dispose()
51  {
52  foreach (var job in jobs)
53  job.Value.Dispose();
54  }
55 
62  {
63  lock (synchronizationLock)
64  {
65  if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler))
66  throw new InvalidOperationException("Job not running!");
67  return jobHandler;
68  }
69  }
70 
78  async Task RunJob(Job job, Func<Job, IDatabaseContextFactory, CancellationToken, Task> operation, CancellationToken cancellationToken)
79  {
80  try
81  {
82  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
83  try
84  {
85  var oldJob = job;
86  job = new Job { Id = oldJob.Id };
87 
88  await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false);
89 
90  logger.LogDebug("Job {0} completed!", job.Id);
91  }
92  catch (OperationCanceledException)
93  {
94  logger.LogDebug("Job {0} cancelled!", job.Id);
95  job.Cancelled = true;
96  }
97  catch (JobException e)
98  {
99  job.ErrorCode = e.ErrorCode;
100  job.ExceptionDetails = e.Message;
101  LogRegularException();
102  if (e.InnerException != null)
103  logger.LogDebug(
104  "Inner exception for job {0}: {1}",
105  job.Id,
106  e.InnerException is JobException
107  ? e.InnerException.Message
108  : e.InnerException.ToString());
109  }
110  catch (Exception e)
111  {
112  job.ExceptionDetails = e.ToString();
113  LogRegularException();
114  }
115 
116  await databaseContextFactory.UseContext(async databaseContext =>
117  {
118  var attachedJob = new Job
119  {
120  Id = job.Id
121  };
122 
123  databaseContext.Jobs.Attach(attachedJob);
124  attachedJob.StoppedAt = DateTimeOffset.Now;
125  attachedJob.ExceptionDetails = job.ExceptionDetails;
126  attachedJob.ErrorCode = job.ErrorCode;
127  attachedJob.Cancelled = job.Cancelled;
128 
129  await databaseContext.Save(default).ConfigureAwait(false);
130  }).ConfigureAwait(false);
131  }
132  finally
133  {
134  lock (synchronizationLock)
135  {
136  var handler = jobs[job.Id];
137  jobs.Remove(job.Id);
138  handler.Dispose();
139  }
140  }
141  }
142 
144  public Task RegisterOperation(Job job, Func<Job, IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
145  {
146  if (job == null)
147  throw new ArgumentNullException(nameof(job));
148  if (operation == null)
149  throw new ArgumentNullException(nameof(operation));
150 
151  job.StartedAt = DateTimeOffset.Now;
152  job.Cancelled = false;
153 
154  job.Instance = new Instance
155  {
156  Id = job.Instance.Id
157  };
158  databaseContext.Instances.Attach(job.Instance);
159 
160  job.StartedBy = new User
161  {
162  Id = job.StartedBy.Id
163  };
164  databaseContext.Users.Attach(job.StartedBy);
165 
166  databaseContext.Jobs.Add(job);
167 
168  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
169  logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
170  var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
171  operation(jobParam, serviceProvider, y =>
172  {
173  lock (synchronizationLock)
174  if (jobs.TryGetValue(job.Id, out var handler))
175  handler.Progress = y;
176  }, ct),
177  x));
178  lock (synchronizationLock)
179  jobs.Add(job.Id, jobHandler);
180  });
181 
183  public async Task StartAsync(CancellationToken cancellationToken)
184  {
185  logger.LogTrace("Starting job manager...");
186  await databaseContextFactory.UseContext(async databaseContext =>
187  {
188  // mark all jobs as cancelled
189  var badJobs = await databaseContext
190  .Jobs
191  .AsQueryable()
192  .Where(y => !y.StoppedAt.HasValue)
193  .Select(y => y.Id)
194  .ToListAsync(cancellationToken)
195  .ConfigureAwait(false);
196  if (badJobs.Count > 0)
197  {
198  logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
199  foreach (var I in badJobs)
200  {
201  var job = new Job { Id = I };
202  databaseContext.Jobs.Attach(job);
203  job.Cancelled = true;
204  job.StoppedAt = DateTimeOffset.Now;
205  }
206 
207  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
208  }
209  }).ConfigureAwait(false);
210  logger.LogDebug("Job manager started!");
211  }
212 
214  public async Task StopAsync(CancellationToken cancellationToken)
215  {
216  var joinTasks = jobs.Select(x =>
217  {
218  x.Value.Cancel();
219  return x.Value.Wait(cancellationToken);
220  });
221  await Task.WhenAll(joinTasks).ConfigureAwait(false);
222  }
223 
225  public async Task<Job> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
226  {
227  if (job == null)
228  throw new ArgumentNullException(nameof(job));
229  if (user == null)
230  throw new ArgumentNullException(nameof(user));
231  JobHandler handler;
232  try
233  {
234  handler = CheckGetJob(job);
235  }
236  catch (InvalidOperationException)
237  {
238  // this is fine
239  return null;
240  }
241 
242  handler.Cancel(); // this will ensure the db update is only done once
243  await databaseContextFactory.UseContext(async databaseContext =>
244  {
245  var updatedJob = new Job { Id = job.Id };
246  databaseContext.Jobs.Attach(job);
247  var attachedUser = new User { Id = user.Id };
248  databaseContext.Users.Attach(user);
249  updatedJob.CancelledBy = attachedUser;
250 
251  // let either startup or cancellation set job.cancelled
252  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
253  job.CancelledBy = user;
254  }).ConfigureAwait(false);
255  if (blocking)
256  await handler.Wait(cancellationToken).ConfigureAwait(false);
257  return job;
258  }
259 
261  public int? JobProgress(Job job)
262  {
263  if (job == null)
264  throw new ArgumentNullException(nameof(job));
265  lock (synchronizationLock)
266  {
267  if (!jobs.TryGetValue(job.Id, out var handler))
268  return null;
269  return handler.Progress;
270  }
271  }
272 
274  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
275  {
276  if (job == null)
277  throw new ArgumentNullException(nameof(job));
278  if (canceller == null)
279  throw new ArgumentNullException(nameof(canceller));
280  JobHandler handler;
281  lock (synchronizationLock)
282  {
283  if (!jobs.TryGetValue(job.Id, out handler))
284  return;
285  }
286 
287  Task cancelTask = null;
288  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
289  await handler.Wait(cancellationToken).ConfigureAwait(false);
290 
291  if (cancelTask != null)
292  await cancelTask.ConfigureAwait(false);
293  }
294  }
295 }
long Id
The ID of the entity.
Definition: EntityId.cs:11
readonly IDatabaseContextFactory databaseContextFactory
The IServiceProvider for the JobManager
Definition: JobManager.cs:19
User StartedBy
See Api.Models.Job.StartedBy
Definition: Job.cs:12
@@ -106,20 +106,20 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DateTimeOffset StartedAt
When the Job was started
Definition: Job.cs:32
string Description
English description of the Job
Definition: Job.cs:16
-
int JobProgress(Job job)
Get the Api.Models.Job.Progress for a job
Definition: JobManager.cs:255
+
int JobProgress(Job job)
Get the Api.Models.Job.Progress for a job
Definition: JobManager.cs:261
string ExceptionDetails
Details of any exceptions caught during the Job
Definition: Job.cs:26
Class for pairing Tasks with CancellationTokenSources
Definition: JobHandler.cs:10
User CancelledBy
See Api.Models.Job.CancelledBy
Definition: Job.cs:17
Instance Instance
The Models.Instance the job belongs to if any
Definition: Job.cs:23
-
async Task< Job > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job
Definition: JobManager.cs:219
+
async Task< Job > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job
Definition: JobManager.cs:225
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
JobHandler CheckGetJob(Job job)
Gets the JobHandler for a given job if it exists
Definition: JobManager.cs:61
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
ErrorCode ErrorCode
The Models.ErrorCode associated with the Job if any.
Definition: Job.cs:21
-
async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete
Definition: JobManager.cs:268
+
async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete
Definition: JobManager.cs:274
long Id
The ID of the User
Definition: User.cs:16
JobManager(IDatabaseContextFactory databaseContextFactory, ILogger< JobManager > logger)
Construct a JobManager
Definition: JobManager.cs:41
@@ -129,13 +129,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task RunJob(Job job, Func< Job, IDatabaseContextFactory, CancellationToken, Task > operation, CancellationToken cancellationToken)
Runner for JobHandlers
Definition: JobManager.cs:78
bool Cancelled
If the Job was cancelled
Definition: Job.cs:43
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:208
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:214
async Task Wait(CancellationToken cancellationToken)
Wait for task to complete
Definition: JobHandler.cs:47
ErrorCode ErrorCode
The Api.Models.ErrorCode associated with the JobException.
Definition: JobException.cs:14
readonly ILogger< JobManager > logger
The ILogger for the JobManager
Definition: JobManager.cs:24
- +
diff --git a/_jobs_client_8cs.html b/_jobs_client_8cs.html index 3a716ba198..2dc647b616 100644 --- a/_jobs_client_8cs.html +++ b/_jobs_client_8cs.html @@ -25,7 +25,7 @@
diff --git a/_jobs_client_8cs_source.html b/_jobs_client_8cs_source.html index 79db622d07..ae2bea36f7 100644 --- a/_jobs_client_8cs_source.html +++ b/_jobs_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_kek_command_8cs.html b/_kek_command_8cs.html index 94f67dfc63..b0d1485a0e 100644 --- a/_kek_command_8cs.html +++ b/_kek_command_8cs.html @@ -25,7 +25,7 @@ diff --git a/_kek_command_8cs_source.html b/_kek_command_8cs_source.html index 6f20a44862..1bf5bac422 100644 --- a/_kek_command_8cs_source.html +++ b/_kek_command_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_launch_result_8cs.html b/_launch_result_8cs.html index 6d9384d2a7..db5db5dfd4 100644 --- a/_launch_result_8cs.html +++ b/_launch_result_8cs.html @@ -25,7 +25,7 @@ diff --git a/_launch_result_8cs_source.html b/_launch_result_8cs_source.html index d0231c9133..df305ae06c 100644 --- a/_launch_result_8cs_source.html +++ b/_launch_result_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_lib_git2_commands_8cs.html b/_lib_git2_commands_8cs.html index dbb76f088a..54a3586ad6 100644 --- a/_lib_git2_commands_8cs.html +++ b/_lib_git2_commands_8cs.html @@ -25,7 +25,7 @@ diff --git a/_lib_git2_commands_8cs_source.html b/_lib_git2_commands_8cs_source.html index b15e3d8e5b..2f4d55ab17 100644 --- a/_lib_git2_commands_8cs_source.html +++ b/_lib_git2_commands_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_lib_git2_repository_factory_8cs.html b/_lib_git2_repository_factory_8cs.html index e663ba85b1..bc4e2d9fa5 100644 --- a/_lib_git2_repository_factory_8cs.html +++ b/_lib_git2_repository_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_lib_git2_repository_factory_8cs_source.html b/_lib_git2_repository_factory_8cs_source.html index 501116be38..e118d98ac5 100644 --- a/_lib_git2_repository_factory_8cs_source.html +++ b/_lib_git2_repository_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_limits_8cs.html b/_limits_8cs.html index 22113640a2..28748c467d 100644 --- a/_limits_8cs.html +++ b/_limits_8cs.html @@ -25,7 +25,7 @@ diff --git a/_limits_8cs_source.html b/_limits_8cs_source.html index c2a6c1e97f..83d1c4bc94 100644 --- a/_limits_8cs_source.html +++ b/_limits_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_message_8cs.html b/_message_8cs.html index b28f60842f..4268ab0464 100644 --- a/_message_8cs.html +++ b/_message_8cs.html @@ -25,7 +25,7 @@ diff --git a/_message_8cs_source.html b/_message_8cs_source.html index c62875d509..444ce2a5f8 100644 --- a/_message_8cs_source.html +++ b/_message_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_method_not_supported_exception_8cs.html b/_method_not_supported_exception_8cs.html index 7a388950da..5769a24aa2 100644 --- a/_method_not_supported_exception_8cs.html +++ b/_method_not_supported_exception_8cs.html @@ -25,7 +25,7 @@ diff --git a/_method_not_supported_exception_8cs_source.html b/_method_not_supported_exception_8cs_source.html index 432649c57a..319ab8e7dc 100644 --- a/_method_not_supported_exception_8cs_source.html +++ b/_method_not_supported_exception_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_models_2_dual_reattach_information_8cs.html b/_models_2_dual_reattach_information_8cs.html index b243c19d13..bd706c215a 100644 --- a/_models_2_dual_reattach_information_8cs.html +++ b/_models_2_dual_reattach_information_8cs.html @@ -25,7 +25,7 @@ diff --git a/_models_2_dual_reattach_information_8cs_source.html b/_models_2_dual_reattach_information_8cs_source.html index a89a6b6b23..78ff5f12b0 100644 --- a/_models_2_dual_reattach_information_8cs_source.html +++ b/_models_2_dual_reattach_information_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_models_2_reattach_information_8cs.html b/_models_2_reattach_information_8cs.html index 1ee6f7795d..91ca2da75c 100644 --- a/_models_2_reattach_information_8cs.html +++ b/_models_2_reattach_information_8cs.html @@ -25,7 +25,7 @@ diff --git a/_models_2_reattach_information_8cs_source.html b/_models_2_reattach_information_8cs_source.html index adbcead7cd..21f8a87101 100644 --- a/_models_2_reattach_information_8cs_source.html +++ b/_models_2_reattach_information_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_monitor_action_8cs.html b/_monitor_action_8cs.html index 6f54df4d36..a4e21ad81b 100644 --- a/_monitor_action_8cs.html +++ b/_monitor_action_8cs.html @@ -25,7 +25,7 @@ diff --git a/_monitor_action_8cs_source.html b/_monitor_action_8cs_source.html index eb8e47aaa4..2b5f5d4fbb 100644 --- a/_monitor_action_8cs_source.html +++ b/_monitor_action_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_monitor_activation_reason_8cs.html b/_monitor_activation_reason_8cs.html index 43eda764b8..766a695091 100644 --- a/_monitor_activation_reason_8cs.html +++ b/_monitor_activation_reason_8cs.html @@ -25,7 +25,7 @@ diff --git a/_monitor_activation_reason_8cs_source.html b/_monitor_activation_reason_8cs_source.html index c01b55157f..e8eded70f1 100644 --- a/_monitor_activation_reason_8cs_source.html +++ b/_monitor_activation_reason_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_monitor_state_8cs.html b/_monitor_state_8cs.html index 35378a2a1f..114d9d6915 100644 --- a/_monitor_state_8cs.html +++ b/_monitor_state_8cs.html @@ -25,7 +25,7 @@ diff --git a/_monitor_state_8cs_source.html b/_monitor_state_8cs_source.html index 6b390d916f..4ffb9d5e82 100644 --- a/_monitor_state_8cs_source.html +++ b/_monitor_state_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_my_sql_database_context_8cs.html b/_my_sql_database_context_8cs.html index a32fe98232..babd6923ad 100644 --- a/_my_sql_database_context_8cs.html +++ b/_my_sql_database_context_8cs.html @@ -25,7 +25,7 @@ @@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

diff --git a/_design_time_db_context_factory_helpers_8cs_source.html b/_design_time_db_context_factory_helpers_8cs_source.html index cb3c212cd6..09b6bee607 100644 --- a/_design_time_db_context_factory_helpers_8cs_source.html +++ b/_design_time_db_context_factory_helpers_8cs_source.html @@ -25,7 +25,7 @@

tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite

Classes

class  Tgstation.Server.Host.Database.MySqlDatabaseContext
 DatabaseContext<TParentContext> for MySQL More...
 DatabaseContext for MySQL More...
 
@@ -94,18 +94,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
MySqlDatabaseContext.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using MySql.Data.MySqlClient;
5 using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
6 using System;
8 
9 namespace Tgstation.Server.Host.Database
10 {
14  sealed class MySqlDatabaseContext : DatabaseContext<MySqlDatabaseContext>
15  {
23  public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<MySqlDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
24  { }
25 
27  protected override void OnConfiguring(DbContextOptionsBuilder options)
28  {
29  base.OnConfiguring(options);
30  var stringDeconstructor = new MySqlConnectionStringBuilder
31  {
32  ConnectionString = DatabaseConfiguration.ConnectionString
33  };
34  if (stringDeconstructor.Server == "localhost")
35  Logger.LogWarning("MariaDB/MySQL server address is set to 'localhost'! If there are connection issues, try setting it to '127.0.0.1'!");
36  options.UseMySql(
38  mySqlOptions =>
39  {
40  mySqlOptions.EnableRetryOnFailure();
41 
42  if (!String.IsNullOrEmpty(DatabaseConfiguration.MySqlServerVersion))
43  mySqlOptions.ServerVersion(
44  Version.Parse(DatabaseConfiguration.MySqlServerVersion),
45  DatabaseConfiguration.DatabaseType == DatabaseType.MariaDB
46  ? ServerType.MariaDb
47  : ServerType.MySql);
48  });
49  }
50 
52  protected override void ValidateDatabaseType()
53  {
54  if (DatabaseType != DatabaseType.MariaDB && DatabaseType != DatabaseType.MySql)
55  throw new InvalidOperationException("Invalid DatabaseType for MySqlDatabaseContext!");
56  }
57  }
58 }
string ConnectionString
The connection string for the database
-
override void ValidateDatabaseType()
Ensure the DatabaseType is correct for the DatabaseContext<TParentContext>.
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using MySql.Data.MySqlClient;
5 using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
6 using System;
8 
9 namespace Tgstation.Server.Host.Database
10 {
15  {
23  public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<MySqlDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
24  { }
25 
27  protected override void OnConfiguring(DbContextOptionsBuilder options)
28  {
29  base.OnConfiguring(options);
30  var stringDeconstructor = new MySqlConnectionStringBuilder
31  {
32  ConnectionString = DatabaseConfiguration.ConnectionString
33  };
34  if (stringDeconstructor.Server == "localhost")
35  Logger.LogWarning("MariaDB/MySQL server address is set to 'localhost'! If there are connection issues, try setting it to '127.0.0.1'!");
36  options.UseMySql(
38  mySqlOptions =>
39  {
40  mySqlOptions.EnableRetryOnFailure();
41 
42  if (!String.IsNullOrEmpty(DatabaseConfiguration.MySqlServerVersion))
43  mySqlOptions.ServerVersion(
44  Version.Parse(DatabaseConfiguration.MySqlServerVersion),
45  DatabaseConfiguration.DatabaseType == DatabaseType.MariaDB
46  ? ServerType.MariaDb
47  : ServerType.MySql);
48  });
49  }
50 
52  protected override void ValidateDatabaseType()
53  {
54  if (DatabaseType != DatabaseType.MariaDB && DatabaseType != DatabaseType.MySql)
55  throw new InvalidOperationException("Invalid DatabaseType for MySqlDatabaseContext!");
56  }
57  }
58 }
string ConnectionString
The connection string for the database
+
override void ValidateDatabaseType()
Ensure the DatabaseType is correct for the DatabaseContext.
MySqlDatabaseContext(DbContextOptions< MySqlDatabaseContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger< MySqlDatabaseContext > logger)
Construct a MySqlDatabaseContext
override void OnConfiguring(DbContextOptionsBuilder options)
For initially seeding a database
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
-
DatabaseContext<TParentContext> for MySQL
-
Configuration options for the Database.DatabaseContext<TParentContext>
+ +
Configuration options for the Database.DatabaseContext
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
diff --git a/_my_sql_database_context_model_snapshot_8cs.html b/_my_sql_database_context_model_snapshot_8cs.html index 35c484330d..428f6f63a3 100644 --- a/_my_sql_database_context_model_snapshot_8cs.html +++ b/_my_sql_database_context_model_snapshot_8cs.html @@ -25,7 +25,7 @@
diff --git a/_my_sql_database_context_model_snapshot_8cs_source.html b/_my_sql_database_context_model_snapshot_8cs_source.html index 9127ee101b..e92b141b5f 100644 --- a/_my_sql_database_context_model_snapshot_8cs_source.html +++ b/_my_sql_database_context_model_snapshot_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_my_sql_design_time_db_context_factory_8cs.html b/_my_sql_design_time_db_context_factory_8cs.html index f3342531d4..4982e14917 100644 --- a/_my_sql_design_time_db_context_factory_8cs.html +++ b/_my_sql_design_time_db_context_factory_8cs.html @@ -25,7 +25,7 @@ @@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

diff --git a/_my_sql_database_context_8cs_source.html b/_my_sql_database_context_8cs_source.html index fad0396984..a8294bae85 100644 --- a/_my_sql_database_context_8cs_source.html +++ b/_my_sql_database_context_8cs_source.html @@ -25,7 +25,7 @@

tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite

Classes

class  Tgstation.Server.Host.Database.Design.MySqlDesignTimeDbContextFactory
 IDesignTimeDbContextFactory<TContext> for creating MySqlDatabaseContexts. More...
 IDesignTimeDbContextFactory<TContext> for creating MySqlDatabaseContexts. More...
 
@@ -94,19 +94,19 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
MySqlDesignTimeDbContextFactory.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
9 
10 namespace Tgstation.Server.Host.Database.Design
11 {
15  sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MySqlDatabaseContext>
16  {
18  public MySqlDatabaseContext CreateDbContext(string[] args)
19  {
20  using var loggerFactory = new LoggerFactory();
21  return new MySqlDatabaseContext(
22  new DbContextOptions<MySqlDatabaseContext>(),
24  DatabaseType.MariaDB,
25  "Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"),
26  new DatabaseSeeder(
28  new PasswordHasher<User>()),
29  new PlatformIdentifier()),
30  loggerFactory.CreateLogger<MySqlDatabaseContext>());
31  }
32  }
33 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
9 
10 namespace Tgstation.Server.Host.Database.Design
11 {
15  sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MySqlDatabaseContext>
16  {
18  public MySqlDatabaseContext CreateDbContext(string[] args)
19  {
20  using var loggerFactory = new LoggerFactory();
21  return new MySqlDatabaseContext(
22  new DbContextOptions<MySqlDatabaseContext>(),
24  DatabaseType.MariaDB,
25  "Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"),
26  new DatabaseSeeder(
28  new PasswordHasher<User>()),
29  new PlatformIdentifier()),
30  loggerFactory.CreateLogger<MySqlDatabaseContext>());
31  }
32  }
33 }
-
static IOptions< DatabaseConfiguration > GetDbContextOptions(DatabaseType databaseType, string connectionString)
Get the IOptions<TOptions> for the DatabaseConfiguration
+
static IOptions< DatabaseConfiguration > GetDatabaseConfiguration(DatabaseType databaseType, string connectionString)
Get the IOptions<TOptions> for the DatabaseConfiguration
-
DatabaseContext<TParentContext> for MySQL
+
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
Contains helpers for creating design time DatabaseContext<TParentContext>s
+
IDesignTimeDbContextFactory<TContext> for creating MySqlDatabaseContexts.
diff --git a/_native_methods_8cs.html b/_native_methods_8cs.html index 09be792392..7f57d8c78d 100644 --- a/_native_methods_8cs.html +++ b/_native_methods_8cs.html @@ -25,7 +25,7 @@
diff --git a/_native_methods_8cs_source.html b/_native_methods_8cs_source.html index 9c06b2400c..c861e96dab 100644 --- a/_native_methods_8cs_source.html +++ b/_native_methods_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_open_api_enum_var_names_extension_8cs.html b/_open_api_enum_var_names_extension_8cs.html index b98fcbd7a3..f9fdb7d11a 100644 --- a/_open_api_enum_var_names_extension_8cs.html +++ b/_open_api_enum_var_names_extension_8cs.html @@ -25,7 +25,7 @@ diff --git a/_open_api_enum_var_names_extension_8cs_source.html b/_open_api_enum_var_names_extension_8cs_source.html index 1cb119ffcf..0b2154dede 100644 --- a/_open_api_enum_var_names_extension_8cs_source.html +++ b/_open_api_enum_var_names_extension_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_platform_identifier_8cs.html b/_platform_identifier_8cs.html index 5cbc011863..aef6426be9 100644 --- a/_platform_identifier_8cs.html +++ b/_platform_identifier_8cs.html @@ -25,7 +25,7 @@ diff --git a/_platform_identifier_8cs_source.html b/_platform_identifier_8cs_source.html index 01f30a6b09..01b28c2947 100644 --- a/_platform_identifier_8cs_source.html +++ b/_platform_identifier_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_posix_byond_installer_8cs.html b/_posix_byond_installer_8cs.html index d11b69f3a6..78be1fcc7a 100644 --- a/_posix_byond_installer_8cs.html +++ b/_posix_byond_installer_8cs.html @@ -25,7 +25,7 @@ diff --git a/_posix_byond_installer_8cs_source.html b/_posix_byond_installer_8cs_source.html index e915e0c451..3969154fa6 100644 --- a/_posix_byond_installer_8cs_source.html +++ b/_posix_byond_installer_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_posix_network_prompt_reaper_8cs.html b/_posix_network_prompt_reaper_8cs.html index 674ea9da13..dd6f3ea4f9 100644 --- a/_posix_network_prompt_reaper_8cs.html +++ b/_posix_network_prompt_reaper_8cs.html @@ -25,7 +25,7 @@ diff --git a/_posix_network_prompt_reaper_8cs_source.html b/_posix_network_prompt_reaper_8cs_source.html index 866b87d630..92cc5a6ca1 100644 --- a/_posix_network_prompt_reaper_8cs_source.html +++ b/_posix_network_prompt_reaper_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_posix_post_write_handler_8cs.html b/_posix_post_write_handler_8cs.html index 235a308276..bf4d560cf0 100644 --- a/_posix_post_write_handler_8cs.html +++ b/_posix_post_write_handler_8cs.html @@ -25,7 +25,7 @@ diff --git a/_posix_post_write_handler_8cs_source.html b/_posix_post_write_handler_8cs_source.html index 1110596ee6..9ba026cb4e 100644 --- a/_posix_post_write_handler_8cs_source.html +++ b/_posix_post_write_handler_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_posix_process_features_8cs.html b/_posix_process_features_8cs.html index b3c01b7204..a91baf1871 100644 --- a/_posix_process_features_8cs.html +++ b/_posix_process_features_8cs.html @@ -25,7 +25,7 @@ diff --git a/_posix_process_features_8cs_source.html b/_posix_process_features_8cs_source.html index 4e93f82802..62d6c27186 100644 --- a/_posix_process_features_8cs_source.html +++ b/_posix_process_features_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_posix_symlink_factory_8cs.html b/_posix_symlink_factory_8cs.html index ddb2e0a289..2b0f14925f 100644 --- a/_posix_symlink_factory_8cs.html +++ b/_posix_symlink_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_posix_symlink_factory_8cs_source.html b/_posix_symlink_factory_8cs_source.html index 9053640501..2cfc66fcde 100644 --- a/_posix_symlink_factory_8cs_source.html +++ b/_posix_symlink_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_posix_system_identity_8cs.html b/_posix_system_identity_8cs.html index a7e5d56130..d118eb032b 100644 --- a/_posix_system_identity_8cs.html +++ b/_posix_system_identity_8cs.html @@ -25,7 +25,7 @@ diff --git a/_posix_system_identity_8cs_source.html b/_posix_system_identity_8cs_source.html index 9a5b98d400..fae9270d99 100644 --- a/_posix_system_identity_8cs_source.html +++ b/_posix_system_identity_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_posix_system_identity_factory_8cs.html b/_posix_system_identity_factory_8cs.html index 9d3eef03e2..c281538401 100644 --- a/_posix_system_identity_factory_8cs.html +++ b/_posix_system_identity_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_posix_system_identity_factory_8cs_source.html b/_posix_system_identity_factory_8cs_source.html index 796bc7b612..5581362e5c 100644 --- a/_posix_system_identity_factory_8cs_source.html +++ b/_posix_system_identity_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_post_setup_services_8cs.html b/_post_setup_services_8cs.html index 5e71ccce12..9154c6afc9 100644 --- a/_post_setup_services_8cs.html +++ b/_post_setup_services_8cs.html @@ -25,7 +25,7 @@ diff --git a/_post_setup_services_8cs_source.html b/_post_setup_services_8cs_source.html index 0ff9295617..917bb41aed 100644 --- a/_post_setup_services_8cs_source.html +++ b/_post_setup_services_8cs_source.html @@ -25,7 +25,7 @@ @@ -106,7 +106,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Set of objects needed to configure an Core.Application.
readonly IOptions< GeneralConfiguration > generalConfigurationOptions
Backing IOptions<TOptions> for GeneralConfiguration.
-
Configuration options for the Database.DatabaseContext<TParentContext>
+
Configuration options for the Database.DatabaseContext
For identifying the current platform
diff --git a/_postgres_sql_database_context_8cs.html b/_postgres_sql_database_context_8cs.html new file mode 100644 index 0000000000..1052bc0a44 --- /dev/null +++ b/_postgres_sql_database_context_8cs.html @@ -0,0 +1,122 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs File Reference + + + + + + + + + + +
+
+

diff --git a/_my_sql_design_time_db_context_factory_8cs_source.html b/_my_sql_design_time_db_context_factory_8cs_source.html index 4567c59b58..046a51bc3a 100644 --- a/_my_sql_design_time_db_context_factory_8cs_source.html +++ b/_my_sql_design_time_db_context_factory_8cs_source.html @@ -25,7 +25,7 @@

tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
+ + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
PostgresSqlDatabaseContext.cs File Reference
+
+ + + + + diff --git a/_postgres_sql_database_context_8cs_source.html b/_postgres_sql_database_context_8cs_source.html new file mode 100644 index 0000000000..3c77bd182b --- /dev/null +++ b/_postgres_sql_database_context_8cs_source.html @@ -0,0 +1,120 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs Source File + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
PostgresSqlDatabaseContext.cs
+
+
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using System;
5 using System.Diagnostics;
7 
8 namespace Tgstation.Server.Host.Database
9 {
14  {
23  DbContextOptions<PostgresSqlDatabaseContext> dbContextOptions,
24  IOptions<DatabaseConfiguration> databaseConfiguration,
25  IDatabaseSeeder databaseSeeder,
26  ILogger<PostgresSqlDatabaseContext> logger)
27  : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
28  { }
29 
31  protected override void OnConfiguring(DbContextOptionsBuilder options)
32  {
33  base.OnConfiguring(options);
34  options.UseNpgsql(DatabaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure());
35  }
36 
38  protected override void ValidateDatabaseType()
39  {
40  if (!Debugger.IsAttached)
41  throw new NotImplementedException("PostgresSQL implementation is not complete yet!");
42 
43  if (DatabaseType != DatabaseType.PostgresSql)
44  throw new InvalidOperationException("Invalid DatabaseType for PostgresSqlDatabaseContext!");
45  }
46  }
47 }
string ConnectionString
The connection string for the database
+ + +
For initially seeding a database
+
Backend abstract implementation of IDatabaseContext
+ + +
override void OnConfiguring(DbContextOptionsBuilder options)
+
override void ValidateDatabaseType()
Ensure the DatabaseType is correct for the DatabaseContext.
+
PostgresSqlDatabaseContext(DbContextOptions< PostgresSqlDatabaseContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger< PostgresSqlDatabaseContext > logger)
Construct a SqlServerDatabaseContext
+
Configuration options for the Database.DatabaseContext
+
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
+ + + +
+ + + + diff --git a/_postgres_sql_database_context_model_snapshot_8cs.html b/_postgres_sql_database_context_model_snapshot_8cs.html new file mode 100644 index 0000000000..7e06fee0e7 --- /dev/null +++ b/_postgres_sql_database_context_model_snapshot_8cs.html @@ -0,0 +1,121 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs File Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
PostgresSqlDatabaseContextModelSnapshot.cs File Reference
+
+ + + + + diff --git a/_postgres_sql_database_context_model_snapshot_8cs_source.html b/_postgres_sql_database_context_model_snapshot_8cs_source.html new file mode 100644 index 0000000000..708b5a5fcf --- /dev/null +++ b/_postgres_sql_database_context_model_snapshot_8cs_source.html @@ -0,0 +1,117 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/Migrations/PostgresSqlDatabaseContextModelSnapshot.cs Source File + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
PostgresSqlDatabaseContextModelSnapshot.cs
+
+
+Go to the documentation of this file.
1 // <auto-generated />
2 using System;
3 using Microsoft.EntityFrameworkCore;
4 using Microsoft.EntityFrameworkCore.Infrastructure;
5 using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
7 
8 namespace Tgstation.Server.Host.Migrations
9 {
12  {
13  protected override void BuildModel(ModelBuilder modelBuilder)
14  {
15 #pragma warning disable 612, 618
16  modelBuilder
17  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
18  .HasAnnotation("ProductVersion", "3.1.4")
19  .HasAnnotation("Relational:MaxIdentifierLength", 63);
20 
21  modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
22  {
23  b.Property<long>("Id")
24  .ValueGeneratedOnAdd()
25  .HasColumnType("bigint")
26  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
27 
28  b.Property<int>("ChannelLimit")
29  .HasColumnType("integer");
30 
31  b.Property<string>("ConnectionString")
32  .IsRequired()
33  .HasColumnType("character varying(10000)")
34  .HasMaxLength(10000);
35 
36  b.Property<bool?>("Enabled")
37  .HasColumnType("boolean");
38 
39  b.Property<long>("InstanceId")
40  .HasColumnType("bigint");
41 
42  b.Property<string>("Name")
43  .IsRequired()
44  .HasColumnType("character varying(100)")
45  .HasMaxLength(100);
46 
47  b.Property<int>("Provider")
48  .HasColumnType("integer");
49 
50  b.Property<long>("ReconnectionInterval")
51  .HasColumnType("bigint");
52 
53  b.HasKey("Id");
54 
55  b.HasIndex("InstanceId", "Name")
56  .IsUnique();
57 
58  b.ToTable("ChatBots");
59  });
60 
61  modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
62  {
63  b.Property<long>("Id")
64  .ValueGeneratedOnAdd()
65  .HasColumnType("bigint")
66  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
67 
68  b.Property<long>("ChatSettingsId")
69  .HasColumnType("bigint");
70 
71  b.Property<decimal?>("DiscordChannelId")
72  .HasColumnType("numeric(20,0)");
73 
74  b.Property<string>("IrcChannel")
75  .HasColumnType("character varying(100)")
76  .HasMaxLength(100);
77 
78  b.Property<bool?>("IsAdminChannel")
79  .IsRequired()
80  .HasColumnType("boolean");
81 
82  b.Property<bool?>("IsUpdatesChannel")
83  .IsRequired()
84  .HasColumnType("boolean");
85 
86  b.Property<bool?>("IsWatchdogChannel")
87  .IsRequired()
88  .HasColumnType("boolean");
89 
90  b.Property<string>("Tag")
91  .HasColumnType("character varying(10000)")
92  .HasMaxLength(10000);
93 
94  b.HasKey("Id");
95 
96  b.HasIndex("ChatSettingsId", "DiscordChannelId")
97  .IsUnique();
98 
99  b.HasIndex("ChatSettingsId", "IrcChannel")
100  .IsUnique();
101 
102  b.ToTable("ChatChannels");
103  });
104 
105  modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
106  {
107  b.Property<long>("Id")
108  .ValueGeneratedOnAdd()
109  .HasColumnType("bigint")
110  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
111 
112  b.Property<string>("ByondVersion")
113  .IsRequired()
114  .HasColumnType("text");
115 
116  b.Property<int?>("DMApiMajorVersion")
117  .HasColumnType("integer");
118 
119  b.Property<int?>("DMApiMinorVersion")
120  .HasColumnType("integer");
121 
122  b.Property<int?>("DMApiPatchVersion")
123  .HasColumnType("integer");
124 
125  b.Property<Guid?>("DirectoryName")
126  .IsRequired()
127  .HasColumnType("uuid");
128 
129  b.Property<string>("DmeName")
130  .IsRequired()
131  .HasColumnType("text");
132 
133  b.Property<long>("JobId")
134  .HasColumnType("bigint");
135 
136  b.Property<int>("MinimumSecurityLevel")
137  .HasColumnType("integer");
138 
139  b.Property<string>("Output")
140  .IsRequired()
141  .HasColumnType("text");
142 
143  b.Property<long>("RevisionInformationId")
144  .HasColumnType("bigint");
145 
146  b.HasKey("Id");
147 
148  b.HasIndex("DirectoryName");
149 
150  b.HasIndex("JobId")
151  .IsUnique();
152 
153  b.HasIndex("RevisionInformationId");
154 
155  b.ToTable("CompileJobs");
156  });
157 
158  modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
159  {
160  b.Property<long>("Id")
161  .ValueGeneratedOnAdd()
162  .HasColumnType("bigint")
163  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
164 
165  b.Property<bool?>("AllowWebClient")
166  .IsRequired()
167  .HasColumnType("boolean");
168 
169  b.Property<bool?>("AutoStart")
170  .IsRequired()
171  .HasColumnType("boolean");
172 
173  b.Property<long>("HeartbeatSeconds")
174  .HasColumnType("bigint");
175 
176  b.Property<long>("InstanceId")
177  .HasColumnType("bigint");
178 
179  b.Property<int>("PrimaryPort")
180  .HasColumnType("integer");
181 
182  b.Property<int>("SecondaryPort")
183  .HasColumnType("integer");
184 
185  b.Property<int>("SecurityLevel")
186  .HasColumnType("integer");
187 
188  b.Property<long>("StartupTimeout")
189  .HasColumnType("bigint");
190 
191  b.HasKey("Id");
192 
193  b.HasIndex("InstanceId")
194  .IsUnique();
195 
196  b.ToTable("DreamDaemonSettings");
197  });
198 
199  modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
200  {
201  b.Property<long>("Id")
202  .ValueGeneratedOnAdd()
203  .HasColumnType("bigint")
204  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
205 
206  b.Property<int>("ApiValidationPort")
207  .HasColumnType("integer");
208 
209  b.Property<int>("ApiValidationSecurityLevel")
210  .HasColumnType("integer");
211 
212  b.Property<long>("InstanceId")
213  .HasColumnType("bigint");
214 
215  b.Property<string>("ProjectName")
216  .HasColumnType("character varying(10000)")
217  .HasMaxLength(10000);
218 
219  b.HasKey("Id");
220 
221  b.HasIndex("InstanceId")
222  .IsUnique();
223 
224  b.ToTable("DreamMakerSettings");
225  });
226 
227  modelBuilder.Entity("Tgstation.Server.Host.Models.DualReattachInformation", b =>
228  {
229  b.Property<long>("Id")
230  .ValueGeneratedOnAdd()
231  .HasColumnType("bigint")
232  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
233 
234  b.Property<long?>("AlphaId")
235  .HasColumnType("bigint");
236 
237  b.Property<bool>("AlphaIsActive")
238  .HasColumnType("boolean");
239 
240  b.Property<long?>("BravoId")
241  .HasColumnType("bigint");
242 
243  b.Property<long>("InstanceId")
244  .HasColumnType("bigint");
245 
246  b.HasKey("Id");
247 
248  b.HasIndex("AlphaId");
249 
250  b.HasIndex("BravoId");
251 
252  b.HasIndex("InstanceId")
253  .IsUnique();
254 
255  b.ToTable("WatchdogReattachInformations");
256  });
257 
258  modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
259  {
260  b.Property<long>("Id")
261  .ValueGeneratedOnAdd()
262  .HasColumnType("bigint")
263  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
264 
265  b.Property<long>("AutoUpdateInterval")
266  .HasColumnType("bigint");
267 
268  b.Property<int>("ChatBotLimit")
269  .HasColumnType("integer");
270 
271  b.Property<int>("ConfigurationType")
272  .HasColumnType("integer");
273 
274  b.Property<string>("Name")
275  .IsRequired()
276  .HasColumnType("character varying(10000)")
277  .HasMaxLength(10000);
278 
279  b.Property<bool?>("Online")
280  .IsRequired()
281  .HasColumnType("boolean");
282 
283  b.Property<string>("Path")
284  .IsRequired()
285  .HasColumnType("text");
286 
287  b.HasKey("Id");
288 
289  b.HasIndex("Path")
290  .IsUnique();
291 
292  b.ToTable("Instances");
293  });
294 
295  modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
296  {
297  b.Property<long>("Id")
298  .ValueGeneratedOnAdd()
299  .HasColumnType("bigint")
300  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
301 
302  b.Property<decimal>("ByondRights")
303  .HasColumnType("numeric(20,0)");
304 
305  b.Property<decimal>("ChatBotRights")
306  .HasColumnType("numeric(20,0)");
307 
308  b.Property<decimal>("ConfigurationRights")
309  .HasColumnType("numeric(20,0)");
310 
311  b.Property<decimal>("DreamDaemonRights")
312  .HasColumnType("numeric(20,0)");
313 
314  b.Property<decimal>("DreamMakerRights")
315  .HasColumnType("numeric(20,0)");
316 
317  b.Property<long>("InstanceId")
318  .HasColumnType("bigint");
319 
320  b.Property<decimal>("InstanceUserRights")
321  .HasColumnType("numeric(20,0)");
322 
323  b.Property<decimal>("RepositoryRights")
324  .HasColumnType("numeric(20,0)");
325 
326  b.Property<long?>("UserId")
327  .IsRequired()
328  .HasColumnType("bigint");
329 
330  b.HasKey("Id");
331 
332  b.HasIndex("InstanceId");
333 
334  b.HasIndex("UserId", "InstanceId")
335  .IsUnique();
336 
337  b.ToTable("InstanceUsers");
338  });
339 
340  modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
341  {
342  b.Property<long>("Id")
343  .ValueGeneratedOnAdd()
344  .HasColumnType("bigint")
345  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
346 
347  b.Property<decimal?>("CancelRight")
348  .HasColumnType("numeric(20,0)");
349 
350  b.Property<decimal?>("CancelRightsType")
351  .HasColumnType("numeric(20,0)");
352 
353  b.Property<bool?>("Cancelled")
354  .IsRequired()
355  .HasColumnType("boolean");
356 
357  b.Property<long?>("CancelledById")
358  .HasColumnType("bigint");
359 
360  b.Property<string>("Description")
361  .IsRequired()
362  .HasColumnType("text");
363 
364  b.Property<long?>("ErrorCode")
365  .HasColumnType("bigint");
366 
367  b.Property<string>("ExceptionDetails")
368  .HasColumnType("text");
369 
370  b.Property<long>("InstanceId")
371  .HasColumnType("bigint");
372 
373  b.Property<DateTimeOffset?>("StartedAt")
374  .IsRequired()
375  .HasColumnType("timestamp with time zone");
376 
377  b.Property<long>("StartedById")
378  .HasColumnType("bigint");
379 
380  b.Property<DateTimeOffset?>("StoppedAt")
381  .HasColumnType("timestamp with time zone");
382 
383  b.HasKey("Id");
384 
385  b.HasIndex("CancelledById");
386 
387  b.HasIndex("InstanceId");
388 
389  b.HasIndex("StartedById");
390 
391  b.ToTable("Jobs");
392  });
393 
394  modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
395  {
396  b.Property<long>("Id")
397  .ValueGeneratedOnAdd()
398  .HasColumnType("bigint")
399  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
400 
401  b.Property<string>("AccessIdentifier")
402  .IsRequired()
403  .HasColumnType("text");
404 
405  b.Property<long>("CompileJobId")
406  .HasColumnType("bigint");
407 
408  b.Property<bool>("IsPrimary")
409  .HasColumnType("boolean");
410 
411  b.Property<int>("LaunchSecurityLevel")
412  .HasColumnType("integer");
413 
414  b.Property<int>("Port")
415  .HasColumnType("integer");
416 
417  b.Property<int>("ProcessId")
418  .HasColumnType("integer");
419 
420  b.Property<int>("RebootState")
421  .HasColumnType("integer");
422 
423  b.HasKey("Id");
424 
425  b.HasIndex("CompileJobId");
426 
427  b.ToTable("ReattachInformations");
428  });
429 
430  modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
431  {
432  b.Property<long>("Id")
433  .ValueGeneratedOnAdd()
434  .HasColumnType("bigint")
435  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
436 
437  b.Property<string>("AccessToken")
438  .HasColumnType("character varying(10000)")
439  .HasMaxLength(10000);
440 
441  b.Property<string>("AccessUser")
442  .HasColumnType("character varying(10000)")
443  .HasMaxLength(10000);
444 
445  b.Property<bool?>("AutoUpdatesKeepTestMerges")
446  .IsRequired()
447  .HasColumnType("boolean");
448 
449  b.Property<bool?>("AutoUpdatesSynchronize")
450  .IsRequired()
451  .HasColumnType("boolean");
452 
453  b.Property<string>("CommitterEmail")
454  .IsRequired()
455  .HasColumnType("character varying(10000)")
456  .HasMaxLength(10000);
457 
458  b.Property<string>("CommitterName")
459  .IsRequired()
460  .HasColumnType("character varying(10000)")
461  .HasMaxLength(10000);
462 
463  b.Property<long>("InstanceId")
464  .HasColumnType("bigint");
465 
466  b.Property<bool?>("PostTestMergeComment")
467  .IsRequired()
468  .HasColumnType("boolean");
469 
470  b.Property<bool?>("PushTestMergeCommits")
471  .IsRequired()
472  .HasColumnType("boolean");
473 
474  b.Property<bool?>("ShowTestMergeCommitters")
475  .IsRequired()
476  .HasColumnType("boolean");
477 
478  b.HasKey("Id");
479 
480  b.HasIndex("InstanceId")
481  .IsUnique();
482 
483  b.ToTable("RepositorySettings");
484  });
485 
486  modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
487  {
488  b.Property<long>("Id")
489  .ValueGeneratedOnAdd()
490  .HasColumnType("bigint")
491  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
492 
493  b.Property<long>("RevisionInformationId")
494  .HasColumnType("bigint");
495 
496  b.Property<long>("TestMergeId")
497  .HasColumnType("bigint");
498 
499  b.HasKey("Id");
500 
501  b.HasIndex("RevisionInformationId");
502 
503  b.HasIndex("TestMergeId");
504 
505  b.ToTable("RevInfoTestMerges");
506  });
507 
508  modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
509  {
510  b.Property<long>("Id")
511  .ValueGeneratedOnAdd()
512  .HasColumnType("bigint")
513  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
514 
515  b.Property<string>("CommitSha")
516  .IsRequired()
517  .HasColumnType("character varying(40)")
518  .HasMaxLength(40);
519 
520  b.Property<long>("InstanceId")
521  .HasColumnType("bigint");
522 
523  b.Property<string>("OriginCommitSha")
524  .IsRequired()
525  .HasColumnType("character varying(40)")
526  .HasMaxLength(40);
527 
528  b.HasKey("Id");
529 
530  b.HasIndex("InstanceId", "CommitSha")
531  .IsUnique();
532 
533  b.ToTable("RevisionInformations");
534  });
535 
536  modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
537  {
538  b.Property<long>("Id")
539  .ValueGeneratedOnAdd()
540  .HasColumnType("bigint")
541  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
542 
543  b.Property<string>("Author")
544  .IsRequired()
545  .HasColumnType("text");
546 
547  b.Property<string>("BodyAtMerge")
548  .IsRequired()
549  .HasColumnType("text");
550 
551  b.Property<string>("Comment")
552  .HasColumnType("character varying(10000)")
553  .HasMaxLength(10000);
554 
555  b.Property<DateTimeOffset>("MergedAt")
556  .HasColumnType("timestamp with time zone");
557 
558  b.Property<long>("MergedById")
559  .HasColumnType("bigint");
560 
561  b.Property<int>("Number")
562  .HasColumnType("integer");
563 
564  b.Property<long?>("PrimaryRevisionInformationId")
565  .IsRequired()
566  .HasColumnType("bigint");
567 
568  b.Property<string>("PullRequestRevision")
569  .IsRequired()
570  .HasColumnType("character varying(40)")
571  .HasMaxLength(40);
572 
573  b.Property<string>("TitleAtMerge")
574  .IsRequired()
575  .HasColumnType("text");
576 
577  b.Property<string>("Url")
578  .IsRequired()
579  .HasColumnType("text");
580 
581  b.HasKey("Id");
582 
583  b.HasIndex("MergedById");
584 
585  b.HasIndex("PrimaryRevisionInformationId")
586  .IsUnique();
587 
588  b.ToTable("TestMerges");
589  });
590 
591  modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
592  {
593  b.Property<long?>("Id")
594  .ValueGeneratedOnAdd()
595  .HasColumnType("bigint")
596  .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
597 
598  b.Property<decimal>("AdministrationRights")
599  .HasColumnType("numeric(20,0)");
600 
601  b.Property<string>("CanonicalName")
602  .IsRequired()
603  .HasColumnType("text");
604 
605  b.Property<DateTimeOffset?>("CreatedAt")
606  .IsRequired()
607  .HasColumnType("timestamp with time zone");
608 
609  b.Property<long?>("CreatedById")
610  .HasColumnType("bigint");
611 
612  b.Property<bool?>("Enabled")
613  .IsRequired()
614  .HasColumnType("boolean");
615 
616  b.Property<decimal>("InstanceManagerRights")
617  .HasColumnType("numeric(20,0)");
618 
619  b.Property<DateTimeOffset?>("LastPasswordUpdate")
620  .HasColumnType("timestamp with time zone");
621 
622  b.Property<string>("Name")
623  .IsRequired()
624  .HasColumnType("character varying(10000)")
625  .HasMaxLength(10000);
626 
627  b.Property<string>("PasswordHash")
628  .HasColumnType("text");
629 
630  b.Property<string>("SystemIdentifier")
631  .HasColumnType("text");
632 
633  b.HasKey("Id");
634 
635  b.HasIndex("CanonicalName")
636  .IsUnique();
637 
638  b.HasIndex("CreatedById");
639 
640  b.HasIndex("SystemIdentifier")
641  .IsUnique();
642 
643  b.ToTable("Users");
644  });
645 
646  modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
647  {
648  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
649  .WithMany("ChatSettings")
650  .HasForeignKey("InstanceId")
651  .OnDelete(DeleteBehavior.Cascade)
652  .IsRequired();
653  });
654 
655  modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
656  {
657  b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings")
658  .WithMany("Channels")
659  .HasForeignKey("ChatSettingsId")
660  .OnDelete(DeleteBehavior.Cascade)
661  .IsRequired();
662  });
663 
664  modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
665  {
666  b.HasOne("Tgstation.Server.Host.Models.Job", "Job")
667  .WithOne()
668  .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId")
669  .OnDelete(DeleteBehavior.Cascade)
670  .IsRequired();
671 
672  b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
673  .WithMany("CompileJobs")
674  .HasForeignKey("RevisionInformationId")
675  .OnDelete(DeleteBehavior.ClientNoAction)
676  .IsRequired();
677  });
678 
679  modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
680  {
681  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
682  .WithOne("DreamDaemonSettings")
683  .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId")
684  .OnDelete(DeleteBehavior.Cascade)
685  .IsRequired();
686  });
687 
688  modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
689  {
690  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
691  .WithOne("DreamMakerSettings")
692  .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId")
693  .OnDelete(DeleteBehavior.Cascade)
694  .IsRequired();
695  });
696 
697  modelBuilder.Entity("Tgstation.Server.Host.Models.DualReattachInformation", b =>
698  {
699  b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Alpha")
700  .WithMany()
701  .HasForeignKey("AlphaId");
702 
703  b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo")
704  .WithMany()
705  .HasForeignKey("BravoId");
706 
707  b.HasOne("Tgstation.Server.Host.Models.Instance", null)
708  .WithOne("WatchdogReattachInformation")
709  .HasForeignKey("Tgstation.Server.Host.Models.DualReattachInformation", "InstanceId")
710  .OnDelete(DeleteBehavior.Cascade)
711  .IsRequired();
712  });
713 
714  modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b =>
715  {
716  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
717  .WithMany("InstanceUsers")
718  .HasForeignKey("InstanceId")
719  .OnDelete(DeleteBehavior.Cascade)
720  .IsRequired();
721 
722  b.HasOne("Tgstation.Server.Host.Models.User", null)
723  .WithMany("InstanceUsers")
724  .HasForeignKey("UserId")
725  .OnDelete(DeleteBehavior.Cascade)
726  .IsRequired();
727  });
728 
729  modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
730  {
731  b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy")
732  .WithMany()
733  .HasForeignKey("CancelledById");
734 
735  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
736  .WithMany("Jobs")
737  .HasForeignKey("InstanceId")
738  .OnDelete(DeleteBehavior.Cascade)
739  .IsRequired();
740 
741  b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy")
742  .WithMany()
743  .HasForeignKey("StartedById")
744  .OnDelete(DeleteBehavior.Cascade)
745  .IsRequired();
746  });
747 
748  modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
749  {
750  b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
751  .WithMany()
752  .HasForeignKey("CompileJobId")
753  .OnDelete(DeleteBehavior.Cascade)
754  .IsRequired();
755  });
756 
757  modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
758  {
759  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
760  .WithOne("RepositorySettings")
761  .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId")
762  .OnDelete(DeleteBehavior.Cascade)
763  .IsRequired();
764  });
765 
766  modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
767  {
768  b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
769  .WithMany("ActiveTestMerges")
770  .HasForeignKey("RevisionInformationId")
771  .OnDelete(DeleteBehavior.Cascade)
772  .IsRequired();
773 
774  b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge")
775  .WithMany("RevisonInformations")
776  .HasForeignKey("TestMergeId")
777  .OnDelete(DeleteBehavior.ClientNoAction)
778  .IsRequired();
779  });
780 
781  modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
782  {
783  b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
784  .WithMany("RevisionInformations")
785  .HasForeignKey("InstanceId")
786  .OnDelete(DeleteBehavior.Cascade)
787  .IsRequired();
788  });
789 
790  modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
791  {
792  b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy")
793  .WithMany("TestMerges")
794  .HasForeignKey("MergedById")
795  .OnDelete(DeleteBehavior.Restrict)
796  .IsRequired();
797 
798  b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation")
799  .WithOne("PrimaryTestMerge")
800  .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId")
801  .OnDelete(DeleteBehavior.Cascade)
802  .IsRequired();
803  });
804 
805  modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
806  {
807  b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy")
808  .WithMany("CreatedUsers")
809  .HasForeignKey("CreatedById");
810  });
811 #pragma warning restore 612, 618
812  }
813  }
814 }
+ + + + + + + + + + + +
+ + + + diff --git a/_postgres_sql_design_time_db_context_factory_8cs.html b/_postgres_sql_design_time_db_context_factory_8cs.html new file mode 100644 index 0000000000..a4b1eec059 --- /dev/null +++ b/_postgres_sql_design_time_db_context_factory_8cs.html @@ -0,0 +1,121 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs File Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
PostgresSqlDesignTimeDbContextFactory.cs File Reference
+
+ + + + + diff --git a/_postgres_sql_design_time_db_context_factory_8cs_source.html b/_postgres_sql_design_time_db_context_factory_8cs_source.html new file mode 100644 index 0000000000..f2ebbee6f7 --- /dev/null +++ b/_postgres_sql_design_time_db_context_factory_8cs_source.html @@ -0,0 +1,123 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs Source File + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.0 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
PostgresSqlDesignTimeDbContextFactory.cs
+
+
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
9 
10 namespace Tgstation.Server.Host.Database.Design
11 {
13  sealed class PostgresSqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<PostgresSqlDatabaseContext>
14  {
17  {
18  using var loggerFactory = new LoggerFactory();
19  return new PostgresSqlDatabaseContext(
20  new DbContextOptions<PostgresSqlDatabaseContext>(),
22  DatabaseType.PostgresSql,
23  "Application Name=tgstation-server;Host=127.0.0.1;Password=qCkWimNgLfWwpr7TnUHs;Username=postgres;Database=TGS_Design"),
24  new DatabaseSeeder(
26  new PasswordHasher<User>()),
27  new PlatformIdentifier()),
28  loggerFactory.CreateLogger<PostgresSqlDatabaseContext>());
29  }
30  }
31 }
+ + +
static IOptions< DatabaseConfiguration > GetDatabaseConfiguration(DatabaseType databaseType, string connectionString)
Get the IOptions<TOptions> for the DatabaseConfiguration
+ + + + + + + +
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
+ + + + + + +
+ + + + diff --git a/_process_8cs.html b/_process_8cs.html index 812582226f..d52c2bd0fb 100644 --- a/_process_8cs.html +++ b/_process_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_process_8cs_source.html b/_process_8cs_source.html index c004804688..ae58bcd504 100644 --- a/_process_8cs_source.html +++ b/_process_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_process_executor_8cs.html b/_process_executor_8cs.html index 9c2ec99b84..db26fafada 100644 --- a/_process_executor_8cs.html +++ b/_process_executor_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_process_executor_8cs_source.html b/_process_executor_8cs_source.html index afa8bea50a..3a51fd1b48 100644 --- a/_process_executor_8cs_source.html +++ b/_process_executor_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_program_8cs.html b/_program_8cs.html index 9597fdf28e..f3fcd21a80 100644 --- a/_program_8cs.html +++ b/_program_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_program_8cs_source.html b/_program_8cs_source.html index 97784e067a..edbf9501c1 100644 --- a/_program_8cs_source.html +++ b/_program_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Entrypoint for the Process
Definition: Program.cs:15
Sets up dependency injection.
Definition: Application.cs:47
-
Implementation of IServerFactory.
+
Implementation of IServerFactory.
Contains a CancellationToken that triggers when the operating system requests the program shuts down...
Represents the host
Definition: IServer.cs:9
diff --git a/_program_shutdown_token_source_8cs.html b/_program_shutdown_token_source_8cs.html index 0b438dea31..69f98bb7cc 100644 --- a/_program_shutdown_token_source_8cs.html +++ b/_program_shutdown_token_source_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_program_shutdown_token_source_8cs_source.html b/_program_shutdown_token_source_8cs_source.html index 6aaf9aef32..f82da758e1 100644 --- a/_program_shutdown_token_source_8cs_source.html +++ b/_program_shutdown_token_source_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_provider_8cs.html b/_provider_8cs.html index e0b079860b..01ffc3d7e8 100644 --- a/_provider_8cs.html +++ b/_provider_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_provider_8cs_source.html b/_provider_8cs_source.html index ba5c8acf26..e97cd061f0 100644 --- a/_provider_8cs_source.html +++ b/_provider_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,23 +94,27 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Provider.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Threading;
5 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Components.Chat.Providers
8 {
10  abstract class Provider : IProvider
11  {
15  protected ILogger Logger { get; }
16 
20  readonly Queue<Message> messageQueue;
21 
25  readonly object reconnectTaskLock;
26 
30  TaskCompletionSource<object> nextMessage;
31 
36 
40  CancellationTokenSource reconnectCts;
41 
47  protected Provider(ILogger logger, uint reconnectInterval)
48  {
49  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
50 
51  messageQueue = new Queue<Message>();
52  nextMessage = new TaskCompletionSource<object>();
53 
54  reconnectTaskLock = new object();
55 
56  SetReconnectInterval(reconnectInterval).GetAwaiter().GetResult();
57  logger.LogTrace("Created.");
58  }
59 
61  public abstract bool Connected { get; }
62 
64  public abstract string BotMention { get; }
65 
70  protected void EnqueueMessage(Message message)
71  {
72  lock (messageQueue)
73  {
74  messageQueue.Enqueue(message);
75  nextMessage.TrySetResult(null);
76  }
77  }
78 
80  public virtual void Dispose()
81  {
82  StopReconnectionTimer().GetAwaiter().GetResult();
83  Logger.LogTrace("Disposed");
84  }
85 
87  public abstract Task<bool> Connect(CancellationToken cancellationToken);
88 
90  public abstract Task Disconnect(CancellationToken cancellationToken);
91 
93  public abstract Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken);
94 
96  public async Task<Message> NextMessage(CancellationToken cancellationToken)
97  {
98  var cancelTcs = new TaskCompletionSource<object>();
99  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
100  await Task.WhenAny(nextMessage.Task, cancelTcs.Task).ConfigureAwait(false);
101  cancellationToken.ThrowIfCancellationRequested();
102  lock (messageQueue)
103  {
104  var result = messageQueue.Dequeue();
105  if (messageQueue.Count == 0)
106  nextMessage = new TaskCompletionSource<object>();
107  return result;
108  }
109  }
110 
116  {
117  lock (reconnectTaskLock)
118  if (reconnectCts != null)
119  {
120  reconnectCts.Cancel();
121  reconnectCts.Dispose();
122  reconnectCts = null;
123  Task reconnectTask = this.reconnectTask;
124  this.reconnectTask = null;
125  return reconnectTask;
126  }
127 
128  return Task.CompletedTask;
129  }
130 
132  public Task SetReconnectInterval(uint reconnectInterval)
133  {
134  if (reconnectInterval == 0)
135  throw new ArgumentOutOfRangeException(nameof(reconnectInterval), reconnectInterval, "Reconnect interval cannot be zero!");
136 
137  Task stopOldTimerTask;
138  lock (reconnectTaskLock)
139  {
140  stopOldTimerTask = StopReconnectionTimer();
141  reconnectCts = new CancellationTokenSource();
142  reconnectTask = ReconnectionLoop(reconnectInterval, reconnectCts.Token);
143  }
144 
145  return stopOldTimerTask;
146  }
147 
154  async Task ReconnectionLoop(uint reconnectInterval, CancellationToken cancellationToken)
155  {
156  do
157  {
158  try
159  {
160  await Task.Delay(TimeSpan.FromMinutes(reconnectInterval), cancellationToken).ConfigureAwait(false);
161  if (!Connected)
162  {
163  Logger.LogInformation("Attempting to reconnect provider...");
164  await Disconnect(cancellationToken).ConfigureAwait(false);
165  if (await Connect(cancellationToken).ConfigureAwait(false))
166  EnqueueMessage(null);
167  }
168  }
169  catch (OperationCanceledException)
170  {
171  break;
172  }
173  catch(Exception e)
174  {
175  Logger.LogError(e, "Error reconnecting!");
176  }
177  }
178  while (true);
179  }
180 
182  public abstract Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
183  }
184 }
-
readonly object reconnectTaskLock
Used for synchronizing access to reconnectCts and reconnectTask.
Definition: Provider.cs:25
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Threading;
5 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Components.Chat.Providers
9 {
11  abstract class Provider : IProvider
12  {
16  protected ILogger Logger { get; }
17 
21  readonly Queue<Message> messageQueue;
22 
26  readonly object reconnectTaskLock;
27 
31  TaskCompletionSource<object> nextMessage;
32 
37 
41  CancellationTokenSource reconnectCts;
42 
48  protected Provider(ILogger logger, uint reconnectInterval)
49  {
50  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
51 
52  messageQueue = new Queue<Message>();
53  nextMessage = new TaskCompletionSource<object>();
54 
55  reconnectTaskLock = new object();
56 
57  SetReconnectInterval(reconnectInterval).GetAwaiter().GetResult();
58  logger.LogTrace("Created.");
59  }
60 
62  public abstract bool Connected { get; }
63 
65  public abstract string BotMention { get; }
66 
71  protected void EnqueueMessage(Message message)
72  {
73  lock (messageQueue)
74  {
75  messageQueue.Enqueue(message);
76  nextMessage.TrySetResult(null);
77  }
78  }
79 
81  public virtual void Dispose()
82  {
83  StopReconnectionTimer().GetAwaiter().GetResult();
84  Logger.LogTrace("Disposed");
85  }
86 
88  public abstract Task<bool> Connect(CancellationToken cancellationToken);
89 
91  public abstract Task Disconnect(CancellationToken cancellationToken);
92 
94  public abstract Task<IReadOnlyCollection<ChannelRepresentation>> MapChannels(IEnumerable<Api.Models.ChatChannel> channels, CancellationToken cancellationToken);
95 
97  public async Task<Message> NextMessage(CancellationToken cancellationToken)
98  {
99  var cancelTcs = new TaskCompletionSource<object>();
100  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
101  await Task.WhenAny(nextMessage.Task, cancelTcs.Task).ConfigureAwait(false);
102  cancellationToken.ThrowIfCancellationRequested();
103  lock (messageQueue)
104  {
105  var result = messageQueue.Dequeue();
106  if (messageQueue.Count == 0)
107  nextMessage = new TaskCompletionSource<object>();
108  return result;
109  }
110  }
111 
117  {
118  lock (reconnectTaskLock)
119  if (reconnectCts != null)
120  {
121  reconnectCts.Cancel();
122  reconnectCts.Dispose();
123  reconnectCts = null;
124  Task reconnectTask = this.reconnectTask;
125  this.reconnectTask = null;
126  return reconnectTask;
127  }
128 
129  return Task.CompletedTask;
130  }
131 
133  public Task SetReconnectInterval(uint reconnectInterval)
134  {
135  if (reconnectInterval == 0)
136  throw new ArgumentOutOfRangeException(nameof(reconnectInterval), reconnectInterval, "Reconnect interval cannot be zero!");
137 
138  Task stopOldTimerTask;
139  lock (reconnectTaskLock)
140  {
141  stopOldTimerTask = StopReconnectionTimer();
142  reconnectCts = new CancellationTokenSource();
143  reconnectTask = ReconnectionLoop(reconnectInterval, reconnectCts.Token);
144  }
145 
146  return stopOldTimerTask;
147  }
148 
155  async Task ReconnectionLoop(uint reconnectInterval, CancellationToken cancellationToken)
156  {
157  do
158  {
159  try
160  {
161  await Task.Delay(TimeSpan.FromMinutes(reconnectInterval), cancellationToken).ConfigureAwait(false);
162  if (!Connected)
163  {
164  Logger.LogInformation("Attempting to reconnect provider...");
165  await Disconnect(cancellationToken).ConfigureAwait(false);
166  if (await Connect(cancellationToken).ConfigureAwait(false))
167  EnqueueMessage(null);
168  }
169  }
170  catch (OperationCanceledException)
171  {
172  break;
173  }
174  catch(Exception e)
175  {
176  Logger.LogError(e, "Error reconnecting!");
177  }
178  }
179  while (true);
180  }
181 
183  public abstract Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken);
184 
186  public abstract Task<Func<string, string, Task>> SendUpdateMessage(
187  RevisionInformation revisionInformation,
188  Version byondVersion,
189  DateTimeOffset? estimatedCompletionTime,
190  string gitHubOwner,
191  string gitHubRepo,
192  ulong channelId,
193  bool localCommitPushed,
194  CancellationToken cancellationToken);
195  }
196 }
+
readonly object reconnectTaskLock
Used for synchronizing access to reconnectCts and reconnectTask.
Definition: Provider.cs:26
- -
CancellationTokenSource reconnectCts
CancellationTokenSource for reconnectTask
Definition: Provider.cs:40
+ + +
CancellationTokenSource reconnectCts
CancellationTokenSource for reconnectTask
Definition: Provider.cs:41
- +
Represents a message recieved by a IProvider
Definition: Message.cs:6
-
For interacting with a chat service
Definition: IProvider.cs:11
-
async Task< Message > NextMessage(CancellationToken cancellationToken)
Get a Task<TResult> resulting in the next Message the IProvider recieves or on a disconnect ...
Definition: Provider.cs:96
-
TaskCompletionSource< object > nextMessage
TaskCompletionSource<TResult> that completes while messageQueue isn&#39;t empty
Definition: Provider.cs:30
-
Task SetReconnectInterval(uint reconnectInterval)
Set the interval at which the provider tries to reconnect.
Definition: Provider.cs:132
-
Task StopReconnectionTimer()
Stops and awaits the reconnectTask.
Definition: Provider.cs:115
-
Provider(ILogger logger, uint reconnectInterval)
Construct a Provider
Definition: Provider.cs:47
-
readonly Queue< Message > messageQueue
Queue<T> of received Messages
Definition: Provider.cs:20
-
async Task ReconnectionLoop(uint reconnectInterval, CancellationToken cancellationToken)
Creates a Task that will attempt to reconnect the Provider every reconnectInterval minutes...
Definition: Provider.cs:154
-
void EnqueueMessage(Message message)
Queues a message for NextMessage(CancellationToken)
Definition: Provider.cs:70
+
For interacting with a chat service
Definition: IProvider.cs:12
+
async Task< Message > NextMessage(CancellationToken cancellationToken)
Get a Task<TResult> resulting in the next Message the IProvider recieves or on a disconnect ...
Definition: Provider.cs:97
+ +
TaskCompletionSource< object > nextMessage
TaskCompletionSource<TResult> that completes while messageQueue isn&#39;t empty
Definition: Provider.cs:31
+
Task SetReconnectInterval(uint reconnectInterval)
Set the interval at which the provider tries to reconnect.
Definition: Provider.cs:133
+
Task StopReconnectionTimer()
Stops and awaits the reconnectTask.
Definition: Provider.cs:116
+ +
Provider(ILogger logger, uint reconnectInterval)
Construct a Provider
Definition: Provider.cs:48
+
readonly Queue< Message > messageQueue
Queue<T> of received Messages
Definition: Provider.cs:21
+
async Task ReconnectionLoop(uint reconnectInterval, CancellationToken cancellationToken)
Creates a Task that will attempt to reconnect the Provider every reconnectInterval minutes...
Definition: Provider.cs:155
+ +
void EnqueueMessage(Message message)
Queues a message for NextMessage(CancellationToken)
Definition: Provider.cs:71
diff --git a/_provider_factory_8cs.html b/_provider_factory_8cs.html index 57f66fe54e..5cb5383cf1 100644 --- a/_provider_factory_8cs.html +++ b/_provider_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_provider_factory_8cs_source.html b/_provider_factory_8cs_source.html index d43d846c1f..95e21e4d91 100644 --- a/_provider_factory_8cs_source.html +++ b/_provider_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,15 +94,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ProviderFactory.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
7 
8 namespace Tgstation.Server.Host.Components.Chat.Providers
9 {
12  {
17 
22 
26  readonly ILoggerFactory loggerFactory;
27 
35  IAssemblyInformationProvider assemblyInformationProvider,
36  IAsyncDelayer asyncDelayer,
37  ILoggerFactory loggerFactory)
38  {
39  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
40  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
41  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
42  }
43 
45  public IProvider CreateProvider(Api.Models.Internal.ChatBot settings)
46  {
47  if (settings == null)
48  throw new ArgumentNullException(nameof(settings));
49  var builder = settings.CreateConnectionStringBuilder();
50  if (builder == null || !builder.Valid)
51  throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!");
52  switch (settings.Provider)
53  {
54  case ChatProvider.Irc:
55  var ircBuilder = (IrcConnectionStringBuilder)builder;
56  return new IrcProvider(assemblyInformationProvider, asyncDelayer, loggerFactory.CreateLogger<IrcProvider>(), ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, settings.ReconnectionInterval.Value, ircBuilder.UseSsl.Value);
57  case ChatProvider.Discord:
58  var discordBuilder = (DiscordConnectionStringBuilder)builder;
59  return new DiscordProvider(loggerFactory.CreateLogger<DiscordProvider>(), discordBuilder.BotToken, settings.ReconnectionInterval.Value);
60  default:
61  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider));
62  }
63  }
64  }
65 }
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProviderFactory
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
7 
8 namespace Tgstation.Server.Host.Components.Chat.Providers
9 {
12  {
17 
22 
26  readonly ILoggerFactory loggerFactory;
27 
35  IAssemblyInformationProvider assemblyInformationProvider,
36  IAsyncDelayer asyncDelayer,
37  ILoggerFactory loggerFactory)
38  {
39  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
40  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
41  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
42  }
43 
45  public IProvider CreateProvider(Api.Models.Internal.ChatBot settings)
46  {
47  if (settings == null)
48  throw new ArgumentNullException(nameof(settings));
49  var builder = settings.CreateConnectionStringBuilder();
50  if (builder == null || !builder.Valid)
51  throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!");
52  switch (settings.Provider)
53  {
54  case ChatProvider.Irc:
55  var ircBuilder = (IrcConnectionStringBuilder)builder;
56  return new IrcProvider(assemblyInformationProvider, asyncDelayer, loggerFactory.CreateLogger<IrcProvider>(), ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, settings.ReconnectionInterval.Value, ircBuilder.UseSsl.Value);
57  case ChatProvider.Discord:
58  var discordBuilder = (DiscordConnectionStringBuilder)builder;
59  return new DiscordProvider(
60  assemblyInformationProvider,
61  loggerFactory.CreateLogger<DiscordProvider>(),
62  discordBuilder.BotToken,
63  settings.ReconnectionInterval.Value);
64  default:
65  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider));
66  }
67  }
68  }
69 }
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProviderFactory
- +
ChatConnectionStringBuilder for ChatProvider.Discord
-
For interacting with a chat service
Definition: IProvider.cs:11
+
For interacting with a chat service
Definition: IProvider.cs:12
ProviderFactory(IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory)
Construct a ProviderFactory
diff --git a/_pull_requests_command_8cs.html b/_pull_requests_command_8cs.html index ec3dccc8ea..904c56de65 100644 --- a/_pull_requests_command_8cs.html +++ b/_pull_requests_command_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_pull_requests_command_8cs_source.html b/_pull_requests_command_8cs_source.html index c32ff51572..d9bc0a89e4 100644 --- a/_pull_requests_command_8cs_source.html +++ b/_pull_requests_command_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
PullRequestsCommand.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Components.Chat.Commands
13 {
18  {
20  public string Name => "prs";
21 
23  public string HelpText => "Display live test merge pull request numbers. Add --repo to view repository test merges";
24 
26  public bool AdminOnly => false;
27 
31  readonly IWatchdog watchdog;
32 
37 
42 
46  readonly Models.Instance instance;
47 
55  public PullRequestsCommand(IWatchdog watchdog, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
56  {
57  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
58  this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
59  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
60  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
61  }
62 
64  // TODO: Decomplexify
65  #pragma warning disable CA1506
66  public async Task<string> Invoke(string arguments, ChatUser user, CancellationToken cancellationToken)
67  {
68  IEnumerable<Models.TestMerge> results = null;
69  if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--REPO"))
70  {
71  string head;
72  using (var repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
73  {
74  if (repo == null)
75  return "Repository unavailable!";
76  head = repo.Head;
77  }
78 
79  await databaseContextFactory.UseContext(async db => results = await db.RevisionInformations.Where(x => x.Instance.Id == instance.Id && x.CommitSha == head)
80  .SelectMany(x => x.ActiveTestMerges)
81  .Select(x => x.TestMerge)
82  .Select(x => new Models.TestMerge
83  {
84  Number = x.Number,
85  PullRequestRevision = x.PullRequestRevision
86  }).ToListAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
87  }
88  else
89  {
90  if (!watchdog.Running)
91  return "Server offline!";
92  results = watchdog.ActiveCompileJob?.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList() ?? new List<Models.TestMerge>();
93  }
94 
95  return !results.Any() ? "None!" : String.Join(", ", results.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7))));
96  }
97  #pragma warning restore CA1506
98  }
99 }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the PullRequestsCommand
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Components.Chat.Commands
13 {
18  {
20  public string Name => "prs";
21 
23  public string HelpText => "Display live test merge pull request numbers. Add --repo to view repository test merges";
24 
26  public bool AdminOnly => false;
27 
31  readonly IWatchdog watchdog;
32 
37 
42 
46  readonly Models.Instance instance;
47 
55  public PullRequestsCommand(IWatchdog watchdog, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
56  {
57  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
58  this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
59  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
60  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
61  }
62 
64  // TODO: Decomplexify
65  #pragma warning disable CA1506
66  public async Task<string> Invoke(string arguments, ChatUser user, CancellationToken cancellationToken)
67  {
68  IEnumerable<Models.TestMerge> results = null;
69  if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--REPO"))
70  {
71  string head;
72  using (var repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
73  {
74  if (repo == null)
75  return "Repository unavailable!";
76  head = repo.Head;
77  }
78 
79  await databaseContextFactory.UseContext(
80  async db => results = await db
81  .RevisionInformations
82  .AsQueryable()
83  .Where(x => x.Instance.Id == instance.Id && x.CommitSha == head)
84  .SelectMany(x => x.ActiveTestMerges)
85  .Select(x => x.TestMerge)
86  .Select(x => new Models.TestMerge
87  {
88  Number = x.Number,
89  PullRequestRevision = x.PullRequestRevision
90  })
91  .ToListAsync(cancellationToken)
92  .ConfigureAwait(false))
93  .ConfigureAwait(false);
94  }
95  else
96  {
97  if (!watchdog.Running)
98  return "Server offline!";
99  results = watchdog.ActiveCompileJob?.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList() ?? new List<Models.TestMerge>();
100  }
101 
102  return !results.Any() ? "None!" : String.Join(", ", results.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7))));
103  }
104  #pragma warning restore CA1506
105  }
106 }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the PullRequestsCommand
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
@@ -112,7 +112,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
PullRequestsCommand(IWatchdog watchdog, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
Construct a PullRequestsCommand
Represents a command that can be invoked by talking to chat bots
Definition: ICommand.cs:9
- +
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:13
diff --git a/_r_e_a_d_m_e_8md.html b/_r_e_a_d_m_e_8md.html index 8bdcc8dc98..3a6c6cff40 100644 --- a/_r_e_a_d_m_e_8md.html +++ b/_r_e_a_d_m_e_8md.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_r_e_a_d_m_e_8md_source.html b/_r_e_a_d_m_e_8md_source.html index b82519015b..de8e672d41 100644 --- a/_r_e_a_d_m_e_8md_source.html +++ b/_r_e_a_d_m_e_8md_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_rate_limit_exception_8cs.html b/_rate_limit_exception_8cs.html index 1a85550c95..70cd33394b 100644 --- a/_rate_limit_exception_8cs.html +++ b/_rate_limit_exception_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_rate_limit_exception_8cs_source.html b/_rate_limit_exception_8cs_source.html index 9fde5bee7a..a568ab8e4e 100644 --- a/_rate_limit_exception_8cs_source.html +++ b/_rate_limit_exception_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_reattach_info_handler_8cs.html b/_reattach_info_handler_8cs.html index 821dd0b8dc..dfabcb4b26 100644 --- a/_reattach_info_handler_8cs.html +++ b/_reattach_info_handler_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_reattach_info_handler_8cs_source.html b/_reattach_info_handler_8cs_source.html index 6f30a3c037..864977597f 100644 --- a/_reattach_info_handler_8cs_source.html +++ b/_reattach_info_handler_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ReattachInfoHandler.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Linq;
5 using System.Threading;
6 using System.Threading.Tasks;
9 using Z.EntityFramework.Plus;
10 
11 namespace Tgstation.Server.Host.Components.Session
12 {
15  {
20 
25 
29  readonly ILogger<ReattachInfoHandler> logger;
30 
34  readonly Api.Models.Instance metadata;
35 
43  public ReattachInfoHandler(IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, ILogger<ReattachInfoHandler> logger, Api.Models.Instance metadata)
44  {
45  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
46  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
47  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
48  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
49  }
50 
52  public Task Save(DualReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
53  {
54  if (reattachInformation == null)
55  throw new ArgumentNullException(nameof(reattachInformation));
56 
57  logger.LogDebug("Saving reattach information: {0}...", reattachInformation);
58 
59  var deleteTask = db.WatchdogReattachInformations.Where(x => x.InstanceId == metadata.Id).DeleteAsync(cancellationToken);
60 
61  Models.ReattachInformation ConvertReattachInfo(ReattachInformation wdInfo)
62  {
63  if (wdInfo == null)
64  return null;
65  db.CompileJobs.Attach(wdInfo.Dmb.CompileJob);
66  return new Models.ReattachInformation
67  {
68  AccessIdentifier = wdInfo.AccessIdentifier,
69  CompileJob = wdInfo.Dmb.CompileJob,
70  IsPrimary = wdInfo.IsPrimary,
71  Port = wdInfo.Port,
72  ProcessId = wdInfo.ProcessId,
73  RebootState = wdInfo.RebootState,
74  LaunchSecurityLevel = wdInfo.LaunchSecurityLevel
75  };
76  }
77 
78  await deleteTask.ConfigureAwait(false);
79 
80  db.WatchdogReattachInformations.Add(new Models.DualReattachInformation
81  {
82  Alpha = ConvertReattachInfo(reattachInformation.Alpha),
83  Bravo = ConvertReattachInfo(reattachInformation.Bravo),
84  AlphaIsActive = reattachInformation.AlphaIsActive,
85  InstanceId = metadata.Id
86  });
87  await db.Save(cancellationToken).ConfigureAwait(false);
88  });
89 
91  public async Task<DualReattachInformation> Load(CancellationToken cancellationToken)
92  {
93  Models.DualReattachInformation result = null;
94  await databaseContextFactory.UseContext(async (db) =>
95  {
96  var instance = await db.Instances.Where(x => x.Id == metadata.Id)
97  .Include(x => x.WatchdogReattachInformation).ThenInclude(x => x.Alpha).ThenInclude(x => x.CompileJob)
98  .Include(x => x.WatchdogReattachInformation).ThenInclude(x => x.Bravo).ThenInclude(x => x.CompileJob)
99  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
100  result = instance.WatchdogReattachInformation;
101  if (result == default)
102  return;
103  instance.WatchdogReattachInformation = null;
104  db.WatchdogReattachInformations.Remove(result);
105  await db.Save(cancellationToken).ConfigureAwait(false);
106  }).ConfigureAwait(false);
107 
108  if (result == default)
109  {
110  logger.LogDebug("Reattach information not found!");
111  return null;
112  }
113 
114  Task<IDmbProvider> GetDmbForReattachInfo(Models.ReattachInformation reattachInformation) => reattachInformation != null
115  ? dmbFactory.FromCompileJob(reattachInformation.CompileJob, cancellationToken)
116  : Task.FromResult<IDmbProvider>(null);
117 
118  var bravoDmbTask = GetDmbForReattachInfo(result.Bravo);
119  var info = new DualReattachInformation(result, await GetDmbForReattachInfo(result.Alpha).ConfigureAwait(false), await bravoDmbTask.ConfigureAwait(false));
120  logger.LogDebug("Reattach information loaded: {0}", info);
121  return info;
122  }
123  }
124 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Linq;
5 using System.Threading;
6 using System.Threading.Tasks;
9 using Z.EntityFramework.Plus;
10 
11 namespace Tgstation.Server.Host.Components.Session
12 {
15  {
20 
25 
29  readonly ILogger<ReattachInfoHandler> logger;
30 
34  readonly Api.Models.Instance metadata;
35 
43  public ReattachInfoHandler(IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, ILogger<ReattachInfoHandler> logger, Api.Models.Instance metadata)
44  {
45  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
46  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
47  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
48  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
49  }
50 
52  public Task Save(DualReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
53  {
54  if (reattachInformation == null)
55  throw new ArgumentNullException(nameof(reattachInformation));
56 
57  logger.LogDebug("Saving reattach information: {0}...", reattachInformation);
58 
59  var deleteTask = db
60  .WatchdogReattachInformations
61  .AsQueryable()
62  .Where(x => x.InstanceId == metadata.Id)
63  .DeleteAsync(cancellationToken);
64 
65  Models.ReattachInformation ConvertReattachInfo(ReattachInformation wdInfo)
66  {
67  if (wdInfo == null)
68  return null;
69  db.CompileJobs.Attach(wdInfo.Dmb.CompileJob);
70  return new Models.ReattachInformation
71  {
72  AccessIdentifier = wdInfo.AccessIdentifier,
73  CompileJob = wdInfo.Dmb.CompileJob,
74  IsPrimary = wdInfo.IsPrimary,
75  Port = wdInfo.Port,
76  ProcessId = wdInfo.ProcessId,
77  RebootState = wdInfo.RebootState,
78  LaunchSecurityLevel = wdInfo.LaunchSecurityLevel
79  };
80  }
81 
82  await deleteTask.ConfigureAwait(false);
83 
84  db.WatchdogReattachInformations.Add(new Models.DualReattachInformation
85  {
86  Alpha = ConvertReattachInfo(reattachInformation.Alpha),
87  Bravo = ConvertReattachInfo(reattachInformation.Bravo),
88  AlphaIsActive = reattachInformation.AlphaIsActive,
89  InstanceId = metadata.Id
90  });
91  await db.Save(cancellationToken).ConfigureAwait(false);
92  });
93 
95  public async Task<DualReattachInformation> Load(CancellationToken cancellationToken)
96  {
97  Models.DualReattachInformation result = null;
98  await databaseContextFactory.UseContext(async (db) =>
99  {
100  var instance = await db.Instances
101  .AsQueryable()
102  .Where(x => x.Id == metadata.Id)
103  .Include(x => x.WatchdogReattachInformation).ThenInclude(x => x.Alpha).ThenInclude(x => x.CompileJob)
104  .Include(x => x.WatchdogReattachInformation).ThenInclude(x => x.Bravo).ThenInclude(x => x.CompileJob)
105  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
106  result = instance.WatchdogReattachInformation;
107  if (result == default)
108  return;
109  instance.WatchdogReattachInformation = null;
110  db.WatchdogReattachInformations.Remove(result);
111  await db.Save(cancellationToken).ConfigureAwait(false);
112  }).ConfigureAwait(false);
113 
114  if (result == default)
115  {
116  logger.LogDebug("Reattach information not found!");
117  return null;
118  }
119 
120  Task<IDmbProvider> GetDmbForReattachInfo(Models.ReattachInformation reattachInformation) => reattachInformation != null
121  ? dmbFactory.FromCompileJob(reattachInformation.CompileJob, cancellationToken)
122  : Task.FromResult<IDmbProvider>(null);
123 
124  var bravoDmbTask = GetDmbForReattachInfo(result.Bravo);
125  var info = new DualReattachInformation(result, await GetDmbForReattachInfo(result.Alpha).ConfigureAwait(false), await bravoDmbTask.ConfigureAwait(false));
126  logger.LogDebug("Reattach information loaded: {0}", info);
127  return info;
128  }
129  }
130 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
@@ -121,8 +121,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IDmbFactory dmbFactory
The IDmbFactory for the ReattachInfoHandler
-
async Task< DualReattachInformation > Load(CancellationToken cancellationToken)
Load a saved DualReattachInformation
- +
async Task< DualReattachInformation > Load(CancellationToken cancellationToken)
Load a saved DualReattachInformation
+
diff --git a/_reattach_information_base_8cs.html b/_reattach_information_base_8cs.html index 679c712e23..95f173cd21 100644 --- a/_reattach_information_base_8cs.html +++ b/_reattach_information_base_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_reattach_information_base_8cs_source.html b/_reattach_information_base_8cs_source.html index 0f676c8522..022cbf8751 100644 --- a/_reattach_information_base_8cs_source.html +++ b/_reattach_information_base_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_reboot_state_8cs.html b/_reboot_state_8cs.html index b9ec2273cc..7c658ea835 100644 --- a/_reboot_state_8cs.html +++ b/_reboot_state_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_reboot_state_8cs_source.html b/_reboot_state_8cs_source.html index 0c61b31441..90b874ab94 100644 --- a/_reboot_state_8cs_source.html +++ b/_reboot_state_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_repository_client_8cs.html b/_repository_client_8cs.html index 88e83aa786..34b3940773 100644 --- a/_repository_client_8cs.html +++ b/_repository_client_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_repository_client_8cs_source.html b/_repository_client_8cs_source.html index aa7b5969c2..9e05f15188 100644 --- a/_repository_client_8cs_source.html +++ b/_repository_client_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_repository_controller_8cs.html b/_repository_controller_8cs.html index 0c41eb6d27..53a166f338 100644 --- a/_repository_controller_8cs.html +++ b/_repository_controller_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_repository_controller_8cs_source.html b/_repository_controller_8cs_source.html index 53a88bfbb9..c10664d89a 100644 --- a/_repository_controller_8cs_source.html +++ b/_repository_controller_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,8 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RepositoryController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Linq.Expressions;
10 using System.Net;
11 using System.Reflection;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
24 
25 namespace Tgstation.Server.Host.Controllers
26 {
30  [Route(Routes.Repository)]
31  #pragma warning disable CA1506 // TODO: Decomplexify
32  public sealed class RepositoryController : ApiController
33  {
38 
43 
48 
53 
64  public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger<RepositoryController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true, true)
65  {
66  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
67  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
68  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
69  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
70  }
71 
72  async Task<bool> LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action<Models.RevisionInformation> revInfoSink, CancellationToken cancellationToken)
73  {
74  var repoSha = repository.Head;
75 
76  IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> query) => query
77  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
78  .Include(x => x.CompileJobs)
79  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy);
80 
81  var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
82 
83  // If the DB doesn't have it, check the local set
84  if (revisionInfo == default)
85  revisionInfo = databaseContext
87  .Local
88  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
89  .FirstOrDefault();
90 
91  var needsDbUpdate = revisionInfo == default;
92  if (needsDbUpdate)
93  {
94  // needs insertion
95  revisionInfo = new Models.RevisionInformation
96  {
97  Instance = instance,
98  CommitSha = repoSha,
99  CompileJobs = new List<Models.CompileJob>(),
100  ActiveTestMerges = new List<RevInfoTestMerge>() // non null vals for api returns
101  };
102 
103  lock (databaseContext) // cleaner this way
104  databaseContext.RevisionInformations.Add(revisionInfo);
105  }
106 
107  revisionInfo.OriginCommitSha ??= lastOriginCommitSha;
108  if (revisionInfo.OriginCommitSha == null)
109  {
110  revisionInfo.OriginCommitSha = repoSha;
111  Logger.LogWarning(Components.Repository.Repository.OriginTrackingErrorTemplate, repoSha);
112  }
113 
114  revInfoSink?.Invoke(revisionInfo);
115  return needsDbUpdate;
116  }
117 
118  async Task<bool> PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
119  {
120  if (repository.IsGitHubRepository)
121  {
122  model.GitHubOwner = repository.GitHubOwner;
123  model.GitHubName = repository.GitHubRepoName;
124  }
125 
126  model.Origin = repository.Origin;
127  model.Reference = repository.Reference;
128 
129  // rev info stuff
130  Models.RevisionInformation revisionInfo = null;
131  var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, null, x => revisionInfo = x, cancellationToken).ConfigureAwait(false);
132  model.RevisionInformation = revisionInfo.ToApi();
133  return needsDbUpdate;
134  }
135 
144  [HttpPut]
145  [TgsAuthorize(RepositoryRights.SetOrigin)]
146  [ProducesResponseType(typeof(Repository), 201)]
147  [ProducesResponseType(410)]
148  public async Task<IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken)
149  {
150  if (model == null)
151  throw new ArgumentNullException(nameof(model));
152 
153  if (model.Origin == null)
154  return BadRequest(ErrorCode.RepoMissingOrigin);
155 
156  if (model.AccessUser == null ^ model.AccessToken == null)
157  return BadRequest(ErrorCode.RepoMismatchUserAndAccessToken);
158 
159  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
160 
161  if (currentModel == default)
162  return StatusCode((int)HttpStatusCode.Gone);
163 
164  // normalize github urls
165  const string BadGitHubUrl = "://www.github.com/";
166  var uiOrigin = model.Origin.ToUpperInvariant();
167  var uiBad = BadGitHubUrl.ToUpperInvariant();
168  var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant();
169  if (uiOrigin.Contains(uiBad, StringComparison.Ordinal))
170  model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal);
171 
172  currentModel.AccessToken = model.AccessToken;
173  currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm
174  var cloneBranch = model.Reference;
175  var origin = model.Origin;
176 
177  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
178 
179  if (repoManager.CloneInProgress)
180  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
181 
182  if (repoManager.InUse)
183  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
184 
185  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
186 
187  // clone conflict
188  if (repo != null)
189  return Conflict(new ErrorMessage(ErrorCode.RepoExists));
190 
191  var job = new Models.Job
192  {
193  Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"),
194  StartedBy = AuthenticationContext.User,
195  CancelRightsType = RightsType.Repository,
196  CancelRight = (ulong)RepositoryRights.CancelClone,
198  };
199  var api = currentModel.ToApi();
200  await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
201  {
202  using var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false);
203  if (repos == null)
204  throw new JobException(ErrorCode.RepoExists);
205  var instance = new Models.Instance
206  {
207  Id = Instance.Id
208  };
209  await databaseContextFactory.UseContext(
210  async databaseContext =>
211  {
212  databaseContext.Instances.Attach(instance);
213  if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false))
214  await databaseContext.Save(ct).ConfigureAwait(false);
215  })
216  .ConfigureAwait(false);
217  }, cancellationToken).ConfigureAwait(false);
218 
219  api.Origin = model.Origin;
220  api.Reference = model.Reference;
221  api.ActiveJob = job.ToApi();
222 
223  return StatusCode((int)HttpStatusCode.Created, api);
224  }
225 
233  [HttpDelete]
234  [TgsAuthorize(RepositoryRights.Delete)]
235  [ProducesResponseType(typeof(Repository), 202)]
236  [ProducesResponseType(410)]
237  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
238  {
239  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
240 
241  if (currentModel == default)
242  return StatusCode((int)HttpStatusCode.Gone);
243 
244  currentModel.AccessToken = null;
245  currentModel.AccessUser = null;
246 
247  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
248 
249  Logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id);
250 
251  var job = new Models.Job
252  {
253  Description = "Delete repository",
254  StartedBy = AuthenticationContext.User,
256  };
257  var api = currentModel.ToApi();
258  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
259  api.ActiveJob = job.ToApi();
260  return Accepted(api);
261  }
262 
271  [HttpGet]
272  [TgsAuthorize(RepositoryRights.Read)]
273  [ProducesResponseType(typeof(Repository), 200)]
274  [ProducesResponseType(typeof(Repository), 201)]
275  [ProducesResponseType(410)]
276  public async Task<IActionResult> Read(CancellationToken cancellationToken)
277  {
278  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
279 
280  if (currentModel == default)
281  return StatusCode((int)HttpStatusCode.Gone, new ErrorMessage(ErrorCode.RepoMissing));
282 
283  var api = currentModel.ToApi();
284  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
285 
286  if (repoManager.CloneInProgress)
287  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
288 
289  if (repoManager.InUse)
290  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
291 
292  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
293  if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false))
294  {
295  // user may have fucked with the repo manually, do what we can
296  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
297  return StatusCode((int)HttpStatusCode.Created, api);
298  }
299 
300  return Json(api);
301  }
302 
312  [HttpPost]
313  [TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)]
314  [ProducesResponseType(typeof(Repository), 200)]
315  [ProducesResponseType(typeof(Repository), 202)]
316  [ProducesResponseType(410)]
317  #pragma warning disable CA1502, CA1505 // TODO: Decomplexify
318  public async Task<IActionResult> Update([FromBody]Repository model, CancellationToken cancellationToken)
319  {
320  if (model == null)
321  throw new ArgumentNullException(nameof(model));
322 
323  if (model.AccessUser == null ^ model.AccessToken == null)
324  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchUserAndAccessToken));
325 
326  if (model.CheckoutSha != null && model.Reference != null)
327  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndReference));
328 
329  if (model.CheckoutSha != null && model.UpdateFromOrigin == true)
330  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndUpdate));
331 
332  if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true)
333  return BadRequest(new ErrorMessage(ErrorCode.RepoDuplicateTestMerge));
334 
335  if (model.CommitterName?.Length == 0)
336  return BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterName));
337 
338  if (model.CommitterEmail?.Length == 0)
339  return BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterEmail));
340 
341  var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0;
342  var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository);
343  if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest))
344  return Forbid();
345 
346  var currentModel = await DatabaseContext
348  .Where(x => x.InstanceId == Instance.Id)
349  .FirstOrDefaultAsync(cancellationToken)
350  .ConfigureAwait(false);
351 
352  if (currentModel == default)
353  return StatusCode((int)HttpStatusCode.Gone);
354 
355  bool CheckModified<T>(Expression<Func<Api.Models.Internal.RepositorySettings, T>> expression, RepositoryRights requiredRight)
356  {
357  var memberSelectorExpression = (MemberExpression)expression.Body;
358  var property = (PropertyInfo)memberSelectorExpression.Member;
359 
360  var newVal = property.GetValue(model);
361  if (newVal == null)
362  return false;
363  if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal)
364  return true;
365 
366  property.SetValue(currentModel, newVal);
367  return false;
368  }
369 
370  if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials)
371  || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials)
372  || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings)
373  || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings)
374  || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter)
375  || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter)
376  || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits)
377  || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits)
378  || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits)
379  || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch)))
380  return Forbid();
381 
382  if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0)
383  {
384  // setting an empty string clears everything
385  currentModel.AccessUser = null;
386  currentModel.AccessToken = null;
387  }
388 
389  var canRead = userRights.HasFlag(RepositoryRights.Read);
390 
391  var api = canRead ? currentModel.ToApi() : new Repository();
392  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
393 
394  if (canRead)
395  {
396  if (repoManager.CloneInProgress)
397  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
398 
399  if (repoManager.InUse)
400  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
401 
402  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
403  if (repo == null)
404  return Conflict(new ErrorMessage(ErrorCode.RepoMissing));
405  await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false);
406 
407  if (model.Origin != null && model.Origin != repo.Origin)
408  return BadRequest(new ErrorMessage(ErrorCode.RepoCantChangeOrigin));
409  }
410 
411  // this is just db stuf so stow it away
412  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
413 
414  // format the job description
415  string description = null;
416  if (model.UpdateFromOrigin == true)
417  if (model.Reference != null)
418  description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference);
419  else if (model.CheckoutSha != null)
420  description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha);
421  else
422  description = "Pull current repository reference";
423  else if (model.Reference != null || model.CheckoutSha != null)
424  description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha);
425 
426  if (newTestMerges)
427  description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}",
428  description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T",
429  String.Join(", ", model.NewTestMerges.Select(x =>
430  String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number,
431  x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))),
432  description != null ? String.Empty : " in repository");
433 
434  if (description == null)
435  return Json(api); // no git changes
436 
437  var job = new Models.Job
438  {
439  Description = description,
440  StartedBy = AuthenticationContext.User,
441  Instance = Instance,
442  CancelRightsType = RightsType.Repository,
443  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
444  };
445 
446  // Time to access git, do it in a job
447  await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
448  {
449  using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false);
450  if (repo == null)
451  throw new JobException(ErrorCode.RepoMissing);
452 
453  var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null;
454 
455  var startReference = repo.Reference;
456  var startSha = repo.Head;
457  string postUpdateSha = null;
458 
459  if (newTestMerges && !repo.IsGitHubRepository)
460  throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote);
461 
462  var committerName = currentModel.ShowTestMergeCommitters.Value
464  : currentModel.CommitterName;
465 
466  var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null;
467 
468  var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
469  var doneSteps = 0;
470 
471  Action<int> NextProgressReporter()
472  {
473  var tmpDoneSteps = doneSteps;
474  ++doneSteps;
475  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps);
476  }
477 
478  progressReporter(0);
479 
480  // get a base line for where we are
481  Models.RevisionInformation lastRevisionInfo = null;
482 
483  var attachedInstance = new Models.Instance
484  {
485  Id = Instance.Id
486  };
487 
488  Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory
489  .UseContext(
490  async databaseContext =>
491  {
492  databaseContext.Instances.Attach(attachedInstance);
493  var previousRevInfo = lastRevisionInfo;
494  var needsUpdate = await LoadRevisionInformation(
495  repo,
496  databaseContext,
497  attachedInstance,
498  lastOriginCommitSha,
499  x => lastRevisionInfo = x,
500  ct)
501  .ConfigureAwait(false);
502 
503  if (testMergeToAdd != null)
504  {
505  // rev info may have already loaded the user
506  var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id);
507  if (mergedBy == default)
508  {
509  mergedBy = new Models.User
510  {
512  };
513 
514  databaseContext.Users.Attach(mergedBy);
515  }
516 
517  testMergeToAdd.MergedBy = mergedBy;
518 
519  lastRevisionInfo.ActiveTestMerges.AddRange(previousRevInfo.ActiveTestMerges);
520  lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
521  {
522  TestMerge = testMergeToAdd
523  });
524  lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
525 
526  needsUpdate = true;
527  }
528 
529  if (needsUpdate)
530  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
531  });
532 
533  await CallLoadRevInfo().ConfigureAwait(false);
534 
535  // apply new rev info, tracking applied test merges
536  Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha);
537 
538  try
539  {
540  // fetch/pull
541  if (model.UpdateFromOrigin == true)
542  {
543  if (!repo.Tracking)
544  throw new JobException(ErrorCode.RepoReferenceRequired);
545  await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false);
546  doneSteps = 1;
547  if (!modelHasShaOrReference)
548  {
549  var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false);
550  if (!fastForward.HasValue)
551  throw new JobException(ErrorCode.RepoMergeConflict);
552  await UpdateRevInfo().ConfigureAwait(false);
553  if (fastForward.Value)
554  {
555  lastRevisionInfo.OriginCommitSha = repo.Head;
556  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
557  postUpdateSha = repo.Head;
558  }
559  else
560  NextProgressReporter()(100);
561  }
562  }
563 
564  // checkout/hard reset
565  if (modelHasShaOrReference)
566  {
567  var validCheckoutSha =
568  model.CheckoutSha != null
569  && !repo.Head.StartsWith(model.CheckoutSha, StringComparison.OrdinalIgnoreCase);
570  var validCheckoutReference =
571  model.Reference != null
572  && !repo.Reference.Equals(model.Reference, StringComparison.OrdinalIgnoreCase);
573  if (validCheckoutSha || validCheckoutReference)
574  {
575  var committish = model.CheckoutSha ?? model.Reference;
576  var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false);
577 
578  if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
579  throw new JobException(ErrorCode.RepoSwappedShaOrReference);
580 
581  await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false);
582  await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin
583  }
584  else
585  NextProgressReporter()(100);
586 
587  if (hardResettingToOriginReference)
588  {
589  if (!repo.Tracking)
590  throw new JobException(ErrorCode.RepoReferenceNotTracking);
591  await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false);
592  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
593  await CallLoadRevInfo().ConfigureAwait(false);
594 
595  // repo head is on origin so force this
596  // will update the db if necessary
597  lastRevisionInfo.OriginCommitSha = repo.Head;
598  }
599  }
600 
601  // test merging
602  Dictionary<int, Octokit.PullRequest> prMap = null;
603  if (newTestMerges)
604  {
605  // bit of sanitization
606  foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision)))
607  I.PullRequestRevision = null;
608 
609  var gitHubClient = currentModel.AccessToken != null
610  ? gitHubClientFactory.CreateClient(currentModel.AccessToken)
611  : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken)
612  ? gitHubClientFactory.CreateClient()
613  : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken));
614 
615  var repoOwner = repo.GitHubOwner;
616  var repoName = repo.GitHubRepoName;
617 
618  // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out
619  Models.RevisionInformation revInfoWereLookingFor = null;
620  bool needToApplyRemainingPrs = true;
621  if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
622  {
623  // In order for this to work though we need the shas of all the commits
624  if (model.NewTestMerges.Any(x => x.PullRequestRevision == null))
625  prMap = new Dictionary<int, Octokit.PullRequest>();
626 
627  bool cantSearch = false;
628  foreach (var I in model.NewTestMerges)
629  {
630  if (I.PullRequestRevision != null)
631 #pragma warning disable CA1308 // Normalize strings to uppercase
632  I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant(); // ala libgit2
633 #pragma warning restore CA1308 // Normalize strings to uppercase
634  else
635  try
636  {
637  // retrieve the latest sha
638  var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
639  prMap.Add(I.Number, pr);
640  I.PullRequestRevision = pr.Head.Sha;
641  }
642  catch
643  {
644  cantSearch = true;
645  break;
646  }
647  }
648 
649  if (!cantSearch)
650  {
651  List<Models.RevisionInformation> dbPull = null;
652 
653  await databaseContextFactory.UseContext(
654  async databaseContext =>
655  dbPull = await databaseContext.RevisionInformations
656  .Where(x => x.Instance.Id == Instance.Id
657  && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
658  && x.ActiveTestMerges.Count <= model.NewTestMerges.Count
659  && x.ActiveTestMerges.Count > 0)
660  .Include(x => x.ActiveTestMerges)
661  .ThenInclude(x => x.TestMerge)
662  .ToListAsync(cancellationToken)
663  .ConfigureAwait(false))
664  .ConfigureAwait(false);
665 
666  // split here cause this bit has to be done locally
667  revInfoWereLookingFor = dbPull
668  .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count
669  && x.ActiveTestMerges.Select(y => y.TestMerge)
670  .All(y => model.NewTestMerges.Any(z =>
671  y.Number == z.Number
672  && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
673  && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
674  .FirstOrDefault();
675 
676  if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1)
677  {
678  // okay try to add at least SOME prs we've seen before
679  var search = model.NewTestMerges.ToList();
680 
681  var appliedTestMergeIds = new List<long>();
682 
683  Models.RevisionInformation lastGoodRevInfo = null;
684  do
685  {
686  foreach (var I in search)
687  {
688  revInfoWereLookingFor = dbPull
689  .Where(x => model.NewTestMerges.Any(z =>
690  x.PrimaryTestMerge.Number == z.Number
691  && x.PrimaryTestMerge.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
692  && (x.PrimaryTestMerge.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))
693  && x.ActiveTestMerges.Select(y => y.TestMerge).All(y => appliedTestMergeIds.Contains(y.Id)))
694  .FirstOrDefault();
695 
696  if (revInfoWereLookingFor != null)
697  {
698  lastGoodRevInfo = revInfoWereLookingFor;
699  appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id);
700  search.Remove(I);
701  break;
702  }
703  }
704  }
705  while (revInfoWereLookingFor != null && search.Count > 0);
706 
707  revInfoWereLookingFor = lastGoodRevInfo;
708  needToApplyRemainingPrs = search.Count != 0;
709  if (needToApplyRemainingPrs)
710  model.NewTestMerges = search;
711  }
712  else if (revInfoWereLookingFor != null)
713  needToApplyRemainingPrs = false;
714  }
715  }
716 
717  if (revInfoWereLookingFor != null)
718  {
719  // goteem
720  Logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha);
721  await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false);
722  lastRevisionInfo = revInfoWereLookingFor;
723  }
724 
725  if (needToApplyRemainingPrs)
726  {
727  foreach (var I in model.NewTestMerges)
728  {
729  Octokit.PullRequest pr = null;
730  string errorMessage = null;
731 
732  if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number))
733  throw new JobException(ErrorCode.RepoDuplicateTestMerge);
734 
735  Exception exception = null;
736  try
737  {
738  // load from cache if possible
739  if (prMap == null || !prMap.TryGetValue(I.Number, out pr))
740  pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
741  }
742  catch (Octokit.RateLimitExceededException ex)
743  {
744  // you look at your anonymous access and sigh
745  errorMessage = "REMOTE API ERROR: RATE LIMITED";
746  exception = ex;
747  }
748  catch (Octokit.AuthorizationException ex)
749  {
750  errorMessage = "REMOTE API ERROR: BAD CREDENTIALS";
751  exception = ex;
752  }
753  catch (Octokit.NotFoundException ex)
754  {
755  // you look at your shithub and sigh
756  errorMessage = "REMOTE API ERROR: PULL REQUEST NOT FOUND";
757  exception = ex;
758  }
759 
760  if (exception != null)
761  Logger.LogWarning("Error retrieving pull request metadata: {0}", exception);
762 
763  // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it
764  if (I.PullRequestRevision == null && pr != null)
765  I.PullRequestRevision = pr.Head.Sha;
766 
767  var mergeResult = await repo.AddTestMerge(
768  I,
769  committerName,
770  currentModel.CommitterEmail,
771  currentModel.AccessUser,
772  currentModel.AccessToken,
773  NextProgressReporter(),
774  ct).ConfigureAwait(false);
775 
776  if (!mergeResult.HasValue)
777  throw new JobException(
778  ErrorCode.RepoTestMergeConflict,
779  new JobException(
780  $"Merge of PR #{I.Number} at {I.PullRequestRevision.Substring(0, 7)} conflicted!"));
781 
782  ++doneSteps;
783 
784  // MergedBy will be set later
785  var tm = new Models.TestMerge
786  {
787  Author = pr?.User.Login ?? errorMessage,
788  BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty,
789  MergedAt = DateTimeOffset.Now,
790  TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty,
791  Comment = I.Comment,
792  Number = I.Number,
793  PullRequestRevision = I.PullRequestRevision,
794  Url = pr?.HtmlUrl ?? errorMessage
795  };
796 
797  await UpdateRevInfo(tm).ConfigureAwait(false);
798  }
799  }
800  }
801 
802  var currentHead = repo.Head;
803  if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))
804  {
805  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false);
806  await UpdateRevInfo().ConfigureAwait(false);
807  }
808  }
809  catch
810  {
811  doneSteps = 0;
812  numSteps = 2;
813 
814  // Forget what we've done and abort
815  await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false);
816  if (startReference != null && repo.Head != startSha)
817  await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false);
818  else
819  progressReporter(100);
820  throw;
821  }
822  }, cancellationToken).ConfigureAwait(false);
823 
824  api.ActiveJob = job.ToApi();
825  return Accepted(api);
826  }
827  #pragma warning restore CA1502, CA1505
828  }
829 }
string Name
The name of the User
Definition: User.cs:41
-
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Linq.Expressions;
10 using System.Net;
11 using System.Reflection;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
24 
25 namespace Tgstation.Server.Host.Controllers
26 {
30  [Route(Routes.Repository)]
31  #pragma warning disable CA1506 // TODO: Decomplexify
32  public sealed class RepositoryController : ApiController
33  {
38 
43 
48 
53 
64  public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger<RepositoryController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true, true)
65  {
66  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
67  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
68  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
69  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
70  }
71 
72  async Task<bool> LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action<Models.RevisionInformation> revInfoSink, CancellationToken cancellationToken)
73  {
74  var repoSha = repository.Head;
75 
76  IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> query) => query
77  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
78  .Include(x => x.CompileJobs)
79  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy);
80 
81  var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
82 
83  // If the DB doesn't have it, check the local set
84  if (revisionInfo == default)
85  revisionInfo = databaseContext
87  .Local
88  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
89  .FirstOrDefault();
90 
91  var needsDbUpdate = revisionInfo == default;
92  if (needsDbUpdate)
93  {
94  // needs insertion
95  revisionInfo = new Models.RevisionInformation
96  {
97  Instance = instance,
98  CommitSha = repoSha,
99  CompileJobs = new List<Models.CompileJob>(),
100  ActiveTestMerges = new List<RevInfoTestMerge>() // non null vals for api returns
101  };
102 
103  lock (databaseContext) // cleaner this way
104  databaseContext.RevisionInformations.Add(revisionInfo);
105  }
106 
107  revisionInfo.OriginCommitSha ??= lastOriginCommitSha;
108  if (revisionInfo.OriginCommitSha == null)
109  {
110  revisionInfo.OriginCommitSha = repoSha;
111  Logger.LogWarning(Components.Repository.Repository.OriginTrackingErrorTemplate, repoSha);
112  }
113 
114  revInfoSink?.Invoke(revisionInfo);
115  return needsDbUpdate;
116  }
117 
118  async Task<bool> PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
119  {
120  if (repository.IsGitHubRepository)
121  {
122  model.GitHubOwner = repository.GitHubOwner;
123  model.GitHubName = repository.GitHubRepoName;
124  }
125 
126  model.Origin = repository.Origin;
127  model.Reference = repository.Reference;
128 
129  // rev info stuff
130  Models.RevisionInformation revisionInfo = null;
131  var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, null, x => revisionInfo = x, cancellationToken).ConfigureAwait(false);
132  model.RevisionInformation = revisionInfo.ToApi();
133  return needsDbUpdate;
134  }
135 
144  [HttpPut]
145  [TgsAuthorize(RepositoryRights.SetOrigin)]
146  [ProducesResponseType(typeof(Repository), 201)]
147  [ProducesResponseType(410)]
148  public async Task<IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken)
149  {
150  if (model == null)
151  throw new ArgumentNullException(nameof(model));
152 
153  if (model.Origin == null)
154  return BadRequest(ErrorCode.RepoMissingOrigin);
155 
156  if (model.AccessUser == null ^ model.AccessToken == null)
157  return BadRequest(ErrorCode.RepoMismatchUserAndAccessToken);
158 
159  var currentModel = await DatabaseContext
161  .AsQueryable()
162  .Where(x => x.InstanceId == Instance.Id)
163  .FirstOrDefaultAsync(cancellationToken)
164  .ConfigureAwait(false);
165 
166  if (currentModel == default)
167  return StatusCode((int)HttpStatusCode.Gone);
168 
169  // normalize github urls
170  const string BadGitHubUrl = "://www.github.com/";
171  var uiOrigin = model.Origin.ToUpperInvariant();
172  var uiBad = BadGitHubUrl.ToUpperInvariant();
173  var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant();
174  if (uiOrigin.Contains(uiBad, StringComparison.Ordinal))
175  model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal);
176 
177  currentModel.AccessToken = model.AccessToken;
178  currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm
179  var cloneBranch = model.Reference;
180  var origin = model.Origin;
181 
182  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
183 
184  if (repoManager.CloneInProgress)
185  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
186 
187  if (repoManager.InUse)
188  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
189 
190  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
191 
192  // clone conflict
193  if (repo != null)
194  return Conflict(new ErrorMessage(ErrorCode.RepoExists));
195 
196  var job = new Models.Job
197  {
198  Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"),
199  StartedBy = AuthenticationContext.User,
200  CancelRightsType = RightsType.Repository,
201  CancelRight = (ulong)RepositoryRights.CancelClone,
203  };
204  var api = currentModel.ToApi();
205  await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
206  {
207  using var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false);
208  if (repos == null)
209  throw new JobException(ErrorCode.RepoExists);
210  var instance = new Models.Instance
211  {
212  Id = Instance.Id
213  };
214  await databaseContextFactory.UseContext(
215  async databaseContext =>
216  {
217  databaseContext.Instances.Attach(instance);
218  if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false))
219  await databaseContext.Save(ct).ConfigureAwait(false);
220  })
221  .ConfigureAwait(false);
222  }, cancellationToken).ConfigureAwait(false);
223 
224  api.Origin = model.Origin;
225  api.Reference = model.Reference;
226  api.ActiveJob = job.ToApi();
227 
228  return StatusCode((int)HttpStatusCode.Created, api);
229  }
230 
238  [HttpDelete]
239  [TgsAuthorize(RepositoryRights.Delete)]
240  [ProducesResponseType(typeof(Repository), 202)]
241  [ProducesResponseType(410)]
242  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
243  {
244  var currentModel = await DatabaseContext
246  .AsQueryable()
247  .Where(x => x.InstanceId == Instance.Id)
248  .FirstOrDefaultAsync(cancellationToken)
249  .ConfigureAwait(false);
250 
251  if (currentModel == default)
252  return StatusCode((int)HttpStatusCode.Gone);
253 
254  currentModel.AccessToken = null;
255  currentModel.AccessUser = null;
256 
257  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
258 
259  Logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id);
260 
261  var job = new Models.Job
262  {
263  Description = "Delete repository",
264  StartedBy = AuthenticationContext.User,
266  };
267  var api = currentModel.ToApi();
268  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
269  api.ActiveJob = job.ToApi();
270  return Accepted(api);
271  }
272 
281  [HttpGet]
282  [TgsAuthorize(RepositoryRights.Read)]
283  [ProducesResponseType(typeof(Repository), 200)]
284  [ProducesResponseType(typeof(Repository), 201)]
285  [ProducesResponseType(410)]
286  public async Task<IActionResult> Read(CancellationToken cancellationToken)
287  {
288  var currentModel = await DatabaseContext
290  .AsQueryable()
291  .Where(x => x.InstanceId == Instance.Id)
292  .FirstOrDefaultAsync(cancellationToken)
293  .ConfigureAwait(false);
294 
295  if (currentModel == default)
296  return StatusCode((int)HttpStatusCode.Gone, new ErrorMessage(ErrorCode.RepoMissing));
297 
298  var api = currentModel.ToApi();
299  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
300 
301  if (repoManager.CloneInProgress)
302  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
303 
304  if (repoManager.InUse)
305  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
306 
307  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
308  if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false))
309  {
310  // user may have fucked with the repo manually, do what we can
311  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
312  return StatusCode((int)HttpStatusCode.Created, api);
313  }
314 
315  return Json(api);
316  }
317 
327  [HttpPost]
328  [TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)]
329  [ProducesResponseType(typeof(Repository), 200)]
330  [ProducesResponseType(typeof(Repository), 202)]
331  [ProducesResponseType(410)]
332  #pragma warning disable CA1502, CA1505 // TODO: Decomplexify
333  public async Task<IActionResult> Update([FromBody]Repository model, CancellationToken cancellationToken)
334  {
335  if (model == null)
336  throw new ArgumentNullException(nameof(model));
337 
338  if (model.AccessUser == null ^ model.AccessToken == null)
339  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchUserAndAccessToken));
340 
341  if (model.CheckoutSha != null && model.Reference != null)
342  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndReference));
343 
344  if (model.CheckoutSha != null && model.UpdateFromOrigin == true)
345  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndUpdate));
346 
347  if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true)
348  return BadRequest(new ErrorMessage(ErrorCode.RepoDuplicateTestMerge));
349 
350  if (model.CommitterName?.Length == 0)
351  return BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterName));
352 
353  if (model.CommitterEmail?.Length == 0)
354  return BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterEmail));
355 
356  var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0;
357  var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository);
358  if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest))
359  return Forbid();
360 
361  var currentModel = await DatabaseContext
363  .AsQueryable()
364  .Where(x => x.InstanceId == Instance.Id)
365  .FirstOrDefaultAsync(cancellationToken)
366  .ConfigureAwait(false);
367 
368  if (currentModel == default)
369  return StatusCode((int)HttpStatusCode.Gone);
370 
371  bool CheckModified<T>(Expression<Func<Api.Models.Internal.RepositorySettings, T>> expression, RepositoryRights requiredRight)
372  {
373  var memberSelectorExpression = (MemberExpression)expression.Body;
374  var property = (PropertyInfo)memberSelectorExpression.Member;
375 
376  var newVal = property.GetValue(model);
377  if (newVal == null)
378  return false;
379  if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal)
380  return true;
381 
382  property.SetValue(currentModel, newVal);
383  return false;
384  }
385 
386  if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials)
387  || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials)
388  || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings)
389  || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings)
390  || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter)
391  || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter)
392  || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits)
393  || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits)
394  || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits)
395  || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch)))
396  return Forbid();
397 
398  if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0)
399  {
400  // setting an empty string clears everything
401  currentModel.AccessUser = null;
402  currentModel.AccessToken = null;
403  }
404 
405  var canRead = userRights.HasFlag(RepositoryRights.Read);
406 
407  var api = canRead ? currentModel.ToApi() : new Repository();
408  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
409 
410  if (canRead)
411  {
412  if (repoManager.CloneInProgress)
413  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
414 
415  if (repoManager.InUse)
416  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
417 
418  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
419  if (repo == null)
420  return Conflict(new ErrorMessage(ErrorCode.RepoMissing));
421  await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false);
422 
423  if (model.Origin != null && model.Origin != repo.Origin)
424  return BadRequest(new ErrorMessage(ErrorCode.RepoCantChangeOrigin));
425  }
426 
427  // this is just db stuf so stow it away
428  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
429 
430  // format the job description
431  string description = null;
432  if (model.UpdateFromOrigin == true)
433  if (model.Reference != null)
434  description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference);
435  else if (model.CheckoutSha != null)
436  description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha);
437  else
438  description = "Pull current repository reference";
439  else if (model.Reference != null || model.CheckoutSha != null)
440  description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha);
441 
442  if (newTestMerges)
443  description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}",
444  description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T",
445  String.Join(", ", model.NewTestMerges.Select(x =>
446  String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number,
447  x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))),
448  description != null ? String.Empty : " in repository");
449 
450  if (description == null)
451  return Json(api); // no git changes
452 
453  var job = new Models.Job
454  {
455  Description = description,
456  StartedBy = AuthenticationContext.User,
457  Instance = Instance,
458  CancelRightsType = RightsType.Repository,
459  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
460  };
461 
462  // Time to access git, do it in a job
463  await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
464  {
465  using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false);
466  if (repo == null)
467  throw new JobException(ErrorCode.RepoMissing);
468 
469  var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null;
470 
471  var startReference = repo.Reference;
472  var startSha = repo.Head;
473  string postUpdateSha = null;
474 
475  if (newTestMerges && !repo.IsGitHubRepository)
476  throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote);
477 
478  var committerName = currentModel.ShowTestMergeCommitters.Value
480  : currentModel.CommitterName;
481 
482  var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null;
483 
484  var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
485  var doneSteps = 0;
486 
487  Action<int> NextProgressReporter()
488  {
489  var tmpDoneSteps = doneSteps;
490  ++doneSteps;
491  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps);
492  }
493 
494  progressReporter(0);
495 
496  // get a base line for where we are
497  Models.RevisionInformation lastRevisionInfo = null;
498 
499  var attachedInstance = new Models.Instance
500  {
501  Id = Instance.Id
502  };
503 
504  Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory
505  .UseContext(
506  async databaseContext =>
507  {
508  databaseContext.Instances.Attach(attachedInstance);
509  var previousRevInfo = lastRevisionInfo;
510  var needsUpdate = await LoadRevisionInformation(
511  repo,
512  databaseContext,
513  attachedInstance,
514  lastOriginCommitSha,
515  x => lastRevisionInfo = x,
516  ct)
517  .ConfigureAwait(false);
518 
519  if (testMergeToAdd != null)
520  {
521  // rev info may have already loaded the user
522  var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id);
523  if (mergedBy == default)
524  {
525  mergedBy = new Models.User
526  {
528  };
529 
530  databaseContext.Users.Attach(mergedBy);
531  }
532 
533  testMergeToAdd.MergedBy = mergedBy;
534 
535  lastRevisionInfo.ActiveTestMerges.AddRange(previousRevInfo.ActiveTestMerges);
536  lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
537  {
538  TestMerge = testMergeToAdd
539  });
540  lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
541 
542  needsUpdate = true;
543  }
544 
545  if (needsUpdate)
546  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
547  });
548 
549  await CallLoadRevInfo().ConfigureAwait(false);
550 
551  // apply new rev info, tracking applied test merges
552  Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha);
553 
554  try
555  {
556  // fetch/pull
557  if (model.UpdateFromOrigin == true)
558  {
559  if (!repo.Tracking)
560  throw new JobException(ErrorCode.RepoReferenceRequired);
561  await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false);
562  doneSteps = 1;
563  if (!modelHasShaOrReference)
564  {
565  var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false);
566  if (!fastForward.HasValue)
567  throw new JobException(ErrorCode.RepoMergeConflict);
568  await UpdateRevInfo().ConfigureAwait(false);
569  if (fastForward.Value)
570  {
571  lastRevisionInfo.OriginCommitSha = repo.Head;
572  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
573  postUpdateSha = repo.Head;
574  }
575  else
576  NextProgressReporter()(100);
577  }
578  }
579 
580  // checkout/hard reset
581  if (modelHasShaOrReference)
582  {
583  var validCheckoutSha =
584  model.CheckoutSha != null
585  && !repo.Head.StartsWith(model.CheckoutSha, StringComparison.OrdinalIgnoreCase);
586  var validCheckoutReference =
587  model.Reference != null
588  && !repo.Reference.Equals(model.Reference, StringComparison.OrdinalIgnoreCase);
589  if (validCheckoutSha || validCheckoutReference)
590  {
591  var committish = model.CheckoutSha ?? model.Reference;
592  var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false);
593 
594  if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
595  throw new JobException(ErrorCode.RepoSwappedShaOrReference);
596 
597  await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false);
598  await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin
599  }
600  else
601  NextProgressReporter()(100);
602 
603  if (hardResettingToOriginReference)
604  {
605  if (!repo.Tracking)
606  throw new JobException(ErrorCode.RepoReferenceNotTracking);
607  await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false);
608  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
609  await CallLoadRevInfo().ConfigureAwait(false);
610 
611  // repo head is on origin so force this
612  // will update the db if necessary
613  lastRevisionInfo.OriginCommitSha = repo.Head;
614  }
615  }
616 
617  // test merging
618  Dictionary<int, Octokit.PullRequest> prMap = null;
619  if (newTestMerges)
620  {
621  // bit of sanitization
622  foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision)))
623  I.PullRequestRevision = null;
624 
625  var gitHubClient = currentModel.AccessToken != null
626  ? gitHubClientFactory.CreateClient(currentModel.AccessToken)
627  : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken)
628  ? gitHubClientFactory.CreateClient()
629  : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken));
630 
631  var repoOwner = repo.GitHubOwner;
632  var repoName = repo.GitHubRepoName;
633 
634  // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out
635  Models.RevisionInformation revInfoWereLookingFor = null;
636  bool needToApplyRemainingPrs = true;
637  if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
638  {
639  // In order for this to work though we need the shas of all the commits
640  if (model.NewTestMerges.Any(x => x.PullRequestRevision == null))
641  prMap = new Dictionary<int, Octokit.PullRequest>();
642 
643  bool cantSearch = false;
644  foreach (var I in model.NewTestMerges)
645  {
646  if (I.PullRequestRevision != null)
647 #pragma warning disable CA1308 // Normalize strings to uppercase
648  I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant(); // ala libgit2
649 #pragma warning restore CA1308 // Normalize strings to uppercase
650  else
651  try
652  {
653  // retrieve the latest sha
654  var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
655  prMap.Add(I.Number, pr);
656  I.PullRequestRevision = pr.Head.Sha;
657  }
658  catch
659  {
660  cantSearch = true;
661  break;
662  }
663  }
664 
665  if (!cantSearch)
666  {
667  List<Models.RevisionInformation> dbPull = null;
668 
669  await databaseContextFactory.UseContext(
670  async databaseContext =>
671  dbPull = await databaseContext.RevisionInformations
672  .AsQueryable()
673  .Where(x => x.Instance.Id == Instance.Id
674  && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
675  && x.ActiveTestMerges.Count <= model.NewTestMerges.Count
676  && x.ActiveTestMerges.Count > 0)
677  .Include(x => x.ActiveTestMerges)
678  .ThenInclude(x => x.TestMerge)
679  .ToListAsync(cancellationToken)
680  .ConfigureAwait(false))
681  .ConfigureAwait(false);
682 
683  // split here cause this bit has to be done locally
684  revInfoWereLookingFor = dbPull
685  .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count
686  && x.ActiveTestMerges.Select(y => y.TestMerge)
687  .All(y => model.NewTestMerges.Any(z =>
688  y.Number == z.Number
689  && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
690  && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
691  .FirstOrDefault();
692 
693  if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1)
694  {
695  // okay try to add at least SOME prs we've seen before
696  var search = model.NewTestMerges.ToList();
697 
698  var appliedTestMergeIds = new List<long>();
699 
700  Models.RevisionInformation lastGoodRevInfo = null;
701  do
702  {
703  foreach (var I in search)
704  {
705  revInfoWereLookingFor = dbPull
706  .Where(x => model.NewTestMerges.Any(z =>
707  x.PrimaryTestMerge.Number == z.Number
708  && x.PrimaryTestMerge.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
709  && (x.PrimaryTestMerge.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))
710  && x.ActiveTestMerges.Select(y => y.TestMerge).All(y => appliedTestMergeIds.Contains(y.Id)))
711  .FirstOrDefault();
712 
713  if (revInfoWereLookingFor != null)
714  {
715  lastGoodRevInfo = revInfoWereLookingFor;
716  appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id);
717  search.Remove(I);
718  break;
719  }
720  }
721  }
722  while (revInfoWereLookingFor != null && search.Count > 0);
723 
724  revInfoWereLookingFor = lastGoodRevInfo;
725  needToApplyRemainingPrs = search.Count != 0;
726  if (needToApplyRemainingPrs)
727  model.NewTestMerges = search;
728  }
729  else if (revInfoWereLookingFor != null)
730  needToApplyRemainingPrs = false;
731  }
732  }
733 
734  if (revInfoWereLookingFor != null)
735  {
736  // goteem
737  Logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha);
738  await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false);
739  lastRevisionInfo = revInfoWereLookingFor;
740  }
741 
742  if (needToApplyRemainingPrs)
743  {
744  foreach (var I in model.NewTestMerges)
745  {
746  Octokit.PullRequest pr = null;
747  string errorMessage = null;
748 
749  if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number))
750  throw new JobException(ErrorCode.RepoDuplicateTestMerge);
751 
752  Exception exception = null;
753  try
754  {
755  // load from cache if possible
756  if (prMap == null || !prMap.TryGetValue(I.Number, out pr))
757  pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
758  }
759  catch (Octokit.RateLimitExceededException ex)
760  {
761  // you look at your anonymous access and sigh
762  errorMessage = "REMOTE API ERROR: RATE LIMITED";
763  exception = ex;
764  }
765  catch (Octokit.AuthorizationException ex)
766  {
767  errorMessage = "REMOTE API ERROR: BAD CREDENTIALS";
768  exception = ex;
769  }
770  catch (Octokit.NotFoundException ex)
771  {
772  // you look at your shithub and sigh
773  errorMessage = "REMOTE API ERROR: PULL REQUEST NOT FOUND";
774  exception = ex;
775  }
776 
777  if (exception != null)
778  Logger.LogWarning("Error retrieving pull request metadata: {0}", exception);
779 
780  // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it
781  if (I.PullRequestRevision == null && pr != null)
782  I.PullRequestRevision = pr.Head.Sha;
783 
784  var mergeResult = await repo.AddTestMerge(
785  I,
786  committerName,
787  currentModel.CommitterEmail,
788  currentModel.AccessUser,
789  currentModel.AccessToken,
790  NextProgressReporter(),
791  ct).ConfigureAwait(false);
792 
793  if (!mergeResult.HasValue)
794  throw new JobException(
795  ErrorCode.RepoTestMergeConflict,
796  new JobException(
797  $"Merge of PR #{I.Number} at {I.PullRequestRevision.Substring(0, 7)} conflicted!"));
798 
799  ++doneSteps;
800 
801  // MergedBy will be set later
802  var tm = new Models.TestMerge
803  {
804  Author = pr?.User.Login ?? errorMessage,
805  BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty,
806  MergedAt = DateTimeOffset.Now,
807  TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty,
808  Comment = I.Comment,
809  Number = I.Number,
810  PullRequestRevision = I.PullRequestRevision,
811  Url = pr?.HtmlUrl ?? errorMessage
812  };
813 
814  await UpdateRevInfo(tm).ConfigureAwait(false);
815  }
816  }
817  }
818 
819  var currentHead = repo.Head;
820  if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))
821  {
822  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false);
823  await UpdateRevInfo().ConfigureAwait(false);
824  }
825  }
826  catch
827  {
828  doneSteps = 0;
829  numSteps = 2;
830 
831  // Forget what we've done and abort
832  await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false);
833  if (startReference != null && repo.Head != startSha)
834  await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false);
835  else
836  progressReporter(100);
837  throw;
838  }
839  }, cancellationToken).ConfigureAwait(false);
840 
841  api.ActiveJob = job.ToApi();
842  return Accepted(api);
843  }
844  #pragma warning restore CA1502, CA1505
845  }
846 }
string Name
The name of the User
Definition: User.cs:41
IDatabaseCollection< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
@@ -106,20 +105,21 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
string GitHubOwner
If the repository was cloned from GitHub.com this will be set with the owner of the repository ...
Definition: Repository.cs:28
-
DbSet< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the DatabaseContext<TParentContext>.
Many to many relationship for Models.RevisionInformation and Models.TestMerge
void Add(TModel model)
Add a given model to the the working set.
-
async Task< IActionResult > Read(CancellationToken cancellationToken)
Get Repository status.
-
async Task< IActionResult > Update([FromBody]Repository model, CancellationToken cancellationToken)
Perform updats to the Repository.
+
async Task< IActionResult > Read(CancellationToken cancellationToken)
Get Repository status.
+
async Task< IActionResult > Update([FromBody]Repository model, CancellationToken cancellationToken)
Perform updats to the Repository.
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
+
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
async Task< bool > PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
User may list and read all Models.CompileJobs
+
DbSet< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the DatabaseContext.
readonly IInstanceManager instanceManager
The IInstanceManager for the RepositoryController
Routes to a server actions
Definition: Routes.cs:9
@@ -137,7 +137,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for the RepositoryController
string Reference
The branch or tag HEAD points to
Definition: Repository.cs:48
-
IEnumerable< TModel > Local
An IEnumerable<T> of TModel s prioritizing in the working set.
+
IEnumerable< TModel > Local
An IEnumerable<T> of TModel s prioritizing in the working set.
RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger< RepositoryController > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct a RepositoryController
@@ -156,9 +156,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
-
async Task< IActionResult > Delete(CancellationToken cancellationToken)
Delete the Repository.
+
async Task< IActionResult > Delete(CancellationToken cancellationToken)
Delete the Repository.
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the RepositoryController
- +
diff --git a/_repository_manager_8cs.html b/_repository_manager_8cs.html index 96a8424ab2..a24d40395d 100644 --- a/_repository_manager_8cs.html +++ b/_repository_manager_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_repository_manager_8cs_source.html b/_repository_manager_8cs_source.html index cf0cc9d1e3..af8eb12bdb 100644 --- a/_repository_manager_8cs_source.html +++ b/_repository_manager_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_repository_rights_8cs.html b/_repository_rights_8cs.html index 3e6d202069..4ab0599bbd 100644 --- a/_repository_rights_8cs.html +++ b/_repository_rights_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_repository_rights_8cs_source.html b/_repository_rights_8cs_source.html index 4db0bac41f..0710baac81 100644 --- a/_repository_rights_8cs_source.html +++ b/_repository_rights_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_request_timeout_exception_8cs.html b/_request_timeout_exception_8cs.html index bbffacd1c7..86bc344673 100644 --- a/_request_timeout_exception_8cs.html +++ b/_request_timeout_exception_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_request_timeout_exception_8cs_source.html b/_request_timeout_exception_8cs_source.html index ea9852ee06..f73571dc2f 100644 --- a/_request_timeout_exception_8cs_source.html +++ b/_request_timeout_exception_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_resolving_i_o_manager_8cs.html b/_resolving_i_o_manager_8cs.html index 55aa171b10..5028c45e3f 100644 --- a/_resolving_i_o_manager_8cs.html +++ b/_resolving_i_o_manager_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_resolving_i_o_manager_8cs_source.html b/_resolving_i_o_manager_8cs_source.html index 42dd74fae2..59eed0265f 100644 --- a/_resolving_i_o_manager_8cs_source.html +++ b/_resolving_i_o_manager_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_restart_registration_8cs.html b/_restart_registration_8cs.html index 7f7045dde1..055ec46361 100644 --- a/_restart_registration_8cs.html +++ b/_restart_registration_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_restart_registration_8cs_source.html b/_restart_registration_8cs_source.html index 0a31e5df81..335e389ef1 100644 --- a/_restart_registration_8cs_source.html +++ b/_restart_registration_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_rev_info_test_merge_8cs.html b/_rev_info_test_merge_8cs.html index 829512d366..f92d877f4b 100644 --- a/_rev_info_test_merge_8cs.html +++ b/_rev_info_test_merge_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_rev_info_test_merge_8cs_source.html b/_rev_info_test_merge_8cs_source.html index 6a5fcd3484..7e53a0b779 100644 --- a/_rev_info_test_merge_8cs_source.html +++ b/_rev_info_test_merge_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_revision_command_8cs.html b/_revision_command_8cs.html index adff7c682f..3a54a20ed3 100644 --- a/_revision_command_8cs.html +++ b/_revision_command_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_revision_command_8cs_source.html b/_revision_command_8cs_source.html index 87cfce17b7..bf369c1d7b 100644 --- a/_revision_command_8cs_source.html +++ b/_revision_command_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_rights_helper_8cs.html b/_rights_helper_8cs.html index 89ff8b1600..42ded87cc4 100644 --- a/_rights_helper_8cs.html +++ b/_rights_helper_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_rights_helper_8cs_source.html b/_rights_helper_8cs_source.html index b1251e7b73..0086d2dc93 100644 --- a/_rights_helper_8cs_source.html +++ b/_rights_helper_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_rights_type_8cs.html b/_rights_type_8cs.html index 6829a5d67c..cbc359236b 100644 --- a/_rights_type_8cs.html +++ b/_rights_type_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_rights_type_8cs_source.html b/_rights_type_8cs_source.html index 88b29d1975..5cee5570a2 100644 --- a/_rights_type_8cs_source.html +++ b/_rights_type_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_routes_8cs.html b/_routes_8cs.html index bd84ac2fce..d99452b44e 100644 --- a/_routes_8cs.html +++ b/_routes_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_routes_8cs_source.html b/_routes_8cs_source.html index 6cded58f4e..183994944b 100644 --- a/_routes_8cs_source.html +++ b/_routes_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_runtime_information_8cs.html b/_runtime_information_8cs.html index 915dcac683..bfc48c5988 100644 --- a/_runtime_information_8cs.html +++ b/_runtime_information_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_runtime_information_8cs_source.html b/_runtime_information_8cs_source.html index f03d320627..f47262d439 100644 --- a/_runtime_information_8cs_source.html +++ b/_runtime_information_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_semaphore_slim_context_8cs.html b/_semaphore_slim_context_8cs.html index b85ee7efda..fc6d48dca2 100644 --- a/_semaphore_slim_context_8cs.html +++ b/_semaphore_slim_context_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_semaphore_slim_context_8cs_source.html b/_semaphore_slim_context_8cs_source.html index d5ef7a001e..8343d357d4 100644 --- a/_semaphore_slim_context_8cs_source.html +++ b/_semaphore_slim_context_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_8cs.html b/_server_8cs.html index eb2f6e0362..b5ae7e7428 100644 --- a/_server_8cs.html +++ b/_server_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_8cs_source.html b/_server_8cs_source.html index b13cdaea4e..da584d7a32 100644 --- a/_server_8cs_source.html +++ b/_server_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Server.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.DependencyInjection;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host
16 {
18  sealed class Server : IServer, IServerControl
19  {
21  public bool RestartRequested { get; private set; }
22 
24  public bool WatchdogPresent => updatePath != null;
25 
29  readonly IHostBuilder hostBuilder;
30 
34  readonly List<IRestartHandler> restartHandlers;
35 
39  readonly string updatePath;
40 
44  readonly object restartLock;
45 
49  ILogger<Server> logger;
50 
55 
59  CancellationTokenSource cancellationTokenSource;
60 
65 
69  bool updating;
70 
76  public Server(IHostBuilder hostBuilder, string updatePath)
77  {
78  this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
79  this.updatePath = updatePath;
80 
81  hostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
82 
83  restartHandlers = new List<IRestartHandler>();
84  restartLock = new object();
85  }
86 
91  void CheckSanity(bool checkWatchdog)
92  {
93  if (checkWatchdog && !WatchdogPresent && propagatedException == null)
94  throw new InvalidOperationException("Server restarts are not supported");
95 
96  if (cancellationTokenSource == null || logger == null)
97  throw new InvalidOperationException("Tried to control a non-running Server!");
98  }
99 
104  void CheckExceptionPropagation(Exception otherException)
105  {
106  if (propagatedException == null)
107  return;
108 
109  if (otherException != null)
110  throw new AggregateException(propagatedException, otherException);
111 
112  throw propagatedException;
113  }
114 
116  public async Task Run(CancellationToken cancellationToken)
117  {
118  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
119  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
120  {
121  if (fsWatcher != null)
122  {
123  fsWatcher.Created += (a, b) =>
124  {
125  if (b.FullPath == updatePath && File.Exists(b.FullPath))
126  {
127  if (logger != null)
128  logger.LogInformation("Host watchdog appears to be requesting server termination!");
129  cancellationTokenSource.Cancel();
130  }
131  };
132  fsWatcher.EnableRaisingEvents = true;
133  }
134 
135  using var host = hostBuilder.Build();
136  try
137  {
138  logger = host.Services.GetRequiredService<ILogger<Server>>();
139  using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!")))
140  {
141  var generalConfigurationOptions = host.Services.GetRequiredService<IOptions<GeneralConfiguration>>();
142  generalConfiguration = generalConfigurationOptions.Value;
143  await host.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
144  }
145  }
146  catch (Exception ex)
147  {
148  CheckExceptionPropagation(ex);
149  throw;
150  }
151  }
152 
153  CheckExceptionPropagation(null);
154  }
155 
157  public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
158  {
159  if (version == null)
160  throw new ArgumentNullException(nameof(version));
161  if (updateZipUrl == null)
162  throw new ArgumentNullException(nameof(updateZipUrl));
163  if (ioManager == null)
164  throw new ArgumentNullException(nameof(ioManager));
165 
166  CheckSanity(true);
167 
168  logger.LogTrace("Begin ApplyUpdate...");
169 
170  lock (restartLock)
171  {
172  if (updating || RestartRequested)
173  {
174  logger.LogTrace("Aborted due to concurrency conflict!");
175  return false;
176  }
177 
178  updating = true;
179  }
180 
181  async void RunUpdate()
182  {
183  try
184  {
185  logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl);
186 
187  if (cancellationTokenSource == null)
188  throw new InvalidOperationException("Tried to update a non-running Server!");
189  var cancellationToken = cancellationTokenSource.Token;
190 
191  logger.LogTrace("Downloading zip package...");
192  var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false);
193 
194  try
195  {
196  logger.LogTrace("Exctracting zip package to {0}...", updatePath);
197  await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false);
198  }
199  catch (Exception e)
200  {
201  updating = false;
202  try
203  {
204  // important to not leave this directory around if possible
205  await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false);
206  }
207  catch (Exception e2)
208  {
209  throw new AggregateException(e, e2);
210  }
211 
212  throw;
213  }
214 
215  await Restart(version, null, true).ConfigureAwait(false);
216  }
217  catch (OperationCanceledException)
218  {
219  logger.LogInformation("Server update cancelled!");
220  }
221  catch (Exception e)
222  {
223  logger.LogError("Error updating server! Exception: {0}", e);
224  }
225  finally
226  {
227  updating = false;
228  }
229  }
230 
231  RunUpdate();
232  return true;
233  }
234 
237  {
238  if (handler == null)
239  throw new ArgumentNullException(nameof(handler));
240 
241  CheckSanity(false);
242 
243  lock (restartLock)
244  if (!RestartRequested)
245  {
246  logger.LogTrace("Registering restart handler {0}...", handler);
247  restartHandlers.Add(handler);
248  return new RestartRegistration(() =>
249  {
250  lock (restartLock)
251  if (!RestartRequested)
252  restartHandlers.Remove(handler);
253  });
254  }
255 
256  return new RestartRegistration(() => { });
257  }
258 
260  public Task Restart() => Restart(null, null, true);
261 
269  async Task Restart(Version newVersion, Exception exception, bool requireWatchdog)
270  {
271  CheckSanity(requireWatchdog);
272 
273  logger.LogTrace("Begin Restart...");
274 
275  lock (restartLock)
276  {
277  if ((updating && newVersion == null) || RestartRequested)
278  {
279  logger.LogTrace("Aborted due to concurrency conflict!");
280  return;
281  }
282 
283  RestartRequested = true;
284  propagatedException = exception;
285  }
286 
287  if (exception == null)
288  using (var cts = new CancellationTokenSource())
289  {
290  logger.LogInformation("Restarting server...");
291  var cancellationToken = cts.Token;
292  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
293 
294  var expiryTask = Task.Delay(TimeSpan.FromMilliseconds(generalConfiguration.RestartTimeout));
295  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
296  logger.LogTrace("Joining restart handlers...");
297  cts.Cancel();
298  try
299  {
300  await eventsTask.ConfigureAwait(false);
301  }
302  catch (OperationCanceledException) { }
303  catch (Exception e)
304  {
305  logger.LogError("Restart handlers error! Exception: {0}", e);
306  }
307  }
308 
309  logger.LogTrace("Stopping host...");
310  cancellationTokenSource.Cancel();
311  }
312 
314  public Task Die(Exception exception) => Restart(null, exception, false);
315  }
316 }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd.
+Go to the documentation of this file.
1 using Microsoft.Extensions.DependencyInjection;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host
16 {
18  sealed class Server : IServer, IServerControl
19  {
21  public bool RestartRequested { get; private set; }
22 
24  public bool WatchdogPresent => updatePath != null;
25 
29  readonly IHostBuilder hostBuilder;
30 
34  readonly List<IRestartHandler> restartHandlers;
35 
39  readonly string updatePath;
40 
44  readonly object restartLock;
45 
49  ILogger<Server> logger;
50 
55 
59  CancellationTokenSource cancellationTokenSource;
60 
65 
69  bool updating;
70 
76  public Server(IHostBuilder hostBuilder, string updatePath)
77  {
78  this.hostBuilder = hostBuilder ?? throw new ArgumentNullException(nameof(hostBuilder));
79  this.updatePath = updatePath;
80 
81  hostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
82 
83  restartHandlers = new List<IRestartHandler>();
84  restartLock = new object();
85  }
86 
91  void CheckSanity(bool checkWatchdog)
92  {
93  if (checkWatchdog && !WatchdogPresent && propagatedException == null)
94  throw new InvalidOperationException("Server restarts are not supported");
95 
96  if (cancellationTokenSource == null || logger == null)
97  throw new InvalidOperationException("Tried to control a non-running Server!");
98  }
99 
104  void CheckExceptionPropagation(Exception otherException)
105  {
106  if (propagatedException == null)
107  return;
108 
109  if (otherException != null)
110  throw new AggregateException(propagatedException, otherException);
111 
112  throw propagatedException;
113  }
114 
116  public async Task Run(CancellationToken cancellationToken)
117  {
118  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
119  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
120  {
121  if (fsWatcher != null)
122  {
123  fsWatcher.Created += (a, b) =>
124  {
125  if (b.FullPath == updatePath && File.Exists(b.FullPath))
126  {
127  if (logger != null)
128  logger.LogInformation("Host watchdog appears to be requesting server termination!");
129  cancellationTokenSource.Cancel();
130  }
131  };
132  fsWatcher.EnableRaisingEvents = true;
133  }
134 
135  using var host = hostBuilder.Build();
136  try
137  {
138  logger = host.Services.GetRequiredService<ILogger<Server>>();
139  using (cancellationToken.Register(() => logger.LogInformation("Server termination requested!")))
140  {
141  var generalConfigurationOptions = host.Services.GetRequiredService<IOptions<GeneralConfiguration>>();
142  generalConfiguration = generalConfigurationOptions.Value;
143  await host.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
144  }
145  }
146  catch (Exception ex)
147  {
148  CheckExceptionPropagation(ex);
149  throw;
150  }
151  }
152 
153  CheckExceptionPropagation(null);
154  }
155 
157  public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
158  {
159  if (version == null)
160  throw new ArgumentNullException(nameof(version));
161  if (updateZipUrl == null)
162  throw new ArgumentNullException(nameof(updateZipUrl));
163  if (ioManager == null)
164  throw new ArgumentNullException(nameof(ioManager));
165 
166  CheckSanity(true);
167 
168  logger.LogTrace("Begin ApplyUpdate...");
169 
170  lock (restartLock)
171  {
172  if (updating || RestartRequested)
173  {
174  logger.LogTrace("Aborted due to concurrency conflict!");
175  return false;
176  }
177 
178  updating = true;
179  }
180 
181  async void RunUpdate()
182  {
183  try
184  {
185  logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl);
186 
187  if (cancellationTokenSource == null)
188  throw new InvalidOperationException("Tried to update a non-running Server!");
189  var cancellationToken = cancellationTokenSource.Token;
190 
191  logger.LogTrace("Downloading zip package...");
192  var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false);
193 
194  try
195  {
196  logger.LogTrace("Exctracting zip package to {0}...", updatePath);
197  await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false);
198  }
199  catch (Exception e)
200  {
201  updating = false;
202  try
203  {
204  // important to not leave this directory around if possible
205  await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false);
206  }
207  catch (Exception e2)
208  {
209  throw new AggregateException(e, e2);
210  }
211 
212  throw;
213  }
214 
215  await Restart(version, null, true).ConfigureAwait(false);
216  }
217  catch (OperationCanceledException)
218  {
219  logger.LogInformation("Server update cancelled!");
220  }
221  catch (Exception e)
222  {
223  logger.LogError("Error updating server! Exception: {0}", e);
224  }
225  finally
226  {
227  updating = false;
228  }
229  }
230 
231  RunUpdate();
232  return true;
233  }
234 
237  {
238  if (handler == null)
239  throw new ArgumentNullException(nameof(handler));
240 
241  CheckSanity(false);
242 
243  lock (restartLock)
244  if (!RestartRequested)
245  {
246  logger.LogTrace("Registering restart handler {0}...", handler);
247  restartHandlers.Add(handler);
248  return new RestartRegistration(() =>
249  {
250  lock (restartLock)
251  if (!RestartRequested)
252  restartHandlers.Remove(handler);
253  });
254  }
255 
256  return new RestartRegistration(() => { });
257  }
258 
260  public Task Restart() => Restart(null, null, true);
261 
269  async Task Restart(Version newVersion, Exception exception, bool requireWatchdog)
270  {
271  CheckSanity(requireWatchdog);
272 
273  logger.LogTrace("Begin Restart...");
274 
275  lock (restartLock)
276  {
277  if ((updating && newVersion == null) || RestartRequested)
278  {
279  logger.LogTrace("Aborted due to concurrency conflict!");
280  return;
281  }
282 
283  RestartRequested = true;
284  propagatedException = exception;
285  }
286 
287  if (exception == null)
288  using (var cts = new CancellationTokenSource())
289  {
290  logger.LogInformation("Restarting server...");
291  var cancellationToken = cts.Token;
292  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
293 
294  var expiryTask = Task.Delay(TimeSpan.FromMilliseconds(generalConfiguration.RestartTimeout));
295  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
296  logger.LogTrace("Joining restart handlers...");
297  cts.Cancel();
298  try
299  {
300  await eventsTask.ConfigureAwait(false);
301  }
302  catch (OperationCanceledException)
303  {
304  logger.LogError("Restart timeout hit! Existing DreamDaemon processes will be lost and must be killed manually before being restarted with TGS!");
305  }
306  catch (Exception e)
307  {
308  logger.LogError("Restart handlers error! Exception: {0}", e);
309  }
310  }
311 
312  logger.LogTrace("Stopping host...");
313  cancellationTokenSource.Cancel();
314  }
315 
317  public Task Die(Exception exception) => Restart(null, exception, false);
318  }
319 }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd.
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:49
bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
Run a new Host assembly and stop the current one. This will likely trigger all active CancellationTok...
Definition: Server.cs:157
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts
Definition: Server.cs:34
@@ -110,7 +110,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
GeneralConfiguration generalConfiguration
The GeneralConfiguration for the Server
Definition: Server.cs:54
void CheckExceptionPropagation(Exception otherException)
Re-throw propagatedException if it exists
Definition: Server.cs:104
-
int RestartTimeout
The timeout milliseconds for restarting the server
+
int RestartTimeout
The timeout milliseconds for restarting the server
readonly IHostBuilder hostBuilder
The IHostBuilder for the Server
Definition: Server.cs:29
readonly string updatePath
The absolute path to install updates to
Definition: Server.cs:39
diff --git a/_server_client_8cs.html b/_server_client_8cs.html index fa42c85895..95c33bfbfc 100644 --- a/_server_client_8cs.html +++ b/_server_client_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_client_8cs_source.html b/_server_client_8cs_source.html index 5c56033da7..6b20a71c33 100644 --- a/_server_client_8cs_source.html +++ b/_server_client_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_client_factory_8cs.html b/_server_client_factory_8cs.html index 623e020e76..ab6b9dab8a 100644 --- a/_server_client_factory_8cs.html +++ b/_server_client_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_client_factory_8cs_source.html b/_server_client_factory_8cs_source.html index bee93875eb..4c7a92be39 100644 --- a/_server_client_factory_8cs_source.html +++ b/_server_client_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_error_exception_8cs.html b/_server_error_exception_8cs.html index 637e00bf87..950b7f1a71 100644 --- a/_server_error_exception_8cs.html +++ b/_server_error_exception_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_error_exception_8cs_source.html b/_server_error_exception_8cs_source.html index 4b42ba91d4..2c8faad91b 100644 --- a/_server_error_exception_8cs_source.html +++ b/_server_error_exception_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_factory_8cs.html b/_server_factory_8cs.html index c8184ab76c..4b1c8b304c 100644 --- a/_server_factory_8cs.html +++ b/_server_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_factory_8cs_source.html b/_server_factory_8cs_source.html index ecc79a2e28..8ea0dcb34f 100644 --- a/_server_factory_8cs_source.html +++ b/_server_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,21 +94,23 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServerFactory.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.Configuration;
3 using Microsoft.Extensions.DependencyInjection;
4 using Microsoft.Extensions.Hosting;
5 using System;
6 using System.Threading;
7 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host
14 {
19  {
24 
26  public IIOManager IOManager { get; }
27 
33  internal ServerFactory(IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager)
34  {
35  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
36  IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
37  }
38 
40  public async Task<IServer> CreateServer(string[] args, string updatePath, CancellationToken cancellationToken)
41  {
42  if (args == null)
43  throw new ArgumentNullException(nameof(args));
44 
45  IHostBuilder CreateDefaultBuilder() => Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)
46  .ConfigureAppConfiguration((context, configuration) => configuration
47  .SetBasePath(
48  IOManager.ResolvePath()));
49 
50  var setupWizardHostBuilder = CreateDefaultBuilder()
51  .UseSetupApplication();
52 
53  IPostSetupServices postSetupServices;
54  using (var setupHost = setupWizardHostBuilder.Build())
55  {
56  postSetupServices = setupHost.Services.GetRequiredService<IPostSetupServices>();
57  await setupHost.RunAsync(cancellationToken).ConfigureAwait(false);
58  }
59 
60  var hostBuilder = CreateDefaultBuilder()
61  .ConfigureWebHostDefaults(webHostBuilder =>
62  webHostBuilder
63  .UseApplication(postSetupServices)
64  .SuppressStatusMessages(true)
65  .UseShutdownTimeout(TimeSpan.FromMinutes(1)));
66 
67  if (updatePath != null)
68  hostBuilder.UseContentRoot(
69  IOManager.ResolvePath(
70  IOManager.GetDirectoryName(assemblyInformationProvider.Path)));
71 
72  return new Server(hostBuilder, updatePath);
73  }
74  }
75 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.AspNetCore.Server.Kestrel.Core;
3 using Microsoft.Extensions.Configuration;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Hosting;
6 using System;
7 using System.Threading;
8 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host
16 {
21  {
26 
28  public IIOManager IOManager { get; }
29 
35  internal ServerFactory(IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager)
36  {
37  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
38  IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
39  }
40 
42  public async Task<IServer> CreateServer(string[] args, string updatePath, CancellationToken cancellationToken)
43  {
44  if (args == null)
45  throw new ArgumentNullException(nameof(args));
46 
47  IHostBuilder CreateDefaultBuilder() => Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args)
48  .ConfigureAppConfiguration((context, configuration) => configuration
49  .SetBasePath(
50  IOManager.ResolvePath()));
51 
52  var setupWizardHostBuilder = CreateDefaultBuilder()
53  .UseSetupApplication();
54 
55  IPostSetupServices postSetupServices;
56  using (var setupHost = setupWizardHostBuilder.Build())
57  {
58  postSetupServices = setupHost.Services.GetRequiredService<IPostSetupServices>();
59  await setupHost.RunAsync(cancellationToken).ConfigureAwait(false);
60  }
61 
62  var hostBuilder = CreateDefaultBuilder()
63  .ConfigureWebHost(webHostBuilder =>
64  webHostBuilder
65  .UseKestrel(kestrelOptions =>
66  {
67  var serverPortProvider = kestrelOptions.ApplicationServices.GetRequiredService<IServerPortProvider>();
68  kestrelOptions.ListenAnyIP(
69  serverPortProvider.HttpApiPort,
70  listenOptions => listenOptions.Protocols = HttpProtocols.Http1AndHttp2);
71  })
72  .UseIIS()
73  .UseIISIntegration()
74  .UseApplication(postSetupServices)
75  .SuppressStatusMessages(true)
76  .UseShutdownTimeout(TimeSpan.FromMinutes(1)));
77 
78  if (updatePath != null)
79  hostBuilder.UseContentRoot(
80  IOManager.ResolvePath(
81  IOManager.GetDirectoryName(assemblyInformationProvider.Path)));
82 
83  return new Server(hostBuilder, updatePath);
84  }
85  }
86 }
-
async Task< IServer > CreateServer(string[] args, string updatePath, CancellationToken cancellationToken)
Create a IServer
+
async Task< IServer > CreateServer(string[] args, string updatePath, CancellationToken cancellationToken)
Create a IServer
string Path
Gets the path to the executing assembly.
-
Implementation of IServerFactory.
-
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the ServerFactory.
+
Implementation of IServerFactory.
+
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the ServerFactory.
+
Set of objects needed to configure an Core.Application.
Interface for using filesystems
Definition: IIOManager.cs:11
+
Provides access to the server&#39;s HttpApiPort.
diff --git a/_server_information_8cs.html b/_server_information_8cs.html index e1e3680c12..28a1fed99d 100644 --- a/_server_information_8cs.html +++ b/_server_information_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_information_8cs_source.html b/_server_information_8cs_source.html index 37db2dfab0..fc71141d81 100644 --- a/_server_information_8cs_source.html +++ b/_server_information_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_port_proivder_8cs.html b/_server_port_proivder_8cs.html index eefd26a2f1..03a98044e5 100644 --- a/_server_port_proivder_8cs.html +++ b/_server_port_proivder_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_port_proivder_8cs_source.html b/_server_port_proivder_8cs_source.html index 4613fded0a..c5dd2fa2c2 100644 --- a/_server_port_proivder_8cs_source.html +++ b/_server_port_proivder_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,11 +94,16 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServerPortProivder.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Configuration;
2 using System;
3 using System.Linq;
4 
5 namespace Tgstation.Server.Host.Core
6 {
9  {
11  public ushort HttpApiPort { get; }
12 
17  public ServerPortProivder(IConfiguration configuration)
18  {
19  if (configuration == null)
20  throw new ArgumentNullException(nameof(configuration));
21 
22  var httpEndpoint = configuration
23  .GetSection("Kestrel")
24  .GetSection("EndPoints")
25  .GetSection("Http")
26  .GetSection("Url")
27  .Value;
28 
29  if (httpEndpoint == null)
30  throw new InvalidOperationException("Missing required configuration option Kestrel:EndPoints:Http:Url!");
31 
32  var splits = httpEndpoint.Split(":", StringSplitOptions.RemoveEmptyEntries);
33  var portString = splits.Last();
34  portString = portString.TrimEnd('/');
35 
36  if (!UInt16.TryParse(portString, out var result))
37  throw new InvalidOperationException($"Failed to parse HTTP EndPoint port: {httpEndpoint}");
38 
39  HttpApiPort = result;
40  }
41  }
42 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Configuration;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using System;
5 using System.Linq;
7 
8 namespace Tgstation.Server.Host.Core
9 {
12  {
14  public ushort HttpApiPort => generalConfiguration.ApiPort;
15 
20 
28  IOptions<GeneralConfiguration> generalConfigurationOptions,
29  IConfiguration configuration,
30  ILogger<ServerPortProivder> logger)
31  {
32  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
33  if (configuration == null)
34  throw new ArgumentNullException(nameof(configuration));
35 
36  var httpEndpoint = configuration
37  .GetSection("Kestrel")
38  .GetSection("EndPoints")
39  .GetSection("Http")
40  .GetSection("Url")
41  .Value;
42 
43  if (generalConfiguration.ApiPort == default && httpEndpoint == null)
44  throw new InvalidOperationException("Missing required configuration option General:ApiPort!");
45 
46  if (generalConfiguration.ApiPort != default)
47  return;
48 
49  logger.LogWarning("The \"Kestrel\" configuration section is deprecated! Please set your API port using the \"General:ApiPort\" configuration option!");
50 
51  var splits = httpEndpoint.Split(":", StringSplitOptions.RemoveEmptyEntries);
52  var portString = splits.Last();
53  portString = portString.TrimEnd('/');
54 
55  if (!UInt16.TryParse(portString, out var result))
56  throw new InvalidOperationException($"Failed to parse HTTP EndPoint port: {httpEndpoint}");
57 
58  generalConfiguration.ApiPort = result;
59  }
60  }
61 }
-
ServerPortProivder(IConfiguration configuration)
Initializes a new instance of the ServerPortProivder .
- +
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the ServerPortProivder.
+ + + +
ServerPortProivder(IOptions< GeneralConfiguration > generalConfigurationOptions, IConfiguration configuration, ILogger< ServerPortProivder > logger)
Initializes a new instance of the ServerPortProivder .
+
Provides access to the server&#39;s HttpApiPort.
+
diff --git a/_server_service_8cs.html b/_server_service_8cs.html index 4dbd293f60..4ff54ab750 100644 --- a/_server_service_8cs.html +++ b/_server_service_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_service_8cs_source.html b/_server_service_8cs_source.html index 021bd5f3ff..9e02578c19 100644 --- a/_server_service_8cs_source.html +++ b/_server_service_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_side_modifications_8cs.html b/_server_side_modifications_8cs.html index 4ceb9959a7..d28f6a6594 100644 --- a/_server_side_modifications_8cs.html +++ b/_server_side_modifications_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_server_side_modifications_8cs_source.html b/_server_side_modifications_8cs_source.html index 1ce4ac3c34..a7880cea3d 100644 --- a/_server_side_modifications_8cs_source.html +++ b/_server_side_modifications_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_service_2_program_8cs.html b/_service_2_program_8cs.html index bebf1fc041..cb7d4af3d8 100644 --- a/_service_2_program_8cs.html +++ b/_service_2_program_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_service_2_program_8cs_source.html b/_service_2_program_8cs_source.html index b4172e5aa0..71a6a3a1e6 100644 --- a/_service_2_program_8cs_source.html +++ b/_service_2_program_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_service_collection_extensions_8cs.html b/_service_collection_extensions_8cs.html index 2ebf19b757..0f3ebd2a32 100644 --- a/_service_collection_extensions_8cs.html +++ b/_service_collection_extensions_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_service_collection_extensions_8cs_source.html b/_service_collection_extensions_8cs_source.html index 7fabac2eed..ce0070a508 100644 --- a/_service_collection_extensions_8cs_source.html +++ b/_service_collection_extensions_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_service_unavailable_exception_8cs.html b/_service_unavailable_exception_8cs.html index e420a23e44..26ca6c93e4 100644 --- a/_service_unavailable_exception_8cs.html +++ b/_service_unavailable_exception_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_service_unavailable_exception_8cs_source.html b/_service_unavailable_exception_8cs_source.html index aa6c4ef715..d13b07b138 100644 --- a/_service_unavailable_exception_8cs_source.html +++ b/_service_unavailable_exception_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_session_controller_8cs.html b/_session_controller_8cs.html index 7bac756270..3c0dfdab7a 100644 --- a/_session_controller_8cs.html +++ b/_session_controller_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_session_controller_8cs_source.html b/_session_controller_8cs_source.html index 9930766267..af5a51a545 100644 --- a/_session_controller_8cs_source.html +++ b/_session_controller_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_session_controller_factory_8cs.html b/_session_controller_factory_8cs.html index fe6fcbe98e..d5b1cbadad 100644 --- a/_session_controller_factory_8cs.html +++ b/_session_controller_factory_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_session_controller_factory_8cs_source.html b/_session_controller_factory_8cs_source.html index 54ffcee050..52fa1e0211 100644 --- a/_session_controller_factory_8cs_source.html +++ b/_session_controller_factory_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_setup_application_8cs.html b/_setup_application_8cs.html index 00839f1dc4..a6d45b0322 100644 --- a/_setup_application_8cs.html +++ b/_setup_application_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_setup_application_8cs_source.html b/_setup_application_8cs_source.html index 7af3eb713e..30e69f8882 100644 --- a/_setup_application_8cs_source.html +++ b/_setup_application_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -109,13 +109,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DI root for configuring a SetupWizard.
- +
void ConfigureServices(IServiceCollection services)
Configure dependency injected services.
Set of objects needed to configure an Core.Application.
-
Configuration options for the Database.DatabaseContext<TParentContext>
+
Configuration options for the Database.DatabaseContext
virtual void ConfigureHostedService(IServiceCollection services)
Configures the IHostedService.
IIOManager that resolves paths to Environment.CurrentDirectory
@@ -125,9 +125,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Interface for using filesystems
Definition: IIOManager.cs:11
For identifying the current platform
- + - +
diff --git a/_setup_wizard_8cs.html b/_setup_wizard_8cs.html index 7c5cf2cb65..5d073f711f 100644 --- a/_setup_wizard_8cs.html +++ b/_setup_wizard_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_setup_wizard_8cs_source.html b/_setup_wizard_8cs_source.html index 8676930778..c74f72bf2b 100644 --- a/_setup_wizard_8cs_source.html +++ b/_setup_wizard_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -94,61 +94,64 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SetupWizard.cs
-Go to the documentation of this file.
1 using Microsoft.Data.Sqlite;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using MySql.Data.MySqlClient;
6 using Newtonsoft.Json;
7 using System;
8 using System.Collections.Generic;
9 using System.Data.Common;
10 using System.Data.SqlClient;
11 using System.Globalization;
12 using System.IO;
13 using System.Linq;
14 using System.Text;
15 using System.Text.RegularExpressions;
16 using System.Threading;
17 using System.Threading.Tasks;
21 using Tgstation.Server.Host.IO;
23 
24 namespace Tgstation.Server.Host.Setup
25 {
27  sealed class SetupWizard : IHostedService
28  {
33 
37  readonly IConsole console;
38 
42  readonly IHostEnvironment hostingEnvironment;
43 
48 
53 
58 
63 
67  readonly IHostApplicationLifetime applicationLifetime;
68 
73 
86  public SetupWizard(
87  IIOManager ioManager,
88  IConsole console,
89  IHostEnvironment hostingEnvironment,
90  IAssemblyInformationProvider assemblyInformationProvider,
91  IDatabaseConnectionFactory dbConnectionFactory,
92  IPlatformIdentifier platformIdentifier,
93  IAsyncDelayer asyncDelayer,
94  IHostApplicationLifetime applicationLifetime,
95  IOptions<GeneralConfiguration> generalConfigurationOptions)
96  {
97  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
98  this.console = console ?? throw new ArgumentNullException(nameof(console));
99  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
100  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
101  this.dbConnectionFactory = dbConnectionFactory ?? throw new ArgumentNullException(nameof(dbConnectionFactory));
102  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
103  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
104  this.applicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
105  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
106  }
107 
114  async Task<bool> PromptYesNo(string question, CancellationToken cancellationToken)
115  {
116  do
117  {
118  await console.WriteAsync(question, false, cancellationToken).ConfigureAwait(false);
119  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
120  var upperResponse = responseString.ToUpperInvariant();
121  if (upperResponse == "Y" || upperResponse == "YES")
122  return true;
123  else if (upperResponse == "N" || upperResponse == "NO")
124  return false;
125  await console.WriteAsync("Invalid response!", true, cancellationToken).ConfigureAwait(false);
126  }
127  while (true);
128  }
129 
135  async Task<ushort?> PromptForHostingPort(CancellationToken cancellationToken)
136  {
137  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
138  await console.WriteAsync("What port would you like to connect to TGS on?", true, cancellationToken).ConfigureAwait(false);
139  await console.WriteAsync("Note: If this is a docker container with the default port already mapped, use the default.", true, cancellationToken).ConfigureAwait(false);
140 
141  do
142  {
143  await console.WriteAsync("API Port (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
144  var portString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
145  if (String.IsNullOrWhiteSpace(portString))
146  return null;
147  if (UInt16.TryParse(portString, out var port) && port != 0)
148  return port;
149  await console.WriteAsync("Invalid port! Please enter a value between 1 and 65535", true, cancellationToken).ConfigureAwait(false);
150  }
151  while (true);
152  }
153 
164  DbConnection testConnection,
165  DatabaseConfiguration databaseConfiguration,
166  string databaseName,
167  bool dbExists,
168  CancellationToken cancellationToken)
169  {
170  bool isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite;
171  using (testConnection)
172  {
173  await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
174  await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
175  await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
176 
177  if (databaseConfiguration.DatabaseType == DatabaseType.MariaDB
178  || databaseConfiguration.DatabaseType == DatabaseType.MySql)
179  {
180  await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
181  using var command = testConnection.CreateCommand();
182  command.CommandText = "SELECT VERSION()";
183  var fullVersion = (string)await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
184  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
185  var splits = fullVersion.Split('-');
186  databaseConfiguration.MySqlServerVersion = splits.First();
187  }
188 
189  if (!isSqliteDB && !dbExists)
190  {
191  await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
192  using (var command = testConnection.CreateCommand())
193  {
194  // I really don't care about user sanitization here, they want to fuck their own DB? so be it
195 #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
196  command.CommandText = $"CREATE DATABASE {databaseName}";
197 #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
198  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
199  }
200 
201  await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
202  await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
203  using (var command = testConnection.CreateCommand())
204  {
205 #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
206  command.CommandText = $"DROP DATABASE {databaseName}";
207 #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
208  try
209  {
210  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
211  }
212  catch (OperationCanceledException)
213  {
214  throw;
215  }
216  catch (Exception e)
217  {
218  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
219  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
220  await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
221  await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
222  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
223  }
224  }
225  }
226  }
227 
228  if (isSqliteDB && !dbExists)
229  await Task.WhenAll(
230  console.WriteAsync("Deleting test database file...", true, cancellationToken),
231  ioManager.DeleteFile(databaseName, cancellationToken)).ConfigureAwait(false);
232  }
233 
234  async Task<string> ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken)
235  {
236  var resolvedPath = ioManager.ResolvePath(databaseName);
237  try
238  {
239  var directoryName = ioManager.GetDirectoryName(resolvedPath);
240  bool directoryExisted = await ioManager.DirectoryExists(directoryName, cancellationToken).ConfigureAwait(false);
241  await ioManager.CreateDirectory(directoryName, cancellationToken).ConfigureAwait(false);
242  try
243  {
244  await ioManager.WriteAllBytes(resolvedPath, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
245  }
246  catch
247  {
248  if (!directoryExisted)
249  await ioManager.DeleteDirectory(directoryName, cancellationToken).ConfigureAwait(false);
250  throw;
251  }
252  }
253  catch (IOException)
254  {
255  return null;
256  }
257 
258  if (!Path.IsPathRooted(databaseName))
259  {
260  await console.WriteAsync("Note, this relative path (currently) resolves to the following:", true, cancellationToken).ConfigureAwait(false);
261  await console.WriteAsync(resolvedPath, true, cancellationToken).ConfigureAwait(false);
262  bool writeResolved = await PromptYesNo(
263  "Would you like to save the relative path in the configuration? If not, the full path will be saved. (y/n): ",
264  cancellationToken)
265  .ConfigureAwait(false);
266 
267  if (writeResolved)
268  databaseName = resolvedPath;
269  }
270 
271  await ioManager.DeleteFile(databaseName, cancellationToken).ConfigureAwait(false);
272  return databaseName;
273  }
274 
280  async Task<DatabaseType> PromptDatabaseType(CancellationToken cancellationToken)
281  {
282  await console.WriteAsync(String.Empty, true, cancellationToken).ConfigureAwait(false);
283  await console.WriteAsync(
284  "NOTE: It is HIGHLY reccommended that TGS runs on a complete relational database, specfically *NOT* Sqlite.",
285  true,
286  cancellationToken)
287  .ConfigureAwait(false);
288  await console.WriteAsync(
289  "Sqlite, by nature cannot perform several DDL operations. Because of this future compatiblility cannot be guaranteed.",
290  true,
291  cancellationToken)
292  .ConfigureAwait(false);
293  await console.WriteAsync(
294  "This means that you may not be able to update to the next minor version of TGS4 without a clean re-installation!",
295  true,
296  cancellationToken)
297  .ConfigureAwait(false);
298  await console.WriteAsync(
299  "Please consider taking the time to set up a relational database if this is meant to be a long-standing server.",
300  true,
301  cancellationToken)
302  .ConfigureAwait(false);
303  await console.WriteAsync(String.Empty, true, cancellationToken).ConfigureAwait(false);
304 
305  await asyncDelayer.Delay(TimeSpan.FromSeconds(3), cancellationToken).ConfigureAwait(false);
306 
307  await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
308  do
309  {
310  await console.WriteAsync(
311  String.Format(
312  CultureInfo.InvariantCulture,
313  "Please enter one of {0}, {1}, {2} or {3}: ",
314  DatabaseType.MariaDB,
315  DatabaseType.MySql,
316  DatabaseType.SqlServer,
317  DatabaseType.Sqlite),
318  false,
319  cancellationToken)
320  .ConfigureAwait(false);
321  var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
322  if (Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
323  return databaseType;
324 
325  await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
326  }
327  while (true);
328  }
329 
335  async Task<DatabaseConfiguration> ConfigureDatabase(CancellationToken cancellationToken)
336  {
337  do
338  {
339  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
340 
341  var databaseConfiguration = new DatabaseConfiguration
342  {
343  DatabaseType = await PromptDatabaseType(cancellationToken).ConfigureAwait(false)
344  };
345 
346  string serverAddress = null;
347  uint? mySQLServerPort = null;
348 
349  bool isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite;
350  if (!isSqliteDB)
351  do
352  {
353  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
354  await console.WriteAsync("Enter the server's address and port [<server>:<port> or <server>] (blank for local): ", false, cancellationToken).ConfigureAwait(false);
355  serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
356  if (!String.IsNullOrWhiteSpace(serverAddress) && databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
357  {
358  var match = Regex.Match(serverAddress, @"^(?<server>.+):(?<port>.+)$");
359  if (match.Success)
360  {
361  serverAddress = match.Groups["server"].Value;
362  var portString = match.Groups["port"].Value;
363  if (uint.TryParse(portString, out uint port))
364  mySQLServerPort = port;
365  else
366  {
367  await console.WriteAsync($"Failed to parse port \"{portString}\", please try again.", true, cancellationToken).ConfigureAwait(false);
368  continue;
369  }
370  }
371  }
372 
373  break;
374  }
375  while (true);
376 
377  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
378  await console.WriteAsync($"Enter the database {(isSqliteDB ? "file path" : "name")} (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
379 
380  string databaseName;
381  bool dbExists = false;
382  do
383  {
384  databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
385  if (!String.IsNullOrWhiteSpace(databaseName))
386  {
387  if (isSqliteDB)
388  {
389  dbExists = await ioManager.FileExists(databaseName, cancellationToken).ConfigureAwait(false);
390  if (!dbExists)
391  databaseName = await ValidateNonExistantSqliteDBName(databaseName, cancellationToken).ConfigureAwait(false);
392  }
393  else
394  dbExists = await PromptYesNo("Does this database already exist? If not, we will attempt to CREATE it. (y/n): ", cancellationToken).ConfigureAwait(false);
395  }
396 
397  if (String.IsNullOrWhiteSpace(databaseName))
398  await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
399  else
400  break;
401  }
402  while (true);
403 
404  bool useWinAuth;
405  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
406  useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
407  else
408  useWinAuth = false;
409 
410  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
411 
412  string username = null;
413  string password = null;
414  if (!isSqliteDB)
415  if (!useWinAuth)
416  {
417  await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
418  username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
419  await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
420  password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
421  }
422  else
423  {
424  await console.WriteAsync("IMPORTANT: If using the service runner, ensure this computer's LocalSystem account has CREATE DATABASE permissions on the target server!", true, cancellationToken).ConfigureAwait(false);
425  await console.WriteAsync("The account it uses in MSSQL is usually \"NT AUTHORITY\\SYSTEM\" and the role it needs is usually \"dbcreator\".", true, cancellationToken).ConfigureAwait(false);
426  await console.WriteAsync("We'll run a sanity test here, but it won't be indicative of the service's permissions if that is the case", true, cancellationToken).ConfigureAwait(false);
427  }
428 
429  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
430 
431  DbConnection testConnection;
432  void CreateTestConnection(string connectionString) =>
433  testConnection = dbConnectionFactory.CreateConnection(
434  connectionString,
435  databaseConfiguration.DatabaseType);
436 
437  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
438  {
439  var csb = new SqlConnectionStringBuilder
440  {
441  ApplicationName = assemblyInformationProvider.VersionPrefix,
442  DataSource = serverAddress ?? "(local)"
443  };
444 
445  if (useWinAuth)
446  csb.IntegratedSecurity = true;
447  else
448  {
449  csb.UserID = username;
450  csb.Password = password;
451  }
452 
453  CreateTestConnection(csb.ConnectionString);
454  csb.InitialCatalog = databaseName;
455  databaseConfiguration.ConnectionString = csb.ConnectionString;
456  }
457  else if(databaseConfiguration.DatabaseType == DatabaseType.Sqlite)
458  {
459  var csb = new SqliteConnectionStringBuilder
460  {
461  DataSource = databaseName,
462  Mode = dbExists ? SqliteOpenMode.ReadOnly : SqliteOpenMode.ReadWriteCreate
463  };
464 
465  CreateTestConnection(csb.ConnectionString);
466  databaseConfiguration.ConnectionString = csb.ConnectionString;
467  }
468  else
469  {
470  // MySQL/MariaDB
471  var csb = new MySqlConnectionStringBuilder
472  {
473  Server = serverAddress ?? "127.0.0.1",
474  UserID = username,
475  Password = password
476  };
477 
478  if (mySQLServerPort.HasValue)
479  csb.Port = mySQLServerPort.Value;
480 
481  CreateTestConnection(csb.ConnectionString);
482  csb.Database = databaseName;
483  databaseConfiguration.ConnectionString = csb.ConnectionString;
484  }
485 
486  try
487  {
488  await TestDatabaseConnection(testConnection, databaseConfiguration, databaseName, dbExists, cancellationToken).ConfigureAwait(false);
489 
490  return databaseConfiguration;
491  }
492  catch (OperationCanceledException)
493  {
494  throw;
495  }
496  catch (Exception e)
497  {
498  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
499  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
500  await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
501  }
502  }
503  while (true);
504  }
505 
511  async Task<GeneralConfiguration> ConfigureGeneral(CancellationToken cancellationToken)
512  {
513  var newGeneralConfiguration = new GeneralConfiguration
514  {
516  };
517 
518  do
519  {
520  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
521  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
522  var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
523  if (String.IsNullOrWhiteSpace(passwordLengthString))
524  break;
525  if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
526  {
527  newGeneralConfiguration.MinimumPasswordLength = passwordLength;
528  break;
529  }
530 
531  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
532  }
533  while (true);
534 
535  do
536  {
537  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
538  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Timeout for sending and receiving BYOND topics (ms, 0 for infinite, leave blank for default of {0}): ", newGeneralConfiguration.ByondTopicTimeout), false, cancellationToken).ConfigureAwait(false);
539  var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
540  if (String.IsNullOrWhiteSpace(topicTimeoutString))
541  break;
542  if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
543  {
544  newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
545  break;
546  }
547 
548  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
549  }
550  while (true);
551 
552  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
553  await console.WriteAsync("Enter a GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)", true, cancellationToken).ConfigureAwait(false);
554  await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
555  newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
556  if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
557  newGeneralConfiguration.GitHubAccessToken = null;
558 
559  // newGeneralConfiguration.UseExperimentalWatchdog = await PromptYesNo("Use the experimental watchdog (NOT RECOMMENDED)? (y/n): ", cancellationToken).ConfigureAwait(false);
560  return newGeneralConfiguration;
561  }
562 
568  async Task<FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
569  {
570  var fileLoggingConfiguration = new FileLoggingConfiguration();
571  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
572  fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);
573 
574  if (!fileLoggingConfiguration.Disable)
575  {
576  do
577  {
578  await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
579  fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
580  if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
581  {
582  fileLoggingConfiguration.Directory = null;
583  break;
584  }
585 
586  // test a write of it
587  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
588  await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);
589  try
590  {
591  await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);
592  var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
593  await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
594  try
595  {
596  await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
597  }
598  catch (OperationCanceledException)
599  {
600  throw;
601  }
602  catch (Exception e)
603  {
604  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);
605  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
606  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
607  }
608 
609  break;
610  }
611  catch (OperationCanceledException)
612  {
613  throw;
614  }
615  catch (Exception e)
616  {
617  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
618  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
619  await console.WriteAsync("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
620  }
621  }
622  while (true);
623 
624  async Task<LogLevel?> PromptLogLevel(string question)
625  {
626  do
627  {
628  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
629  await console.WriteAsync(question, true, cancellationToken).ConfigureAwait(false);
630  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Enter one of {0}/{1}/{2}/{3}/{4}/{5} (leave blank for default): ", nameof(LogLevel.Trace), nameof(LogLevel.Debug), nameof(LogLevel.Information), nameof(LogLevel.Warning), nameof(LogLevel.Error), nameof(LogLevel.Critical)), false, cancellationToken).ConfigureAwait(false);
631  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
632  if (String.IsNullOrWhiteSpace(responseString))
633  return null;
634  if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
635  return logLevel;
636  await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
637  }
638  while (true);
639  }
640 
641  fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;
642  fileLoggingConfiguration.MicrosoftLogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for Microsoft logs (VERY verbose, default {0}).", fileLoggingConfiguration.MicrosoftLogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.MicrosoftLogLevel;
643  }
644 
645  return fileLoggingConfiguration;
646  }
647 
653  async Task<ControlPanelConfiguration> ConfigureControlPanel(CancellationToken cancellationToken)
654  {
655  var config = new ControlPanelConfiguration
656  {
657  Enable = await PromptYesNo("Enable the web control panel (Incomplete)? (y/n): ", cancellationToken).ConfigureAwait(false),
658  AllowAnyOrigin = await PromptYesNo("Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *) (y/n): ", cancellationToken).ConfigureAwait(false)
659  };
660 
661  if (!config.AllowAnyOrigin)
662  {
663  await console.WriteAsync("Enter a comma seperated list of CORS allowed origins (optional): ", false, cancellationToken).ConfigureAwait(false);
664  var commaSeperatedOrigins = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
665  if (!String.IsNullOrWhiteSpace(commaSeperatedOrigins))
666  {
667  var splits = commaSeperatedOrigins.Split(',');
668  config.AllowedOrigins = new List<string>(splits.Select(x => x.Trim()));
669  }
670  }
671 
672  return config;
673  }
674 
686  async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
687  {
688  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
689 
690  var map = new Dictionary<string, object>()
691  {
692  { DatabaseConfiguration.Section, databaseConfiguration },
693  { GeneralConfiguration.Section, newGeneralConfiguration },
694  { FileLoggingConfiguration.Section, fileLoggingConfiguration },
695  { ControlPanelConfiguration.Section, controlPanelConfiguration }
696  };
697 
698  if (hostingPort.HasValue)
699  map.Add("Kestrel", new
700  {
701  EndPoints = new
702  {
703  Http = new
704  {
705  Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort)
706  }
707  }
708  });
709 
710  var json = JsonConvert.SerializeObject(map, Formatting.Indented);
711  var configBytes = Encoding.UTF8.GetBytes(json);
712 
713  try
714  {
715  await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
716  }
717  catch (OperationCanceledException)
718  {
719  throw;
720  }
721  catch (Exception e)
722  {
723  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
724  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
725  await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
726  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
727  await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
728  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
729  await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
730  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
731  throw new OperationCanceledException();
732  }
733  }
734 
741  async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
742  {
743  // welcome message
744  await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
745  await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
746 
747  var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
748 
749  var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
750 
751  var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
752 
753  var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false);
754 
755  var controlPanelConfiguration = await ConfigureControlPanel(cancellationToken).ConfigureAwait(false);
756 
757  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
758 
759  await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, controlPanelConfiguration, cancellationToken).ConfigureAwait(false);
760  }
761 
767  async Task CheckRunWizard(CancellationToken cancellationToken)
768  {
769  var setupWizardMode = generalConfiguration.SetupWizardMode;
770  if (setupWizardMode == SetupWizardMode.Never)
771  return;
772 
773  var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
774  if (!console.Available)
775  {
776  if (forceRun)
777  throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!");
778  return;
779  }
780 
781  var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
782 
783  async Task HandleSetupCancel()
784  {
785  await console.WriteAsync(String.Empty, true, default).ConfigureAwait(false);
786  await console.WriteAsync("Aborting setup!", true, default).ConfigureAwait(false);
787  }
788 
789  // Link passed cancellationToken with cancel key press
790  Task finalTask = Task.CompletedTask;
791  using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, console.CancelKeyPress))
792  using ((cancellationToken = cts.Token).Register(() => finalTask = HandleSetupCancel()))
793  try
794  {
795  var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);
796 
797  bool shouldRunBasedOnAutodetect;
798  if (exists)
799  {
800  var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);
801  var contents = Encoding.UTF8.GetString(bytes);
802  var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}";
803  shouldRunBasedOnAutodetect = existingConfigIsEmpty;
804  }
805  else
806  shouldRunBasedOnAutodetect = true;
807 
808  if (!shouldRunBasedOnAutodetect)
809  {
810  if (forceRun)
811  {
812  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
813 
814  forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
815  }
816 
817  if (!forceRun)
818  return;
819  }
820 
821  // flush the logs to prevent console conflicts
822  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
823 
824  await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
825  }
826  finally
827  {
828  await finalTask.ConfigureAwait(false);
829  }
830  }
831 
833  public async Task StartAsync(CancellationToken cancellationToken)
834  {
835  await CheckRunWizard(cancellationToken).ConfigureAwait(false);
836  applicationLifetime.StopApplication();
837  }
838 
840  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
841  }
842 }
+Go to the documentation of this file.
1 using Microsoft.Data.Sqlite;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using MySql.Data.MySqlClient;
6 using Newtonsoft.Json;
7 using Npgsql;
8 using System;
9 using System.Collections.Generic;
10 using System.Data.Common;
11 using System.Data.SqlClient;
12 using System.Globalization;
13 using System.IO;
14 using System.Linq;
15 using System.Text;
16 using System.Text.RegularExpressions;
17 using System.Threading;
18 using System.Threading.Tasks;
22 using Tgstation.Server.Host.IO;
24 
25 namespace Tgstation.Server.Host.Setup
26 {
28  sealed class SetupWizard : IHostedService
29  {
34 
38  readonly IConsole console;
39 
43  readonly IHostEnvironment hostingEnvironment;
44 
49 
54 
59 
64 
68  readonly IHostApplicationLifetime applicationLifetime;
69 
74 
87  public SetupWizard(
88  IIOManager ioManager,
89  IConsole console,
90  IHostEnvironment hostingEnvironment,
91  IAssemblyInformationProvider assemblyInformationProvider,
92  IDatabaseConnectionFactory dbConnectionFactory,
93  IPlatformIdentifier platformIdentifier,
94  IAsyncDelayer asyncDelayer,
95  IHostApplicationLifetime applicationLifetime,
96  IOptions<GeneralConfiguration> generalConfigurationOptions)
97  {
98  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
99  this.console = console ?? throw new ArgumentNullException(nameof(console));
100  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
101  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
102  this.dbConnectionFactory = dbConnectionFactory ?? throw new ArgumentNullException(nameof(dbConnectionFactory));
103  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
104  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
105  this.applicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
106  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
107  }
108 
115  async Task<bool> PromptYesNo(string question, CancellationToken cancellationToken)
116  {
117  do
118  {
119  await console.WriteAsync(question, false, cancellationToken).ConfigureAwait(false);
120  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
121  var upperResponse = responseString.ToUpperInvariant();
122  if (upperResponse == "Y" || upperResponse == "YES")
123  return true;
124  else if (upperResponse == "N" || upperResponse == "NO")
125  return false;
126  await console.WriteAsync("Invalid response!", true, cancellationToken).ConfigureAwait(false);
127  }
128  while (true);
129  }
130 
136  async Task<ushort?> PromptForHostingPort(CancellationToken cancellationToken)
137  {
138  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
139  await console.WriteAsync("What port would you like to connect to TGS on?", true, cancellationToken).ConfigureAwait(false);
140  await console.WriteAsync("Note: If this is a docker container with the default port already mapped, use the default.", true, cancellationToken).ConfigureAwait(false);
141 
142  do
143  {
144  await console.WriteAsync(
145  $"API Port (leave blank for default of {GeneralConfiguration.DefaultApiPort}): ",
146  false,
147  cancellationToken)
148  .ConfigureAwait(false);
149  var portString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
150  if (String.IsNullOrWhiteSpace(portString))
151  return null;
152  if (UInt16.TryParse(portString, out var port) && port != 0)
153  return port;
154  await console.WriteAsync("Invalid port! Please enter a value between 1 and 65535", true, cancellationToken).ConfigureAwait(false);
155  }
156  while (true);
157  }
158 
169  DbConnection testConnection,
170  DatabaseConfiguration databaseConfiguration,
171  string databaseName,
172  bool dbExists,
173  CancellationToken cancellationToken)
174  {
175  bool isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite;
176  using (testConnection)
177  {
178  await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
179  await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
180  await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
181 
182  if (databaseConfiguration.DatabaseType == DatabaseType.MariaDB
183  || databaseConfiguration.DatabaseType == DatabaseType.MySql)
184  {
185  await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
186  using var command = testConnection.CreateCommand();
187  command.CommandText = "SELECT VERSION()";
188  var fullVersion = (string)await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
189  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
190  var splits = fullVersion.Split('-');
191  databaseConfiguration.MySqlServerVersion = splits.First();
192  }
193 
194  if (!isSqliteDB && !dbExists)
195  {
196  await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
197  using (var command = testConnection.CreateCommand())
198  {
199  // I really don't care about user sanitization here, they want to fuck their own DB? so be it
200 #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
201  command.CommandText = $"CREATE DATABASE {databaseName}";
202 #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
203  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
204  }
205 
206  await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
207  await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
208  using (var command = testConnection.CreateCommand())
209  {
210 #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities
211  command.CommandText = $"DROP DATABASE {databaseName}";
212 #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities
213  try
214  {
215  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
216  }
217  catch (OperationCanceledException)
218  {
219  throw;
220  }
221  catch (Exception e)
222  {
223  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
224  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
225  await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
226  await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
227  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
228  }
229  }
230  }
231  }
232 
233  if (isSqliteDB && !dbExists)
234  await Task.WhenAll(
235  console.WriteAsync("Deleting test database file...", true, cancellationToken),
236  ioManager.DeleteFile(databaseName, cancellationToken)).ConfigureAwait(false);
237  }
238 
239  async Task<string> ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken)
240  {
241  var resolvedPath = ioManager.ResolvePath(databaseName);
242  try
243  {
244  var directoryName = ioManager.GetDirectoryName(resolvedPath);
245  bool directoryExisted = await ioManager.DirectoryExists(directoryName, cancellationToken).ConfigureAwait(false);
246  await ioManager.CreateDirectory(directoryName, cancellationToken).ConfigureAwait(false);
247  try
248  {
249  await ioManager.WriteAllBytes(resolvedPath, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
250  }
251  catch
252  {
253  if (!directoryExisted)
254  await ioManager.DeleteDirectory(directoryName, cancellationToken).ConfigureAwait(false);
255  throw;
256  }
257  }
258  catch (IOException)
259  {
260  return null;
261  }
262 
263  if (!Path.IsPathRooted(databaseName))
264  {
265  await console.WriteAsync("Note, this relative path (currently) resolves to the following:", true, cancellationToken).ConfigureAwait(false);
266  await console.WriteAsync(resolvedPath, true, cancellationToken).ConfigureAwait(false);
267  bool writeResolved = await PromptYesNo(
268  "Would you like to save the relative path in the configuration? If not, the full path will be saved. (y/n): ",
269  cancellationToken)
270  .ConfigureAwait(false);
271 
272  if (writeResolved)
273  databaseName = resolvedPath;
274  }
275 
276  await ioManager.DeleteFile(databaseName, cancellationToken).ConfigureAwait(false);
277  return databaseName;
278  }
279 
286  async Task<DatabaseType> PromptDatabaseType(bool firstTime, CancellationToken cancellationToken)
287  {
288  if (firstTime)
289  {
290  await console.WriteAsync(String.Empty, true, cancellationToken).ConfigureAwait(false);
291  await console.WriteAsync(
292  "NOTE: It is HIGHLY reccommended that TGS runs on a complete relational database, specfically *NOT* Sqlite.",
293  true,
294  cancellationToken)
295  .ConfigureAwait(false);
296  await console.WriteAsync(
297  "Sqlite, by nature cannot perform several DDL operations. Because of this future compatiblility cannot be guaranteed.",
298  true,
299  cancellationToken)
300  .ConfigureAwait(false);
301  await console.WriteAsync(
302  "This means that you may not be able to update to the next minor version of TGS4 without a clean re-installation!",
303  true,
304  cancellationToken)
305  .ConfigureAwait(false);
306  await console.WriteAsync(
307  "Please consider taking the time to set up a relational database if this is meant to be a long-standing server.",
308  true,
309  cancellationToken)
310  .ConfigureAwait(false);
311  await console.WriteAsync(String.Empty, true, cancellationToken).ConfigureAwait(false);
312 
313  await asyncDelayer.Delay(TimeSpan.FromSeconds(3), cancellationToken).ConfigureAwait(false);
314  }
315 
316  await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
317  do
318  {
319  await console.WriteAsync(
320  String.Format(
321  CultureInfo.InvariantCulture,
322  "Please enter one of {0}, {1}, {2}, or {3}: ",
323  DatabaseType.MariaDB,
324  DatabaseType.MySql,
325 #pragma warning disable SA1515 // Single-line comment should be preceded by blank line
326  // DatabaseType.PostgresSql,
327  DatabaseType.SqlServer,
328 #pragma warning restore SA1515 // Single-line comment should be preceded by blank line
329  DatabaseType.Sqlite),
330  false,
331  cancellationToken)
332  .ConfigureAwait(false);
333  var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
334  if (Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
335  return databaseType;
336 
337  await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
338  }
339  while (true);
340  }
341 
347  #pragma warning disable CA1502 // TODO: Decomplexify
348  async Task<DatabaseConfiguration> ConfigureDatabase(CancellationToken cancellationToken)
349  {
350  bool firstTime = true;
351  do
352  {
353  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
354 
355  var databaseConfiguration = new DatabaseConfiguration
356  {
357  DatabaseType = await PromptDatabaseType(firstTime, cancellationToken).ConfigureAwait(false)
358  };
359  firstTime = false;
360 
361  string serverAddress = null;
362  ushort? serverPort = null;
363 
364  bool isSqliteDB = databaseConfiguration.DatabaseType == DatabaseType.Sqlite;
365  if (!isSqliteDB)
366  do
367  {
368  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
369  await console.WriteAsync("Enter the server's address and port [<server>:<port> or <server>] (blank for local): ", false, cancellationToken).ConfigureAwait(false);
370  serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
371  if (String.IsNullOrWhiteSpace(serverAddress))
372  serverAddress = null;
373  else if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
374  {
375  var match = Regex.Match(serverAddress, @"^(?<server>.+):(?<port>.+)$");
376  if (match.Success)
377  {
378  serverAddress = match.Groups["server"].Value;
379  var portString = match.Groups["port"].Value;
380  if (UInt16.TryParse(portString, out var port))
381  serverPort = port;
382  else
383  {
384  await console.WriteAsync($"Failed to parse port \"{portString}\", please try again.", true, cancellationToken).ConfigureAwait(false);
385  continue;
386  }
387  }
388  }
389 
390  break;
391  }
392  while (true);
393 
394  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
395  await console.WriteAsync($"Enter the database {(isSqliteDB ? "file path" : "name")} (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
396 
397  string databaseName;
398  bool dbExists = false;
399  do
400  {
401  databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
402  if (!String.IsNullOrWhiteSpace(databaseName))
403  {
404  if (isSqliteDB)
405  {
406  dbExists = await ioManager.FileExists(databaseName, cancellationToken).ConfigureAwait(false);
407  if (!dbExists)
408  databaseName = await ValidateNonExistantSqliteDBName(databaseName, cancellationToken).ConfigureAwait(false);
409  }
410  else
411  dbExists = await PromptYesNo("Does this database already exist? If not, we will attempt to CREATE it. (y/n): ", cancellationToken).ConfigureAwait(false);
412  }
413 
414  if (String.IsNullOrWhiteSpace(databaseName))
415  await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
416  else
417  break;
418  }
419  while (true);
420 
421  bool useWinAuth;
422  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
423  useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
424  else
425  useWinAuth = false;
426 
427  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
428 
429  string username = null;
430  string password = null;
431  if (!isSqliteDB)
432  if (!useWinAuth)
433  {
434  await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
435  username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
436  await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
437  password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
438  }
439  else
440  {
441  await console.WriteAsync("IMPORTANT: If using the service runner, ensure this computer's LocalSystem account has CREATE DATABASE permissions on the target server!", true, cancellationToken).ConfigureAwait(false);
442  await console.WriteAsync("The account it uses in MSSQL is usually \"NT AUTHORITY\\SYSTEM\" and the role it needs is usually \"dbcreator\".", true, cancellationToken).ConfigureAwait(false);
443  await console.WriteAsync("We'll run a sanity test here, but it won't be indicative of the service's permissions if that is the case", true, cancellationToken).ConfigureAwait(false);
444  }
445 
446  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
447 
448  DbConnection testConnection;
449  void CreateTestConnection(string connectionString) =>
450  testConnection = dbConnectionFactory.CreateConnection(
451  connectionString,
452  databaseConfiguration.DatabaseType);
453 
454  switch (databaseConfiguration.DatabaseType)
455  {
456  case DatabaseType.SqlServer:
457  {
458  var csb = new SqlConnectionStringBuilder
459  {
460  ApplicationName = assemblyInformationProvider.VersionPrefix,
461  DataSource = serverAddress ?? "(local)"
462  };
463 
464  if (useWinAuth)
465  csb.IntegratedSecurity = true;
466  else
467  {
468  csb.UserID = username;
469  csb.Password = password;
470  }
471 
472  CreateTestConnection(csb.ConnectionString);
473  csb.InitialCatalog = databaseName;
474  databaseConfiguration.ConnectionString = csb.ConnectionString;
475  }
476 
477  break;
478  case DatabaseType.MariaDB:
479  case DatabaseType.MySql:
480  {
481  // MySQL/MariaDB
482  var csb = new MySqlConnectionStringBuilder
483  {
484  Server = serverAddress ?? "127.0.0.1",
485  UserID = username,
486  Password = password
487  };
488 
489  if (serverPort.HasValue)
490  csb.Port = serverPort.Value;
491 
492  CreateTestConnection(csb.ConnectionString);
493  csb.Database = databaseName;
494  databaseConfiguration.ConnectionString = csb.ConnectionString;
495  }
496 
497  break;
498  case DatabaseType.Sqlite:
499  {
500  var csb = new SqliteConnectionStringBuilder
501  {
502  DataSource = databaseName,
503  Mode = dbExists ? SqliteOpenMode.ReadOnly : SqliteOpenMode.ReadWriteCreate
504  };
505 
506  CreateTestConnection(csb.ConnectionString);
507  databaseConfiguration.ConnectionString = csb.ConnectionString;
508  }
509 
510  break;
511  case DatabaseType.PostgresSql:
512  {
513  var csb = new NpgsqlConnectionStringBuilder
514  {
515  ApplicationName = assemblyInformationProvider.VersionPrefix,
516  Host = serverAddress ?? "127.0.0.1",
517  Password = password,
518  Username = username
519  };
520 
521  if (serverPort.HasValue)
522  csb.Port = serverPort.Value;
523 
524  CreateTestConnection(csb.ConnectionString);
525  csb.Database = databaseName;
526  databaseConfiguration.ConnectionString = csb.ConnectionString;
527  }
528 
529  break;
530  default:
531  throw new InvalidOperationException("Invalid DatabaseType!");
532  }
533 
534  try
535  {
536  await TestDatabaseConnection(testConnection, databaseConfiguration, databaseName, dbExists, cancellationToken).ConfigureAwait(false);
537 
538  return databaseConfiguration;
539  }
540  catch (OperationCanceledException)
541  {
542  throw;
543  }
544  catch (Exception e)
545  {
546  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
547  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
548  await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
549  }
550  }
551  while (true);
552  }
553  #pragma warning restore CA1502
554 
560  async Task<GeneralConfiguration> ConfigureGeneral(CancellationToken cancellationToken)
561  {
562  var newGeneralConfiguration = new GeneralConfiguration
563  {
565  };
566 
567  do
568  {
569  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
570  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
571  var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
572  if (String.IsNullOrWhiteSpace(passwordLengthString))
573  break;
574  if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
575  {
576  newGeneralConfiguration.MinimumPasswordLength = passwordLength;
577  break;
578  }
579 
580  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
581  }
582  while (true);
583 
584  do
585  {
586  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
587  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Timeout for sending and receiving BYOND topics (ms, 0 for infinite, leave blank for default of {0}): ", newGeneralConfiguration.ByondTopicTimeout), false, cancellationToken).ConfigureAwait(false);
588  var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
589  if (String.IsNullOrWhiteSpace(topicTimeoutString))
590  break;
591  if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
592  {
593  newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
594  break;
595  }
596 
597  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
598  }
599  while (true);
600 
601  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
602  await console.WriteAsync("Enter a GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)", true, cancellationToken).ConfigureAwait(false);
603  await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
604  newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
605  if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
606  newGeneralConfiguration.GitHubAccessToken = null;
607 
608  // newGeneralConfiguration.UseExperimentalWatchdog = await PromptYesNo("Use the experimental watchdog (NOT RECOMMENDED)? (y/n): ", cancellationToken).ConfigureAwait(false);
609  return newGeneralConfiguration;
610  }
611 
617  async Task<FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
618  {
619  var fileLoggingConfiguration = new FileLoggingConfiguration();
620  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
621  fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);
622 
623  if (!fileLoggingConfiguration.Disable)
624  {
625  do
626  {
627  await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
628  fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
629  if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
630  {
631  fileLoggingConfiguration.Directory = null;
632  break;
633  }
634 
635  // test a write of it
636  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
637  await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);
638  try
639  {
640  await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);
641  var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
642  await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
643  try
644  {
645  await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
646  }
647  catch (OperationCanceledException)
648  {
649  throw;
650  }
651  catch (Exception e)
652  {
653  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);
654  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
655  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
656  }
657 
658  break;
659  }
660  catch (OperationCanceledException)
661  {
662  throw;
663  }
664  catch (Exception e)
665  {
666  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
667  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
668  await console.WriteAsync("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
669  }
670  }
671  while (true);
672 
673  async Task<LogLevel?> PromptLogLevel(string question)
674  {
675  do
676  {
677  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
678  await console.WriteAsync(question, true, cancellationToken).ConfigureAwait(false);
679  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Enter one of {0}/{1}/{2}/{3}/{4}/{5} (leave blank for default): ", nameof(LogLevel.Trace), nameof(LogLevel.Debug), nameof(LogLevel.Information), nameof(LogLevel.Warning), nameof(LogLevel.Error), nameof(LogLevel.Critical)), false, cancellationToken).ConfigureAwait(false);
680  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
681  if (String.IsNullOrWhiteSpace(responseString))
682  return null;
683  if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
684  return logLevel;
685  await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
686  }
687  while (true);
688  }
689 
690  fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;
691  fileLoggingConfiguration.MicrosoftLogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for Microsoft logs (VERY verbose, default {0}).", fileLoggingConfiguration.MicrosoftLogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.MicrosoftLogLevel;
692  }
693 
694  return fileLoggingConfiguration;
695  }
696 
702  async Task<ControlPanelConfiguration> ConfigureControlPanel(CancellationToken cancellationToken)
703  {
704  var config = new ControlPanelConfiguration
705  {
706  Enable = await PromptYesNo("Enable the web control panel (Incomplete)? (y/n): ", cancellationToken).ConfigureAwait(false),
707  AllowAnyOrigin = await PromptYesNo("Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *) (y/n): ", cancellationToken).ConfigureAwait(false)
708  };
709 
710  if (!config.AllowAnyOrigin)
711  {
712  await console.WriteAsync("Enter a comma seperated list of CORS allowed origins (optional): ", false, cancellationToken).ConfigureAwait(false);
713  var commaSeperatedOrigins = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
714  if (!String.IsNullOrWhiteSpace(commaSeperatedOrigins))
715  {
716  var splits = commaSeperatedOrigins.Split(',');
717  config.AllowedOrigins = new List<string>(splits.Select(x => x.Trim()));
718  }
719  }
720 
721  return config;
722  }
723 
735  async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
736  {
737  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
738 
739  newGeneralConfiguration.ApiPort = hostingPort ?? GeneralConfiguration.DefaultApiPort;
740  var map = new Dictionary<string, object>()
741  {
742  { DatabaseConfiguration.Section, databaseConfiguration },
743  { GeneralConfiguration.Section, newGeneralConfiguration },
744  { FileLoggingConfiguration.Section, fileLoggingConfiguration },
745  { ControlPanelConfiguration.Section, controlPanelConfiguration }
746  };
747 
748  var json = JsonConvert.SerializeObject(map, Formatting.Indented);
749  var configBytes = Encoding.UTF8.GetBytes(json);
750 
751  try
752  {
753  await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
754  }
755  catch (OperationCanceledException)
756  {
757  throw;
758  }
759  catch (Exception e)
760  {
761  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
762  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
763  await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
764  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
765  await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
766  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
767  await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
768  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
769  throw new OperationCanceledException();
770  }
771  }
772 
779  async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
780  {
781  // welcome message
782  await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
783  await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
784 
785  var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
786 
787  var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
788 
789  var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
790 
791  var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false);
792 
793  var controlPanelConfiguration = await ConfigureControlPanel(cancellationToken).ConfigureAwait(false);
794 
795  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
796 
797  await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, controlPanelConfiguration, cancellationToken).ConfigureAwait(false);
798  }
799 
805  async Task CheckRunWizard(CancellationToken cancellationToken)
806  {
807  var setupWizardMode = generalConfiguration.SetupWizardMode;
808  if (setupWizardMode == SetupWizardMode.Never)
809  return;
810 
811  var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
812  if (!console.Available)
813  {
814  if (forceRun)
815  throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!");
816  return;
817  }
818 
819  var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
820 
821  async Task HandleSetupCancel()
822  {
823  await console.WriteAsync(String.Empty, true, default).ConfigureAwait(false);
824  await console.WriteAsync("Aborting setup!", true, default).ConfigureAwait(false);
825  }
826 
827  // Link passed cancellationToken with cancel key press
828  Task finalTask = Task.CompletedTask;
829  using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, console.CancelKeyPress))
830  using ((cancellationToken = cts.Token).Register(() => finalTask = HandleSetupCancel()))
831  try
832  {
833  var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);
834 
835  bool shouldRunBasedOnAutodetect;
836  if (exists)
837  {
838  var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);
839  var contents = Encoding.UTF8.GetString(bytes);
840  var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}";
841  shouldRunBasedOnAutodetect = existingConfigIsEmpty;
842  }
843  else
844  shouldRunBasedOnAutodetect = true;
845 
846  if (!shouldRunBasedOnAutodetect)
847  {
848  if (forceRun)
849  {
850  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
851 
852  forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
853  }
854 
855  if (!forceRun)
856  return;
857  }
858 
859  // flush the logs to prevent console conflicts
860  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
861 
862  await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
863  }
864  finally
865  {
866  await finalTask.ConfigureAwait(false);
867  }
868  }
869 
871  public async Task StartAsync(CancellationToken cancellationToken)
872  {
873  await CheckRunWizard(cancellationToken).ConfigureAwait(false);
874  applicationLifetime.StopApplication();
875  }
876 
878  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
879  }
880 }
+
List< string > AllowedOrigins
Origins allowed for CORS requests
-
async Task< DatabaseType > PromptDatabaseType(CancellationToken cancellationToken)
Prompt the user for the DatabaseType.
Definition: SetupWizard.cs:280
-
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the SetupWizard
Definition: SetupWizard.cs:62
-
readonly IIOManager ioManager
The IIOManager for the SetupWizard
Definition: SetupWizard.cs:32
-
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the SetupWizard
Definition: SetupWizard.cs:72
+
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the SetupWizard
Definition: SetupWizard.cs:63
+
readonly IIOManager ioManager
The IIOManager for the SetupWizard
Definition: SetupWizard.cs:33
+
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the SetupWizard
Definition: SetupWizard.cs:73
-
async Task TestDatabaseConnection(DbConnection testConnection, DatabaseConfiguration databaseConfiguration, string databaseName, bool dbExists, CancellationToken cancellationToken)
Ensure a given testConnection works.
Definition: SetupWizard.cs:163
+
async Task TestDatabaseConnection(DbConnection testConnection, DatabaseConfiguration databaseConfiguration, string databaseName, bool dbExists, CancellationToken cancellationToken)
Ensure a given testConnection works.
Definition: SetupWizard.cs:168
SetupWizardMode
Determines if the Setup.SetupWizard will run
Abstraction for global::System.Console
Definition: IConsole.cs:9
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
-
async Task< string > ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken)
Definition: SetupWizard.cs:234
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: SetupWizard.cs:833
-
async Task< GeneralConfiguration > ConfigureGeneral(CancellationToken cancellationToken)
Prompts the user to create a GeneralConfiguration
Definition: SetupWizard.cs:511
+
async Task< string > ValidateNonExistantSqliteDBName(string databaseName, CancellationToken cancellationToken)
Definition: SetupWizard.cs:239
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: SetupWizard.cs:871
+
async Task< GeneralConfiguration > ConfigureGeneral(CancellationToken cancellationToken)
Prompts the user to create a GeneralConfiguration
Definition: SetupWizard.cs:560
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the DatabaseConfiguration re...
DatabaseType DatabaseType
The Configuration.DatabaseType to create
-
async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
Runs the SetupWizard
Definition: SetupWizard.cs:741
+
async Task< DatabaseType > PromptDatabaseType(bool firstTime, CancellationToken cancellationToken)
Prompt the user for the DatabaseType.
Definition: SetupWizard.cs:286
+
async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
Runs the SetupWizard
Definition: SetupWizard.cs:779
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the ControlPanelConfiguratio...
+ -
async Task< ushort?> PromptForHostingPort(CancellationToken cancellationToken)
Prompts the user to enter the port to host TGS on
Definition: SetupWizard.cs:135
+
async Task< ushort?> PromptForHostingPort(CancellationToken cancellationToken)
Prompts the user to enter the port to host TGS on
Definition: SetupWizard.cs:136
-
async Task< DatabaseConfiguration > ConfigureDatabase(CancellationToken cancellationToken)
Prompts the user to create a DatabaseConfiguration
Definition: SetupWizard.cs:335
+
async Task< DatabaseConfiguration > ConfigureDatabase(CancellationToken cancellationToken)
Prompts the user to create a DatabaseConfiguration
Definition: SetupWizard.cs:348
uint MinimumPasswordLength
Minimum length of database user passwords.
- -
readonly IHostApplicationLifetime applicationLifetime
The IHostApplicationLifetime for the SetupWizard.
Definition: SetupWizard.cs:67
-
async Task< ControlPanelConfiguration > ConfigureControlPanel(CancellationToken cancellationToken)
Prompts the user to create a ControlPanelConfiguration
Definition: SetupWizard.cs:653
-
SetupWizard(IIOManager ioManager, IConsole console, IHostEnvironment hostingEnvironment, IAssemblyInformationProvider assemblyInformationProvider, IDatabaseConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, IHostApplicationLifetime applicationLifetime, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct a SetupWizard
Definition: SetupWizard.cs:86
+ +
readonly IHostApplicationLifetime applicationLifetime
The IHostApplicationLifetime for the SetupWizard.
Definition: SetupWizard.cs:68
+
const ushort DefaultApiPort
The default value of ApiPort.
+
async Task< ControlPanelConfiguration > ConfigureControlPanel(CancellationToken cancellationToken)
Prompts the user to create a ControlPanelConfiguration
Definition: SetupWizard.cs:702
+
SetupWizard(IIOManager ioManager, IConsole console, IHostEnvironment hostingEnvironment, IAssemblyInformationProvider assemblyInformationProvider, IDatabaseConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, IHostApplicationLifetime applicationLifetime, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct a SetupWizard
Definition: SetupWizard.cs:87
-
async Task< bool > PromptYesNo(string question, CancellationToken cancellationToken)
A prompt for a yes or no value
Definition: SetupWizard.cs:114
-
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:37
+
async Task< bool > PromptYesNo(string question, CancellationToken cancellationToken)
A prompt for a yes or no value
Definition: SetupWizard.cs:115
+
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:38
string MySqlServerVersion
The string form of the global::System.Version of a target MySQL/MariaDB server
-
async Task CheckRunWizard(CancellationToken cancellationToken)
Check if it should and run the SetupWizard if necessary.
Definition: SetupWizard.cs:767
-
Configuration options for the Database.DatabaseContext<TParentContext>
-
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SetupWizard
Definition: SetupWizard.cs:57
+
async Task CheckRunWizard(CancellationToken cancellationToken)
Check if it should and run the SetupWizard if necessary.
Definition: SetupWizard.cs:805
+
Configuration options for the Database.DatabaseContext
+
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SetupWizard
Definition: SetupWizard.cs:58
-
async Task< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration
Definition: SetupWizard.cs:568
+
async Task< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration
Definition: SetupWizard.cs:617
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the SetupWizard
Definition: SetupWizard.cs:47
-
async Task SaveConfiguration(string userConfigFileName, ushort?hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
Saves a given Configuration set to userConfigFileName
Definition: SetupWizard.cs:686
+
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the SetupWizard
Definition: SetupWizard.cs:48
+
async Task SaveConfiguration(string userConfigFileName, ushort?hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
Saves a given Configuration set to userConfigFileName
Definition: SetupWizard.cs:735
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the FileLoggingConfiguration...
Interface for using filesystems
Definition: IIOManager.cs:11
-
readonly IHostEnvironment hostingEnvironment
The IHostEnvironment for the SetupWizard
Definition: SetupWizard.cs:42
-
readonly IDatabaseConnectionFactory dbConnectionFactory
The IDatabaseConnectionFactory for the SetupWizard
Definition: SetupWizard.cs:52
+
readonly IHostEnvironment hostingEnvironment
The IHostEnvironment for the SetupWizard
Definition: SetupWizard.cs:43
+
readonly IDatabaseConnectionFactory dbConnectionFactory
The IDatabaseConnectionFactory for the SetupWizard
Definition: SetupWizard.cs:53
For identifying the current platform
- +
diff --git a/_setup_wizard_mode_8cs.html b/_setup_wizard_mode_8cs.html index eefa5c4bbd..87620a588a 100644 --- a/_setup_wizard_mode_8cs.html +++ b/_setup_wizard_mode_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_setup_wizard_mode_8cs_source.html b/_setup_wizard_mode_8cs_source.html index e9d2cd20e8..f7e5da09d6 100644 --- a/_setup_wizard_mode_8cs_source.html +++ b/_setup_wizard_mode_8cs_source.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/_sql_server_database_context_8cs.html b/_sql_server_database_context_8cs.html index 4ed248d0f8..d44cd0b63c 100644 --- a/_sql_server_database_context_8cs.html +++ b/_sql_server_database_context_8cs.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

Classes

class  Tgstation.Server.Host.Database.SqlServerDatabaseContextDatabaseContext<TParentContext> for Sqlserver More...
DatabaseContext for Sqlserver More...
  @@ -94,19 +94,19 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SqlServerDatabaseContext.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using System;
6 
7 namespace Tgstation.Server.Host.Database
8 {
12  sealed class SqlServerDatabaseContext : DatabaseContext<SqlServerDatabaseContext>
13  {
21  public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqlServerDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
22  { }
23 
25  protected override void OnConfiguring(DbContextOptionsBuilder options)
26  {
27  base.OnConfiguring(options);
28  options.UseSqlServer(DatabaseConfiguration.ConnectionString);
29  }
30 
32  protected override void ValidateDatabaseType()
33  {
34  if (DatabaseType != DatabaseType.SqlServer)
35  throw new InvalidOperationException("Invalid DatabaseType for SqlServerDatabaseContext!");
36  }
37  }
38 }
string ConnectionString
The connection string for the database
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using System;
6 
7 namespace Tgstation.Server.Host.Database
8 {
13  {
21  public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqlServerDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
22  { }
23 
25  protected override void OnConfiguring(DbContextOptionsBuilder options)
26  {
27  base.OnConfiguring(options);
28  options.UseSqlServer(DatabaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure());
29  }
30 
32  protected override void ValidateDatabaseType()
33  {
34  if (DatabaseType != DatabaseType.SqlServer)
35  throw new InvalidOperationException("Invalid DatabaseType for SqlServerDatabaseContext!");
36  }
37  }
38 }
string ConnectionString
The connection string for the database
For initially seeding a database
-
Backend abstract implementation of IDatabaseContext
+
Backend abstract implementation of IDatabaseContext
override void OnConfiguring(DbContextOptionsBuilder options)
-
Configuration options for the Database.DatabaseContext<TParentContext>
+
Configuration options for the Database.DatabaseContext
SqlServerDatabaseContext(DbContextOptions< SqlServerDatabaseContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger< SqlServerDatabaseContext > logger)
Construct a SqlServerDatabaseContext
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
override void ValidateDatabaseType()
Ensure the DatabaseType is correct for the DatabaseContext<TParentContext>.
-
DatabaseContext<TParentContext> for Sqlserver
+
override void ValidateDatabaseType()
Ensure the DatabaseType is correct for the DatabaseContext.
+
diff --git a/_sql_server_database_context_model_snapshot_8cs.html b/_sql_server_database_context_model_snapshot_8cs.html index 8e44ea680e..934daaa9d2 100644 --- a/_sql_server_database_context_model_snapshot_8cs.html +++ b/_sql_server_database_context_model_snapshot_8cs.html @@ -25,7 +25,7 @@
diff --git a/_sql_server_database_context_model_snapshot_8cs_source.html b/_sql_server_database_context_model_snapshot_8cs_source.html index 225b265932..b9c6d41b81 100644 --- a/_sql_server_database_context_model_snapshot_8cs_source.html +++ b/_sql_server_database_context_model_snapshot_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_sql_server_design_time_db_context_factory_8cs.html b/_sql_server_design_time_db_context_factory_8cs.html index 1c3f6b06c1..ffbae10487 100644 --- a/_sql_server_design_time_db_context_factory_8cs.html +++ b/_sql_server_design_time_db_context_factory_8cs.html @@ -25,7 +25,7 @@ @@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

diff --git a/_sql_server_database_context_8cs_source.html b/_sql_server_database_context_8cs_source.html index 977497ba9a..a49d13d301 100644 --- a/_sql_server_database_context_8cs_source.html +++ b/_sql_server_database_context_8cs_source.html @@ -25,7 +25,7 @@

tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite

Classes

class  Tgstation.Server.Host.Database.Design.SqlServerDesignTimeDbContextFactory
 IDesignTimeDbContextFactory<TContext> for creating SqlServerDatabaseContexts. More...
 IDesignTimeDbContextFactory<TContext> for creating SqlServerDatabaseContexts. More...
 
@@ -94,11 +94,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SqlServerDesignTimeDbContextFactory.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
9 
10 namespace Tgstation.Server.Host.Database.Design
11 {
15  sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqlServerDatabaseContext>
16  {
19  {
20  using var loggerFactory = new LoggerFactory();
21  return new SqlServerDatabaseContext(
22  new DbContextOptions<SqlServerDatabaseContext>(),
24  DatabaseType.SqlServer,
25  "Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server"),
26  new DatabaseSeeder(
28  new PasswordHasher<User>()),
29  new PlatformIdentifier()),
30  loggerFactory.CreateLogger<SqlServerDatabaseContext>());
31  }
32  }
33 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
9 
10 namespace Tgstation.Server.Host.Database.Design
11 {
15  sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqlServerDatabaseContext>
16  {
19  {
20  using var loggerFactory = new LoggerFactory();
21  return new SqlServerDatabaseContext(
22  new DbContextOptions<SqlServerDatabaseContext>(),
24  DatabaseType.SqlServer,
25  "Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server"),
26  new DatabaseSeeder(
28  new PasswordHasher<User>()),
29  new PlatformIdentifier()),
30  loggerFactory.CreateLogger<SqlServerDatabaseContext>());
31  }
32  }
33 }
IDesignTimeDbContextFactory<TContext> for creating SqlServerDatabaseContexts.
-
static IOptions< DatabaseConfiguration > GetDbContextOptions(DatabaseType databaseType, string connectionString)
Get the IOptions<TOptions> for the DatabaseConfiguration
+
static IOptions< DatabaseConfiguration > GetDatabaseConfiguration(DatabaseType databaseType, string connectionString)
Get the IOptions<TOptions> for the DatabaseConfiguration
@@ -106,9 +106,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
Contains helpers for creating design time DatabaseContext<TParentContext>s
+ -
DatabaseContext<TParentContext> for Sqlserver
+ diff --git a/_sqlite_database_context_8cs.html b/_sqlite_database_context_8cs.html index c644360a5b..f219c1ee42 100644 --- a/_sqlite_database_context_8cs.html +++ b/_sqlite_database_context_8cs.html @@ -25,7 +25,7 @@
@@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

diff --git a/_sql_server_design_time_db_context_factory_8cs_source.html b/_sql_server_design_time_db_context_factory_8cs_source.html index b2539f3c62..d3c27ba88f 100644 --- a/_sql_server_design_time_db_context_factory_8cs_source.html +++ b/_sql_server_design_time_db_context_factory_8cs_source.html @@ -25,7 +25,7 @@

tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite

Classes

class  Tgstation.Server.Host.Database.SqliteDatabaseContext
 DatabaseContext<TParentContext> for MySQL More...
 DatabaseContext for MySQL More...
 
@@ -94,19 +94,19 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SqliteDatabaseContext.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Linq;
8 
9 namespace Tgstation.Server.Host.Database
10 {
14  sealed class SqliteDatabaseContext : DatabaseContext<SqliteDatabaseContext>
15  {
23  public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqliteDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
24  { }
25 
27  protected override void OnConfiguring(DbContextOptionsBuilder options)
28  {
29  base.OnConfiguring(options);
30  options.UseSqlite(DatabaseConfiguration.ConnectionString);
31  }
32 
34  protected override void OnModelCreating(ModelBuilder modelBuilder)
35  {
36  base.OnModelCreating(modelBuilder);
37 
38  // Shamelessly stolen from https://blog.dangl.me/archive/handling-datetimeoffset-in-sqlite-with-entity-framework-core/
39 
40  // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
41  // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
42  // To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset
43  // use the DateTimeOffsetToBinaryConverter
44  // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
45  // This only supports millisecond precision, but should be sufficient for most use cases.
47  foreach (var entityType in modelBuilder.Model.GetEntityTypes())
48  {
49  var properties = entityType
50  .ClrType
51  .GetProperties()
52  .Where(p => p.PropertyType == typeof(DateTimeOffset) || p.PropertyType == typeof(DateTimeOffset?));
53  foreach (var property in properties)
54  modelBuilder
55  .Entity(entityType.Name)
56  .Property(property.Name)
57  .HasConversion(new DateTimeOffsetToBinaryConverter());
58  }
59  }
60 
62  protected override void ValidateDatabaseType()
63  {
64  if (DatabaseType != DatabaseType.Sqlite)
65  throw new InvalidOperationException("Invalid DatabaseType for SqliteDatabaseContext!");
66  }
67  }
68 }
override void OnConfiguring(DbContextOptionsBuilder options)
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Linq;
8 
9 namespace Tgstation.Server.Host.Database
10 {
15  {
23  public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqliteDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
24  { }
25 
27  protected override void OnConfiguring(DbContextOptionsBuilder options)
28  {
29  base.OnConfiguring(options);
30  options.UseSqlite(DatabaseConfiguration.ConnectionString);
31  }
32 
34  protected override void OnModelCreating(ModelBuilder modelBuilder)
35  {
36  base.OnModelCreating(modelBuilder);
37 
38  // Shamelessly stolen from https://blog.dangl.me/archive/handling-datetimeoffset-in-sqlite-with-entity-framework-core/
39 
40  // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations
41  // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations
42  // To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset
43  // use the DateTimeOffsetToBinaryConverter
44  // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754
45  // This only supports millisecond precision, but should be sufficient for most use cases.
47  foreach (var entityType in modelBuilder.Model.GetEntityTypes())
48  {
49  var properties = entityType
50  .ClrType
51  .GetProperties()
52  .Where(p => p.PropertyType == typeof(DateTimeOffset) || p.PropertyType == typeof(DateTimeOffset?));
53  foreach (var property in properties)
54  modelBuilder
55  .Entity(entityType.Name)
56  .Property(property.Name)
57  .HasConversion(new DateTimeOffsetToBinaryConverter());
58  }
59  }
60 
62  protected override void ValidateDatabaseType()
63  {
64  if (DatabaseType != DatabaseType.Sqlite)
65  throw new InvalidOperationException("Invalid DatabaseType for SqliteDatabaseContext!");
66  }
67  }
68 }
override void OnConfiguring(DbContextOptionsBuilder options)
string ConnectionString
The connection string for the database
bool DesignTime
Used to indicate that the database is being loaded to generate migrations. Should not be used in prod...
For initially seeding a database
-
DatabaseContext<TParentContext> for MySQL
-
Backend abstract implementation of IDatabaseContext
+ +
Backend abstract implementation of IDatabaseContext
-
Configuration options for the Database.DatabaseContext<TParentContext>
+
Configuration options for the Database.DatabaseContext
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
override void ValidateDatabaseType()
Ensure the DatabaseType is correct for the DatabaseContext<TParentContext>.
+
override void ValidateDatabaseType()
Ensure the DatabaseType is correct for the DatabaseContext.
override void OnModelCreating(ModelBuilder modelBuilder)
diff --git a/_sqlite_database_context_model_snapshot_8cs.html b/_sqlite_database_context_model_snapshot_8cs.html index 1ae41cfe2c..da79c36391 100644 --- a/_sqlite_database_context_model_snapshot_8cs.html +++ b/_sqlite_database_context_model_snapshot_8cs.html @@ -25,7 +25,7 @@
diff --git a/_sqlite_database_context_model_snapshot_8cs_source.html b/_sqlite_database_context_model_snapshot_8cs_source.html index 2c5491877a..52023dd679 100644 --- a/_sqlite_database_context_model_snapshot_8cs_source.html +++ b/_sqlite_database_context_model_snapshot_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_sqlite_design_time_db_context_factory_8cs.html b/_sqlite_design_time_db_context_factory_8cs.html index e8dfbf461b..aa022b8f65 100644 --- a/_sqlite_design_time_db_context_factory_8cs.html +++ b/_sqlite_design_time_db_context_factory_8cs.html @@ -25,7 +25,7 @@ @@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

diff --git a/_sqlite_database_context_8cs_source.html b/_sqlite_database_context_8cs_source.html index f50297237b..8c77355490 100644 --- a/_sqlite_database_context_8cs_source.html +++ b/_sqlite_database_context_8cs_source.html @@ -25,7 +25,7 @@

tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite

Classes

class  Tgstation.Server.Host.Database.Design.SqliteDesignTimeDbContextFactory
 IDesignTimeDbContextFactory<TContext> for creating SqliteDatabaseContexts. More...
 IDesignTimeDbContextFactory<TContext> for creating SqliteDatabaseContexts. More...
 
@@ -94,18 +94,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SqliteDesignTimeDbContextFactory.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
9 
10 namespace Tgstation.Server.Host.Database.Design
11 {
15  sealed class SqliteDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqliteDatabaseContext>
16  {
18  public SqliteDatabaseContext CreateDbContext(string[] args)
19  {
20  using var loggerFactory = new LoggerFactory();
21  return new SqliteDatabaseContext(
22  new DbContextOptions<SqliteDatabaseContext>(),
24  DatabaseType.Sqlite,
25  "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"),
26  new DatabaseSeeder(
28  new PasswordHasher<User>()),
29  new PlatformIdentifier()),
30  loggerFactory.CreateLogger<SqliteDatabaseContext>());
31  }
32  }
33 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
9 
10 namespace Tgstation.Server.Host.Database.Design
11 {
15  sealed class SqliteDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqliteDatabaseContext>
16  {
18  public SqliteDatabaseContext CreateDbContext(string[] args)
19  {
20  using var loggerFactory = new LoggerFactory();
21  return new SqliteDatabaseContext(
22  new DbContextOptions<SqliteDatabaseContext>(),
24  DatabaseType.Sqlite,
25  "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"),
26  new DatabaseSeeder(
28  new PasswordHasher<User>()),
29  new PlatformIdentifier()),
30  loggerFactory.CreateLogger<SqliteDatabaseContext>());
31  }
32  }
33 }
-
static IOptions< DatabaseConfiguration > GetDbContextOptions(DatabaseType databaseType, string connectionString)
Get the IOptions<TOptions> for the DatabaseConfiguration
+
static IOptions< DatabaseConfiguration > GetDatabaseConfiguration(DatabaseType databaseType, string connectionString)
Get the IOptions<TOptions> for the DatabaseConfiguration
-
DatabaseContext<TParentContext> for MySQL
+
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
Contains helpers for creating design time DatabaseContext<TParentContext>s
+ diff --git a/_swagger_configuration_8cs.html b/_swagger_configuration_8cs.html index 86a1756c2c..8233b51dd4 100644 --- a/_swagger_configuration_8cs.html +++ b/_swagger_configuration_8cs.html @@ -25,7 +25,7 @@
diff --git a/_swagger_configuration_8cs_source.html b/_swagger_configuration_8cs_source.html index ec2b7a3608..5f06d41bf0 100644 --- a/_swagger_configuration_8cs_source.html +++ b/_swagger_configuration_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,17 +94,17 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SwaggerConfiguration.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.DependencyInjection;
2 using Microsoft.Net.Http.Headers;
3 using Microsoft.OpenApi.Any;
4 using Microsoft.OpenApi.Models;
5 using Swashbuckle.AspNetCore.SwaggerGen;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 using System.Net;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Core
16 {
21  {
25  const string PasswordSecuritySchemeId = "Password_Login_Scheme";
26 
30  const string TokenSecuritySchemeId = "Token_Authorization_Scheme";
31 
32  static void AddDefaultResponses(OpenApiDocument document)
33  {
34  var errorMessageContent = new Dictionary<string, OpenApiMediaType>
35  {
36  {
38  new OpenApiMediaType
39  {
40  Schema = new OpenApiSchema
41  {
42  Reference = new OpenApiReference
43  {
44  Id = nameof(ErrorMessage),
45  Type = ReferenceType.Schema
46  }
47  }
48  }
49  }
50  };
51 
52  void AddDefaultResponse(HttpStatusCode code, OpenApiResponse concrete)
53  {
54  string responseKey = $"{(int)code}";
55 
56  document.Components.Responses.Add(responseKey, concrete);
57 
58  var referenceResponse = new OpenApiResponse
59  {
60  Reference = new OpenApiReference
61  {
62  Type = ReferenceType.Response,
63  Id = responseKey
64  }
65  };
66 
67  foreach (var operation in document.Paths.SelectMany(path => path.Value.Operations))
68  operation.Value.Responses.TryAdd(responseKey, referenceResponse);
69  }
70 
71  AddDefaultResponse(HttpStatusCode.BadRequest, new OpenApiResponse
72  {
73  Description = "A badly formatted request was made. See error message for details.",
74  Content = errorMessageContent,
75  });
76 
77  AddDefaultResponse(HttpStatusCode.Unauthorized, new OpenApiResponse
78  {
79  Description = "No/invalid token provided."
80  });
81 
82  AddDefaultResponse(HttpStatusCode.Forbidden, new OpenApiResponse
83  {
84  Description = "User lacks sufficient permissions for the operation."
85  });
86 
87  AddDefaultResponse(HttpStatusCode.Conflict, new OpenApiResponse
88  {
89  Description = "A data integrity check failed while performing the operation. See error message for details.",
90  Content = errorMessageContent
91  });
92 
93  AddDefaultResponse(HttpStatusCode.InternalServerError, new OpenApiResponse
94  {
95  Description = "The server encountered an unhandled error. See error message for details.",
96  Content = errorMessageContent
97  });
98 
99  AddDefaultResponse(HttpStatusCode.ServiceUnavailable, new OpenApiResponse
100  {
101  Description = "The server may be starting up or shutting down."
102  });
103 
104  AddDefaultResponse(HttpStatusCode.NotImplemented, new OpenApiResponse
105  {
106  Description = "This operation requires POSIX system identites to be implemented. See https://github.com/tgstation/tgstation-server/issues/709",
107  Content = errorMessageContent
108  });
109  }
110 
117  public static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath)
118  {
119  swaggerGenOptions.SwaggerDoc(
120  "v1",
121  new OpenApiInfo
122  {
123  Title = "TGS API",
124  Version = ApiHeaders.Version.Semver().ToString()
125  });
126 
127  // Important to do this before applying our own filters
128  // Otherwise we'll get NullReferenceExceptions on parameters to be setup in our document filter
129  swaggerGenOptions.IncludeXmlComments(assemblyDocumentationPath);
130  swaggerGenOptions.IncludeXmlComments(apiDocumentationPath);
131 
132  swaggerGenOptions.OperationFilter<SwaggerConfiguration>();
133  swaggerGenOptions.DocumentFilter<SwaggerConfiguration>();
134  swaggerGenOptions.SchemaFilter<SwaggerConfiguration>();
135 
136  swaggerGenOptions.AddSecurityDefinition(PasswordSecuritySchemeId, new OpenApiSecurityScheme
137  {
138  In = ParameterLocation.Header,
139  Type = SecuritySchemeType.Http,
140  Name = HeaderNames.Authorization,
142  });
143 
144  swaggerGenOptions.AddSecurityDefinition(TokenSecuritySchemeId, new OpenApiSecurityScheme
145  {
146  BearerFormat = "JWT",
147  In = ParameterLocation.Header,
148  Type = SecuritySchemeType.Http,
149  Name = HeaderNames.Authorization,
151  });
152  }
153 
155  public void Apply(OpenApiOperation operation, OperationFilterContext context)
156  {
157  if (operation == null)
158  throw new ArgumentNullException(nameof(operation));
159  if (context == null)
160  throw new ArgumentNullException(nameof(context));
161 
162  operation.OperationId = $"{context.MethodInfo.DeclaringType.Name}.{context.MethodInfo.Name}";
163 
164  var authAttributes = context
165  .MethodInfo
166  .DeclaringType
167  .GetCustomAttributes(true)
168  .Union(
169  context
170  .MethodInfo
171  .GetCustomAttributes(true))
172  .OfType<TgsAuthorizeAttribute>();
173 
174  if (authAttributes.Any())
175  {
176  var tokenScheme = new OpenApiSecurityScheme
177  {
178  Reference = new OpenApiReference
179  {
180  Type = ReferenceType.SecurityScheme,
181  Id = TokenSecuritySchemeId
182  }
183  };
184 
185  operation.Security = new List<OpenApiSecurityRequirement>
186  {
187  new OpenApiSecurityRequirement
188  {
189  {
190  tokenScheme,
191  new List<string>()
192  }
193  }
194  };
195 
196  if (authAttributes.Any(attr => attr.RightsType.HasValue && RightsHelper.IsInstanceRight(attr.RightsType.Value)))
197  operation.Parameters.Add(new OpenApiParameter
198  {
199  Reference = new OpenApiReference
200  {
201  Type = ReferenceType.Parameter,
203  }
204  });
205  }
206  else
207  {
208  // HomeController.CreateToken
209  var passwordScheme = new OpenApiSecurityScheme
210  {
211  Reference = new OpenApiReference
212  {
213  Type = ReferenceType.SecurityScheme,
214  Id = PasswordSecuritySchemeId
215  }
216  };
217 
218  operation.Security = new List<OpenApiSecurityRequirement>
219  {
220  new OpenApiSecurityRequirement
221  {
222  {
223  passwordScheme,
224  new List<string>()
225  }
226  }
227  };
228  }
229  }
230 
232  public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
233  {
234  if (swaggerDoc == null)
235  throw new ArgumentNullException(nameof(swaggerDoc));
236  if (context == null)
237  throw new ArgumentNullException(nameof(context));
238 
239  swaggerDoc.Components.Parameters.Add(ApiHeaders.InstanceIdHeader, new OpenApiParameter
240  {
241  In = ParameterLocation.Header,
242  Name = ApiHeaders.InstanceIdHeader,
243  Description = "The instance ID being accessed",
244  Required = true,
245  Style = ParameterStyle.Simple,
246  Schema = new OpenApiSchema
247  {
248  Type = "integer"
249  }
250  });
251 
252  var productHeaderSchema = new OpenApiSchema
253  {
254  Type = "string",
255  Format = "productheader"
256  };
257 
258  swaggerDoc.Components.Parameters.Add(ApiHeaders.ApiVersionHeader, new OpenApiParameter
259  {
260  In = ParameterLocation.Header,
261  Name = ApiHeaders.ApiVersionHeader,
262  Description = "The API version being used in the form \"Tgstation.Server.Api/[API version]\"",
263  Required = true,
264  Style = ParameterStyle.Simple,
265  Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}"),
266  Schema = productHeaderSchema
267  });
268 
269  swaggerDoc.Components.Parameters.Add(HeaderNames.UserAgent, new OpenApiParameter
270  {
271  In = ParameterLocation.Header,
272  Name = HeaderNames.UserAgent,
273  Description = "The user agent of the calling client.",
274  Required = true,
275  Style = ParameterStyle.Simple,
276  Example = new OpenApiString("Your-user-agent/1.0.0.0"),
277  Schema = productHeaderSchema
278  });
279 
280  string bridgeOperationPath = null;
281  foreach (var path in swaggerDoc.Paths)
282  foreach (var operation in path.Value.Operations.Select(x => x.Value))
283  {
284  if (operation.OperationId.Equals("BridgeController.Process", StringComparison.Ordinal))
285  {
286  bridgeOperationPath = path.Key;
287  continue;
288  }
289 
290  operation.Parameters.Add(new OpenApiParameter
291  {
292  Reference = new OpenApiReference
293  {
294  Type = ReferenceType.Parameter,
296  },
297  });
298 
299  operation.Parameters.Add(new OpenApiParameter
300  {
301  Reference = new OpenApiReference
302  {
303  Type = ReferenceType.Parameter,
304  Id = HeaderNames.UserAgent
305  }
306  });
307  }
308 
309  swaggerDoc.Paths.Remove(bridgeOperationPath);
310 
311  AddDefaultResponses(swaggerDoc);
312  }
313 
315  public void Apply(OpenApiSchema schema, SchemaFilterContext context)
316  {
317  if (schema == null)
318  throw new ArgumentNullException(nameof(schema));
319  if (context == null)
320  throw new ArgumentNullException(nameof(context));
321 
322  // Nothing is required
323  schema.Required.Clear();
324 
325  if (!schema.Enum?.Any() ?? false)
326  return;
327 
328  // Could be nullable type, make sure to get the right one
329  Type enumType = context.Type.IsConstructedGenericType
330  ? context.Type.GenericTypeArguments.First()
331  : context.Type;
332 
333  OpenApiEnumVarNamesExtension.Apply(schema, enumType);
334  }
335  }
336 }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
-
void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
+Go to the documentation of this file.
1 using Microsoft.Extensions.DependencyInjection;
2 using Microsoft.Net.Http.Headers;
3 using Microsoft.OpenApi.Any;
4 using Microsoft.OpenApi.Models;
5 using Swashbuckle.AspNetCore.SwaggerGen;
6 using System;
7 using System.Collections.Generic;
8 using System.Linq;
9 using System.Net;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Core
16 {
21  {
25  const string PasswordSecuritySchemeId = "Password_Login_Scheme";
26 
30  const string TokenSecuritySchemeId = "Token_Authorization_Scheme";
31 
32  static void AddDefaultResponses(OpenApiDocument document)
33  {
34  var errorMessageContent = new Dictionary<string, OpenApiMediaType>
35  {
36  {
38  new OpenApiMediaType
39  {
40  Schema = new OpenApiSchema
41  {
42  Reference = new OpenApiReference
43  {
44  Id = nameof(ErrorMessage),
45  Type = ReferenceType.Schema
46  }
47  }
48  }
49  }
50  };
51 
52  void AddDefaultResponse(HttpStatusCode code, OpenApiResponse concrete)
53  {
54  string responseKey = $"{(int)code}";
55 
56  document.Components.Responses.Add(responseKey, concrete);
57 
58  var referenceResponse = new OpenApiResponse
59  {
60  Reference = new OpenApiReference
61  {
62  Type = ReferenceType.Response,
63  Id = responseKey
64  }
65  };
66 
67  foreach (var operation in document.Paths.SelectMany(path => path.Value.Operations))
68  operation.Value.Responses.TryAdd(responseKey, referenceResponse);
69  }
70 
71  AddDefaultResponse(HttpStatusCode.BadRequest, new OpenApiResponse
72  {
73  Description = "A badly formatted request was made. See error message for details.",
74  Content = errorMessageContent,
75  });
76 
77  AddDefaultResponse(HttpStatusCode.Unauthorized, new OpenApiResponse
78  {
79  Description = "No/invalid token provided."
80  });
81 
82  AddDefaultResponse(HttpStatusCode.Forbidden, new OpenApiResponse
83  {
84  Description = "User lacks sufficient permissions for the operation."
85  });
86 
87  AddDefaultResponse(HttpStatusCode.Conflict, new OpenApiResponse
88  {
89  Description = "A data integrity check failed while performing the operation. See error message for details.",
90  Content = errorMessageContent
91  });
92 
93  AddDefaultResponse(HttpStatusCode.InternalServerError, new OpenApiResponse
94  {
95  Description = "The server encountered an unhandled error. See error message for details.",
96  Content = errorMessageContent
97  });
98 
99  AddDefaultResponse(HttpStatusCode.ServiceUnavailable, new OpenApiResponse
100  {
101  Description = "The server may be starting up or shutting down."
102  });
103 
104  AddDefaultResponse(HttpStatusCode.NotImplemented, new OpenApiResponse
105  {
106  Description = "This operation requires POSIX system identites to be implemented. See https://github.com/tgstation/tgstation-server/issues/709",
107  Content = errorMessageContent
108  });
109  }
110 
117  public static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath)
118  {
119  swaggerGenOptions.SwaggerDoc(
120  "v1",
121  new OpenApiInfo
122  {
123  Title = "TGS API",
124  Version = ApiHeaders.Version.Semver().ToString()
125  });
126 
127  // Important to do this before applying our own filters
128  // Otherwise we'll get NullReferenceExceptions on parameters to be setup in our document filter
129  swaggerGenOptions.IncludeXmlComments(assemblyDocumentationPath);
130  swaggerGenOptions.IncludeXmlComments(apiDocumentationPath);
131 
132  swaggerGenOptions.OperationFilter<SwaggerConfiguration>();
133  swaggerGenOptions.DocumentFilter<SwaggerConfiguration>();
134  swaggerGenOptions.SchemaFilter<SwaggerConfiguration>();
135 
136  swaggerGenOptions.CustomSchemaIds(type =>
137  {
138  if (type == typeof(Api.Models.Internal.User))
139  return "ShallowUser";
140 
141  return type.Name;
142  });
143 
144  swaggerGenOptions.AddSecurityDefinition(PasswordSecuritySchemeId, new OpenApiSecurityScheme
145  {
146  In = ParameterLocation.Header,
147  Type = SecuritySchemeType.Http,
148  Name = HeaderNames.Authorization,
150  });
151 
152  swaggerGenOptions.AddSecurityDefinition(TokenSecuritySchemeId, new OpenApiSecurityScheme
153  {
154  BearerFormat = "JWT",
155  In = ParameterLocation.Header,
156  Type = SecuritySchemeType.Http,
157  Name = HeaderNames.Authorization,
159  });
160  }
161 
163  public void Apply(OpenApiOperation operation, OperationFilterContext context)
164  {
165  if (operation == null)
166  throw new ArgumentNullException(nameof(operation));
167  if (context == null)
168  throw new ArgumentNullException(nameof(context));
169 
170  operation.OperationId = $"{context.MethodInfo.DeclaringType.Name}.{context.MethodInfo.Name}";
171 
172  var authAttributes = context
173  .MethodInfo
174  .DeclaringType
175  .GetCustomAttributes(true)
176  .Union(
177  context
178  .MethodInfo
179  .GetCustomAttributes(true))
180  .OfType<TgsAuthorizeAttribute>();
181 
182  if (authAttributes.Any())
183  {
184  var tokenScheme = new OpenApiSecurityScheme
185  {
186  Reference = new OpenApiReference
187  {
188  Type = ReferenceType.SecurityScheme,
189  Id = TokenSecuritySchemeId
190  }
191  };
192 
193  operation.Security = new List<OpenApiSecurityRequirement>
194  {
195  new OpenApiSecurityRequirement
196  {
197  {
198  tokenScheme,
199  new List<string>()
200  }
201  }
202  };
203 
204  if (authAttributes.Any(attr => attr.RightsType.HasValue && RightsHelper.IsInstanceRight(attr.RightsType.Value)))
205  operation.Parameters.Add(new OpenApiParameter
206  {
207  Reference = new OpenApiReference
208  {
209  Type = ReferenceType.Parameter,
211  }
212  });
213  }
214  else
215  {
216  // HomeController.CreateToken
217  var passwordScheme = new OpenApiSecurityScheme
218  {
219  Reference = new OpenApiReference
220  {
221  Type = ReferenceType.SecurityScheme,
222  Id = PasswordSecuritySchemeId
223  }
224  };
225 
226  operation.Security = new List<OpenApiSecurityRequirement>
227  {
228  new OpenApiSecurityRequirement
229  {
230  {
231  passwordScheme,
232  new List<string>()
233  }
234  }
235  };
236  }
237  }
238 
240  public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
241  {
242  if (swaggerDoc == null)
243  throw new ArgumentNullException(nameof(swaggerDoc));
244  if (context == null)
245  throw new ArgumentNullException(nameof(context));
246 
247  swaggerDoc.Components.Parameters.Add(ApiHeaders.InstanceIdHeader, new OpenApiParameter
248  {
249  In = ParameterLocation.Header,
250  Name = ApiHeaders.InstanceIdHeader,
251  Description = "The instance ID being accessed",
252  Required = true,
253  Style = ParameterStyle.Simple,
254  Schema = new OpenApiSchema
255  {
256  Type = "integer"
257  }
258  });
259 
260  var productHeaderSchema = new OpenApiSchema
261  {
262  Type = "string",
263  Format = "productheader"
264  };
265 
266  swaggerDoc.Components.Parameters.Add(ApiHeaders.ApiVersionHeader, new OpenApiParameter
267  {
268  In = ParameterLocation.Header,
269  Name = ApiHeaders.ApiVersionHeader,
270  Description = "The API version being used in the form \"Tgstation.Server.Api/[API version]\"",
271  Required = true,
272  Style = ParameterStyle.Simple,
273  Example = new OpenApiString($"Tgstation.Server.Api/{ApiHeaders.Version}"),
274  Schema = productHeaderSchema
275  });
276 
277  swaggerDoc.Components.Parameters.Add(HeaderNames.UserAgent, new OpenApiParameter
278  {
279  In = ParameterLocation.Header,
280  Name = HeaderNames.UserAgent,
281  Description = "The user agent of the calling client.",
282  Required = true,
283  Style = ParameterStyle.Simple,
284  Example = new OpenApiString("Your-user-agent/1.0.0.0"),
285  Schema = productHeaderSchema
286  });
287 
288  string bridgeOperationPath = null;
289  foreach (var path in swaggerDoc.Paths)
290  foreach (var operation in path.Value.Operations.Select(x => x.Value))
291  {
292  if (operation.OperationId.Equals("BridgeController.Process", StringComparison.Ordinal))
293  {
294  bridgeOperationPath = path.Key;
295  continue;
296  }
297 
298  operation.Parameters.Add(new OpenApiParameter
299  {
300  Reference = new OpenApiReference
301  {
302  Type = ReferenceType.Parameter,
304  },
305  });
306 
307  operation.Parameters.Add(new OpenApiParameter
308  {
309  Reference = new OpenApiReference
310  {
311  Type = ReferenceType.Parameter,
312  Id = HeaderNames.UserAgent
313  }
314  });
315  }
316 
317  swaggerDoc.Paths.Remove(bridgeOperationPath);
318 
319  AddDefaultResponses(swaggerDoc);
320  }
321 
323  public void Apply(OpenApiSchema schema, SchemaFilterContext context)
324  {
325  if (schema == null)
326  throw new ArgumentNullException(nameof(schema));
327  if (context == null)
328  throw new ArgumentNullException(nameof(context));
329 
330  // Nothing is required
331  schema.Required.Clear();
332 
333  if (!schema.Enum?.Any() ?? false)
334  return;
335 
336  // Could be nullable type, make sure to get the right one
337  Type enumType = context.Type.IsConstructedGenericType
338  ? context.Type.GenericTypeArguments.First()
339  : context.Type;
340 
341  OpenApiEnumVarNamesExtension.Apply(schema, enumType);
342  }
343  }
344 }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
+
void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
-
void Apply(OpenApiSchema schema, SchemaFilterContext context)
+
void Apply(OpenApiSchema schema, SchemaFilterContext context)
const string ApplicationJson
TODO: Remove this when we upgrade to .NET Standard 2.1
Definition: ApiHeaders.cs:22
Helper for using the AuthorizeAttribute with the Api.Rights system
-
void Apply(OpenApiOperation operation, OperationFilterContext context)
+
void Apply(OpenApiOperation operation, OperationFilterContext context)
Implements various filters for Swashbuckle.
diff --git a/_synchronous_i_o_manager_8cs.html b/_synchronous_i_o_manager_8cs.html index 4dc52567c9..13fe062b6e 100644 --- a/_synchronous_i_o_manager_8cs.html +++ b/_synchronous_i_o_manager_8cs.html @@ -25,7 +25,7 @@
diff --git a/_synchronous_i_o_manager_8cs_source.html b/_synchronous_i_o_manager_8cs_source.html index 992a695970..777263a7ac 100644 --- a/_synchronous_i_o_manager_8cs_source.html +++ b/_synchronous_i_o_manager_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_task_extensions_8cs.html b/_task_extensions_8cs.html index 019dac8ce7..0e7bd7101e 100644 --- a/_task_extensions_8cs.html +++ b/_task_extensions_8cs.html @@ -25,7 +25,7 @@ diff --git a/_task_extensions_8cs_source.html b/_task_extensions_8cs_source.html index 56c8c3098a..cf3a3deb25 100644 --- a/_task_extensions_8cs_source.html +++ b/_task_extensions_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_temporary_dmb_provider_8cs.html b/_temporary_dmb_provider_8cs.html index 0666baf1df..efdfb8f3b4 100644 --- a/_temporary_dmb_provider_8cs.html +++ b/_temporary_dmb_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_temporary_dmb_provider_8cs_source.html b/_temporary_dmb_provider_8cs_source.html index 69a85d5729..add8796bd3 100644 --- a/_temporary_dmb_provider_8cs_source.html +++ b/_temporary_dmb_provider_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_test_merge_base_8cs.html b/_test_merge_base_8cs.html index 9ad9c85d24..a0f02bb49c 100644 --- a/_test_merge_base_8cs.html +++ b/_test_merge_base_8cs.html @@ -25,7 +25,7 @@ diff --git a/_test_merge_base_8cs_source.html b/_test_merge_base_8cs_source.html index 02da24526d..541a8258c4 100644 --- a/_test_merge_base_8cs_source.html +++ b/_test_merge_base_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_test_merge_information_8cs.html b/_test_merge_information_8cs.html index 0be07f8cff..32b023a330 100644 --- a/_test_merge_information_8cs.html +++ b/_test_merge_information_8cs.html @@ -25,7 +25,7 @@ diff --git a/_test_merge_information_8cs_source.html b/_test_merge_information_8cs_source.html index 34f69fdc27..c2abec02ae 100644 --- a/_test_merge_information_8cs_source.html +++ b/_test_merge_information_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_test_merge_parameters_8cs.html b/_test_merge_parameters_8cs.html index 4919970a3e..ac04d30a49 100644 --- a/_test_merge_parameters_8cs.html +++ b/_test_merge_parameters_8cs.html @@ -25,7 +25,7 @@ diff --git a/_test_merge_parameters_8cs_source.html b/_test_merge_parameters_8cs_source.html index 40941c2646..49e3cb9f57 100644 --- a/_test_merge_parameters_8cs_source.html +++ b/_test_merge_parameters_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgs_authorize_attribute_8cs.html b/_tgs_authorize_attribute_8cs.html index c96f2261d9..546e76ef12 100644 --- a/_tgs_authorize_attribute_8cs.html +++ b/_tgs_authorize_attribute_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgs_authorize_attribute_8cs_source.html b/_tgs_authorize_attribute_8cs_source.html index fb885dc3d5..14a52ebf2d 100644 --- a/_tgs_authorize_attribute_8cs_source.html +++ b/_tgs_authorize_attribute_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_chat_bot_8cs.html b/_tgstation_8_server_8_api_2_models_2_chat_bot_8cs.html index 435d7310bf..3446d102af 100644 --- a/_tgstation_8_server_8_api_2_models_2_chat_bot_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_chat_bot_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_chat_bot_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_chat_bot_8cs_source.html index fa80ec013a..dffb793052 100644 --- a/_tgstation_8_server_8_api_2_models_2_chat_bot_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_chat_bot_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_chat_channel_8cs.html b/_tgstation_8_server_8_api_2_models_2_chat_channel_8cs.html index 543a259f43..0cf5187353 100644 --- a/_tgstation_8_server_8_api_2_models_2_chat_channel_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_chat_channel_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_chat_channel_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_chat_channel_8cs_source.html index 994269479e..117dffa941 100644 --- a/_tgstation_8_server_8_api_2_models_2_chat_channel_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_chat_channel_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_compile_job_8cs.html b/_tgstation_8_server_8_api_2_models_2_compile_job_8cs.html index 8ea782874b..83dcd88e15 100644 --- a/_tgstation_8_server_8_api_2_models_2_compile_job_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_compile_job_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_compile_job_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_compile_job_8cs_source.html index d7fbc7262a..307f7e5f84 100644 --- a/_tgstation_8_server_8_api_2_models_2_compile_job_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_compile_job_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_dream_maker_8cs.html b/_tgstation_8_server_8_api_2_models_2_dream_maker_8cs.html index aefa11b05c..bdae7bee52 100644 --- a/_tgstation_8_server_8_api_2_models_2_dream_maker_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_dream_maker_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_dream_maker_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_dream_maker_8cs_source.html index 64c9d4ad33..cd36fb6ccf 100644 --- a/_tgstation_8_server_8_api_2_models_2_dream_maker_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_dream_maker_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_instance_8cs.html b/_tgstation_8_server_8_api_2_models_2_instance_8cs.html index a8833ffb43..8b955a2cb6 100644 --- a/_tgstation_8_server_8_api_2_models_2_instance_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_instance_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_instance_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_instance_8cs_source.html index 9da8245b20..6ba0465f43 100644 --- a/_tgstation_8_server_8_api_2_models_2_instance_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_instance_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_instance_user_8cs.html b/_tgstation_8_server_8_api_2_models_2_instance_user_8cs.html index 7d3c70b169..59409db74c 100644 --- a/_tgstation_8_server_8_api_2_models_2_instance_user_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_instance_user_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_instance_user_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_instance_user_8cs_source.html index 1f8265f8d4..2d1bceb05a 100644 --- a/_tgstation_8_server_8_api_2_models_2_instance_user_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_instance_user_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_chat_bot_8cs.html b/_tgstation_8_server_8_api_2_models_2_internal_2_chat_bot_8cs.html index 59e3879581..c2d068572a 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_chat_bot_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_chat_bot_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_chat_bot_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_chat_bot_8cs_source.html index ba931fdeb7..d51e138bb2 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_chat_bot_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_chat_bot_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs.html b/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs.html index e29d7c5ee1..a0e8f3e4cb 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs_source.html index ebc576df17..7c8e6743c4 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_dream_daemon_settings_8cs.html b/_tgstation_8_server_8_api_2_models_2_internal_2_dream_daemon_settings_8cs.html index 1f2eb3bb4b..9a64f5d281 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_dream_daemon_settings_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_dream_daemon_settings_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_dream_daemon_settings_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_dream_daemon_settings_8cs_source.html index f8661dec47..82ad6bbef0 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_dream_daemon_settings_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_dream_daemon_settings_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs.html b/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs.html index 671830c01f..417e943f13 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs_source.html index 2db3001661..714a51c781 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_repository_settings_8cs.html b/_tgstation_8_server_8_api_2_models_2_internal_2_repository_settings_8cs.html index afe2488a4c..354256d680 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_repository_settings_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_repository_settings_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_repository_settings_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_repository_settings_8cs_source.html index 7b5200cea8..eb394e1910 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_repository_settings_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_repository_settings_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs.html b/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs.html index ffce146cd6..43fd1b5621 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs_source.html index ac7be82adb..e2c4f8393f 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_test_merge_8cs.html b/_tgstation_8_server_8_api_2_models_2_internal_2_test_merge_8cs.html index 3277198395..dfa2422f60 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_test_merge_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_test_merge_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_test_merge_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_test_merge_8cs_source.html index 6debaa9c8f..54779f674c 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_test_merge_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_test_merge_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_user_8cs.html b/_tgstation_8_server_8_api_2_models_2_internal_2_user_8cs.html index f3ac9a3fe8..ad1a7a84f9 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_user_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_user_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_user_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_user_8cs_source.html index 6400198b2f..2d226e11f6 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_user_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_user_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_job_8cs.html b/_tgstation_8_server_8_api_2_models_2_job_8cs.html index f0441b0fe4..f4ce9552a9 100644 --- a/_tgstation_8_server_8_api_2_models_2_job_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_job_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_job_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_job_8cs_source.html index ca1b308cc8..931f798f24 100644 --- a/_tgstation_8_server_8_api_2_models_2_job_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_job_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_repository_8cs.html b/_tgstation_8_server_8_api_2_models_2_repository_8cs.html index 03cd2d982c..885ad2c477 100644 --- a/_tgstation_8_server_8_api_2_models_2_repository_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_repository_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_repository_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_repository_8cs_source.html index 2cf4fe15ed..e822e6154e 100644 --- a/_tgstation_8_server_8_api_2_models_2_repository_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_repository_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_revision_information_8cs.html b/_tgstation_8_server_8_api_2_models_2_revision_information_8cs.html index 3556996098..e5ca6794be 100644 --- a/_tgstation_8_server_8_api_2_models_2_revision_information_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_revision_information_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_revision_information_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_revision_information_8cs_source.html index 5dce9387c0..bb708304de 100644 --- a/_tgstation_8_server_8_api_2_models_2_revision_information_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_revision_information_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RevisionInformation.cs
-Go to the documentation of this file.
1 using System.Collections.Generic;
2 
3 namespace Tgstation.Server.Api.Models
4 {
6  public sealed class RevisionInformation : Internal.RevisionInformation
7  {
11  public TestMerge? PrimaryTestMerge { get; set; }
12 
16  public ICollection<TestMerge>? ActiveTestMerges { get; set; }
17 
21  public ICollection<CompileJob>? CompileJobs { get; set; }
22  }
23 }
+Go to the documentation of this file.
1 using System.Collections.Generic;
2 
3 namespace Tgstation.Server.Api.Models
4 {
6  public sealed class RevisionInformation : Internal.RevisionInformation
7  {
11  public TestMerge? PrimaryTestMerge { get; set; }
12 
16  public ICollection<TestMerge>? ActiveTestMerges { get; set; }
17 
21  public ICollection<EntityId>? CompileJobs { get; set; }
22  }
23 }
diff --git a/_tgstation_8_server_8_api_2_models_2_test_merge_8cs.html b/_tgstation_8_server_8_api_2_models_2_test_merge_8cs.html index 68c35d131c..05d9c0ae01 100644 --- a/_tgstation_8_server_8_api_2_models_2_test_merge_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_test_merge_8cs.html @@ -25,7 +25,7 @@
diff --git a/_tgstation_8_server_8_api_2_models_2_test_merge_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_test_merge_8cs_source.html index 389489554e..93bf6f7321 100644 --- a/_tgstation_8_server_8_api_2_models_2_test_merge_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_test_merge_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_user_8cs.html b/_tgstation_8_server_8_api_2_models_2_user_8cs.html index b80bbeb784..941cb8b565 100644 --- a/_tgstation_8_server_8_api_2_models_2_user_8cs.html +++ b/_tgstation_8_server_8_api_2_models_2_user_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_api_2_models_2_user_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_user_8cs_source.html index fa9f93c017..cc5596e3cc 100644 --- a/_tgstation_8_server_8_api_2_models_2_user_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_user_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
User.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
4  public class User : Internal.User
5  {
9  public static readonly string AdminName = "Admin";
10 
14  public static readonly string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
15 
19  public User? CreatedBy { get; set; }
20  }
21 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
4  public class User : Internal.User
5  {
9  public static readonly string AdminName = "Admin";
10 
14  public static readonly string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
15 
19  public Internal.User? CreatedBy { get; set; }
20  }
21 }
diff --git a/_tgstation_8_server_8_client_2_assembly_info_8cs.html b/_tgstation_8_server_8_client_2_assembly_info_8cs.html index c1c0d8b940..ceecad93d1 100644 --- a/_tgstation_8_server_8_client_2_assembly_info_8cs.html +++ b/_tgstation_8_server_8_client_2_assembly_info_8cs.html @@ -25,7 +25,7 @@
diff --git a/_tgstation_8_server_8_client_2_assembly_info_8cs_source.html b/_tgstation_8_server_8_client_2_assembly_info_8cs_source.html index 9d0b557b96..c11d32e4ce 100644 --- a/_tgstation_8_server_8_client_2_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_client_2_assembly_info_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs.html b/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs.html index 8951ba316b..035e03ff8d 100644 --- a/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs.html +++ b/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs_source.html b/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs_source.html index 405f2054a1..0bca3d6bd0 100644 --- a/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,91 +94,87 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamMaker.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Octokit;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
20 using Tgstation.Server.Host.IO;
24 
25 namespace Tgstation.Server.Host.Components.Deployment
26 {
28  sealed class DreamMaker : IDreamMaker
29  {
33  public const string ADirectoryName = "A";
34 
38  public const string BDirectoryName = "B";
39 
43  public const string DmbExtension = ".dmb";
44 
48  const string DmeExtension = "dme";
49 
53  readonly IByondManager byond;
54 
59 
63  readonly StaticFiles.IConfiguration configuration;
64 
69 
74 
79 
84 
88  readonly IWatchdog watchdog;
89 
94 
99 
104 
108  readonly ILogger<DreamMaker> logger;
109 
113  readonly Api.Models.Instance metadata;
114 
118  readonly object compilingLock;
119 
123  bool compiling;
124 
141  public DreamMaker(
142  IByondManager byond,
143  IIOManager ioManager,
144  StaticFiles.IConfiguration configuration,
145  ISessionControllerFactory sessionControllerFactory,
146  IEventConsumer eventConsumer,
147  IChatManager chatManager,
148  IProcessExecutor processExecutor,
149  IWatchdog watchdog,
150  IGitHubClientFactory gitHubClientFactory,
151  ICompileJobSink compileJobConsumer,
152  IRepositoryManager repositoryManager,
153  ILogger<DreamMaker> logger,
154  Api.Models.Instance metadata)
155  {
156  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
157  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
158  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
159  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
160  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
161  this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager));
162  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
163  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
164  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
165  this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
166  this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
167  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
168  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
169 
170  compilingLock = new object();
171  }
172 
180  async Task ProgressTask(Action<int> progressReporter, TimeSpan estimatedDuration, CancellationToken cancellationToken)
181  {
182  progressReporter(0);
183  var sleepInterval = estimatedDuration / 100;
184 
185  logger.LogDebug("Compile is expected to take: {0}", estimatedDuration);
186  try
187  {
188  for (var I = 0; I < 99; ++I)
189  {
190  await Task.Delay(sleepInterval, cancellationToken).ConfigureAwait(false);
191  progressReporter(I + 1);
192  }
193  }
194  catch (OperationCanceledException) { }
195  }
196 
207  async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
208  {
209  logger.LogTrace("Verifying DMAPI...");
210  var launchParameters = new DreamDaemonLaunchParameters
211  {
212  AllowWebClient = false,
213  PrimaryPort = portToUse,
214  SecurityLevel = securityLevel,
215  StartupTimeout = timeout
216  };
217 
218  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
219 
220  job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
221  var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);
222 
223  using var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job);
224  using var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, true, true, cancellationToken).ConfigureAwait(false);
225  var launchResult = await controller.LaunchResult.ConfigureAwait(false);
226 
227  var now = DateTimeOffset.Now;
228  if (now < timeoutAt && launchResult.StartupTime.HasValue)
229  {
230  var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken);
231 
232  await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);
233  cancellationToken.ThrowIfCancellationRequested();
234  }
235 
236  if (controller.Lifetime.IsCompleted)
237  {
238  var validationStatus = controller.ApiValidationStatus;
239  logger.LogTrace("API validation status: {0}", validationStatus);
240 
241  job.DMApiVersion = controller.DMApiVersion;
242  switch (validationStatus)
243  {
244  case ApiValidationStatus.RequiresUltrasafe:
245  job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
246  return;
247  case ApiValidationStatus.RequiresSafe:
248  job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
249  return;
250  case ApiValidationStatus.RequiresTrusted:
251  job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
252  return;
253  case ApiValidationStatus.NeverValidated:
254  throw new JobException(ErrorCode.DreamMakerNeverValidated);
255  case ApiValidationStatus.BadValidationRequest:
256  throw new JobException(ErrorCode.DreamMakerInvalidValidation);
257  case ApiValidationStatus.UnaskedValidationRequest:
258  default:
259  throw new InvalidOperationException(
260  $"Session controller returned unexpected ApiValidationStatus: {validationStatus}");
261  }
262  }
263 
264  throw new JobException(ErrorCode.DreamMakerValidationTimeout);
265  }
266 
274  async Task<int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
275  {
276  using var dm = processExecutor.LaunchProcess(
277  dreamMakerPath,
278  ioManager.ResolvePath(
279  ioManager.ConcatPath(
280  job.DirectoryName.ToString(),
281  ADirectoryName)),
282  $"-clean {job.DmeName}.{DmeExtension}",
283  true,
284  true,
285  true);
286  int exitCode;
287  using (cancellationToken.Register(() => dm.Terminate()))
288  exitCode = await dm.Lifetime.ConfigureAwait(false);
289  cancellationToken.ThrowIfCancellationRequested();
290 
291  logger.LogDebug("DreamMaker exit code: {0}", exitCode);
292  job.Output = dm.GetCombinedOutput();
293  logger.LogDebug("DreamMaker output: {0}{1}", Environment.NewLine, job.Output);
294  return exitCode;
295  }
296 
303  async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
304  {
305  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
306  var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
307  var dmePath = ioManager.ConcatPath(dirA, dmeFileName);
308  var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
309 
310  var dmeModificationsTask = configuration.CopyDMFilesTo(dmeFileName, ioManager.ResolvePath(dirA), cancellationToken);
311 
312  var dmeBytes = await dmeReadTask.ConfigureAwait(false);
313  var dme = Encoding.UTF8.GetString(dmeBytes);
314 
315  var dmeModifications = await dmeModificationsTask.ConfigureAwait(false);
316 
317  if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
318  {
319  if (dmeModifications != null)
320  logger.LogDebug(".dme replacement configured!");
321  else
322  logger.LogTrace("No .dme modifications required.");
323  return;
324  }
325 
326  if (dmeModifications.HeadIncludeLine != null)
327  logger.LogDebug("Head .dme include line: {0}", dmeModifications.HeadIncludeLine);
328  if (dmeModifications.TailIncludeLine != null)
329  logger.LogDebug("Tail .dme include line: {0}", dmeModifications.TailIncludeLine);
330 
331  var dmeLines = new List<string>(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None));
332  for (var I = 0; I < dmeLines.Count; ++I)
333  {
334  var line = dmeLines[I];
335  if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
336  {
337  dmeLines.Insert(I + 1, dmeModifications.HeadIncludeLine);
338  ++I;
339  }
340  else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
341  {
342  dmeLines.Insert(I, dmeModifications.TailIncludeLine);
343  break;
344  }
345  }
346 
347  dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines));
348  await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false);
349  }
350 
358  async Task CleanupFailedCompile(Models.CompileJob job, bool cancelled, CancellationToken cancellationToken)
359  {
360  logger.LogTrace("Cleaning compile directory...");
361  var chatTask = chatManager.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);
362  var jobPath = job.DirectoryName.ToString();
363  try
364  {
365  await ioManager.DeleteDirectory(jobPath, CancellationToken.None).ConfigureAwait(false);
366  }
367  catch (Exception e)
368  {
369  logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(jobPath), e);
370  }
371 
372  await chatTask.ConfigureAwait(false);
373  }
374 
382  async Task SendDeploymentMessage(Models.RevisionInformation revisionInformation, IByondExecutableLock byondLock, CancellationToken cancellationToken)
383  {
384  var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
385  string remoteCommitInsert;
386  if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
387  {
388  commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
389  remoteCommitInsert = String.Empty;
390  }
391  else
392  remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
393 
394  var testmergeInsert = (revisionInformation.ActiveTestMerges?.Count ?? 0) == 0
395  ? String.Empty
396  : String.Format(
397  CultureInfo.InvariantCulture,
398  "{0}Test Merges:{1}",
399  Environment.NewLine,
400  String.Join(
401  Environment.NewLine,
402  revisionInformation
403  .ActiveTestMerges
404  .Select(x => x.TestMerge)
405  .Select(x =>
406  {
407  var result = String.Format(
408  CultureInfo.InvariantCulture,
409  "- #{0} at {1}",
410  x.Number,
411  x.PullRequestRevision.Substring(0, 7));
412 
413  if (x.Comment != null)
414  result += $": {x.Comment}";
415 
416  return result;
417  })));
418 
419  await chatManager.SendUpdateMessage(
420  String.Format(
421  CultureInfo.InvariantCulture,
422  "*Deployment Triggered*{0}Revision: {1}{2}{3}{0}BYOND Version: {4}.{5}",
423  Environment.NewLine,
424  commitInsert,
425  testmergeInsert,
426  remoteCommitInsert,
427  byondLock.Version.Major,
428  byondLock.Version.Minor),
429  cancellationToken)
430  .ConfigureAwait(false);
431  }
432 
443  async Task RunCompileJob(Models.CompileJob job, Api.Models.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken)
444  {
445  var jobPath = job.DirectoryName.ToString();
446  logger.LogTrace("Compile output GUID: {0}", jobPath);
447 
448  try
449  {
450  var dirA = ioManager.ConcatPath(jobPath, ADirectoryName);
451  var dirB = ioManager.ConcatPath(jobPath, BDirectoryName);
452 
453  // copy the repository
454  logger.LogTrace("Copying repository to game directory...");
455  var resolvedADirectory = ioManager.ResolvePath(dirA);
456  var repoOrigin = repository.Origin;
457  using (repository)
458  await repository.CopyTo(resolvedADirectory, cancellationToken).ConfigureAwait(false);
459 
460  // repository closed now
461 
462  // run precompile scripts
463  await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedADirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
464 
465  // determine the dme
466  if (job.DmeName == null)
467  {
468  logger.LogTrace("Searching for available .dmes...");
469  var foundPaths = await ioManager.GetFilesWithExtension(dirA, DmeExtension, true, cancellationToken).ConfigureAwait(false);
470  var foundPath = foundPaths.FirstOrDefault();
471  if (foundPath == default)
472  throw new JobException(ErrorCode.DreamMakerNoDme);
473  var dmeWithExtension = ioManager.GetFileName(foundPath);
474  job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
475  }
476  else
477  {
478  var targetDme = ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension));
479  var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false);
480  if (!targetDmeExists)
481  throw new JobException(ErrorCode.DreamMakerMissingDme);
482  }
483 
484  logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);
485 
486  await ModifyDme(job, cancellationToken).ConfigureAwait(false);
487 
488  // run compiler
489  var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
490 
491  // verify api
492  try
493  {
494  if (exitCode != 0)
495  throw new JobException(
496  ErrorCode.DreamMakerExitCode,
497  new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}"));
498 
499  await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
500  }
501  catch (JobException)
502  {
503  // DD never validated or compile failed
504  await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedADirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
505  throw;
506  }
507 
508  logger.LogTrace("Running post compile event...");
509  await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { resolvedADirectory }, cancellationToken).ConfigureAwait(false);
510 
511  logger.LogTrace("Duplicating compiled game...");
512 
513  // duplicate the dmb et al
514  await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
515 
516  logger.LogTrace("Applying static game file symlinks...");
517 
518  // symlink in the static data
519  var symATask = configuration.SymlinkStaticFilesTo(resolvedADirectory, cancellationToken);
520  var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
521 
522  await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
523 
524  logger.LogDebug("Compile complete!");
525  }
526  catch (Exception e)
527  {
528  await CleanupFailedCompile(job, e is OperationCanceledException, cancellationToken).ConfigureAwait(false);
529  throw;
530  }
531  }
532 
534  #pragma warning disable CA1506
535  public async Task DeploymentProcess(
536  Models.Job job,
537  IDatabaseContextFactory databaseContextFactory,
538  Action<int> progressReporter,
539  CancellationToken cancellationToken)
540  {
541 #pragma warning disable IDE0016 // Use 'throw' expression
542  if (job == null)
543  throw new ArgumentNullException(nameof(job));
544 #pragma warning restore IDE0016 // Use 'throw' expression
545  if (databaseContextFactory == null)
546  throw new ArgumentNullException(nameof(databaseContextFactory));
547  if (progressReporter == null)
548  throw new ArgumentNullException(nameof(progressReporter));
549 
550  string repoOwner = null;
551  string repoName = null;
552  TimeSpan? averageSpan = null;
553  Models.RepositorySettings repositorySettings = null;
554  Models.DreamDaemonSettings ddSettings = null;
555  DreamMakerSettings dreamMakerSettings = null;
556  IRepository repo = null;
557  Models.CompileJob compileJob = null;
558  Models.RevisionInformation revInfo = null;
559  await databaseContextFactory.UseContext(
560  async databaseContext =>
561  {
562  averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false);
563 
564  ddSettings = await databaseContext
565  .DreamDaemonSettings
566  .Where(x => x.InstanceId == metadata.Id)
567  .Select(x => new Models.DreamDaemonSettings
568  {
569  StartupTimeout = x.StartupTimeout,
570  })
571  .FirstOrDefaultAsync(cancellationToken)
572  .ConfigureAwait(false);
573  if (ddSettings == default)
574  throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
575 
576  dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
577  if (dreamMakerSettings == default)
578  throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
579 
580  repositorySettings = await databaseContext
581  .RepositorySettings
582  .Where(x => x.InstanceId == metadata.Id)
583  .Select(x => new Models.RepositorySettings
584  {
585  AccessToken = x.AccessToken,
586  ShowTestMergeCommitters = x.ShowTestMergeCommitters,
587  PushTestMergeCommits = x.PushTestMergeCommits,
588  PostTestMergeComment = x.PostTestMergeComment
589  })
590  .FirstOrDefaultAsync(cancellationToken)
591  .ConfigureAwait(false);
592  if (repositorySettings == default)
593  throw new JobException(ErrorCode.InstanceMissingRepositorySettings);
594 
595  repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false);
596  try
597  {
598  if (repo == null)
599  throw new JobException(ErrorCode.RepoMissing);
600 
601  if (repo.IsGitHubRepository)
602  {
603  repoOwner = repo.GitHubOwner;
604  repoName = repo.GitHubRepoName;
605  }
606 
607  var repoSha = repo.Head;
608  revInfo = await databaseContext
609  .RevisionInformations
610  .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id)
611  .Include(x => x.ActiveTestMerges)
612  .ThenInclude(x => x.TestMerge)
613  .ThenInclude(x => x.MergedBy)
614  .FirstOrDefaultAsync(cancellationToken)
615  .ConfigureAwait(false);
616 
617  if (revInfo == default)
618  {
619  revInfo = new Models.RevisionInformation
620  {
621  CommitSha = repoSha,
622  OriginCommitSha = repoSha,
623  Instance = new Models.Instance
624  {
625  Id = metadata.Id
626  },
627  ActiveTestMerges = new List<RevInfoTestMerge>()
628  };
629 
630  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
631  databaseContext.Instances.Attach(revInfo.Instance);
632  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
633  }
634  }
635  catch
636  {
637  repo.Dispose();
638  throw;
639  }
640  })
641  .ConfigureAwait(false);
642 
643  using (repo)
644  compileJob = await Compile(
645  revInfo,
646  dreamMakerSettings,
647  ddSettings.StartupTimeout.Value,
648  repo,
649  progressReporter,
650  averageSpan,
651  cancellationToken)
652  .ConfigureAwait(false);
653 
654  var activeCompileJob = compileJobConsumer.LatestCompileJob();
655  try
656  {
657  await databaseContextFactory.UseContext(
658  async databaseContext =>
659  {
660  compileJob.Job = new Models.Job
661  {
662  Id = job.Id
663  };
664  compileJob.RevisionInformation = new Models.RevisionInformation
665  {
666  Id = revInfo.Id
667  };
668 
669  databaseContext.Jobs.Attach(compileJob.Job);
670  databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
671  databaseContext.CompileJobs.Add(compileJob);
672 
673  // The difficulty with compile jobs is they have a two part commit
674  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
675  try
676  {
677  await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
678  }
679  catch
680  {
681  // So we need to un-commit the compile job if the above throws
682  databaseContext.CompileJobs.Remove(compileJob);
683  await databaseContext.Save(default).ConfigureAwait(false);
684  throw;
685  }
686  })
687  .ConfigureAwait(false);
688  }
689  catch (Exception ex)
690  {
691  await CleanupFailedCompile(compileJob, ex is OperationCanceledException, default).ConfigureAwait(false);
692  throw;
693  }
694 
695  var commentsTask = PostDeploymentComments(
696  revInfo,
697  activeCompileJob?.RevisionInformation,
698  repositorySettings,
699  repoOwner,
700  repoName);
701 
702  var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken);
703 
704  var chatTask = chatManager.SendUpdateMessage(
705  String.Format(
706  CultureInfo.InvariantCulture,
707  "Deployment complete! Changes will be applied when DreamDaemon {0}.",
708  watchdog.Running ? "reboots" : "is launched"),
709  cancellationToken);
710 
711  try
712  {
713  await Task.WhenAll(commentsTask, eventTask, chatTask).ConfigureAwait(false);
714  }
715  catch (Exception ex)
716  {
717  throw new JobException(ErrorCode.PostDeployFailure, ex);
718  }
719  }
720  #pragma warning restore CA1506
721 
728  async Task<TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
729  {
730  var previousCompileJobs = await databaseContext.CompileJobs
731  .Where(x => x.Job.Instance.Id == metadata.Id)
732  .OrderByDescending(x => x.Job.StoppedAt)
733  .Take(10)
734  .Select(x => new Models.Job
735  {
736  StoppedAt = x.Job.StoppedAt,
737  StartedAt = x.Job.StartedAt
738  })
739  .ToListAsync(cancellationToken)
740  .ConfigureAwait(false);
741 
742  TimeSpan? averageSpan = null;
743  if (previousCompileJobs.Count != 0)
744  {
745  var totalSpan = TimeSpan.Zero;
746  foreach (var I in previousCompileJobs)
747  totalSpan += I.StoppedAt.Value - I.StartedAt.Value;
748  averageSpan = totalSpan / previousCompileJobs.Count;
749  }
750 
751  return averageSpan;
752  }
753 
754  async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
755  {
756  logger.LogTrace("Begin Compile");
757 
758  lock (compilingLock)
759  {
760  if (compiling)
761  throw new JobException(ErrorCode.DreamMakerCompileJobInProgress);
762  compiling = true;
763  }
764 
765  using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
766  var progressTask = estimatedDuration.HasValue ? ProgressTask(progressReporter, estimatedDuration.Value, progressCts.Token) : Task.CompletedTask;
767  try
768  {
769  using var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false);
770  await SendDeploymentMessage(revisionInformation, byondLock, cancellationToken).ConfigureAwait(false);
771 
772  var job = new Models.CompileJob
773  {
774  DirectoryName = Guid.NewGuid(),
775  DmeName = dreamMakerSettings.ProjectName,
776  RevisionInformation = revisionInformation,
777  ByondVersion = byondLock.Version.ToString()
778  };
779 
780  await RunCompileJob(job, dreamMakerSettings, byondLock, repository, apiValidateTimeout, cancellationToken).ConfigureAwait(false);
781 
782  return job;
783  }
784  catch (OperationCanceledException)
785  {
786  await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);
787  throw;
788  }
789  finally
790  {
791  compiling = false;
792  progressCts.Cancel();
793  await progressTask.ConfigureAwait(false);
794  }
795  }
796 
807  Models.RevisionInformation deployedRevisionInformation,
808  Models.RevisionInformation previousRevisionInformation,
809  Models.RepositorySettings repositorySettings,
810  string repoOwner,
811  string repoName)
812  {
813  if (repositorySettings?.AccessToken == null)
814  return;
815 
816  if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == previousRevisionInformation.CommitSha)
817  || !repositorySettings.PostTestMergeComment.Value)
818  return;
819 
820  previousRevisionInformation = new Models.RevisionInformation
821  {
822  ActiveTestMerges = new List<RevInfoTestMerge>()
823  };
824 
825  var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken);
826 
827  async Task CommentOnPR(int prNumber, string comment)
828  {
829  try
830  {
831  await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
832  }
833  catch (ApiException e)
834  {
835  logger.LogWarning("Error posting GitHub comment! Exception: {0}", e);
836  }
837  }
838 
839  var tasks = new List<Task>();
840 
841  string FormatTestMerge(Models.TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}",
842  Environment.NewLine,
843  repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty,
844  testMerge.PullRequestRevision,
845  testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty,
846  updated ? "Updated" : "Deployed",
847  metadata.Name,
848  deployedRevisionInformation.OriginCommitSha,
849  deployedRevisionInformation.CommitSha);
850 
851  // added prs
852  foreach (var I in deployedRevisionInformation
853  .ActiveTestMerges
854  .Select(x => x.TestMerge)
855  .Where(x => !previousRevisionInformation
856  .ActiveTestMerges
857  .Any(y => y.TestMerge.Number == x.Number)))
858  tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, false)));
859 
860  // removed prs
861  foreach (var I in previousRevisionInformation
862  .ActiveTestMerges
863  .Select(x => x.TestMerge)
864  .Where(x => !deployedRevisionInformation
865  .ActiveTestMerges
866  .Any(y => y.TestMerge.Number == x.Number)))
867  tasks.Add(CommentOnPR(I.Number, "#### Test Merge Removed"));
868 
869  // updated prs
870  foreach (var I in deployedRevisionInformation
871  .ActiveTestMerges
872  .Select(x => x.TestMerge)
873  .Where(x => previousRevisionInformation
874  .ActiveTestMerges
875  .Any(y => y.TestMerge.Number == x.Number)))
876  tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, true)));
877 
878  if (tasks.Any())
879  await Task.WhenAll(tasks).ConfigureAwait(false);
880  }
881  }
882 }
readonly IEventConsumer eventConsumer
The IEventConsumer for DreamMaker
Definition: DreamMaker.cs:73
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Octokit;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
19 using Tgstation.Server.Host.IO;
23 
24 namespace Tgstation.Server.Host.Components.Deployment
25 {
27  sealed class DreamMaker : IDreamMaker
28  {
32  public const string ADirectoryName = "A";
33 
37  public const string BDirectoryName = "B";
38 
42  public const string DmbExtension = ".dmb";
43 
47  const string DmeExtension = "dme";
48 
52  readonly IByondManager byond;
53 
58 
62  readonly StaticFiles.IConfiguration configuration;
63 
68 
73 
78 
83 
88 
93 
98 
102  readonly ILogger<DreamMaker> logger;
103 
107  readonly Api.Models.Instance metadata;
108 
112  readonly object deploymentLock;
113 
114  Func<string, string, Task> currentChatCallback;
115 
117 
121  bool deploying;
122 
138  public DreamMaker(
139  IByondManager byond,
140  IIOManager ioManager,
141  StaticFiles.IConfiguration configuration,
142  ISessionControllerFactory sessionControllerFactory,
143  IEventConsumer eventConsumer,
144  IChatManager chatManager,
145  IProcessExecutor processExecutor,
146  IGitHubClientFactory gitHubClientFactory,
147  ICompileJobSink compileJobConsumer,
148  IRepositoryManager repositoryManager,
149  ILogger<DreamMaker> logger,
150  Api.Models.Instance metadata)
151  {
152  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
153  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
154  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
155  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
156  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
157  this.chatManager = chatManager ?? throw new ArgumentNullException(nameof(chatManager));
158  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
159  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
160  this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
161  this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
162  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
163  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
164 
165  deploymentLock = new object();
166  }
167 
175  async Task ProgressTask(Action<int> progressReporter, TimeSpan estimatedDuration, CancellationToken cancellationToken)
176  {
177  progressReporter(0);
178  var sleepInterval = estimatedDuration / 100;
179 
180  logger.LogDebug("Compile is expected to take: {0}", estimatedDuration);
181  try
182  {
183  for (var I = 0; I < 99; ++I)
184  {
185  await Task.Delay(sleepInterval, cancellationToken).ConfigureAwait(false);
186  progressReporter(I + 1);
187  }
188  }
189  catch (OperationCanceledException) { }
190  }
191 
202  async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
203  {
204  logger.LogTrace("Verifying DMAPI...");
205  var launchParameters = new DreamDaemonLaunchParameters
206  {
207  AllowWebClient = false,
208  PrimaryPort = portToUse,
209  SecurityLevel = securityLevel,
210  StartupTimeout = timeout
211  };
212 
213  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
214 
215  job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
216  var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);
217 
218  using var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job);
219  using var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, true, true, cancellationToken).ConfigureAwait(false);
220  var launchResult = await controller.LaunchResult.ConfigureAwait(false);
221 
222  var now = DateTimeOffset.Now;
223  if (now < timeoutAt && launchResult.StartupTime.HasValue)
224  {
225  var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken);
226 
227  await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);
228  cancellationToken.ThrowIfCancellationRequested();
229  }
230 
231  if (controller.Lifetime.IsCompleted)
232  {
233  var validationStatus = controller.ApiValidationStatus;
234  logger.LogTrace("API validation status: {0}", validationStatus);
235 
236  job.DMApiVersion = controller.DMApiVersion;
237  switch (validationStatus)
238  {
239  case ApiValidationStatus.RequiresUltrasafe:
240  job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
241  return;
242  case ApiValidationStatus.RequiresSafe:
243  job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
244  return;
245  case ApiValidationStatus.RequiresTrusted:
246  job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
247  return;
248  case ApiValidationStatus.NeverValidated:
249  throw new JobException(ErrorCode.DreamMakerNeverValidated);
250  case ApiValidationStatus.BadValidationRequest:
251  throw new JobException(ErrorCode.DreamMakerInvalidValidation);
252  case ApiValidationStatus.UnaskedValidationRequest:
253  default:
254  throw new InvalidOperationException(
255  $"Session controller returned unexpected ApiValidationStatus: {validationStatus}");
256  }
257  }
258 
259  throw new JobException(ErrorCode.DreamMakerValidationTimeout);
260  }
261 
269  async Task<int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
270  {
271  using var dm = processExecutor.LaunchProcess(
272  dreamMakerPath,
273  ioManager.ResolvePath(
274  ioManager.ConcatPath(
275  job.DirectoryName.ToString(),
276  ADirectoryName)),
277  $"-clean {job.DmeName}.{DmeExtension}",
278  true,
279  true,
280  true);
281  int exitCode;
282  using (cancellationToken.Register(() => dm.Terminate()))
283  exitCode = await dm.Lifetime.ConfigureAwait(false);
284  cancellationToken.ThrowIfCancellationRequested();
285 
286  logger.LogDebug("DreamMaker exit code: {0}", exitCode);
287  currentDreamMakerOutput = job.Output = dm.GetCombinedOutput();
288  logger.LogDebug("DreamMaker output: {0}{1}", Environment.NewLine, job.Output);
289  return exitCode;
290  }
291 
298  async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
299  {
300  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
301  var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
302  var dmePath = ioManager.ConcatPath(dirA, dmeFileName);
303  var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
304 
305  var dmeModificationsTask = configuration.CopyDMFilesTo(dmeFileName, ioManager.ResolvePath(dirA), cancellationToken);
306 
307  var dmeBytes = await dmeReadTask.ConfigureAwait(false);
308  var dme = Encoding.UTF8.GetString(dmeBytes);
309 
310  var dmeModifications = await dmeModificationsTask.ConfigureAwait(false);
311 
312  if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
313  {
314  if (dmeModifications != null)
315  logger.LogDebug(".dme replacement configured!");
316  else
317  logger.LogTrace("No .dme modifications required.");
318  return;
319  }
320 
321  if (dmeModifications.HeadIncludeLine != null)
322  logger.LogDebug("Head .dme include line: {0}", dmeModifications.HeadIncludeLine);
323  if (dmeModifications.TailIncludeLine != null)
324  logger.LogDebug("Tail .dme include line: {0}", dmeModifications.TailIncludeLine);
325 
326  var dmeLines = new List<string>(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None));
327  for (var I = 0; I < dmeLines.Count; ++I)
328  {
329  var line = dmeLines[I];
330  if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
331  {
332  dmeLines.Insert(I + 1, dmeModifications.HeadIncludeLine);
333  ++I;
334  }
335  else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
336  {
337  dmeLines.Insert(I, dmeModifications.TailIncludeLine);
338  break;
339  }
340  }
341 
342  dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines));
343  await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false);
344  }
345 
351  async Task CleanupFailedCompile(Models.CompileJob job)
352  {
353  logger.LogTrace("Cleaning compile directory...");
354  var jobPath = job.DirectoryName.ToString();
355  try
356  {
357  await ioManager.DeleteDirectory(jobPath, CancellationToken.None).ConfigureAwait(false);
358  }
359  catch (Exception e)
360  {
361  logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(jobPath), e);
362  }
363  }
364 
375  async Task RunCompileJob(Models.CompileJob job, Api.Models.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken)
376  {
377  var jobPath = job.DirectoryName.ToString();
378  logger.LogTrace("Compile output GUID: {0}", jobPath);
379 
380  try
381  {
382  var dirA = ioManager.ConcatPath(jobPath, ADirectoryName);
383  var dirB = ioManager.ConcatPath(jobPath, BDirectoryName);
384 
385  // copy the repository
386  logger.LogTrace("Copying repository to game directory...");
387  var resolvedADirectory = ioManager.ResolvePath(dirA);
388  var repoOrigin = repository.Origin;
389  using (repository)
390  await repository.CopyTo(resolvedADirectory, cancellationToken).ConfigureAwait(false);
391 
392  // repository closed now
393 
394  // run precompile scripts
395  await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedADirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
396 
397  // determine the dme
398  if (job.DmeName == null)
399  {
400  logger.LogTrace("Searching for available .dmes...");
401  var foundPaths = await ioManager.GetFilesWithExtension(dirA, DmeExtension, true, cancellationToken).ConfigureAwait(false);
402  var foundPath = foundPaths.FirstOrDefault();
403  if (foundPath == default)
404  throw new JobException(ErrorCode.DreamMakerNoDme);
405  var dmeWithExtension = ioManager.GetFileName(foundPath);
406  job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
407  }
408  else
409  {
410  var targetDme = ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension));
411  var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false);
412  if (!targetDmeExists)
413  throw new JobException(ErrorCode.DreamMakerMissingDme);
414  }
415 
416  logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);
417 
418  await ModifyDme(job, cancellationToken).ConfigureAwait(false);
419 
420  // run compiler
421  var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
422 
423  // verify api
424  try
425  {
426  if (exitCode != 0)
427  throw new JobException(
428  ErrorCode.DreamMakerExitCode,
429  new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}"));
430 
431  await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
432  }
433  catch (JobException)
434  {
435  // DD never validated or compile failed
436  await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedADirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
437  throw;
438  }
439 
440  logger.LogTrace("Running post compile event...");
441  await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { resolvedADirectory }, cancellationToken).ConfigureAwait(false);
442 
443  logger.LogTrace("Duplicating compiled game...");
444 
445  // duplicate the dmb et al
446  await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
447 
448  logger.LogTrace("Applying static game file symlinks...");
449 
450  // symlink in the static data
451  var symATask = configuration.SymlinkStaticFilesTo(resolvedADirectory, cancellationToken);
452  var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
453 
454  await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
455 
456  logger.LogDebug("Compile complete!");
457  }
458  catch
459  {
460  await CleanupFailedCompile(job).ConfigureAwait(false);
461  throw;
462  }
463  }
464 
466  #pragma warning disable CA1506
467  public async Task DeploymentProcess(
468  Models.Job job,
469  IDatabaseContextFactory databaseContextFactory,
470  Action<int> progressReporter,
471  CancellationToken cancellationToken)
472  {
473 #pragma warning disable IDE0016 // Use 'throw' expression
474  if (job == null)
475  throw new ArgumentNullException(nameof(job));
476 #pragma warning restore IDE0016 // Use 'throw' expression
477  if (databaseContextFactory == null)
478  throw new ArgumentNullException(nameof(databaseContextFactory));
479  if (progressReporter == null)
480  throw new ArgumentNullException(nameof(progressReporter));
481 
482  lock (deploymentLock)
483  {
484  if (deploying)
485  throw new JobException(ErrorCode.DreamMakerCompileJobInProgress);
486  deploying = true;
487  }
488 
489  currentChatCallback = null;
490  currentDreamMakerOutput = null;
491  Models.CompileJob compileJob = null;
492  try
493  {
494  string repoOwner = null;
495  string repoName = null;
496  TimeSpan? averageSpan = null;
497  Models.RepositorySettings repositorySettings = null;
498  Models.DreamDaemonSettings ddSettings = null;
499  DreamMakerSettings dreamMakerSettings = null;
500  IRepository repo = null;
501  Models.RevisionInformation revInfo = null;
502  await databaseContextFactory.UseContext(
503  async databaseContext =>
504  {
505  averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false);
506 
507  ddSettings = await databaseContext
508  .DreamDaemonSettings
509  .AsQueryable()
510  .Where(x => x.InstanceId == metadata.Id)
511  .Select(x => new Models.DreamDaemonSettings
512  {
513  StartupTimeout = x.StartupTimeout,
514  })
515  .FirstOrDefaultAsync(cancellationToken)
516  .ConfigureAwait(false);
517  if (ddSettings == default)
518  throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
519 
520  dreamMakerSettings = await databaseContext
521  .DreamMakerSettings
522  .AsQueryable()
523  .Where(x => x.InstanceId == metadata.Id)
524  .FirstAsync(cancellationToken)
525  .ConfigureAwait(false);
526  if (dreamMakerSettings == default)
527  throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
528 
529  repositorySettings = await databaseContext
530  .RepositorySettings
531  .AsQueryable()
532  .Where(x => x.InstanceId == metadata.Id)
533  .Select(x => new Models.RepositorySettings
534  {
535  AccessToken = x.AccessToken,
536  ShowTestMergeCommitters = x.ShowTestMergeCommitters,
537  PushTestMergeCommits = x.PushTestMergeCommits,
538  PostTestMergeComment = x.PostTestMergeComment
539  })
540  .FirstOrDefaultAsync(cancellationToken)
541  .ConfigureAwait(false);
542  if (repositorySettings == default)
543  throw new JobException(ErrorCode.InstanceMissingRepositorySettings);
544 
545  repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false);
546  try
547  {
548  if (repo == null)
549  throw new JobException(ErrorCode.RepoMissing);
550 
551  if (repo.IsGitHubRepository)
552  {
553  repoOwner = repo.GitHubOwner;
554  repoName = repo.GitHubRepoName;
555  }
556 
557  var repoSha = repo.Head;
558  revInfo = await databaseContext
559  .RevisionInformations
560  .AsQueryable()
561  .Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id)
562  .Include(x => x.ActiveTestMerges)
563  .ThenInclude(x => x.TestMerge)
564  .ThenInclude(x => x.MergedBy)
565  .FirstOrDefaultAsync(cancellationToken)
566  .ConfigureAwait(false);
567 
568  if (revInfo == default)
569  {
570  revInfo = new Models.RevisionInformation
571  {
572  CommitSha = repoSha,
573  OriginCommitSha = repoSha,
574  Instance = new Models.Instance
575  {
576  Id = metadata.Id
577  },
578  ActiveTestMerges = new List<RevInfoTestMerge>()
579  };
580 
581  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
582  databaseContext.Instances.Attach(revInfo.Instance);
583  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
584  }
585  }
586  catch
587  {
588  repo.Dispose();
589  throw;
590  }
591  })
592  .ConfigureAwait(false);
593 
594  var likelyPushedTestMergeCommit =
595  repositorySettings.PushTestMergeCommits.Value
596  && repositorySettings.AccessToken != null
597  && repositorySettings.AccessUser != null;
598  using (repo)
599  compileJob = await Compile(
600  revInfo,
601  dreamMakerSettings,
602  ddSettings.StartupTimeout.Value,
603  repo,
604  progressReporter,
605  averageSpan,
606  likelyPushedTestMergeCommit,
607  cancellationToken)
608  .ConfigureAwait(false);
609 
610  var activeCompileJob = compileJobConsumer.LatestCompileJob();
611  try
612  {
613  await databaseContextFactory.UseContext(
614  async databaseContext =>
615  {
616  compileJob.Job = new Models.Job
617  {
618  Id = job.Id
619  };
620  compileJob.RevisionInformation = new Models.RevisionInformation
621  {
622  Id = revInfo.Id
623  };
624 
625  databaseContext.Jobs.Attach(compileJob.Job);
626  databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
627  databaseContext.CompileJobs.Add(compileJob);
628 
629  // The difficulty with compile jobs is they have a two part commit
630  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
631  try
632  {
633  await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
634  }
635  catch
636  {
637  // So we need to un-commit the compile job if the above throws
638  databaseContext.CompileJobs.Remove(compileJob);
639  await databaseContext.Save(default).ConfigureAwait(false);
640  throw;
641  }
642  })
643  .ConfigureAwait(false);
644  }
645  catch
646  {
647  await CleanupFailedCompile(compileJob).ConfigureAwait(false);
648  throw;
649  }
650 
651  var commentsTask = PostDeploymentComments(
652  revInfo,
653  activeCompileJob?.RevisionInformation,
654  repositorySettings,
655  repoOwner,
656  repoName);
657 
658  var eventTask = eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken);
659 
660  var chatTask = currentChatCallback(null, compileJob.Output);
661  currentChatCallback = null;
662 
663  try
664  {
665  await Task.WhenAll(commentsTask, eventTask, chatTask).ConfigureAwait(false);
666  }
667  catch (Exception ex)
668  {
669  throw new JobException(ErrorCode.PostDeployFailure, ex);
670  }
671  }
672  catch (Exception ex)
673  {
674  if (currentChatCallback != null)
675  await currentChatCallback(
676  ex.Message,
677  currentDreamMakerOutput)
678  .ConfigureAwait(false);
679 
680  throw;
681  }
682  finally
683  {
684  deploying = false;
685  }
686  }
687  #pragma warning restore CA1506
688 
695  async Task<TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
696  {
697  var previousCompileJobs = await databaseContext
698  .CompileJobs
699  .AsQueryable()
700  .Where(x => x.Job.Instance.Id == metadata.Id)
701  .OrderByDescending(x => x.Job.StoppedAt)
702  .Take(10)
703  .Select(x => new Models.Job
704  {
705  StoppedAt = x.Job.StoppedAt,
706  StartedAt = x.Job.StartedAt
707  })
708  .ToListAsync(cancellationToken)
709  .ConfigureAwait(false);
710 
711  TimeSpan? averageSpan = null;
712  if (previousCompileJobs.Count != 0)
713  {
714  var totalSpan = TimeSpan.Zero;
715  foreach (var I in previousCompileJobs)
716  totalSpan += I.StoppedAt.Value - I.StartedAt.Value;
717  averageSpan = totalSpan / previousCompileJobs.Count;
718  }
719 
720  return averageSpan;
721  }
722 
723  async Task<Models.CompileJob> Compile(
724  Models.RevisionInformation revisionInformation,
725  Api.Models.DreamMaker dreamMakerSettings,
726  uint apiValidateTimeout,
727  IRepository repository,
728  Action<int> progressReporter,
729  TimeSpan? estimatedDuration,
730  bool localCommitExistsOnRemote,
731  CancellationToken cancellationToken)
732  {
733  logger.LogTrace("Begin Compile");
734 
735  using var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
736  var progressTask = estimatedDuration.HasValue ? ProgressTask(progressReporter, estimatedDuration.Value, progressCts.Token) : Task.CompletedTask;
737  try
738  {
739  using var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false);
740  currentChatCallback = await chatManager.SendDeploymentMessage(
741  revisionInformation,
742  byondLock.Version,
743  DateTimeOffset.Now + estimatedDuration,
744  repository.GitHubOwner,
745  repository.GitHubRepoName,
746  localCommitExistsOnRemote,
747  cancellationToken)
748  .ConfigureAwait(false);
749 
750  var job = new Models.CompileJob
751  {
752  DirectoryName = Guid.NewGuid(),
753  DmeName = dreamMakerSettings.ProjectName,
754  RevisionInformation = revisionInformation,
755  ByondVersion = byondLock.Version.ToString()
756  };
757 
758  await RunCompileJob(job, dreamMakerSettings, byondLock, repository, apiValidateTimeout, cancellationToken).ConfigureAwait(false);
759 
760  return job;
761  }
762  catch (OperationCanceledException)
763  {
764  await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);
765  throw;
766  }
767  finally
768  {
769  progressCts.Cancel();
770  await progressTask.ConfigureAwait(false);
771  }
772  }
773 
784  Models.RevisionInformation deployedRevisionInformation,
785  Models.RevisionInformation previousRevisionInformation,
786  Models.RepositorySettings repositorySettings,
787  string repoOwner,
788  string repoName)
789  {
790  if (repositorySettings?.AccessToken == null)
791  return;
792 
793  if ((previousRevisionInformation != null && previousRevisionInformation.CommitSha == previousRevisionInformation.CommitSha)
794  || !repositorySettings.PostTestMergeComment.Value)
795  return;
796 
797  previousRevisionInformation = new Models.RevisionInformation
798  {
799  ActiveTestMerges = new List<RevInfoTestMerge>()
800  };
801 
802  var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken);
803 
804  async Task CommentOnPR(int prNumber, string comment)
805  {
806  try
807  {
808  await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
809  }
810  catch (ApiException e)
811  {
812  logger.LogWarning("Error posting GitHub comment! Exception: {0}", e);
813  }
814  }
815 
816  var tasks = new List<Task>();
817 
818  string FormatTestMerge(Models.TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}",
819  Environment.NewLine,
820  repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty,
821  testMerge.PullRequestRevision,
822  testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty,
823  updated ? "Updated" : "Deployed",
824  metadata.Name,
825  deployedRevisionInformation.OriginCommitSha,
826  deployedRevisionInformation.CommitSha);
827 
828  // added prs
829  foreach (var I in deployedRevisionInformation
830  .ActiveTestMerges
831  .Select(x => x.TestMerge)
832  .Where(x => !previousRevisionInformation
833  .ActiveTestMerges
834  .Any(y => y.TestMerge.Number == x.Number)))
835  tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, false)));
836 
837  // removed prs
838  foreach (var I in previousRevisionInformation
839  .ActiveTestMerges
840  .Select(x => x.TestMerge)
841  .Where(x => !deployedRevisionInformation
842  .ActiveTestMerges
843  .Any(y => y.TestMerge.Number == x.Number)))
844  tasks.Add(CommentOnPR(I.Number, "#### Test Merge Removed"));
845 
846  // updated prs
847  foreach (var I in deployedRevisionInformation
848  .ActiveTestMerges
849  .Select(x => x.TestMerge)
850  .Where(x => previousRevisionInformation
851  .ActiveTestMerges
852  .Any(y => y.TestMerge.Number == x.Number)))
853  tasks.Add(CommentOnPR(I.Number, FormatTestMerge(I, true)));
854 
855  if (tasks.Any())
856  await Task.WhenAll(tasks).ConfigureAwait(false);
857  }
858  }
859 }
readonly object deploymentLock
object for deploying.
Definition: DreamMaker.cs:112
+
readonly IEventConsumer eventConsumer
The IEventConsumer for DreamMaker
Definition: DreamMaker.cs:72
ApiValidationStatus
Status of DMAPI validation
-
DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, IEventConsumer eventConsumer, IChatManager chatManager, IProcessExecutor processExecutor, IWatchdog watchdog, IGitHubClientFactory gitHubClientFactory, ICompileJobSink compileJobConsumer, IRepositoryManager repositoryManager, ILogger< DreamMaker > logger, Api.Models.Instance metadata)
Construct DreamMaker
Definition: DreamMaker.cs:141
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
- -
readonly object compilingLock
object for compiling.
Definition: DreamMaker.cs:118
-
async Task ProgressTask(Action< int > progressReporter, TimeSpan estimatedDuration, CancellationToken cancellationToken)
Gradually triggers a given progressReporter over a given estimatedDuration
Definition: DreamMaker.cs:180
+
async Task ProgressTask(Action< int > progressReporter, TimeSpan estimatedDuration, CancellationToken cancellationToken)
Gradually triggers a given progressReporter over a given estimatedDuration
Definition: DreamMaker.cs:175
Consumes EventTypes and takes the appropriate actions
string GitHubOwner
The Octokit.Repository.Owner if this IsGitHubRepository
Definition: IRepository.cs:21
-
bool compiling
If a compile job is running
Definition: DreamMaker.cs:123
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
Adds server side includes to the .dme being compiled
Definition: DreamMaker.cs:303
-
readonly IProcessExecutor processExecutor
The IProcessExecutor for DreamMaker
Definition: DreamMaker.cs:83
+
async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
Adds server side includes to the .dme being compiled
Definition: DreamMaker.cs:298
+
readonly IProcessExecutor processExecutor
The IProcessExecutor for DreamMaker
Definition: DreamMaker.cs:82
+
DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, IEventConsumer eventConsumer, IChatManager chatManager, IProcessExecutor processExecutor, IGitHubClientFactory gitHubClientFactory, ICompileJobSink compileJobConsumer, IRepositoryManager repositoryManager, ILogger< DreamMaker > logger, Api.Models.Instance metadata)
Construct DreamMaker
Definition: DreamMaker.cs:138
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:102
- -
async Task DeploymentProcess(Models.Job job, IDatabaseContextFactory databaseContextFactory, Action< int > progressReporter, CancellationToken cancellationToken)
Definition: DreamMaker.cs:535
-
readonly Api.Models.Instance metadata
The Instance DreamMaker belongs to.
Definition: DreamMaker.cs:113
+ +
async Task DeploymentProcess(Models.Job job, IDatabaseContextFactory databaseContextFactory, Action< int > progressReporter, CancellationToken cancellationToken)
Definition: DreamMaker.cs:467
+
readonly Api.Models.Instance metadata
The Instance DreamMaker belongs to.
Definition: DreamMaker.cs:107
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
Represents an on-disk git repository
Definition: IRepository.cs:11
IDatabaseCollection< CompileJob > CompileJobs
The CompileJobs in the IDatabaseContext
-
readonly StaticFiles.IConfiguration configuration
The StaticFiles.IConfiguration for DreamMaker
Definition: DreamMaker.cs:63
+
readonly StaticFiles.IConfiguration configuration
The StaticFiles.IConfiguration for DreamMaker
Definition: DreamMaker.cs:62
For managing connected chat services
Definition: IChatManager.cs:13
-
async Task SendDeploymentMessage(Models.RevisionInformation revisionInformation, IByondExecutableLock byondLock, CancellationToken cancellationToken)
Send a message to chatManager about a deployment
Definition: DreamMaker.cs:382
-
async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
Run a quick DD instance to test the DMAPI is installed on the target code
Definition: DreamMaker.cs:207
+
async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
Run a quick DD instance to test the DMAPI is installed on the target code
Definition: DreamMaker.cs:202
-
readonly ICompileJobSink compileJobConsumer
The ICompileJobSink for DreamMaker.
Definition: DreamMaker.cs:103
+
readonly ICompileJobSink compileJobConsumer
The ICompileJobSink for DreamMaker.
Definition: DreamMaker.cs:97
-
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for DreamMaker
Definition: DreamMaker.cs:68
-
readonly IIOManager ioManager
The IIOManager for DreamMaker
Definition: DreamMaker.cs:58
+
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for DreamMaker
Definition: DreamMaker.cs:67
+
readonly IIOManager ioManager
The IIOManager for DreamMaker
Definition: DreamMaker.cs:57
string GitHubRepoName
The Octokit.Repository.Name if this IsGitHubRepository
Definition: IRepository.cs:26
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
-
async Task< int > RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
Compiles a .dme with DreamMaker
Definition: DreamMaker.cs:274
+
bool deploying
If a compile job is running
Definition: DreamMaker.cs:121
+
async Task< int > RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
Compiles a .dme with DreamMaker
Definition: DreamMaker.cs:269
Represents a git repository
Definition: Repository.cs:8
-
async Task PostDeploymentComments(Models.RevisionInformation deployedRevisionInformation, Models.RevisionInformation previousRevisionInformation, Models.RepositorySettings repositorySettings, string repoOwner, string repoName)
Post deployment GitHub comments.
Definition: DreamMaker.cs:806
+
async Task PostDeploymentComments(Models.RevisionInformation deployedRevisionInformation, Models.RevisionInformation previousRevisionInformation, Models.RepositorySettings repositorySettings, string repoOwner, string repoName)
Post deployment GitHub comments.
Definition: DreamMaker.cs:783
-
readonly IByondManager byond
The IByondManager for DreamMaker
Definition: DreamMaker.cs:53
+
readonly IByondManager byond
The IByondManager for DreamMaker
Definition: DreamMaker.cs:52
+
async Task CleanupFailedCompile(Models.CompileJob job)
Cleans up a failed compile job
Definition: DreamMaker.cs:351
string Origin
The current origin remote the IRepository is using
Definition: IRepository.cs:46
-
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for DreamMaker.
Definition: DreamMaker.cs:98
+
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for DreamMaker.
Definition: DreamMaker.cs:92
string DreamMakerPath
The path to the dm/DreamMaker executable
+
For managing the BYOND installation
-
readonly IWatchdog watchdog
The IWatchdog for DreamMaker
Definition: DreamMaker.cs:88
Task UseContext(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext
-
async Task RunCompileJob(Models.CompileJob job, Api.Models.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken)
Executes and populate a given job
Definition: DreamMaker.cs:443
+
async Task RunCompileJob(Models.CompileJob job, Api.Models.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken)
Executes and populate a given job
Definition: DreamMaker.cs:375
+
async Task< Models.CompileJob > Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action< int > progressReporter, TimeSpan?estimatedDuration, bool localCommitExistsOnRemote, CancellationToken cancellationToken)
Definition: DreamMaker.cs:723
Task CopyTo(string path, CancellationToken cancellationToken)
Copies the current working directory to a given path
Represents usage of the two primary BYOND server executables
Factory for creating and loading IRepositorys
string Head
The SHA of the IRepository HEAD
Definition: IRepository.cs:36
-
readonly IRepositoryManager repositoryManager
The IRepositoryManager for DreamMaker.
Definition: DreamMaker.cs:93
-
readonly ILogger< DreamMaker > logger
The ILogger for DreamMaker
Definition: DreamMaker.cs:108
+
readonly IRepositoryManager repositoryManager
The IRepositoryManager for DreamMaker.
Definition: DreamMaker.cs:87
+
readonly ILogger< DreamMaker > logger
The ILogger for DreamMaker
Definition: DreamMaker.cs:102
bool IsGitHubRepository
If the IRepository was cloned from GitHub.com
Definition: IRepository.cs:16
DreamDaemonSecurity
DreamDaemon&#39;s security level
-
async Task< Models.CompileJob > Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action< int > progressReporter, TimeSpan?estimatedDuration, CancellationToken cancellationToken)
Definition: DreamMaker.cs:754
-
Task< List< TModel > > ToListAsync(CancellationToken cancellationToken)
Retrieve all the TModel s in the table.
Interface for using filesystems
Definition: IIOManager.cs:11
-
async Task CleanupFailedCompile(Models.CompileJob job, bool cancelled, CancellationToken cancellationToken)
Cleans up a failed compile job
Definition: DreamMaker.cs:358
Represents information about a current git revison
-
Version Version
The global::System.Version of the locked executables
+ -
readonly IChatManager chatManager
The IChatManager for DreamMaker
Definition: DreamMaker.cs:78
+
readonly IChatManager chatManager
The IChatManager for DreamMaker
Definition: DreamMaker.cs:77
- -
async Task< TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Calculate the average length of a deployment using a given databaseContext .
Definition: DreamMaker.cs:728
+ +
async Task< TimeSpan?> CalculateExpectedDeploymentTime(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Calculate the average length of a deployment using a given databaseContext .
Definition: DreamMaker.cs:695
-
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:13
diff --git a/_tgstation_8_server_8_host_2_components_2_instance_8cs.html b/_tgstation_8_server_8_host_2_components_2_instance_8cs.html index 8aa2a2f811..f75f322dc7 100644 --- a/_tgstation_8_server_8_host_2_components_2_instance_8cs.html +++ b/_tgstation_8_server_8_host_2_components_2_instance_8cs.html @@ -25,7 +25,7 @@
diff --git a/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html b/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html index cfcf16d3f7..f0fe6163b9 100644 --- a/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,8 +94,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Instance.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
17 
18 namespace Tgstation.Server.Host.Components
19 {
21  #pragma warning disable CA1506 // TODO: Decomplexify
22  sealed class Instance : IInstance
23  {
26 
28  public IByondManager ByondManager { get; }
29 
31  public IWatchdog Watchdog { get; }
32 
34  public IChatManager Chat { get; }
35 
37  public StaticFiles.IConfiguration Configuration { get; }
38 
40  public IDreamMaker DreamMaker { get; }
41 
46 
51 
56 
61 
65  readonly ILogger<Instance> logger;
66 
70  readonly Api.Models.Instance metadata;
71 
75  readonly object timerLock;
76 
80  Task timerTask;
81 
85  CancellationTokenSource timerCts;
86 
102  public Instance(
103  Api.Models.Instance metadata,
104  IRepositoryManager repositoryManager,
105  IByondManager byondManager,
106  IDreamMaker dreamMaker,
107  IWatchdog watchdog,
108  IChatManager chat,
109  StaticFiles.IConfiguration
110  configuration,
111  IDatabaseContextFactory databaseContextFactory,
112  IDmbFactory dmbFactory,
113  IJobManager jobManager,
114  IEventConsumer eventConsumer,
115  ILogger<Instance> logger)
116  {
117  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
118  RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
119  ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
120  DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
121  Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
122  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
123  Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
124  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
125  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
126  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
127  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
128  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
129 
130  timerLock = new object();
131  }
132 
134  public void Dispose()
135  {
136  timerCts?.Dispose();
137  Configuration.Dispose();
138  Chat.Dispose();
139  Watchdog.Dispose();
140  dmbFactory.Dispose();
142  }
143 
150  #pragma warning disable CA1502 // TODO: Decomplexify
151  async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
152  {
153  while (true)
154  try
155  {
156  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
157  logger.LogInformation("Beginning auto update...");
158  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
159  try
160  {
161  User user = null;
162  await databaseContextFactory.UseContext(
163  async (db) => user = await db
164  .Users
165  .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName))
166  .FirstAsync(cancellationToken)
167  .ConfigureAwait(false))
168  .ConfigureAwait(false);
169  var repositoryUpdateJob = new Job
170  {
171  Instance = new Models.Instance
172  {
173  Id = metadata.Id
174  },
175  Description = "Scheduled repository update",
176  CancelRightsType = RightsType.Repository,
177  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
178  StartedBy = user
179  };
180 
181  string deploySha = null;
182  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContextFactory, progressReporter, jobCancellationToken) =>
183  {
184  // assume 5 steps with synchronize
185  const int ProgressSections = 7;
186  const int ProgressStep = 100 / ProgressSections;
187  string repoHead = null;
188 
189  await databaseContextFactory.UseContext(
190  async databaseContext =>
191  {
192  var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
193 
194  const int NumSteps = 3;
195  var doneSteps = 0;
196 
197  Action<int> NextProgressReporter()
198  {
199  var tmpDoneSteps = doneSteps;
200  ++doneSteps;
201  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps);
202  }
203 
204  using var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false);
205  if (repo == null)
206  {
207  logger.LogTrace("Aborting repo update, no repository!");
208  return;
209  }
210 
211  var startSha = repo.Head;
212  if (!repo.Tracking)
213  {
214  logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
215  deploySha = startSha;
216  return;
217  }
218 
219  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
220 
221  // the main point of auto update is to pull the remote
222  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
223 
224  RevisionInformation currentRevInfo = null;
225  bool hasDbChanges = false;
226 
227  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
228  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
229  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
230  .FirstOrDefaultAsync(cancellationToken);
231 
232  async Task UpdateRevInfo(string currentHead, bool onOrigin)
233  {
234  if (currentRevInfo == null)
235  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
236 
237  if (currentRevInfo == default)
238  {
239  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
240  onOrigin = true;
241  }
242 
243  var attachedInstance = new Models.Instance
244  {
245  Id = metadata.Id
246  };
247  var oldRevInfo = currentRevInfo;
248  currentRevInfo = new RevisionInformation
249  {
250  CommitSha = currentHead,
251  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
252  Instance = attachedInstance
253  };
254  if (!onOrigin)
255  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
256 
257  databaseContext.Instances.Attach(attachedInstance);
258  databaseContext.RevisionInformations.Add(currentRevInfo);
259  hasDbChanges = true;
260  }
261 
262  // take appropriate auto update actions
263  bool shouldSyncTracked;
264  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
265  {
266  logger.LogTrace("Preserving test merges...");
267 
268  var currentRevInfoTask = LoadRevInfo();
269 
270  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
271 
272  if (!result.HasValue)
273  throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict);
274 
275  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
276 
277  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
278  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
279 
280  var currentHead = repo.Head;
281  if (currentHead != startSha)
282  {
283  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
284  shouldSyncTracked = stillOnOrigin;
285  }
286  else
287  shouldSyncTracked = false;
288  }
289  else
290  {
291  logger.LogTrace("Not preserving test merges...");
292  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
293 
294  var currentHead = repo.Head;
295 
296  currentRevInfo = await databaseContext.RevisionInformations
297  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
298  .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
299 
300  if (currentHead != startSha && currentRevInfo != default)
301  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
302 
303  shouldSyncTracked = true;
304  }
305 
306  // synch if necessary
307  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
308  {
309  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
310  var currentHead = repo.Head;
311  if (currentHead != currentRevInfo.CommitSha)
312  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
313  }
314 
315  repoHead = repo.Head;
316 
317  if (hasDbChanges)
318  try
319  {
320  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
321  }
322  catch
323  {
324  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
325  throw;
326  }
327  })
328  .ConfigureAwait(false);
329 
330  progressReporter(5 * ProgressStep);
331  deploySha = repoHead;
332  }, cancellationToken).ConfigureAwait(false);
333 
334  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
335 
336  if (deploySha == null)
337  {
338  logger.LogTrace("Aborting auto update, repository error!");
339  continue;
340  }
341 
342  if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
343  {
344  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
345  continue;
346  }
347 
348  // finally set up the job
349  var compileProcessJob = new Job
350  {
351  StartedBy = user,
352  Instance = repositoryUpdateJob.Instance,
353  Description = "Scheduled code deployment",
354  CancelRightsType = RightsType.DreamMaker,
355  CancelRight = (ulong)DreamMakerRights.CancelCompile
356  };
357 
358  await jobManager.RegisterOperation(
359  compileProcessJob,
361  cancellationToken).ConfigureAwait(false);
362 
363  await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
364  }
365  catch (OperationCanceledException)
366  {
367  logger.LogDebug("Cancelled auto update job!");
368  throw;
369  }
370  catch (Exception e)
371  {
372  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
373  continue;
374  }
375  }
376  catch (OperationCanceledException)
377  {
378  break;
379  }
380 
381  logger.LogTrace("Leaving auto update loop...");
382  }
383  #pragma warning restore CA1502
384 
386  public void Rename(string newName)
387  {
388  if (String.IsNullOrWhiteSpace(newName))
389  throw new ArgumentNullException(nameof(newName));
390  metadata.Name = newName;
391  }
392 
394  public async Task StartAsync(CancellationToken cancellationToken)
395  {
396  await Task.WhenAll(
397  SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value),
398  Configuration.StartAsync(cancellationToken),
399  ByondManager.StartAsync(cancellationToken),
400  Chat.StartAsync(cancellationToken),
401  dmbFactory.StartAsync(cancellationToken))
402  .ConfigureAwait(false);
403 
404  // dependent on so many things, its just safer this way
405  await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
406 
407  await dmbFactory.CleanUnusedCompileJobs(cancellationToken).ConfigureAwait(false);
408  }
409 
411  public async Task StopAsync(CancellationToken cancellationToken)
412  {
413  await SetAutoUpdateInterval(0).ConfigureAwait(false);
414  await Watchdog.StopAsync(cancellationToken).ConfigureAwait(false);
415  await Task.WhenAll(
416  Configuration.StopAsync(cancellationToken),
417  ByondManager.StopAsync(cancellationToken),
418  Chat.StopAsync(cancellationToken),
419  dmbFactory.StopAsync(cancellationToken))
420  .ConfigureAwait(false);
421  }
422 
424  public async Task SetAutoUpdateInterval(uint newInterval)
425  {
426  Task toWait;
427  lock (timerLock)
428  {
429  if (timerTask != null)
430  {
431  timerCts.Cancel();
432  toWait = timerTask;
433  }
434  else
435  toWait = Task.CompletedTask;
436  }
437 
438  await toWait.ConfigureAwait(false);
439  if (newInterval == 0)
440  return;
441  lock (timerLock)
442  {
443  // race condition, just quit
444  if (timerTask != null)
445  return;
446  timerCts?.Dispose();
447  timerCts = new CancellationTokenSource();
448  timerTask = TimerLoop(newInterval, timerCts.Token);
449  }
450  }
451 
453  public CompileJob LatestCompileJob() => dmbFactory.LatestCompileJob();
454  }
455 }
-
async Task SetAutoUpdateInterval(uint newInterval)
Change the Api.Models.Instance.AutoUpdateInterval for the IInstance
Definition: Instance.cs:424
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
17 
18 namespace Tgstation.Server.Host.Components
19 {
21  #pragma warning disable CA1506 // TODO: Decomplexify
22  sealed class Instance : IInstance
23  {
26 
28  public IByondManager ByondManager { get; }
29 
31  public IWatchdog Watchdog { get; }
32 
34  public IChatManager Chat { get; }
35 
37  public StaticFiles.IConfiguration Configuration { get; }
38 
40  public IDreamMaker DreamMaker { get; }
41 
46 
51 
56 
61 
65  readonly ILogger<Instance> logger;
66 
70  readonly Api.Models.Instance metadata;
71 
75  readonly object timerLock;
76 
80  Task timerTask;
81 
85  CancellationTokenSource timerCts;
86 
102  public Instance(
103  Api.Models.Instance metadata,
104  IRepositoryManager repositoryManager,
105  IByondManager byondManager,
106  IDreamMaker dreamMaker,
107  IWatchdog watchdog,
108  IChatManager chat,
109  StaticFiles.IConfiguration
110  configuration,
111  IDatabaseContextFactory databaseContextFactory,
112  IDmbFactory dmbFactory,
113  IJobManager jobManager,
114  IEventConsumer eventConsumer,
115  ILogger<Instance> logger)
116  {
117  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
118  RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
119  ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
120  DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
121  Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
122  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
123  Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
124  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
125  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
126  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
127  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
128  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
129 
130  timerLock = new object();
131  }
132 
134  public void Dispose()
135  {
136  timerCts?.Dispose();
137  Configuration.Dispose();
138  Chat.Dispose();
139  Watchdog.Dispose();
140  dmbFactory.Dispose();
142  }
143 
150  #pragma warning disable CA1502 // TODO: Decomplexify
151  async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
152  {
153  while (true)
154  try
155  {
156  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
157  logger.LogInformation("Beginning auto update...");
158  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
159  try
160  {
161  User user = null;
162  await databaseContextFactory.UseContext(
163  async (db) => user = await db
164  .Users
165  .AsQueryable()
166  .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName))
167  .FirstAsync(cancellationToken)
168  .ConfigureAwait(false))
169  .ConfigureAwait(false);
170  var repositoryUpdateJob = new Job
171  {
172  Instance = new Models.Instance
173  {
174  Id = metadata.Id
175  },
176  Description = "Scheduled repository update",
177  CancelRightsType = RightsType.Repository,
178  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
179  StartedBy = user
180  };
181 
182  string deploySha = null;
183  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContextFactory, progressReporter, jobCancellationToken) =>
184  {
185  // assume 5 steps with synchronize
186  const int ProgressSections = 7;
187  const int ProgressStep = 100 / ProgressSections;
188  string repoHead = null;
189 
190  await databaseContextFactory.UseContext(
191  async databaseContext =>
192  {
193  var repositorySettingsTask = databaseContext
194  .RepositorySettings
195  .AsQueryable()
196  .Where(x => x.InstanceId == metadata.Id)
197  .FirstAsync(jobCancellationToken);
198 
199  const int NumSteps = 3;
200  var doneSteps = 0;
201 
202  Action<int> NextProgressReporter()
203  {
204  var tmpDoneSteps = doneSteps;
205  ++doneSteps;
206  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps);
207  }
208 
209  using var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false);
210  if (repo == null)
211  {
212  logger.LogTrace("Aborting repo update, no repository!");
213  return;
214  }
215 
216  var startSha = repo.Head;
217  if (!repo.Tracking)
218  {
219  logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
220  deploySha = startSha;
221  return;
222  }
223 
224  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
225 
226  // the main point of auto update is to pull the remote
227  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
228 
229  RevisionInformation currentRevInfo = null;
230  bool hasDbChanges = false;
231 
232  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
233  .AsQueryable()
234  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
235  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
236  .FirstOrDefaultAsync(cancellationToken);
237 
238  async Task UpdateRevInfo(string currentHead, bool onOrigin)
239  {
240  if (currentRevInfo == null)
241  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
242 
243  if (currentRevInfo == default)
244  {
245  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
246  onOrigin = true;
247  }
248 
249  var attachedInstance = new Models.Instance
250  {
251  Id = metadata.Id
252  };
253  var oldRevInfo = currentRevInfo;
254  currentRevInfo = new RevisionInformation
255  {
256  CommitSha = currentHead,
257  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
258  Instance = attachedInstance
259  };
260  if (!onOrigin)
261  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
262 
263  databaseContext.Instances.Attach(attachedInstance);
264  databaseContext.RevisionInformations.Add(currentRevInfo);
265  hasDbChanges = true;
266  }
267 
268  // take appropriate auto update actions
269  bool shouldSyncTracked;
270  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
271  {
272  logger.LogTrace("Preserving test merges...");
273 
274  var currentRevInfoTask = LoadRevInfo();
275 
276  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
277 
278  if (!result.HasValue)
279  throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict);
280 
281  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
282 
283  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
284  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
285 
286  var currentHead = repo.Head;
287  if (currentHead != startSha)
288  {
289  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
290  shouldSyncTracked = stillOnOrigin;
291  }
292  else
293  shouldSyncTracked = false;
294  }
295  else
296  {
297  logger.LogTrace("Not preserving test merges...");
298  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
299 
300  var currentHead = repo.Head;
301 
302  currentRevInfo = await databaseContext.RevisionInformations
303  .AsQueryable()
304  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
305  .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
306 
307  if (currentHead != startSha && currentRevInfo != default)
308  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
309 
310  shouldSyncTracked = true;
311  }
312 
313  // synch if necessary
314  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
315  {
316  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
317  var currentHead = repo.Head;
318  if (currentHead != currentRevInfo.CommitSha)
319  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
320  }
321 
322  repoHead = repo.Head;
323 
324  if (hasDbChanges)
325  try
326  {
327  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
328  }
329  catch
330  {
331  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
332  throw;
333  }
334  })
335  .ConfigureAwait(false);
336 
337  progressReporter(5 * ProgressStep);
338  deploySha = repoHead;
339  }, cancellationToken).ConfigureAwait(false);
340 
341  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
342 
343  if (deploySha == null)
344  {
345  logger.LogTrace("Aborting auto update, repository error!");
346  continue;
347  }
348 
349  if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
350  {
351  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
352  continue;
353  }
354 
355  // finally set up the job
356  var compileProcessJob = new Job
357  {
358  StartedBy = user,
359  Instance = repositoryUpdateJob.Instance,
360  Description = "Scheduled code deployment",
361  CancelRightsType = RightsType.DreamMaker,
362  CancelRight = (ulong)DreamMakerRights.CancelCompile
363  };
364 
365  await jobManager.RegisterOperation(
366  compileProcessJob,
368  cancellationToken).ConfigureAwait(false);
369 
370  await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
371  }
372  catch (OperationCanceledException)
373  {
374  logger.LogDebug("Cancelled auto update job!");
375  throw;
376  }
377  catch (Exception e)
378  {
379  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
380  continue;
381  }
382  }
383  catch (OperationCanceledException)
384  {
385  break;
386  }
387 
388  logger.LogTrace("Leaving auto update loop...");
389  }
390  #pragma warning restore CA1502
391 
393  public void Rename(string newName)
394  {
395  if (String.IsNullOrWhiteSpace(newName))
396  throw new ArgumentNullException(nameof(newName));
397  metadata.Name = newName;
398  }
399 
401  public async Task StartAsync(CancellationToken cancellationToken)
402  {
403  await Task.WhenAll(
404  SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value),
405  Configuration.StartAsync(cancellationToken),
406  ByondManager.StartAsync(cancellationToken),
407  Chat.StartAsync(cancellationToken),
408  dmbFactory.StartAsync(cancellationToken))
409  .ConfigureAwait(false);
410 
411  // dependent on so many things, its just safer this way
412  await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
413 
414  await dmbFactory.CleanUnusedCompileJobs(cancellationToken).ConfigureAwait(false);
415  }
416 
418  public async Task StopAsync(CancellationToken cancellationToken)
419  {
420  await SetAutoUpdateInterval(0).ConfigureAwait(false);
421  await Watchdog.StopAsync(cancellationToken).ConfigureAwait(false);
422  await Task.WhenAll(
423  Configuration.StopAsync(cancellationToken),
424  ByondManager.StopAsync(cancellationToken),
425  Chat.StopAsync(cancellationToken),
426  dmbFactory.StopAsync(cancellationToken))
427  .ConfigureAwait(false);
428  }
429 
431  public async Task SetAutoUpdateInterval(uint newInterval)
432  {
433  Task toWait;
434  lock (timerLock)
435  {
436  if (timerTask != null)
437  {
438  timerCts.Cancel();
439  toWait = timerTask;
440  }
441  else
442  toWait = Task.CompletedTask;
443  }
444 
445  await toWait.ConfigureAwait(false);
446  if (newInterval == 0)
447  return;
448  lock (timerLock)
449  {
450  // race condition, just quit
451  if (timerTask != null)
452  return;
453  timerCts?.Dispose();
454  timerCts = new CancellationTokenSource();
455  timerTask = TimerLoop(newInterval, timerCts.Token);
456  }
457  }
458 
460  public CompileJob LatestCompileJob() => dmbFactory.LatestCompileJob();
461  }
462 }
+
async Task SetAutoUpdateInterval(uint newInterval)
Change the Api.Models.Instance.AutoUpdateInterval for the IInstance
Definition: Instance.cs:431
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
@@ -106,11 +106,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly object timerLock
object for timerCts and timerTask.
Definition: Instance.cs:75
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: Instance.cs:411
-
void Rename(string newName)
Rename the IInstance
Definition: Instance.cs:386
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: Instance.cs:418
+
void Rename(string newName)
Rename the IInstance
Definition: Instance.cs:393
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:102
- -
async Task DeploymentProcess(Models.Job job, IDatabaseContextFactory databaseContextFactory, Action< int > progressReporter, CancellationToken cancellationToken)
Definition: DreamMaker.cs:535
+ +
async Task DeploymentProcess(Models.Job job, IDatabaseContextFactory databaseContextFactory, Action< int > progressReporter, CancellationToken cancellationToken)
Definition: DreamMaker.cs:467
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
async Task StartAsync(CancellationToken cancellationToken)
@@ -152,13 +152,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IJobManager jobManager
The IJobManager for the Instance
Definition: Instance.cs:55
Repository(LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger< Repository > logger, Action onDispose)
Construct a Repository
Definition: Repository.cs:114
async Task< IRepository > LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Instance.cs:394
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Instance.cs:401
Task StopAsync(CancellationToken cancellationToken)
List< RevInfoTestMerge > ActiveTestMerges
See Api.Models.RevisionInformation.ActiveTestMerges
string OriginCommitSha
The sha of the most recent remote commit
- +
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:13
diff --git a/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs.html b/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs.html index 41a0ee43ef..d5f38b97b0 100644 --- a/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs.html +++ b/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs.html @@ -25,7 +25,7 @@
diff --git a/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs_source.html b/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs_source.html index 224aff5490..0001bfb9e9 100644 --- a/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_chat_bot_8cs.html b/_tgstation_8_server_8_host_2_models_2_chat_bot_8cs.html index dacbc1d9b6..d49ed09ecc 100644 --- a/_tgstation_8_server_8_host_2_models_2_chat_bot_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_chat_bot_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_chat_bot_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_chat_bot_8cs_source.html index 624c9a8a5f..d66c48d362 100644 --- a/_tgstation_8_server_8_host_2_models_2_chat_bot_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_chat_bot_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_chat_channel_8cs.html b/_tgstation_8_server_8_host_2_models_2_chat_channel_8cs.html index b019e03a9c..d3065f0515 100644 --- a/_tgstation_8_server_8_host_2_models_2_chat_channel_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_chat_channel_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_chat_channel_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_chat_channel_8cs_source.html index 07c85b5ebd..4a8f9d0ca3 100644 --- a/_tgstation_8_server_8_host_2_models_2_chat_channel_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_chat_channel_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_compile_job_8cs.html b/_tgstation_8_server_8_host_2_models_2_compile_job_8cs.html index 1f51a539ae..66ac833bce 100644 --- a/_tgstation_8_server_8_host_2_models_2_compile_job_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_compile_job_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_compile_job_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_compile_job_8cs_source.html index 5d7f3ce75e..ee26d61c76 100644 --- a/_tgstation_8_server_8_host_2_models_2_compile_job_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_compile_job_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_dream_daemon_settings_8cs.html b/_tgstation_8_server_8_host_2_models_2_dream_daemon_settings_8cs.html index decb7e5344..8761148d79 100644 --- a/_tgstation_8_server_8_host_2_models_2_dream_daemon_settings_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_dream_daemon_settings_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_dream_daemon_settings_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_dream_daemon_settings_8cs_source.html index d140e52022..82aea43428 100644 --- a/_tgstation_8_server_8_host_2_models_2_dream_daemon_settings_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_dream_daemon_settings_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_instance_8cs.html b/_tgstation_8_server_8_host_2_models_2_instance_8cs.html index 77679b1292..bfc8c92600 100644 --- a/_tgstation_8_server_8_host_2_models_2_instance_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_instance_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_instance_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_instance_8cs_source.html index 1cf1974299..e43b6af3ca 100644 --- a/_tgstation_8_server_8_host_2_models_2_instance_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_instance_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_instance_user_8cs.html b/_tgstation_8_server_8_host_2_models_2_instance_user_8cs.html index 1eafd6a117..cba4c6f594 100644 --- a/_tgstation_8_server_8_host_2_models_2_instance_user_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_instance_user_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_instance_user_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_instance_user_8cs_source.html index bcf096b3a5..c5feeacdab 100644 --- a/_tgstation_8_server_8_host_2_models_2_instance_user_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_instance_user_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_job_8cs.html b/_tgstation_8_server_8_host_2_models_2_job_8cs.html index 74620c5b06..301a7f6749 100644 --- a/_tgstation_8_server_8_host_2_models_2_job_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_job_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_job_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_job_8cs_source.html index 36c36c9f43..266a056eff 100644 --- a/_tgstation_8_server_8_host_2_models_2_job_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_job_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs.html b/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs.html index 239b1673af..110cdb41ff 100644 --- a/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs_source.html index 79c7440de1..7bfb480ef7 100644 --- a/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_revision_information_8cs.html b/_tgstation_8_server_8_host_2_models_2_revision_information_8cs.html index 1143574a15..d85ea072ce 100644 --- a/_tgstation_8_server_8_host_2_models_2_revision_information_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_revision_information_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_revision_information_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_revision_information_8cs_source.html index 451b4dbb81..4ff61a28d5 100644 --- a/_tgstation_8_server_8_host_2_models_2_revision_information_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_revision_information_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RevisionInformation.cs
-Go to the documentation of this file.
1 using System.Collections.Generic;
2 using System.ComponentModel.DataAnnotations;
3 using System.Linq;
4 
5 namespace Tgstation.Server.Host.Models
6 {
8  public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation
9  {
13  public long Id { get; set; }
14 
18  public long InstanceId { get; set; }
19 
23  [Required]
24  public Instance Instance { get; set; }
25 
29  public TestMerge PrimaryTestMerge { get; set; }
30 
34  public List<RevInfoTestMerge> ActiveTestMerges { get; set; }
35 
39  public List<CompileJob> CompileJobs { get; set; }
40 
45  public Api.Models.RevisionInformation ToApi() => new Api.Models.RevisionInformation
46  {
47  CommitSha = CommitSha,
48  OriginCommitSha = OriginCommitSha,
49  PrimaryTestMerge = PrimaryTestMerge?.ToApi(),
50  ActiveTestMerges = ActiveTestMerges.Select(x => x.TestMerge.ToApi()).ToList(),
51  CompileJobs = CompileJobs.Select(x => new Api.Models.CompileJob
52  {
53  Id = x.Id // anti recursion measure
54  }).ToList()
55  };
56  }
57 }
+Go to the documentation of this file.
1 using System.Collections.Generic;
2 using System.ComponentModel.DataAnnotations;
3 using System.Linq;
4 
5 namespace Tgstation.Server.Host.Models
6 {
8  public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation
9  {
13  public long Id { get; set; }
14 
18  public long InstanceId { get; set; }
19 
23  [Required]
24  public Instance Instance { get; set; }
25 
29  public TestMerge PrimaryTestMerge { get; set; }
30 
34  public List<RevInfoTestMerge> ActiveTestMerges { get; set; }
35 
39  public List<CompileJob> CompileJobs { get; set; }
40 
45  public Api.Models.RevisionInformation ToApi() => new Api.Models.RevisionInformation
46  {
47  CommitSha = CommitSha,
48  OriginCommitSha = OriginCommitSha,
49  PrimaryTestMerge = PrimaryTestMerge?.ToApi(),
50  ActiveTestMerges = ActiveTestMerges.Select(x => x.TestMerge.ToApi()).ToList(),
51  CompileJobs = CompileJobs.Select(x => new Api.Models.EntityId
52  {
53  Id = x.Id
54  }).ToList()
55  };
56  }
57 }
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
diff --git a/_tgstation_8_server_8_host_2_models_2_test_merge_8cs.html b/_tgstation_8_server_8_host_2_models_2_test_merge_8cs.html index 7cdb4d0d81..23ba71bb6b 100644 --- a/_tgstation_8_server_8_host_2_models_2_test_merge_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_test_merge_8cs.html @@ -25,7 +25,7 @@
diff --git a/_tgstation_8_server_8_host_2_models_2_test_merge_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_test_merge_8cs_source.html index c2d4aa1049..b166eaff7a 100644 --- a/_tgstation_8_server_8_host_2_models_2_test_merge_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_test_merge_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_user_8cs.html b/_tgstation_8_server_8_host_2_models_2_user_8cs.html index b59c819668..d9e948d6ab 100644 --- a/_tgstation_8_server_8_host_2_models_2_user_8cs.html +++ b/_tgstation_8_server_8_host_2_models_2_user_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_models_2_user_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_user_8cs_source.html index a0d3bfd4ac..a7baa68f5c 100644 --- a/_tgstation_8_server_8_host_2_models_2_user_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_user_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_properties_2_assembly_info_8cs.html b/_tgstation_8_server_8_host_2_properties_2_assembly_info_8cs.html index 19814a3b80..5ef7e1b182 100644 --- a/_tgstation_8_server_8_host_2_properties_2_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_2_properties_2_assembly_info_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_2_properties_2_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_2_properties_2_assembly_info_8cs_source.html index ed71761df3..df7a6359be 100644 --- a/_tgstation_8_server_8_host_2_properties_2_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_2_properties_2_assembly_info_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_8_console_2_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_console_2_assembly_info_8cs.html index 2ccaad966e..fa5341f87c 100644 --- a/_tgstation_8_server_8_host_8_console_2_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_console_2_assembly_info_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_8_console_2_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_console_2_assembly_info_8cs_source.html index 999013cc7f..d137f3af8c 100644 --- a/_tgstation_8_server_8_host_8_console_2_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_console_2_assembly_info_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_8_service_2_properties_2_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_service_2_properties_2_assembly_info_8cs.html index f774ba8a7f..a53c81f684 100644 --- a/_tgstation_8_server_8_host_8_service_2_properties_2_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_service_2_properties_2_assembly_info_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_8_service_2_properties_2_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_service_2_properties_2_assembly_info_8cs_source.html index c65c907ff1..8b90886b62 100644 --- a/_tgstation_8_server_8_host_8_service_2_properties_2_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_service_2_properties_2_assembly_info_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_8_watchdog_2_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_watchdog_2_assembly_info_8cs.html index d016ffa5e1..e8f9fac212 100644 --- a/_tgstation_8_server_8_host_8_watchdog_2_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_watchdog_2_assembly_info_8cs.html @@ -25,7 +25,7 @@ diff --git a/_tgstation_8_server_8_host_8_watchdog_2_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_watchdog_2_assembly_info_8cs_source.html index 8e369ebc05..2c7fb3234d 100644 --- a/_tgstation_8_server_8_host_8_watchdog_2_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_watchdog_2_assembly_info_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_token_8cs.html b/_token_8cs.html index b25149ee2c..bc8d40651d 100644 --- a/_token_8cs.html +++ b/_token_8cs.html @@ -25,7 +25,7 @@ diff --git a/_token_8cs_source.html b/_token_8cs_source.html index 03ff15c9e4..7444f7f26e 100644 --- a/_token_8cs_source.html +++ b/_token_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_token_factory_8cs.html b/_token_factory_8cs.html index 9df9aa7273..1017823332 100644 --- a/_token_factory_8cs.html +++ b/_token_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_token_factory_8cs_source.html b/_token_factory_8cs_source.html index a275938e82..e98d5dfefd 100644 --- a/_token_factory_8cs_source.html +++ b/_token_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_topic_command_type_8cs.html b/_topic_command_type_8cs.html index c909c6666e..94f27bba2d 100644 --- a/_topic_command_type_8cs.html +++ b/_topic_command_type_8cs.html @@ -25,7 +25,7 @@ diff --git a/_topic_command_type_8cs_source.html b/_topic_command_type_8cs_source.html index 0aeadaf57f..a5bc80b68f 100644 --- a/_topic_command_type_8cs_source.html +++ b/_topic_command_type_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_topic_parameters_8cs.html b/_topic_parameters_8cs.html index 5ef9b59e4b..22b654ff7c 100644 --- a/_topic_parameters_8cs.html +++ b/_topic_parameters_8cs.html @@ -25,7 +25,7 @@ diff --git a/_topic_parameters_8cs_source.html b/_topic_parameters_8cs_source.html index 52731001c7..3506110d52 100644 --- a/_topic_parameters_8cs_source.html +++ b/_topic_parameters_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_topic_response_8cs.html b/_topic_response_8cs.html index 74e89bcdfc..64d0c996b4 100644 --- a/_topic_response_8cs.html +++ b/_topic_response_8cs.html @@ -25,7 +25,7 @@ diff --git a/_topic_response_8cs_source.html b/_topic_response_8cs_source.html index 0a1e5b5331..eb41dc159e 100644 --- a/_topic_response_8cs_source.html +++ b/_topic_response_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_unauthorized_exception_8cs.html b/_unauthorized_exception_8cs.html index 1c08af8512..a29a15450c 100644 --- a/_unauthorized_exception_8cs.html +++ b/_unauthorized_exception_8cs.html @@ -25,7 +25,7 @@ diff --git a/_unauthorized_exception_8cs_source.html b/_unauthorized_exception_8cs_source.html index e863564950..909799dbd2 100644 --- a/_unauthorized_exception_8cs_source.html +++ b/_unauthorized_exception_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_unrecognized_response_exception_8cs.html b/_unrecognized_response_exception_8cs.html index b799aec7f9..b5a2ef1777 100644 --- a/_unrecognized_response_exception_8cs.html +++ b/_unrecognized_response_exception_8cs.html @@ -25,7 +25,7 @@ diff --git a/_unrecognized_response_exception_8cs_source.html b/_unrecognized_response_exception_8cs_source.html index b1b0051538..207c8db4e8 100644 --- a/_unrecognized_response_exception_8cs_source.html +++ b/_unrecognized_response_exception_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_updates_configuration_8cs.html b/_updates_configuration_8cs.html index ae83c90bd2..c42e6a6750 100644 --- a/_updates_configuration_8cs.html +++ b/_updates_configuration_8cs.html @@ -25,7 +25,7 @@ diff --git a/_updates_configuration_8cs_source.html b/_updates_configuration_8cs_source.html index ee369d2189..78bbfd1c09 100644 --- a/_updates_configuration_8cs_source.html +++ b/_updates_configuration_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_user_controller_8cs.html b/_user_controller_8cs.html index cf77d5ee23..90fc6e8772 100644 --- a/_user_controller_8cs.html +++ b/_user_controller_8cs.html @@ -25,7 +25,7 @@ diff --git a/_user_controller_8cs_source.html b/_user_controller_8cs_source.html index 9cd50d3e11..1107fae009 100644 --- a/_user_controller_8cs_source.html +++ b/_user_controller_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,8 +94,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
UserController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Net;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
18 
19 namespace Tgstation.Server.Host.Controllers
20 {
24  [Route(Routes.User)]
25  public sealed class UserController : ApiController
26  {
31 
36 
41 
51  public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger<UserController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true)
52  {
53  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
54  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
55  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
56  }
57 
64  BadRequestObjectResult CheckValidName(UserUpdate model, bool newUser)
65  {
66  var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null;
67  if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name)))
68  return BadRequest(new ErrorMessage(ErrorCode.UserMissingName));
69 
70  model.Name = model.Name?.Trim();
71  if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
72  return BadRequest(new ErrorMessage(ErrorCode.UserColonInName));
73  return null;
74  }
75 
83  BadRequestObjectResult TrySetPassword(Models.User dbUser, string newPassword, bool newUser)
84  {
85  newPassword ??= String.Empty;
86  if (newPassword.Length < generalConfiguration.MinimumPasswordLength)
87  return BadRequest(new ErrorMessage(ErrorCode.UserPasswordLength)
88  {
89  AdditionalData = $"Required password length: {generalConfiguration.MinimumPasswordLength}"
90  });
91  cryptographySuite.SetUserPassword(dbUser, newPassword, newUser);
92  return null;
93  }
94 
104  [HttpPut]
105  [TgsAuthorize(AdministrationRights.WriteUsers)]
106  [ProducesResponseType(typeof(Api.Models.User), 201)]
107  [ProducesResponseType(410)]
108  public async Task<IActionResult> Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
109  {
110  if (model == null)
111  throw new ArgumentNullException(nameof(model));
112 
113  if (!(model.Password == null ^ model.SystemIdentifier == null))
114  return BadRequest(new ErrorMessage(ErrorCode.UserMismatchPasswordSid));
115 
116  model.Name = model.Name?.Trim();
117  if (model.Name?.Length == 0)
118  model.Name = null;
119 
120  if (!(model.Name == null ^ model.SystemIdentifier == null))
121  return BadRequest(new ErrorMessage(ErrorCode.UserMismatchNameSid));
122 
123  var fail = CheckValidName(model, true);
124  if (fail != null)
125  return fail;
126 
127  var dbUser = new Models.User
128  {
129  AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? AdministrationRights.None),
130  CreatedAt = DateTimeOffset.Now,
131  CreatedBy = AuthenticationContext.User,
132  Enabled = model.Enabled ?? false,
133  InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? InstanceManagerRights.None),
134  Name = model.Name,
135  SystemIdentifier = model.SystemIdentifier,
136  InstanceUsers = new List<Models.InstanceUser>()
137  };
138 
139  if (model.SystemIdentifier != null)
140  try
141  {
142  using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken).ConfigureAwait(false);
143  if (sysIdentity == null)
144  return StatusCode((int)HttpStatusCode.Gone);
145  dbUser.Name = sysIdentity.Username;
146  dbUser.SystemIdentifier = sysIdentity.Uid;
147  }
148  catch (NotImplementedException)
149  {
150  return StatusCode((int)HttpStatusCode.NotImplemented, new ErrorMessage(ErrorCode.RequiresPosixSystemIdentity));
151  }
152  else
153  {
154  var result = TrySetPassword(dbUser, model.Password, true);
155  if (result != null)
156  return result;
157  }
158 
159  dbUser.CanonicalName = Models.User.CanonicalizeName(dbUser.Name);
160 
161  DatabaseContext.Users.Add(dbUser);
162 
163  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
164 
165  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true));
166  }
167 
176  [HttpPost]
177  [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
178  [ProducesResponseType(typeof(Api.Models.User), 200)]
179  [ProducesResponseType(404)]
180  #pragma warning disable CA1502 // TODO: Decomplexify
181  #pragma warning disable CA1506
182  public async Task<IActionResult> Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
183  {
184  if (model == null)
185  throw new ArgumentNullException(nameof(model));
186 
187  if (!model.Id.HasValue)
188  return BadRequest(new ErrorMessage(ErrorCode.UserMissingId));
189 
190  var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration);
191  var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers);
192 
193  var originalUser = passwordEditOnly
195  : await DatabaseContext.Users.Where(x => x.Id == model.Id)
196  .Include(x => x.CreatedBy)
197  .FirstOrDefaultAsync(cancellationToken)
198  .ConfigureAwait(false);
199 
200  if (originalUser == default)
201  return NotFound();
202 
203  // Ensure they are only trying to edit password (system identity change will trigger a bad request)
204  if (passwordEditOnly
205  && (model.Id != originalUser.Id
206  || model.InstanceManagerRights.HasValue
207  || model.AdministrationRights.HasValue
208  || model.Enabled.HasValue
209  || model.Name != null))
210  return Forbid();
211 
212  if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
213  return BadRequest(new ErrorMessage(ErrorCode.UserSidChange));
214 
215  if (model.Password != null)
216  {
217  var result = TrySetPassword(originalUser, model.Password, false);
218  if (result != null)
219  return result;
220  }
221 
222  if (model.Name != null && Models.User.CanonicalizeName(model.Name) != originalUser.CanonicalName)
223  return BadRequest(new ErrorMessage(ErrorCode.UserNameChange));
224 
225  originalUser.InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? originalUser.InstanceManagerRights.Value);
226  originalUser.AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? originalUser.AdministrationRights.Value);
227  if (model.Enabled.HasValue)
228  {
229  if (originalUser.Enabled.Value && !model.Enabled.Value)
230  originalUser.LastPasswordUpdate = DateTimeOffset.Now;
231 
232  originalUser.Enabled = model.Enabled.Value;
233  }
234 
235  var fail = CheckValidName(model, false);
236  if (fail != null)
237  return fail;
238 
239  originalUser.Name = model.Name ?? originalUser.Name;
240 
241  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
242 
243  // return id only if not a self update and cannot read users
244  return Json(
245  model.Id == originalUser.Id
246  || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers)
247  ? originalUser.ToApi(true)
248  : new Api.Models.User
249  {
250  Id = originalUser.Id
251  });
252  }
253  #pragma warning restore CA1506
254  #pragma warning restore CA1502
255 
261  [HttpGet]
262  [TgsAuthorize]
263  [ProducesResponseType(typeof(Api.Models.User), 200)]
264  public IActionResult Read() => Json(AuthenticationContext.User.ToApi(true));
265 
272  [HttpGet(Routes.List)]
273  [TgsAuthorize(AdministrationRights.ReadUsers)]
274  [ProducesResponseType(typeof(IEnumerable<Api.Models.User>), 200)]
275  public async Task<IActionResult> List(CancellationToken cancellationToken)
276  {
277  var users = await DatabaseContext.Users
278  .Include(x => x.CreatedBy)
279  .ToListAsync(cancellationToken).ConfigureAwait(false);
280  return Json(users.Select(x => x.ToApi(true)));
281  }
282 
291  [HttpGet("{id}")]
292  [TgsAuthorize]
293  [ProducesResponseType(typeof(Api.Models.User), 200)]
294  [ProducesResponseType(404)]
295  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
296  {
297  if (id == AuthenticationContext.User.Id)
298  return Read();
299 
300  if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
301  return Forbid();
302 
303  var user = await DatabaseContext.Users
304  .Where(x => x.Id == id)
305  .Include(x => x.CreatedBy)
306  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
307  if (user == default)
308  return NotFound();
309  return Json(user.ToApi(true));
310  }
311  }
312 }
string Name
The name of the User
Definition: User.cs:41
-
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Net;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
18 
19 namespace Tgstation.Server.Host.Controllers
20 {
24  [Route(Routes.User)]
25  public sealed class UserController : ApiController
26  {
31 
36 
41 
51  public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger<UserController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false, true)
52  {
53  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
54  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
55  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
56  }
57 
64  BadRequestObjectResult CheckValidName(UserUpdate model, bool newUser)
65  {
66  var userInvalidWithNullName = newUser && model.Name == null && model.SystemIdentifier == null;
67  if (userInvalidWithNullName || (model.Name != null && String.IsNullOrWhiteSpace(model.Name)))
68  return BadRequest(new ErrorMessage(ErrorCode.UserMissingName));
69 
70  model.Name = model.Name?.Trim();
71  if (model.Name != null && model.Name.Contains(':', StringComparison.InvariantCulture))
72  return BadRequest(new ErrorMessage(ErrorCode.UserColonInName));
73  return null;
74  }
75 
83  BadRequestObjectResult TrySetPassword(Models.User dbUser, string newPassword, bool newUser)
84  {
85  newPassword ??= String.Empty;
86  if (newPassword.Length < generalConfiguration.MinimumPasswordLength)
87  return BadRequest(new ErrorMessage(ErrorCode.UserPasswordLength)
88  {
89  AdditionalData = $"Required password length: {generalConfiguration.MinimumPasswordLength}"
90  });
91  cryptographySuite.SetUserPassword(dbUser, newPassword, newUser);
92  return null;
93  }
94 
104  [HttpPut]
105  [TgsAuthorize(AdministrationRights.WriteUsers)]
106  [ProducesResponseType(typeof(Api.Models.User), 201)]
107  [ProducesResponseType(410)]
108  public async Task<IActionResult> Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
109  {
110  if (model == null)
111  throw new ArgumentNullException(nameof(model));
112 
113  if (!(model.Password == null ^ model.SystemIdentifier == null))
114  return BadRequest(new ErrorMessage(ErrorCode.UserMismatchPasswordSid));
115 
116  model.Name = model.Name?.Trim();
117  if (model.Name?.Length == 0)
118  model.Name = null;
119 
120  if (!(model.Name == null ^ model.SystemIdentifier == null))
121  return BadRequest(new ErrorMessage(ErrorCode.UserMismatchNameSid));
122 
123  var fail = CheckValidName(model, true);
124  if (fail != null)
125  return fail;
126 
127  var dbUser = new Models.User
128  {
129  AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? AdministrationRights.None),
130  CreatedAt = DateTimeOffset.Now,
131  CreatedBy = AuthenticationContext.User,
132  Enabled = model.Enabled ?? false,
133  InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? InstanceManagerRights.None),
134  Name = model.Name,
135  SystemIdentifier = model.SystemIdentifier,
136  InstanceUsers = new List<Models.InstanceUser>()
137  };
138 
139  if (model.SystemIdentifier != null)
140  try
141  {
142  using var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken).ConfigureAwait(false);
143  if (sysIdentity == null)
144  return StatusCode((int)HttpStatusCode.Gone);
145  dbUser.Name = sysIdentity.Username;
146  dbUser.SystemIdentifier = sysIdentity.Uid;
147  }
148  catch (NotImplementedException)
149  {
150  return StatusCode((int)HttpStatusCode.NotImplemented, new ErrorMessage(ErrorCode.RequiresPosixSystemIdentity));
151  }
152  else
153  {
154  var result = TrySetPassword(dbUser, model.Password, true);
155  if (result != null)
156  return result;
157  }
158 
159  dbUser.CanonicalName = Models.User.CanonicalizeName(dbUser.Name);
160 
161  DatabaseContext.Users.Add(dbUser);
162 
163  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
164 
165  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true));
166  }
167 
176  [HttpPost]
177  [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
178  [ProducesResponseType(typeof(Api.Models.User), 200)]
179  [ProducesResponseType(404)]
180  #pragma warning disable CA1502 // TODO: Decomplexify
181  #pragma warning disable CA1506
182  public async Task<IActionResult> Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
183  {
184  if (model == null)
185  throw new ArgumentNullException(nameof(model));
186 
187  if (!model.Id.HasValue)
188  return BadRequest(new ErrorMessage(ErrorCode.UserMissingId));
189 
190  var callerAdministrationRights = (AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration);
191  var passwordEditOnly = !callerAdministrationRights.HasFlag(AdministrationRights.WriteUsers);
192 
193  var originalUser = passwordEditOnly
195  : await DatabaseContext
196  .Users
197  .AsQueryable()
198  .Where(x => x.Id == model.Id)
199  .Include(x => x.CreatedBy)
200  .FirstOrDefaultAsync(cancellationToken)
201  .ConfigureAwait(false);
202 
203  if (originalUser == default)
204  return NotFound();
205 
206  // Ensure they are only trying to edit password (system identity change will trigger a bad request)
207  if (passwordEditOnly
208  && (model.Id != originalUser.Id
209  || model.InstanceManagerRights.HasValue
210  || model.AdministrationRights.HasValue
211  || model.Enabled.HasValue
212  || model.Name != null))
213  return Forbid();
214 
215  if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
216  return BadRequest(new ErrorMessage(ErrorCode.UserSidChange));
217 
218  if (model.Password != null)
219  {
220  var result = TrySetPassword(originalUser, model.Password, false);
221  if (result != null)
222  return result;
223  }
224 
225  if (model.Name != null && Models.User.CanonicalizeName(model.Name) != originalUser.CanonicalName)
226  return BadRequest(new ErrorMessage(ErrorCode.UserNameChange));
227 
228  originalUser.InstanceManagerRights = RightsHelper.Clamp(model.InstanceManagerRights ?? originalUser.InstanceManagerRights.Value);
229  originalUser.AdministrationRights = RightsHelper.Clamp(model.AdministrationRights ?? originalUser.AdministrationRights.Value);
230  if (model.Enabled.HasValue)
231  {
232  if (originalUser.Enabled.Value && !model.Enabled.Value)
233  originalUser.LastPasswordUpdate = DateTimeOffset.Now;
234 
235  originalUser.Enabled = model.Enabled.Value;
236  }
237 
238  var fail = CheckValidName(model, false);
239  if (fail != null)
240  return fail;
241 
242  originalUser.Name = model.Name ?? originalUser.Name;
243 
244  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
245 
246  // return id only if not a self update and cannot read users
247  return Json(
248  model.Id == originalUser.Id
249  || callerAdministrationRights.HasFlag(AdministrationRights.ReadUsers)
250  ? originalUser.ToApi(true)
251  : new Api.Models.User
252  {
253  Id = originalUser.Id
254  });
255  }
256  #pragma warning restore CA1506
257  #pragma warning restore CA1502
258 
264  [HttpGet]
265  [TgsAuthorize]
266  [ProducesResponseType(typeof(Api.Models.User), 200)]
267  public IActionResult Read() => Json(AuthenticationContext.User.ToApi(true));
268 
275  [HttpGet(Routes.List)]
276  [TgsAuthorize(AdministrationRights.ReadUsers)]
277  [ProducesResponseType(typeof(IEnumerable<Api.Models.User>), 200)]
278  public async Task<IActionResult> List(CancellationToken cancellationToken)
279  {
280  var users = await DatabaseContext.Users
281  .Include(x => x.CreatedBy)
282  .ToListAsync(cancellationToken).ConfigureAwait(false);
283  return Json(users.Select(x => x.ToApi(true)));
284  }
285 
294  [HttpGet("{id}")]
295  [TgsAuthorize]
296  [ProducesResponseType(typeof(Api.Models.User), 200)]
297  [ProducesResponseType(404)]
298  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
299  {
300  if (id == AuthenticationContext.User.Id)
301  return Read();
302 
303  if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
304  return Forbid();
305 
306  var user = await DatabaseContext.Users
307  .AsQueryable()
308  .Where(x => x.Id == id)
309  .Include(x => x.CreatedBy)
310  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
311  if (user == default)
312  return NotFound();
313  return Json(user.ToApi(true));
314  }
315  }
316 }
string Name
The name of the User
Definition: User.cs:41
+
DbSet< User > Users
The Users in the DatabaseContext.
@@ -114,11 +114,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the UserController
AdministrationRights
Rights for Models.Administration
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
+
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
Contains various cryptographic functions
-
Backend abstract implementation of IDatabaseContext
-
DbSet< User > Users
The Users in the DatabaseContext<TParentContext>.
-
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.User.
+
Backend abstract implementation of IDatabaseContext
+
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.User.
Routes to a server actions
Definition: Routes.cs:9
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the UserController
@@ -132,7 +132,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task< IActionResult > Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
Update a Api.Models.User.
-
async Task< IActionResult > List(CancellationToken cancellationToken)
List all Api.Models.Users in the server.
+
async Task< IActionResult > List(CancellationToken cancellationToken)
List all Api.Models.Users in the server.
For editing a given User. Will never be returned by the API
Definition: UserUpdate.cs:8
const string List
The postfix for list operations
Definition: Routes.cs:84
User can read all chat settings except Models.Internal.ChatBot.ConnectionString
@@ -143,7 +143,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task< IActionResult > Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
Create a Api.Models.User.
string SystemIdentifier
The SID/UID of the User on Windows/POSIX respectively
Definition: User.cs:34
- +
diff --git a/_user_update_8cs.html b/_user_update_8cs.html index b8df7b89c5..7aba12112b 100644 --- a/_user_update_8cs.html +++ b/_user_update_8cs.html @@ -25,7 +25,7 @@
diff --git a/_user_update_8cs_source.html b/_user_update_8cs_source.html index 45c3599e59..13eba4d1f8 100644 --- a/_user_update_8cs_source.html +++ b/_user_update_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_users_client_8cs.html b/_users_client_8cs.html index 2c49af0764..81a9de0319 100644 --- a/_users_client_8cs.html +++ b/_users_client_8cs.html @@ -25,7 +25,7 @@ diff --git a/_users_client_8cs_source.html b/_users_client_8cs_source.html index 6207149d61..6a9eacfdec 100644 --- a/_users_client_8cs_source.html +++ b/_users_client_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_version_command_8cs.html b/_version_command_8cs.html index 81f283e2cb..3959a91869 100644 --- a/_version_command_8cs.html +++ b/_version_command_8cs.html @@ -25,7 +25,7 @@ diff --git a/_version_command_8cs_source.html b/_version_command_8cs_source.html index 6f0e89e046..f40bf5c82f 100644 --- a/_version_command_8cs_source.html +++ b/_version_command_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_version_converter_8cs.html b/_version_converter_8cs.html index 23065da5ec..285107fb3c 100644 --- a/_version_converter_8cs.html +++ b/_version_converter_8cs.html @@ -25,7 +25,7 @@ diff --git a/_version_converter_8cs_source.html b/_version_converter_8cs_source.html index c4ad3ab9de..d1c11364df 100644 --- a/_version_converter_8cs_source.html +++ b/_version_converter_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_version_extensions_8cs.html b/_version_extensions_8cs.html index 4d168398d6..624179a2c0 100644 --- a/_version_extensions_8cs.html +++ b/_version_extensions_8cs.html @@ -25,7 +25,7 @@ diff --git a/_version_extensions_8cs_source.html b/_version_extensions_8cs_source.html index f8b7dbde16..719f9bce72 100644 --- a/_version_extensions_8cs_source.html +++ b/_version_extensions_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_version_mismatch_exception_8cs.html b/_version_mismatch_exception_8cs.html index 5e06fc855e..21adfc8bfa 100644 --- a/_version_mismatch_exception_8cs.html +++ b/_version_mismatch_exception_8cs.html @@ -25,7 +25,7 @@ diff --git a/_version_mismatch_exception_8cs_source.html b/_version_mismatch_exception_8cs_source.html index b5440c9ae8..52f50f5f36 100644 --- a/_version_mismatch_exception_8cs_source.html +++ b/_version_mismatch_exception_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_2_i_watchdog_8cs.html b/_watchdog_2_i_watchdog_8cs.html index fcb4deb1a8..324988a6c7 100644 --- a/_watchdog_2_i_watchdog_8cs.html +++ b/_watchdog_2_i_watchdog_8cs.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_2_i_watchdog_8cs_source.html b/_watchdog_2_i_watchdog_8cs_source.html index 117d34546e..f0a1456f96 100644 --- a/_watchdog_2_i_watchdog_8cs_source.html +++ b/_watchdog_2_i_watchdog_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_2_i_watchdog_factory_8cs.html b/_watchdog_2_i_watchdog_factory_8cs.html index 9a29172539..78df298fa7 100644 --- a/_watchdog_2_i_watchdog_factory_8cs.html +++ b/_watchdog_2_i_watchdog_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_2_i_watchdog_factory_8cs_source.html b/_watchdog_2_i_watchdog_factory_8cs_source.html index 52f3da94a3..6d855b2d37 100644 --- a/_watchdog_2_i_watchdog_factory_8cs_source.html +++ b/_watchdog_2_i_watchdog_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_2_watchdog_factory_8cs.html b/_watchdog_2_watchdog_factory_8cs.html index 16a5beae77..24bb5fe101 100644 --- a/_watchdog_2_watchdog_factory_8cs.html +++ b/_watchdog_2_watchdog_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_2_watchdog_factory_8cs_source.html b/_watchdog_2_watchdog_factory_8cs_source.html index 6d2f0a0d2b..dc9ef14015 100644 --- a/_watchdog_2_watchdog_factory_8cs_source.html +++ b/_watchdog_2_watchdog_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_8cs.html b/_watchdog_8cs.html index 78a057fc09..f5998d3c49 100644 --- a/_watchdog_8cs.html +++ b/_watchdog_8cs.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_8cs_source.html b/_watchdog_8cs_source.html index 5f80a545b1..74c2d6dcc0 100644 --- a/_watchdog_8cs_source.html +++ b/_watchdog_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_base_8cs.html b/_watchdog_base_8cs.html index 8aca1df837..f6866c35b2 100644 --- a/_watchdog_base_8cs.html +++ b/_watchdog_base_8cs.html @@ -25,7 +25,7 @@ diff --git a/_watchdog_base_8cs_source.html b/_watchdog_base_8cs_source.html index 7b7bdf173c..3ac61a7813 100644 --- a/_watchdog_base_8cs_source.html +++ b/_watchdog_base_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,9 +94,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WatchdogBase.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
20 
21 namespace Tgstation.Server.Host.Components.Watchdog
22 {
26  #pragma warning disable CA1506 // TODO: Decomplexify
28  {
30  public bool Running
31  {
32  get => running;
33  set
34  {
35  running = value;
36  Logger.LogTrace("Running set to {0}", running);
37  }
38  }
39 
41  public abstract bool AlphaIsActive { get; }
42 
44  public abstract Models.CompileJob ActiveCompileJob { get; }
45 
47  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; }
48 
50  public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; }
51 
53  public abstract RebootState? RebootState { get; }
54 
58  protected TaskCompletionSource<object> ActiveParametersUpdated { get; set; }
59 
63  protected SemaphoreSlim Semaphore { get; }
64 
68  protected ILogger Logger { get; }
69 
73  protected IChatManager Chat { get; }
74 
79 
83  protected IDmbFactory DmbFactory { get; }
84 
88  protected IAsyncDelayer AsyncDelayer { get; }
89 
93  readonly Api.Models.Instance instance;
94 
99 
104 
109 
114 
118  readonly bool autoStart;
119 
124 
128  CancellationTokenSource monitorCts;
129 
134 
139 
144 
148  bool running;
149 
153  bool disposed;
154 
170  protected WatchdogBase(
171  IChatManager chat,
172  ISessionControllerFactory sessionControllerFactory,
173  IDmbFactory dmbFactory,
174  IReattachInfoHandler reattachInfoHandler,
175  IDatabaseContextFactory databaseContextFactory,
176  IJobManager jobManager,
177  IServerControl serverControl,
178  IAsyncDelayer asyncDelayer,
179  ILogger logger,
180  DreamDaemonLaunchParameters initialLaunchParameters,
181  Api.Models.Instance instance,
182  bool autoStart)
183  {
184  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
185  SessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
186  DmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
187  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
188  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
189  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
190  AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
191  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
192  ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
193  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
194  this.autoStart = autoStart;
195 
196  if (serverControl == null)
197  throw new ArgumentNullException(nameof(serverControl));
198 
199  chat.RegisterCommandHandler(this);
200 
201  ActiveLaunchParameters = initialLaunchParameters;
202  releaseServers = false;
203  ActiveParametersUpdated = new TaskCompletionSource<object>();
204 
205  restartRegistration = serverControl.RegisterForRestart(this);
206  try
207  {
208  Semaphore = new SemaphoreSlim(1);
209  }
210  catch
211  {
212  restartRegistration.Dispose();
213  throw;
214  }
215  }
216 
218  public void Dispose()
219  {
220  Logger.LogTrace("Disposing...");
221  Semaphore.Dispose();
222  restartRegistration.Dispose();
223  DisposeAndNullControllers();
224  monitorCts?.Dispose();
225  disposed = true;
226  }
227 
235  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
236  {
237  if (!Running)
238  return;
239  if (!graceful)
240  {
241  var eventTask = HandleEvent(releaseServers ? EventType.WatchdogDetach : EventType.WatchdogShutdown, null, cancellationToken);
242 
243  var chatTask = announce ? Chat.SendWatchdogMessage("Shutting down...", false, cancellationToken) : Task.CompletedTask;
244 
245  await eventTask.ConfigureAwait(false);
246 
247  await StopMonitor().ConfigureAwait(false);
248 
249  DisposeAndNullControllers();
250 
251  LastLaunchParameters = null;
252 
253  await chatTask.ConfigureAwait(false);
254  return;
255  }
256 
257  // merely set the reboot state
258  var toKill = GetActiveController();
259  if (toKill != null)
260  {
261  await toKill.SetRebootState(Session.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
262  Logger.LogTrace("Graceful termination requested");
263  }
264  else
265  Logger.LogTrace("Could not gracefully terminate as there is no active controller!");
266  }
267 
274  async Task<MonitorAction> HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
275  {
276  Logger.LogTrace("Sending heartbeat to active server...");
277  var response = await activeServer.SendCommand(new TopicParameters(), cancellationToken).ConfigureAwait(false);
278 
279  var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown;
280  if (response == null)
281  {
282  switch (++heartbeatsMissed)
283  {
284  case 1:
285  Logger.LogDebug("DEFCON 4: Watchdog missed first heartbeat!");
286  break;
287  case 2:
288  var message2 = "DEFCON 3: Watchdog has missed 2 heartbeats!";
289  Logger.LogInformation(message2);
290  await Chat.SendWatchdogMessage(message2, true, cancellationToken).ConfigureAwait(false);
291  break;
292  case 3:
293  var actionToTake = shouldShutdown
294  ? "shutdown"
295  : "be restarted";
296  var message3 = $"DEFCON 2: Watchdog has missed 3 heartbeats! If DreamDaemon does not respond to the next one, the watchdog will {actionToTake}!";
297  Logger.LogWarning(message3);
298  await Chat.SendWatchdogMessage(message3, false, cancellationToken).ConfigureAwait(false);
299  break;
300  case 4:
301  var actionTaken = shouldShutdown
302  ? "Shutting down due to graceful termination request"
303  : "Restarting";
304  var message4 = $"DEFCON 1: Four heartbeats have been missed! {actionTaken}...";
305  Logger.LogWarning(message4);
306  DisposeAndNullControllers();
307  await Chat.SendWatchdogMessage(message4, false, cancellationToken).ConfigureAwait(false);
308  return shouldShutdown ? MonitorAction.Exit : MonitorAction.Restart;
309  default:
310  Logger.LogError("Invalid heartbeats missed count: {0}", heartbeatsMissed);
311  break;
312  }
313  }
314  else
315  heartbeatsMissed = 0;
316 
317  return MonitorAction.Continue;
318  }
319 
328  protected async Task LaunchImplNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
329  {
330  Logger.LogTrace("Begin LaunchImplNoLock");
331 
332  if (Running)
333  throw new JobException(ErrorCode.WatchdogRunning);
334 
336  throw new JobException(ErrorCode.WatchdogCompileJobCorrupted);
337 
338  // this is necessary, the monitor could be in it's sleep loop trying to restart, if so cancel THAT monitor and start our own with blackjack and hookers
339  Task chatTask;
340  if (startMonitor && await StopMonitor().ConfigureAwait(false))
341  chatTask = Chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", false, cancellationToken);
342  else if (announce)
343  chatTask = Chat.SendWatchdogMessage(reattachInfo == null ? "Launching..." : "Reattaching...", false, cancellationToken); // simple announce
344  else
345  chatTask = Task.CompletedTask; // no announce
346 
347  // since neither server is running, this is safe to do
348  LastLaunchParameters = ActiveLaunchParameters;
349  heartbeatsMissed = 0;
350 
351  // for when we call ourself and want to not catch thrown exceptions
352  var ignoreNestedException = false;
353  try
354  {
355  await InitControllers(() => ignoreNestedException = true, chatTask, reattachInfo, cancellationToken).ConfigureAwait(false);
356  await chatTask.ConfigureAwait(false);
357 
358  Logger.LogInformation("Launched servers successfully");
359  Running = true;
360 
361  if (startMonitor)
362  {
363  StartMonitor();
364  }
365  }
366  catch (Exception e)
367  {
368  // don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
369  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
370  {
371  var originalChatTask = chatTask;
372  async Task ChainChatTaskWithErrorMessage()
373  {
374  await originalChatTask.ConfigureAwait(false);
375  await Chat.SendWatchdogMessage("Startup failed!", false, cancellationToken).ConfigureAwait(false);
376  }
377 
378  chatTask = ChainChatTaskWithErrorMessage();
379  Logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
380  }
381 
382  throw;
383  }
384  finally
385  {
386  // finish the chat task that's in flight
387  try
388  {
389  await chatTask.ConfigureAwait(false);
390  }
391  catch (OperationCanceledException) { }
392  }
393  }
394 
398  protected void StartMonitor()
399  {
400  monitorCts = new CancellationTokenSource();
401  monitorTask = MonitorLifetimes(monitorCts.Token);
402  }
403 
408  protected async Task<bool> StopMonitor()
409  {
410  Logger.LogTrace("StopMonitor");
411  if (monitorTask == null)
412  return false;
413  var wasRunning = !monitorTask.IsCompleted;
414  monitorCts.Cancel();
415  await monitorTask.ConfigureAwait(false);
416  Logger.LogTrace("Stopped Monitor");
417  monitorCts.Dispose();
418  monitorTask = null;
419  monitorCts = null;
420  return wasRunning;
421  }
422 
429  protected async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken)
430  {
431  // we lost the server, just restart entirely
432  DisposeAndNullControllers();
433  const string FailReattachMessage = "Unable to properly reattach to server! Restarting...";
434  Logger.LogWarning(FailReattachMessage);
435  Logger.LogDebug(inactiveReattachSuccess ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
436  Task chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken);
437  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
438  await chatTask.ConfigureAwait(false);
439  }
440 
448  protected async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
449  {
450  var launchResult = await controller.LaunchResult.WithToken(cancellationToken).ConfigureAwait(false);
451 
452  // Dead sessions won't trigger this
453  if (launchResult.ExitCode.HasValue) // you killed us ray...
454  throw new JobException(
455  ErrorCode.WatchdogStartupFailed,
456  new JobException($"{serverName} failed to start: {launchResult}"));
457  if (!launchResult.StartupTime.HasValue)
458  throw new JobException(
459  ErrorCode.WatchdogStartupTimeout,
460  new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s"));
461  }
462 
466  protected abstract void DisposeAndNullControllers();
467 
472  protected abstract ISessionController GetActiveController();
473 
478  protected abstract DualReattachInformation CreateReattachInformation();
479 
486  protected abstract IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState);
487 
495  protected abstract Task HandleMonitorWakeup(
496  MonitorActivationReason activationReason,
497  MonitorState monitorState,
498  CancellationToken cancellationToken);
499 
500  private async Task<MonitorState> MonitorRestart(CancellationToken cancellationToken)
501  {
502  Logger.LogTrace("Monitor restart!");
503  DisposeAndNullControllers();
504 
505  var chatTask = Task.CompletedTask;
506  for (var retryAttempts = 1; ; ++retryAttempts)
507  {
508  Exception launchException = null;
509  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
510  try
511  {
512  // use LaunchImplNoLock without announcements or restarting the monitor
513  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
514  if (Running)
515  {
516  Logger.LogDebug("Relaunch successful, resetting monitor state...");
517  return new MonitorState();
518  }
519  }
520  catch (OperationCanceledException)
521  {
522  throw;
523  }
524  catch (Exception e)
525  {
526  launchException = e;
527  }
528 
529  await chatTask.ConfigureAwait(false);
530  if (!Running)
531  {
532  if (launchException == null)
533  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
534  else
535  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
536  var retryDelay = Math.Min(
537  Convert.ToInt32(
538  Math.Pow(2, retryAttempts)),
539  TimeSpan.FromHours(1).Seconds); // max of one hour, increasing by a power of 2 each time
540  chatTask = Chat.SendWatchdogMessage(
541  $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}",
542  false,
543  cancellationToken);
544  await Task.WhenAll(
546  TimeSpan.FromSeconds(retryDelay),
547  cancellationToken),
548  chatTask)
549  .ConfigureAwait(false);
550  }
551  }
552  }
553 
559  private async Task MonitorLifetimes(CancellationToken cancellationToken)
560  {
561  Logger.LogTrace("Entered MonitorLifetimes");
562  using var _ = cancellationToken.Register(() => Logger.LogTrace("Monitor cancellationToken triggered"));
563 
564  // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
565  var iteration = 1;
566  try
567  {
568  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
569  try
570  {
571  Logger.LogDebug("Iteration {0} of monitor loop", iteration);
572 
573  // load the activation tasks into local variables
574  var serverTasks = GetMonitoredServerTasks(monitorState);
575  if (serverTasks.Count != 5)
576  throw new InvalidOperationException("Expected 5 monitored server tasks!");
577 
578  var activeServerLifetime = serverTasks[MonitorActivationReason.ActiveServerCrashed];
579  var activeServerReboot = serverTasks[MonitorActivationReason.ActiveServerRebooted];
580  var inactiveServerLifetime = serverTasks[MonitorActivationReason.InactiveServerCrashed];
581  var inactiveServerReboot = serverTasks[MonitorActivationReason.InactiveServerRebooted];
582  var inactiveStartupComplete = serverTasks[MonitorActivationReason.InactiveServerStartupComplete];
583 
584  Task activeLaunchParametersChanged = ActiveParametersUpdated.Task;
585  var newDmbAvailable = DmbFactory.OnNewerDmb;
586 
587  var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value;
588  var heartbeat = heartbeatSeconds == 0
589  ? Extensions.TaskExtensions.InfiniteTask()
590  : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds));
591 
592  // cancel waiting if requested
593  var cancelTcs = new TaskCompletionSource<object>();
594  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
595  {
596  var toWaitOn = Task.WhenAny(
597  activeServerLifetime,
598  activeServerReboot,
599  inactiveServerLifetime,
600  inactiveServerReboot,
601  inactiveStartupComplete,
602  heartbeat,
603  newDmbAvailable,
604  cancelTcs.Task,
605  activeLaunchParametersChanged);
606 
607  // wait for something to happen
608  await toWaitOn.ConfigureAwait(false);
609  cancellationToken.ThrowIfCancellationRequested();
610  }
611 
612  Logger.LogTrace("Monitor activated");
613 
614  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
615  {
616  // always run HandleMonitorWakeup from the context of the semaphore lock
617  // multiple things may have happened, handle them one at a time
618  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
619  {
620  MonitorActivationReason activationReason = default; // this will always be assigned before being used
621 
622  // process the tasks in this order and call HandlerMonitorWakup for each
623  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
624  {
625  var taskCompleted = task?.IsCompleted == true;
626  task = null;
627  if (monitorState.NextAction == MonitorAction.Skip)
628  monitorState.NextAction = MonitorAction.Continue;
629  else if (taskCompleted)
630  {
631  activationReason = testActivationReason;
632  return true;
633  }
634 
635  return false;
636  }
637 
638  var anyActivation = CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
639  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
640  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
641  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
642  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
643  || CheckActivationReason(ref inactiveStartupComplete, MonitorActivationReason.InactiveServerStartupComplete)
644  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated)
645  || CheckActivationReason(ref heartbeat, MonitorActivationReason.Heartbeat);
646 
647  if (!anyActivation)
648  moreActivationsToProcess = false;
649  else
650  {
651  Logger.LogTrace("Reason: {0}", activationReason);
652  if (activationReason == MonitorActivationReason.Heartbeat)
653  monitorState.NextAction = await HandleHeartbeat(
654  monitorState.ActiveServer,
655  cancellationToken)
656  .ConfigureAwait(false);
657  else
658  await HandleMonitorWakeup(
659  activationReason,
660  monitorState,
661  cancellationToken)
662  .ConfigureAwait(false);
663  }
664  }
665  }
666 
667  Logger.LogTrace("Next monitor action is to {0}", monitorState.NextAction);
668  if (monitorState.NextAction == MonitorAction.Restart)
669  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
670  }
671  catch (OperationCanceledException)
672  {
673  throw;
674  }
675  catch (Exception e)
676  {
677  // really, this should NEVER happen
678  Logger.LogError(
679  "Monitor crashed! Iteration: {0}, Monitor State: {1}, Exception: {2}",
680  iteration,
681  JsonConvert.SerializeObject(monitorState),
682  e);
683 
684  var nextActionMessage = monitorState.NextAction != MonitorAction.Exit
685  ? "Restarting"
686  : "Shutting down";
687  var chatTask = Chat.SendWatchdogMessage(
688  $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}",
689  false,
690  cancellationToken);
691 
692  if (disposed)
693  monitorState.NextAction = MonitorAction.Exit;
694  else if (monitorState.NextAction != MonitorAction.Exit)
695  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
696 
697  await chatTask.ConfigureAwait(false);
698  }
699  }
700  catch (OperationCanceledException)
701  {
702  Logger.LogDebug("Monitor cancelled");
703 
704  if (releaseServers)
705  {
706  Logger.LogTrace("Detaching servers...");
707  releasedReattachInformation = CreateReattachInformation();
708  }
709  }
710 
711  DisposeAndNullControllers();
712 
713  Logger.LogTrace("Monitor exiting...");
714  }
715 
724  protected abstract Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken);
725 
727  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
728  {
729  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
730  {
731  if (launchParameters.Match(ActiveLaunchParameters))
732  return;
733  ActiveLaunchParameters = launchParameters;
734  if (Running)
735  ActiveParametersUpdated.TrySetResult(null); // queue an update
736  }
737  }
738 
740  public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
741  {
742  var activeServer = GetActiveController();
743 
744  // Server may have ended
745  if (activeServer == null)
746  return;
747 
748  var notification = new EventNotification(eventType, parameters);
749  var result = await activeServer.SendCommand(
750  new TopicParameters(notification),
751  cancellationToken)
752  .ConfigureAwait(false);
753 
754  if (result?.InteropResponse?.ChatResponses != null)
755  await Task.WhenAll(
756  result.InteropResponse.ChatResponses.Select(
757  x => Chat.SendMessage(
758  x.Text,
759  x.ChannelIds
760  .Select(channelIdString =>
761  {
762  if (UInt64.TryParse(channelIdString, out var channelId))
763  return (ulong?)channelId;
764 
765  return null;
766  })
767  .Where(nullableChannelId => nullableChannelId.HasValue)
768  .Select(nullableChannelId => nullableChannelId.Value),
769  cancellationToken)))
770  .ConfigureAwait(false);
771  }
772 
774  public async Task<string> HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
775  {
776  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
777  {
778  if (!Running)
779  return "ERROR: Server offline!";
780 
781  var commandObject = new ChatCommand(sender, commandName, arguments);
782 
783  var command = new TopicParameters(commandObject);
784 
785  var activeServer = GetActiveController();
786  var commandResult = await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false);
787 
788  if (commandResult == null)
789  return "TGS: Bad topic exchange!";
790 
791  if (commandResult.InteropResponse == null)
792  return "TGS: Bad topic response!";
793 
794  return commandResult.InteropResponse.CommandResponseMessage ??
795  "TGS: Command processed but no DMAPI response returned!";
796  }
797  }
798 
800  public async Task Launch(CancellationToken cancellationToken)
801  {
802  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
803  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
804  }
805 
807  public virtual async Task ResetRebootState(CancellationToken cancellationToken)
808  {
809  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
810  {
811  if (!Running)
812  return;
813  var toClear = GetActiveController();
814  if (toClear != null)
815  await toClear.SetRebootState(Session.RebootState.Normal, cancellationToken).ConfigureAwait(false);
816  }
817  }
818 
820  public async Task Restart(bool graceful, CancellationToken cancellationToken)
821  {
822  Logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
823  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
824  {
825  if (!graceful || !Running)
826  {
827  Task chatTask;
828  if (Running)
829  {
830  chatTask = Chat.SendWatchdogMessage("Manual restart triggered...", false, cancellationToken);
831  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
832  }
833  else
834  chatTask = Task.CompletedTask;
835  await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
836  await chatTask.ConfigureAwait(false);
837  }
838 
839  var toReboot = GetActiveController();
840  if (toReboot != null)
841  {
842  if (!await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false))
843  Logger.LogWarning("Unable to send reboot state change event!");
844  }
845  }
846  }
847 
849  public async Task StartAsync(CancellationToken cancellationToken)
850  {
851  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
852  if (!autoStart && reattachInfo == null)
853  return;
854 
855  long? adminUserId = null;
856 
857  await databaseContextFactory.UseContext(
858  async db => adminUserId = await db
859  .Users
860  .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
861  .Select(x => x.Id)
862  .FirstAsync(cancellationToken)
863  .ConfigureAwait(false))
864  .ConfigureAwait(false);
865  var job = new Models.Job
866  {
867  StartedBy = new Models.User
868  {
869  Id = adminUserId.Value
870  },
871  Instance = new Models.Instance
872  {
873  Id = instance.Id
874  },
875  Description = $"Instance startup watchdog {(reattachInfo != null ? "reattach" : "launch")}",
876  CancelRight = (ulong)DreamDaemonRights.Shutdown,
877  CancelRightsType = RightsType.DreamDaemon
878  };
879  await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) =>
880  {
881  using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false))
882  await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
883  }, cancellationToken).ConfigureAwait(false);
884  }
885 
887  public async Task StopAsync(CancellationToken cancellationToken)
888  {
889  if (releaseServers)
890  {
891  await StopMonitor().ConfigureAwait(false);
892  if (releasedReattachInformation != null)
893  await reattachInfoHandler.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(false);
894  }
895 
896  await TerminateNoLock(false, !releaseServers, cancellationToken).ConfigureAwait(false);
897  }
898 
900  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
901  {
902  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
903  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
904  }
905 
907  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
908  {
909  releaseServers = true;
910  if (Running)
911  await Chat.SendWatchdogMessage("Detaching...", false, cancellationToken).ConfigureAwait(false);
912  }
913  }
914 }
readonly bool autoStart
If the WatchdogBase should LaunchImplNoLock(bool, bool, DualReattachInformation, CancellationToken) i...
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
20 
21 namespace Tgstation.Server.Host.Components.Watchdog
22 {
26  #pragma warning disable CA1506 // TODO: Decomplexify
28  {
30  public bool Running
31  {
32  get => running;
33  set
34  {
35  running = value;
36  Logger.LogTrace("Running set to {0}", running);
37  }
38  }
39 
41  public abstract bool AlphaIsActive { get; }
42 
44  public abstract Models.CompileJob ActiveCompileJob { get; }
45 
47  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; }
48 
50  public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; }
51 
53  public abstract RebootState? RebootState { get; }
54 
58  protected TaskCompletionSource<object> ActiveParametersUpdated { get; set; }
59 
63  protected SemaphoreSlim Semaphore { get; }
64 
68  protected ILogger Logger { get; }
69 
73  protected IChatManager Chat { get; }
74 
79 
83  protected IDmbFactory DmbFactory { get; }
84 
88  protected IAsyncDelayer AsyncDelayer { get; }
89 
93  readonly Api.Models.Instance instance;
94 
99 
104 
109 
114 
118  readonly object controllerDisposeLock;
119 
123  readonly bool autoStart;
124 
129 
133  CancellationTokenSource monitorCts;
134 
139 
144 
149 
153  bool running;
154 
158  bool disposed;
159 
175  protected WatchdogBase(
176  IChatManager chat,
177  ISessionControllerFactory sessionControllerFactory,
178  IDmbFactory dmbFactory,
179  IReattachInfoHandler reattachInfoHandler,
180  IDatabaseContextFactory databaseContextFactory,
181  IJobManager jobManager,
182  IServerControl serverControl,
183  IAsyncDelayer asyncDelayer,
184  ILogger logger,
185  DreamDaemonLaunchParameters initialLaunchParameters,
186  Api.Models.Instance instance,
187  bool autoStart)
188  {
189  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
190  SessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
191  DmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
192  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
193  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
194  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
195  AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
196  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
197  ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
198  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
199  this.autoStart = autoStart;
200 
201  if (serverControl == null)
202  throw new ArgumentNullException(nameof(serverControl));
203 
204  chat.RegisterCommandHandler(this);
205 
206  ActiveLaunchParameters = initialLaunchParameters;
207  releaseServers = false;
208  ActiveParametersUpdated = new TaskCompletionSource<object>();
209  controllerDisposeLock = new object();
210 
211  restartRegistration = serverControl.RegisterForRestart(this);
212  try
213  {
214  Semaphore = new SemaphoreSlim(1);
215  }
216  catch
217  {
218  restartRegistration.Dispose();
219  throw;
220  }
221  }
222 
224  public void Dispose()
225  {
226  Logger.LogTrace("Disposing...");
227  Semaphore.Dispose();
228  restartRegistration.Dispose();
229  DisposeAndNullControllers();
230  monitorCts?.Dispose();
231  disposed = true;
232  }
233 
241  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
242  {
243  if (!Running)
244  return;
245  if (!graceful)
246  {
247  var eventTask = HandleEvent(releaseServers ? EventType.WatchdogDetach : EventType.WatchdogShutdown, null, cancellationToken);
248 
249  var chatTask = announce ? Chat.SendWatchdogMessage("Shutting down...", false, cancellationToken) : Task.CompletedTask;
250 
251  await eventTask.ConfigureAwait(false);
252 
253  await StopMonitor().ConfigureAwait(false);
254 
255  DisposeAndNullControllers();
256 
257  LastLaunchParameters = null;
258 
259  await chatTask.ConfigureAwait(false);
260  return;
261  }
262 
263  // merely set the reboot state
264  var toKill = GetActiveController();
265  if (toKill != null)
266  {
267  await toKill.SetRebootState(Session.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
268  Logger.LogTrace("Graceful termination requested");
269  }
270  else
271  Logger.LogTrace("Could not gracefully terminate as there is no active controller!");
272  }
273 
280  async Task<MonitorAction> HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
281  {
282  Logger.LogTrace("Sending heartbeat to active server...");
283  var response = await activeServer.SendCommand(new TopicParameters(), cancellationToken).ConfigureAwait(false);
284 
285  var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown;
286  if (response == null)
287  {
288  switch (++heartbeatsMissed)
289  {
290  case 1:
291  Logger.LogDebug("DEFCON 4: Watchdog missed first heartbeat!");
292  break;
293  case 2:
294  var message2 = "DEFCON 3: Watchdog has missed 2 heartbeats!";
295  Logger.LogInformation(message2);
296  await Chat.SendWatchdogMessage(message2, true, cancellationToken).ConfigureAwait(false);
297  break;
298  case 3:
299  var actionToTake = shouldShutdown
300  ? "shutdown"
301  : "be restarted";
302  var message3 = $"DEFCON 2: Watchdog has missed 3 heartbeats! If DreamDaemon does not respond to the next one, the watchdog will {actionToTake}!";
303  Logger.LogWarning(message3);
304  await Chat.SendWatchdogMessage(message3, false, cancellationToken).ConfigureAwait(false);
305  break;
306  case 4:
307  var actionTaken = shouldShutdown
308  ? "Shutting down due to graceful termination request"
309  : "Restarting";
310  var message4 = $"DEFCON 1: Four heartbeats have been missed! {actionTaken}...";
311  Logger.LogWarning(message4);
312  DisposeAndNullControllers();
313  await Chat.SendWatchdogMessage(message4, false, cancellationToken).ConfigureAwait(false);
314  return shouldShutdown ? MonitorAction.Exit : MonitorAction.Restart;
315  default:
316  Logger.LogError("Invalid heartbeats missed count: {0}", heartbeatsMissed);
317  break;
318  }
319  }
320  else
321  heartbeatsMissed = 0;
322 
323  return MonitorAction.Continue;
324  }
325 
334  protected async Task LaunchImplNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
335  {
336  Logger.LogTrace("Begin LaunchImplNoLock");
337 
338  if (Running)
339  throw new JobException(ErrorCode.WatchdogRunning);
340 
342  throw new JobException(ErrorCode.WatchdogCompileJobCorrupted);
343 
344  // this is necessary, the monitor could be in it's sleep loop trying to restart, if so cancel THAT monitor and start our own with blackjack and hookers
345  Task chatTask;
346  if (startMonitor && await StopMonitor().ConfigureAwait(false))
347  chatTask = Chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", false, cancellationToken);
348  else if (announce)
349  chatTask = Chat.SendWatchdogMessage(reattachInfo == null ? "Launching..." : "Reattaching...", false, cancellationToken); // simple announce
350  else
351  chatTask = Task.CompletedTask; // no announce
352 
353  // since neither server is running, this is safe to do
354  LastLaunchParameters = ActiveLaunchParameters;
355  heartbeatsMissed = 0;
356 
357  // for when we call ourself and want to not catch thrown exceptions
358  var ignoreNestedException = false;
359  try
360  {
361  await InitControllers(() => ignoreNestedException = true, chatTask, reattachInfo, cancellationToken).ConfigureAwait(false);
362  await chatTask.ConfigureAwait(false);
363 
364  Logger.LogInformation("Launched servers successfully");
365  Running = true;
366 
367  if (startMonitor)
368  {
369  StartMonitor();
370  }
371  }
372  catch (Exception e)
373  {
374  // don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
375  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
376  {
377  var originalChatTask = chatTask;
378  async Task ChainChatTaskWithErrorMessage()
379  {
380  await originalChatTask.ConfigureAwait(false);
381  await Chat.SendWatchdogMessage("Startup failed!", false, cancellationToken).ConfigureAwait(false);
382  }
383 
384  chatTask = ChainChatTaskWithErrorMessage();
385  Logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
386  }
387 
388  throw;
389  }
390  finally
391  {
392  // finish the chat task that's in flight
393  try
394  {
395  await chatTask.ConfigureAwait(false);
396  }
397  catch (OperationCanceledException) { }
398  }
399  }
400 
404  protected void StartMonitor()
405  {
406  monitorCts = new CancellationTokenSource();
407  monitorTask = MonitorLifetimes(monitorCts.Token);
408  }
409 
414  protected async Task<bool> StopMonitor()
415  {
416  Logger.LogTrace("StopMonitor");
417  if (monitorTask == null)
418  return false;
419  var wasRunning = !monitorTask.IsCompleted;
420  monitorCts.Cancel();
421  await monitorTask.ConfigureAwait(false);
422  Logger.LogTrace("Stopped Monitor");
423  monitorCts.Dispose();
424  monitorTask = null;
425  monitorCts = null;
426  return wasRunning;
427  }
428 
435  protected async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken)
436  {
437  // we lost the server, just restart entirely
438  DisposeAndNullControllers();
439  const string FailReattachMessage = "Unable to properly reattach to server! Restarting...";
440  Logger.LogWarning(FailReattachMessage);
441  Logger.LogDebug(inactiveReattachSuccess ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
442  Task chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken);
443  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
444  await chatTask.ConfigureAwait(false);
445  }
446 
454  protected async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
455  {
456  var launchResult = await controller.LaunchResult.WithToken(cancellationToken).ConfigureAwait(false);
457 
458  // Dead sessions won't trigger this
459  if (launchResult.ExitCode.HasValue) // you killed us ray...
460  throw new JobException(
461  ErrorCode.WatchdogStartupFailed,
462  new JobException($"{serverName} failed to start: {launchResult}"));
463  if (!launchResult.StartupTime.HasValue)
464  throw new JobException(
465  ErrorCode.WatchdogStartupTimeout,
466  new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s"));
467  }
468 
472  protected abstract void DisposeAndNullControllersImpl();
473 
477  protected void DisposeAndNullControllers()
478  {
479  lock (controllerDisposeLock)
480  DisposeAndNullControllersImpl();
481  }
482 
487  protected abstract ISessionController GetActiveController();
488 
493  protected abstract DualReattachInformation CreateReattachInformation();
494 
501  protected abstract IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState);
502 
510  protected abstract Task HandleMonitorWakeup(
511  MonitorActivationReason activationReason,
512  MonitorState monitorState,
513  CancellationToken cancellationToken);
514 
515  private async Task<MonitorState> MonitorRestart(CancellationToken cancellationToken)
516  {
517  Logger.LogTrace("Monitor restart!");
518  DisposeAndNullControllers();
519 
520  var chatTask = Task.CompletedTask;
521  for (var retryAttempts = 1; ; ++retryAttempts)
522  {
523  Exception launchException = null;
524  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
525  try
526  {
527  // use LaunchImplNoLock without announcements or restarting the monitor
528  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
529  if (Running)
530  {
531  Logger.LogDebug("Relaunch successful, resetting monitor state...");
532  return new MonitorState();
533  }
534  }
535  catch (OperationCanceledException)
536  {
537  throw;
538  }
539  catch (Exception e)
540  {
541  launchException = e;
542  }
543 
544  await chatTask.ConfigureAwait(false);
545  if (!Running)
546  {
547  if (launchException == null)
548  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
549  else
550  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
551  var retryDelay = Math.Min(
552  Convert.ToInt32(
553  Math.Pow(2, retryAttempts)),
554  TimeSpan.FromHours(1).Seconds); // max of one hour, increasing by a power of 2 each time
555  chatTask = Chat.SendWatchdogMessage(
556  $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}",
557  false,
558  cancellationToken);
559  await Task.WhenAll(
561  TimeSpan.FromSeconds(retryDelay),
562  cancellationToken),
563  chatTask)
564  .ConfigureAwait(false);
565  }
566  }
567  }
568 
574  private async Task MonitorLifetimes(CancellationToken cancellationToken)
575  {
576  Logger.LogTrace("Entered MonitorLifetimes");
577  using var _ = cancellationToken.Register(() => Logger.LogTrace("Monitor cancellationToken triggered"));
578 
579  // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
580  var iteration = 1;
581  try
582  {
583  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
584  try
585  {
586  Logger.LogDebug("Iteration {0} of monitor loop", iteration);
587 
588  // load the activation tasks into local variables
589  var serverTasks = GetMonitoredServerTasks(monitorState);
590  if (serverTasks.Count != 5)
591  throw new InvalidOperationException("Expected 5 monitored server tasks!");
592 
593  var activeServerLifetime = serverTasks[MonitorActivationReason.ActiveServerCrashed];
594  var activeServerReboot = serverTasks[MonitorActivationReason.ActiveServerRebooted];
595  var inactiveServerLifetime = serverTasks[MonitorActivationReason.InactiveServerCrashed];
596  var inactiveServerReboot = serverTasks[MonitorActivationReason.InactiveServerRebooted];
597  var inactiveStartupComplete = serverTasks[MonitorActivationReason.InactiveServerStartupComplete];
598 
599  Task activeLaunchParametersChanged = ActiveParametersUpdated.Task;
600  var newDmbAvailable = DmbFactory.OnNewerDmb;
601 
602  var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value;
603  var heartbeat = heartbeatSeconds == 0
604  ? Extensions.TaskExtensions.InfiniteTask()
605  : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds));
606 
607  // cancel waiting if requested
608  var cancelTcs = new TaskCompletionSource<object>();
609  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
610  {
611  var toWaitOn = Task.WhenAny(
612  activeServerLifetime,
613  activeServerReboot,
614  inactiveServerLifetime,
615  inactiveServerReboot,
616  inactiveStartupComplete,
617  heartbeat,
618  newDmbAvailable,
619  cancelTcs.Task,
620  activeLaunchParametersChanged);
621 
622  // wait for something to happen
623  await toWaitOn.ConfigureAwait(false);
624  cancellationToken.ThrowIfCancellationRequested();
625  }
626 
627  Logger.LogTrace("Monitor activated");
628 
629  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
630  {
631  // always run HandleMonitorWakeup from the context of the semaphore lock
632  // multiple things may have happened, handle them one at a time
633  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
634  {
635  MonitorActivationReason activationReason = default; // this will always be assigned before being used
636 
637  // process the tasks in this order and call HandlerMonitorWakup for each
638  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
639  {
640  var taskCompleted = task?.IsCompleted == true;
641  task = null;
642  if (monitorState.NextAction == MonitorAction.Skip)
643  monitorState.NextAction = MonitorAction.Continue;
644  else if (taskCompleted)
645  {
646  activationReason = testActivationReason;
647  return true;
648  }
649 
650  return false;
651  }
652 
653  var anyActivation = CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
654  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
655  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
656  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
657  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
658  || CheckActivationReason(ref inactiveStartupComplete, MonitorActivationReason.InactiveServerStartupComplete)
659  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated)
660  || CheckActivationReason(ref heartbeat, MonitorActivationReason.Heartbeat);
661 
662  if (!anyActivation)
663  moreActivationsToProcess = false;
664  else
665  {
666  Logger.LogTrace("Reason: {0}", activationReason);
667  if (activationReason == MonitorActivationReason.Heartbeat)
668  monitorState.NextAction = await HandleHeartbeat(
669  monitorState.ActiveServer,
670  cancellationToken)
671  .ConfigureAwait(false);
672  else
673  await HandleMonitorWakeup(
674  activationReason,
675  monitorState,
676  cancellationToken)
677  .ConfigureAwait(false);
678  }
679  }
680  }
681 
682  Logger.LogTrace("Next monitor action is to {0}", monitorState.NextAction);
683  if (monitorState.NextAction == MonitorAction.Restart)
684  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
685  }
686  catch (OperationCanceledException)
687  {
688  throw;
689  }
690  catch (Exception e)
691  {
692  // really, this should NEVER happen
693  Logger.LogError(
694  "Monitor crashed! Iteration: {0}, Monitor State: {1}, Exception: {2}",
695  iteration,
696  JsonConvert.SerializeObject(monitorState),
697  e);
698 
699  var nextActionMessage = monitorState.NextAction != MonitorAction.Exit
700  ? "Restarting"
701  : "Shutting down";
702  var chatTask = Chat.SendWatchdogMessage(
703  $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}",
704  false,
705  cancellationToken);
706 
707  if (disposed)
708  monitorState.NextAction = MonitorAction.Exit;
709  else if (monitorState.NextAction != MonitorAction.Exit)
710  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
711 
712  await chatTask.ConfigureAwait(false);
713  }
714  }
715  catch (OperationCanceledException)
716  {
717  Logger.LogDebug("Monitor cancelled");
718 
719  if (releaseServers)
720  {
721  Logger.LogTrace("Detaching servers...");
722  releasedReattachInformation = CreateReattachInformation();
723  }
724  }
725 
726  DisposeAndNullControllers();
727 
728  Logger.LogTrace("Monitor exiting...");
729  }
730 
739  protected abstract Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken);
740 
742  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
743  {
744  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
745  {
746  if (launchParameters.Match(ActiveLaunchParameters))
747  return;
748  ActiveLaunchParameters = launchParameters;
749  if (Running)
750  ActiveParametersUpdated.TrySetResult(null); // queue an update
751  }
752  }
753 
755  public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
756  {
757  var activeServer = GetActiveController();
758 
759  // Server may have ended
760  if (activeServer == null)
761  return;
762 
763  var notification = new EventNotification(eventType, parameters);
764  var result = await activeServer.SendCommand(
765  new TopicParameters(notification),
766  cancellationToken)
767  .ConfigureAwait(false);
768 
769  if (result?.InteropResponse?.ChatResponses != null)
770  await Task.WhenAll(
771  result.InteropResponse.ChatResponses.Select(
772  x => Chat.SendMessage(
773  x.Text,
774  x.ChannelIds
775  .Select(channelIdString =>
776  {
777  if (UInt64.TryParse(channelIdString, out var channelId))
778  return (ulong?)channelId;
779 
780  return null;
781  })
782  .Where(nullableChannelId => nullableChannelId.HasValue)
783  .Select(nullableChannelId => nullableChannelId.Value),
784  cancellationToken)))
785  .ConfigureAwait(false);
786  }
787 
789  public async Task<string> HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
790  {
791  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
792  {
793  if (!Running)
794  return "ERROR: Server offline!";
795 
796  var commandObject = new ChatCommand(sender, commandName, arguments);
797 
798  var command = new TopicParameters(commandObject);
799 
800  var activeServer = GetActiveController();
801  var commandResult = await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false);
802 
803  if (commandResult == null)
804  return "TGS: Bad topic exchange!";
805 
806  if (commandResult.InteropResponse == null)
807  return "TGS: Bad topic response!";
808 
809  return commandResult.InteropResponse.CommandResponseMessage ??
810  "TGS: Command processed but no DMAPI response returned!";
811  }
812  }
813 
815  public async Task Launch(CancellationToken cancellationToken)
816  {
817  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
818  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
819  }
820 
822  public virtual async Task ResetRebootState(CancellationToken cancellationToken)
823  {
824  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
825  {
826  if (!Running)
827  return;
828  var toClear = GetActiveController();
829  if (toClear != null)
830  await toClear.SetRebootState(Session.RebootState.Normal, cancellationToken).ConfigureAwait(false);
831  }
832  }
833 
835  public async Task Restart(bool graceful, CancellationToken cancellationToken)
836  {
837  Logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
838  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
839  {
840  if (!graceful || !Running)
841  {
842  Task chatTask;
843  if (Running)
844  {
845  chatTask = Chat.SendWatchdogMessage("Manual restart triggered...", false, cancellationToken);
846  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
847  }
848  else
849  chatTask = Task.CompletedTask;
850  await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
851  await chatTask.ConfigureAwait(false);
852  }
853 
854  var toReboot = GetActiveController();
855  if (toReboot != null)
856  {
857  if (!await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false))
858  Logger.LogWarning("Unable to send reboot state change event!");
859  }
860  }
861  }
862 
864  public async Task StartAsync(CancellationToken cancellationToken)
865  {
866  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
867  if (!autoStart && reattachInfo == null)
868  return;
869 
870  long? adminUserId = null;
871 
872  await databaseContextFactory.UseContext(
873  async db => adminUserId = await db
874  .Users
875  .AsQueryable()
876  .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
877  .Select(x => x.Id)
878  .FirstAsync(cancellationToken)
879  .ConfigureAwait(false))
880  .ConfigureAwait(false);
881  var job = new Models.Job
882  {
883  StartedBy = new Models.User
884  {
885  Id = adminUserId.Value
886  },
887  Instance = new Models.Instance
888  {
889  Id = instance.Id
890  },
891  Description = $"Instance startup watchdog {(reattachInfo != null ? "reattach" : "launch")}",
892  CancelRight = (ulong)DreamDaemonRights.Shutdown,
893  CancelRightsType = RightsType.DreamDaemon
894  };
895  await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) =>
896  {
897  using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false))
898  await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
899  }, cancellationToken).ConfigureAwait(false);
900  }
901 
903  public async Task StopAsync(CancellationToken cancellationToken)
904  {
905  if (releaseServers)
906  {
907  await StopMonitor().ConfigureAwait(false);
908  if (releasedReattachInformation != null)
909  await reattachInfoHandler.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(false);
910  }
911 
912  await TerminateNoLock(false, !releaseServers, cancellationToken).ConfigureAwait(false);
913  }
914 
916  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
917  {
918  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
919  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
920  }
921 
923  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
924  {
925  releaseServers = true;
926  if (Running)
927  await Chat.SendWatchdogMessage("Detaching...", false, cancellationToken).ConfigureAwait(false);
928  }
929  }
930 }
readonly bool autoStart
If the WatchdogBase should LaunchImplNoLock(bool, bool, DualReattachInformation, CancellationToken) i...
-
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
+
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
@@ -109,72 +109,74 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
async Task MonitorLifetimes(CancellationToken cancellationToken)
The loop that watches the watchdog.
+
async Task MonitorLifetimes(CancellationToken cancellationToken)
The loop that watches the watchdog.
Represents a chat command to be handled by DD
Definition: ChatCommand.cs:9
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:102
-
WatchdogBase(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the WatchdogBase .
+
WatchdogBase(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the WatchdogBase .
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
-
async Task< MonitorState > MonitorRestart(CancellationToken cancellationToken)
-
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock Semaphore
-
async Task HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
+
async Task< MonitorState > MonitorRestart(CancellationToken cancellationToken)
+
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock Semaphore
+
async Task HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
-
async Task Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
+
async Task Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
readonly IJobManager jobManager
The IJobManager for the WatchdogBase.
readonly IReattachInfoHandler reattachInfoHandler
The IReattachInfoHandler for the WatchdogBase
Definition: WatchdogBase.cs:98
- +
For managing connected chat services
Definition: IChatManager.cs:13
-
void StartMonitor()
Call MonitorLifetimes(CancellationToken) and setup monitorCts and monitorTask.
-
Task monitorTask
The Task running the monitor loop
-
async Task StopAsync(CancellationToken cancellationToken)
+
void StartMonitor()
Call MonitorLifetimes(CancellationToken) and setup monitorCts and monitorTask.
+
Task monitorTask
The Task running the monitor loop
+
async Task StopAsync(CancellationToken cancellationToken)
DreamDaemonRights
Rights for Models.DreamDaemon
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the WatchdogBase
-
int heartbeatsMissed
The number of hearbeats missed.
+
int heartbeatsMissed
The number of hearbeats missed.
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
-
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
+
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
-
async Task< MonitorAction > HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
Handles a watchdog heartbeat.
+
async Task< MonitorAction > HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
Handles a watchdog heartbeat.
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
-
virtual async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
+
virtual async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
+
void DisposeAndNullControllers()
Wrapper for DisposeAndNullControllersImpl under a locked context.
Task Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan
MonitorActivationReason
Reasons for the monitor to wake up
Data structure for TopicCommandType.EventNotification requests.
-
async Task< string > HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
Handle a chat command
+
async Task< string > HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
Handle a chat command
Task< CombinedTopicResponse > SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
- +
ISessionController ActiveServer
The active ISessionController
Definition: MonitorState.cs:30
Represents a tgs_chat_user datum
Definition: ChatUser.cs:10
-
async Task StartAsync(CancellationToken cancellationToken)
+
async Task StartAsync(CancellationToken cancellationToken)
+
readonly object controllerDisposeLock
object used for DisposeAndNullControllers.
-
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently Running triggers a graceful restart ...
+
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently Running triggers a graceful restart ...
-
async Task LaunchImplNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the watchdog.
-
bool releaseServers
If the servers should be released instead of shutdown
+
async Task LaunchImplNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the watchdog.
+
bool releaseServers
If the servers should be released instead of shutdown
Handles communication with a DreamDaemon IProcess
Manages the runtime of Jobs
Definition: IJobManager.cs:13
-
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
+
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Handles Commands.ICommands that map to those defined in a IChatTrackingContext
-
async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken)
Send a chat message and log about a failed reattach operation and attempts another call to LaunchImpl...
-
async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
-
async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
Check the LaunchResult of a given controller for errors and throw a JobException if any are detected...
+
async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken)
Send a chat message and log about a failed reattach operation and attempts another call to LaunchImpl...
+
async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
+
async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
Check the LaunchResult of a given controller for errors and throw a JobException if any are detected...
Handles saving and loading DualReattachInformation
-
DualReattachInformation releasedReattachInformation
Used when detaching servers.
+
DualReattachInformation releasedReattachInformation
Used when detaching servers.
readonly Api.Models.Instance instance
The Api.Models.Instance for the WatchdogBase.
Definition: WatchdogBase.cs:93
-
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
+
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
bool Match(DreamDaemonLaunchParameters otherParameters)
Check if we match a given set of otherParameters
Represents the lifetime of a IRestartHandler registration
MonitorAction
The action for the monitor loop to take when control is returned to it
Definition: MonitorAction.cs:6
@@ -188,10 +190,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
- + -
bool disposed
If the WatchdogBase has been Disposed.
+
bool disposed
If the WatchdogBase has been Disposed.
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:13
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_web_host_builder_extensions_8cs.html b/_web_host_builder_extensions_8cs.html index 20934a431b..b187f581d7 100644 --- a/_web_host_builder_extensions_8cs.html +++ b/_web_host_builder_extensions_8cs.html @@ -25,7 +25,7 @@
diff --git a/_web_host_builder_extensions_8cs_source.html b/_web_host_builder_extensions_8cs_source.html index 1f12201e96..8bd63dd40c 100644 --- a/_web_host_builder_extensions_8cs_source.html +++ b/_web_host_builder_extensions_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_windows_byond_installer_8cs.html b/_windows_byond_installer_8cs.html index d5df7d50bd..87f6747d89 100644 --- a/_windows_byond_installer_8cs.html +++ b/_windows_byond_installer_8cs.html @@ -25,7 +25,7 @@ diff --git a/_windows_byond_installer_8cs_source.html b/_windows_byond_installer_8cs_source.html index 807c497b4e..f52391fc9f 100644 --- a/_windows_byond_installer_8cs_source.html +++ b/_windows_byond_installer_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_windows_network_prompt_reaper_8cs.html b/_windows_network_prompt_reaper_8cs.html index 2d301b9a17..bec31f31ba 100644 --- a/_windows_network_prompt_reaper_8cs.html +++ b/_windows_network_prompt_reaper_8cs.html @@ -25,7 +25,7 @@ diff --git a/_windows_network_prompt_reaper_8cs_source.html b/_windows_network_prompt_reaper_8cs_source.html index fcf2664637..1b5395c78b 100644 --- a/_windows_network_prompt_reaper_8cs_source.html +++ b/_windows_network_prompt_reaper_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_windows_post_write_handler_8cs.html b/_windows_post_write_handler_8cs.html index bc1b24d97c..4286cd5908 100644 --- a/_windows_post_write_handler_8cs.html +++ b/_windows_post_write_handler_8cs.html @@ -25,7 +25,7 @@ diff --git a/_windows_post_write_handler_8cs_source.html b/_windows_post_write_handler_8cs_source.html index c19a0e67ab..b7e109e9e8 100644 --- a/_windows_post_write_handler_8cs_source.html +++ b/_windows_post_write_handler_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_windows_process_features_8cs.html b/_windows_process_features_8cs.html index d2173d87f9..d96cf29d10 100644 --- a/_windows_process_features_8cs.html +++ b/_windows_process_features_8cs.html @@ -25,7 +25,7 @@ diff --git a/_windows_process_features_8cs_source.html b/_windows_process_features_8cs_source.html index c62a6c838c..a577a7f167 100644 --- a/_windows_process_features_8cs_source.html +++ b/_windows_process_features_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_windows_swappable_dmb_provider_8cs.html b/_windows_swappable_dmb_provider_8cs.html index 08f63d86a0..d5e7b0d7f4 100644 --- a/_windows_swappable_dmb_provider_8cs.html +++ b/_windows_swappable_dmb_provider_8cs.html @@ -25,7 +25,7 @@ diff --git a/_windows_swappable_dmb_provider_8cs_source.html b/_windows_swappable_dmb_provider_8cs_source.html index 1f664ebb1f..1852f43b5b 100644 --- a/_windows_swappable_dmb_provider_8cs_source.html +++ b/_windows_swappable_dmb_provider_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_windows_symlink_factory_8cs.html b/_windows_symlink_factory_8cs.html index 61b7cc677c..e11dea6b8f 100644 --- a/_windows_symlink_factory_8cs.html +++ b/_windows_symlink_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_windows_symlink_factory_8cs_source.html b/_windows_symlink_factory_8cs_source.html index 9b06346873..7c7889129d 100644 --- a/_windows_symlink_factory_8cs_source.html +++ b/_windows_symlink_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_windows_system_identity_8cs.html b/_windows_system_identity_8cs.html index 9041de3e8a..8fe084d184 100644 --- a/_windows_system_identity_8cs.html +++ b/_windows_system_identity_8cs.html @@ -25,7 +25,7 @@ diff --git a/_windows_system_identity_8cs_source.html b/_windows_system_identity_8cs_source.html index c241526088..95a91dbe2d 100644 --- a/_windows_system_identity_8cs_source.html +++ b/_windows_system_identity_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_windows_system_identity_factory_8cs.html b/_windows_system_identity_factory_8cs.html index b5c4b2fac4..db356375f5 100644 --- a/_windows_system_identity_factory_8cs.html +++ b/_windows_system_identity_factory_8cs.html @@ -25,7 +25,7 @@ diff --git a/_windows_system_identity_factory_8cs_source.html b/_windows_system_identity_factory_8cs_source.html index 3f98cb68db..85ab667043 100644 --- a/_windows_system_identity_factory_8cs_source.html +++ b/_windows_system_identity_factory_8cs_source.html @@ -25,7 +25,7 @@ diff --git a/_windows_watchdog_8cs.html b/_windows_watchdog_8cs.html index 2be910b4cd..23f2baf44f 100644 --- a/_windows_watchdog_8cs.html +++ b/_windows_watchdog_8cs.html @@ -25,7 +25,7 @@ diff --git a/_windows_watchdog_8cs_source.html b/_windows_watchdog_8cs_source.html index 3e7805cf2d..724858e8b1 100644 --- a/_windows_watchdog_8cs_source.html +++ b/_windows_watchdog_8cs_source.html @@ -25,7 +25,7 @@ @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WindowsWatchdog.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Threading;
4 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
13 
14 namespace Tgstation.Server.Host.Components.Watchdog
15 {
20  {
25 
30 
35 
40 
45 
64  IChatManager chat,
65  ISessionControllerFactory sessionControllerFactory,
66  IDmbFactory dmbFactory,
67  IReattachInfoHandler reattachInfoHandler,
68  IDatabaseContextFactory databaseContextFactory,
69  IJobManager jobManager,
70  IServerControl serverControl,
71  IAsyncDelayer asyncDelayer,
72  IIOManager ioManager,
73  ISymlinkFactory symlinkFactory,
74  ILogger<WindowsWatchdog> logger,
75  DreamDaemonLaunchParameters initialLaunchParameters,
76  Api.Models.Instance instance, bool autoStart)
77  : base(
78  chat,
79  sessionControllerFactory,
80  dmbFactory,
81  reattachInfoHandler,
82  databaseContextFactory,
83  jobManager,
84  serverControl,
85  asyncDelayer,
86  logger,
87  initialLaunchParameters,
88  instance,
89  autoStart)
90  {
91  try
92  {
93  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
94  this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
95  }
96  catch
97  {
98  Dispose();
99  throw;
100  }
101  }
102 
104  protected override void DisposeAndNullControllers()
105  {
106  base.DisposeAndNullControllers();
107 
108  // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again.
109  activeSwappable = null;
110  pendingSwappable?.Dispose();
111  pendingSwappable = null;
112 
113  startupDmbProvider?.Dispose();
114  startupDmbProvider = null;
115  }
116 
118  protected override MonitorAction HandleNormalReboot()
119  {
120  if (activeSwappable == null)
121  throw new InvalidOperationException("Expected activeSwappable to not be null!");
122  if (pendingSwappable != null)
123  {
124  Logger.LogTrace("Replacing activeSwappable with pendingSwappable...");
125  Server.ReplaceDmbProvider(pendingSwappable);
126  activeSwappable = pendingSwappable;
127  pendingSwappable = null;
128  }
129  else
130  Logger.LogTrace("Nothing to do as pendingSwappable is null.");
131 
132  return MonitorAction.Continue;
133  }
134 
136  protected override async Task HandleNewDmbAvailable(CancellationToken cancellationToken)
137  {
138  IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1);
139  if (compileJobProvider.CompileJob.ByondVersion != ActiveCompileJob.ByondVersion)
140  {
141  // have to do a graceful restart
142  Logger.LogDebug(
143  "Not swapping to new compile job {0} as it uses a different BYOND version ({1}) than what is currently active {2}. Queueing graceful restart instead...",
144  compileJobProvider.CompileJob.Id,
145  compileJobProvider.CompileJob.ByondVersion,
146  ActiveCompileJob.ByondVersion);
147  compileJobProvider.Dispose();
148  await base.HandleNewDmbAvailable(cancellationToken).ConfigureAwait(false);
149  return;
150  }
151 
152  WindowsSwappableDmbProvider windowsProvider = null;
153  bool suspended = false;
154  try
155  {
156  windowsProvider = new WindowsSwappableDmbProvider(compileJobProvider, ioManager, symlinkFactory);
157 
158  Logger.LogDebug("Swapping to compile job {0}...", windowsProvider.CompileJob.Id);
159  try
160  {
161  Server.Suspend();
162  suspended = true;
163  }
164  catch (Exception ex)
165  {
166  Logger.LogWarning("Exception while suspending server: {0}", ex);
167  }
168 
169  await windowsProvider.MakeActive(cancellationToken).ConfigureAwait(false);
170  }
171  catch(Exception ex)
172  {
173  Logger.LogError("Exception while swapping: {0}", ex);
174  IDmbProvider providerToDispose = windowsProvider ?? compileJobProvider;
175  providerToDispose.Dispose();
176  throw;
177  }
178 
179  // Let this throw hard if it fails
180  if (suspended)
181  Server.Resume();
182 
183  pendingSwappable?.Dispose();
184  pendingSwappable = windowsProvider;
185  }
186 
188  protected override async Task<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken)
189  {
190  if(activeSwappable != null)
191  throw new InvalidOperationException("Expected activeSwappable to be null!");
192  if(startupDmbProvider != null)
193  throw new InvalidOperationException("Expected startupDmbProvider to be null!");
194 
195  Logger.LogTrace("Prep for server launch. pendingSwappable is {0}available", pendingSwappable == null ? "not " : String.Empty);
196 
197  // Add another lock to the startup DMB because it'll be used throughout the lifetime of the watchdog
198  startupDmbProvider = await DmbFactory.FromCompileJob(dmbToUse.CompileJob, cancellationToken).ConfigureAwait(false);
199 
200  activeSwappable = pendingSwappable ?? new WindowsSwappableDmbProvider(dmbToUse, ioManager, symlinkFactory);
201  pendingSwappable = null;
202 
203  try
204  {
205  await activeSwappable.MakeActive(cancellationToken).ConfigureAwait(false);
206  }
207  catch
208  {
209  // We won't worry about disposing activeSwappable here as we can't dispose dmbToUse here.
210  activeSwappable = null;
211  throw;
212  }
213 
214  return activeSwappable;
215  }
216  }
217 }
async Task MakeActive(CancellationToken cancellationToken)
Make the WindowsSwappableDmbProvider active by replacing the live link with our CompileJob.
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Threading;
4 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
13 
14 namespace Tgstation.Server.Host.Components.Watchdog
15 {
20  {
25 
30 
35 
40 
45 
64  IChatManager chat,
65  ISessionControllerFactory sessionControllerFactory,
66  IDmbFactory dmbFactory,
67  IReattachInfoHandler reattachInfoHandler,
68  IDatabaseContextFactory databaseContextFactory,
69  IJobManager jobManager,
70  IServerControl serverControl,
71  IAsyncDelayer asyncDelayer,
72  IIOManager ioManager,
73  ISymlinkFactory symlinkFactory,
74  ILogger<WindowsWatchdog> logger,
75  DreamDaemonLaunchParameters initialLaunchParameters,
76  Api.Models.Instance instance, bool autoStart)
77  : base(
78  chat,
79  sessionControllerFactory,
80  dmbFactory,
81  reattachInfoHandler,
82  databaseContextFactory,
83  jobManager,
84  serverControl,
85  asyncDelayer,
86  logger,
87  initialLaunchParameters,
88  instance,
89  autoStart)
90  {
91  try
92  {
93  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
94  this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
95  }
96  catch
97  {
98  Dispose();
99  throw;
100  }
101  }
102 
104  protected override void DisposeAndNullControllersImpl()
105  {
106  base.DisposeAndNullControllersImpl();
107 
108  // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again.
109  activeSwappable = null;
110  pendingSwappable?.Dispose();
111  pendingSwappable = null;
112 
113  startupDmbProvider?.Dispose();
114  startupDmbProvider = null;
115  }
116 
118  protected override MonitorAction HandleNormalReboot()
119  {
120  if (activeSwappable == null)
121  throw new InvalidOperationException("Expected activeSwappable to not be null!");
122  if (pendingSwappable != null)
123  {
124  Logger.LogTrace("Replacing activeSwappable with pendingSwappable...");
125  Server.ReplaceDmbProvider(pendingSwappable);
126  activeSwappable = pendingSwappable;
127  pendingSwappable = null;
128  }
129  else
130  Logger.LogTrace("Nothing to do as pendingSwappable is null.");
131 
132  return MonitorAction.Continue;
133  }
134 
136  protected override async Task HandleNewDmbAvailable(CancellationToken cancellationToken)
137  {
138  IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1);
139  if (compileJobProvider.CompileJob.ByondVersion != ActiveCompileJob.ByondVersion)
140  {
141  // have to do a graceful restart
142  Logger.LogDebug(
143  "Not swapping to new compile job {0} as it uses a different BYOND version ({1}) than what is currently active {2}. Queueing graceful restart instead...",
144  compileJobProvider.CompileJob.Id,
145  compileJobProvider.CompileJob.ByondVersion,
146  ActiveCompileJob.ByondVersion);
147  compileJobProvider.Dispose();
148  await base.HandleNewDmbAvailable(cancellationToken).ConfigureAwait(false);
149  return;
150  }
151 
152  WindowsSwappableDmbProvider windowsProvider = null;
153  bool suspended = false;
154  try
155  {
156  windowsProvider = new WindowsSwappableDmbProvider(compileJobProvider, ioManager, symlinkFactory);
157 
158  Logger.LogDebug("Swapping to compile job {0}...", windowsProvider.CompileJob.Id);
159  try
160  {
161  Server.Suspend();
162  suspended = true;
163  }
164  catch (Exception ex)
165  {
166  Logger.LogWarning("Exception while suspending server: {0}", ex);
167  }
168 
169  await windowsProvider.MakeActive(cancellationToken).ConfigureAwait(false);
170  }
171  catch(Exception ex)
172  {
173  Logger.LogError("Exception while swapping: {0}", ex);
174  IDmbProvider providerToDispose = windowsProvider ?? compileJobProvider;
175  providerToDispose.Dispose();
176  throw;
177  }
178 
179  // Let this throw hard if it fails
180  if (suspended)
181  Server.Resume();
182 
183  pendingSwappable?.Dispose();
184  pendingSwappable = windowsProvider;
185  }
186 
188  protected override async Task<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken)
189  {
190  if(activeSwappable != null)
191  throw new InvalidOperationException("Expected activeSwappable to be null!");
192  if(startupDmbProvider != null)
193  throw new InvalidOperationException("Expected startupDmbProvider to be null!");
194 
195  Logger.LogTrace("Prep for server launch. pendingSwappable is {0}available", pendingSwappable == null ? "not " : String.Empty);
196 
197  // Add another lock to the startup DMB because it'll be used throughout the lifetime of the watchdog
198  startupDmbProvider = await DmbFactory.FromCompileJob(dmbToUse.CompileJob, cancellationToken).ConfigureAwait(false);
199 
200  activeSwappable = pendingSwappable ?? new WindowsSwappableDmbProvider(dmbToUse, ioManager, symlinkFactory);
201  pendingSwappable = null;
202 
203  try
204  {
205  await activeSwappable.MakeActive(cancellationToken).ConfigureAwait(false);
206  }
207  catch
208  {
209  // We won't worry about disposing activeSwappable here as we can't dispose dmbToUse here.
210  activeSwappable = null;
211  throw;
212  }
213 
214  return activeSwappable;
215  }
216  }
217 }
async Task MakeActive(CancellationToken cancellationToken)
Make the WindowsSwappableDmbProvider active by replacing the live link with our CompileJob.
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
override async Task< IDmbProvider > PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken)
Prepare the server to launch a new instance with the WatchdogBase.ActiveLaunchParameters and a given ...
@@ -104,6 +104,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WindowsSwappableDmbProvider pendingSwappable
The active WindowsSwappableDmbProvider for WatchdogBase.ActiveLaunchParameters.
+
override void DisposeAndNullControllersImpl()
Call IDisposable.Dispose and null the fields for all ISessionControllers and set Running to ...
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
@@ -113,7 +114,6 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For managing connected chat services
Definition: IChatManager.cs:13
-
override void DisposeAndNullControllers()
Call IDisposable.Dispose and null the fields for all ISessionControllers and set Running to ...
@@ -141,7 +141,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
override async Task HandleNewDmbAvailable(CancellationToken cancellationToken)
Handler for MonitorActivationReason.NewDmbAvailable.
A version of the BasicWatchdog that, instead of killing servers for updates, uses the wonders of syml...
readonly ISymlinkFactory symlinkFactory
The ISymlinkFactory for the WindowsWatchdog.
- +
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_windows_watchdog_factory_8cs.html b/_windows_watchdog_factory_8cs.html index 34fa73e613..23e557a96b 100644 --- a/_windows_watchdog_factory_8cs.html +++ b/_windows_watchdog_factory_8cs.html @@ -25,7 +25,7 @@
diff --git a/_windows_watchdog_factory_8cs_source.html b/_windows_watchdog_factory_8cs_source.html index 90f6836ae8..f33e7c244a 100644 --- a/_windows_watchdog_factory_8cs_source.html +++ b/_windows_watchdog_factory_8cs_source.html @@ -25,7 +25,7 @@ @@ -127,7 +127,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
A version of the BasicWatchdog that, instead of killing servers for updates, uses the wonders of syml...
- +
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:13
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/annotated.html b/annotated.html index 5544e86634..fadf8f70b1 100644 --- a/annotated.html +++ b/annotated.html @@ -25,7 +25,7 @@ @@ -314,7 +314,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - + @@ -353,10 +353,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - - - - + + + + + @@ -391,11 +392,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - - - - - + + + + + + @@ -406,9 +408,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - - - + + + + @@ -436,95 +439,98 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

diff --git a/_sqlite_design_time_db_context_factory_8cs_source.html b/_sqlite_design_time_db_context_factory_8cs_source.html index e5ddfe2697..d0b240b2c8 100644 --- a/_sqlite_design_time_db_context_factory_8cs_source.html +++ b/_sqlite_design_time_db_context_factory_8cs_source.html @@ -25,7 +25,7 @@

tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
 NConfiguration
 NDatabase
 NExtensions
 NModels
 NSecurity
 NService
 NSetup
 NSystem
 NWatchdog
 CIServerRepresents the host
 CIServerFactoryFor creating IServers
 CNativeMethodsNative Windows methods used by the code
 CProgramEntrypoint for the Process
 CServer
 CServerFactoryImplementation of IServerFactory.
 CAuthorizeAttribute
 CController
 CDbContext
 CIDesignTimeDbContextFactory
 CIDisposable
 CIDocumentFilter
 CIHostedService
 CIOpenApiExtension
 CIOperationFilter
 CIQueryable
 CISchemaFilter
 CJsonConverter
 CMigration
 CModelSnapshot
 CServiceBase
 NMigrations
 NModels
 NSecurity
 NService
 NSetup
 NSystem
 NWatchdog
 CIServerRepresents the host
 CIServerFactoryFor creating IServers
 CNativeMethodsNative Windows methods used by the code
 CProgramEntrypoint for the Process
 CServer
 CServerFactoryImplementation of IServerFactory.
 CAuthorizeAttribute
 CController
 CDbContext
 CIAsyncEnumerable
 CIDesignTimeDbContextFactory
 CIDisposable
 CIDocumentFilter
 CIHostedService
 CIOpenApiExtension
 CIOperationFilter
 CIQueryable
 CISchemaFilter
 CJsonConverter
 CMigration
 CModelSnapshot
 CServiceBase
diff --git a/api.html b/api.html index ac297b8b01..18d48b1a7c 100644 --- a/api.html +++ b/api.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/architecture.html b/architecture.html index f0b9297ca8..6615d9a720 100644 --- a/architecture.html +++ b/architecture.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -153,7 +153,7 @@ Server Initialization

Instance Manager Initialization

Once the web host starts, the Tgstation.Server.Host.Components.InstanceManager.StartAsync function is called (due to being registered as a IHostedService in Tgstation.Server.Host.Core.Application) this is the only StartAsync implementation that should be called by the framework, others should be called from this to maintain a cohesive initialization order.

-

The first thing this function does is call Tgstation.Server.Host.Database.DatabaseContext.Initialize which ensures the database is migrated, seeded, and ready to go. Then the Tgstation.Server.Host.Core.JobManager is started, which cleans up any jobs that are considered "still running" in the database. Finally all instances configured to be online are created in parallel (See Instances for onlining process) and the Tgstation.Server.Host.Core.Application is signalled to stop blocking requests with 503 responses before they are processed.

+

The first thing this function does is call Tgstation.Server.Host.Database.DatabaseContext.Initialize which ensures the database is migrated, seeded, and ready to go. Then the Tgstation.Server.Host.Core.JobManager is started, which cleans up any jobs that are considered "still running" in the database. Finally all instances configured to be online are created in parallel (See Instances for onlining process) and the Tgstation.Server.Host.Core.Application is signalled to stop blocking requests with 503 responses before they are processed.

Database and Context

The database is exposed as a series of DbSet<T> objects through Tgstation.Server.Host.Database.IDatabaseContext . Queries are performed via async LINQ expressions. Inserts, updates, and deletes are done via modifiying the DbSet<T>s and then calling Tgstation.Server.Host.Database.IDatabaseContext.Save . Do some reading on Entity Framework Core for a deeper understanding.

diff --git a/class_authorize_attribute.html b/class_authorize_attribute.html index b9917d3c67..ce9821f653 100644 --- a/class_authorize_attribute.html +++ b/class_authorize_attribute.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/class_controller.html b/class_controller.html index 03f2207e6b..f805c0135c 100644 --- a/class_controller.html +++ b/class_controller.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
diff --git a/class_db_context.html b/class_db_context.html index 21544b81d9..5e6e316326 100644 --- a/class_db_context.html +++ b/class_db_context.html @@ -25,7 +25,7 @@ Logo
tgstation-server -  4.2.8 +  4.3.0
The /tg/station 13 server suite
@@ -98,7 +98,11 @@ Inheritance diagram for DbContext:
Inheritance graph
- + + + + +
[legend]

The documentation for this class was generated from the following file: