From 6859153aff57ad74bc88f6da18a25dc5e444aebe Mon Sep 17 00:00:00 2001 From: tgstation-server Date: Thu, 29 Nov 2018 15:15:16 +0000 Subject: [PATCH] Deploy code docs to GitHub Pages for Appveyor build 1042 Commit: 68d108756296f60ab03a0036aa482130825cee31 --- _administration_controller_8cs_source.html | 2 +- _api_conflict_exception_8cs_source.html | 3 +- _api_controller_8cs_source.html | 7 +- _api_mismatch_exception_8cs_source.html | 3 +- _application_8cs_source.html | 16 +- _byond_manager_8cs_source.html | 3 +- _chat_8cs_source.html | 3 +- _claims_injector_8cs_source.html | 3 +- _client_exception_8cs_source.html | 3 +- _comm_context_8cs_source.html | 3 +- ...ents_2_watchdog_2_watchdog_8cs_source.html | 3 +- _conflict_exception_8cs_source.html | 3 +- _control_panel_configuration_8cs.html | 120 +++ _control_panel_configuration_8cs_source.html | 107 +++ _d_b_connection_factory_8cs_source.html | 2 +- _database_configuration_8cs_source.html | 5 +- _database_context_8cs_source.html | 5 +- ...db_context_factory_helpers_8cs_source.html | 2 +- _discord_provider_8cs_source.html | 3 +- _dmb_factory_8cs_source.html | 3 +- _home_controller_8cs_source.html | 24 +- _i_application_8cs_source.html | 3 +- _i_d_b_connection_factory_8cs_source.html | 2 +- _i_server_control_8cs_source.html | 2 +- _instance_controller_8cs_source.html | 3 +- _instance_manager_8cs_source.html | 28 +- ...ient_permissions_exception_8cs_source.html | 3 +- _irc_provider_8cs_source.html | 3 +- _job_exception_8cs_source.html | 3 +- _job_manager_8cs_source.html | 3 +- _json_tracking_context_8cs_source.html | 3 +- ...od_not_supported_exception_8cs_source.html | 3 +- _model_controller_8cs_source.html | 2 +- _my_sql_database_context_8cs_source.html | 2 +- _posix_byond_installer_8cs_source.html | 3 +- _process_8cs_source.html | 3 +- _process_executor_8cs_source.html | 3 +- _r_e_a_d_m_e_8md_source.html | 2 +- _rate_limit_exception_8cs_source.html | 3 +- _release_notes_8_assembly_info_8cs.html | 4 +- ...ease_notes_8_assembly_info_8cs_source.html | 6 +- _repository_controller_8cs_source.html | 2 +- _repository_manager_8cs_source.html | 3 +- _request_timeout_exception_8cs_source.html | 3 +- _server_8cs_source.html | 20 +- _server_error_exception_8cs_source.html | 3 +- _server_factory_8cs_source.html | 9 +- _server_service_8cs_source.html | 3 +- _service_collection_extensions_8cs.html | 120 +++ ...vice_collection_extensions_8cs_source.html | 112 +++ ...vice_unavailable_exception_8cs_source.html | 3 +- _session_controller_8cs_source.html | 3 +- _setup_wizard_8cs_source.html | 17 +- _sql_server_database_context_8cs_source.html | 2 +- _test_application_8cs_source.html | 13 +- _test_d_b_connection_factory_8cs_source.html | 2 +- _test_job_exception_8cs_source.html | 3 +- _test_posix_byond_installer_8cs_source.html | 3 +- _test_server_factory_8cs_source.html | 2 +- _test_server_service_8cs_source.html | 2 +- _test_service_collection_extensions_8cs.html | 127 +++ ...vice_collection_extensions_8cs_source.html | 114 +++ _test_setup_wizard_8cs_source.html | 5 +- _testing_server_8cs_source.html | 6 +- ...on_8_server_8_api_8_assembly_info_8cs.html | 4 +- ...rver_8_api_8_assembly_info_8cs_source.html | 6 +- ...ver_8_api_8_tests_8_assembly_info_8cs.html | 4 +- ...pi_8_tests_8_assembly_info_8cs_source.html | 6 +- ..._8_server_8_api_8_tests_8_program_8cs.html | 4 +- ...er_8_api_8_tests_8_program_8cs_source.html | 4 +- ...8_server_8_client_8_assembly_info_8cs.html | 4 +- ...r_8_client_8_assembly_info_8cs_source.html | 6 +- ..._8_client_8_tests_8_assembly_info_8cs.html | 4 +- ...nt_8_tests_8_assembly_info_8cs_source.html | 6 +- ...server_8_client_8_tests_8_program_8cs.html | 4 +- ...8_client_8_tests_8_program_8cs_source.html | 4 +- ...s_2_compiler_2_dream_maker_8cs_source.html | 3 +- ...st_2_components_2_instance_8cs_source.html | 3 +- ...n_8_server_8_host_8_assembly_info_8cs.html | 4 +- ...ver_8_host_8_assembly_info_8cs_source.html | 6 +- ..._8_host_8_console_8_assembly_info_8cs.html | 4 +- ..._8_console_8_assembly_info_8cs_source.html | 6 +- ...8_console_8_tests_8_assembly_info_8cs.html | 4 +- ...le_8_tests_8_assembly_info_8cs_source.html | 6 +- ..._host_8_console_8_tests_8_program_8cs.html | 4 +- ..._console_8_tests_8_program_8cs_source.html | 4 +- ...rver_8_host_8_razor_assembly_info_8cs.html | 4 +- ...host_8_razor_assembly_info_8cs_source.html | 4 +- ...er_8_host_8_tests_8_assembly_info_8cs.html | 4 +- ...st_8_tests_8_assembly_info_8cs_source.html | 6 +- ...8_server_8_host_8_tests_8_program_8cs.html | 4 +- ...r_8_host_8_tests_8_program_8cs_source.html | 4 +- ...8_host_8_watchdog_8_assembly_info_8cs.html | 4 +- ...8_watchdog_8_assembly_info_8cs_source.html | 6 +- ..._watchdog_8_tests_8_assembly_info_8cs.html | 4 +- ...og_8_tests_8_assembly_info_8cs_source.html | 6 +- ...host_8_watchdog_8_tests_8_program_8cs.html | 4 +- ...watchdog_8_tests_8_program_8cs_source.html | 4 +- ..._8_server_8_tests_8_assembly_info_8cs.html | 4 +- ...er_8_tests_8_assembly_info_8cs_source.html | 6 +- ...tation_8_server_8_tests_8_program_8cs.html | 4 +- ...8_server_8_tests_8_program_8cs_source.html | 4 +- _unauthorized_exception_8cs_source.html | 3 +- ...ognized_response_exception_8cs_source.html | 3 +- _user_controller_8cs_source.html | 2 +- _watchdog_2_watchdog_8cs_source.html | 3 +- _windows_byond_installer_8cs_source.html | 3 +- ...ws_system_identity_factory_8cs_source.html | 3 +- annotated.html | 38 +- architecture.html | 2 +- class_auto_generated_program.html | 12 +- class_exception.png | Bin 9718 -> 0 bytes class_release_notes_1_1_program.html | 3 +- ...on_1_1_server_1_1_api_1_1_api_headers.html | 14 +- ..._1_1_api_1_1_models_1_1_error_message.html | 2 +- ..._api_1_1_models_1_1_internal_1_1_user.html | 2 +- ...1_1_client_1_1_api_conflict_exception.html | 13 +- ..._1_1_client_1_1_api_conflict_exception.png | Bin 1182 -> 1188 bytes ...1_1_client_1_1_api_mismatch_exception.html | 13 +- ..._1_1_client_1_1_api_mismatch_exception.png | Bin 1324 -> 1336 bytes ...erver_1_1_client_1_1_client_exception.html | 11 +- ...server_1_1_client_1_1_client_exception.png | Bin 9461 -> 9509 bytes ...ver_1_1_client_1_1_conflict_exception.html | 13 +- ...rver_1_1_client_1_1_conflict_exception.png | Bin 1141 -> 1152 bytes ..._1_insufficient_permissions_exception.html | 13 +- ...1_1_insufficient_permissions_exception.png | Bin 1468 -> 1486 bytes ...nt_1_1_method_not_supported_exception.html | 13 +- ...ent_1_1_method_not_supported_exception.png | Bin 1445 -> 1462 bytes ...r_1_1_client_1_1_rate_limit_exception.html | 13 +- ...er_1_1_client_1_1_rate_limit_exception.png | Bin 1159 -> 1172 bytes ..._client_1_1_request_timeout_exception.html | 13 +- ...1_client_1_1_request_timeout_exception.png | Bin 1388 -> 1405 bytes ...1_1_client_1_1_server_error_exception.html | 11 +- ..._1_1_client_1_1_server_error_exception.png | Bin 1225 -> 1235 bytes ...ent_1_1_service_unavailable_exception.html | 13 +- ...ient_1_1_service_unavailable_exception.png | Bin 1421 -> 1437 bytes ...1_1_client_1_1_unauthorized_exception.html | 13 +- ..._1_1_client_1_1_unauthorized_exception.png | Bin 1345 -> 1361 bytes ...t_1_1_unrecognized_response_exception.html | 13 +- ...nt_1_1_unrecognized_response_exception.png | Bin 1455 -> 1472 bytes ...omponents_1_1_byond_1_1_byond_manager.html | 3 +- ...s_1_1_byond_1_1_posix_byond_installer.html | 3 +- ..._tests_1_1_test_posix_byond_installer.html | 3 +- ...1_1_byond_1_1_windows_byond_installer.html | 6 +- ...host_1_1_components_1_1_chat_1_1_chat.html | 6 +- ...ts_1_1_chat_1_1_json_tracking_context.html | 3 +- ...at_1_1_providers_1_1_discord_provider.html | 9 +- ...1_chat_1_1_providers_1_1_irc_provider.html | 3 +- ...mponents_1_1_compiler_1_1_dmb_factory.html | 3 +- ...mponents_1_1_compiler_1_1_dream_maker.html | 3 +- ..._1_1_host_1_1_components_1_1_instance.html | 3 +- ...mponents_1_1_instance_manager-members.html | 5 +- ...t_1_1_components_1_1_instance_manager.html | 87 +- ...mponents_1_1_interop_1_1_comm_context.html | 3 +- ...1_1_repository_1_1_repository_manager.html | 3 +- ...s_1_1_watchdog_1_1_session_controller.html | 6 +- ..._components_1_1_watchdog_1_1_watchdog.html | 9 +- ...1_control_panel_configuration-members.html | 113 +++ ...ation_1_1_control_panel_configuration.html | 235 ++++++ ...figuration_1_1_database_configuration.html | 2 +- ...ration_1_1_file_logging_configuration.html | 2 +- ...nfiguration_1_1_general_configuration.html | 2 +- ...nfiguration_1_1_updates_configuration.html | 2 - ...ontrollers_1_1_api_controller-members.html | 5 +- ...st_1_1_controllers_1_1_api_controller.html | 54 +- ...ntrollers_1_1_home_controller-members.html | 8 +- ...t_1_1_controllers_1_1_home_controller.html | 133 ++- ...1_controllers_1_1_instance_controller.html | 3 +- ...trollers_1_1_model_controller-members.html | 2 +- ..._1_1_controllers_1_1_model_controller.html | 8 +- ...host_1_1_core_1_1_application-members.html | 2 +- ...ver_1_1_host_1_1_core_1_1_application.html | 38 +- ...r_1_1_host_1_1_core_1_1_job_exception.html | 11 +- ...er_1_1_host_1_1_core_1_1_job_exception.png | Bin 728 -> 722 bytes ...ver_1_1_host_1_1_core_1_1_job_manager.html | 3 +- ..._server_1_1_host_1_1_core_1_1_process.html | 6 +- ..._1_host_1_1_core_1_1_process_executor.html | 3 +- ...service_collection_extensions-members.html | 110 +++ ...ore_1_1_service_collection_extensions.html | 191 +++++ ...ost_1_1_core_1_1_setup_wizard-members.html | 31 +- ...er_1_1_host_1_1_core_1_1_setup_wizard.html | 94 ++- ...1_core_1_1_tests_1_1_test_application.html | 7 +- ...ication_1_1_mock_application_lifetime.html | 12 +- ...est_application_1_1_mock_setup_wizard.html | 2 +- ...core_1_1_tests_1_1_test_job_exception.html | 3 +- ...service_collection_extensions-members.html | 110 +++ ..._1_test_service_collection_extensions.html | 151 ++++ ...on_extensions_1_1_bad_config1-members.html | 110 +++ ...collection_extensions_1_1_bad_config1.html | 66 +- ...on_extensions_1_1_bad_config2-members.html | 110 +++ ...collection_extensions_1_1_bad_config2.html | 136 ++++ ...collection_extensions_1_1_bad_config3.html | 112 +++ ...on_extensions_1_1_good_config-members.html | 110 +++ ...collection_extensions_1_1_good_config.html | 136 ++++ ..._core_1_1_tests_1_1_test_setup_wizard.html | 3 +- ..._host_1_1_models_1_1_database_context.html | 7 +- ...ation_1_1_server_1_1_host_1_1_program.html | 3 +- ...1_security_1_1_authentication_context.html | 4 +- ...host_1_1_security_1_1_claims_injector.html | 3 +- ..._1_server_1_1_host_1_1_server-members.html | 27 +- ...tation_1_1_server_1_1_host_1_1_server.html | 146 +++- ..._1_server_1_1_host_1_1_server_factory.html | 8 +- ...1_host_1_1_service_1_1_server_service.html | 4 +- ...ice_1_1_tests_1_1_test_server_service.html | 2 +- ...er_1_1_host_1_1_watchdog_1_1_watchdog.html | 3 +- ...1_server_1_1_tests_1_1_testing_server.html | 6 +- classes.html | 181 ++--- ... dir_009a3598be1715e27911e1588bdd551c.html | 4 +- ... dir_019c6d3af924151b681494cc466be1ac.html | 8 +- dir_08963d262fa9e27215dce7b7c6e97f1b.html | 2 + ... dir_0b6a380fe7fc1e1d640d1ac854fc3fb6.html | 8 +- dir_0de875c2ee7edd4009e892c5045f7b3a.html | 2 + dir_103b33cf1ab7043de6f87dfa61afa23f.html | 2 +- ... dir_1083fcf52a7d94a2efffb6ca88246acf.html | 4 +- dir_1302441b003a340c6a16c9bc1b51c94e.html | 2 + dir_177c182c6d1f29bec543c3fdad3072d5.html | 2 +- ... dir_2553b13cbb871373b0c30fb90d66217c.html | 4 +- ... dir_2b9d136d9ffdab429a63a74fca28b300.html | 8 +- ... dir_2e39d68cad3f83629c74ea67329dead9.html | 8 +- ... dir_2e67631cb23401d696bcfdbe81f86f0f.html | 8 +- dir_446bdfc1cd19b69c11765b5665ebfd45.html | 2 +- ... dir_48968f849f7ddb061f7130e079fe66a6.html | 8 +- ... dir_4c0c7e2f4fc60f51e2347a2d8dda94ce.html | 4 +- dir_685b5aa2d8a661ab5bc3f68b331887b0.html | 2 +- dir_689f0584ee97ae9ca308532531d9c5d2.html | 2 +- dir_74017a19412e2f90cc6ad11fcac82495.html | 2 +- ... dir_87039fa7b841d848ca75981d64237477.html | 8 +- dir_87a4f2035ed18e30422f4b60aebc4381.html | 2 +- ... dir_9257d56e926145e494a5aa5279887044.html | 4 +- ... dir_a3714ab037275c284988fc90b4d045a5.html | 4 +- ... dir_a8807eef6592489c6a360d587f5b1a66.html | 4 +- dir_b524b00ec2259c3c83b44909fd47f6e3.html | 2 +- ... dir_b551a02288ef39e099594710382363a0.html | 4 +- ... dir_b87cbb2c60cf682b61fbfcceb9bced43.html | 4 +- ... dir_b92573f47b933d7a3661c26d1af2098b.html | 8 +- ... dir_be9f3ed3485a880e3213506a311823d1.html | 8 +- dir_bf23e1e49f40264f2966f02ed35d8142.html | 2 +- ... dir_c5f98c05147bcbba78e71856485f5049.html | 4 +- ... dir_d8a279f7113e0073f6cd65dd050c1475.html | 4 +- dir_e0c1bccb03249d93bc42ac0968ce43fd.html | 2 +- ... dir_e97d380c3eebaf0856a4ec180d749620.html | 8 +- ... dir_ea36b5ab01ad6e8b607438a02ae91964.html | 8 +- dir_f0581cdd528a911f791df57d0f438bcb.html | 2 +- ... dir_f88948f9bde0555374e8a59a663bdfc9.html | 4 +- ... dir_fab1362e86740a2c573ef875dd474a67.html | 8 +- dir_fb12273b841438c2f2c9b5bc5398c636.html | 2 +- files.html | 67 +- functions.html | 12 +- functions_b.html | 3 + functions_c.html | 31 +- functions_d.html | 4 + functions_e.html | 3 + functions_func.html | 2 +- functions_func_c.html | 16 +- functions_func_d.html | 6 +- functions_func_h.html | 4 +- functions_func_r.html | 2 +- functions_func_s.html | 2 +- functions_func_t.html | 5 +- functions_func_u.html | 3 + functions_h.html | 4 +- functions_i.html | 2 +- functions_p.html | 3 + functions_prop.html | 6 + functions_prop_e.html | 3 + functions_r.html | 9 +- functions_s.html | 9 +- functions_t.html | 5 +- functions_u.html | 3 + functions_vars_b.html | 3 + functions_vars_c.html | 3 + functions_vars_p.html | 3 + functions_vars_r.html | 3 + functions_vars_s.html | 7 +- hierarchy.html | 769 +++++++++--------- index.html | 3 + ...r_1_1_host_1_1_core_1_1_i_application.html | 6 +- ...1_1_core_1_1_i_server_control-members.html | 7 +- ..._1_host_1_1_core_1_1_i_server_control.html | 36 +- ...er_1_1_host_1_1_i_o_1_1_i_i_o_manager.html | 6 +- ..._1_1_i_authentication_context_factory.html | 2 +- ...st_1_1_security_1_1_i_system_identity.html | 4 +- ...1_1_server_1_1_host_1_1_configuration.html | 3 + ...gstation_1_1_server_1_1_host_1_1_core.html | 3 + ..._1_server_1_1_host_1_1_core_1_1_tests.html | 2 + search/all_1.js | 14 +- search/all_10.js | 17 +- search/all_12.js | 13 +- search/all_13.js | 16 +- search/all_14.js | 25 +- search/all_15.js | 5 +- search/all_2.js | 14 +- search/all_3.js | 23 +- search/all_4.js | 15 +- search/all_5.js | 4 +- search/all_7.js | 5 +- search/all_8.js | 4 +- search/all_9.js | 6 +- search/all_a.js | 2 +- search/classes_1.js | 3 + search/classes_10.js | 1 + search/classes_11.js | 3 +- search/classes_12.js | 4 +- search/classes_2.js | 1 + search/classes_4.js | 3 +- search/classes_6.js | 3 +- search/classes_8.js | 4 +- search/classes_9.js | 2 +- search/classes_e.js | 6 +- search/classes_f.js | 2 +- search/files_11.js | 1 + search/files_12.js | 1 + search/files_3.js | 1 + search/functions_0.js | 2 +- search/functions_10.js | 2 +- search/functions_11.js | 2 +- search/functions_12.js | 1 + search/functions_13.js | 3 +- search/functions_2.js | 4 +- search/functions_3.js | 1 + search/functions_7.js | 4 +- search/properties_0.js | 2 + search/properties_4.js | 1 + search/variables_1.js | 1 + search/variables_11.js | 3 +- search/variables_12.js | 4 +- search/variables_2.js | 1 + search/variables_f.js | 1 + ..._8_server_8_host_2_program_8cs_source.html | 5 +- ..._2_release_notes_2_program_8cs_source.html | 3 +- 330 files changed, 4153 insertions(+), 1399 deletions(-) create mode 100644 _control_panel_configuration_8cs.html create mode 100644 _control_panel_configuration_8cs_source.html create mode 100644 _service_collection_extensions_8cs.html create mode 100644 _service_collection_extensions_8cs_source.html create mode 100644 _test_service_collection_extensions_8cs.html create mode 100644 _test_service_collection_extensions_8cs_source.html delete mode 100644 class_exception.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_control_panel_configuration-members.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_control_panel_configuration.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_service_collection_extensions-members.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_service_collection_extensions.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions-members.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config1-members.html rename class_exception.html => class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config1.html (52%) create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config2-members.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config2.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config3.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_good_config-members.html create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_good_config.html rename dir_4e036e007444785d010e90b5c9a24acd.html => dir_009a3598be1715e27911e1588bdd551c.html (94%) rename dir_91e2bbee1291fcb0db7e0417b617d01e.html => dir_019c6d3af924151b681494cc466be1ac.html (93%) rename dir_53c056aa6a96f034cca75f793964be6e.html => dir_0b6a380fe7fc1e1d640d1ac854fc3fb6.html (92%) rename dir_b2c89c850c962495793d09ea3a2ee385.html => dir_1083fcf52a7d94a2efffb6ca88246acf.html (95%) rename dir_53aae571c66a052729f8e64ccc397130.html => dir_2553b13cbb871373b0c30fb90d66217c.html (94%) rename dir_90400719279795a0c5ea912516a4c874.html => dir_2b9d136d9ffdab429a63a74fca28b300.html (92%) rename dir_ec133cefc1442f53ec97e0d7a57cf129.html => dir_2e39d68cad3f83629c74ea67329dead9.html (92%) rename dir_7ae0efc0613c36c848fa310214e0aee3.html => dir_2e67631cb23401d696bcfdbe81f86f0f.html (92%) rename dir_4122b363a9d577bed19bef337e757ff3.html => dir_48968f849f7ddb061f7130e079fe66a6.html (92%) rename dir_d45f494620bc0048ee4a31fa4f7391e1.html => dir_4c0c7e2f4fc60f51e2347a2d8dda94ce.html (94%) rename dir_0e792d76844bf2d7c79795a56a50ebec.html => dir_87039fa7b841d848ca75981d64237477.html (92%) rename dir_6db5af7fcfa17a34c7abf23e195af4db.html => dir_9257d56e926145e494a5aa5279887044.html (94%) rename dir_13bee0ed20ecc83b9bcd4b869b726d42.html => dir_a3714ab037275c284988fc90b4d045a5.html (95%) rename dir_fcc41a8faa41492aba200a49fc7d0aed.html => dir_a8807eef6592489c6a360d587f5b1a66.html (93%) rename dir_900257602d294c4a4799e88420919068.html => dir_b551a02288ef39e099594710382363a0.html (95%) rename dir_e928c7e494884ad322b5f6005c2954e1.html => dir_b87cbb2c60cf682b61fbfcceb9bced43.html (94%) rename dir_40138fd4849573e743490d583f6d06d6.html => dir_b92573f47b933d7a3661c26d1af2098b.html (93%) rename dir_18a5e30c14613227f95d12bd9fddc614.html => dir_be9f3ed3485a880e3213506a311823d1.html (92%) rename dir_298f2a364723f4f4cea115538f944a94.html => dir_c5f98c05147bcbba78e71856485f5049.html (95%) rename dir_83a6f600c846cd444f33e44bb9b4f9cb.html => dir_d8a279f7113e0073f6cd65dd050c1475.html (94%) rename dir_c65d377d3c73fb4a07b3637f9c0caf4d.html => dir_e97d380c3eebaf0856a4ec180d749620.html (93%) rename dir_6c8d8f20501e8e412458f542d0998195.html => dir_ea36b5ab01ad6e8b607438a02ae91964.html (93%) rename dir_383ca23c40357ede4cca827b4a365c76.html => dir_f88948f9bde0555374e8a59a663bdfc9.html (94%) rename dir_7a9d035dcfcd6d9c7aa1efef4b6b58a4.html => dir_fab1362e86740a2c573ef875dd474a67.html (92%) diff --git a/_administration_controller_8cs_source.html b/_administration_controller_8cs_source.html index 723546d96e..7d1cfc7c7d 100644 --- a/_administration_controller_8cs_source.html +++ b/_administration_controller_8cs_source.html @@ -116,7 +116,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Configuration for the automatic update system
Routes to a server actions
Definition: Routes.cs:9
-
+
AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger< AdministrationController > logger, IOptions< UpdatesConfiguration > updatesConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct an AdministrationController
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the AdministrationController
An ApiController representing a TModel
diff --git a/_api_conflict_exception_8cs_source.html b/_api_conflict_exception_8cs_source.html index 42aa827bda..7dc710f9f7 100644 --- a/_api_conflict_exception_8cs_source.html +++ b/_api_conflict_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiConflictException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class ApiConflictException : ClientException
11  {
17  public ApiConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage ?? new ErrorMessage
18  {
19  Message = "An unknown API error occurred!",
20  SeverApiVersion = null
21  }, statusCode)
22  { }
23 
27  public ApiConflictException() { }
28 
33  public ApiConflictException(string message) : base(message) { }
34 
40  public ApiConflictException(string message, Exception innerException) : base(message, innerException) { }
41  }
42 }
- +Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class ApiConflictException : ClientException
11  {
17  public ApiConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage ?? new ErrorMessage
18  {
19  Message = "An unknown API error occurred!",
20  SeverApiVersion = null
21  }, statusCode)
22  { }
23 
27  public ApiConflictException() { }
28 
33  public ApiConflictException(string message) : base(message) { }
34 
40  public ApiConflictException(string message, Exception innerException) : base(message, innerException) { }
41  }
42 }
ApiConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode)
Construct an ApiConflictException using an errorMessage
diff --git a/_api_controller_8cs_source.html b/_api_controller_8cs_source.html index 60d0697d52..04ddb01e74 100644 --- a/_api_controller_8cs_source.html +++ b/_api_controller_8cs_source.html @@ -92,12 +92,12 @@ 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  [Consumes(ApiHeaders.ApplicationJson)]
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 
61  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
62  {
63  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
64  if (authenticationContextFactory == null)
65  throw new ArgumentNullException(nameof(authenticationContextFactory));
66  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
69  this.requireInstance = requireInstance;
70  }
71 
73  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
74  {
75  //ALL valid token and login requests that match a route go through this function
76  //404 is returned before
77 
78  if (AuthenticationContext != null && AuthenticationContext.User == null)
79  {
80  //valid token, expired password
81  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
82  return;
83  }
84 
85  //validate the headers
86  try
87  {
88  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
89 
90  if (!ApiHeaders.Compatible())
91  {
92  await StatusCode((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
93  {
94  Message = "Provided API version is incompatible with server version!"
95  }).ExecuteResultAsync(context).ConfigureAwait(false);
96  return;
97  }
98 
99  if (requireInstance)
100  {
101  if (!ApiHeaders.InstanceId.HasValue)
102  {
103  await BadRequest(new ErrorMessage { Message = "Missing Instance header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
104  return;
105  }
107  {
108  //accessing an instance they don't have access to or one that's disabled
109  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
110  return;
111  }
112  }
113  }
114  catch (InvalidOperationException e)
115  {
116  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
117  return;
118  }
119 
120  if (ModelState?.IsValid == false)
121  {
122  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
123  //HACK
124  //do some fuckery to remove RequiredAttribute errors
125  for (var I = 0; I < errorMessages.Count; ++I)
126  {
127  var message = errorMessages[I];
128  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
129  {
130  errorMessages.RemoveAt(I);
131  --I;
132  }
133  }
134  if (errorMessages.Count > 0)
135  {
136  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
137  return;
138  }
139  }
140 
141  Logger.LogDebug("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path, Request.QueryString, ApiHeaders.InstanceId);
142 
143  try
144  {
145  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
146  }
147  catch (OperationCanceledException e)
148  {
149  Logger.LogDebug("Request cancelled! Exception: {0}", e);
150  throw;
151  }
152  }
153  }
154 }
+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  [Consumes(ApiHeaders.ApplicationJson)]
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  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
81  {
82  //ALL valid token and login requests that match a route go through this function
83  //404 is returned before
84 
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((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
100  {
101  Message = "Provided API version is incompatible with server version!"
102  }).ExecuteResultAsync(context).ConfigureAwait(false);
103  return;
104  }
105 
106  if (requireInstance)
107  {
108  if (!ApiHeaders.InstanceId.HasValue)
109  {
110  await BadRequest(new ErrorMessage { Message = "Missing Instance header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
111  return;
112  }
114  {
115  //accessing an instance they don't have access to or one that's disabled
116  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
117  return;
118  }
119  }
120  }
121  catch (InvalidOperationException e)
122  {
123  if (requireHeaders)
124  {
125  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
126  return;
127  }
128  }
129 
130  if (ModelState?.IsValid == false)
131  {
132  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
133  //HACK
134  //do some fuckery to remove RequiredAttribute errors
135  for (var I = 0; I < errorMessages.Count; ++I)
136  {
137  var message = errorMessages[I];
138  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
139  {
140  errorMessages.RemoveAt(I);
141  --I;
142  }
143  }
144  if (errorMessages.Count > 0)
145  {
146  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
147  return;
148  }
149  }
150 
151  if (ApiHeaders != null)
152  Logger.LogDebug("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path, Request.QueryString, ApiHeaders.InstanceId);
153 
154  try
155  {
156  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
157  }
158  catch (OperationCanceledException e)
159  {
160  Logger.LogDebug("Request cancelled! Exception: {0}", e);
161  throw;
162  }
163  }
164  }
165 }
-
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
Construct an ApiController
+
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
Construct an ApiController
-
override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+
override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
const string ApplicationJson
TODO: Remove this when https://github.com/dotnet/corefx/pull/26701 makes it into the sdk ...
Definition: ApiHeaders.cs:21
@@ -115,6 +115,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); +
readonly bool requireHeaders
If ApiHeaders are required
long Id
The ID of the User
Definition: User.cs:15
readonly bool requireInstance
If IAuthenticationContext.InstanceUser permissions are required to access the ApiController ...
Metadata about a server instance
Definition: Instance.cs:9
diff --git a/_api_mismatch_exception_8cs_source.html b/_api_mismatch_exception_8cs_source.html index f498deea61..bf550a9559 100644 --- a/_api_mismatch_exception_8cs_source.html +++ b/_api_mismatch_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiMismatchException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class ApiMismatchException : ClientException
11  {
16  public ApiMismatchException(ErrorMessage errorMessage) : base(errorMessage, HttpStatusCode.BadRequest)
17  {
18  if (errorMessage == null)
19  throw new ArgumentNullException(nameof(errorMessage));
20  }
21 
25  public ApiMismatchException() { }
26 
31  public ApiMismatchException(string message) : base(message) { }
32 
38  public ApiMismatchException(string message, Exception innerException) : base(message, innerException) { }
39  }
40 }
- +Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class ApiMismatchException : ClientException
11  {
16  public ApiMismatchException(ErrorMessage errorMessage) : base(errorMessage, HttpStatusCode.BadRequest)
17  {
18  if (errorMessage == null)
19  throw new ArgumentNullException(nameof(errorMessage));
20  }
21 
25  public ApiMismatchException() { }
26 
31  public ApiMismatchException(string message) : base(message) { }
32 
38  public ApiMismatchException(string message, Exception innerException) : base(message, innerException) { }
39  }
40 }
diff --git a/_application_8cs_source.html b/_application_8cs_source.html index 92bcb81805..0ebd634bbf 100644 --- a/_application_8cs_source.html +++ b/_application_8cs_source.html @@ -92,9 +92,8 @@ 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.Hosting;
6 using Microsoft.AspNetCore.Identity;
7 using Microsoft.Extensions.Configuration;
8 using Microsoft.Extensions.DependencyInjection;
9 using Microsoft.Extensions.Hosting;
10 using Microsoft.Extensions.Logging;
11 using Microsoft.Extensions.Options;
12 using Microsoft.Extensions.Primitives;
13 using Newtonsoft.Json;
14 using Newtonsoft.Json.Converters;
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.Reflection;
22 using System.Runtime.InteropServices;
23 using System.Threading.Tasks;
30 using Tgstation.Server.Host.IO;
33 
34 namespace Tgstation.Server.Host.Core
35 {
37  sealed class Application : IApplication
38  {
40  public string VersionPrefix => "tgstation-server";
41 
43  public Version Version { get; }
44 
46  public string VersionString { get; }
47 
51  readonly IConfiguration configuration;
52 
56  readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment;
57 
61  readonly TaskCompletionSource<object> startupTcs;
62 
67 
73  public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
74  {
75  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
76  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
77 
78  startupTcs = new TaskCompletionSource<object>();
79 
80  Version = Assembly.GetExecutingAssembly().GetName().Version;
81  VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
82  }
83 
88  public void ConfigureServices(IServiceCollection services)
89  {
90  if (services == null)
91  throw new ArgumentNullException(nameof(services));
92 
93  //needful
94  services.AddSingleton<IApplication>(this);
95 
96  //configure configuration
97  services.Configure<UpdatesConfiguration>(configuration.GetSection(UpdatesConfiguration.Section));
98  services.Configure<DatabaseConfiguration>(configuration.GetSection(DatabaseConfiguration.Section));
99  services.Configure<GeneralConfiguration>(configuration.GetSection(GeneralConfiguration.Section));
100  services.Configure<FileLoggingConfiguration>(configuration.GetSection(FileLoggingConfiguration.Section));
101 
102  //enable options which give us config reloading
103  services.AddOptions();
104 
105  //other stuff needed for for setup wizard and configuration
106  services.AddSingleton<IIOManager, DefaultIOManager>();
107  services.AddSingleton<IConsole, IO.Console>();
108  services.AddSingleton<IDBConnectionFactory, DBConnectionFactory>();
109  services.AddSingleton<ISetupWizard, SetupWizard>();
110  services.AddSingleton<IPlatformIdentifier, PlatformIdentifier>();
111  services.AddSingleton<IAsyncDelayer, AsyncDelayer>();
112 
113  GeneralConfiguration generalConfiguration;
114  DatabaseConfiguration databaseConfiguration;
115  FileLoggingConfiguration fileLoggingConfiguration;
116  IIOManager ioManager;
117  IPlatformIdentifier platformIdentifier;
118 
119  //temporarily build the service provider in it's current state
120  //do it here so we can run the setup wizard if necessary
121  //also allows us to get some options and other services we need for continued configuration
122  using (var provider = services.BuildServiceProvider())
123  {
124  //run the wizard if necessary
125  var setupWizard = provider.GetRequiredService<ISetupWizard>();
126  var applicationLifetime = provider.GetRequiredService<Microsoft.AspNetCore.Hosting.IApplicationLifetime>();
127  var setupWizardRan = setupWizard.CheckRunWizard(applicationLifetime.ApplicationStopping).GetAwaiter().GetResult();
128 
129  //load the configuration options we need
130  var generalOptions = provider.GetRequiredService<IOptions<GeneralConfiguration>>();
131  generalConfiguration = generalOptions.Value;
132 
133  //unless this is set, in which case, we leave
134  if (setupWizardRan && generalConfiguration.SetupWizardMode == SetupWizardMode.Only)
135  //we don't inject a logger in the constuctor to log this because it's not yet configured
136  throw new OperationCanceledException("Exiting due to SetupWizardMode configuration!");
137 
138  var dbOptions = provider.GetRequiredService<IOptions<DatabaseConfiguration>>();
139  databaseConfiguration = dbOptions.Value;
140 
141  var loggingOptions = provider.GetRequiredService<IOptions<FileLoggingConfiguration>>();
142  fileLoggingConfiguration = loggingOptions.Value;
143 
144  ioManager = provider.GetRequiredService<IIOManager>();
145  platformIdentifier = provider.GetRequiredService<IPlatformIdentifier>();
146  }
147 
148  //setup file logging via serilog
149  if (!fileLoggingConfiguration.Disable)
150  services.AddLogging(builder =>
151  {
152  //common app data is C:/ProgramData on windows, else /usr/shar
153  var logPath = !String.IsNullOrEmpty(fileLoggingConfiguration.Directory) ? fileLoggingConfiguration.Directory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
154 
155  logPath = ioManager.ConcatPath(logPath, "tgs-{Date}.log");
156 
157  LogEventLevel? ConvertLogLevel(LogLevel logLevel)
158  {
159  switch (logLevel)
160  {
161  case LogLevel.Critical:
162  return LogEventLevel.Fatal;
163  case LogLevel.Debug:
164  return LogEventLevel.Debug;
165  case LogLevel.Error:
166  return LogEventLevel.Error;
167  case LogLevel.Information:
168  return LogEventLevel.Information;
169  case LogLevel.Trace:
170  return LogEventLevel.Verbose;
171  case LogLevel.Warning:
172  return LogEventLevel.Warning;
173  case LogLevel.None:
174  return null;
175  default:
176  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel));
177  }
178  };
179 
180  var logEventLevel = ConvertLogLevel(fileLoggingConfiguration.LogLevel);
181  var microsoftEventLevel = ConvertLogLevel(fileLoggingConfiguration.MicrosoftLogLevel);
182 
183  var formatter = new MessageTemplateTextFormatter("{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", null);
184 
185  var configuration = new LoggerConfiguration()
186  .Enrich.FromLogContext()
187  .WriteTo.Async(w => w.RollingFile(formatter, logPath, shared: true, flushToDiskInterval: TimeSpan.FromSeconds(2)));
188 
189  if (logEventLevel.HasValue)
190  configuration.MinimumLevel.Is(logEventLevel.Value);
191 
192  if (microsoftEventLevel.HasValue)
193  configuration.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
194 
195  builder.AddSerilog(configuration.CreateLogger(), true);
196  });
197 
198  //configure bearer token validation
199  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
200  {
201  //this line isn't actually run until the first request is made
202  //at that point tokenFactory will be populated
203  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
204  jwtBearerOptions.Events = new JwtBearerEvents
205  {
206  //Application is our composition root so this monstrosity of a line is okay
207  OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService<IClaimsInjector>().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted)
208  };
209  });
210  //fucking converts 'sub' to M$ bs
211  //can't be done in the above lambda, that's too late
212  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
213 
214  //add mvc, configure the json serializer settings
215  services.AddMvc().AddJsonOptions(options =>
216  {
217  options.AllowInputFormatterExceptionMessages = true;
218  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
219  options.SerializerSettings.CheckAdditionalContent = true;
220  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
221  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
222  options.SerializerSettings.Converters = new[] { new VersionConverter() };
223  });
224 
225  void AddTypedContext<TContext>() where TContext : DatabaseContext<TContext>
226  {
227  services.AddDbContext<TContext>(builder =>
228  {
229  if (hostingEnvironment.IsDevelopment())
230  builder.EnableSensitiveDataLogging();
231  });
232  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
233  }
234 
235  //add the correct database context type
236  var dbType = databaseConfiguration.DatabaseType;
237  switch (dbType)
238  {
239  case DatabaseType.MySql:
240  case DatabaseType.MariaDB:
241  AddTypedContext<MySqlDatabaseContext>();
242  break;
243  case DatabaseType.SqlServer:
244  AddTypedContext<SqlServerDatabaseContext>();
245  break;
246  default:
247  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
248  }
249 
250  //configure other database services
251  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
252  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
253 
254  //configure security services
256  services.AddScoped<IClaimsInjector, ClaimsInjector>();
257  services.AddSingleton<IIdentityCache, IdentityCache>();
258  services.AddSingleton<ICryptographySuite, CryptographySuite>();
259  services.AddSingleton<ITokenFactory, TokenFactory>();
260  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
261 
262  //configure platform specific services
263  if (platformIdentifier.IsWindows)
264  {
265  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
266  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
267  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
268  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
269 
270  services.AddSingleton<WindowsNetworkPromptReaper>();
271  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
272  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
273  }
274  else
275  {
276  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
277  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
278  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
279  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
280  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
281  }
282 
283  //configure misc services
284  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
285  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
286  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
287  services.AddSingleton<IByondTopicSender>(new ByondTopicSender
288  {
289  ReceiveTimeout = generalConfiguration.ByondTopicTimeout,
290  SendTimeout = generalConfiguration.ByondTopicTimeout
291  });
292 
293  //configure component services
294  services.AddSingleton<ICredentialsProvider, CredentialsProvider>();
295  services.AddSingleton<IProviderFactory, ProviderFactory>();
296  services.AddSingleton<IChatFactory, ChatFactory>();
297  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
298  services.AddSingleton<IInstanceFactory, InstanceFactory>();
299 
300  //configure root services
301  services.AddSingleton<InstanceManager>();
302  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
303  services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
304 
305  services.AddSingleton<IJobManager, JobManager>();
306  }
307 
315  public void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, ILogger<Application> logger)
316  {
317  if (applicationBuilder == null)
318  throw new ArgumentNullException(nameof(applicationBuilder));
319  if (serverControl == null)
320  throw new ArgumentNullException(nameof(serverControl));
321 
322  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
323 
324  if (logger == null)
325  throw new ArgumentNullException(nameof(logger));
326 
327  logger.LogInformation(VersionString);
328 
329  //attempt to restart the server if the configuration changes
330  if(serverControl.WatchdogPresent)
331  ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart());
332 
333  //now setup the HTTP request pipeline
334 
335  //should anything after this throw an exception, catch it and display a detailed html page
336  applicationBuilder.UseDeveloperExceptionPage(); //it is not worth it to limit this, you should only ever get it if you're an authorized user
337 
338  //suppress OperationCancelledExceptions, they are just aborted HTTP requests
339  applicationBuilder.UseCancelledRequestSuppression();
340 
341  //Do not service requests until Ready is called, this will return 503 until that point
342  applicationBuilder.UseAsyncInitialization(async cancellationToken =>
343  {
344  using (cancellationToken.Register(() => startupTcs.SetCanceled()))
345  await startupTcs.Task.ConfigureAwait(false);
346  });
347 
348  //authenticate JWT tokens using our security pipeline if present, returns 401 if bad
349  applicationBuilder.UseAuthentication();
350 
351  //suppress and log database exceptions
352  applicationBuilder.UseDbConflictHandling();
353 
354  //majority of handling is done in the controllers
355  applicationBuilder.UseMvc();
356 
357  //404 anything that gets this far
358  }
359 
361  public void Ready(Exception initializationError)
362  {
363  lock (startupTcs)
364  {
365  if (startupTcs.Task.IsCompleted)
366  throw new InvalidOperationException("Ready has already been called!");
367  if (initializationError == null)
368  startupTcs.SetResult(null);
369  else
370  startupTcs.SetException(initializationError);
371  }
372  }
373  }
374 }
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 Newtonsoft.Json.Converters;
16 using Serilog;
17 using Serilog.Events;
18 using Serilog.Formatting.Display;
19 using System;
20 using System.Globalization;
21 using System.IdentityModel.Tokens.Jwt;
22 using System.Reflection;
23 using System.Threading.Tasks;
30 using Tgstation.Server.Host.IO;
33 
34 namespace Tgstation.Server.Host.Core
35 {
37  sealed class Application : IApplication
38  {
40  public string VersionPrefix => "tgstation-server";
41 
43  public Version Version { get; }
44 
46  public string VersionString { get; }
47 
51  readonly IConfiguration configuration;
52 
56  readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment;
57 
61  readonly TaskCompletionSource<object> startupTcs;
62 
67 
73  public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
74  {
75  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
76  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
77 
78  startupTcs = new TaskCompletionSource<object>();
79 
80  Version = Assembly.GetExecutingAssembly().GetName().Version;
81  VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
82  }
83 
88  public void ConfigureServices(IServiceCollection services)
89  {
90  if (services == null)
91  throw new ArgumentNullException(nameof(services));
92 
93  //needful
94  services.AddSingleton<IApplication>(this);
95 
96  //configure configuration
97  services.UseStandardConfig<UpdatesConfiguration>(configuration);
98  services.UseStandardConfig<DatabaseConfiguration>(configuration);
99  services.UseStandardConfig<GeneralConfiguration>(configuration);
100  services.UseStandardConfig<FileLoggingConfiguration>(configuration);
101  services.UseStandardConfig<ControlPanelConfiguration>(configuration);
102 
103  //enable options which give us config reloading
104  services.AddOptions();
105 
106  //other stuff needed for for setup wizard and configuration
107  services.AddSingleton<IIOManager, DefaultIOManager>();
108  services.AddSingleton<IConsole, IO.Console>();
109  services.AddSingleton<IDBConnectionFactory, DBConnectionFactory>();
110  services.AddSingleton<ISetupWizard, SetupWizard>();
111  services.AddSingleton<IPlatformIdentifier, PlatformIdentifier>();
112  services.AddSingleton<IAsyncDelayer, AsyncDelayer>();
113 
114  GeneralConfiguration generalConfiguration;
115  DatabaseConfiguration databaseConfiguration;
116  FileLoggingConfiguration fileLoggingConfiguration;
117  ControlPanelConfiguration controlPanelConfiguration;
118  IIOManager ioManager;
119  IPlatformIdentifier platformIdentifier;
120 
121  //temporarily build the service provider in it's current state
122  //do it here so we can run the setup wizard if necessary
123  //also allows us to get some options and other services we need for continued configuration
124  using (var provider = services.BuildServiceProvider())
125  {
126  //run the wizard if necessary
127  var setupWizard = provider.GetRequiredService<ISetupWizard>();
128  var applicationLifetime = provider.GetRequiredService<Microsoft.AspNetCore.Hosting.IApplicationLifetime>();
129  var setupWizardRan = setupWizard.CheckRunWizard(applicationLifetime.ApplicationStopping).GetAwaiter().GetResult();
130 
131  //load the configuration options we need
132  var generalOptions = provider.GetRequiredService<IOptions<GeneralConfiguration>>();
133  generalConfiguration = generalOptions.Value;
134 
135  //unless this is set, in which case, we leave
136  if (setupWizardRan && generalConfiguration.SetupWizardMode == SetupWizardMode.Only)
137  //we don't inject a logger in the constuctor to log this because it's not yet configured
138  throw new OperationCanceledException("Exiting due to SetupWizardMode configuration!");
139 
140  var dbOptions = provider.GetRequiredService<IOptions<DatabaseConfiguration>>();
141  databaseConfiguration = dbOptions.Value;
142 
143  var loggingOptions = provider.GetRequiredService<IOptions<FileLoggingConfiguration>>();
144  fileLoggingConfiguration = loggingOptions.Value;
145 
146  var controlPanelOptions = provider.GetRequiredService<IOptions<ControlPanelConfiguration>>();
147  controlPanelConfiguration = controlPanelOptions.Value;
148 
149  ioManager = provider.GetRequiredService<IIOManager>();
150  platformIdentifier = provider.GetRequiredService<IPlatformIdentifier>();
151  }
152 
153  //setup file logging via serilog
154  if (!fileLoggingConfiguration.Disable)
155  services.AddLogging(builder =>
156  {
157  //common app data is C:/ProgramData on windows, else /usr/shar
158  var logPath = !String.IsNullOrEmpty(fileLoggingConfiguration.Directory) ? fileLoggingConfiguration.Directory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
159 
160  logPath = ioManager.ConcatPath(logPath, "tgs-{Date}.log");
161 
162  LogEventLevel? ConvertLogLevel(LogLevel logLevel)
163  {
164  switch (logLevel)
165  {
166  case LogLevel.Critical:
167  return LogEventLevel.Fatal;
168  case LogLevel.Debug:
169  return LogEventLevel.Debug;
170  case LogLevel.Error:
171  return LogEventLevel.Error;
172  case LogLevel.Information:
173  return LogEventLevel.Information;
174  case LogLevel.Trace:
175  return LogEventLevel.Verbose;
176  case LogLevel.Warning:
177  return LogEventLevel.Warning;
178  case LogLevel.None:
179  return null;
180  default:
181  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel));
182  }
183  };
184 
185  var logEventLevel = ConvertLogLevel(fileLoggingConfiguration.LogLevel);
186  var microsoftEventLevel = ConvertLogLevel(fileLoggingConfiguration.MicrosoftLogLevel);
187 
188  var formatter = new MessageTemplateTextFormatter("{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", null);
189 
190  var configuration = new LoggerConfiguration()
191  .Enrich.FromLogContext()
192  .WriteTo.Async(w => w.RollingFile(formatter, logPath, shared: true, flushToDiskInterval: TimeSpan.FromSeconds(2)));
193 
194  if (logEventLevel.HasValue)
195  configuration.MinimumLevel.Is(logEventLevel.Value);
196 
197  if (microsoftEventLevel.HasValue)
198  configuration.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
199 
200  builder.AddSerilog(configuration.CreateLogger(), true);
201  });
202 
203  //configure bearer token validation
204  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
205  {
206  //this line isn't actually run until the first request is made
207  //at that point tokenFactory will be populated
208  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
209  jwtBearerOptions.Events = new JwtBearerEvents
210  {
211  //Application is our composition root so this monstrosity of a line is okay
212  OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService<IClaimsInjector>().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted)
213  };
214  });
215  //fucking converts 'sub' to M$ bs
216  //can't be done in the above lambda, that's too late
217  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
218 
219  //add mvc, configure the json serializer settings
220  services.AddMvc().AddJsonOptions(options =>
221  {
222  options.AllowInputFormatterExceptionMessages = true;
223  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
224  options.SerializerSettings.CheckAdditionalContent = true;
225  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
226  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
227  options.SerializerSettings.Converters = new[] { new VersionConverter() };
228  });
229 
230  //enable browser detection
231  services.AddDetectionCore().AddBrowser();
232 
233  //enable CORS if necessary
234  if (controlPanelConfiguration.AllowAnyOrigin || controlPanelConfiguration.AllowedOrigins?.Count > 0)
235  services.AddCors();
236 
237  void AddTypedContext<TContext>() where TContext : DatabaseContext<TContext>
238  {
239  services.AddDbContext<TContext>(builder =>
240  {
241  if (hostingEnvironment.IsDevelopment())
242  builder.EnableSensitiveDataLogging();
243  });
244  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
245  }
246 
247  //add the correct database context type
248  var dbType = databaseConfiguration.DatabaseType;
249  switch (dbType)
250  {
251  case DatabaseType.MySql:
252  case DatabaseType.MariaDB:
253  AddTypedContext<MySqlDatabaseContext>();
254  break;
255  case DatabaseType.SqlServer:
256  AddTypedContext<SqlServerDatabaseContext>();
257  break;
258  default:
259  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
260  }
261  //configure other database services
262  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
263  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
264 
265  //configure security services
267  services.AddScoped<IClaimsInjector, ClaimsInjector>();
268  services.AddSingleton<IIdentityCache, IdentityCache>();
269  services.AddSingleton<ICryptographySuite, CryptographySuite>();
270  services.AddSingleton<ITokenFactory, TokenFactory>();
271  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
272 
273  //configure platform specific services
274  if (platformIdentifier.IsWindows)
275  {
276  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
277  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
278  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
279  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
280 
281  services.AddSingleton<WindowsNetworkPromptReaper>();
282  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
283  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
284  }
285  else
286  {
287  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
288  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
289  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
290  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
291  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
292  }
293 
294  //configure misc services
295  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
296  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
297  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
298  services.AddSingleton<IByondTopicSender>(new ByondTopicSender
299  {
300  ReceiveTimeout = generalConfiguration.ByondTopicTimeout,
301  SendTimeout = generalConfiguration.ByondTopicTimeout
302  });
303 
304  //configure component services
305  services.AddSingleton<ICredentialsProvider, CredentialsProvider>();
306  services.AddSingleton<IProviderFactory, ProviderFactory>();
307  services.AddSingleton<IChatFactory, ChatFactory>();
308  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
309  services.AddSingleton<IInstanceFactory, InstanceFactory>();
310 
311  //configure root services
312  services.AddSingleton<InstanceManager>();
313  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
314  services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
315 
316  services.AddSingleton<IJobManager, JobManager>();
317  }
318 
327  public void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions, 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 
338  if (logger == null)
339  throw new ArgumentNullException(nameof(logger));
340 
341  logger.LogInformation(VersionString);
342  logger.LogDebug("Content Root: {0}", hostingEnvironment.ContentRootPath);
343  logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
344 
345  //attempt to restart the server if the configuration changes
346  if(serverControl.WatchdogPresent)
347  ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart());
348 
349  //now setup the HTTP request pipeline
350 
351  //should anything after this throw an exception, catch it and display a detailed html page
352  applicationBuilder.UseDeveloperExceptionPage(); //it is not worth it to limit this, you should only ever get it if you're an authorized user
353 
354  //suppress OperationCancelledExceptions, they are just aborted HTTP requests
355  applicationBuilder.UseCancelledRequestSuppression();
356 
357  //Set up CORS based on configuration if necessary
358  Action<CorsPolicyBuilder> corsBuilder = null;
359  if (controlPanelConfiguration.AllowAnyOrigin)
360  {
361  logger.LogTrace("Access-Control-Allow-Origin: *");
362  corsBuilder = builder => builder.AllowAnyOrigin();
363  }
364  else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
365  {
366  logger.LogTrace("Access-Control-Allow-Origin: ", String.Join(',', controlPanelConfiguration.AllowedOrigins));
367  corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
368  }
369 
370  if (corsBuilder != null)
371  {
372  var originalBuilder = corsBuilder;
373  corsBuilder = builder => originalBuilder(builder.AllowAnyHeader().AllowAnyMethod());
374  applicationBuilder.UseCors(corsBuilder);
375  }
376 
377  //Do not service requests until Ready is called, this will return 503 until that point
378  applicationBuilder.UseAsyncInitialization(async cancellationToken =>
379  {
380  using (cancellationToken.Register(() => startupTcs.SetCanceled()))
381  await startupTcs.Task.ConfigureAwait(false);
382  });
383 
384  //spa loading if necessary
385  if (controlPanelConfiguration.Enable)
386  {
387  logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
388  applicationBuilder.UseStaticFiles();
389  }
390  else
391  logger.LogDebug("Web control panel disabled!");
392 
393  //authenticate JWT tokens using our security pipeline if present, returns 401 if bad
394  applicationBuilder.UseAuthentication();
395 
396  //suppress and log database exceptions
397  applicationBuilder.UseDbConflictHandling();
398 
399  //majority of handling is done in the controllers
400  applicationBuilder.UseMvc();
401 
402  //404 anything that gets this far
403  }
404 
406  public void Ready(Exception initializationError)
407  {
408  lock (startupTcs)
409  {
410  if (startupTcs.Task.IsCompleted)
411  throw new InvalidOperationException("Ready has already been called!");
412  if (initializationError == null)
413  startupTcs.SetResult(null);
414  else
415  startupTcs.SetException(initializationError);
416  }
417  }
418  }
419 }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
-
Manages the runtime of Jobs
Definition: IJobManager.cs:12
@@ -105,7 +104,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
+
List< string > AllowedOrigins
Origins allowed for CORS requests
+
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:327
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
@@ -121,7 +122,6 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ITokenFactory tokenFactory
The ITokenFactory for the Application
Definition: Application.cs:66
-
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
For initially seeding a database
@@ -130,13 +130,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the DatabaseConfiguration re...
ISystemIdentityFactory for windows systems. Uses long running tasks due to potential networked domain...
Contains various cryptographic functions
+
bool AllowAnyOrigin
If any origin is allowed for CORS requests. This overrides AllowedOrigins
Configuration for the automatic update system
For downloading and installing BYOND extractions for a given system
@@ -144,13 +144,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseType DatabaseType
The Configuration.DatabaseType to create
-
void Ready(Exception initializationError)
Mark the IApplication as ready to run
Definition: Application.cs:361
+
void Ready(Exception initializationError)
Mark the IApplication as ready to run
Definition: Application.cs:406
-
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the UpdatesConfiguration res...
- +
IPostWriteHandler for Windows systems
+
bool IsWindows
If the current platform is a Windows platform
@@ -181,13 +181,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
The command line Configuration setup wizard
Definition: ISetupWizard.cs:9
-
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the FileLoggingConfiguration...
Task< bool > CheckRunWizard(CancellationToken cancellationToken)
Run the setup wizard if necessary
Interface for using filesystems
Definition: IIOManager.cs:11
-
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:315
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
For identifying the current platform
diff --git a/_byond_manager_8cs_source.html b/_byond_manager_8cs_source.html index 214ad8ce16..410613956f 100644 --- a/_byond_manager_8cs_source.html +++ b/_byond_manager_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ByondManager.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.Linq;
6 using System.Net;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Byond
14 {
16  sealed class ByondManager : IByondManager
17  {
21  public const string BinPath = "byond/bin";
22 
26  const string VersionFileName = "Version.txt";
30  const string ActiveVersionFileName = "ActiveVersion.txt";
31 
33  public Version ActiveVersion { get; private set; }
34 
36  public IReadOnlyList<Version> InstalledVersions
37  {
38  get
39  {
40  lock (installedVersions)
41  return installedVersions.Select(x => Version.Parse(x.Key)).ToList();
42  }
43  }
44 
49 
54 
59 
63  readonly ILogger<ByondManager> logger;
64 
68  readonly Dictionary<string, Task> installedVersions;
69 
73  readonly SemaphoreSlim semaphore;
74 
80  static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
81 
89  public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger<ByondManager> logger)
90  {
91  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
92  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
93  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
94  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
95 
96  installedVersions = new Dictionary<string, Task>();
97  semaphore = new SemaphoreSlim(1);
98  }
99 
101  public void Dispose() => semaphore.Dispose();
102 
109  async Task InstallVersion(Version version, CancellationToken cancellationToken)
110  {
111  var ourTcs = new TaskCompletionSource<object>();
112  Task inProgressTask;
113 
114  var versionKey = VersionKey(version);
115  bool installed;
116  lock (installedVersions)
117  {
118  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
119  if (!installed)
120  installedVersions.Add(versionKey, ourTcs.Task);
121  }
122  if (installed)
123  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
124  {
125  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
126  cancellationToken.ThrowIfCancellationRequested();
127  return;
128  }
129  //okay up to us to install it then
130  try
131  {
132  await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
133  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
134 
135  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
136  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
137 
138  try
139  {
140  var download = await downloadTask.ConfigureAwait(false);
141  await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
142  await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
143 
144  //make sure to do this last because this is what tells us we have a valid version in the future
145  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
146  }
147  catch (WebException e)
148  {
149  //since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
150  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message));
151  }
152  catch (OperationCanceledException)
153  {
154  throw;
155  }
156  catch
157  {
158  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
159  throw;
160  }
161 
162  ourTcs.SetResult(null);
163  }
164  catch (Exception e)
165  {
166  if (!(e is OperationCanceledException))
167  await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken).ConfigureAwait(false);
168  lock (installedVersions)
169  installedVersions.Remove(versionKey);
170  ourTcs.SetException(e);
171  throw;
172  }
173  }
174 
176  public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
177  {
178  if (version == null)
179  throw new ArgumentNullException(nameof(version));
180  var versionKey = VersionKey(version);
181  await InstallVersion(version, cancellationToken).ConfigureAwait(false);
182  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
183  {
184  await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false);
185  await eventConsumer.HandleEvent(EventType.ByondActiveVersionChange, new List<string> { ActiveVersion != null ? VersionKey(ActiveVersion) : null, versionKey }, cancellationToken).ConfigureAwait(false);
186  ActiveVersion = version;
187  }
188  }
189 
191  public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
192  {
193  var versionToUse = requiredVersion ?? ActiveVersion;
194  if (versionToUse == null)
195  throw new JobException("No BYOND versions installed!");
196  await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false);
197 
198  var versionKey = VersionKey(versionToUse);
199 
200  return new ByondExecutableLock(versionToUse, ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)), ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)));
201  }
202 
204  public async Task StartAsync(CancellationToken cancellationToken)
205  {
206  async Task<byte[]> GetActiveVersion()
207  {
208  if (!await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false))
209  return null;
210  return await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
211  }
212 
213  var activeVersionBytesTask = GetActiveVersion();
214 
215  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
216  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
217 
218  async Task ReadVersion(string path)
219  {
220  var versionFile = ioManager.ConcatPath(path, VersionFileName);
221  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
222  {
223  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
224  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); //cleanup
225  return;
226  }
227  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
228  var text = Encoding.UTF8.GetString(bytes);
229  if (Version.TryParse(text, out var version))
230  {
231  var key = VersionKey(version);
232  lock (installedVersions)
233  if (!installedVersions.ContainsKey(key))
234  {
235  installedVersions.Add(key, Task.CompletedTask);
236  return;
237  }
238  }
239  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
240  };
241 
242  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
243 
244  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
245  if (activeVersionBytes != null)
246  {
247  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
248  if (Version.TryParse(activeVersionString, out var activeVersion))
249  ActiveVersion = activeVersion;
250  else
251  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
252  }
253  }
254 
256  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
257  }
258 }
-
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:48
+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.Linq;
6 using System.Net;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Byond
14 {
16  sealed class ByondManager : IByondManager
17  {
21  public const string BinPath = "byond/bin";
22 
26  const string VersionFileName = "Version.txt";
30  const string ActiveVersionFileName = "ActiveVersion.txt";
31 
33  public Version ActiveVersion { get; private set; }
34 
36  public IReadOnlyList<Version> InstalledVersions
37  {
38  get
39  {
40  lock (installedVersions)
41  return installedVersions.Select(x => Version.Parse(x.Key)).ToList();
42  }
43  }
44 
49 
54 
59 
63  readonly ILogger<ByondManager> logger;
64 
68  readonly Dictionary<string, Task> installedVersions;
69 
73  readonly SemaphoreSlim semaphore;
74 
80  static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
81 
89  public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger<ByondManager> logger)
90  {
91  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
92  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
93  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
94  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
95 
96  installedVersions = new Dictionary<string, Task>();
97  semaphore = new SemaphoreSlim(1);
98  }
99 
101  public void Dispose() => semaphore.Dispose();
102 
109  async Task InstallVersion(Version version, CancellationToken cancellationToken)
110  {
111  var ourTcs = new TaskCompletionSource<object>();
112  Task inProgressTask;
113 
114  var versionKey = VersionKey(version);
115  bool installed;
116  lock (installedVersions)
117  {
118  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
119  if (!installed)
120  installedVersions.Add(versionKey, ourTcs.Task);
121  }
122  if (installed)
123  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
124  {
125  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
126  cancellationToken.ThrowIfCancellationRequested();
127  return;
128  }
129  //okay up to us to install it then
130  try
131  {
132  await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
133  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
134 
135  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
136  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
137 
138  try
139  {
140  var download = await downloadTask.ConfigureAwait(false);
141  await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
142  await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
143 
144  //make sure to do this last because this is what tells us we have a valid version in the future
145  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
146  }
147  catch (WebException e)
148  {
149  //since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
150  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message));
151  }
152  catch (OperationCanceledException)
153  {
154  throw;
155  }
156  catch
157  {
158  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
159  throw;
160  }
161 
162  ourTcs.SetResult(null);
163  }
164  catch (Exception e)
165  {
166  if (!(e is OperationCanceledException))
167  await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken).ConfigureAwait(false);
168  lock (installedVersions)
169  installedVersions.Remove(versionKey);
170  ourTcs.SetException(e);
171  throw;
172  }
173  }
174 
176  public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
177  {
178  if (version == null)
179  throw new ArgumentNullException(nameof(version));
180  var versionKey = VersionKey(version);
181  await InstallVersion(version, cancellationToken).ConfigureAwait(false);
182  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
183  {
184  await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false);
185  await eventConsumer.HandleEvent(EventType.ByondActiveVersionChange, new List<string> { ActiveVersion != null ? VersionKey(ActiveVersion) : null, versionKey }, cancellationToken).ConfigureAwait(false);
186  ActiveVersion = version;
187  }
188  }
189 
191  public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
192  {
193  var versionToUse = requiredVersion ?? ActiveVersion;
194  if (versionToUse == null)
195  throw new JobException("No BYOND versions installed!");
196  await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false);
197 
198  var versionKey = VersionKey(versionToUse);
199 
200  return new ByondExecutableLock(versionToUse, ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)), ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)));
201  }
202 
204  public async Task StartAsync(CancellationToken cancellationToken)
205  {
206  async Task<byte[]> GetActiveVersion()
207  {
208  if (!await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false))
209  return null;
210  return await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
211  }
212 
213  var activeVersionBytesTask = GetActiveVersion();
214 
215  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
216  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
217 
218  async Task ReadVersion(string path)
219  {
220  var versionFile = ioManager.ConcatPath(path, VersionFileName);
221  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
222  {
223  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
224  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); //cleanup
225  return;
226  }
227  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
228  var text = Encoding.UTF8.GetString(bytes);
229  if (Version.TryParse(text, out var version))
230  {
231  var key = VersionKey(version);
232  lock (installedVersions)
233  if (!installedVersions.ContainsKey(key))
234  {
235  installedVersions.Add(key, Task.CompletedTask);
236  return;
237  }
238  }
239  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
240  };
241 
242  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
243 
244  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
245  if (activeVersionBytes != null)
246  {
247  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
248  if (Version.TryParse(activeVersionString, out var activeVersion))
249  ActiveVersion = activeVersion;
250  else
251  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
252  }
253  }
254 
256  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
257  }
258 }
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:48
Consumes EventTypes and takes the appropriate actions
diff --git a/_chat_8cs_source.html b/_chat_8cs_source.html index 61da661081..b170b56794 100644 --- a/_chat_8cs_source.html +++ b/_chat_8cs_source.html @@ -92,9 +92,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Chat.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  sealed class Chat : IChat, IRestartHandler
19  {
20  const string CommonMention = "!tgs";
21 
26 
31 
36 
41 
46 
50  readonly ILoggerFactory loggerFactory;
51 
55  readonly ILogger<Chat> logger;
56 
60  readonly Dictionary<string, ICommand> builtinCommands;
61 
65  readonly Dictionary<long, IProvider> providers;
66 
70  readonly Dictionary<ulong, ChannelMapping> mappedChannels;
71 
75  readonly List<IJsonTrackingContext> trackingContexts;
76 
80  readonly CancellationTokenSource handlerCts;
81 
85  readonly List<Models.ChatBot> initialChatBots;
86 
91 
96 
100  TaskCompletionSource<object> connectionsUpdated;
101 
106 
110  bool started;
111 
123  public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<Chat> logger, IEnumerable<Models.ChatBot> initialChatBots)
124  {
125  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
126  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
127  this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
128  if (serverControl == null)
129  throw new ArgumentNullException(nameof(serverControl));
130  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
131  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
132  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
133  this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
134 
135  restartRegistration = serverControl.RegisterForRestart(this);
136 
137  builtinCommands = new Dictionary<string, ICommand>();
138  providers = new Dictionary<long, IProvider>();
139  mappedChannels = new Dictionary<ulong, ChannelMapping>();
140  trackingContexts = new List<IJsonTrackingContext>();
141  handlerCts = new CancellationTokenSource();
142  connectionsUpdated = new TaskCompletionSource<object>();
143  channelIdCounter = 1;
144  }
145 
147  public void Dispose()
148  {
149  restartRegistration.Dispose();
150  handlerCts.Dispose();
151  foreach (var I in providers)
152  I.Value.Dispose();
153  }
154 
162  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
163  {
164  IProvider provider;
165  lock (providers)
166  if (!providers.TryGetValue(connectionId, out provider))
167  return null;
168  Task task;
169  lock (mappedChannels)
170  {
171  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
172  mappedChannels.Remove(I);
173 
174  if (updateTrackings)
175  lock (trackingContexts)
176  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
177  else
178  task = Task.CompletedTask;
179  }
180  await task.ConfigureAwait(false);
181  return provider;
182  }
183 
191  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
192  {
193  //map the channel if it's private and we haven't seen it
194  lock (providers)
195  {
196  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
197  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
198  if (message.User.Channel.IsPrivateChannel)
199  lock (mappedChannels)
200  {
201  if (!provider.Connected)
202  return;
203  if (!enumerable.Any())
204  {
205  ulong newId;
206  lock (this)
207  newId = channelIdCounter++;
208  mappedChannels.Add(newId, new ChannelMapping
209  {
210  IsWatchdogChannel = false,
211  ProviderChannelId = message.User.Channel.RealId,
212  ProviderId = providerId,
213  Channel = message.User.Channel
214  });
215  message.User.Channel.RealId = newId;
216  }
217  else
218  message.User.Channel.RealId = enumerable.First().Key;
219  }
220  else
221  {
222  //need to add tag and isAdminChannel
223  var mapping = enumerable.First().Value;
224  message.User.Channel.Id = mapping.Channel.Id;
225  message.User.Channel.Tag = mapping.Channel.Tag;
226  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
227  }
228  }
229 
230  var splits = new List<string>(message.Content.Trim().Split(' '));
231  var address = splits[0];
232  if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ','))
233  address = address.Substring(0, address.Length - 1);
234 
235  address = address.ToUpperInvariant();
236 
237  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
238 
239  if (!addressed && !message.User.Channel.IsPrivateChannel)
240  //no mention
241  return;
242 
243  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
244 
245  if (addressed)
246  splits.RemoveAt(0);
247 
248  if (splits.Count == 0)
249  {
250  //just a mention
251  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
252  return;
253  }
254 
255  var command = splits[0].ToUpperInvariant();
256  splits.RemoveAt(0);
257  var arguments = String.Join(" ", splits);
258 
259  try
260  {
261  async Task<ICommand> GetCommand(string commandName)
262  {
263  if (!builtinCommands.TryGetValue(commandName, out var handler))
264  {
265  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken));
266  await Task.WhenAll(tasks).ConfigureAwait(false);
267  handler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == commandName).FirstOrDefault();
268  }
269  return handler;
270  };
271 
272  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
273 
274  if (command == "HELP" || command == "?")
275  {
276  string helpText;
277  if (splits.Count == 0)
278  {
279  var allCommands = builtinCommands.Select(x => x.Value).ToList();
280  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)).ToList();
281  await Task.WhenAll(tasks).ConfigureAwait(false);
282  allCommands.AddRange(tasks.SelectMany(x => x.Result));
283  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)));
284  }
285  else
286  {
287  var helpHandler = await GetCommand(splits[0].ToUpperInvariant()).ConfigureAwait(false);
288  if (helpHandler != default)
289  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
290  else
291  helpText = UnknownCommandMessage;
292  }
293  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
294  return;
295  }
296 
297  var commandHandler = await GetCommand(command).ConfigureAwait(false);
298 
299  if (commandHandler == default)
300  {
301  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
302  return;
303  }
304 
305  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
306  {
307  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
308  return;
309  }
310 
311  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
312  if (result != null)
313  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
314  }
315  catch (OperationCanceledException)
316  {
317  throw;
318  }
319  catch (Exception e)
320  {
321  //error bc custom commands should reply about why it failed
322  logger.LogError("Error processing chat command: {0}", e);
323  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
324  }
325  }
326 
332  async Task MonitorMessages(CancellationToken cancellationToken)
333  {
334  var messageTasks = new Dictionary<IProvider, Task<Message>>();
335  try
336  {
337  while (!cancellationToken.IsCancellationRequested)
338  {
339  //prune disconnected providers
340  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
341  messageTasks.Remove(I.Key);
342 
343  //add new ones
344  Task updatedTask;
345  lock (this)
346  updatedTask = connectionsUpdated.Task;
347  lock (providers)
348  foreach (var I in providers)
349  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
350  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
351 
352  if (messageTasks.Count == 0)
353  {
354  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
355  continue;
356  }
357 
358  //wait for a message
359  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
360 
361  //process completed ones
362  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
363  {
364  var message = await I.Value.ConfigureAwait(false);
365 
366  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
367 
368  messageTasks.Remove(I.Key);
369  }
370  }
371  }
372  catch (OperationCanceledException) { }
373  catch (Exception e)
374  {
375  logger.LogError("Message monitor crashed!: Exception: {0}", e);
376  }
377  }
378 
380  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
381  {
382  if (newChannels == null)
383  throw new ArgumentNullException(nameof(newChannels));
384  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
385  if (provider == null)
386  return;
387  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
388  if (results == null) //aborted
389  return;
390  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
391  {
392  IsWatchdogChannel = x.IsWatchdogChannel == true,
393  IsUpdatesChannel = x.IsUpdatesChannel == true,
394  ProviderChannelId = y.RealId,
395  ProviderId = connectionId,
396  Channel = y
397  });
398 
399  ulong baseId;
400  lock (this)
401  {
402  baseId = channelIdCounter;
403  channelIdCounter += (ulong)results.Count;
404  }
405 
406  Task task;
407  lock (mappedChannels)
408  {
409  lock (providers)
410  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again
411  return;
412  foreach (var I in mappings)
413  {
414  var newId = baseId++;
415  mappedChannels.Add(newId, I);
416  I.Channel.RealId = newId;
417  }
418 
419  lock (trackingContexts)
420  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
421  }
422  await task.ConfigureAwait(false);
423  }
424 
426  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
427  {
428  if (newSettings == null)
429  throw new ArgumentNullException(nameof(newSettings));
430  IProvider provider;
431 
432  async Task DisconnectProvider(IProvider p)
433  {
434  try
435  {
436  await p.Disconnect(cancellationToken).ConfigureAwait(false);
437  }
438  finally
439  {
440  p.Dispose();
441  }
442  }
443 
444  Task disconnectTask;
445  lock (providers)
446  {
447  //raw settings changes forces a rebuild of the provider
448  if (providers.TryGetValue(newSettings.Id, out provider))
449  {
450  providers.Remove(newSettings.Id);
451  disconnectTask = DisconnectProvider(provider);
452  }
453  else
454  disconnectTask = Task.CompletedTask;
455  if (newSettings.Enabled.Value)
456  {
457  provider = providerFactory.CreateProvider(newSettings);
458  providers.Add(newSettings.Id, provider);
459  }
460  }
461 
462  lock (mappedChannels)
463  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
464  mappedChannels.Remove(I);
465 
466  await disconnectTask.ConfigureAwait(false);
467 
468  if (started)
469  {
470  if (newSettings.Enabled.Value)
471  await provider.Connect(cancellationToken).ConfigureAwait(false);
472  lock (this)
473  {
474  //same thread shennanigans
475  var oldOne = connectionsUpdated;
476  connectionsUpdated = new TaskCompletionSource<object>();
477  oldOne.SetResult(null);
478  }
479  }
480  }
481 
483  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
484  {
485  if (message == null)
486  throw new ArgumentNullException(nameof(message));
487  if (channelIds == null)
488  throw new ArgumentNullException(nameof(channelIds));
489 
490  return Task.WhenAll(channelIds.Select(x =>
491  {
492  ChannelMapping channelMapping;
493  lock (mappedChannels)
494  if (!mappedChannels.TryGetValue(x, out channelMapping))
495  return Task.CompletedTask;
496  IProvider provider;
497  lock (providers)
498  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
499  return Task.CompletedTask;
500  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
501  }));
502  }
503 
505  public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
506  {
507  List<ulong> wdChannels;
508  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
509  lock (mappedChannels) //so it doesn't change while we're using it
510  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
511  return SendMessage(message, wdChannels, cancellationToken);
512  }
513 
515  public Task SendUpdateMessage(string message, CancellationToken cancellationToken)
516  {
517  List<ulong> wdChannels;
518  message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message);
519  lock (mappedChannels) //so it doesn't change while we're using it
520  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
521  return SendMessage(message, wdChannels, cancellationToken);
522  }
523 
525  public async Task StartAsync(CancellationToken cancellationToken)
526  {
527  foreach (var I in commandFactory.GenerateCommands())
528  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
529  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
530  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
531  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
532  chatHandler = MonitorMessages(handlerCts.Token);
533  started = true;
534  }
535 
537  public async Task StopAsync(CancellationToken cancellationToken)
538  {
539  handlerCts.Cancel();
540  await chatHandler.ConfigureAwait(false);
541  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
542  }
543 
545  public async Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
546  {
547  if (customCommandHandler == null)
548  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
549  JsonTrackingContext context = null;
550  context = new JsonTrackingContext(ioManager, customCommandHandler, loggerFactory.CreateLogger<JsonTrackingContext>(), () =>
551  {
552  lock (trackingContexts)
553  trackingContexts.Remove(context);
554  }, ioManager.ConcatPath(basePath, commandsJsonName), ioManager.ConcatPath(basePath, channelsJsonName));
555  Task task;
556  lock (trackingContexts)
557  {
558  trackingContexts.Add(context);
559  lock (mappedChannels)
560  task = context.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken);
561  }
562  await task.ConfigureAwait(false);
563  return context;
564  }
565 
567  public bool Connected(long connectionId)
568  {
569  lock (providers)
570  return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
571  }
572 
574  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
575  {
576  if (this.customCommandHandler != null)
577  throw new InvalidOperationException("RegisterCommandHandler() already called!");
578  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
579  }
580 
582  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
583  {
584  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
585  if (provider != null)
586  try
587  {
588  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
589  }
590  finally
591  {
592  provider.Dispose();
593  }
594  }
595 
597  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
598  {
599  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
600  List<ulong> wdChannels;
601  lock (mappedChannels) //so it doesn't change while we're using it
602  wdChannels = mappedChannels.Select(x => x.Key).ToList();
603  return SendMessage(message, wdChannels, cancellationToken);
604  }
605  }
606 }
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: Chat.cs:574
+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  sealed class Chat : IChat, IRestartHandler
19  {
20  const string CommonMention = "!tgs";
21 
26 
31 
36 
41 
46 
50  readonly ILoggerFactory loggerFactory;
51 
55  readonly ILogger<Chat> logger;
56 
60  readonly Dictionary<string, ICommand> builtinCommands;
61 
65  readonly Dictionary<long, IProvider> providers;
66 
70  readonly Dictionary<ulong, ChannelMapping> mappedChannels;
71 
75  readonly List<IJsonTrackingContext> trackingContexts;
76 
80  readonly CancellationTokenSource handlerCts;
81 
85  readonly List<Models.ChatBot> initialChatBots;
86 
91 
96 
100  TaskCompletionSource<object> connectionsUpdated;
101 
106 
110  bool started;
111 
123  public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<Chat> logger, IEnumerable<Models.ChatBot> initialChatBots)
124  {
125  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
126  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
127  this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
128  if (serverControl == null)
129  throw new ArgumentNullException(nameof(serverControl));
130  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
131  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
132  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
133  this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
134 
135  restartRegistration = serverControl.RegisterForRestart(this);
136 
137  builtinCommands = new Dictionary<string, ICommand>();
138  providers = new Dictionary<long, IProvider>();
139  mappedChannels = new Dictionary<ulong, ChannelMapping>();
140  trackingContexts = new List<IJsonTrackingContext>();
141  handlerCts = new CancellationTokenSource();
142  connectionsUpdated = new TaskCompletionSource<object>();
143  channelIdCounter = 1;
144  }
145 
147  public void Dispose()
148  {
149  restartRegistration.Dispose();
150  handlerCts.Dispose();
151  foreach (var I in providers)
152  I.Value.Dispose();
153  }
154 
162  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
163  {
164  IProvider provider;
165  lock (providers)
166  if (!providers.TryGetValue(connectionId, out provider))
167  return null;
168  Task task;
169  lock (mappedChannels)
170  {
171  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
172  mappedChannels.Remove(I);
173 
174  if (updateTrackings)
175  lock (trackingContexts)
176  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
177  else
178  task = Task.CompletedTask;
179  }
180  await task.ConfigureAwait(false);
181  return provider;
182  }
183 
191  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
192  {
193  //map the channel if it's private and we haven't seen it
194  lock (providers)
195  {
196  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
197  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
198  if (message.User.Channel.IsPrivateChannel)
199  lock (mappedChannels)
200  {
201  if (!provider.Connected)
202  return;
203  if (!enumerable.Any())
204  {
205  ulong newId;
206  lock (this)
207  newId = channelIdCounter++;
208  mappedChannels.Add(newId, new ChannelMapping
209  {
210  IsWatchdogChannel = false,
211  ProviderChannelId = message.User.Channel.RealId,
212  ProviderId = providerId,
213  Channel = message.User.Channel
214  });
215  message.User.Channel.RealId = newId;
216  }
217  else
218  message.User.Channel.RealId = enumerable.First().Key;
219  }
220  else
221  {
222  //need to add tag and isAdminChannel
223  var mapping = enumerable.First().Value;
224  message.User.Channel.Id = mapping.Channel.Id;
225  message.User.Channel.Tag = mapping.Channel.Tag;
226  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
227  }
228  }
229 
230  var splits = new List<string>(message.Content.Trim().Split(' '));
231  var address = splits[0];
232  if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ','))
233  address = address.Substring(0, address.Length - 1);
234 
235  address = address.ToUpperInvariant();
236 
237  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
238 
239  if (!addressed && !message.User.Channel.IsPrivateChannel)
240  //no mention
241  return;
242 
243  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
244 
245  if (addressed)
246  splits.RemoveAt(0);
247 
248  if (splits.Count == 0)
249  {
250  //just a mention
251  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
252  return;
253  }
254 
255  var command = splits[0].ToUpperInvariant();
256  splits.RemoveAt(0);
257  var arguments = String.Join(" ", splits);
258 
259  try
260  {
261  async Task<ICommand> GetCommand(string commandName)
262  {
263  if (!builtinCommands.TryGetValue(commandName, out var handler))
264  {
265  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken));
266  await Task.WhenAll(tasks).ConfigureAwait(false);
267  handler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == commandName).FirstOrDefault();
268  }
269  return handler;
270  };
271 
272  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
273 
274  if (command == "HELP" || command == "?")
275  {
276  string helpText;
277  if (splits.Count == 0)
278  {
279  var allCommands = builtinCommands.Select(x => x.Value).ToList();
280  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)).ToList();
281  await Task.WhenAll(tasks).ConfigureAwait(false);
282  allCommands.AddRange(tasks.SelectMany(x => x.Result));
283  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)));
284  }
285  else
286  {
287  var helpHandler = await GetCommand(splits[0].ToUpperInvariant()).ConfigureAwait(false);
288  if (helpHandler != default)
289  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
290  else
291  helpText = UnknownCommandMessage;
292  }
293  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
294  return;
295  }
296 
297  var commandHandler = await GetCommand(command).ConfigureAwait(false);
298 
299  if (commandHandler == default)
300  {
301  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
302  return;
303  }
304 
305  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
306  {
307  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
308  return;
309  }
310 
311  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
312  if (result != null)
313  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
314  }
315  catch (OperationCanceledException)
316  {
317  throw;
318  }
319  catch (Exception e)
320  {
321  //error bc custom commands should reply about why it failed
322  logger.LogError("Error processing chat command: {0}", e);
323  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
324  }
325  }
326 
332  async Task MonitorMessages(CancellationToken cancellationToken)
333  {
334  var messageTasks = new Dictionary<IProvider, Task<Message>>();
335  try
336  {
337  while (!cancellationToken.IsCancellationRequested)
338  {
339  //prune disconnected providers
340  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
341  messageTasks.Remove(I.Key);
342 
343  //add new ones
344  Task updatedTask;
345  lock (this)
346  updatedTask = connectionsUpdated.Task;
347  lock (providers)
348  foreach (var I in providers)
349  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
350  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
351 
352  if (messageTasks.Count == 0)
353  {
354  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
355  continue;
356  }
357 
358  //wait for a message
359  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
360 
361  //process completed ones
362  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
363  {
364  var message = await I.Value.ConfigureAwait(false);
365 
366  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
367 
368  messageTasks.Remove(I.Key);
369  }
370  }
371  }
372  catch (OperationCanceledException) { }
373  catch (Exception e)
374  {
375  logger.LogError("Message monitor crashed!: Exception: {0}", e);
376  }
377  }
378 
380  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
381  {
382  if (newChannels == null)
383  throw new ArgumentNullException(nameof(newChannels));
384  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
385  if (provider == null)
386  return;
387  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
388  if (results == null) //aborted
389  return;
390  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
391  {
392  IsWatchdogChannel = x.IsWatchdogChannel == true,
393  IsUpdatesChannel = x.IsUpdatesChannel == true,
394  ProviderChannelId = y.RealId,
395  ProviderId = connectionId,
396  Channel = y
397  });
398 
399  ulong baseId;
400  lock (this)
401  {
402  baseId = channelIdCounter;
403  channelIdCounter += (ulong)results.Count;
404  }
405 
406  Task task;
407  lock (mappedChannels)
408  {
409  lock (providers)
410  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again
411  return;
412  foreach (var I in mappings)
413  {
414  var newId = baseId++;
415  mappedChannels.Add(newId, I);
416  I.Channel.RealId = newId;
417  }
418 
419  lock (trackingContexts)
420  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
421  }
422  await task.ConfigureAwait(false);
423  }
424 
426  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
427  {
428  if (newSettings == null)
429  throw new ArgumentNullException(nameof(newSettings));
430  IProvider provider;
431 
432  async Task DisconnectProvider(IProvider p)
433  {
434  try
435  {
436  await p.Disconnect(cancellationToken).ConfigureAwait(false);
437  }
438  finally
439  {
440  p.Dispose();
441  }
442  }
443 
444  Task disconnectTask;
445  lock (providers)
446  {
447  //raw settings changes forces a rebuild of the provider
448  if (providers.TryGetValue(newSettings.Id, out provider))
449  {
450  providers.Remove(newSettings.Id);
451  disconnectTask = DisconnectProvider(provider);
452  }
453  else
454  disconnectTask = Task.CompletedTask;
455  if (newSettings.Enabled.Value)
456  {
457  provider = providerFactory.CreateProvider(newSettings);
458  providers.Add(newSettings.Id, provider);
459  }
460  }
461 
462  lock (mappedChannels)
463  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
464  mappedChannels.Remove(I);
465 
466  await disconnectTask.ConfigureAwait(false);
467 
468  if (started)
469  {
470  if (newSettings.Enabled.Value)
471  await provider.Connect(cancellationToken).ConfigureAwait(false);
472  lock (this)
473  {
474  //same thread shennanigans
475  var oldOne = connectionsUpdated;
476  connectionsUpdated = new TaskCompletionSource<object>();
477  oldOne.SetResult(null);
478  }
479  }
480  }
481 
483  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
484  {
485  if (message == null)
486  throw new ArgumentNullException(nameof(message));
487  if (channelIds == null)
488  throw new ArgumentNullException(nameof(channelIds));
489 
490  return Task.WhenAll(channelIds.Select(x =>
491  {
492  ChannelMapping channelMapping;
493  lock (mappedChannels)
494  if (!mappedChannels.TryGetValue(x, out channelMapping))
495  return Task.CompletedTask;
496  IProvider provider;
497  lock (providers)
498  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
499  return Task.CompletedTask;
500  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
501  }));
502  }
503 
505  public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
506  {
507  List<ulong> wdChannels;
508  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
509  lock (mappedChannels) //so it doesn't change while we're using it
510  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
511  return SendMessage(message, wdChannels, cancellationToken);
512  }
513 
515  public Task SendUpdateMessage(string message, CancellationToken cancellationToken)
516  {
517  List<ulong> wdChannels;
518  message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message);
519  lock (mappedChannels) //so it doesn't change while we're using it
520  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
521  return SendMessage(message, wdChannels, cancellationToken);
522  }
523 
525  public async Task StartAsync(CancellationToken cancellationToken)
526  {
527  foreach (var I in commandFactory.GenerateCommands())
528  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
529  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
530  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
531  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
532  chatHandler = MonitorMessages(handlerCts.Token);
533  started = true;
534  }
535 
537  public async Task StopAsync(CancellationToken cancellationToken)
538  {
539  handlerCts.Cancel();
540  await chatHandler.ConfigureAwait(false);
541  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
542  }
543 
545  public async Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
546  {
547  if (customCommandHandler == null)
548  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
549  JsonTrackingContext context = null;
550  context = new JsonTrackingContext(ioManager, customCommandHandler, loggerFactory.CreateLogger<JsonTrackingContext>(), () =>
551  {
552  lock (trackingContexts)
553  trackingContexts.Remove(context);
554  }, ioManager.ConcatPath(basePath, commandsJsonName), ioManager.ConcatPath(basePath, channelsJsonName));
555  Task task;
556  lock (trackingContexts)
557  {
558  trackingContexts.Add(context);
559  lock (mappedChannels)
560  task = context.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken);
561  }
562  await task.ConfigureAwait(false);
563  return context;
564  }
565 
567  public bool Connected(long connectionId)
568  {
569  lock (providers)
570  return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
571  }
572 
574  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
575  {
576  if (this.customCommandHandler != null)
577  throw new InvalidOperationException("RegisterCommandHandler() already called!");
578  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
579  }
580 
582  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
583  {
584  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
585  if (provider != null)
586  try
587  {
588  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
589  }
590  finally
591  {
592  provider.Dispose();
593  }
594  }
595 
597  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
598  {
599  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
600  List<ulong> wdChannels;
601  lock (mappedChannels) //so it doesn't change while we're using it
602  wdChannels = mappedChannels.Select(x => x.Key).ToList();
603  return SendMessage(message, wdChannels, cancellationToken);
604  }
605  }
606 }
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: Chat.cs:574
bool IsWatchdogChannel
If Channel is a watchdog channel
-
Represents a mapping of a Channel.RealId
User User
The Components.Chat.User who sent the Message
Definition: Message.cs:16
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the Chat
Definition: Chat.cs:50
diff --git a/_claims_injector_8cs_source.html b/_claims_injector_8cs_source.html index e39c419c46..cae8a262ee 100644 --- a/_claims_injector_8cs_source.html +++ b/_claims_injector_8cs_source.html @@ -92,9 +92,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ClaimsInjector.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authentication.JwtBearer;
2 using Microsoft.AspNetCore.Http;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.IdentityModel.Tokens.Jwt;
7 using System.Security.Claims;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Security
15 {
18  {
23 
28 
34  public ClaimsInjector(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory)
35  {
36  this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
37  this.authenticationContextFactory = authenticationContextFactory ?? throw new ArgumentNullException(nameof(authenticationContextFactory));
38  }
39 
41  public async Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
42  {
43  if (tokenValidatedContext == null)
44  throw new ArgumentNullException(nameof(tokenValidatedContext));
45 
46  //Find the user id in the token
47  var userIdClaim = tokenValidatedContext.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
48  if (userIdClaim == default)
49  throw new InvalidOperationException("Missing required claim!");
50 
51  long userId;
52  try
53  {
54  userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
55  }
56  catch (Exception e)
57  {
58  throw new InvalidOperationException("Failed to parse user ID!", e);
59  }
60 
61  ApiHeaders apiHeaders;
62  try
63  {
64  apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders());
65  }
66  catch (InvalidOperationException)
67  {
68  //we are not responsible for handling header validation issues
69  return;
70  }
71 
72  //This populates the CurrentAuthenticationContext field for use by us and subsequent controllers
73  await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, tokenValidatedContext.SecurityToken.ValidFrom, cancellationToken).ConfigureAwait(false);
74 
75  var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
76 
77  var enumerator = Enum.GetValues(typeof(RightsType));
78  var claims = new List<Claim>();
79  foreach (RightsType I in enumerator)
80  {
81  //if there's no instance user, do a weird thing and add all the instance roles
82  //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
83  //if user is null that means they got the token with an expired password
84  var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I);
85  var rightEnum = RightsHelper.RightToType(I);
86  var right = (Enum)Enum.ToObject(rightEnum, rightInt);
87  foreach (Enum J in Enum.GetValues(rightEnum))
88  if (right.HasFlag(J))
89  claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
90  }
91 
92  tokenValidatedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
93  }
94  }
95 }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authentication.JwtBearer;
2 using Microsoft.AspNetCore.Http;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.IdentityModel.Tokens.Jwt;
7 using System.Security.Claims;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Security
15 {
18  {
23 
28 
34  public ClaimsInjector(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory)
35  {
36  this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
37  this.authenticationContextFactory = authenticationContextFactory ?? throw new ArgumentNullException(nameof(authenticationContextFactory));
38  }
39 
41  public async Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
42  {
43  if (tokenValidatedContext == null)
44  throw new ArgumentNullException(nameof(tokenValidatedContext));
45 
46  //Find the user id in the token
47  var userIdClaim = tokenValidatedContext.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
48  if (userIdClaim == default)
49  throw new InvalidOperationException("Missing required claim!");
50 
51  long userId;
52  try
53  {
54  userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
55  }
56  catch (Exception e)
57  {
58  throw new InvalidOperationException("Failed to parse user ID!", e);
59  }
60 
61  ApiHeaders apiHeaders;
62  try
63  {
64  apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders());
65  }
66  catch (InvalidOperationException)
67  {
68  //we are not responsible for handling header validation issues
69  return;
70  }
71 
72  //This populates the CurrentAuthenticationContext field for use by us and subsequent controllers
73  await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, tokenValidatedContext.SecurityToken.ValidFrom, cancellationToken).ConfigureAwait(false);
74 
75  var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
76 
77  var enumerator = Enum.GetValues(typeof(RightsType));
78  var claims = new List<Claim>();
79  foreach (RightsType I in enumerator)
80  {
81  //if there's no instance user, do a weird thing and add all the instance roles
82  //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
83  //if user is null that means they got the token with an expired password
84  var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I);
85  var rightEnum = RightsHelper.RightToType(I);
86  var right = (Enum)Enum.ToObject(rightEnum, rightInt);
87  foreach (Enum J in Enum.GetValues(rightEnum))
88  if (right.HasFlag(J))
89  claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
90  }
91 
92  tokenValidatedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
93  }
94  }
95 }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
-
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
diff --git a/_client_exception_8cs_source.html b/_client_exception_8cs_source.html index 34327e00b6..d5a584bae6 100644 --- a/_client_exception_8cs_source.html +++ b/_client_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ClientException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public abstract class ClientException : Exception
11  {
15  public HttpStatusCode StatusCode { get; }
16 
20  public Version ServerApiVersion { get; }
21 
27  protected ClientException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage == null ? throw new ArgumentNullException(nameof(errorMessage)) : errorMessage.Message ?? "Unknown Error")
28  {
29  StatusCode = statusCode;
30  ServerApiVersion = errorMessage?.SeverApiVersion;
31  }
32 
36  protected ClientException() { }
37 
42  protected ClientException(string message) : base(message) { }
43 
49  protected ClientException(string message, Exception innerException) : base(message, innerException) { }
50  }
51 }
-
ClientException()
Construct a ClientException
+Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public abstract class ClientException : Exception
11  {
15  public HttpStatusCode StatusCode { get; }
16 
20  public Version ServerApiVersion { get; }
21 
27  protected ClientException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage == null ? throw new ArgumentNullException(nameof(errorMessage)) : errorMessage.Message ?? "Unknown Error")
28  {
29  StatusCode = statusCode;
30  ServerApiVersion = errorMessage?.SeverApiVersion;
31  }
32 
36  protected ClientException() { }
37 
42  protected ClientException(string message) : base(message) { }
43 
49  protected ClientException(string message, Exception innerException) : base(message, innerException) { }
50  }
51 }
ClientException()
Construct a ClientException
ClientException(ErrorMessage errorMessage, HttpStatusCode statusCode)
Construct a ClientException using an errorMessage and statusCode
diff --git a/_comm_context_8cs_source.html b/_comm_context_8cs_source.html index 3fe3fdf25c..63f9d41ef5 100644 --- a/_comm_context_8cs_source.html +++ b/_comm_context_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
CommContext.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.IO;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
10 
11 namespace Tgstation.Server.Host.Components.Interop
12 {
14  sealed class CommContext : ICommContext
15  {
20 
24  readonly ILogger<CommContext> logger;
25 
29  readonly FileSystemWatcher fileSystemWatcher;
30 
34  readonly CancellationTokenSource cancellationTokenSource;
35 
39  readonly CancellationToken cancellationToken;
40 
45 
49  bool disposed;
50 
58  public CommContext(IIOManager ioManager, ILogger<CommContext> logger, string directory, string filter)
59  {
60  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
61  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
62 
63  directory = ioManager.ResolvePath(directory) ?? throw new ArgumentNullException(nameof(directory));
64  if (filter == null)
65  throw new ArgumentNullException(nameof(filter));
66 
67  fileSystemWatcher = new FileSystemWatcher(directory, filter)
68  {
69  EnableRaisingEvents = true,
70  IncludeSubdirectories = false,
71  NotifyFilter = NotifyFilters.LastWrite
72  };
73 
74  fileSystemWatcher.Created += HandleWrite;
75  fileSystemWatcher.Changed += HandleWrite;
76 
77  cancellationTokenSource = new CancellationTokenSource();
78  cancellationToken = cancellationTokenSource.Token;
79  disposed = false;
80  }
81 
83  public void Dispose()
84  {
85  if (disposed)
86  return;
87  disposed = true;
88  fileSystemWatcher.Dispose();
89  cancellationTokenSource.Cancel();
90  cancellationTokenSource.Dispose();
91  }
92 
98  async void HandleWrite(object sender, FileSystemEventArgs e) //this is what async void was made for
99  {
100  try
101  {
102  var fileBytes = await ioManager.ReadAllBytes(e.FullPath, cancellationToken).ConfigureAwait(false);
103  var file = Encoding.UTF8.GetString(fileBytes);
104 
105  logger.LogTrace("Read interop command json: {0}", file);
106 
107  CommCommand command;
108  try
109  {
110  command = new CommCommand
111  {
112  Parameters = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, object>>(file),
113  RawJson = file
114  };
115  }
116  catch (JsonException ex)
117  {
118  //file not fully written yet
119  logger.LogDebug("Suppressing json convert exception for command file write: {0}", ex);
120  return;
121  }
122 
123  await (handler?.HandleInterop(command, cancellationToken) ?? Task.CompletedTask).ConfigureAwait(false);
124  }
125  catch (OperationCanceledException) { }
126  catch (Exception ex)
127  {
128  logger.LogDebug("Exception while trying to handle command json write: {0}", ex);
129  }
130  }
131 
133  public void RegisterHandler(ICommHandler handler)
134  {
135  if (this.handler != null)
136  throw new InvalidOperationException("RegisterHandler already called!");
137  this.handler = handler ?? throw new ArgumentNullException(nameof(handler));
138  }
139  }
140 }
- +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.IO;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
10 
11 namespace Tgstation.Server.Host.Components.Interop
12 {
14  sealed class CommContext : ICommContext
15  {
20 
24  readonly ILogger<CommContext> logger;
25 
29  readonly FileSystemWatcher fileSystemWatcher;
30 
34  readonly CancellationTokenSource cancellationTokenSource;
35 
39  readonly CancellationToken cancellationToken;
40 
45 
49  bool disposed;
50 
58  public CommContext(IIOManager ioManager, ILogger<CommContext> logger, string directory, string filter)
59  {
60  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
61  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
62 
63  directory = ioManager.ResolvePath(directory) ?? throw new ArgumentNullException(nameof(directory));
64  if (filter == null)
65  throw new ArgumentNullException(nameof(filter));
66 
67  fileSystemWatcher = new FileSystemWatcher(directory, filter)
68  {
69  EnableRaisingEvents = true,
70  IncludeSubdirectories = false,
71  NotifyFilter = NotifyFilters.LastWrite
72  };
73 
74  fileSystemWatcher.Created += HandleWrite;
75  fileSystemWatcher.Changed += HandleWrite;
76 
77  cancellationTokenSource = new CancellationTokenSource();
78  cancellationToken = cancellationTokenSource.Token;
79  disposed = false;
80  }
81 
83  public void Dispose()
84  {
85  if (disposed)
86  return;
87  disposed = true;
88  fileSystemWatcher.Dispose();
89  cancellationTokenSource.Cancel();
90  cancellationTokenSource.Dispose();
91  }
92 
98  async void HandleWrite(object sender, FileSystemEventArgs e) //this is what async void was made for
99  {
100  try
101  {
102  var fileBytes = await ioManager.ReadAllBytes(e.FullPath, cancellationToken).ConfigureAwait(false);
103  var file = Encoding.UTF8.GetString(fileBytes);
104 
105  logger.LogTrace("Read interop command json: {0}", file);
106 
107  CommCommand command;
108  try
109  {
110  command = new CommCommand
111  {
112  Parameters = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, object>>(file),
113  RawJson = file
114  };
115  }
116  catch (JsonException ex)
117  {
118  //file not fully written yet
119  logger.LogDebug("Suppressing json convert exception for command file write: {0}", ex);
120  return;
121  }
122 
123  await (handler?.HandleInterop(command, cancellationToken) ?? Task.CompletedTask).ConfigureAwait(false);
124  }
125  catch (OperationCanceledException) { }
126  catch (Exception ex)
127  {
128  logger.LogDebug("Exception while trying to handle command json write: {0}", ex);
129  }
130  }
131 
133  public void RegisterHandler(ICommHandler handler)
134  {
135  if (this.handler != null)
136  throw new InvalidOperationException("RegisterHandler already called!");
137  this.handler = handler ?? throw new ArgumentNullException(nameof(handler));
138  }
139  }
140 }
readonly CancellationToken cancellationToken
The CancellationToken for the CommContext
Definition: CommContext.cs:39
Represents a registration of an interop session
Definition: ICommContext.cs:8
diff --git a/_components_2_watchdog_2_watchdog_8cs_source.html b/_components_2_watchdog_2_watchdog_8cs_source.html index 5110e9d684..4ae910f2da 100644 --- a/_components_2_watchdog_2_watchdog_8cs_source.html +++ b/_components_2_watchdog_2_watchdog_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Watchdog.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Newtonsoft.Json;
5 using Newtonsoft.Json.Serialization;
6 using System;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Linq;
10 using System.Text;
11 using System.Threading;
12 using System.Threading.Tasks;
19 
20 namespace Tgstation.Server.Host.Components.Watchdog
21 {
24  {
28  const int AlphaBravoStartupSeperationInterval = 10;
29 
31  public bool Running { get; private set; }
32 
34  public bool AlphaIsActive { get; private set; }
35 
37  public Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
38 
40  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; private set; }
41 
43  public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; }
44 
46  public RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
47 
51  readonly IChat chat;
52 
57 
62 
67 
72 
76  readonly IByondTopicSender byondTopicSender;
77 
82 
87 
92 
97 
101  readonly ILogger<Watchdog> logger;
102 
106  readonly SemaphoreSlim semaphore;
107 
111  readonly Api.Models.Instance instance;
112 
116  readonly bool autoStart;
117 
121  CancellationTokenSource monitorCts;
122 
127 
131  TaskCompletionSource<object> activeParametersUpdated;
132 
141 
146 
164  public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger<Watchdog> logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
165  {
166  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
167  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
168  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
169  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
170  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
171  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
172  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
173  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
174  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
175  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
176  ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
177  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
178  this.autoStart = autoStart;
179 
180  if (serverControl == null)
181  throw new ArgumentNullException(nameof(serverControl));
182 
183  chat.RegisterCommandHandler(this);
184 
185  AlphaIsActive = true;
186  ActiveLaunchParameters = initialLaunchParameters;
187  releaseServers = false;
188  activeParametersUpdated = new TaskCompletionSource<object>();
189 
190  restartRegistration = serverControl.RegisterForRestart(this);
191  try
192  {
193  semaphore = new SemaphoreSlim(1);
194  }
195  catch
196  {
197  restartRegistration.Dispose();
198  throw;
199  }
200  }
201 
203  public void Dispose()
204  {
205  DisposeAndNullControllers();
206  semaphore.Dispose();
207  restartRegistration.Dispose();
208  }
209 
214  {
215  alphaServer?.Dispose();
216  alphaServer = null;
217  bravoServer?.Dispose();
218  bravoServer = null;
219  Running = false;
220  }
221 
228  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
229  {
230  if (!Running)
231  return;
232  if (!graceful)
233  {
234  var chatTask = announce ? chat.SendWatchdogMessage("Terminating...", cancellationToken) : Task.CompletedTask;
235  await StopMonitor().ConfigureAwait(false);
236  DisposeAndNullControllers();
237  LastLaunchParameters = null;
238  await chatTask.ConfigureAwait(false);
239  return;
240  }
241 
242  //merely set the reboot state
243  var toKill = AlphaIsActive ? alphaServer : bravoServer;
244  var other = AlphaIsActive ? bravoServer : alphaServer;
245  if (toKill != null)
246  await toKill.SetRebootState(Components.Watchdog.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
247  }
248 
256  async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
257  {
258  logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
259 
260  //this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
261  //I'll do my best to walk you through it
262 
263  //returns true if the inactive server can't be used immediately
264  //also sets monitor to restart if the above holds
265  bool FullRestartDeadInactive()
266  {
267  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
268  {
269  logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
270  monitorState.NextAction = MonitorAction.Restart; //will dispose server
271  return true;
272  }
273  return false;
274  };
275 
276  //trys to set inactive server's port to the public game port
277  //doesn't handle closing active server's port
278  //returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
279  //on failure, sets monitor to restart
280  async Task<bool> MakeInactiveActive()
281  {
282  logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
283  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
284 
285  if (!result)
286  {
287  logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
288  monitorState.NextAction = MonitorAction.Restart; //will dispose server
289  return false;
290  }
291 
292  //inactive server should always be using active launch parameters
293  LastLaunchParameters = ActiveLaunchParameters;
294 
295  var tmp = monitorState.ActiveServer;
296  monitorState.ActiveServer = monitorState.InactiveServer;
297  monitorState.InactiveServer = tmp;
298  AlphaIsActive = !AlphaIsActive;
299  monitorState.ActiveServer.EnableCustomChatCommands();
300  return true;
301  }
302 
303  // Kills and tries to launch inactive server with the latest dmb
304  // falls back to current dmb on failure
305  // Sets critfail on inactive server failing that
306  // returns false if the backup dmb was used successfully, true otherwise
307  async Task UpdateAndRestartInactiveServer(bool breakAfter)
308  {
309  activeParametersUpdated = new TaskCompletionSource<object>();
310  monitorState.InactiveServer.Dispose(); //kill or recycle it
311  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
312  monitorState.NextAction = desiredNextAction;
313 
314  logger.LogInformation("Rebooting inactive server...");
315  var newDmb = dmbFactory.LockNextDmb(1);
316  try
317  {
318  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, newDmb, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
319  monitorState.InactiveServer.SetHighPriority();
320  }
321  catch (OperationCanceledException)
322  {
323  throw;
324  }
325  catch (Exception e)
326  {
327  logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
328  //ahh jeez, what do we do here?
329  //this is our fault, so it should never happen but
330  //idk maybe a database error while handling the newest dmb?
331  //either way try to start it using the active server's dmb as a backup
332  try
333  {
334  var dmbBackup = await dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
335 
336  if (dmbBackup == null) //NANI!?
337  //just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
338  throw new JobException("Creating backup DMB provider failed!");
339 
340  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
341  monitorState.InactiveServer.SetHighPriority();
342  await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
343  }
344  catch (OperationCanceledException)
345  {
346  throw;
347  }
348  catch (Exception e2)
349  {
350  //fuuuuucckkk
351  logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
352  monitorState.InactiveServerCritFail = true;
353  await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
354  return;
355  }
356  }
357 
358  logger.LogInformation("Successfully relaunched inactive server!");
359  monitorState.RebootingInactiveServer = true;
360  }
361 
362  string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
363 
364  //reason handling
365  switch (activationReason)
366  {
367  case MonitorActivationReason.ActiveServerCrashed:
368  if (monitorState.ActiveServer.RebootState == Components.Watchdog.RebootState.Shutdown)
369  {
370  //the time for graceful shutdown is now
371  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Exiting due to graceful termination request...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
372  DisposeAndNullControllers();
373  monitorState.NextAction = MonitorAction.Exit;
374  break;
375  }
376 
377  if (FullRestartDeadInactive())
378  {
379  //tell chat about it and go ahead
380  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Inactive server unable to online!", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
381  //we've already been set to restart
382  break;
383  }
384 
385  //tell chat about it
386  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Onlining inactive server...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
387 
388  //try to active the inactive server
389  if (!await MakeInactiveActive().ConfigureAwait(false))
390  //failing that, we've already been set to restart
391  break;
392 
393  //bring up another inactive server
394  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
395  break;
396  case MonitorActivationReason.InactiveServerCrashed:
397  //just announce and try to bring it back
398  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Inactive server {0}! Rebooting...", ExitWord(monitorState.InactiveServer)), cancellationToken).ConfigureAwait(false);
399  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
400  break;
401  case MonitorActivationReason.ActiveServerRebooted:
402  //ideal goal: active server just closed its port
403  //tell inactive server to open it's port and that's now the active server
404  var rebootState = monitorState.ActiveServer.RebootState;
405  monitorState.ActiveServer.ResetRebootState(); //the DMAPI has already done this internally
406 
407  if (FullRestartDeadInactive() && rebootState != Components.Watchdog.RebootState.Shutdown)
408  //full restart if the inactive server is being fucky
409  break;
410 
411  //what matters here is the RebootState
412  var restartOnceSwapped = false;
413 
414  switch (rebootState)
415  {
416  case Components.Watchdog.RebootState.Normal:
417  //life as normal
418  break;
419  case Components.Watchdog.RebootState.Restart:
420  //reboot the current active server once the inactive one activates
421  restartOnceSwapped = true;
422  break;
423  case Components.Watchdog.RebootState.Shutdown:
424  //graceful shutdown time
425  await chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
426  DisposeAndNullControllers();
427  monitorState.NextAction = MonitorAction.Exit;
428  return;
429  }
430 
431  //are both servers now running the same CompileJob?
432  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
433 
434  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
435  //need a new launch to update either settings or compile job
436  restartOnceSwapped = true;
437 
438  if (restartOnceSwapped)
439  //we need to manually restart active server
440  //just kill it here, easier that way
441  monitorState.ActiveServer.Dispose();
442 
443  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
444 
445  if (activeServerStillHasPortOpen)
446  //we didn't want active server to swap for some reason and it still has it's port open
447  //just continue as normal
448  break;
449 
450  if (!await MakeInactiveActive().ConfigureAwait(false))
451  //monitor will restart
452  break;
453 
454  //servers now swapped
455 
456  //enable this now if inactive server is not still valid
457  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
458 
459  if (!restartOnceSwapped)
460  //now try to reopen it on the private port
461  //failing that, just reboot it
462  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
463 
464  //break either way because any issues past this point would be solved by the reboot
465  if (restartOnceSwapped)
466  //for one reason or another
467  //update and reboot
468  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
469  else
470  //only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
471  monitorState.NextAction = MonitorAction.Skip;
472  break;
473  case MonitorActivationReason.InactiveServerRebooted:
474  //just don't let the active server close it's port if the inactive server isn't ready
475  monitorState.RebootingInactiveServer = true;
476  monitorState.InactiveServer.ResetRebootState();
477  monitorState.ActiveServer.ClosePortOnReboot = false;
478  monitorState.NextAction = MonitorAction.Continue;
479  break;
480  case MonitorActivationReason.InactiveServerStartupComplete:
481  //opposite of above case
482  monitorState.RebootingInactiveServer = false;
483  monitorState.ActiveServer.ClosePortOnReboot = true;
484  monitorState.NextAction = MonitorAction.Continue;
485  break;
486  case MonitorActivationReason.NewDmbAvailable:
487  case MonitorActivationReason.ActiveLaunchParametersUpdated:
488  //just reload the inactive server and wait for a swap to apply the changes
489  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
490  break;
491  }
492  }
493 
499  async Task MonitorLifetimes(CancellationToken cancellationToken)
500  {
501  logger.LogTrace("Entered MonitorLifetimes");
502 
503  //this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
504 
505  var iteration = 1;
506  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
507  {
508  //always start out with continue
509  monitorState.NextAction = MonitorAction.Continue;
510 
511  //dump some info to the logs
512  logger.LogDebug("Iteration {0} of monitor loop", iteration);
513  try
514  {
515  if (AlphaIsActive)
516  logger.LogDebug("Alpha is the active server");
517  else
518  logger.LogDebug("Bravo is the active server");
519 
520 
521  if (monitorState.RebootingInactiveServer)
522  logger.LogDebug("Inactive server is rebooting");
523 
524  //update the monitor state with the inactive/active servers
525  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
526  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
527 
528  if (monitorState.ActiveServer.ClosePortOnReboot)
529  logger.LogDebug("Active server will close port on reboot");
530  if (monitorState.InactiveServer.ClosePortOnReboot)
531  logger.LogDebug("Inactive server will close port on reboot");
532 
533  logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
534  logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
535 
536  //load the activation tasks into local variables
537  Task activeServerLifetime = monitorState.ActiveServer.Lifetime;
538  Task inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
539  var activeServerReboot = monitorState.ActiveServer.OnReboot;
540  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
541  Task inactiveServerStartup = monitorState.RebootingInactiveServer ? monitorState.InactiveServer.LaunchResult : null;
542  Task activeLaunchParametersChanged = activeParametersUpdated.Task;
543  var newDmbAvailable = dmbFactory.OnNewerDmb;
544 
545  //cancel waiting if requested
546  var cancelTcs = new TaskCompletionSource<object>();
547  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
548  {
549  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
550  if (monitorState.RebootingInactiveServer)
551  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
552  //wait for something to happen
553  await toWaitOn.ConfigureAwait(false);
554  cancellationToken.ThrowIfCancellationRequested();
555  }
556 
557  var chatTask = Task.CompletedTask;
558  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
559  {
560  //always run HandleMonitorWakeup from the context of the semaphore lock
561  //multiple things may have happened, handle them one at a time
562  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
563  {
564  MonitorActivationReason activationReason = default; //this will always be assigned before being used
565 
566  //process the tasks in this order and call HandlerMonitorWakup for each
567 
568  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
569  {
570  var taskCompleted = task?.IsCompleted == true;
571  task = null;
572  if (monitorState.NextAction == MonitorAction.Skip)
573  monitorState.NextAction = MonitorAction.Continue;
574  else if (taskCompleted)
575  {
576  activationReason = testActivationReason;
577  return true;
578  }
579  return false;
580  };
581 
582  if (CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
583  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
584  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
585  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
586  || CheckActivationReason(ref inactiveServerStartup, MonitorActivationReason.InactiveServerStartupComplete)
587  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
588  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated))
589  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
590  else
591  moreActivationsToProcess = false;
592  }
593 
594  //writeback alphaServer and bravoServer from monitor state in case they changesd
595  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
596  bravoServer = !AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
597  }
598 
599  //full reboot required
600  if (monitorState.NextAction == MonitorAction.Restart)
601  {
602  logger.LogDebug("Next state action is to restart");
603  DisposeAndNullControllers();
604  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
605 
606  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
607  {
608  Exception launchException = null;
609  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
610  try
611  {
612  //use LaunchImplNoLock without announcements or restarting the monitor
613  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
614  if (Running)
615  {
616  logger.LogDebug("Relaunch successful, resetting monitor state...");
617  monitorState = new MonitorState(); //clean the slate and continue
618  }
619  }
620  catch (OperationCanceledException)
621  {
622  throw;
623  }
624  catch (Exception e)
625  {
626  launchException = e;
627  }
628 
629  await chatTask.ConfigureAwait(false);
630  if (!Running)
631  {
632  if (launchException == null)
633  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
634  else
635  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
636  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); //max of one hour, increasing by a power of 2 each time
637  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
638  await Task.WhenAll(asyncDelayer.Delay(TimeSpan.FromSeconds(retryDelay), cancellationToken), chatTask).ConfigureAwait(false);
639  }
640  }
641  }
642  }
643  catch (OperationCanceledException)
644  {
645  logger.LogDebug("Monitor cancelled");
646  break;
647  }
648  catch (Exception e)
649  {
650  //really, this should NEVER happen
651  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}, Exception: {2}", iteration, JsonConvert.SerializeObject(monitorState), e);
652  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Monitor crashed, this should NEVER happen! Please report this, full details in logs! Restarting monitor... Error: {0}", e.Message), cancellationToken).ConfigureAwait(false);
653  }
654  }
655  logger.LogTrace("Monitor exiting...");
656  }
657 
662  async Task<bool> StopMonitor()
663  {
664  logger.LogTrace("StopMonitor");
665  if (monitorTask == null)
666  return false;
667  monitorCts.Cancel();
668  await monitorTask.ConfigureAwait(false);
669  monitorCts.Dispose();
670  monitorTask = null;
671  return true;
672  }
673 
674 
676  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
677  {
678  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
679  {
680  if (launchParameters.Match(ActiveLaunchParameters))
681  return;
682  ActiveLaunchParameters = launchParameters;
683  if (Running)
684  //queue an update
685  activeParametersUpdated.TrySetResult(null);
686  }
687  }
688 
697  async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
698  {
699  logger.LogTrace("Begin LaunchNoLock");
700 
701  if (Running)
702  throw new JobException("Watchdog already running!");
703 
704  Task chatTask;
705  //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
706  if (startMonitor && await StopMonitor().ConfigureAwait(false))
707  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
708  else if (announce)
709  //simple announce
710  chatTask = chat.SendWatchdogMessage(reattachInfo == null ? "Starting..." : "Reattaching...", cancellationToken);
711  else
712  //no announce
713  chatTask = Task.CompletedTask;
714 
715  //since neither server is running, this is safe to do
716  LastLaunchParameters = ActiveLaunchParameters;
717 
718  //for when we call ourself and want to not catch thrown exceptions
719  var ignoreNestedException = false;
720  try
721  {
722  //good ole sanity, should never fucking trigger but i don't trust myself even though I should
723  if (alphaServer != null || bravoServer != null)
724  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
725 
726  //don't need a new dmb if reattaching
727  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
728  var dmbToUse = doesntNeedNewDmb ? null : dmbFactory.LockNextDmb(2);
729 
730  //if this try catches something, both servers are killed
731  try
732  {
733  //start the alpha server task, either by launch a new process or attaching to an existing one
734  //The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
735  //The tasks pertaining to server startup times are in the ISessionControllers
736  Task<ISessionController> alphaServerTask;
737  if (!doesntNeedNewDmb)
738  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken);
739  else
740  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
741 
742  //retrieve the session controller
743  var startTime = DateTimeOffset.Now;
744  alphaServer = await alphaServerTask.ConfigureAwait(false);
745  //failed reattaches will return null
746  alphaServer?.SetHighPriority();
747 
748  //extra delay for total ordering
749  var now = DateTimeOffset.Now;
750  var delay = now - startTime;
751 
752  //definitely not if reattaching though
753  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
754  await asyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
755 
756  //now bring bravo up
757  if (!doesntNeedNewDmb)
758  bravoServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken).ConfigureAwait(false);
759  else
760  bravoServer = await sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
761  //failed reattaches will return null
762  bravoServer?.SetHighPriority();
763 
764  //possiblity of null servers due to failed reattaches
765  if (alphaServer == null || bravoServer == null)
766  {
767  await chatTask.ConfigureAwait(false);
768  var bothServersDead = alphaServer == null && bravoServer == null;
769  if (bothServersDead
770  || (alphaServer == null && reattachInfo.AlphaIsActive)
771  || (bravoServer == null && !reattachInfo.AlphaIsActive))
772  {
773  //we lost the active server, just restart entirely
774  DisposeAndNullControllers();
775  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
776  logger.LogWarning(FailReattachMessage);
777  logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
778  chatTask = chat.SendWatchdogMessage(FailReattachMessage, cancellationToken);
779  ignoreNestedException = true;
780  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
781  return;
782  }
783 
784  //we still have the active server but the other one is dead to us, hand it off to the monitor to restart
785  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
786  chatTask = chat.SendWatchdogMessage(InactiveReattachFailureMessage, cancellationToken);
787  logger.LogWarning(InactiveReattachFailureMessage);
788 
789  if (reattachInfo.AlphaIsActive)
790  bravoServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
791  else
792  alphaServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
793  }
794 
795  //throws a JobException if something went wrong with a launch
796  //Dead sessions won't trigger this
797  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
798  {
799  var launch = await controller.LaunchResult.ConfigureAwait(false);
800  if (launch.ExitCode.HasValue)
801  //you killed us ray...
802  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
803  if (!launch.StartupTime.HasValue)
804  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server timed out on startup: {0}s", launch.ToString(), ActiveLaunchParameters.StartupTimeout.Value));
805  return launch;
806  }
807 
808  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
809  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
810  //this task completes when both serers have finished booting
811  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
812 
813  //don't forget about the cancellationToken
814  var cancelTcs = new TaskCompletionSource<object>();
815  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
816  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
817  cancellationToken.ThrowIfCancellationRequested();
818 
819  await allTask.ConfigureAwait(false);
820 
821  //both servers are now running, alpha is the active server(unless reattach), huzzah
822  AlphaIsActive = reattachInfo?.AlphaIsActive ?? true;
823 
824  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
825  activeServer.EnableCustomChatCommands();
826  activeServer.ClosePortOnReboot = true;
827 
828  logger.LogInformation("Launched servers successfully");
829  Running = true;
830 
831  if (startMonitor)
832  {
833  monitorCts = new CancellationTokenSource();
834  monitorTask = MonitorLifetimes(monitorCts.Token);
835  }
836  }
837  catch
838  {
839  if (dmbToUse != null)
840  {
841  //we locked 2 dmbs
842  if (bravoServer == null)
843  {
844  //bravo didn't get control of his
845  dmbToUse.Dispose();
846  if (alphaServer == null)
847  //alpha didn't get control of his
848  dmbToUse.Dispose();
849  }
850  }
851  else if (doesntNeedNewDmb)
852  //we have reattachInfo
853  if (bravoServer == null)
854  {
855  //bravo didn't get control of his
856  reattachInfo.Bravo?.Dmb.Dispose();
857  if (alphaServer == null)
858  //alpha didn't get control of his
859  reattachInfo.Alpha?.Dmb.Dispose();
860  }
861  //kill the controllers
862  DisposeAndNullControllers();
863  throw;
864  }
865  }
866  catch (Exception e)
867  {
868  //don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
869  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
870  {
871  var originalChatTask = chatTask;
872  async Task ChainChatTaskWithErrorMessage()
873  {
874  await originalChatTask.ConfigureAwait(false);
875  await chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false);
876  }
877  chatTask = ChainChatTaskWithErrorMessage();
878  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
879  }
880  throw;
881  }
882  finally
883  {
884  //finish the chat task that's in flight
885  try
886  {
887  await chatTask.ConfigureAwait(false);
888  }
889  catch (OperationCanceledException) { }
890  }
891  }
892 
894  public async Task Launch(CancellationToken cancellationToken)
895  {
896  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
897  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
898  }
899 
901  public async Task ResetRebootState(CancellationToken cancellationToken)
902  {
903  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
904  {
905  if (!Running)
906  return;
907  var toClear = AlphaIsActive ? alphaServer : bravoServer;
908  if (toClear != null)
909  toClear.ResetRebootState();
910  }
911  }
912 
914  public async Task Restart(bool graceful, CancellationToken cancellationToken)
915  {
916  logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
917  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
918  {
919  if (!graceful || !Running)
920  {
921  Task chatTask;
922  if (Running)
923  {
924  chatTask = chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken);
925  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
926  }
927  else
928  chatTask = Task.CompletedTask;
929  await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
930  await chatTask.ConfigureAwait(false);
931  }
932  var toReboot = AlphaIsActive ? alphaServer : bravoServer;
933  if (toReboot != null)
934  {
935  if (!await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false))
936  logger.LogWarning("Unable to send reboot state change event!");
937  }
938  }
939  }
940 
942  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
943  {
944  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
945  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
946  }
947 
949  public async Task StartAsync(CancellationToken cancellationToken)
950  {
951  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
952  if (!autoStart && reattachInfo == null)
953  return;
954 
955  long? adminUserId = null;
956 
957  await databaseContextFactory.UseContext(async db => adminUserId = await db.Users
958  .Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant())
959  .Select(x => x.Id)
960  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
961  var job = new Models.Job
962  {
963  StartedBy = new Models.User
964  {
965  Id = adminUserId.Value
966  },
967  Instance = new Models.Instance
968  {
969  Id = instance.Id
970  },
971  Description = "Instance startup watchdog launch",
972  CancelRight = (ulong)DreamDaemonRights.Shutdown,
973  CancelRightsType = RightsType.DreamDaemon
974  };
975  await jobManager.RegisterOperation(job, async (j, databaseContext, progressFunction, ct) =>
976  {
977  using (await SemaphoreSlimContext.Lock(semaphore, ct).ConfigureAwait(false))
978  await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
979  }, cancellationToken).ConfigureAwait(false);
980  }
981 
983  public async Task StopAsync(CancellationToken cancellationToken)
984  {
985  if (releaseServers && Running)
986  {
987  await StopMonitor().ConfigureAwait(false);
988 
989  var reattachInformation = new WatchdogReattachInformation
990  {
991  AlphaIsActive = AlphaIsActive
992  };
993  reattachInformation.Alpha = alphaServer?.Release();
994  reattachInformation.Bravo = bravoServer?.Release();
995  await reattachInfoHandler.Save(reattachInformation, cancellationToken).ConfigureAwait(false);
996  }
997  await Terminate(false, cancellationToken).ConfigureAwait(false);
998  }
999 
1001  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
1002  {
1003  string results;
1004  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1005  {
1006  if (!Running)
1007  return true;
1008 
1009  var builder = new StringBuilder(Constants.DMTopicEvent);
1010  builder.Append('&');
1011  var notification = new EventNotification
1012  {
1013  Type = eventType,
1014  Parameters = parameters
1015  };
1016  var json = JsonConvert.SerializeObject(notification);
1017  builder.Append(byondTopicSender.SanitizeString(Constants.DMParameterData));
1018  builder.Append('=');
1019  builder.Append(byondTopicSender.SanitizeString(json));
1020 
1021  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1022  results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
1023  }
1024 
1025  if (results == Constants.DMResponseSuccess)
1026  return true;
1027 
1028  List<Response> responses;
1029  try
1030  {
1031  responses = JsonConvert.DeserializeObject<List<Response>>(results);
1032  }
1033  catch
1034  {
1035  logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
1036  return true;
1037  }
1038 
1039  await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
1040 
1041  return true;
1042  }
1043 
1045  public async Task<string> HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
1046  {
1047  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1048  {
1049  if (!Running)
1050  return "ERROR: Server offline!";
1051 
1052  var commandObject = new ChatCommand
1053  {
1054  Command = commandName,
1055  Params = arguments,
1056  User = sender
1057  };
1058 
1059  var json = JsonConvert.SerializeObject(commandObject, new JsonSerializerSettings
1060  {
1061  ContractResolver = new CamelCasePropertyNamesContractResolver()
1062  });
1063 
1064  var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json));
1065 
1066  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1067  return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
1068  }
1069  }
1070 
1072  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
1073  {
1074  releaseServers = true;
1075  if (Running)
1076  await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false);
1077  }
1078  }
1079 }
-
bool InactiveServerCritFail
If the inactive server is in an unrecoverable state
Definition: MonitorState.cs:18
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Newtonsoft.Json;
5 using Newtonsoft.Json.Serialization;
6 using System;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Linq;
10 using System.Text;
11 using System.Threading;
12 using System.Threading.Tasks;
19 
20 namespace Tgstation.Server.Host.Components.Watchdog
21 {
24  {
28  const int AlphaBravoStartupSeperationInterval = 10;
29 
31  public bool Running { get; private set; }
32 
34  public bool AlphaIsActive { get; private set; }
35 
37  public Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
38 
40  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; private set; }
41 
43  public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; }
44 
46  public RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
47 
51  readonly IChat chat;
52 
57 
62 
67 
72 
76  readonly IByondTopicSender byondTopicSender;
77 
82 
87 
92 
97 
101  readonly ILogger<Watchdog> logger;
102 
106  readonly SemaphoreSlim semaphore;
107 
111  readonly Api.Models.Instance instance;
112 
116  readonly bool autoStart;
117 
121  CancellationTokenSource monitorCts;
122 
127 
131  TaskCompletionSource<object> activeParametersUpdated;
132 
141 
146 
164  public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger<Watchdog> logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
165  {
166  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
167  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
168  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
169  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
170  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
171  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
172  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
173  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
174  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
175  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
176  ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
177  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
178  this.autoStart = autoStart;
179 
180  if (serverControl == null)
181  throw new ArgumentNullException(nameof(serverControl));
182 
183  chat.RegisterCommandHandler(this);
184 
185  AlphaIsActive = true;
186  ActiveLaunchParameters = initialLaunchParameters;
187  releaseServers = false;
188  activeParametersUpdated = new TaskCompletionSource<object>();
189 
190  restartRegistration = serverControl.RegisterForRestart(this);
191  try
192  {
193  semaphore = new SemaphoreSlim(1);
194  }
195  catch
196  {
197  restartRegistration.Dispose();
198  throw;
199  }
200  }
201 
203  public void Dispose()
204  {
205  DisposeAndNullControllers();
206  semaphore.Dispose();
207  restartRegistration.Dispose();
208  }
209 
214  {
215  alphaServer?.Dispose();
216  alphaServer = null;
217  bravoServer?.Dispose();
218  bravoServer = null;
219  Running = false;
220  }
221 
228  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
229  {
230  if (!Running)
231  return;
232  if (!graceful)
233  {
234  var chatTask = announce ? chat.SendWatchdogMessage("Terminating...", cancellationToken) : Task.CompletedTask;
235  await StopMonitor().ConfigureAwait(false);
236  DisposeAndNullControllers();
237  LastLaunchParameters = null;
238  await chatTask.ConfigureAwait(false);
239  return;
240  }
241 
242  //merely set the reboot state
243  var toKill = AlphaIsActive ? alphaServer : bravoServer;
244  var other = AlphaIsActive ? bravoServer : alphaServer;
245  if (toKill != null)
246  await toKill.SetRebootState(Components.Watchdog.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
247  }
248 
256  async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
257  {
258  logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
259 
260  //this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
261  //I'll do my best to walk you through it
262 
263  //returns true if the inactive server can't be used immediately
264  //also sets monitor to restart if the above holds
265  bool FullRestartDeadInactive()
266  {
267  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
268  {
269  logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
270  monitorState.NextAction = MonitorAction.Restart; //will dispose server
271  return true;
272  }
273  return false;
274  };
275 
276  //trys to set inactive server's port to the public game port
277  //doesn't handle closing active server's port
278  //returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
279  //on failure, sets monitor to restart
280  async Task<bool> MakeInactiveActive()
281  {
282  logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
283  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
284 
285  if (!result)
286  {
287  logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
288  monitorState.NextAction = MonitorAction.Restart; //will dispose server
289  return false;
290  }
291 
292  //inactive server should always be using active launch parameters
293  LastLaunchParameters = ActiveLaunchParameters;
294 
295  var tmp = monitorState.ActiveServer;
296  monitorState.ActiveServer = monitorState.InactiveServer;
297  monitorState.InactiveServer = tmp;
298  AlphaIsActive = !AlphaIsActive;
299  monitorState.ActiveServer.EnableCustomChatCommands();
300  return true;
301  }
302 
303  // Kills and tries to launch inactive server with the latest dmb
304  // falls back to current dmb on failure
305  // Sets critfail on inactive server failing that
306  // returns false if the backup dmb was used successfully, true otherwise
307  async Task UpdateAndRestartInactiveServer(bool breakAfter)
308  {
309  activeParametersUpdated = new TaskCompletionSource<object>();
310  monitorState.InactiveServer.Dispose(); //kill or recycle it
311  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
312  monitorState.NextAction = desiredNextAction;
313 
314  logger.LogInformation("Rebooting inactive server...");
315  var newDmb = dmbFactory.LockNextDmb(1);
316  try
317  {
318  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, newDmb, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
319  monitorState.InactiveServer.SetHighPriority();
320  }
321  catch (OperationCanceledException)
322  {
323  throw;
324  }
325  catch (Exception e)
326  {
327  logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
328  //ahh jeez, what do we do here?
329  //this is our fault, so it should never happen but
330  //idk maybe a database error while handling the newest dmb?
331  //either way try to start it using the active server's dmb as a backup
332  try
333  {
334  var dmbBackup = await dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
335 
336  if (dmbBackup == null) //NANI!?
337  //just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
338  throw new JobException("Creating backup DMB provider failed!");
339 
340  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
341  monitorState.InactiveServer.SetHighPriority();
342  await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
343  }
344  catch (OperationCanceledException)
345  {
346  throw;
347  }
348  catch (Exception e2)
349  {
350  //fuuuuucckkk
351  logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
352  monitorState.InactiveServerCritFail = true;
353  await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
354  return;
355  }
356  }
357 
358  logger.LogInformation("Successfully relaunched inactive server!");
359  monitorState.RebootingInactiveServer = true;
360  }
361 
362  string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
363 
364  //reason handling
365  switch (activationReason)
366  {
367  case MonitorActivationReason.ActiveServerCrashed:
368  if (monitorState.ActiveServer.RebootState == Components.Watchdog.RebootState.Shutdown)
369  {
370  //the time for graceful shutdown is now
371  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Exiting due to graceful termination request...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
372  DisposeAndNullControllers();
373  monitorState.NextAction = MonitorAction.Exit;
374  break;
375  }
376 
377  if (FullRestartDeadInactive())
378  {
379  //tell chat about it and go ahead
380  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Inactive server unable to online!", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
381  //we've already been set to restart
382  break;
383  }
384 
385  //tell chat about it
386  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Onlining inactive server...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
387 
388  //try to active the inactive server
389  if (!await MakeInactiveActive().ConfigureAwait(false))
390  //failing that, we've already been set to restart
391  break;
392 
393  //bring up another inactive server
394  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
395  break;
396  case MonitorActivationReason.InactiveServerCrashed:
397  //just announce and try to bring it back
398  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Inactive server {0}! Rebooting...", ExitWord(monitorState.InactiveServer)), cancellationToken).ConfigureAwait(false);
399  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
400  break;
401  case MonitorActivationReason.ActiveServerRebooted:
402  //ideal goal: active server just closed its port
403  //tell inactive server to open it's port and that's now the active server
404  var rebootState = monitorState.ActiveServer.RebootState;
405  monitorState.ActiveServer.ResetRebootState(); //the DMAPI has already done this internally
406 
407  if (FullRestartDeadInactive() && rebootState != Components.Watchdog.RebootState.Shutdown)
408  //full restart if the inactive server is being fucky
409  break;
410 
411  //what matters here is the RebootState
412  var restartOnceSwapped = false;
413 
414  switch (rebootState)
415  {
416  case Components.Watchdog.RebootState.Normal:
417  //life as normal
418  break;
419  case Components.Watchdog.RebootState.Restart:
420  //reboot the current active server once the inactive one activates
421  restartOnceSwapped = true;
422  break;
423  case Components.Watchdog.RebootState.Shutdown:
424  //graceful shutdown time
425  await chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
426  DisposeAndNullControllers();
427  monitorState.NextAction = MonitorAction.Exit;
428  return;
429  }
430 
431  //are both servers now running the same CompileJob?
432  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
433 
434  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
435  //need a new launch to update either settings or compile job
436  restartOnceSwapped = true;
437 
438  if (restartOnceSwapped)
439  //we need to manually restart active server
440  //just kill it here, easier that way
441  monitorState.ActiveServer.Dispose();
442 
443  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
444 
445  if (activeServerStillHasPortOpen)
446  //we didn't want active server to swap for some reason and it still has it's port open
447  //just continue as normal
448  break;
449 
450  if (!await MakeInactiveActive().ConfigureAwait(false))
451  //monitor will restart
452  break;
453 
454  //servers now swapped
455 
456  //enable this now if inactive server is not still valid
457  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
458 
459  if (!restartOnceSwapped)
460  //now try to reopen it on the private port
461  //failing that, just reboot it
462  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
463 
464  //break either way because any issues past this point would be solved by the reboot
465  if (restartOnceSwapped)
466  //for one reason or another
467  //update and reboot
468  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
469  else
470  //only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
471  monitorState.NextAction = MonitorAction.Skip;
472  break;
473  case MonitorActivationReason.InactiveServerRebooted:
474  //just don't let the active server close it's port if the inactive server isn't ready
475  monitorState.RebootingInactiveServer = true;
476  monitorState.InactiveServer.ResetRebootState();
477  monitorState.ActiveServer.ClosePortOnReboot = false;
478  monitorState.NextAction = MonitorAction.Continue;
479  break;
480  case MonitorActivationReason.InactiveServerStartupComplete:
481  //opposite of above case
482  monitorState.RebootingInactiveServer = false;
483  monitorState.ActiveServer.ClosePortOnReboot = true;
484  monitorState.NextAction = MonitorAction.Continue;
485  break;
486  case MonitorActivationReason.NewDmbAvailable:
487  case MonitorActivationReason.ActiveLaunchParametersUpdated:
488  //just reload the inactive server and wait for a swap to apply the changes
489  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
490  break;
491  }
492  }
493 
499  async Task MonitorLifetimes(CancellationToken cancellationToken)
500  {
501  logger.LogTrace("Entered MonitorLifetimes");
502 
503  //this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
504 
505  var iteration = 1;
506  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
507  {
508  //always start out with continue
509  monitorState.NextAction = MonitorAction.Continue;
510 
511  //dump some info to the logs
512  logger.LogDebug("Iteration {0} of monitor loop", iteration);
513  try
514  {
515  if (AlphaIsActive)
516  logger.LogDebug("Alpha is the active server");
517  else
518  logger.LogDebug("Bravo is the active server");
519 
520 
521  if (monitorState.RebootingInactiveServer)
522  logger.LogDebug("Inactive server is rebooting");
523 
524  //update the monitor state with the inactive/active servers
525  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
526  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
527 
528  if (monitorState.ActiveServer.ClosePortOnReboot)
529  logger.LogDebug("Active server will close port on reboot");
530  if (monitorState.InactiveServer.ClosePortOnReboot)
531  logger.LogDebug("Inactive server will close port on reboot");
532 
533  logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
534  logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
535 
536  //load the activation tasks into local variables
537  Task activeServerLifetime = monitorState.ActiveServer.Lifetime;
538  Task inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
539  var activeServerReboot = monitorState.ActiveServer.OnReboot;
540  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
541  Task inactiveServerStartup = monitorState.RebootingInactiveServer ? monitorState.InactiveServer.LaunchResult : null;
542  Task activeLaunchParametersChanged = activeParametersUpdated.Task;
543  var newDmbAvailable = dmbFactory.OnNewerDmb;
544 
545  //cancel waiting if requested
546  var cancelTcs = new TaskCompletionSource<object>();
547  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
548  {
549  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
550  if (monitorState.RebootingInactiveServer)
551  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
552  //wait for something to happen
553  await toWaitOn.ConfigureAwait(false);
554  cancellationToken.ThrowIfCancellationRequested();
555  }
556 
557  var chatTask = Task.CompletedTask;
558  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
559  {
560  //always run HandleMonitorWakeup from the context of the semaphore lock
561  //multiple things may have happened, handle them one at a time
562  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
563  {
564  MonitorActivationReason activationReason = default; //this will always be assigned before being used
565 
566  //process the tasks in this order and call HandlerMonitorWakup for each
567 
568  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
569  {
570  var taskCompleted = task?.IsCompleted == true;
571  task = null;
572  if (monitorState.NextAction == MonitorAction.Skip)
573  monitorState.NextAction = MonitorAction.Continue;
574  else if (taskCompleted)
575  {
576  activationReason = testActivationReason;
577  return true;
578  }
579  return false;
580  };
581 
582  if (CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
583  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
584  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
585  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
586  || CheckActivationReason(ref inactiveServerStartup, MonitorActivationReason.InactiveServerStartupComplete)
587  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
588  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated))
589  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
590  else
591  moreActivationsToProcess = false;
592  }
593 
594  //writeback alphaServer and bravoServer from monitor state in case they changesd
595  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
596  bravoServer = !AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
597  }
598 
599  //full reboot required
600  if (monitorState.NextAction == MonitorAction.Restart)
601  {
602  logger.LogDebug("Next state action is to restart");
603  DisposeAndNullControllers();
604  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
605 
606  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
607  {
608  Exception launchException = null;
609  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
610  try
611  {
612  //use LaunchImplNoLock without announcements or restarting the monitor
613  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
614  if (Running)
615  {
616  logger.LogDebug("Relaunch successful, resetting monitor state...");
617  monitorState = new MonitorState(); //clean the slate and continue
618  }
619  }
620  catch (OperationCanceledException)
621  {
622  throw;
623  }
624  catch (Exception e)
625  {
626  launchException = e;
627  }
628 
629  await chatTask.ConfigureAwait(false);
630  if (!Running)
631  {
632  if (launchException == null)
633  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
634  else
635  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
636  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); //max of one hour, increasing by a power of 2 each time
637  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
638  await Task.WhenAll(asyncDelayer.Delay(TimeSpan.FromSeconds(retryDelay), cancellationToken), chatTask).ConfigureAwait(false);
639  }
640  }
641  }
642  }
643  catch (OperationCanceledException)
644  {
645  logger.LogDebug("Monitor cancelled");
646  break;
647  }
648  catch (Exception e)
649  {
650  //really, this should NEVER happen
651  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}, Exception: {2}", iteration, JsonConvert.SerializeObject(monitorState), e);
652  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Monitor crashed, this should NEVER happen! Please report this, full details in logs! Restarting monitor... Error: {0}", e.Message), cancellationToken).ConfigureAwait(false);
653  }
654  }
655  logger.LogTrace("Monitor exiting...");
656  }
657 
662  async Task<bool> StopMonitor()
663  {
664  logger.LogTrace("StopMonitor");
665  if (monitorTask == null)
666  return false;
667  monitorCts.Cancel();
668  await monitorTask.ConfigureAwait(false);
669  monitorCts.Dispose();
670  monitorTask = null;
671  return true;
672  }
673 
674 
676  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
677  {
678  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
679  {
680  if (launchParameters.Match(ActiveLaunchParameters))
681  return;
682  ActiveLaunchParameters = launchParameters;
683  if (Running)
684  //queue an update
685  activeParametersUpdated.TrySetResult(null);
686  }
687  }
688 
697  async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
698  {
699  logger.LogTrace("Begin LaunchNoLock");
700 
701  if (Running)
702  throw new JobException("Watchdog already running!");
703 
704  Task chatTask;
705  //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
706  if (startMonitor && await StopMonitor().ConfigureAwait(false))
707  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
708  else if (announce)
709  //simple announce
710  chatTask = chat.SendWatchdogMessage(reattachInfo == null ? "Starting..." : "Reattaching...", cancellationToken);
711  else
712  //no announce
713  chatTask = Task.CompletedTask;
714 
715  //since neither server is running, this is safe to do
716  LastLaunchParameters = ActiveLaunchParameters;
717 
718  //for when we call ourself and want to not catch thrown exceptions
719  var ignoreNestedException = false;
720  try
721  {
722  //good ole sanity, should never fucking trigger but i don't trust myself even though I should
723  if (alphaServer != null || bravoServer != null)
724  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
725 
726  //don't need a new dmb if reattaching
727  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
728  var dmbToUse = doesntNeedNewDmb ? null : dmbFactory.LockNextDmb(2);
729 
730  //if this try catches something, both servers are killed
731  try
732  {
733  //start the alpha server task, either by launch a new process or attaching to an existing one
734  //The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
735  //The tasks pertaining to server startup times are in the ISessionControllers
736  Task<ISessionController> alphaServerTask;
737  if (!doesntNeedNewDmb)
738  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken);
739  else
740  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
741 
742  //retrieve the session controller
743  var startTime = DateTimeOffset.Now;
744  alphaServer = await alphaServerTask.ConfigureAwait(false);
745  //failed reattaches will return null
746  alphaServer?.SetHighPriority();
747 
748  //extra delay for total ordering
749  var now = DateTimeOffset.Now;
750  var delay = now - startTime;
751 
752  //definitely not if reattaching though
753  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
754  await asyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
755 
756  //now bring bravo up
757  if (!doesntNeedNewDmb)
758  bravoServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken).ConfigureAwait(false);
759  else
760  bravoServer = await sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
761  //failed reattaches will return null
762  bravoServer?.SetHighPriority();
763 
764  //possiblity of null servers due to failed reattaches
765  if (alphaServer == null || bravoServer == null)
766  {
767  await chatTask.ConfigureAwait(false);
768  var bothServersDead = alphaServer == null && bravoServer == null;
769  if (bothServersDead
770  || (alphaServer == null && reattachInfo.AlphaIsActive)
771  || (bravoServer == null && !reattachInfo.AlphaIsActive))
772  {
773  //we lost the active server, just restart entirely
774  DisposeAndNullControllers();
775  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
776  logger.LogWarning(FailReattachMessage);
777  logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
778  chatTask = chat.SendWatchdogMessage(FailReattachMessage, cancellationToken);
779  ignoreNestedException = true;
780  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
781  return;
782  }
783 
784  //we still have the active server but the other one is dead to us, hand it off to the monitor to restart
785  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
786  chatTask = chat.SendWatchdogMessage(InactiveReattachFailureMessage, cancellationToken);
787  logger.LogWarning(InactiveReattachFailureMessage);
788 
789  if (reattachInfo.AlphaIsActive)
790  bravoServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
791  else
792  alphaServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
793  }
794 
795  //throws a JobException if something went wrong with a launch
796  //Dead sessions won't trigger this
797  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
798  {
799  var launch = await controller.LaunchResult.ConfigureAwait(false);
800  if (launch.ExitCode.HasValue)
801  //you killed us ray...
802  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
803  if (!launch.StartupTime.HasValue)
804  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server timed out on startup: {0}s", launch.ToString(), ActiveLaunchParameters.StartupTimeout.Value));
805  return launch;
806  }
807 
808  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
809  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
810  //this task completes when both serers have finished booting
811  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
812 
813  //don't forget about the cancellationToken
814  var cancelTcs = new TaskCompletionSource<object>();
815  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
816  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
817  cancellationToken.ThrowIfCancellationRequested();
818 
819  await allTask.ConfigureAwait(false);
820 
821  //both servers are now running, alpha is the active server(unless reattach), huzzah
822  AlphaIsActive = reattachInfo?.AlphaIsActive ?? true;
823 
824  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
825  activeServer.EnableCustomChatCommands();
826  activeServer.ClosePortOnReboot = true;
827 
828  logger.LogInformation("Launched servers successfully");
829  Running = true;
830 
831  if (startMonitor)
832  {
833  monitorCts = new CancellationTokenSource();
834  monitorTask = MonitorLifetimes(monitorCts.Token);
835  }
836  }
837  catch
838  {
839  if (dmbToUse != null)
840  {
841  //we locked 2 dmbs
842  if (bravoServer == null)
843  {
844  //bravo didn't get control of his
845  dmbToUse.Dispose();
846  if (alphaServer == null)
847  //alpha didn't get control of his
848  dmbToUse.Dispose();
849  }
850  }
851  else if (doesntNeedNewDmb)
852  //we have reattachInfo
853  if (bravoServer == null)
854  {
855  //bravo didn't get control of his
856  reattachInfo.Bravo?.Dmb.Dispose();
857  if (alphaServer == null)
858  //alpha didn't get control of his
859  reattachInfo.Alpha?.Dmb.Dispose();
860  }
861  //kill the controllers
862  DisposeAndNullControllers();
863  throw;
864  }
865  }
866  catch (Exception e)
867  {
868  //don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
869  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
870  {
871  var originalChatTask = chatTask;
872  async Task ChainChatTaskWithErrorMessage()
873  {
874  await originalChatTask.ConfigureAwait(false);
875  await chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false);
876  }
877  chatTask = ChainChatTaskWithErrorMessage();
878  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
879  }
880  throw;
881  }
882  finally
883  {
884  //finish the chat task that's in flight
885  try
886  {
887  await chatTask.ConfigureAwait(false);
888  }
889  catch (OperationCanceledException) { }
890  }
891  }
892 
894  public async Task Launch(CancellationToken cancellationToken)
895  {
896  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
897  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
898  }
899 
901  public async Task ResetRebootState(CancellationToken cancellationToken)
902  {
903  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
904  {
905  if (!Running)
906  return;
907  var toClear = AlphaIsActive ? alphaServer : bravoServer;
908  if (toClear != null)
909  toClear.ResetRebootState();
910  }
911  }
912 
914  public async Task Restart(bool graceful, CancellationToken cancellationToken)
915  {
916  logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
917  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
918  {
919  if (!graceful || !Running)
920  {
921  Task chatTask;
922  if (Running)
923  {
924  chatTask = chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken);
925  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
926  }
927  else
928  chatTask = Task.CompletedTask;
929  await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
930  await chatTask.ConfigureAwait(false);
931  }
932  var toReboot = AlphaIsActive ? alphaServer : bravoServer;
933  if (toReboot != null)
934  {
935  if (!await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false))
936  logger.LogWarning("Unable to send reboot state change event!");
937  }
938  }
939  }
940 
942  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
943  {
944  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
945  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
946  }
947 
949  public async Task StartAsync(CancellationToken cancellationToken)
950  {
951  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
952  if (!autoStart && reattachInfo == null)
953  return;
954 
955  long? adminUserId = null;
956 
957  await databaseContextFactory.UseContext(async db => adminUserId = await db.Users
958  .Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant())
959  .Select(x => x.Id)
960  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
961  var job = new Models.Job
962  {
963  StartedBy = new Models.User
964  {
965  Id = adminUserId.Value
966  },
967  Instance = new Models.Instance
968  {
969  Id = instance.Id
970  },
971  Description = "Instance startup watchdog launch",
972  CancelRight = (ulong)DreamDaemonRights.Shutdown,
973  CancelRightsType = RightsType.DreamDaemon
974  };
975  await jobManager.RegisterOperation(job, async (j, databaseContext, progressFunction, ct) =>
976  {
977  using (await SemaphoreSlimContext.Lock(semaphore, ct).ConfigureAwait(false))
978  await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
979  }, cancellationToken).ConfigureAwait(false);
980  }
981 
983  public async Task StopAsync(CancellationToken cancellationToken)
984  {
985  if (releaseServers && Running)
986  {
987  await StopMonitor().ConfigureAwait(false);
988 
989  var reattachInformation = new WatchdogReattachInformation
990  {
991  AlphaIsActive = AlphaIsActive
992  };
993  reattachInformation.Alpha = alphaServer?.Release();
994  reattachInformation.Bravo = bravoServer?.Release();
995  await reattachInfoHandler.Save(reattachInformation, cancellationToken).ConfigureAwait(false);
996  }
997  await Terminate(false, cancellationToken).ConfigureAwait(false);
998  }
999 
1001  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
1002  {
1003  string results;
1004  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1005  {
1006  if (!Running)
1007  return true;
1008 
1009  var builder = new StringBuilder(Constants.DMTopicEvent);
1010  builder.Append('&');
1011  var notification = new EventNotification
1012  {
1013  Type = eventType,
1014  Parameters = parameters
1015  };
1016  var json = JsonConvert.SerializeObject(notification);
1017  builder.Append(byondTopicSender.SanitizeString(Constants.DMParameterData));
1018  builder.Append('=');
1019  builder.Append(byondTopicSender.SanitizeString(json));
1020 
1021  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1022  results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
1023  }
1024 
1025  if (results == Constants.DMResponseSuccess)
1026  return true;
1027 
1028  List<Response> responses;
1029  try
1030  {
1031  responses = JsonConvert.DeserializeObject<List<Response>>(results);
1032  }
1033  catch
1034  {
1035  logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
1036  return true;
1037  }
1038 
1039  await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
1040 
1041  return true;
1042  }
1043 
1045  public async Task<string> HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
1046  {
1047  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1048  {
1049  if (!Running)
1050  return "ERROR: Server offline!";
1051 
1052  var commandObject = new ChatCommand
1053  {
1054  Command = commandName,
1055  Params = arguments,
1056  User = sender
1057  };
1058 
1059  var json = JsonConvert.SerializeObject(commandObject, new JsonSerializerSettings
1060  {
1061  ContractResolver = new CamelCasePropertyNamesContractResolver()
1062  });
1063 
1064  var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json));
1065 
1066  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1067  return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
1068  }
1069  }
1070 
1072  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
1073  {
1074  releaseServers = true;
1075  if (Running)
1076  await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false);
1077  }
1078  }
1079 }
bool InactiveServerCritFail
If the inactive server is in an unrecoverable state
Definition: MonitorState.cs:18
Manages the runtime of Jobs
Definition: IJobManager.cs:12
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Watchdog
Definition: Watchdog.cs:71
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
Definition: Watchdog.cs:894
diff --git a/_conflict_exception_8cs_source.html b/_conflict_exception_8cs_source.html index 0539cedea4..40dd9210c0 100644 --- a/_conflict_exception_8cs_source.html +++ b/_conflict_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ConflictException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class ConflictException : ClientException
11  {
17  public ConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage, statusCode)
18  { }
19 
23  public ConflictException() { }
24 
29  public ConflictException(string message) : base(message) { }
30 
36  public ConflictException(string message, Exception innerException) : base(message, innerException) { }
37  }
38 }
-
ConflictException()
Construct a ConflictException
+Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class ConflictException : ClientException
11  {
17  public ConflictException(ErrorMessage errorMessage, HttpStatusCode statusCode) : base(errorMessage, statusCode)
18  { }
19 
23  public ConflictException() { }
24 
29  public ConflictException(string message) : base(message) { }
30 
36  public ConflictException(string message, Exception innerException) : base(message, innerException) { }
37  }
38 }
ConflictException()
Construct a ConflictException
ConflictException(string message, Exception innerException)
Construct an ConflictException with a message and innerException
ConflictException(string message)
Construct an ConflictException with a message
diff --git a/_control_panel_configuration_8cs.html b/_control_panel_configuration_8cs.html new file mode 100644 index 0000000000..14abbff295 --- /dev/null +++ b/_control_panel_configuration_8cs.html @@ -0,0 +1,120 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs File Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
ControlPanelConfiguration.cs File Reference
+
+
+ +

Go to the source code of this file.

+ + + + + +

+Classes

class  Tgstation.Server.Host.Configuration.ControlPanelConfiguration
 Configuration options for the web control panel More...
 
+ + + +

+Namespaces

namespace  Tgstation.Server.Host.Configuration
 
+
+ + + + diff --git a/_control_panel_configuration_8cs_source.html b/_control_panel_configuration_8cs_source.html new file mode 100644 index 0000000000..598abfe7da --- /dev/null +++ b/_control_panel_configuration_8cs_source.html @@ -0,0 +1,107 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Configuration/ControlPanelConfiguration.cs Source File + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
ControlPanelConfiguration.cs
+
+
+Go to the documentation of this file.
1 using System.Collections.Generic;
2 
3 namespace Tgstation.Server.Host.Configuration
4 {
8  public sealed class ControlPanelConfiguration
9  {
13  public const string Section = "ControlPanel";
14 
18  public bool Enable { get; set; }
19 
23  public bool AllowAnyOrigin { get; set; }
24 
28  public List<string> AllowedOrigins { get; set; }
29  }
30 }
+ + + +
+ + + + diff --git a/_d_b_connection_factory_8cs_source.html b/_d_b_connection_factory_8cs_source.html index 4738430f50..ff8f0539d1 100644 --- a/_d_b_connection_factory_8cs_source.html +++ b/_d_b_connection_factory_8cs_source.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
1 using MySql.Data.MySqlClient;
2 using System;
3 using System.Data.Common;
4 using System.Data.SqlClient;
5 using System.Globalization;
7 
8 namespace Tgstation.Server.Host.Core
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  default:
33  throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType, "Invalid DatabaseType!");
34  }
35  }
36  }
37 }
- +
DbConnection CreateConnection(string connectionString, DatabaseType databaseType)
Create a DbConnection
diff --git a/_database_configuration_8cs_source.html b/_database_configuration_8cs_source.html index c92d40e674..994bf50b8c 100644 --- a/_database_configuration_8cs_source.html +++ b/_database_configuration_8cs_source.html @@ -92,10 +92,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseConfiguration.cs
-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 string MySqlServerVersion { get; set; }
41  }
42 }
+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 string MySqlServerVersion { get; set; }
41  }
42 }
+ +
Configuration options for the Models.DatabaseContext<TParentContext>
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
diff --git a/_database_context_8cs_source.html b/_database_context_8cs_source.html index e12946dd96..a36e16da99 100644 --- a/_database_context_8cs_source.html +++ b/_database_context_8cs_source.html @@ -92,8 +92,7 @@ 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;
13 
14 namespace Tgstation.Server.Host.Models
15 {
17  abstract class DatabaseContext<TParentContext> : DbContext, IDatabaseContext where TParentContext : DbContext
18  {
20  public DbSet<User> Users { get; set; }
21 
23  public DbSet<Instance> Instances { get; set; }
24 
26  public DbSet<CompileJob> CompileJobs { get; set; }
27 
29  public DbSet<RevisionInformation> RevisionInformations { get; set; }
30 
32  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
33 
35  public DbSet<ChatBot> ChatBots { get; set; }
36 
38  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
39 
41  public DbSet<RepositorySettings> RepositorySettings { get; set; }
42 
44  public DbSet<InstanceUser> InstanceUsers { get; set; }
45 
47  public DbSet<ChatChannel> ChatChannels { get; set; }
48 
50  public DbSet<Job> Jobs { get; set; }
51 
53  public DbSet<ReattachInformation> ReattachInformations { get; set; }
54 
56  public DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
57 
61  public DbSet<TestMerge> TestMerges { get; set; }
62 
66  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
67 
71  protected ILogger Logger { get; }
72 
77 
82 
90  public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
91  {
92  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
93  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
94  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
95  }
96 
98  protected override void OnModelCreating(ModelBuilder modelBuilder)
99  {
100  Logger.LogTrace("Building entity framework context...");
101  base.OnModelCreating(modelBuilder);
102 
103  var userModel = modelBuilder.Entity<User>();
104  userModel.HasIndex(x => x.CanonicalName).IsUnique();
105  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
106 
107  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
108 
109  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.Cascade);
110 
111  var revInfo = modelBuilder.Entity<RevisionInformation>();
112  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
113  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
114  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Restrict);
115  revInfo.HasIndex(x => x.CommitSha).IsUnique();
116 
117  modelBuilder.Entity<CompileJob>().HasIndex(x => x.DirectoryName);
118 
119  modelBuilder.Entity<Job>().HasOne<CompileJob>().WithOne(x => x.Job).OnDelete(DeleteBehavior.Restrict);
120 
121  var chatChannel = modelBuilder.Entity<ChatChannel>();
122  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
123  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
124  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
125 
126  modelBuilder.Entity<ChatBot>().HasIndex(x => x.Name).IsUnique();
127 
128  var instanceModel = modelBuilder.Entity<Instance>();
129  instanceModel.HasIndex(x => x.Path).IsUnique();
130  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
131  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
132  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
133  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
134  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
135  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
136  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
137  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
138  }
139 
141  public async Task Initialize(CancellationToken cancellationToken)
142  {
144  {
145  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
146  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
147  }
148 
149  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
150  var wasEmpty = !migrations.Any();
151 
152  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
153  {
154  Logger.LogInformation("Migrating database...");
155  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
156  }
157  else
158  Logger.LogDebug("No migrations to apply.");
159 
160  wasEmpty |= (await Users.CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
161 
162  if (wasEmpty)
163  {
164  Logger.LogInformation("Seeding database...");
165  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
166  }
168  {
169  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
170  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
171  }
172  }
173 
175  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
176 
181  protected abstract bool UseMySQLMigrations();
182 
184  public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
185  {
186  if (version == null)
187  throw new ArgumentNullException(nameof(version));
188  if (version < new Version(4, 0))
189  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
190 
191  string targetMigration = null;
192 
193  //Update this with new migrations as they are made
194  //Always use the MS class
195 
196  //TODO: Uncomment once #816 is merged
197  /*
198  if (version < new Version(4, 0, 2))
199  targetMigration = nameof(MSReattachCompileJobRequired);
200  */
201 
202  if (targetMigration == null)
203  return;
204 
205  if (UseMySQLMigrations())
206  targetMigration = String.Format(CultureInfo.InvariantCulture, "MY" + targetMigration.Substring(2));
207 
208  //even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
209  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
210  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
211 
212  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
213  try
214  {
215  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
216  }
217  catch (Exception e)
218  {
219  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
220  }
221  }
222  }
223 }
- +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;
13 
14 namespace Tgstation.Server.Host.Models
15 {
17  abstract class DatabaseContext<TParentContext> : DbContext, IDatabaseContext where TParentContext : DbContext
18  {
20  public DbSet<User> Users { get; set; }
21 
23  public DbSet<Instance> Instances { get; set; }
24 
26  public DbSet<CompileJob> CompileJobs { get; set; }
27 
29  public DbSet<RevisionInformation> RevisionInformations { get; set; }
30 
32  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
33 
35  public DbSet<ChatBot> ChatBots { get; set; }
36 
38  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
39 
41  public DbSet<RepositorySettings> RepositorySettings { get; set; }
42 
44  public DbSet<InstanceUser> InstanceUsers { get; set; }
45 
47  public DbSet<ChatChannel> ChatChannels { get; set; }
48 
50  public DbSet<Job> Jobs { get; set; }
51 
53  public DbSet<ReattachInformation> ReattachInformations { get; set; }
54 
56  public DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
57 
61  public DbSet<TestMerge> TestMerges { get; set; }
62 
66  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
67 
71  protected ILogger Logger { get; }
72 
77 
82 
90  public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
91  {
92  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
93  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
94  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
95  }
96 
98  protected override void OnModelCreating(ModelBuilder modelBuilder)
99  {
100  Logger.LogTrace("Building entity framework context...");
101  base.OnModelCreating(modelBuilder);
102 
103  var userModel = modelBuilder.Entity<User>();
104  userModel.HasIndex(x => x.CanonicalName).IsUnique();
105  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
106 
107  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
108 
109  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.Cascade);
110 
111  var revInfo = modelBuilder.Entity<RevisionInformation>();
112  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
113  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
114  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Restrict);
115  revInfo.HasIndex(x => x.CommitSha).IsUnique();
116 
117  modelBuilder.Entity<CompileJob>().HasIndex(x => x.DirectoryName);
118 
119  modelBuilder.Entity<Job>().HasOne<CompileJob>().WithOne(x => x.Job).OnDelete(DeleteBehavior.Restrict);
120 
121  var chatChannel = modelBuilder.Entity<ChatChannel>();
122  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
123  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
124  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
125 
126  modelBuilder.Entity<ChatBot>().HasIndex(x => x.Name).IsUnique();
127 
128  var instanceModel = modelBuilder.Entity<Instance>();
129  instanceModel.HasIndex(x => x.Path).IsUnique();
130  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
131  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
132  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
133  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
134  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
135  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
136  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
137  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
138  }
139 
141  public async Task Initialize(CancellationToken cancellationToken)
142  {
144  {
145  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
146  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
147  }
148 
149  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
150  var wasEmpty = !migrations.Any();
151 
152  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
153  {
154  Logger.LogInformation("Migrating database...");
155  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
156  }
157  else
158  Logger.LogDebug("No migrations to apply.");
159 
160  wasEmpty |= (await Users.CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
161 
162  if (wasEmpty)
163  {
164  Logger.LogInformation("Seeding database...");
165  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
166  }
168  {
169  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
170  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
171  }
172  }
173 
175  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
176 
181  protected abstract bool UseMySQLMigrations();
182 
184  public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
185  {
186  if (version == null)
187  throw new ArgumentNullException(nameof(version));
188  if (version < new Version(4, 0))
189  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
190 
191  string targetMigration = null;
192 
193  //Update this with new migrations as they are made
194  //Always use the MS class
195 
196  //TODO: Uncomment once #816 is merged
197  /*
198  if (version < new Version(4, 0, 2))
199  targetMigration = nameof(MSReattachCompileJobRequired);
200  */
201 
202  if (targetMigration == null)
203  return;
204 
205  if (UseMySQLMigrations())
206  targetMigration = String.Format(CultureInfo.InvariantCulture, "MY" + targetMigration.Substring(2));
207 
208  //even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
209  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
210  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
211 
212  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
213  try
214  {
215  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
216  }
217  catch (Exception e)
218  {
219  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
220  }
221  }
222  }
223 }
@@ -104,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task Initialize(CancellationToken cancellationToken)
Creates and migrates the IDatabaseContext
- +
bool ResetAdminPassword
If the admin user should be enabled and have it&#39;s password reset
override void OnModelCreating(ModelBuilder modelBuilder)
async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
Attempt to downgrade the schema to the migration used for a given server version ...
diff --git a/_design_time_db_context_factory_helpers_8cs_source.html b/_design_time_db_context_factory_helpers_8cs_source.html index 0529fa9dff..7de992db1d 100644 --- a/_design_time_db_context_factory_helpers_8cs_source.html +++ b/_design_time_db_context_factory_helpers_8cs_source.html @@ -97,7 +97,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Contains helpers for creating design time Models.DatabaseContext<TParentContext>s ...
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the DatabaseConfiguration re...
- +
Configuration options for the Models.DatabaseContext<TParentContext>
diff --git a/_discord_provider_8cs_source.html b/_discord_provider_8cs_source.html index 1ca53128b2..3566d21e7d 100644 --- a/_discord_provider_8cs_source.html +++ b/_discord_provider_8cs_source.html @@ -92,8 +92,7 @@ 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;
10 
11 namespace Tgstation.Server.Host.Components.Chat.Providers
12 {
16  sealed class DiscordProvider : Provider
17  {
19  public override bool Connected => client.ConnectionState == ConnectionState.Connected;
20 
22  public override string BotMention
23  {
24  get
25  {
26  if (!Connected)
27  throw new InvalidOperationException("Provider not connected");
28  return NormalizeMention(client.CurrentUser.Mention);
29  }
30  }
31 
35  readonly ILogger<DiscordProvider> logger;
36 
40  readonly DiscordSocketClient client;
41 
45  readonly string botToken;
46 
50  readonly List<ulong> mappedChannels;
51 
57  static string NormalizeMention(string fromDiscord) => fromDiscord.Replace("!", "", StringComparison.Ordinal);
58 
64  public DiscordProvider(ILogger<DiscordProvider> logger, string botToken)
65  {
66  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
68  client = new DiscordSocketClient();
69  client.MessageReceived += Client_MessageReceived;
70  mappedChannels = new List<ulong>();
71  }
72 
74  public override void Dispose() => client.Dispose();
75 
81  Task Client_MessageReceived(SocketMessage e)
82  {
83  if (e.Author.Id == client.CurrentUser.Id)
84  return Task.CompletedTask;
85 
86  var pm = e.Channel is IPrivateChannel;
87 
88  if (!pm && !mappedChannels.Contains(e.Channel.Id))
89  return e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id) ? SendMessage(e.Channel.Id, "I do not respond to this channel!", default) : Task.CompletedTask;
90 
91  var result = new Message
92  {
93  Content = e.Content,
94  User = new User
95  {
96  RealId = e.Author.Id,
97  Channel = new Channel
98  {
99  RealId = e.Channel.Id,
100  IsPrivateChannel = pm,
101  ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
102  FriendlyName = e.Channel.Name
103  //isAdmin and Tag populated by manager
104  },
105  FriendlyName = e.Author.Username,
106  Mention = NormalizeMention(e.Author.Mention)
107  }
108  };
109  EnqueueMessage(result);
110  return Task.CompletedTask;
111  }
112 
114  public override async Task<bool> Connect(CancellationToken cancellationToken)
115  {
116  if (Connected)
117  return true;
118 
119  try
120  {
121  await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
122 
123  cancellationToken.ThrowIfCancellationRequested();
124 
125  await client.StartAsync().ConfigureAwait(false);
126 
127  var channelsAvailable = new TaskCompletionSource<object>();
128  client.Ready += () =>
129  {
130  channelsAvailable.TrySetResult(null);
131  return Task.CompletedTask;
132  };
133  using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
134  await channelsAvailable.Task.ConfigureAwait(false);
135  }
136  catch (OperationCanceledException)
137  {
138  throw;
139  }
140  catch (Exception e)
141  {
142  logger.LogWarning("Error connecting to Discord: {0}", e);
143  return false;
144  }
145 
146  return true;
147  }
148 
150  public override async Task Disconnect(CancellationToken cancellationToken)
151  {
152  if (!Connected)
153  return;
154 
155  try
156  {
157  await client.StopAsync().ConfigureAwait(false);
158  cancellationToken.ThrowIfCancellationRequested();
159  await client.LogoutAsync().ConfigureAwait(false);
160  }
161  catch (OperationCanceledException)
162  {
163  throw;
164  }
165  catch (Exception e)
166  {
167  logger.LogWarning("Error disconnecting from discord: {0}", e);
168  }
169  }
170 
172  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
173  {
174  if (channels == null)
175  throw new ArgumentNullException(nameof(channels));
176 
177  if (!Connected)
178  throw new InvalidOperationException("Provider not connected!");
179 
180  Channel GetChannelForChatChannel(ChatChannel channel)
181  {
182  if (!channel.DiscordChannelId.HasValue)
183  throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
184 
185  if (!(client.GetChannel(channel.DiscordChannelId.Value) is ITextChannel discordChannel))
186  return null;
187 
188  return new Channel
189  {
190  RealId = discordChannel.Id,
191  IsAdminChannel = channel.IsAdminChannel == true,
192  ConnectionName = discordChannel.Guild.Name,
193  FriendlyName = discordChannel.Name,
194  IsPrivateChannel = false,
195  Tag = channel.Tag
196  };
197  };
198 
199  var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null).ToList();
200 
201  lock (this)
202  {
203  mappedChannels.Clear();
204  mappedChannels.AddRange(enumerator.Select(x => x.RealId));
205  }
206 
207  return Task.FromResult<IReadOnlyList<Channel>>(enumerator);
208  }
209 
211  public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
212  {
213  try
214  {
215  var channel = client.GetChannel(channelId) as IMessageChannel;
216  await (channel?.SendMessageAsync(message, false, null, new RequestOptions
217  {
218  CancelToken = cancellationToken
219  }) ?? Task.CompletedTask).ConfigureAwait(false);
220  }
221  catch (OperationCanceledException)
222  {
223  throw;
224  }
225  catch (Exception e)
226  {
227  logger.LogWarning("Error sending discord message: {0}", e);
228  }
229  }
230  }
231 }
-
Indicates a chat channel
Definition: ChatChannel.cs:8
+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;
10 
11 namespace Tgstation.Server.Host.Components.Chat.Providers
12 {
16  sealed class DiscordProvider : Provider
17  {
19  public override bool Connected => client.ConnectionState == ConnectionState.Connected;
20 
22  public override string BotMention
23  {
24  get
25  {
26  if (!Connected)
27  throw new InvalidOperationException("Provider not connected");
28  return NormalizeMention(client.CurrentUser.Mention);
29  }
30  }
31 
35  readonly ILogger<DiscordProvider> logger;
36 
40  readonly DiscordSocketClient client;
41 
45  readonly string botToken;
46 
50  readonly List<ulong> mappedChannels;
51 
57  static string NormalizeMention(string fromDiscord) => fromDiscord.Replace("!", "", StringComparison.Ordinal);
58 
64  public DiscordProvider(ILogger<DiscordProvider> logger, string botToken)
65  {
66  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
68  client = new DiscordSocketClient();
69  client.MessageReceived += Client_MessageReceived;
70  mappedChannels = new List<ulong>();
71  }
72 
74  public override void Dispose() => client.Dispose();
75 
81  Task Client_MessageReceived(SocketMessage e)
82  {
83  if (e.Author.Id == client.CurrentUser.Id)
84  return Task.CompletedTask;
85 
86  var pm = e.Channel is IPrivateChannel;
87 
88  if (!pm && !mappedChannels.Contains(e.Channel.Id))
89  return e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id) ? SendMessage(e.Channel.Id, "I do not respond to this channel!", default) : Task.CompletedTask;
90 
91  var result = new Message
92  {
93  Content = e.Content,
94  User = new User
95  {
96  RealId = e.Author.Id,
97  Channel = new Channel
98  {
99  RealId = e.Channel.Id,
100  IsPrivateChannel = pm,
101  ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
102  FriendlyName = e.Channel.Name
103  //isAdmin and Tag populated by manager
104  },
105  FriendlyName = e.Author.Username,
106  Mention = NormalizeMention(e.Author.Mention)
107  }
108  };
109  EnqueueMessage(result);
110  return Task.CompletedTask;
111  }
112 
114  public override async Task<bool> Connect(CancellationToken cancellationToken)
115  {
116  if (Connected)
117  return true;
118 
119  try
120  {
121  await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
122 
123  cancellationToken.ThrowIfCancellationRequested();
124 
125  await client.StartAsync().ConfigureAwait(false);
126 
127  var channelsAvailable = new TaskCompletionSource<object>();
128  client.Ready += () =>
129  {
130  channelsAvailable.TrySetResult(null);
131  return Task.CompletedTask;
132  };
133  using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
134  await channelsAvailable.Task.ConfigureAwait(false);
135  }
136  catch (OperationCanceledException)
137  {
138  throw;
139  }
140  catch (Exception e)
141  {
142  logger.LogWarning("Error connecting to Discord: {0}", e);
143  return false;
144  }
145 
146  return true;
147  }
148 
150  public override async Task Disconnect(CancellationToken cancellationToken)
151  {
152  if (!Connected)
153  return;
154 
155  try
156  {
157  await client.StopAsync().ConfigureAwait(false);
158  cancellationToken.ThrowIfCancellationRequested();
159  await client.LogoutAsync().ConfigureAwait(false);
160  }
161  catch (OperationCanceledException)
162  {
163  throw;
164  }
165  catch (Exception e)
166  {
167  logger.LogWarning("Error disconnecting from discord: {0}", e);
168  }
169  }
170 
172  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
173  {
174  if (channels == null)
175  throw new ArgumentNullException(nameof(channels));
176 
177  if (!Connected)
178  throw new InvalidOperationException("Provider not connected!");
179 
180  Channel GetChannelForChatChannel(ChatChannel channel)
181  {
182  if (!channel.DiscordChannelId.HasValue)
183  throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
184 
185  if (!(client.GetChannel(channel.DiscordChannelId.Value) is ITextChannel discordChannel))
186  return null;
187 
188  return new Channel
189  {
190  RealId = discordChannel.Id,
191  IsAdminChannel = channel.IsAdminChannel == true,
192  ConnectionName = discordChannel.Guild.Name,
193  FriendlyName = discordChannel.Name,
194  IsPrivateChannel = false,
195  Tag = channel.Tag
196  };
197  };
198 
199  var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null).ToList();
200 
201  lock (this)
202  {
203  mappedChannels.Clear();
204  mappedChannels.AddRange(enumerator.Select(x => x.RealId));
205  }
206 
207  return Task.FromResult<IReadOnlyList<Channel>>(enumerator);
208  }
209 
211  public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
212  {
213  try
214  {
215  var channel = client.GetChannel(channelId) as IMessageChannel;
216  await (channel?.SendMessageAsync(message, false, null, new RequestOptions
217  {
218  CancelToken = cancellationToken
219  }) ?? Task.CompletedTask).ConfigureAwait(false);
220  }
221  catch (OperationCanceledException)
222  {
223  throw;
224  }
225  catch (Exception e)
226  {
227  logger.LogWarning("Error sending discord message: {0}", e);
228  }
229  }
230  }
231 }
Indicates a chat channel
Definition: ChatChannel.cs:8
diff --git a/_dmb_factory_8cs_source.html b/_dmb_factory_8cs_source.html index 1f38ff23ba..1a6bb53368 100644 --- a/_dmb_factory_8cs_source.html +++ b/_dmb_factory_8cs_source.html @@ -92,8 +92,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;
9 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components.Compiler
13 {
18  {
20  public Task OnNewerDmb
21  {
22  get
23  {
24  lock (this)
25  return newerDmbTcs.Task;
26  }
27  }
28 
30  public bool DmbAvailable => nextDmbProvider != null;
31 
43  readonly ILogger<DmbFactory> logger;
44 
48  readonly Api.Models.Instance instance;
52  readonly CancellationTokenSource cleanupCts;
53 
61  TaskCompletionSource<object> newerDmbTcs;
66 
67  Dictionary<long, int> jobLockCounts;
68 
76  public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger<DmbFactory> logger, Api.Models.Instance instance)
77  {
78  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
79  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
80  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
81  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
82 
83  cleanupTask = Task.CompletedTask;
84  newerDmbTcs = new TaskCompletionSource<object>();
85  cleanupCts = new CancellationTokenSource();
86  jobLockCounts = new Dictionary<long, int>();
87  }
88 
90  public void Dispose() => cleanupCts.Dispose(); //we don't dispose nextDmbProvider here, since it might be the only thing we have
91 
96  void CleanJob(CompileJob job)
97  {
98  async Task HandleCleanup()
99  {
100  var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
101  Task otherTask;
102  //lock (this) //already locked below
103  otherTask = cleanupTask;
104  await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
105  }
106  lock (this)
107  {
108  if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
109  {
110  jobLockCounts.Remove(job.Id);
111  logger.LogDebug("Cleaning compile job {0} => {1}", job.Id, job.DirectoryName);
112  cleanupTask = HandleCleanup();
113  }
114  else
115  {
116  var decremented = --jobLockCounts[job.Id];
117  logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented);
118  }
119  }
120  }
121 
123  public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
124  {
125  if (job == null)
126  throw new ArgumentNullException(nameof(job));
127 
128  var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
129  if (newProvider == null)
130  return;
131  lock (this)
132  {
133  nextDmbProvider?.Dispose();
134  nextDmbProvider = newProvider;
135  newerDmbTcs.SetResult(nextDmbProvider);
136  newerDmbTcs = new TaskCompletionSource<object>();
137  }
138  }
139 
141  public IDmbProvider LockNextDmb(int lockCount)
142  {
143  if (!DmbAvailable)
144  throw new InvalidOperationException("No .dmb available!");
145  if (lockCount < 0)
146  throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
147  lock (this)
148  {
149  var jobId = nextDmbProvider.CompileJob.Id;
150  var incremented = jobLockCounts[jobId] += lockCount;
151  logger.LogTrace("Compile job {0} lock count now: {1}", jobId, incremented);
152  return nextDmbProvider;
153  }
154  }
155 
157  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
158  {
159  //where complete clause not necessary, only successful COMPILEjobs get in the db
160  var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id)
161  .OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
162  if (cj == default(CompileJob))
163  return;
164  await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
165  //we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
166  });
167 
169  public async Task StopAsync(CancellationToken cancellationToken)
170  {
171  using (cancellationToken.Register(() => cleanupCts.Cancel()))
172  await cleanupTask.ConfigureAwait(false);
173  }
174 
176  public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
177  {
178  if (compileJob == null)
179  throw new ArgumentNullException(nameof(compileJob));
180 
181  //ensure we have the entire compile job tree
182  await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id)
183  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
184  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
185  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
186  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); //can't wait to see that query
187 
188  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
189  var providerSubmitted = false;
190  var newProvider = new DmbProvider(compileJob, ioManager, () =>
191  {
192  if (providerSubmitted)
193  CleanJob(compileJob);
194  });
195 
196  try
197  {
198  var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
199  var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
200 
201  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
202  {
203  logger.LogWarning("Error loading compile job, .dmb missing!");
204  return null; //omae wa mou shinderu
205  }
206 
207  lock (this)
208  {
209  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
210  {
211  value = 1;
212  jobLockCounts.Add(compileJob.Id, 1);
213  }
214  else
215  jobLockCounts[compileJob.Id] = ++value;
216 
217  logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
218 
219  providerSubmitted = true;
220  return newProvider;
221  }
222  }
223  finally
224  {
225  if (!providerSubmitted)
226  newProvider.Dispose();
227  }
228  }
229 
231  public async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
232  {
233  List<long> jobIdsToSkip;
234  //don't clean locked directories
235  lock (this)
236  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
237 
238  List<string> jobUidsToNotErase = null;
239 
240  //find the uids of locked directories
241  await databaseContextFactory.UseContext(async db =>
242  {
243  jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
244  }).ConfigureAwait(false);
245 
246  //add the other exemption
247  if (exceptThisOne != null)
248  jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
249 
250  //cleanup
251  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
252  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
253  int deleting = 0;
254  var tasks = directories.Select(async x =>
255  {
256  var nameOnly = ioManager.GetFileName(x);
257  if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
258  return;
259  try
260  {
261  ++deleting;
262  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
263  }
264  catch (OperationCanceledException)
265  {
266  throw;
267  }
268  catch (Exception e)
269  {
270  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
271  }
272  }).ToList();
273  if (deleting > 0)
274  {
275  logger.LogDebug("Cleaning {0} unused game folders...", deleting);
276  await Task.WhenAll().ConfigureAwait(false);
277  }
278  }
279  }
280 }
- +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;
9 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components.Compiler
13 {
18  {
20  public Task OnNewerDmb
21  {
22  get
23  {
24  lock (this)
25  return newerDmbTcs.Task;
26  }
27  }
28 
30  public bool DmbAvailable => nextDmbProvider != null;
31 
43  readonly ILogger<DmbFactory> logger;
44 
48  readonly Api.Models.Instance instance;
52  readonly CancellationTokenSource cleanupCts;
53 
61  TaskCompletionSource<object> newerDmbTcs;
66 
67  Dictionary<long, int> jobLockCounts;
68 
76  public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger<DmbFactory> logger, Api.Models.Instance instance)
77  {
78  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
79  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
80  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
81  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
82 
83  cleanupTask = Task.CompletedTask;
84  newerDmbTcs = new TaskCompletionSource<object>();
85  cleanupCts = new CancellationTokenSource();
86  jobLockCounts = new Dictionary<long, int>();
87  }
88 
90  public void Dispose() => cleanupCts.Dispose(); //we don't dispose nextDmbProvider here, since it might be the only thing we have
91 
96  void CleanJob(CompileJob job)
97  {
98  async Task HandleCleanup()
99  {
100  var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
101  Task otherTask;
102  //lock (this) //already locked below
103  otherTask = cleanupTask;
104  await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
105  }
106  lock (this)
107  {
108  if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
109  {
110  jobLockCounts.Remove(job.Id);
111  logger.LogDebug("Cleaning compile job {0} => {1}", job.Id, job.DirectoryName);
112  cleanupTask = HandleCleanup();
113  }
114  else
115  {
116  var decremented = --jobLockCounts[job.Id];
117  logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented);
118  }
119  }
120  }
121 
123  public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
124  {
125  if (job == null)
126  throw new ArgumentNullException(nameof(job));
127 
128  var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
129  if (newProvider == null)
130  return;
131  lock (this)
132  {
133  nextDmbProvider?.Dispose();
134  nextDmbProvider = newProvider;
135  newerDmbTcs.SetResult(nextDmbProvider);
136  newerDmbTcs = new TaskCompletionSource<object>();
137  }
138  }
139 
141  public IDmbProvider LockNextDmb(int lockCount)
142  {
143  if (!DmbAvailable)
144  throw new InvalidOperationException("No .dmb available!");
145  if (lockCount < 0)
146  throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
147  lock (this)
148  {
149  var jobId = nextDmbProvider.CompileJob.Id;
150  var incremented = jobLockCounts[jobId] += lockCount;
151  logger.LogTrace("Compile job {0} lock count now: {1}", jobId, incremented);
152  return nextDmbProvider;
153  }
154  }
155 
157  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
158  {
159  //where complete clause not necessary, only successful COMPILEjobs get in the db
160  var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id)
161  .OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
162  if (cj == default(CompileJob))
163  return;
164  await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
165  //we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
166  });
167 
169  public async Task StopAsync(CancellationToken cancellationToken)
170  {
171  using (cancellationToken.Register(() => cleanupCts.Cancel()))
172  await cleanupTask.ConfigureAwait(false);
173  }
174 
176  public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
177  {
178  if (compileJob == null)
179  throw new ArgumentNullException(nameof(compileJob));
180 
181  //ensure we have the entire compile job tree
182  await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id)
183  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
184  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
185  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
186  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); //can't wait to see that query
187 
188  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
189  var providerSubmitted = false;
190  var newProvider = new DmbProvider(compileJob, ioManager, () =>
191  {
192  if (providerSubmitted)
193  CleanJob(compileJob);
194  });
195 
196  try
197  {
198  var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
199  var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
200 
201  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
202  {
203  logger.LogWarning("Error loading compile job, .dmb missing!");
204  return null; //omae wa mou shinderu
205  }
206 
207  lock (this)
208  {
209  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
210  {
211  value = 1;
212  jobLockCounts.Add(compileJob.Id, 1);
213  }
214  else
215  jobLockCounts[compileJob.Id] = ++value;
216 
217  logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
218 
219  providerSubmitted = true;
220  return newProvider;
221  }
222  }
223  finally
224  {
225  if (!providerSubmitted)
226  newProvider.Dispose();
227  }
228  }
229 
231  public async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
232  {
233  List<long> jobIdsToSkip;
234  //don't clean locked directories
235  lock (this)
236  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
237 
238  List<string> jobUidsToNotErase = null;
239 
240  //find the uids of locked directories
241  await databaseContextFactory.UseContext(async db =>
242  {
243  jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
244  }).ConfigureAwait(false);
245 
246  //add the other exemption
247  if (exceptThisOne != null)
248  jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
249 
250  //cleanup
251  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
252  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
253  int deleting = 0;
254  var tasks = directories.Select(async x =>
255  {
256  var nameOnly = ioManager.GetFileName(x);
257  if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
258  return;
259  try
260  {
261  ++deleting;
262  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
263  }
264  catch (OperationCanceledException)
265  {
266  throw;
267  }
268  catch (Exception e)
269  {
270  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
271  }
272  }).ToList();
273  if (deleting > 0)
274  {
275  logger.LogDebug("Cleaning {0} unused game folders...", deleting);
276  await Task.WhenAll().ConfigureAwait(false);
277  }
278  }
279  }
280 }
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:65
Task cleanupTask
Task representing calls to CleanJob(CompileJob)
Definition: DmbFactory.cs:57
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
diff --git a/_home_controller_8cs_source.html b/_home_controller_8cs_source.html index 350d36aec0..7536a3855c 100644 --- a/_home_controller_8cs_source.html +++ b/_home_controller_8cs_source.html @@ -92,9 +92,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
HomeController.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.Net.Http.Headers;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Controllers
15 {
19  [Route(Routes.Root)]
20  public sealed class HomeController : ApiController
21  {
42 
54  public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, ILogger<HomeController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
55  {
56  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
57  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
58  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
59  this.application = application ?? throw new ArgumentNullException(nameof(application));
60  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
61  }
62 
67  [TgsAuthorize]
68  [HttpGet]
69  public JsonResult Home() => Json(new Api.Models.ServerInformation
70  {
71  Version = application.Version,
72  ApiVersion = ApiHeaders.Version
73  });
74 
80  [HttpPost]
81  public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
82  {
84  return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
85 
86  ISystemIdentity identity;
87  try
88  {
89  //trust the system over the database because a user's name can change while still having the same SID
90  identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
91  }
92  catch (NotImplementedException)
93  {
94  identity = null;
95  }
96  using (identity)
97  {
98  IQueryable<User> query;
99  if (identity == null)
100  query = DatabaseContext.Users.Where(x => x.CanonicalName == ApiHeaders.Username.ToUpperInvariant());
101  else
102  query = DatabaseContext.Users.Where(x => x.SystemIdentifier == identity.Uid);
103  var user = await query.Select(x => new User
104  {
105  Id = x.Id,
106  PasswordHash = x.PasswordHash,
107  Enabled = x.Enabled,
108  Name = x.Name
109  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
110 
111  if (user == null)
112  return Unauthorized();
113 
114  if (identity == null)
115  {
116  var originalHash = user.PasswordHash;
117  if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
118  return Unauthorized();
119  if (user.PasswordHash != originalHash)
120  {
121  var updatedUser = new User
122  {
123  Id = user.Id
124  };
125  DatabaseContext.Users.Attach(updatedUser);
126  updatedUser.PasswordHash = user.PasswordHash;
127  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
128  }
129  }
130  //check if the name changed and updoot accordingly
131  else if (identity.Username != user.Name)
132  {
133  DatabaseContext.Users.Attach(user);
134  user.Name = identity.Username;
135  user.CanonicalName = user.Name.ToUpperInvariant();
136  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
137  }
138 
139  if (!user.Enabled.Value)
140  return Forbid();
141 
142  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
143  if (identity != null)
144  {
145  //expire the identity slightly after the auth token in case of lag
146  var identExpiry = token.ExpiresAt.Value;
147  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
148  identExpiry += TimeSpan.FromSeconds(15);
149  identityCache.CacheSystemIdentity(user, identity, identExpiry);
150  }
151 
152  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
153 
154  return Json(token);
155  }
156  }
157  }
158 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authorization;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.EntityFrameworkCore;
4 using Microsoft.Extensions.Logging;
5 using Microsoft.Extensions.Options;
6 using System;
7 using System.Linq;
8 using System.Net.Mime;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
16 using Wangkanai.Detection;
17 
18 namespace Tgstation.Server.Host.Controllers
19 {
23  [Route(Routes.Root)]
24  public sealed class HomeController : ApiController
25  {
46 
50  readonly IBrowserResolver browserResolver;
51 
56 
70  public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, IBrowserResolver browserResolver, IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions, ILogger<HomeController> logger) : base(databaseContext, authenticationContextFactory, logger, false, false)
71  {
72  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
73  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
74  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
75  this.application = application ?? throw new ArgumentNullException(nameof(application));
76  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
77  this.browserResolver = browserResolver ?? throw new ArgumentNullException(nameof(browserResolver));
78  controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
79  }
80 
85  [TgsAuthorize]
86  [AllowAnonymous]
87  [HttpGet]
88  public IActionResult Home()
89  {
90  if (AuthenticationContext != null)
91  return Json(new Api.Models.ServerInformation
92  {
93  Version = application.Version,
94  ApiVersion = ApiHeaders.Version
95  });
96 
97  //if we are using a browser and the control panel, soft redirect to the app page
98  if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic)
99  {
100  Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), loading control panel...", browserResolver.UserAgent);
101  return File("~/index.html", MediaTypeNames.Text.Html);
102  }
103 
104  return Unauthorized();
105  }
106 
112  [HttpPost]
113  public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
114  {
115  if (ApiHeaders == null)
116  return BadRequest(new Api.Models.ErrorMessage { Message = "Missing API headers!" });
117 
119  return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
120 
121  ISystemIdentity identity;
122  try
123  {
124  //trust the system over the database because a user's name can change while still having the same SID
125  identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
126  }
127  catch (NotImplementedException)
128  {
129  identity = null;
130  }
131  using (identity)
132  {
133  IQueryable<User> query;
134  if (identity == null)
135  query = DatabaseContext.Users.Where(x => x.CanonicalName == ApiHeaders.Username.ToUpperInvariant());
136  else
137  query = DatabaseContext.Users.Where(x => x.SystemIdentifier == identity.Uid);
138  var user = await query.Select(x => new User
139  {
140  Id = x.Id,
141  PasswordHash = x.PasswordHash,
142  Enabled = x.Enabled,
143  Name = x.Name
144  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
145 
146  if (user == null)
147  return Unauthorized();
148 
149  if (identity == null)
150  {
151  var originalHash = user.PasswordHash;
152  if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
153  return Unauthorized();
154  if (user.PasswordHash != originalHash)
155  {
156  var updatedUser = new User
157  {
158  Id = user.Id
159  };
160  DatabaseContext.Users.Attach(updatedUser);
161  updatedUser.PasswordHash = user.PasswordHash;
162  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
163  }
164  }
165  //check if the name changed and updoot accordingly
166  else if (identity.Username != user.Name)
167  {
168  DatabaseContext.Users.Attach(user);
169  user.Name = identity.Username;
170  user.CanonicalName = user.Name.ToUpperInvariant();
171  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
172  }
173 
174  if (!user.Enabled.Value)
175  return Forbid();
176 
177  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
178  if (identity != null)
179  {
180  //expire the identity slightly after the auth token in case of lag
181  var identExpiry = token.ExpiresAt.Value;
182  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
183  identExpiry += TimeSpan.FromSeconds(15);
184  identityCache.CacheSystemIdentity(user, identity, identExpiry);
185  }
186 
187  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
188 
189  return Json(token);
190  }
191  }
192  }
193 }
-
async Task< IActionResult > CreateToken(CancellationToken cancellationToken)
Attempt to authenticate a User using ApiController.ApiHeaders
+ +
async Task< IActionResult > CreateToken(CancellationToken cancellationToken)
Attempt to authenticate a User using ApiController.ApiHeaders
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
@@ -102,19 +103,24 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents a user on the current System.Runtime.InteropServices.OSPlatform
+
IActionResult Home()
Main page of the Application
string Username
The client&#39;s username
Definition: ApiHeaders.cs:81
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
+
HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, IBrowserResolver browserResolver, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< HomeController > logger)
Construct a HomeController
Contains various cryptographic functions
-
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the HomeController
+
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the HomeController
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:16
+
readonly ControlPanelConfiguration controlPanelConfiguration
The ControlPanelConfiguration for the HomeController
string Password
The client&#39;s password
Definition: ApiHeaders.cs:86
-
readonly IApplication application
The IApplication for the HomeController
+
readonly IApplication application
The IApplication for the HomeController
Routes to a server actions
Definition: Routes.cs:9
-
Main ApiController for the Application
+
Main ApiController for the Application
+ -
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController
+
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController
+
bool IsTokenAuthentication
If the header uses password or JWT authentication
Definition: ApiHeaders.cs:91
@@ -122,12 +128,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
long Id
The ID of the User
Definition: User.cs:15
-
HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, ILogger< HomeController > logger)
Construct a HomeController
-
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the HomeController
+
readonly IBrowserResolver browserResolver
The IBrowserResolver for the HomeController
+
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the HomeController
-
readonly ITokenFactory tokenFactory
The ITokenFactory for the HomeController
+
readonly ITokenFactory tokenFactory
The ITokenFactory for the HomeController
string Uid
A unique identifier for the user
diff --git a/_i_application_8cs_source.html b/_i_application_8cs_source.html index d37ba421e2..46912ac3fe 100644 --- a/_i_application_8cs_source.html +++ b/_i_application_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IApplication.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Host.Core
4 {
8  public interface IApplication
9  {
13  string VersionPrefix { get; }
14 
18  string VersionString { get; }
19 
23  Version Version { get; }
24 
29  void Ready(Exception initializationError);
30  }
31 }
-
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Host.Core
4 {
8  public interface IApplication
9  {
13  string VersionPrefix { get; }
14 
18  string VersionString { get; }
19 
23  Version Version { get; }
24 
29  void Ready(Exception initializationError);
30  }
31 }
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
diff --git a/_i_d_b_connection_factory_8cs_source.html b/_i_d_b_connection_factory_8cs_source.html index 2544fbc2f9..86ea4a81ff 100644 --- a/_i_d_b_connection_factory_8cs_source.html +++ b/_i_d_b_connection_factory_8cs_source.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
1 using System.Data.Common;
3 
4 namespace Tgstation.Server.Host.Core
5 {
10  {
17  DbConnection CreateConnection(string connectionString, DatabaseType databaseType);
18  }
19 }
- +
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
diff --git a/_i_server_control_8cs_source.html b/_i_server_control_8cs_source.html index 9efed90ffd..a29579902f 100644 --- a/_i_server_control_8cs_source.html +++ b/_i_server_control_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IServerControl.cs
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 using Tgstation.Server.Host.IO;
5 
6 namespace Tgstation.Server.Host.Core
7 {
11  public interface IServerControl
12  {
16  bool WatchdogPresent { get; }
17 
25  bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager);
26 
32  IRestartRegistration RegisterForRestart(IRestartHandler handler);
33 
38  Task Restart();
39  }
40 }
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 using Tgstation.Server.Host.IO;
5 
6 namespace Tgstation.Server.Host.Core
7 {
11  public interface IServerControl
12  {
16  bool WatchdogPresent { get; }
17 
25  bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager);
26 
32  IRestartRegistration RegisterForRestart(IRestartHandler handler);
33 
38  Task Restart();
39 
45  Task Die(Exception exception);
46  }
47 }
diff --git a/_instance_controller_8cs_source.html b/_instance_controller_8cs_source.html index fef5fbbd4f..3c52c1c10e 100644 --- a/_instance_controller_8cs_source.html +++ b/_instance_controller_8cs_source.html @@ -92,9 +92,8 @@ 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 System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.IO;
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;
19 using Tgstation.Server.Host.IO;
22 
23 namespace Tgstation.Server.Host.Controllers
24 {
28  [Route(Routes.InstanceManager)]
29  public sealed class InstanceController : ModelController<Api.Models.Instance>
30  {
34  const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
35 
36  const string MoveInstanceJobPrefix = "Move instance ID ";
37 
42 
47 
52 
57 
62 
74  public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger<InstanceController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
75  {
76  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
77  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
78  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
79  this.application = application ?? throw new ArgumentNullException(nameof(application));
80  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
81  }
82 
83  void NormalizeModelPath(Api.Models.Instance model, out string absolutePath)
84  {
85  if (model.Path == null)
86  {
87  absolutePath = null;
88  return;
89  }
90  absolutePath = ioManager.ResolvePath(model.Path);
91  if (platformIdentifier.IsWindows)
92  model.Path = absolutePath.ToUpperInvariant();
93  else
94  model.Path = absolutePath;
95  }
96 
97  Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
98  {
99  ByondRights = (ByondRights)~0U,
106  UserId = AuthenticationContext.User.Id
107  };
108 
110  [TgsAuthorize(InstanceManagerRights.Create)]
111  public override async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
112  {
113  if (model == null)
114  throw new ArgumentNullException(nameof(model));
115 
116  if (String.IsNullOrWhiteSpace(model.Name))
117  return BadRequest(new ErrorMessage { Message = "name must not be empty!" });
118 
119  if (model.Path == null)
120  return BadRequest(new ErrorMessage { Message = "path must not be empty!" });
121 
122  NormalizeModelPath(model, out var rawPath);
123 
124  var localPath = ioManager.ResolvePath(".");
125  NormalizeModelPath(new Api.Models.Instance
126  {
127  Path = localPath
128  }, out var normalizedLocalPath);
129 
130  if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal))
131  return Conflict("Instances cannot be created in the installation directory!");
132 
133 
134  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
135  bool attached = false;
136  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
137  if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
138  return Conflict(new ErrorMessage { Message = "Path not empty!" });
139  else
140  attached = true;
141 
142  var newInstance = new Models.Instance
143  {
144  ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
146  {
147  AllowWebClient = false,
148  AutoStart = false,
149  PrimaryPort = 1337,
150  SecondaryPort = 1338,
151  SecurityLevel = DreamDaemonSecurity.Safe,
152  SoftRestart = false,
153  SoftShutdown = false,
154  StartupTimeout = 20
155  },
157  {
158  ApiValidationPort = 1339,
159  ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
160  },
161  Name = model.Name,
162  Online = false,
163  Path = model.Path,
164  AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
166  {
167  CommitterEmail = "tgstation-server@users.noreply.github.com",
168  CommitterName = application.VersionPrefix,
169  PushTestMergeCommits = false,
170  ShowTestMergeCommitters = false,
171  AutoUpdatesKeepTestMerges = false,
172  AutoUpdatesSynchronize = false
173  },
174  //give this user full privileges on the instance
175  InstanceUsers = new List<Models.InstanceUser>
176  {
177  InstanceAdminUser()
178  }
179  };
180 
181  DatabaseContext.Instances.Add(newInstance);
182  try
183  {
184  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
185 
186  try
187  {
188  //actually reserve it now
189  await ioManager.CreateDirectory(rawPath, cancellationToken).ConfigureAwait(false);
190  await ioManager.DeleteFile(ioManager.ConcatPath(rawPath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
191  }
192  catch
193  {
194  //oh shit delete the model
195  DatabaseContext.Instances.Remove(newInstance);
196 
197  await DatabaseContext.Save(default).ConfigureAwait(false);
198  throw;
199  }
200  }
201  catch (IOException e)
202  {
203  return Conflict(new ErrorMessage { Message = e.Message });
204  }
205  catch (DbUpdateException e)
206  {
207  return Conflict(new ErrorMessage { Message = e.Message });
208  }
209 
210  Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
211 
212  var api = newInstance.ToApi();
213  return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
214  }
215 
217  [TgsAuthorize(InstanceManagerRights.Delete)]
218  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
219  {
220  var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id)
221  .Include(x => x.WatchdogReattachInformation)
222  .Include(x => x.WatchdogReattachInformation.Alpha)
223  .Include(x => x.WatchdogReattachInformation.Bravo)
224  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
225  if (originalModel == default)
226  return StatusCode((int)HttpStatusCode.Gone);
227  if (originalModel.Online.Value)
228  return Conflict(new ErrorMessage
229  {
230  Message = "Cannot detach an online instance!"
231  });
232 
233  if (originalModel.WatchdogReattachInformation != null)
234  {
235  DatabaseContext.WatchdogReattachInformations.Remove(originalModel.WatchdogReattachInformation);
236  if (originalModel.WatchdogReattachInformation.Alpha != null)
237  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Alpha);
238  if (originalModel.WatchdogReattachInformation.Bravo != null)
239  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Bravo);
240  }
241 
242  DatabaseContext.Instances.Remove(originalModel);
243 
244  var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
245  await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), default).ConfigureAwait(false);
246  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); //cascades everything
247  return Ok();
248  }
249 
251  [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)]
252  public override async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
253  {
254  var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id);
255 
256  var moveJob = await instanceQuery
257  .SelectMany(x => x.Jobs).
258 #pragma warning disable CA1307 // Specify StringComparison
259  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
260 #pragma warning restore CA1307 // Specify StringComparison
261  .Select(x => new Models.Job
262  {
263  Id = x.Id
264  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
265 
266  if (moveJob != default)
267  //cancel it now
268  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false);
269 
270  var usersInstanceUserTask = instanceQuery.SelectMany(x => x.InstanceUsers).Where(x => x.UserId == AuthenticationContext.User.Id).FirstOrDefaultAsync(cancellationToken);
271 
272  var originalModel = await instanceQuery
273  .Include(x => x.RepositorySettings)
274  .Include(x => x.ChatSettings)
275  .ThenInclude(x => x.Channels)
276  .Include(x => x.DreamDaemonSettings) //need these for onlining
277  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
278  if (originalModel == default(Models.Instance))
279  return StatusCode((int)HttpStatusCode.Gone);
280 
281  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
282  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
283  {
284  var memberSelectorExpression = (MemberExpression)expression.Body;
285  var property = (PropertyInfo)memberSelectorExpression.Member;
286 
287  var newVal = property.GetValue(model);
288  if (newVal == null)
289  return false;
290  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
291  return true;
292 
293  property.SetValue(originalModel, newVal);
294  return false;
295  };
296 
297  string originalModelPath = null;
298  string rawPath = null;
299  if (model.Path != null)
300  {
301  NormalizeModelPath(model, out rawPath);
302 
303  if (model.Path != originalModel.Path)
304  {
305  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
306  return Forbid();
307  if (originalModel.Online.Value && model.Online != true)
308  return Conflict(new ErrorMessage { Message = "Cannot relocate an online instance!" });
309 
310  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
311  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
312  return Conflict(new ErrorMessage { Message = "Path not empty!" });
313 
314  originalModelPath = originalModel.Path;
315  originalModel.Path = model.Path;
316  }
317  }
318 
319  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
320  var originalOnline = originalModel.Online.Value;
321  var renamed = model.Name != null && originalModel.Name != model.Name;
322 
323  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
324  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
325  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
326  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline))
327  return Forbid();
328 
329  //ensure the current user has write privilege on the instance
330  var usersInstanceUser = await usersInstanceUserTask.ConfigureAwait(false);
331  if (usersInstanceUser == default)
332  {
333  var instanceAdminUser = InstanceAdminUser();
334  instanceAdminUser.InstanceId = originalModel.Id;
335  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
336  }
337  else
338  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
339 
340  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
341 
342  if (renamed)
343  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
344 
345  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
346  try
347  {
348  if (originalOnline && model.Online == false)
349  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
350  else if (!originalOnline && model.Online == true)
351  {
352  //force autostart false here because we don't want any long running jobs right now
353  //remember to document this
354  originalModel.DreamDaemonSettings.AutoStart = false;
355  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
356  }
357  }
358  catch (Exception e)
359  {
360  if(!(e is OperationCanceledException))
361  Logger.LogError("Error changing instance online state! Exception: {0}", e);
362  originalModel.Online = originalOnline;
363  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
364  if (originalModelPath != null)
365  originalModel.Path = originalModelPath;
366  await DatabaseContext.Save(default).ConfigureAwait(false);
367  throw;
368  }
369 
370  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
371  {
372  Id = originalModel.Id
373  };
374  if (originalModelPath != null)
375  {
376  var job = new Models.Job
377  {
378  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
379  Instance = originalModel,
380  CancelRightsType = RightsType.InstanceManager,
381  CancelRight = (ulong)InstanceManagerRights.Relocate,
382  StartedBy = AuthenticationContext.User
383  };
384 
385  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
386  api.MoveJob = job.ToApi();
387  }
388 
389  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
390  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
391 
392  return Json(api);
393  }
394 
396  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
397  public override async Task<IActionResult> List(CancellationToken cancellationToken)
398  {
399  IQueryable<Models.Instance> query = DatabaseContext.Instances;
401  query = query.Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id)).Where(x => x.InstanceUsers.Any(y => y.AnyRights));
402 
403  var moveJobTasks = query
404  .SelectMany(x => x.Jobs)
405 #pragma warning disable CA1307 // Specify StringComparison
406  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
407 #pragma warning restore CA1307 // Specify StringComparison
408  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
409  .Include(x => x.Instance)
410  .ToListAsync(cancellationToken);
411  var instances = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
412  var apis = instances.Select(x => x.ToApi());
413  var moveJobs = await moveJobTasks.ConfigureAwait(false);
414  foreach(var I in moveJobs)
415  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
416  return Json(apis);
417  }
418 
420  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
421  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
422  {
423  var query = DatabaseContext.Instances.Where(x => x.Id == id);
424  var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
425 
426  if (cantList)
427  query = query.Include(x => x.InstanceUsers);
428 
429  var moveJobTask = query
430  .SelectMany(x => x.Jobs)
431 #pragma warning disable CA1307 // Specify StringComparison
432  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
433 #pragma warning restore CA1307 // Specify StringComparison
434  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
435  .FirstOrDefaultAsync(cancellationToken);
436  var instance = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
437 
438  if (instance == null)
439  return StatusCode((int)HttpStatusCode.Gone);
440 
441  if (cantList && !instance.InstanceUsers.Any(x => x.UserId == AuthenticationContext.User.Id && x.AnyRights))
442  return Forbid();
443 
444  var api = instance.ToApi();
445  api.MoveJob = (await moveJobTask.ConfigureAwait(false))?.ToApi();
446  return Json(api);
447  }
448  }
449 }
string Name
The name of the User
Definition: User.cs:38
+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.Globalization;
7 using System.IO;
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;
19 using Tgstation.Server.Host.IO;
22 
23 namespace Tgstation.Server.Host.Controllers
24 {
28  [Route(Routes.InstanceManager)]
29  public sealed class InstanceController : ModelController<Api.Models.Instance>
30  {
34  const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
35 
36  const string MoveInstanceJobPrefix = "Move instance ID ";
37 
42 
47 
52 
57 
62 
74  public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger<InstanceController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
75  {
76  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
77  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
78  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
79  this.application = application ?? throw new ArgumentNullException(nameof(application));
80  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
81  }
82 
83  void NormalizeModelPath(Api.Models.Instance model, out string absolutePath)
84  {
85  if (model.Path == null)
86  {
87  absolutePath = null;
88  return;
89  }
90  absolutePath = ioManager.ResolvePath(model.Path);
91  if (platformIdentifier.IsWindows)
92  model.Path = absolutePath.ToUpperInvariant();
93  else
94  model.Path = absolutePath;
95  }
96 
97  Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
98  {
99  ByondRights = (ByondRights)~0U,
106  UserId = AuthenticationContext.User.Id
107  };
108 
110  [TgsAuthorize(InstanceManagerRights.Create)]
111  public override async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
112  {
113  if (model == null)
114  throw new ArgumentNullException(nameof(model));
115 
116  if (String.IsNullOrWhiteSpace(model.Name))
117  return BadRequest(new ErrorMessage { Message = "name must not be empty!" });
118 
119  if (model.Path == null)
120  return BadRequest(new ErrorMessage { Message = "path must not be empty!" });
121 
122  NormalizeModelPath(model, out var rawPath);
123 
124  var localPath = ioManager.ResolvePath(".");
125  NormalizeModelPath(new Api.Models.Instance
126  {
127  Path = localPath
128  }, out var normalizedLocalPath);
129 
130  if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal))
131  return Conflict("Instances cannot be created in the installation directory!");
132 
133 
134  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
135  bool attached = false;
136  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
137  if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
138  return Conflict(new ErrorMessage { Message = "Path not empty!" });
139  else
140  attached = true;
141 
142  var newInstance = new Models.Instance
143  {
144  ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
146  {
147  AllowWebClient = false,
148  AutoStart = false,
149  PrimaryPort = 1337,
150  SecondaryPort = 1338,
151  SecurityLevel = DreamDaemonSecurity.Safe,
152  SoftRestart = false,
153  SoftShutdown = false,
154  StartupTimeout = 20
155  },
157  {
158  ApiValidationPort = 1339,
159  ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
160  },
161  Name = model.Name,
162  Online = false,
163  Path = model.Path,
164  AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
166  {
167  CommitterEmail = "tgstation-server@users.noreply.github.com",
168  CommitterName = application.VersionPrefix,
169  PushTestMergeCommits = false,
170  ShowTestMergeCommitters = false,
171  AutoUpdatesKeepTestMerges = false,
172  AutoUpdatesSynchronize = false
173  },
174  //give this user full privileges on the instance
175  InstanceUsers = new List<Models.InstanceUser>
176  {
177  InstanceAdminUser()
178  }
179  };
180 
181  DatabaseContext.Instances.Add(newInstance);
182  try
183  {
184  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
185 
186  try
187  {
188  //actually reserve it now
189  await ioManager.CreateDirectory(rawPath, cancellationToken).ConfigureAwait(false);
190  await ioManager.DeleteFile(ioManager.ConcatPath(rawPath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
191  }
192  catch
193  {
194  //oh shit delete the model
195  DatabaseContext.Instances.Remove(newInstance);
196 
197  await DatabaseContext.Save(default).ConfigureAwait(false);
198  throw;
199  }
200  }
201  catch (IOException e)
202  {
203  return Conflict(new ErrorMessage { Message = e.Message });
204  }
205  catch (DbUpdateException e)
206  {
207  return Conflict(new ErrorMessage { Message = e.Message });
208  }
209 
210  Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
211 
212  var api = newInstance.ToApi();
213  return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
214  }
215 
217  [TgsAuthorize(InstanceManagerRights.Delete)]
218  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
219  {
220  var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id)
221  .Include(x => x.WatchdogReattachInformation)
222  .Include(x => x.WatchdogReattachInformation.Alpha)
223  .Include(x => x.WatchdogReattachInformation.Bravo)
224  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
225  if (originalModel == default)
226  return StatusCode((int)HttpStatusCode.Gone);
227  if (originalModel.Online.Value)
228  return Conflict(new ErrorMessage
229  {
230  Message = "Cannot detach an online instance!"
231  });
232 
233  if (originalModel.WatchdogReattachInformation != null)
234  {
235  DatabaseContext.WatchdogReattachInformations.Remove(originalModel.WatchdogReattachInformation);
236  if (originalModel.WatchdogReattachInformation.Alpha != null)
237  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Alpha);
238  if (originalModel.WatchdogReattachInformation.Bravo != null)
239  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Bravo);
240  }
241 
242  DatabaseContext.Instances.Remove(originalModel);
243 
244  var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
245  await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), default).ConfigureAwait(false);
246  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); //cascades everything
247  return Ok();
248  }
249 
251  [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)]
252  public override async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
253  {
254  var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id);
255 
256  var moveJob = await instanceQuery
257  .SelectMany(x => x.Jobs).
258 #pragma warning disable CA1307 // Specify StringComparison
259  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
260 #pragma warning restore CA1307 // Specify StringComparison
261  .Select(x => new Models.Job
262  {
263  Id = x.Id
264  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
265 
266  if (moveJob != default)
267  //cancel it now
268  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false);
269 
270  var usersInstanceUserTask = instanceQuery.SelectMany(x => x.InstanceUsers).Where(x => x.UserId == AuthenticationContext.User.Id).FirstOrDefaultAsync(cancellationToken);
271 
272  var originalModel = await instanceQuery
273  .Include(x => x.RepositorySettings)
274  .Include(x => x.ChatSettings)
275  .ThenInclude(x => x.Channels)
276  .Include(x => x.DreamDaemonSettings) //need these for onlining
277  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
278  if (originalModel == default(Models.Instance))
279  return StatusCode((int)HttpStatusCode.Gone);
280 
281  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
282  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
283  {
284  var memberSelectorExpression = (MemberExpression)expression.Body;
285  var property = (PropertyInfo)memberSelectorExpression.Member;
286 
287  var newVal = property.GetValue(model);
288  if (newVal == null)
289  return false;
290  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
291  return true;
292 
293  property.SetValue(originalModel, newVal);
294  return false;
295  };
296 
297  string originalModelPath = null;
298  string rawPath = null;
299  if (model.Path != null)
300  {
301  NormalizeModelPath(model, out rawPath);
302 
303  if (model.Path != originalModel.Path)
304  {
305  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
306  return Forbid();
307  if (originalModel.Online.Value && model.Online != true)
308  return Conflict(new ErrorMessage { Message = "Cannot relocate an online instance!" });
309 
310  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
311  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
312  return Conflict(new ErrorMessage { Message = "Path not empty!" });
313 
314  originalModelPath = originalModel.Path;
315  originalModel.Path = model.Path;
316  }
317  }
318 
319  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
320  var originalOnline = originalModel.Online.Value;
321  var renamed = model.Name != null && originalModel.Name != model.Name;
322 
323  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
324  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
325  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
326  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline))
327  return Forbid();
328 
329  //ensure the current user has write privilege on the instance
330  var usersInstanceUser = await usersInstanceUserTask.ConfigureAwait(false);
331  if (usersInstanceUser == default)
332  {
333  var instanceAdminUser = InstanceAdminUser();
334  instanceAdminUser.InstanceId = originalModel.Id;
335  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
336  }
337  else
338  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
339 
340  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
341 
342  if (renamed)
343  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
344 
345  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
346  try
347  {
348  if (originalOnline && model.Online == false)
349  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
350  else if (!originalOnline && model.Online == true)
351  {
352  //force autostart false here because we don't want any long running jobs right now
353  //remember to document this
354  originalModel.DreamDaemonSettings.AutoStart = false;
355  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
356  }
357  }
358  catch (Exception e)
359  {
360  if(!(e is OperationCanceledException))
361  Logger.LogError("Error changing instance online state! Exception: {0}", e);
362  originalModel.Online = originalOnline;
363  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
364  if (originalModelPath != null)
365  originalModel.Path = originalModelPath;
366  await DatabaseContext.Save(default).ConfigureAwait(false);
367  throw;
368  }
369 
370  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
371  {
372  Id = originalModel.Id
373  };
374  if (originalModelPath != null)
375  {
376  var job = new Models.Job
377  {
378  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
379  Instance = originalModel,
380  CancelRightsType = RightsType.InstanceManager,
381  CancelRight = (ulong)InstanceManagerRights.Relocate,
382  StartedBy = AuthenticationContext.User
383  };
384 
385  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
386  api.MoveJob = job.ToApi();
387  }
388 
389  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
390  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
391 
392  return Json(api);
393  }
394 
396  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
397  public override async Task<IActionResult> List(CancellationToken cancellationToken)
398  {
399  IQueryable<Models.Instance> query = DatabaseContext.Instances;
401  query = query.Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id)).Where(x => x.InstanceUsers.Any(y => y.AnyRights));
402 
403  var moveJobTasks = query
404  .SelectMany(x => x.Jobs)
405 #pragma warning disable CA1307 // Specify StringComparison
406  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
407 #pragma warning restore CA1307 // Specify StringComparison
408  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
409  .Include(x => x.Instance)
410  .ToListAsync(cancellationToken);
411  var instances = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
412  var apis = instances.Select(x => x.ToApi());
413  var moveJobs = await moveJobTasks.ConfigureAwait(false);
414  foreach(var I in moveJobs)
415  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
416  return Json(apis);
417  }
418 
420  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
421  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
422  {
423  var query = DatabaseContext.Instances.Where(x => x.Id == id);
424  var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
425 
426  if (cantList)
427  query = query.Include(x => x.InstanceUsers);
428 
429  var moveJobTask = query
430  .SelectMany(x => x.Jobs)
431 #pragma warning disable CA1307 // Specify StringComparison
432  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
433 #pragma warning restore CA1307 // Specify StringComparison
434  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
435  .FirstOrDefaultAsync(cancellationToken);
436  var instance = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
437 
438  if (instance == null)
439  return StatusCode((int)HttpStatusCode.Gone);
440 
441  if (cantList && !instance.InstanceUsers.Any(x => x.UserId == AuthenticationContext.User.Id && x.AnyRights))
442  return Forbid();
443 
444  var api = instance.ToApi();
445  api.MoveJob = (await moveJobTask.ConfigureAwait(false))?.ToApi();
446  return Json(api);
447  }
448  }
449 }
string Name
The name of the User
Definition: User.cs:38
-
Manages the runtime of Jobs
Definition: IJobManager.cs:12
ConfigurationRights
Rights for Models.ConfigurationFile
diff --git a/_instance_manager_8cs_source.html b/_instance_manager_8cs_source.html index 75978513e9..47b8a5161f 100644 --- a/_instance_manager_8cs_source.html +++ b/_instance_manager_8cs_source.html @@ -92,42 +92,42 @@ 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 System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components
13 {
16  {
21 
26 
31 
36 
41 
45  readonly ILogger<InstanceManager> logger;
46 
50  readonly Dictionary<long, IInstance> instances;
51 
56 
60  bool disposed;
61 
72  public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger<InstanceManager> logger)
73  {
74  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
75  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
76  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
77  this.application = application ?? throw new ArgumentNullException(nameof(application));
78  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
79  if (serverControl == null)
80  throw new ArgumentNullException(nameof(serverControl));
81  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
82 
83  serverControl.RegisterForRestart(this);
84 
85  instances = new Dictionary<long, IInstance>();
86  }
87 
89  public void Dispose()
90  {
91  lock (this)
92  {
93  if (disposed)
94  return;
95  disposed = true;
96  }
97  foreach (var I in instances)
98  I.Value.Dispose();
99  }
100 
102  public IInstance GetInstance(Models.Instance metadata)
103  {
104  if (metadata == null)
105  throw new ArgumentNullException(nameof(metadata));
106  lock (this)
107  {
108  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
109  throw new InvalidOperationException("Instance not online!");
110  return instance;
111  }
112  }
113 
115  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
116  {
117  if (newPath == null)
118  throw new ArgumentNullException(nameof(newPath));
119  if (instance.Online.Value)
120  throw new InvalidOperationException("Cannot move an online instance!");
121  var oldPath = instance.Path;
122  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
123  await databaseContextFactory.UseContext(db =>
124  {
125  var targetInstance = new Models.Instance
126  {
127  Id = instance.Id
128  };
129  db.Instances.Attach(targetInstance);
130  targetInstance.Path = newPath;
131  return db.Save(cancellationToken);
132  }).ConfigureAwait(false);
133  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
134  }
135 
137  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
138  {
139  if (metadata == null)
140  throw new ArgumentNullException(nameof(metadata));
141  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
142  IInstance instance;
143  lock (this)
144  {
145  if (!instances.TryGetValue(metadata.Id, out instance))
146  throw new InvalidOperationException("Instance not online!");
147  instances.Remove(metadata.Id);
148  }
149  try
150  {
151  //we are the one responsible for cancelling his jobs
152  var tasks = new List<Task>();
153  await databaseContextFactory.UseContext(async db =>
154  {
155  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
156  {
157  Id = x.Id
158  }).ToAsyncEnumerable();
159  await jobs.ForEachAsync(job =>
160  {
161  lock (tasks)
162  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
163  }, cancellationToken).ConfigureAwait(false);
164  }).ConfigureAwait(false);
165 
166  await Task.WhenAll(tasks).ConfigureAwait(false);
167 
168  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
169  }
170  finally
171  {
172  instance.Dispose();
173  }
174  }
175 
177  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
178  {
179  if (metadata == null)
180  throw new ArgumentNullException(nameof(metadata));
181  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
182  var instance = instanceFactory.CreateInstance(metadata);
183  try
184  {
185  lock (this)
186  {
187  if (instances.ContainsKey(metadata.Id))
188  throw new InvalidOperationException("Instance already online!");
189  instances.Add(metadata.Id, instance);
190  }
191  }
192  catch
193  {
194  instance.Dispose();
195  throw;
196  }
197  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
198  }
199 
201  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
202  {
203  try
204  {
205  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
206  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
207  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
208  var dbInstances = databaseContext.Instances.Where(x => x.Online.Value)
209  .Include(x => x.RepositorySettings)
210  .Include(x => x.ChatSettings)
211  .ThenInclude(x => x.Channels)
212  .Include(x => x.DreamDaemonSettings)
213  .ToAsyncEnumerable();
214  var tasks = new List<Task>();
215  await factoryStartup.ConfigureAwait(false);
216  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
217  await Task.WhenAll(tasks).ConfigureAwait(false);
218  logger.LogInformation("Instance manager ready!");
219  application.Ready(null);
220  }
221  catch (OperationCanceledException)
222  {
223  logger.LogInformation("Cancelled instance manager initialization!");
224  }
225  catch (Exception e)
226  {
227  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
228  application.Ready(e);
229  }
230  });
231 
233  public async Task StopAsync(CancellationToken cancellationToken)
234  {
235  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
236  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
237  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
238 
239  //downgrade the db if necessary
240  if (downgradeVersion != null)
241  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
242  }
243 
245  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
246  {
247  downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
248  return Task.CompletedTask;
249  }
250  }
251 }
-
Manages the runtime of Jobs
Definition: IJobManager.cs:12
-
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
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;
10 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components
13 {
16  {
21 
26 
31 
36 
41 
46 
50  readonly ILogger<InstanceManager> logger;
51 
55  readonly Dictionary<long, IInstance> instances;
56 
61 
65  bool disposed;
66 
77  public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger<InstanceManager> logger)
78  {
79  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
80  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
81  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
82  this.application = application ?? throw new ArgumentNullException(nameof(application));
83  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
84  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
85  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
86 
87  serverControl.RegisterForRestart(this);
88 
89  instances = new Dictionary<long, IInstance>();
90  }
91 
93  public void Dispose()
94  {
95  lock (this)
96  {
97  if (disposed)
98  return;
99  disposed = true;
100  }
101  foreach (var I in instances)
102  I.Value.Dispose();
103  }
104 
106  public IInstance GetInstance(Models.Instance metadata)
107  {
108  if (metadata == null)
109  throw new ArgumentNullException(nameof(metadata));
110  lock (this)
111  {
112  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
113  throw new InvalidOperationException("Instance not online!");
114  return instance;
115  }
116  }
117 
119  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
120  {
121  if (newPath == null)
122  throw new ArgumentNullException(nameof(newPath));
123  if (instance.Online.Value)
124  throw new InvalidOperationException("Cannot move an online instance!");
125  var oldPath = instance.Path;
126  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
127  await databaseContextFactory.UseContext(db =>
128  {
129  var targetInstance = new Models.Instance
130  {
131  Id = instance.Id
132  };
133  db.Instances.Attach(targetInstance);
134  targetInstance.Path = newPath;
135  return db.Save(cancellationToken);
136  }).ConfigureAwait(false);
137  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
138  }
139 
141  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
142  {
143  if (metadata == null)
144  throw new ArgumentNullException(nameof(metadata));
145  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
146  IInstance instance;
147  lock (this)
148  {
149  if (!instances.TryGetValue(metadata.Id, out instance))
150  throw new InvalidOperationException("Instance not online!");
151  instances.Remove(metadata.Id);
152  }
153  try
154  {
155  //we are the one responsible for cancelling his jobs
156  var tasks = new List<Task>();
157  await databaseContextFactory.UseContext(async db =>
158  {
159  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
160  {
161  Id = x.Id
162  }).ToAsyncEnumerable();
163  await jobs.ForEachAsync(job =>
164  {
165  lock (tasks)
166  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
167  }, cancellationToken).ConfigureAwait(false);
168  }).ConfigureAwait(false);
169 
170  await Task.WhenAll(tasks).ConfigureAwait(false);
171 
172  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
173  }
174  finally
175  {
176  instance.Dispose();
177  }
178  }
179 
181  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
182  {
183  if (metadata == null)
184  throw new ArgumentNullException(nameof(metadata));
185  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
186  var instance = instanceFactory.CreateInstance(metadata);
187  try
188  {
189  lock (this)
190  {
191  if (instances.ContainsKey(metadata.Id))
192  throw new InvalidOperationException("Instance already online!");
193  instances.Add(metadata.Id, instance);
194  }
195  }
196  catch
197  {
198  instance.Dispose();
199  throw;
200  }
201  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
202  }
203 
205  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
206  {
207  try
208  {
209  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
210  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
211  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
212  var dbInstances = databaseContext.Instances.Where(x => x.Online.Value)
213  .Include(x => x.RepositorySettings)
214  .Include(x => x.ChatSettings)
215  .ThenInclude(x => x.Channels)
216  .Include(x => x.DreamDaemonSettings)
217  .ToAsyncEnumerable();
218  var tasks = new List<Task>();
219  await factoryStartup.ConfigureAwait(false);
220  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
221  await Task.WhenAll(tasks).ConfigureAwait(false);
222  logger.LogInformation("Server ready!");
223  application.Ready(null);
224  }
225  catch (OperationCanceledException)
226  {
227  logger.LogInformation("Cancelled instance manager initialization!");
228  }
229  catch (Exception e)
230  {
231  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
232  application.Ready(e);
233  try
234  {
235  await serverControl.Die(e).ConfigureAwait(false);
236  }
237  catch (Exception e2)
238  {
239  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
240  }
241  }
242  });
243 
245  public async Task StopAsync(CancellationToken cancellationToken)
246  {
247  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
248  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
249  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
250 
251  //downgrade the db if necessary
252  if (downgradeVersion != null)
253  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
254  }
255 
257  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
258  {
259  downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
260  return Task.CompletedTask;
261  }
262  }
263 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
+
readonly IServerControl serverControl
The IServerControl for the InstanceManager
+
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
-
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
+
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
bool disposed
If the InstanceManager has been Disposed
-
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
-
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
+
bool disposed
If the InstanceManager has been Disposed
+
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
+
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
readonly IJobManager jobManager
The IJobManager for the InstanceManager
-
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
+
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
For interacting with the instance services
Definition: IInstance.cs:17
- -
async Task StopAsync(CancellationToken cancellationToken)
+ +
async Task StopAsync(CancellationToken cancellationToken)
-
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
+
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
-
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
+
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
readonly IApplication application
The IApplication for the InstanceManager
readonly IIOManager ioManager
The IIOManager for the InstanceManager
Interface for using filesystems
Definition: IIOManager.cs:11
-
async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
Move an IInstance
+
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
-
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger< InstanceManager > logger)
Construct an InstanceManager
+
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger< InstanceManager > logger)
Construct an InstanceManager
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_insufficient_permissions_exception_8cs_source.html b/_insufficient_permissions_exception_8cs_source.html index 4876ca8371..5564174664 100644 --- a/_insufficient_permissions_exception_8cs_source.html +++ b/_insufficient_permissions_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InsufficientPermissionsException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
11  {
16  {
17  Message = "The credentials provided do not have sufficient rights to make this request!",
18  SeverApiVersion = null
19  }, HttpStatusCode.Forbidden)
20  { }
21 
26  public InsufficientPermissionsException(string message) : base(message) { }
27 
33  public InsufficientPermissionsException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
- +Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
11  {
16  {
17  Message = "The credentials provided do not have sufficient rights to make this request!",
18  SeverApiVersion = null
19  }, HttpStatusCode.Forbidden)
20  { }
21 
26  public InsufficientPermissionsException(string message) : base(message) { }
27 
33  public InsufficientPermissionsException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
diff --git a/_irc_provider_8cs_source.html b/_irc_provider_8cs_source.html index 96faeb92a2..217b241e07 100644 --- a/_irc_provider_8cs_source.html +++ b/_irc_provider_8cs_source.html @@ -92,8 +92,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;
12 
13 namespace Tgstation.Server.Host.Components.Chat.Providers
14 {
18  sealed class IrcProvider : Provider
19  {
20  const int TimeoutSeconds = 5;
21 
23  public override bool Connected => client.IsConnected;
24 
26  public override string BotMention => client.Nickname;
27 
32 
36  readonly ILogger<IrcProvider> logger;
37 
41  readonly IrcFeatures client;
42 
46  readonly string address;
50  readonly ushort port;
54  readonly string nickname;
58  readonly string password;
63 
67  readonly Dictionary<ulong, string> channelIdMap;
68 
72  readonly Dictionary<ulong, string> queryChannelIdMap;
73 
78 
82  Task listenTask;
83 
88 
101  public IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger<IrcProvider> logger, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl)
102  {
103  if (application == null)
104  throw new ArgumentNullException(nameof(application));
105  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
106  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
107 
108  this.address = address ?? throw new ArgumentNullException(nameof(address));
109  this.port = port;
110  this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
111 
112  if (passwordType.HasValue && password == null)
113  throw new ArgumentNullException(nameof(password));
114 
115  if (password != null && !passwordType.HasValue)
116  throw new ArgumentNullException(nameof(passwordType));
117 
118  this.password = password;
119  this.passwordType = passwordType;
120 
121  client = new IrcFeatures
122  {
123  SupportNonRfc = true,
124  CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
125  AutoRejoin = true,
126  AutoRejoinOnKick = true,
127  AutoRelogin = true,
128  AutoRetry = true,
129  AutoRetryLimit = TimeoutSeconds,
130  AutoRetryDelay = TimeoutSeconds,
131  ActiveChannelSyncing = true,
132  AutoNickHandling = true,
133  CtcpVersion = application.VersionString,
134  UseSsl = useSsl
135  };
136  if (useSsl)
137  client.ValidateServerCertificate = true; //dunno if it defaults to that or what
138 
139  client.OnChannelMessage += Client_OnChannelMessage;
140  client.OnQueryMessage += Client_OnQueryMessage;
141 
142  channelIdMap = new Dictionary<ulong, string>();
143  queryChannelIdMap = new Dictionary<ulong, string>();
144  channelIdCounter = 1;
145  disconnecting = false;
146  }
147 
149  public override void Dispose()
150  {
151  if (Connected)
152  {
153  disconnecting = true;
154  client.Disconnect(); //just closes the socket
155  }
156  }
157 
163  void HandleMessage(IrcEventArgs e, bool isPrivate)
164  {
165  if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
166  return;
167 
168  var username = e.Data.Nick;
169  var channelName = isPrivate ? username : e.Data.Channel;
170 
171  ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
172  {
173  ulong? resultId = null;
174  if (!dicToCheck.Any(x =>
175  {
176  if (x.Value != channelName)
177  return false;
178  resultId = x.Key;
179  return true;
180  }))
181  {
182  resultId = channelIdCounter++;
183  dicToCheck.Add(resultId.Value, channelName);
184  if (dicToCheck == queryChannelIdMap)
185  channelIdMap.Add(resultId.Value, null);
186  }
187  return resultId.Value;
188  };
189 
190  ulong userId, channelId;
191  lock (this)
192  {
193  userId = MapAndGetChannelId(queryChannelIdMap);
194  channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
195  }
196 
197  var message = new Message
198  {
199  Content = e.Data.Message,
200  User = new User
201  {
202  Channel = new Channel
203  {
204  ConnectionName = address,
205  FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
206  RealId = channelId,
207  IsPrivateChannel = isPrivate
208  //isAdmin and Tag populated by manager
209  },
210  FriendlyName = username,
211  RealId = userId,
212  Mention = username
213  }
214  };
215 
216  EnqueueMessage(message);
217  }
218 
224  void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
225 
231  void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
232 
234  public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
235  {
236  disconnecting = false;
237  lock (this)
238  try
239  {
240  client.Connect(address, port);
241 
242  cancellationToken.ThrowIfCancellationRequested();
243 
244  if (passwordType == IrcPasswordType.Server)
245  client.Login(nickname, nickname, 0, nickname, password);
246  else
247  {
248  if (passwordType == IrcPasswordType.Sasl)
249  {
250  client.WriteLine("CAP REQ :sasl", Priority.Critical); //needs to be put in the buffer before anything else
251  cancellationToken.ThrowIfCancellationRequested();
252  }
253  client.Login(nickname, nickname, 0, nickname);
254  }
255 
256  if (passwordType == IrcPasswordType.NickServ)
257  {
258  cancellationToken.ThrowIfCancellationRequested();
259  client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
260  }
261  else if (passwordType == IrcPasswordType.Sasl)
262  {
263  //wait for the sasl ack or timeout
264  var recievedAck = false;
265  var recievedPlus = false;
266  client.OnReadLine += (sender, e) =>
267  {
268  if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal))
269  recievedAck = true;
270  else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal))
271  recievedPlus = true;
272  };
273 
274  var startTime = DateTimeOffset.Now;
275  var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
276  cancellationToken.ThrowIfCancellationRequested();
277 
278  var listenTimeSpan = TimeSpan.FromMilliseconds(10);
279  for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
280  client.Listen(false);
281 
282  client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
283  cancellationToken.ThrowIfCancellationRequested();
284 
285  for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
286  client.Listen(false);
287 
288  //Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
289  var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password);
290  var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
291  var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64);
292  var chars = authLine.ToCharArray();
293  client.WriteLine(authLine, Priority.Critical);
294 
295  cancellationToken.ThrowIfCancellationRequested();
296  client.WriteLine("CAP END", Priority.Critical);
297  }
298 
299  client.Listen(false);
300 
301  listenTask = Task.Factory.StartNew(() =>
302  {
303  while (!disconnecting && client.IsConnected && client.Nickname != nickname)
304  {
305  client.ListenOnce(true);
306  if (disconnecting || !client.IsConnected)
307  break;
308  client.Listen(false);
309  //ensure we have the correct nick
310  if (client.GetIrcUser(nickname) == null)
311  client.RfcNick(nickname);
312  }
313  client.Listen();
314  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
315  }
316  catch (OperationCanceledException)
317  {
318  throw;
319  }
320  catch (Exception e)
321  {
322  logger.LogWarning("Unable to connect to IRC: {0}", e);
323  return false;
324  }
325  return true;
326  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
327 
329  public override async Task Disconnect(CancellationToken cancellationToken)
330  {
331  if (!Connected)
332  return;
333  try
334  {
335  await Task.Factory.StartNew(() =>
336  {
337  try
338  {
339  client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise it wont go through
340  }
341  catch (Exception e)
342  {
343  logger.LogWarning("Error quitting IRC: {0}", e);
344  }
345  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
346  Dispose();
347  await listenTask.ConfigureAwait(false);
348  }
349  catch (OperationCanceledException)
350  {
351  throw;
352  }
353  catch (Exception e)
354  {
355  logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
356  }
357  }
358 
360  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
361  {
362  if (channels.Any(x => x.IrcChannel == null))
363  throw new InvalidOperationException("ChatChannel missing IrcChannel!");
364  lock (this)
365  {
366  var hs = new HashSet<string>(); //for unique inserts
367  foreach (var I in channels)
368  hs.Add(I.IrcChannel);
369  var toPart = new List<string>();
370  foreach (var I in client.JoinedChannels)
371  if (!hs.Remove(I))
372  toPart.Add(I);
373 
374  foreach (var I in toPart)
375  client.RfcPart(I, "Pretty nice abscond!");
376  foreach (var I in hs)
377  client.RfcJoin(I);
378 
379  return (IReadOnlyList<Channel>)channels.Select(x =>
380  {
381  ulong? id = null;
382  if (!channelIdMap.Any(y =>
383  {
384  if (y.Value != x.IrcChannel)
385  return false;
386  id = y.Key;
387  return true;
388  }))
389  {
390  id = channelIdCounter++;
391  channelIdMap.Add(id.Value, x.IrcChannel);
392  }
393  return new Channel
394  {
395  RealId = id.Value,
396  IsAdminChannel = x.IsAdminChannel == true,
397  ConnectionName = address,
398  FriendlyName = channelIdMap[id.Value],
399  IsPrivateChannel = false,
400  Tag = x.Tag
401  };
402  }).ToList();
403  }
404  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
405 
407  public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
408  {
409  var channelName = channelIdMap[channelId];
410  SendType sendType;
411  if (channelName == null)
412  {
413  channelName = queryChannelIdMap[channelId];
414  sendType = SendType.Notice;
415  }
416  else
417  sendType = SendType.Message;
418  try
419  {
420  client.SendMessage(sendType, channelName, message);
421  }
422  catch (Exception e)
423  {
424  logger.LogWarning("Unable to send to channel: {0}", e);
425  }
426  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
427  }
428 }
-
ulong channelIdCounter
Id counter for channelIdMap
Definition: IrcProvider.cs:77
+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;
12 
13 namespace Tgstation.Server.Host.Components.Chat.Providers
14 {
18  sealed class IrcProvider : Provider
19  {
20  const int TimeoutSeconds = 5;
21 
23  public override bool Connected => client.IsConnected;
24 
26  public override string BotMention => client.Nickname;
27 
32 
36  readonly ILogger<IrcProvider> logger;
37 
41  readonly IrcFeatures client;
42 
46  readonly string address;
50  readonly ushort port;
54  readonly string nickname;
58  readonly string password;
63 
67  readonly Dictionary<ulong, string> channelIdMap;
68 
72  readonly Dictionary<ulong, string> queryChannelIdMap;
73 
78 
82  Task listenTask;
83 
88 
101  public IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger<IrcProvider> logger, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl)
102  {
103  if (application == null)
104  throw new ArgumentNullException(nameof(application));
105  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
106  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
107 
108  this.address = address ?? throw new ArgumentNullException(nameof(address));
109  this.port = port;
110  this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
111 
112  if (passwordType.HasValue && password == null)
113  throw new ArgumentNullException(nameof(password));
114 
115  if (password != null && !passwordType.HasValue)
116  throw new ArgumentNullException(nameof(passwordType));
117 
118  this.password = password;
119  this.passwordType = passwordType;
120 
121  client = new IrcFeatures
122  {
123  SupportNonRfc = true,
124  CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
125  AutoRejoin = true,
126  AutoRejoinOnKick = true,
127  AutoRelogin = true,
128  AutoRetry = true,
129  AutoRetryLimit = TimeoutSeconds,
130  AutoRetryDelay = TimeoutSeconds,
131  ActiveChannelSyncing = true,
132  AutoNickHandling = true,
133  CtcpVersion = application.VersionString,
134  UseSsl = useSsl
135  };
136  if (useSsl)
137  client.ValidateServerCertificate = true; //dunno if it defaults to that or what
138 
139  client.OnChannelMessage += Client_OnChannelMessage;
140  client.OnQueryMessage += Client_OnQueryMessage;
141 
142  channelIdMap = new Dictionary<ulong, string>();
143  queryChannelIdMap = new Dictionary<ulong, string>();
144  channelIdCounter = 1;
145  disconnecting = false;
146  }
147 
149  public override void Dispose()
150  {
151  if (Connected)
152  {
153  disconnecting = true;
154  client.Disconnect(); //just closes the socket
155  }
156  }
157 
163  void HandleMessage(IrcEventArgs e, bool isPrivate)
164  {
165  if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
166  return;
167 
168  var username = e.Data.Nick;
169  var channelName = isPrivate ? username : e.Data.Channel;
170 
171  ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
172  {
173  ulong? resultId = null;
174  if (!dicToCheck.Any(x =>
175  {
176  if (x.Value != channelName)
177  return false;
178  resultId = x.Key;
179  return true;
180  }))
181  {
182  resultId = channelIdCounter++;
183  dicToCheck.Add(resultId.Value, channelName);
184  if (dicToCheck == queryChannelIdMap)
185  channelIdMap.Add(resultId.Value, null);
186  }
187  return resultId.Value;
188  };
189 
190  ulong userId, channelId;
191  lock (this)
192  {
193  userId = MapAndGetChannelId(queryChannelIdMap);
194  channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
195  }
196 
197  var message = new Message
198  {
199  Content = e.Data.Message,
200  User = new User
201  {
202  Channel = new Channel
203  {
204  ConnectionName = address,
205  FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
206  RealId = channelId,
207  IsPrivateChannel = isPrivate
208  //isAdmin and Tag populated by manager
209  },
210  FriendlyName = username,
211  RealId = userId,
212  Mention = username
213  }
214  };
215 
216  EnqueueMessage(message);
217  }
218 
224  void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
225 
231  void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
232 
234  public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
235  {
236  disconnecting = false;
237  lock (this)
238  try
239  {
240  client.Connect(address, port);
241 
242  cancellationToken.ThrowIfCancellationRequested();
243 
244  if (passwordType == IrcPasswordType.Server)
245  client.Login(nickname, nickname, 0, nickname, password);
246  else
247  {
248  if (passwordType == IrcPasswordType.Sasl)
249  {
250  client.WriteLine("CAP REQ :sasl", Priority.Critical); //needs to be put in the buffer before anything else
251  cancellationToken.ThrowIfCancellationRequested();
252  }
253  client.Login(nickname, nickname, 0, nickname);
254  }
255 
256  if (passwordType == IrcPasswordType.NickServ)
257  {
258  cancellationToken.ThrowIfCancellationRequested();
259  client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
260  }
261  else if (passwordType == IrcPasswordType.Sasl)
262  {
263  //wait for the sasl ack or timeout
264  var recievedAck = false;
265  var recievedPlus = false;
266  client.OnReadLine += (sender, e) =>
267  {
268  if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal))
269  recievedAck = true;
270  else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal))
271  recievedPlus = true;
272  };
273 
274  var startTime = DateTimeOffset.Now;
275  var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
276  cancellationToken.ThrowIfCancellationRequested();
277 
278  var listenTimeSpan = TimeSpan.FromMilliseconds(10);
279  for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
280  client.Listen(false);
281 
282  client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
283  cancellationToken.ThrowIfCancellationRequested();
284 
285  for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
286  client.Listen(false);
287 
288  //Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
289  var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password);
290  var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
291  var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64);
292  var chars = authLine.ToCharArray();
293  client.WriteLine(authLine, Priority.Critical);
294 
295  cancellationToken.ThrowIfCancellationRequested();
296  client.WriteLine("CAP END", Priority.Critical);
297  }
298 
299  client.Listen(false);
300 
301  listenTask = Task.Factory.StartNew(() =>
302  {
303  while (!disconnecting && client.IsConnected && client.Nickname != nickname)
304  {
305  client.ListenOnce(true);
306  if (disconnecting || !client.IsConnected)
307  break;
308  client.Listen(false);
309  //ensure we have the correct nick
310  if (client.GetIrcUser(nickname) == null)
311  client.RfcNick(nickname);
312  }
313  client.Listen();
314  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
315  }
316  catch (OperationCanceledException)
317  {
318  throw;
319  }
320  catch (Exception e)
321  {
322  logger.LogWarning("Unable to connect to IRC: {0}", e);
323  return false;
324  }
325  return true;
326  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
327 
329  public override async Task Disconnect(CancellationToken cancellationToken)
330  {
331  if (!Connected)
332  return;
333  try
334  {
335  await Task.Factory.StartNew(() =>
336  {
337  try
338  {
339  client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise it wont go through
340  }
341  catch (Exception e)
342  {
343  logger.LogWarning("Error quitting IRC: {0}", e);
344  }
345  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
346  Dispose();
347  await listenTask.ConfigureAwait(false);
348  }
349  catch (OperationCanceledException)
350  {
351  throw;
352  }
353  catch (Exception e)
354  {
355  logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
356  }
357  }
358 
360  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
361  {
362  if (channels.Any(x => x.IrcChannel == null))
363  throw new InvalidOperationException("ChatChannel missing IrcChannel!");
364  lock (this)
365  {
366  var hs = new HashSet<string>(); //for unique inserts
367  foreach (var I in channels)
368  hs.Add(I.IrcChannel);
369  var toPart = new List<string>();
370  foreach (var I in client.JoinedChannels)
371  if (!hs.Remove(I))
372  toPart.Add(I);
373 
374  foreach (var I in toPart)
375  client.RfcPart(I, "Pretty nice abscond!");
376  foreach (var I in hs)
377  client.RfcJoin(I);
378 
379  return (IReadOnlyList<Channel>)channels.Select(x =>
380  {
381  ulong? id = null;
382  if (!channelIdMap.Any(y =>
383  {
384  if (y.Value != x.IrcChannel)
385  return false;
386  id = y.Key;
387  return true;
388  }))
389  {
390  id = channelIdCounter++;
391  channelIdMap.Add(id.Value, x.IrcChannel);
392  }
393  return new Channel
394  {
395  RealId = id.Value,
396  IsAdminChannel = x.IsAdminChannel == true,
397  ConnectionName = address,
398  FriendlyName = channelIdMap[id.Value],
399  IsPrivateChannel = false,
400  Tag = x.Tag
401  };
402  }).ToList();
403  }
404  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
405 
407  public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
408  {
409  var channelName = channelIdMap[channelId];
410  SendType sendType;
411  if (channelName == null)
412  {
413  channelName = queryChannelIdMap[channelId];
414  sendType = SendType.Notice;
415  }
416  else
417  sendType = SendType.Message;
418  try
419  {
420  client.SendMessage(sendType, channelName, message);
421  }
422  catch (Exception e)
423  {
424  logger.LogWarning("Unable to send to channel: {0}", e);
425  }
426  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
427  }
428 }
ulong channelIdCounter
Id counter for channelIdMap
Definition: IrcProvider.cs:77
readonly Dictionary< ulong, string > queryChannelIdMap
Map of Channel.RealIds to query users
Definition: IrcProvider.cs:72
readonly IrcFeatures client
The IrcFeatures client
Definition: IrcProvider.cs:41
diff --git a/_job_exception_8cs_source.html b/_job_exception_8cs_source.html index 78eb15abf4..2d00b79d42 100644 --- a/_job_exception_8cs_source.html +++ b/_job_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JobException.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Host.Core
4 {
8  public sealed class JobException : Exception
9  {
13  public JobException()
14  {
15  }
16 
21  public JobException(string message) : base(message)
22  {
23  }
24 
30  public JobException(string message, Exception innerException) : base(message, innerException)
31  {
32  }
33  }
34 }
-
JobException()
Construct a JobException
Definition: JobException.cs:13
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Host.Core
4 {
8  public sealed class JobException : Exception
9  {
13  public JobException()
14  {
15  }
16 
21  public JobException(string message) : base(message)
22  {
23  }
24 
30  public JobException(string message, Exception innerException) : base(message, innerException)
31  {
32  }
33  }
34 }
JobException()
Construct a JobException
Definition: JobException.cs:13
JobException(string message, Exception innerException)
Construct a JobException with a message and innerException
Definition: JobException.cs:30
diff --git a/_job_manager_8cs_source.html b/_job_manager_8cs_source.html index 8a8a73a05e..77c862a904 100644 --- a/_job_manager_8cs_source.html +++ b/_job_manager_8cs_source.html @@ -92,8 +92,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;
9 
10 namespace Tgstation.Server.Host.Core
11 {
14  {
19 
23  readonly ILogger<JobManager> logger;
24 
28  readonly Dictionary<long, JobHandler> jobs;
29 
35  public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger<JobManager> logger)
36  {
37  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
38  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
39  jobs = new Dictionary<long, JobHandler>();
40  }
41 
43  public void Dispose()
44  {
45  foreach (var job in jobs)
46  job.Value.Dispose();
47  }
48 
55  {
56  lock (this)
57  {
58  if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler))
59  throw new InvalidOperationException("Job not running!");
60  return jobHandler;
61  }
62  }
63 
71  async Task RunJob(Job job, Func<Job, IDatabaseContext, CancellationToken, Task> operation, CancellationToken cancellationToken)
72  {
73  try
74  {
75  await databaseContextFactory.UseContext(async databaseContext =>
76  {
77  async Task HandleExceptions(Task task)
78  {
79  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
80  try
81  {
82  await task.ConfigureAwait(false);
83  }
84  catch (OperationCanceledException)
85  {
86  logger.LogDebug("Job {0} cancelled!", job.Id);
87  job.Cancelled = true;
88  }
89  catch (JobException e)
90  {
91  job.ExceptionDetails = e.Message;
92  LogRegularException();
93  if (e.InnerException != null)
94  logger.LogDebug("Inner exception for job {0}: {1}", job.Id, e.InnerException);
95  }
96  catch (Exception e)
97  {
98  job.ExceptionDetails = e.ToString();
99  LogRegularException();
100  }
101  finally
102  {
103  job.StoppedAt = DateTimeOffset.Now;
104  }
105  }
106 
107  async Task RunJobInternal()
108  {
109  var oldJob = job;
110  job = new Job { Id = oldJob.Id };
111  databaseContext.Jobs.Attach(job);
112 
113  await operation(job, databaseContext, cancellationToken).ConfigureAwait(false);
114 
115  logger.LogDebug("Job {0} completed!", job.Id);
116  };
117 
118  await HandleExceptions(RunJobInternal()).ConfigureAwait(false);
119 
120  await databaseContext.Save(default).ConfigureAwait(false);
121 
122  bool JobErroredOrCancelled() => job.ExceptionDetails != null || job.Cancelled == true;
123 
124  //ok so, now it's time for the post commit step if it exists
125  if (!JobErroredOrCancelled() && job.PostComplete != null)
126  {
127  await HandleExceptions(job.PostComplete(cancellationToken)).ConfigureAwait(false);
128  if (JobErroredOrCancelled())
129  await databaseContext.Save(default).ConfigureAwait(false);
130  }
131  }).ConfigureAwait(false);
132  }
133  finally
134  {
135  lock (this)
136  {
137  var handler = jobs[job.Id];
138  jobs.Remove(job.Id);
139  handler.Dispose();
140  }
141  }
142  }
143 
145  public Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
146  {
147  if (job == null)
148  throw new ArgumentNullException(nameof(job));
149  if (operation == null)
150  throw new ArgumentNullException(nameof(operation));
151 
152  job.StartedAt = DateTimeOffset.Now;
153  job.Cancelled = false;
154 
155  job.Instance = new Instance
156  {
157  Id = job.Instance.Id
158  };
159  databaseContext.Instances.Attach(job.Instance);
160 
161  job.StartedBy = new User
162  {
163  Id = job.StartedBy.Id
164  };
165  databaseContext.Users.Attach(job.StartedBy);
166 
167  databaseContext.Jobs.Add(job);
168 
169  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
170  logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
171  var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
172  operation(jobParam, serviceProvider, y =>
173  {
174  lock (this)
175  if (jobs.TryGetValue(job.Id, out var handler))
176  handler.Progress = y;
177  }, ct),
178  x));
179  lock (this)
180  jobs.Add(job.Id, jobHandler);
181  });
182 
184  public async Task StartAsync(CancellationToken cancellationToken)
185  {
186  logger.LogTrace("Starting job manager...");
187  await databaseContextFactory.UseContext(async databaseContext =>
188  {
189  //mark all jobs as cancelled
190  var badJobs = await databaseContext.Jobs.Where(y => !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false);
191  if (badJobs.Count > 0)
192  {
193  logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
194  foreach (var I in badJobs)
195  {
196  var job = new Job { Id = I };
197  databaseContext.Jobs.Attach(job);
198  job.Cancelled = true;
199  job.StoppedAt = DateTimeOffset.Now;
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<bool> 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 false;
234  }
235  handler.Cancel(); //this will ensure the db update is only done once
236  await databaseContextFactory.UseContext(async databaseContext =>
237  {
238  job = new Job { Id = job.Id };
239  databaseContext.Jobs.Attach(job);
240  user = new User { Id = user.Id };
241  databaseContext.Users.Attach(user);
242  job.CancelledBy = user;
243  //let either startup or cancellation set job.cancelled
244  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
245  }).ConfigureAwait(false);
246  if (blocking)
247  await handler.Wait(cancellationToken).ConfigureAwait(false);
248  return true;
249  }
250 
252  public int? JobProgress(Job job)
253  {
254  if (job == null)
255  throw new ArgumentNullException(nameof(job));
256  lock (this)
257  {
258  if (!jobs.TryGetValue(job.Id, out var handler))
259  return null;
260  return handler.Progress;
261  }
262  }
263 
265  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
266  {
267  if (job == null)
268  throw new ArgumentNullException(nameof(job));
269  if (canceller == null)
270  throw new ArgumentNullException(nameof(canceller));
271  JobHandler handler;
272  lock (this)
273  {
274  if (!jobs.TryGetValue(job.Id, out handler))
275  return;
276  }
277  Task cancelTask = null;
278  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
279  await handler.Wait(cancellationToken).ConfigureAwait(false);
280 
281  if (cancelTask != null)
282  await cancelTask.ConfigureAwait(false);
283  }
284  }
285 }
- +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;
9 
10 namespace Tgstation.Server.Host.Core
11 {
14  {
19 
23  readonly ILogger<JobManager> logger;
24 
28  readonly Dictionary<long, JobHandler> jobs;
29 
35  public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger<JobManager> logger)
36  {
37  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
38  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
39  jobs = new Dictionary<long, JobHandler>();
40  }
41 
43  public void Dispose()
44  {
45  foreach (var job in jobs)
46  job.Value.Dispose();
47  }
48 
55  {
56  lock (this)
57  {
58  if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler))
59  throw new InvalidOperationException("Job not running!");
60  return jobHandler;
61  }
62  }
63 
71  async Task RunJob(Job job, Func<Job, IDatabaseContext, CancellationToken, Task> operation, CancellationToken cancellationToken)
72  {
73  try
74  {
75  await databaseContextFactory.UseContext(async databaseContext =>
76  {
77  async Task HandleExceptions(Task task)
78  {
79  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
80  try
81  {
82  await task.ConfigureAwait(false);
83  }
84  catch (OperationCanceledException)
85  {
86  logger.LogDebug("Job {0} cancelled!", job.Id);
87  job.Cancelled = true;
88  }
89  catch (JobException e)
90  {
91  job.ExceptionDetails = e.Message;
92  LogRegularException();
93  if (e.InnerException != null)
94  logger.LogDebug("Inner exception for job {0}: {1}", job.Id, e.InnerException);
95  }
96  catch (Exception e)
97  {
98  job.ExceptionDetails = e.ToString();
99  LogRegularException();
100  }
101  finally
102  {
103  job.StoppedAt = DateTimeOffset.Now;
104  }
105  }
106 
107  async Task RunJobInternal()
108  {
109  var oldJob = job;
110  job = new Job { Id = oldJob.Id };
111  databaseContext.Jobs.Attach(job);
112 
113  await operation(job, databaseContext, cancellationToken).ConfigureAwait(false);
114 
115  logger.LogDebug("Job {0} completed!", job.Id);
116  };
117 
118  await HandleExceptions(RunJobInternal()).ConfigureAwait(false);
119 
120  await databaseContext.Save(default).ConfigureAwait(false);
121 
122  bool JobErroredOrCancelled() => job.ExceptionDetails != null || job.Cancelled == true;
123 
124  //ok so, now it's time for the post commit step if it exists
125  if (!JobErroredOrCancelled() && job.PostComplete != null)
126  {
127  await HandleExceptions(job.PostComplete(cancellationToken)).ConfigureAwait(false);
128  if (JobErroredOrCancelled())
129  await databaseContext.Save(default).ConfigureAwait(false);
130  }
131  }).ConfigureAwait(false);
132  }
133  finally
134  {
135  lock (this)
136  {
137  var handler = jobs[job.Id];
138  jobs.Remove(job.Id);
139  handler.Dispose();
140  }
141  }
142  }
143 
145  public Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
146  {
147  if (job == null)
148  throw new ArgumentNullException(nameof(job));
149  if (operation == null)
150  throw new ArgumentNullException(nameof(operation));
151 
152  job.StartedAt = DateTimeOffset.Now;
153  job.Cancelled = false;
154 
155  job.Instance = new Instance
156  {
157  Id = job.Instance.Id
158  };
159  databaseContext.Instances.Attach(job.Instance);
160 
161  job.StartedBy = new User
162  {
163  Id = job.StartedBy.Id
164  };
165  databaseContext.Users.Attach(job.StartedBy);
166 
167  databaseContext.Jobs.Add(job);
168 
169  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
170  logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
171  var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
172  operation(jobParam, serviceProvider, y =>
173  {
174  lock (this)
175  if (jobs.TryGetValue(job.Id, out var handler))
176  handler.Progress = y;
177  }, ct),
178  x));
179  lock (this)
180  jobs.Add(job.Id, jobHandler);
181  });
182 
184  public async Task StartAsync(CancellationToken cancellationToken)
185  {
186  logger.LogTrace("Starting job manager...");
187  await databaseContextFactory.UseContext(async databaseContext =>
188  {
189  //mark all jobs as cancelled
190  var badJobs = await databaseContext.Jobs.Where(y => !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false);
191  if (badJobs.Count > 0)
192  {
193  logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
194  foreach (var I in badJobs)
195  {
196  var job = new Job { Id = I };
197  databaseContext.Jobs.Attach(job);
198  job.Cancelled = true;
199  job.StoppedAt = DateTimeOffset.Now;
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<bool> 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 false;
234  }
235  handler.Cancel(); //this will ensure the db update is only done once
236  await databaseContextFactory.UseContext(async databaseContext =>
237  {
238  job = new Job { Id = job.Id };
239  databaseContext.Jobs.Attach(job);
240  user = new User { Id = user.Id };
241  databaseContext.Users.Attach(user);
242  job.CancelledBy = user;
243  //let either startup or cancellation set job.cancelled
244  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
245  }).ConfigureAwait(false);
246  if (blocking)
247  await handler.Wait(cancellationToken).ConfigureAwait(false);
248  return true;
249  }
250 
252  public int? JobProgress(Job job)
253  {
254  if (job == null)
255  throw new ArgumentNullException(nameof(job));
256  lock (this)
257  {
258  if (!jobs.TryGetValue(job.Id, out var handler))
259  return null;
260  return handler.Progress;
261  }
262  }
263 
265  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
266  {
267  if (job == null)
268  throw new ArgumentNullException(nameof(job));
269  if (canceller == null)
270  throw new ArgumentNullException(nameof(canceller));
271  JobHandler handler;
272  lock (this)
273  {
274  if (!jobs.TryGetValue(job.Id, out handler))
275  return;
276  }
277  Task cancelTask = null;
278  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
279  await handler.Wait(cancellationToken).ConfigureAwait(false);
280 
281  if (cancelTask != null)
282  await cancelTask.ConfigureAwait(false);
283  }
284  }
285 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
JobHandler CheckGetJob(Job job)
Gets the JobHandler for a given job if it exists
Definition: JobManager.cs:54
diff --git a/_json_tracking_context_8cs_source.html b/_json_tracking_context_8cs_source.html index 5095d88dce..41de001daf 100644 --- a/_json_tracking_context_8cs_source.html +++ b/_json_tracking_context_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JsonTrackingContext.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using Newtonsoft.Json.Serialization;
4 using System;
5 using System.Collections.Generic;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Chat
14 {
17  {
19  public bool Active
20  {
21  get => active;
22  set
23  {
24  active = true;
25  logger.LogDebug("Tracking {0}activated", !active ? "de" : String.Empty);
26  }
27  }
28 
31  readonly ILogger<JsonTrackingContext> logger;
32  readonly Action onDispose;
33 
34  readonly string commandsPath;
35  readonly string channelsPath;
36 
37  readonly SemaphoreSlim channelsSemaphore;
38 
39  bool active;
40 
41  public JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, ILogger<JsonTrackingContext> logger, Action onDispose, string commandsPath, string channelsPath)
42  {
43  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
44  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
45  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
46  this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
47  this.commandsPath = commandsPath ?? throw new ArgumentNullException(nameof(commandsPath));
48  this.channelsPath = channelsPath ?? throw new ArgumentNullException(nameof(channelsPath));
49 
50  channelsSemaphore = new SemaphoreSlim(1);
51  active = false;
52 
53  logger.LogTrace("Created tracking context for {0} and {1}", commandsPath, channelsPath);
54  }
55 
57  public void Dispose()
58  {
59  logger.LogTrace("Disposing...");
60  onDispose();
61  }
62 
64  public async Task<IReadOnlyList<CustomCommand>> GetCustomCommands(CancellationToken cancellationToken)
65  {
66  try
67  {
68  if (Active && await ioManager.FileExists(commandsPath, cancellationToken).ConfigureAwait(false))
69  {
70  var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false);
71  var resultJson = Encoding.UTF8.GetString(resultBytes);
72  logger.LogTrace("Read commands JSON: {0}", resultJson);
73  var result = JsonConvert.DeserializeObject<List<CustomCommand>>(resultJson, new JsonSerializerSettings
74  {
75  ContractResolver = new DefaultContractResolver
76  {
77  NamingStrategy = new SnakeCaseNamingStrategy()
78  }
79  });
80  foreach (var I in result)
81  I.SetHandler(customCommandHandler);
82  return result;
83  }
84  }
85  catch (OperationCanceledException)
86  {
87  throw;
88  }
89  catch (Exception e)
90  {
91  logger.LogWarning("Error retrieving custom commands! Exception: {0}", e);
92  }
93  return new List<CustomCommand>();
94  }
95 
97  public async Task SetChannels(IEnumerable<Channel> channels, CancellationToken cancellationToken)
98  {
99  using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false))
100  {
101  var json = JsonConvert.SerializeObject(channels, new JsonSerializerSettings
102  {
103  ContractResolver = new CamelCasePropertyNamesContractResolver()
104  });
105  logger.LogTrace("Writing channels JSON: {0}", json);
106  await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false);
107  }
108  }
109  }
110 }
- +Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using Newtonsoft.Json.Serialization;
4 using System;
5 using System.Collections.Generic;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Chat
14 {
17  {
19  public bool Active
20  {
21  get => active;
22  set
23  {
24  active = true;
25  logger.LogDebug("Tracking {0}activated", !active ? "de" : String.Empty);
26  }
27  }
28 
31  readonly ILogger<JsonTrackingContext> logger;
32  readonly Action onDispose;
33 
34  readonly string commandsPath;
35  readonly string channelsPath;
36 
37  readonly SemaphoreSlim channelsSemaphore;
38 
39  bool active;
40 
41  public JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, ILogger<JsonTrackingContext> logger, Action onDispose, string commandsPath, string channelsPath)
42  {
43  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
44  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
45  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
46  this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
47  this.commandsPath = commandsPath ?? throw new ArgumentNullException(nameof(commandsPath));
48  this.channelsPath = channelsPath ?? throw new ArgumentNullException(nameof(channelsPath));
49 
50  channelsSemaphore = new SemaphoreSlim(1);
51  active = false;
52 
53  logger.LogTrace("Created tracking context for {0} and {1}", commandsPath, channelsPath);
54  }
55 
57  public void Dispose()
58  {
59  logger.LogTrace("Disposing...");
60  onDispose();
61  }
62 
64  public async Task<IReadOnlyList<CustomCommand>> GetCustomCommands(CancellationToken cancellationToken)
65  {
66  try
67  {
68  if (Active && await ioManager.FileExists(commandsPath, cancellationToken).ConfigureAwait(false))
69  {
70  var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false);
71  var resultJson = Encoding.UTF8.GetString(resultBytes);
72  logger.LogTrace("Read commands JSON: {0}", resultJson);
73  var result = JsonConvert.DeserializeObject<List<CustomCommand>>(resultJson, new JsonSerializerSettings
74  {
75  ContractResolver = new DefaultContractResolver
76  {
77  NamingStrategy = new SnakeCaseNamingStrategy()
78  }
79  });
80  foreach (var I in result)
81  I.SetHandler(customCommandHandler);
82  return result;
83  }
84  }
85  catch (OperationCanceledException)
86  {
87  throw;
88  }
89  catch (Exception e)
90  {
91  logger.LogWarning("Error retrieving custom commands! Exception: {0}", e);
92  }
93  return new List<CustomCommand>();
94  }
95 
97  public async Task SetChannels(IEnumerable<Channel> channels, CancellationToken cancellationToken)
98  {
99  using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false))
100  {
101  var json = JsonConvert.SerializeObject(channels, new JsonSerializerSettings
102  {
103  ContractResolver = new CamelCasePropertyNamesContractResolver()
104  });
105  logger.LogTrace("Writing channels JSON: {0}", json);
106  await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false);
107  }
108  }
109  }
110 }
async Task SetChannels(IEnumerable< Channel > channels, CancellationToken cancellationToken)
Writes information about connected channels to the IJsonTrackingContext
diff --git a/_method_not_supported_exception_8cs_source.html b/_method_not_supported_exception_8cs_source.html index 47e5e5d7b8..3b2bfe8ad2 100644 --- a/_method_not_supported_exception_8cs_source.html +++ b/_method_not_supported_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
MethodNotSupportedException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
11  {
16  {
17  Message = "This method is not currently supported!",
18  SeverApiVersion = null
19  }, HttpStatusCode.NotImplemented)
20  { }
21 
26  public MethodNotSupportedException(string message) : base(message) { }
27 
33  public MethodNotSupportedException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
-
MethodNotSupportedException()
Construct an MethodNotSupportedException
+Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
11  {
16  {
17  Message = "This method is not currently supported!",
18  SeverApiVersion = null
19  }, HttpStatusCode.NotImplemented)
20  { }
21 
26  public MethodNotSupportedException(string message) : base(message) { }
27 
33  public MethodNotSupportedException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
MethodNotSupportedException()
Construct an MethodNotSupportedException
diff --git a/_model_controller_8cs_source.html b/_model_controller_8cs_source.html index e925a3364f..2f5dd69573 100644 --- a/_model_controller_8cs_source.html +++ b/_model_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ModelController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System.Threading;
4 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Controllers
9 {
14  public abstract class ModelController<TModel> : ApiController where TModel : class
15  {
23  public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance) : base(databaseContext, authenticationContextFactory, logger, requireInstance) { }
24 
31  [HttpPut]
32  public virtual Task<IActionResult> Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
33 
39  [HttpGet]
40  public virtual Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
41 
48  [HttpGet("{id}")]
49  public virtual Task<IActionResult> GetId(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
50 
57  [HttpPost]
58  public virtual Task<IActionResult> Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
59 
66  [HttpDelete("{id}")]
67  public virtual Task<IActionResult> Delete(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
68 
74  [HttpGet("List")]
75  public virtual Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
76  }
77 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System.Threading;
4 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Controllers
9 {
14  public abstract class ModelController<TModel> : ApiController where TModel : class
15  {
23  public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance) : base(databaseContext, authenticationContextFactory, logger, requireInstance, true) { }
24 
31  [HttpPut]
32  public virtual Task<IActionResult> Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
33 
39  [HttpGet]
40  public virtual Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
41 
48  [HttpGet("{id}")]
49  public virtual Task<IActionResult> GetId(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
50 
57  [HttpPost]
58  public virtual Task<IActionResult> Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
59 
66  [HttpDelete("{id}")]
67  public virtual Task<IActionResult> Delete(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
68 
74  [HttpGet("List")]
75  public virtual Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
76  }
77 }
ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
Construct a ModelController<TModel>
diff --git a/_my_sql_database_context_8cs_source.html b/_my_sql_database_context_8cs_source.html index 60a3afdf66..d83d752bdc 100644 --- a/_my_sql_database_context_8cs_source.html +++ b/_my_sql_database_context_8cs_source.html @@ -99,7 +99,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For initially seeding a database
DatabaseType DatabaseType
The Configuration.DatabaseType to create
- +
string MySqlServerVersion
The string form of the System.Version of a target MySQL/MariaDB server
diff --git a/_posix_byond_installer_8cs_source.html b/_posix_byond_installer_8cs_source.html index 680642f3b5..1f853c918c 100644 --- a/_posix_byond_installer_8cs_source.html +++ b/_posix_byond_installer_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
PosixByondInstaller.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
4 using System.Net;
5 using System.Text;
6 using System.Threading;
7 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
10 
11 namespace Tgstation.Server.Host.Components.Byond
12 {
17  {
21  const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
25  const string ByondCachePath = "~/.byond/cache";
26 
27  const string DreamDaemonExecutableName = "DreamDaemon";
28  const string DreamMakerExecutableName = "DreamMaker";
29  const string ShellScriptExtension = ".sh";
30 
32  public string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension;
33 
35  public string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
36 
41 
46 
50  readonly ILogger<PosixByondInstaller> logger;
51 
58  public PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger<PosixByondInstaller> logger)
59  {
60  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
61  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
62  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
63  }
64 
66  public async Task CleanCache(CancellationToken cancellationToken)
67  {
68  try
69  {
70  await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
71  }
72  catch (OperationCanceledException)
73  {
74  throw;
75  }
76  catch (Exception e)
77  {
78  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
79  }
80  }
81 
83  public async Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
84  {
85  if (version == null)
86  throw new ArgumentNullException(nameof(version));
87 
88  var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor);
89 
90  return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false);
91  }
92 
94  public Task InstallByond(string path, Version version, CancellationToken cancellationToken)
95  {
96  if (path == null)
97  throw new ArgumentNullException(nameof(path));
98  if (version == null)
99  throw new ArgumentNullException(nameof(version));
100 
101  //write the scripts for running the ting
102  //need to add $ORIGIN to LD_LIBRARY_PATH
103  const string StandardScript = "#!/bin/sh\nexport LD_LIBRARY_PATH=\"\\$ORIGIN:$LD_LIBRARY_PATH\"\nBASEDIR=$(dirname \"$0\")\nexec \"$BASEDIR/{0}\" \"$@\"\n";
104 
105  var dreamDaemonScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamDaemonExecutableName);
106  var dreamMakerScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamMakerExecutableName);
107 
108  async Task WriteAndMakeExecutable(string fullPath, string script)
109  {
110  await ioManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false);
111  postWriteHandler.HandleWrite(fullPath);
112  }
113 
114  var basePath = ioManager.ConcatPath(path, ByondManager.BinPath);
115 
116  var task = Task.WhenAll(WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript));
117 
118  postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamDaemonExecutableName));
119  postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamMakerExecutableName));
120 
121  return task;
122  }
123  }
124 }
-
PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger< PosixByondInstaller > logger)
Construct a WindowsByondInstaller
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
4 using System.Net;
5 using System.Text;
6 using System.Threading;
7 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
10 
11 namespace Tgstation.Server.Host.Components.Byond
12 {
17  {
21  const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
25  const string ByondCachePath = "~/.byond/cache";
26 
27  const string DreamDaemonExecutableName = "DreamDaemon";
28  const string DreamMakerExecutableName = "DreamMaker";
29  const string ShellScriptExtension = ".sh";
30 
32  public string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension;
33 
35  public string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
36 
41 
46 
50  readonly ILogger<PosixByondInstaller> logger;
51 
58  public PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger<PosixByondInstaller> logger)
59  {
60  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
61  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
62  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
63  }
64 
66  public async Task CleanCache(CancellationToken cancellationToken)
67  {
68  try
69  {
70  await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
71  }
72  catch (OperationCanceledException)
73  {
74  throw;
75  }
76  catch (Exception e)
77  {
78  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
79  }
80  }
81 
83  public async Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
84  {
85  if (version == null)
86  throw new ArgumentNullException(nameof(version));
87 
88  var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor);
89 
90  return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false);
91  }
92 
94  public Task InstallByond(string path, Version version, CancellationToken cancellationToken)
95  {
96  if (path == null)
97  throw new ArgumentNullException(nameof(path));
98  if (version == null)
99  throw new ArgumentNullException(nameof(version));
100 
101  //write the scripts for running the ting
102  //need to add $ORIGIN to LD_LIBRARY_PATH
103  const string StandardScript = "#!/bin/sh\nexport LD_LIBRARY_PATH=\"\\$ORIGIN:$LD_LIBRARY_PATH\"\nBASEDIR=$(dirname \"$0\")\nexec \"$BASEDIR/{0}\" \"$@\"\n";
104 
105  var dreamDaemonScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamDaemonExecutableName);
106  var dreamMakerScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamMakerExecutableName);
107 
108  async Task WriteAndMakeExecutable(string fullPath, string script)
109  {
110  await ioManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false);
111  postWriteHandler.HandleWrite(fullPath);
112  }
113 
114  var basePath = ioManager.ConcatPath(path, ByondManager.BinPath);
115 
116  var task = Task.WhenAll(WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript));
117 
118  postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamDaemonExecutableName));
119  postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamMakerExecutableName));
120 
121  return task;
122  }
123  }
124 }
PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger< PosixByondInstaller > logger)
Construct a WindowsByondInstaller
diff --git a/_process_8cs_source.html b/_process_8cs_source.html index cf6ada84c2..5338cd3277 100644 --- a/_process_8cs_source.html +++ b/_process_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Process.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Text;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
9  sealed class Process : IProcess
10  {
12  public int Id { get; }
13 
15  public Task Startup { get; }
16 
18  public Task<int> Lifetime { get; }
19 
20  readonly System.Diagnostics.Process handle;
21 
22  readonly StringBuilder outputStringBuilder;
23  readonly StringBuilder errorStringBuilder;
24  readonly StringBuilder combinedStringBuilder;
25 
29  readonly ILogger<Process> logger;
30 
31  public Process(System.Diagnostics.Process handle, Task<int> lifetime, StringBuilder outputStringBuilder, StringBuilder errorStringBuilder, StringBuilder combinedStringBuilder, ILogger<Process> logger, bool preExisting)
32  {
33  this.handle = handle ?? throw new ArgumentNullException(nameof(handle));
34  Lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
35 
36  this.outputStringBuilder = outputStringBuilder;
37  this.errorStringBuilder = errorStringBuilder;
38  this.combinedStringBuilder = combinedStringBuilder;
39 
40  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
41 
42  Id = handle.Id;
43 
44  if (preExisting)
45  {
46  Startup = Task.CompletedTask;
47  return;
48  }
49 
50  Startup = Task.Factory.StartNew(() =>
51  {
52  try
53  {
54  handle.WaitForInputIdle();
55  }
56  catch (InvalidOperationException) { }
57  }, default, TaskCreationOptions.LongRunning, TaskScheduler.Current);
58 
59  logger.LogTrace("Created process ID: {0}", Id);
60  }
61 
63  public void Dispose() => handle.Dispose();
64 
66  public string GetCombinedOutput()
67  {
68  if (combinedStringBuilder == null)
69  throw new InvalidOperationException("Output/Error reading was not enabled!");
70  return combinedStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
71  }
72 
74  public string GetErrorOutput()
75  {
76  if (errorStringBuilder == null)
77  throw new InvalidOperationException("Error reading was not enabled!");
78  return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
79  }
80 
82  public string GetStandardOutput()
83  {
84  if (outputStringBuilder == null)
85  throw new InvalidOperationException("Output reading was not enabled!");
86  return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
87  }
88 
90  public void Terminate()
91  {
92  if (handle.HasExited)
93  return;
94  try
95  {
96  logger.LogTrace("Terminating process...");
97  handle.Kill();
98  handle.WaitForExit();
99  }
100  catch (Exception e)
101  {
102  logger.LogDebug("Process termination exception: {0}", e);
103  }
104  }
105 
106  public void SetHighPriority()
107  {
108  try
109  {
110  handle.PriorityClass = System.Diagnostics.ProcessPriorityClass.AboveNormal;
111  logger.LogTrace("Set to above normal priority", handle.Id);
112  }
113  catch (Exception e)
114  {
115  logger.LogWarning("Unable to raise process priority! Exception: {0}", e);
116  }
117  }
118  }
119 }
-
readonly StringBuilder errorStringBuilder
Definition: Process.cs:23
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Text;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
9  sealed class Process : IProcess
10  {
12  public int Id { get; }
13 
15  public Task Startup { get; }
16 
18  public Task<int> Lifetime { get; }
19 
20  readonly System.Diagnostics.Process handle;
21 
22  readonly StringBuilder outputStringBuilder;
23  readonly StringBuilder errorStringBuilder;
24  readonly StringBuilder combinedStringBuilder;
25 
29  readonly ILogger<Process> logger;
30 
31  public Process(System.Diagnostics.Process handle, Task<int> lifetime, StringBuilder outputStringBuilder, StringBuilder errorStringBuilder, StringBuilder combinedStringBuilder, ILogger<Process> logger, bool preExisting)
32  {
33  this.handle = handle ?? throw new ArgumentNullException(nameof(handle));
34  Lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
35 
36  this.outputStringBuilder = outputStringBuilder;
37  this.errorStringBuilder = errorStringBuilder;
38  this.combinedStringBuilder = combinedStringBuilder;
39 
40  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
41 
42  Id = handle.Id;
43 
44  if (preExisting)
45  {
46  Startup = Task.CompletedTask;
47  return;
48  }
49 
50  Startup = Task.Factory.StartNew(() =>
51  {
52  try
53  {
54  handle.WaitForInputIdle();
55  }
56  catch (InvalidOperationException) { }
57  }, default, TaskCreationOptions.LongRunning, TaskScheduler.Current);
58 
59  logger.LogTrace("Created process ID: {0}", Id);
60  }
61 
63  public void Dispose() => handle.Dispose();
64 
66  public string GetCombinedOutput()
67  {
68  if (combinedStringBuilder == null)
69  throw new InvalidOperationException("Output/Error reading was not enabled!");
70  return combinedStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
71  }
72 
74  public string GetErrorOutput()
75  {
76  if (errorStringBuilder == null)
77  throw new InvalidOperationException("Error reading was not enabled!");
78  return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
79  }
80 
82  public string GetStandardOutput()
83  {
84  if (outputStringBuilder == null)
85  throw new InvalidOperationException("Output reading was not enabled!");
86  return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
87  }
88 
90  public void Terminate()
91  {
92  if (handle.HasExited)
93  return;
94  try
95  {
96  logger.LogTrace("Terminating process...");
97  handle.Kill();
98  handle.WaitForExit();
99  }
100  catch (Exception e)
101  {
102  logger.LogDebug("Process termination exception: {0}", e);
103  }
104  }
105 
106  public void SetHighPriority()
107  {
108  try
109  {
110  handle.PriorityClass = System.Diagnostics.ProcessPriorityClass.AboveNormal;
111  logger.LogTrace("Set to above normal priority", handle.Id);
112  }
113  catch (Exception e)
114  {
115  logger.LogWarning("Unable to raise process priority! Exception: {0}", e);
116  }
117  }
118  }
119 }
readonly StringBuilder errorStringBuilder
Definition: Process.cs:23
string GetErrorOutput()
Get the stderr output of the IProcess
Definition: Process.cs:74
diff --git a/_process_executor_8cs_source.html b/_process_executor_8cs_source.html index 48cc528ee0..2403eea00a 100644 --- a/_process_executor_8cs_source.html +++ b/_process_executor_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ProcessExecutor.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Text;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
10  {
14  readonly ILogger<ProcessExecutor> logger;
15 
19  readonly ILoggerFactory loggerFactory;
20 
26  static Task<int> AttachExitHandler(System.Diagnostics.Process handle)
27  {
28  handle.EnableRaisingEvents = true;
29  var tcs = new TaskCompletionSource<int>();
30  handle.Exited += (a, b) =>
31  {
32  int exitCode;
33  try
34  {
35  exitCode = handle.ExitCode;
36  }
37  catch (InvalidOperationException)
38  {
39  return;
40  }
41  tcs.SetResult(exitCode);
42  };
43  return tcs.Task;
44  }
45 
51  public ProcessExecutor(ILogger<ProcessExecutor> logger, ILoggerFactory loggerFactory)
52  {
53  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
54  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
55  }
56 
58  public IProcess GetProcess(int id)
59  {
60  logger.LogDebug("Attaching to process {0}...", id);
61  System.Diagnostics.Process handle;
62  try
63  {
64  handle = System.Diagnostics.Process.GetProcessById(id);
65  }
66  catch(Exception e)
67  {
68  logger.LogDebug("Unable to get process {0}! Exception: {1}", id, e);
69  return null;
70  }
71  try
72  {
73  return new Process(handle, AttachExitHandler(handle), null, null, null, loggerFactory.CreateLogger<Process>(), true);
74  }
75  catch
76  {
77  handle.Dispose();
78  throw;
79  }
80  }
81 
83  public IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute)
84  {
85  logger.LogDebug("Launching process in {0}: {1} {2}", workingDirectory, fileName, arguments);
86  var handle = new System.Diagnostics.Process();
87  try
88  {
89  handle.StartInfo.FileName = fileName;
90  handle.StartInfo.Arguments = arguments;
91  handle.StartInfo.WorkingDirectory = workingDirectory;
92 
93  handle.StartInfo.UseShellExecute = !(noShellExecute || readOutput || readError);
94 
95  StringBuilder outputStringBuilder = null, errorStringBuilder = null, combinedStringBuilder = null;
96  if (readOutput || readError)
97  {
98  combinedStringBuilder = new StringBuilder();
99  if (readOutput)
100  {
101  outputStringBuilder = new StringBuilder();
102  handle.StartInfo.RedirectStandardOutput = true;
103  handle.OutputDataReceived += (sender, e) =>
104  {
105  combinedStringBuilder.Append(Environment.NewLine);
106  combinedStringBuilder.Append(e.Data);
107  outputStringBuilder.Append(Environment.NewLine);
108  outputStringBuilder.Append(e.Data);
109  };
110  }
111  if (readError)
112  {
113  errorStringBuilder = new StringBuilder();
114  handle.StartInfo.RedirectStandardError = true;
115  handle.ErrorDataReceived += (sender, e) =>
116  {
117  combinedStringBuilder.Append(Environment.NewLine);
118  combinedStringBuilder.Append(e.Data);
119  errorStringBuilder.Append(Environment.NewLine);
120  errorStringBuilder.Append(e.Data);
121  };
122  }
123  }
124 
125  var lifetimeTask = AttachExitHandler(handle);
126 
127  handle.Start();
128  try
129  {
130  if (readOutput)
131  handle.BeginOutputReadLine();
132  }
133  catch (InvalidOperationException) { }
134  try
135  {
136  if (readError)
137  handle.BeginErrorReadLine();
138  }
139  catch (InvalidOperationException) { }
140 
141  return new Process(handle, lifetimeTask, outputStringBuilder, errorStringBuilder, combinedStringBuilder, loggerFactory.CreateLogger<Process>(), false);
142  }
143  catch
144  {
145  handle.Dispose();
146  throw;
147  }
148  }
149  }
150 }
-
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProcessExecutor
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Text;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
10  {
14  readonly ILogger<ProcessExecutor> logger;
15 
19  readonly ILoggerFactory loggerFactory;
20 
26  static Task<int> AttachExitHandler(System.Diagnostics.Process handle)
27  {
28  handle.EnableRaisingEvents = true;
29  var tcs = new TaskCompletionSource<int>();
30  handle.Exited += (a, b) =>
31  {
32  int exitCode;
33  try
34  {
35  exitCode = handle.ExitCode;
36  }
37  catch (InvalidOperationException)
38  {
39  return;
40  }
41  tcs.SetResult(exitCode);
42  };
43  return tcs.Task;
44  }
45 
51  public ProcessExecutor(ILogger<ProcessExecutor> logger, ILoggerFactory loggerFactory)
52  {
53  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
54  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
55  }
56 
58  public IProcess GetProcess(int id)
59  {
60  logger.LogDebug("Attaching to process {0}...", id);
61  System.Diagnostics.Process handle;
62  try
63  {
64  handle = System.Diagnostics.Process.GetProcessById(id);
65  }
66  catch(Exception e)
67  {
68  logger.LogDebug("Unable to get process {0}! Exception: {1}", id, e);
69  return null;
70  }
71  try
72  {
73  return new Process(handle, AttachExitHandler(handle), null, null, null, loggerFactory.CreateLogger<Process>(), true);
74  }
75  catch
76  {
77  handle.Dispose();
78  throw;
79  }
80  }
81 
83  public IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute)
84  {
85  logger.LogDebug("Launching process in {0}: {1} {2}", workingDirectory, fileName, arguments);
86  var handle = new System.Diagnostics.Process();
87  try
88  {
89  handle.StartInfo.FileName = fileName;
90  handle.StartInfo.Arguments = arguments;
91  handle.StartInfo.WorkingDirectory = workingDirectory;
92 
93  handle.StartInfo.UseShellExecute = !(noShellExecute || readOutput || readError);
94 
95  StringBuilder outputStringBuilder = null, errorStringBuilder = null, combinedStringBuilder = null;
96  if (readOutput || readError)
97  {
98  combinedStringBuilder = new StringBuilder();
99  if (readOutput)
100  {
101  outputStringBuilder = new StringBuilder();
102  handle.StartInfo.RedirectStandardOutput = true;
103  handle.OutputDataReceived += (sender, e) =>
104  {
105  combinedStringBuilder.Append(Environment.NewLine);
106  combinedStringBuilder.Append(e.Data);
107  outputStringBuilder.Append(Environment.NewLine);
108  outputStringBuilder.Append(e.Data);
109  };
110  }
111  if (readError)
112  {
113  errorStringBuilder = new StringBuilder();
114  handle.StartInfo.RedirectStandardError = true;
115  handle.ErrorDataReceived += (sender, e) =>
116  {
117  combinedStringBuilder.Append(Environment.NewLine);
118  combinedStringBuilder.Append(e.Data);
119  errorStringBuilder.Append(Environment.NewLine);
120  errorStringBuilder.Append(e.Data);
121  };
122  }
123  }
124 
125  var lifetimeTask = AttachExitHandler(handle);
126 
127  handle.Start();
128  try
129  {
130  if (readOutput)
131  handle.BeginOutputReadLine();
132  }
133  catch (InvalidOperationException) { }
134  try
135  {
136  if (readError)
137  handle.BeginErrorReadLine();
138  }
139  catch (InvalidOperationException) { }
140 
141  return new Process(handle, lifetimeTask, outputStringBuilder, errorStringBuilder, combinedStringBuilder, loggerFactory.CreateLogger<Process>(), false);
142  }
143  catch
144  {
145  handle.Dispose();
146  throw;
147  }
148  }
149  }
150 }
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProcessExecutor
IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute)
Launch a IProcess
diff --git a/_r_e_a_d_m_e_8md_source.html b/_r_e_a_d_m_e_8md_source.html index 21d2584ba5..652e351e31 100644 --- a/_r_e_a_d_m_e_8md_source.html +++ b/_r_e_a_d_m_e_8md_source.html @@ -88,7 +88,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
README.md
-Go to the documentation of this file.
1 # tgstation-server v4:
2 
3 [![Build status](https://ci.appveyor.com/api/projects/status/7t1h7bvuha0p9j5f/branch/master?svg=true)](https://ci.appveyor.com/project/Cyberboss/tgstation-server-tools/branch/master) [![Build Status](https://travis-ci.org/tgstation/tgstation-server.svg?branch=master)](https://travis-ci.org/tgstation/tgstation-server) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) [![Waffle.io - Columns and their card count](https://badge.waffle.io/tgstation/tgstation-server.png?columns=all)](https://waffle.io/tgstation/tgstation-server?utm_source=badge)
4 
5 [![GitHub license](https://img.shields.io/github/license/tgstation/tgstation-server.svg)](https://github.com/tgstation/tgstation-server/blob/master/LICENSE) [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/tgstation/tgstation-server.svg)](http://isitmaintained.com/project/tgstation/tgstation-server "Average time to resolve an issue") [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Api.svg)](https://www.nuget.org/packages/Tgstation.Server.Api) [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Client.svg)](https://www.nuget.org/packages/Tgstation.Server.Client)
6 
7 [![forthebadge](http://forthebadge.com/images/badges/made-with-c-sharp.svg)](http://forthebadge.com) [![forinfinityandbyond](https://user-images.githubusercontent.com/5211576/29499758-4efff304-85e6-11e7-8267-62919c3688a9.gif)](https://www.reddit.com/r/SS13/comments/5oplxp/what_is_the_main_problem_with_byond_as_an_engine/dclbu1a)
8 
9 [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/60-percent-of-the-time-works-every-time.svg)](http://forthebadge.com)
10 
11 This is a toolset to manage production BYOND servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server) the ability start the server and restart it if it crashes, as well as systems for managing code and game files, and merging GitHub Pull Requests for test deployments.
12 
13 ### Legacy Servers
14 
15 Older server versions can be found in the V# branches of this repository. Note that V4 is nearly fully incompatible with existing installations. Only some static files may be copied over: https://github.com/tgstation/tgstation-server#static-files
16 
17 ## Setup
18 
19 ### Pre-Requisites
20 
21 - [.NET Core Runtime (>= v2.1)](https://www.microsoft.com/net/download) If you plan to install tgstation-server as a Windows service, you should also ensure that your .NET Framework runtime version is >= v4.7.1 (Download can be found on same page). On Windows, ensure that the `dotnet` executable file is in your system's `PATH` variable (or the user's that will be running the server).
22 - A [MariaDB](https://downloads.mariadb.org/), MySQL, or [Microsoft SQL Server](https://www.microsoft.com/en-us/download/details.aspx?id=55994) database engine is required
23 
24 ### Installation
25 
26 1. [Download the latest V4 release .zip](https://github.com/tgstation/tgstation-server/releases/latest). The ServerService package will only work on Windows. Choose ServerConsole if that is not your target OS or you prefer not to use the Windows service.
27 2. Extract the .zip file to where you want the server to run from. Note the account running the server must have write and delete access to the `lib` subdirectory.
28 
29 #### Windows
30 
31 If you wish to install the TGS as a service, run `Tgstation.Server.Host.Service.exe`. It should prompt you to install it. Click `Yes` and accept a potential UAC elevation prompt and the setup wizard should run.
32 
33 #### Linux (Native)
34 
35 We recommend using Docker for Linux installations, see below. The content of this parent section may be skipped if you choose to do so.
36 
37 The following dependencies are required to run tgstation-server on Linux alongside the .NET Core runtime
38 
39 - gcc-multilib (on 64-bit systems for running BYOND)
40 
41 Note that tgstation-server has only ever been tested on Linux via it's [docker environment](https://github.com/tgstation/tgstation-server/blob/master/build/Dockerfile#L22). If you are having trouble with something in a native installation, or figure out a required workaround, please contact project maintainers so this documentation may be better updated.
42 
43 #### Docker (Linux)
44 
45 tgstation-server supports running in a docker container and is the recommended deployment method for Linux systems. The official image repository is located at https://hub.docker.com/r/tgstation/server. It can also be built locally by running `docker build . -f build/Dockerfile -t <your tag name>` in the repository root.
46 
47 To create a container run
48 ```sh
49 docker run \
50  -ti \ #start interactive for manual configuration
51  --restart=always \ #if you want maximum uptime
52  --network="host" \ #if your sql server is on the same machine
53  --name="tgs" \ #or whatever else you wanna call it
54  --cap-add=sys_nice \ #allows tgs to schedule DreamDaemon as a higher priority process
55  --init \ #reaps potential zombie processes
56  -p <tgs port>:80 \
57  -p 0.0.0.0:<public game port>:<public game port> \
58  -v /path/to/your/configfile/directory:/config_data \ #only if you want to use manual configuration
59  -v /path/to/store/instances:/tgs4_instances \
60  -v /path/to/your/log/folder:/tgs_logs \
61  tgstation/server #replace this with <your tag name> if you built the image locally
62 ```
63 with any additional options you desire (i.e. You'll have to expose more game ports in order to host more than one instance).
64 
65 Note although `/app/lib` is specified as a volume mount point in the `Dockerfile`, unless you REALLY know what you're doing. Do not mount any volumes over this for fear of breaking your container.
66 
67 If using manual configuration, before starting your container make sure the aforemention `appsettings.Production.json` is setup properly. See below
68 
69 ### Configuring
70 
71 The first time you run TGS4 you should be prompted with a configuration wizard which will guide you through setting up your appsettings.Production.json
72 
73 This wizard will, generally, run whenever the server is launched without detecting the config json. Follow the instructions below to perform this process manually.
74 
75 #### Manual Configuration
76 
77 Create an `appsettings.Production.json` file next to `appsettings.json`. This will override the default settings in appsettings.json with your production settings. There are a few keys meant to be changed by hosts. Modifying any config files while the server is running will trigger a safe restart (Keeps DreamDaemon's running). Note these are all case-sensitive:
78 
79 - `General:MinimumPasswordLength`: Minimum password length requirement for database users
80 
81 - `General:GitHubAccessToken`: Specify a GitHub personal access token with no scopes here to highly mitigate the possiblity of 429 response codes from GitHub requests
82 
83 - `FileLogging:Directory`: Override the default directory where server logs are stored. Default is C:/ProgramData/tgstation-server/logs on Windows, /usr/share/tgstation-server/logs otherwise
84 
85 - `FileLogging:LogLevel`: Can be one of `Trace`, `Debug`, `Information`, `Warning`, `Error`, or `Critical`. Restricts what is put into the log files. Currently `Debug` is reccommended for help with error reporting.
86 
87 - `Kestrel:Endpoints:Http:Url`: The URL (i.e. interface and ports) your application should listen on. General use case should be `http://localhost:<port>` for restricted local connections. See the Remote Access section for configuring public access to the World Wide Web. This doesn't need to be changed using the docker setup and should be mapped with the `-p` option instead
88 
89 - `Database:DatabaseType`: Can be one of `SqlServer`, `MariaDB`, or `MySql`
90 
91 - `Database:MySqlServerVersion`: The version of MySql/MariaDB the database resides on, can be left as null for attempted auto detection. Used by the MySQL/MariaDB provider for selection of [certain features](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/blob/2.1.1/src/EFCore.MySql/Storage/Internal/ServerVersion.cs) ignore at your own risk. A string in the form `<major>.<minor>.<patch>`
92 
93 - `Database:ConnectionString`: Connection string for your database. Click [here](https://www.developerfusion.com/tools/sql-connection-string/) for an SQL Server generator or see [here](https://www.connectionstrings.com/mysql/) for a MySQL guide ([You should probably use '127.0.0.1' instead of 'localhost'](https://stackoverflow.com/questions/19712307/mysql-localhost-127-0-0-1)).
94 
95 ### Database Configuration
96 
97 If using a MariaDB/MySQL server, our client library [recommends you set 'utf8mb4' as your default charset](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1-recommended-server-charset) disregard at your own risk.
98 
99 The user created for the application will need the privilege to create databases on the first run, do not create the database for it. Once the initial set of migrations is run, the create right may be revoked. The user should maintain DDL rights though for applying future migrations
100 
101 Note that the ratio of application installations to databases is 1:1. Do not attempt to share a database amongst multiple TGS installations. (We know SQLite would be perfect for this, but it does not handle the high level of concurrency the server uses)
102 
103 ### Starting
104 
105 For the Windows service version start the `tgstation-server-4` service. If it fails to start, check the Windows event log under Windows/Application for entries from tgstation-server-4 for errors.
106 
107 For the console version run `dotnet Tgstation.Server.Host.Console.dll` in the installation directory. The `tgs.bat` and `tgs.sh` shell scripts are shortcuts for this. If on Windows, you must do this as admin to give the server permission to install the required DirectX dependency for certain 512 BYOND versions as well as create symlinks.
108 
109 For the docker version run `docker start <your container name>`
110 
111 Test your server is running by visiting the local port in your browser. You should receive a 401 Unauthorized response (You may need to view the developer console). Otherwise an error page will be present.
112 
113 ### Stopping
114 
115 Note that the live detach for DreamDaemon servers is only supported for updates or restarts via the API at this time. Stopping tgstation-server will TERMINATE ALL CHILD DREAMDAEMON SERVERS.
116 
117 For the Windows service version stop the `tgstation-server-4` service
118 
119 For the console version press `Ctrl+C` or send a SIGQUIT to the ORIGINAL dotnet process
120 
121 For the docker version run `docker stop <your container name>`
122 
123 ## Integrating
124 
125 tgstation-server 4 now REQUIRES the DMAPI to be integrated into any BYOND codebase which plans on being used by it. The integration process is a fairly simple set of code changes.
126 
127 1. Copy the [DMAPI](https://github.com/tgstation/tgstation-server/tree/master/src/DMAPI) files anywhere in your code base. `tgs.dm` can be seperated from the `tgs` folder, but do not modify or move the contents of the `tgs` folder
128 2. Modify your `.dme`(s) to include the `tgs.dm` and `tgs/includes.dm` files (ORDER OF APPEARANCE IS MANDATORY)
129 3. Follow the instructions in `tgs.dm` to integrate the API with your codebase.
130 
131 The DMAPI is fully backwards compatible and should function with any tgstation-server version to date. Updates can be performed in the same manner. Using the `TGS_EXTERNAL_CONFIGURATION` is recommended in order to make the process as easy as replacing `tgs.dm` and the `tgs` folder with a newer version
132 
133 ### Example
134 
135 Here is a bare minimum example project that implements the essential code changes for integrating the DMAPI
136 
137 Before `tgs.dm`:
138 ```dm
139 //Remember, every codebase is different, you probably have better methods for these defines than the ones given here
140 #define TGS_EXTERNAL_CONFIGURATION
141 #define TGS_DEFINE_AND_SET_GLOBAL(Name, Value) var/global/##Name = ##Value
142 #define TGS_READ_GLOBAL(Name) global.##Name
143 #define TGS_WRITE_GLOBAL(Name, Value) global.##Name = ##Value
144 #define TGS_WORLD_ANNOUNCE(message) world << ##message
145 #define TGS_INFO_LOG(message) world.log << "TGS Info: [##message]"
146 #define TGS_ERROR_LOG(message) world.log << "TGS Error: [##message]"
147 #define TGS_NOTIFY_ADMINS(event) world.log << "TGS Admin Message: [##event]"
148 #define TGS_CLIENT_COUNT global.client_cout
149 #define TGS_PROTECT_DATUM(Path) // Leave blank if your codebase doesn't give administrators code reflection capabilities
150 ```
151 
152 Anywhere else:
153 ```dm
154 var/global/client_count = 0
155 
156 /world/New()
157  ..()
158  TgsNew()
159  TgsInitializationsComplete()
160 
161 /world/Reboot()
162  TgsReboot()
163  ..()
164 
165 /world/Topic()
166  TGS_TOPIC
167  ..()
168 
169 /client/New()
170  ..()
171  ++global.client_count
172 
173 /client/Del()
174  ..()
175  --global.client_count
176 
177 ```
178 
179 ## Remote Access
180 
181 tgstation-server is an [ASP.Net Core](https://docs.microsoft.com/en-us/aspnet/core/) app based on the Kestrel web server. This section is meant to serve as a general use case overview, but the entire Kestrel configuration can be modified to your liking with the configuration JSON. See [the official documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel) for details.
182 
183 Exposing the builtin Kestrel server to the internet directly over HTTP is highly not reccommended due to the lack of security. The recommended way to expose tgstation-server to the internet is to host it through a reverse proxy with HTTPS support. Here are some step by step examples to achieve this for major web servers.
184 
185 System administrators will most likely have their own configuration plans, but here are some basic guides for beginners.
186 
187 Once complete, test that your configuration worked by visiting your proxy site from a browser on a different computer. You should recieve a 401 Unauthorized response.
188 
189 ### IIS (Reccommended for Windows)
190 
191 1. Acquire an HTTPS certificate. The easiet free way for Windows is [win-acme](https://github.com/PKISharp/win-acme) (requires you to set up the website first)
192 2. Install the [Web Platform Installer](https://www.microsoft.com/web/downloads/platform.aspx)
193 3. Open the web platform installer in the IIS Manager and install the Application Request Routing 3.0 module
194 4. Create a new website, bind it to HTTPS only with your chosen certificate and exposed port. The physical path won't matter since it won't be used. Use `Require Server Name Indication` if you want to limit requests to a specific URL prefix.
195 5. Close and reopen the IIS Manager
196 5. Open the site and navigate to the `URL Rewrite` module
197 6. In the `Actions` Pane on the right click `Add Rule(s)...`
198 7. For the rule template, select `Reverse Proxy` under `Inbound and Outbound Rules` and click `OK`
199 8. You may get a prompt about enabling proxy functionality. Click `OK`
200 9. In the window that appears set the `Inbound Rules` textbox to the URL of your tgstation-server i.e. `http://localhost:5000`. Ensure `Enable SSL Offloading` is checked, then click `OK`
201 
202 ### Caddy (Reccommended for Linux, or those unfamilar with configuring NGINX or Apache)
203 
204 1. Setup a basic website configuration. Instructions on how to do so are out of scope.
205 2. In your Caddyfile, under a server entry, add the following (replace 8080 with the port TGS is hosted on):
206 ```
207 proxy /tgs localhost:8080 {
208  transparent
209 }
210 ```
211 
212 See https://caddyserver.com/docs/proxy
213 
214 ### NGINX (Reccommended for Linux)
215 
216 1. Setup a basic website configuration. Instructions on how to do so are out of scope.
217 2. Acquire an HTTPS certificate, likely via Let's Encrypt, and configure NGINX to use it.
218 3. Setup a path under a server like the following (replace 8080 with the port TGS is hosted on):
219 ```
220 location /tgs {
221  proxy_pass http://127.0.0.1:8080;
222  break;
223 }
224 ```
225 
226 See https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/
227 
228 ### Apache
229 
230 1. Ensure the `mod_proxy` extension is installed.
231 2. Setup a basic website configuration. Instructions on how to do so are out of scope.
232 3. Acquire an HTTPS certificate, likely via Let's Encrypt, and configure Apache to use it.
233 4. Under a VirtualHost entry, setup the following (replace 8080 with the port TGS is hosted on):
234 ```
235 ProxyPass / http://127.0.0.1:8080
236 ProxyPassReverse / http://127.0.0.1:8080
237 ```
238 
239 See https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html
240 
241 ## Usage
242 
243 tgstation-server v4 is controlled via a RESTful HTTP json API. Documentation on this API can be found [here](https://tgstation.github.io/tgstation-server/api.html). This section serves to document the concepts of the server.
244 
245 ### Users
246 
247 All actions apart from logging in must be taken by a user. TGS installs with one default user whose credentials can be found [here](https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Api/Models/User.cs). It is recommended to disable this user ASAP as it is used to create Jobs that are started by the system. If access to all users is lost, the default user can be reset using the `Database:ResetAdminPassword` configuration setting.
248 
249 Users can be enabled/disabled and have a very granular set of rights associated to them that determine the actions they are allowed to take (i.e. Modify the user list or create instances). Users can be _database based_ or _system based_. Database users are your standard web users with a username and password. System users, on the otherhand, are authenticated with the host OS. These users cannot have their password or names changed by TGS as they are managed by the system (and in reverse, login tokens don't expire when their password changes). The benefit to having these users is it allows the use of system ACLs for static file control. More on that later.
250 
251 ### Instances
252 
253 A TGS deployment is made up with a set of instances, which each represent a production BYOND server. As many instances as desired can be created. Be aware, however, due to the nature of BYOND, this will quickly result in system resource exhaustion.
254 
255 An instance is stored in a single folder anywhere on a system and is made up of several components: The source code git repository, the BYOND installations, the compiler, the watchdog, chat bots, and static file management systems.
256 
257 ##### Instance Users
258 
259 All users with access to an instance have an InstanceUser object associated with the two that defines more rights specific to that instance (i.e. Deploy code, modify bots, edit other InstanceUsers).
260 
261 #### Repository
262 
263 The `Repository` folder is a git repository containing the code of the game you wish to host. It can be cloned from any public or private remote repository and has capabilities to affect changes back to it. All the standard benefits of git are utilized (i.e. check out any revision or reference).
264 
265 Additional features become available if the remote repository is hosted on https://github.com/. Namely the Test Merge feature, which allows you to take a pull request opened on the repository and compile it into a game deployment for testing. Information about test merges is available in game via the DMAPI and via the main API as well.
266 
267 Manual operations on the repository while an instance is running may lead to git data corruption. Thankfully, it's simple enough to delete and reclone the repository via the API.
268 
269 #### Byond
270 
271 The `Byond` folder contains installations of [BYOND](https://secure.byond.com/) versions. The version which is used by your game code can be changed on a whim (Note that only versions >= 511.1385 have been thouroughly tested. Lower versions should work but if one doesn't function, please open an issue report) and the server will take care of installing it.
272 
273 #### Compiler
274 
275 The compiler deploys code from the `Repository` folder to the `Game` folder and compiles it either by autodetecting the `.dme` or having it set by configuration. Several other step are also run such as validating the DMAPI version and creating symlinks for static files are done at this point. The compiler also applies server side code modifications and duplicates compiled code for the watchdog as well (See following section).
276 
277 #### Watchdog
278 
279 The watchdog is responsible for starting and keeping your server running. It functions by launching two servers which are hot-swapped on `/world/Reboot`s and during crashes to prevent downtime. This hot swapping feature is also what allows TGS to deploy updates to live servers without bringing them down.
280 
281 DreamDaemon can be finicky and will crash with several high load games or bad DM code. The watchdog has several failure prevention methods to keep at least one server running while these issues are sorted out.
282 
283 #### Chat Bots
284 
285 TGS supports creating infinite chat bots for notifying staff or players of things like code deployments and uptime in. Currently the following providers are supported
286 
287 - Internet Relay Chat (IRC)
288 - Discord
289 
290 More can be added by providing a new implementation of the [IProvider](https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs) interface
291 
292 Bots have a set of built-in commands that can be triggered via `!tgs`, mentioning, or private messaging them. Along with these, custom commands can be defined using the DMAPI by creating a subtype of the `/datum/tgs_chat_command` type (See `tgs.dm` for details). Invocation for custom commands can be restricted to certain channels.
293 
294 #### Static Files
295 
296 All files in game code deployments are considered transient by default, meaning when new code is deployed, changes will be lost. Static files allow you to specify which files and folders stick around throughout all deployments.
297 
298 The `StaticFiles` folder contains 3 root folders which cannot be deleted and operate under special rules
299  - `CodeModifications`
300  - `EventScripts`
301  - `GameStaticFiles`
302 
303 These files can be modified either in host mode or system user mode. In host mode, TGS itself is responsible for reading and writing the files. In system user mode read and write actions are performed using the system account of the logged on User, enabling the use of ACLs to control access to files. Database users will not be able to use the static file system if this mode is configured for an instance.
304 
305 This folder may be freely modified manually just beware this may cause in-progress deployments to error if done on Windows systems.
306 
307 #### CodeModifications
308 
309 When a deployment is made by the compiler, all the contents of this folder are copied over the repository contents. Then one of two code change modes are selected based on the prescense of certain files.
310 
311 If `<target dme>.dm` is present, that .dme will be used instead of the repository's `.dme`
312 
313 Otherwise the files `HeadInclude.dm` and `TailInclude.dm` are searched for and added as include lines to the top and bottom of the target `.dme` repsectively if they exist. These files can contain any valid DreamMaker code (Including `#include`ing other `.dm` files!) allowing you to modify the a repository's code on a per instance basis
314 
315 #### EventScripts
316 
317 This folder can contain anything. But, when certain events occur in the instance, TGS will look here for `.bat` or `.sh` files with the same name and run those with corresponding arguments. List of supported events can be found [here](https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs#L28) (subject to expansion) list of event parameters can be found [here](https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Host/Components/EventType.cs)
318 
319 #### GameStaticFiles
320 
321 Any files and folders contained in this root level of this folder will be symbolically linked to all deployments at the time they are created. This allows persistent game data (BYOND `.sav`s or code configuration files for example) to persist across all deployments. This folder contains a .tgsignore file which can be used to prevent symlinks from being generated by entering the names of files and folders (1 per line)
322 
323 ### Updating
324 
325 TGS 4 can self update without stopping your DreamDaemon servers. Any V4 release made to this repository is bound by a contract that allows changes of the runtime assemblies without stopping your servers. Database migrations are automatically applied as well. Because of this REVERTING TO LOWER VERSIONS IS NOT OFFICIALLY SUPPORTED, do so at your own risk (check changes made to `/src/Tgstation.Server.Host/Models/Migrations`).
326 
327 ### Clients
328 
329 Here are tools for interacting with the TGS 4 web API
330 
331 - [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server
332 - [Tgstation.Server.Client](https://www.nuget.org/packages/Tgstation.Server.Client): A nuget .NET Standard 2.0 TAP based library for communicating with tgstation-server
333 - [Tgstation.Server.Api](https://www.nuget.org/packages/Tgstation.Server.Api): A nuget .NET Standard 2.0 library containing API definitions for tgstation-server
334 - [Postman](https://www.getpostman.com/): This repository contains [TGS.postman_collection.json](https://github.com/tgstation/tgstation-server/blob/master/tools/TGS.postman_collection.json) which is used during development for testing. Contains example requests for all endpoints but takes some knowledge to use (Note that the pre-request script is configured to login the default admin user for every request)
335 
336 Contact project maintainers to get your client added to this list
337 
338 ## Backup/Restore
339 
340 Note that tgstation-server is NOT a backup solution, the onus is on the server runners.
341 
342 The `Repository` folder should, by the nature of git, not need to be backed up or should be done so on the remote server if necessary.
343 
344 The `BYOND` and `Game` folders should never be backed up due.
345 
346 The `Configuration` folder should and database be fully backed up
347 
348 To restore an installation from backups, first restore the instance `Configuration` folders in their new homes. Then restore the database, modifying the `Path` column in the `Instances` table where necessary to point to the new instances. Then start the server pointed at the new database.
349 
350 Should you end up with a lost database for some reason or want to reattach a detached instance you can reattach an existing folder by creating an empty file named `TGS4_ALLOW_INSTANCE_ATTACH` inside it (This is automatically created when detaching instances). Then create a new instance with that path, this will bypass the empty folder check. Note that this will not restore things such as user permissions, server config options, or deployment metadata. Those must be reconfigured manually
351 
352 ## Troubleshooting
353 
354 Feel free to ask for help at the coderbus discord in #tooling-questions: https://discord.gg/Vh8TJp9. Cyberboss#8246 can answer most questions.
355 
356 ## Contributing
357 
358 * See [CONTRIBUTING.md](https://github.com/tgstation/tgstation-server/blob/master/.github/CONTRIBUTING.md)
359 
360 ## Licensing
361 
362 * The DMAPI for the project is licensed under the MIT license.
363 * The /tg/station 13 icon is licensed under [Creative Commons 3.0 BY-SA](http://creativecommons.org/licenses/by-sa/3.0/).
364 * The remainder of the project is licensed under [GNU AGPL v3](http://www.gnu.org/licenses/agpl-3.0.html)
365 
366 See the files in the `/src/DMAPI` tree for the MIT license
+Go to the documentation of this file.
1 # tgstation-server v4:
2 
3 [![Build status](https://ci.appveyor.com/api/projects/status/7t1h7bvuha0p9j5f/branch/master?svg=true)](https://ci.appveyor.com/project/Cyberboss/tgstation-server-tools/branch/master) [![Build Status](https://travis-ci.org/tgstation/tgstation-server.svg?branch=master)](https://travis-ci.org/tgstation/tgstation-server) [![codecov](https://codecov.io/gh/tgstation/tgstation-server/branch/master/graph/badge.svg)](https://codecov.io/gh/tgstation/tgstation-server) [![Waffle.io - Columns and their card count](https://badge.waffle.io/tgstation/tgstation-server.png?columns=all)](https://waffle.io/tgstation/tgstation-server?utm_source=badge)
4 
5 [![GitHub license](https://img.shields.io/github/license/tgstation/tgstation-server.svg)](https://github.com/tgstation/tgstation-server/blob/master/LICENSE) [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/tgstation/tgstation-server.svg)](http://isitmaintained.com/project/tgstation/tgstation-server "Average time to resolve an issue") [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Api.svg)](https://www.nuget.org/packages/Tgstation.Server.Api) [![NuGet version](https://img.shields.io/nuget/v/Tgstation.Server.Client.svg)](https://www.nuget.org/packages/Tgstation.Server.Client)
6 
7 [![forthebadge](http://forthebadge.com/images/badges/made-with-c-sharp.svg)](http://forthebadge.com) [![forinfinityandbyond](https://user-images.githubusercontent.com/5211576/29499758-4efff304-85e6-11e7-8267-62919c3688a9.gif)](https://www.reddit.com/r/SS13/comments/5oplxp/what_is_the_main_problem_with_byond_as_an_engine/dclbu1a)
8 
9 [![forthebadge](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com) [![forthebadge](http://forthebadge.com/images/badges/60-percent-of-the-time-works-every-time.svg)](http://forthebadge.com)
10 
11 This is a toolset to manage production BYOND servers. It includes the ability to update the server without having to stop or shutdown the server (the update will take effect on a "reboot" of the server) the ability start the server and restart it if it crashes, as well as systems for managing code and game files, and merging GitHub Pull Requests for test deployments.
12 
13 ### Legacy Servers
14 
15 Older server versions can be found in the V# branches of this repository. Note that V4 is nearly fully incompatible with existing installations. Only some static files may be copied over: https://github.com/tgstation/tgstation-server#static-files
16 
17 ## Setup
18 
19 ### Pre-Requisites
20 
21 - [.NET Core Runtime (>= v2.1)](https://www.microsoft.com/net/download) If you plan to install tgstation-server as a Windows service, you should also ensure that your .NET Framework runtime version is >= v4.7.1 (Download can be found on same page). On Windows, ensure that the `dotnet` executable file is in your system's `PATH` variable (or the user's that will be running the server).
22 - A [MariaDB](https://downloads.mariadb.org/), MySQL, or [Microsoft SQL Server](https://www.microsoft.com/en-us/download/details.aspx?id=55994) database engine is required
23 
24 ### Installation
25 
26 1. [Download the latest V4 release .zip](https://github.com/tgstation/tgstation-server/releases/latest). The ServerService package will only work on Windows. Choose ServerConsole if that is not your target OS or you prefer not to use the Windows service.
27 2. Extract the .zip file to where you want the server to run from. Note the account running the server must have write and delete access to the `lib` subdirectory.
28 
29 #### Windows
30 
31 If you wish to install the TGS as a service, run `Tgstation.Server.Host.Service.exe`. It should prompt you to install it. Click `Yes` and accept a potential UAC elevation prompt and the setup wizard should run.
32 
33 #### Linux (Native)
34 
35 We recommend using Docker for Linux installations, see below. The content of this parent section may be skipped if you choose to do so.
36 
37 The following dependencies are required to run tgstation-server on Linux alongside the .NET Core runtime
38 
39 - gcc-multilib (on 64-bit systems for running BYOND)
40 
41 Note that tgstation-server has only ever been tested on Linux via it's [docker environment](https://github.com/tgstation/tgstation-server/blob/master/build/Dockerfile#L22). If you are having trouble with something in a native installation, or figure out a required workaround, please contact project maintainers so this documentation may be better updated.
42 
43 #### Docker (Linux)
44 
45 tgstation-server supports running in a docker container and is the recommended deployment method for Linux systems. The official image repository is located at https://hub.docker.com/r/tgstation/server. It can also be built locally by running `docker build . -f build/Dockerfile -t <your tag name>` in the repository root.
46 
47 To create a container run
48 ```sh
49 docker run \
50  -ti \ #start interactive for manual configuration
51  --restart=always \ #if you want maximum uptime
52  --network="host" \ #if your sql server is on the same machine
53  --name="tgs" \ #or whatever else you wanna call it
54  --cap-add=sys_nice \ #allows tgs to schedule DreamDaemon as a higher priority process
55  --init \ #reaps potential zombie processes
56  -p <tgs port>:80 \
57  -p 0.0.0.0:<public game port>:<public game port> \
58  -v /path/to/your/configfile/directory:/config_data \ #only if you want to use manual configuration
59  -v /path/to/store/instances:/tgs4_instances \
60  -v /path/to/your/log/folder:/tgs_logs \
61  tgstation/server #replace this with <your tag name> if you built the image locally
62 ```
63 with any additional options you desire (i.e. You'll have to expose more game ports in order to host more than one instance).
64 
65 Note although `/app/lib` is specified as a volume mount point in the `Dockerfile`, unless you REALLY know what you're doing. Do not mount any volumes over this for fear of breaking your container.
66 
67 If using manual configuration, before starting your container make sure the aforemention `appsettings.Production.json` is setup properly. See below
68 
69 ### Configuring
70 
71 The first time you run TGS4 you should be prompted with a configuration wizard which will guide you through setting up your appsettings.Production.json
72 
73 This wizard will, generally, run whenever the server is launched without detecting the config json. Follow the instructions below to perform this process manually.
74 
75 #### Manual Configuration
76 
77 Create an `appsettings.Production.json` file next to `appsettings.json`. This will override the default settings in appsettings.json with your production settings. There are a few keys meant to be changed by hosts. Modifying any config files while the server is running will trigger a safe restart (Keeps DreamDaemon's running). Note these are all case-sensitive:
78 
79 - `General:MinimumPasswordLength`: Minimum password length requirement for database users
80 
81 - `General:GitHubAccessToken`: Specify a GitHub personal access token with no scopes here to highly mitigate the possiblity of 429 response codes from GitHub requests
82 
83 - `FileLogging:Directory`: Override the default directory where server logs are stored. Default is C:/ProgramData/tgstation-server/logs on Windows, /usr/share/tgstation-server/logs otherwise
84 
85 - `FileLogging:LogLevel`: Can be one of `Trace`, `Debug`, `Information`, `Warning`, `Error`, or `Critical`. Restricts what is put into the log files. Currently `Debug` is reccommended for help with error reporting.
86 
87 - `Kestrel:Endpoints:Http:Url`: The URL (i.e. interface and ports) your application should listen on. General use case should be `http://localhost:<port>` for restricted local connections. See the Remote Access section for configuring public access to the World Wide Web. This doesn't need to be changed using the docker setup and should be mapped with the `-p` option instead
88 
89 - `Database:DatabaseType`: Can be one of `SqlServer`, `MariaDB`, or `MySql`
90 
91 - `Database:MySqlServerVersion`: The version of MySql/MariaDB the database resides on, can be left as null for attempted auto detection. Used by the MySQL/MariaDB provider for selection of [certain features](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/blob/2.1.1/src/EFCore.MySql/Storage/Internal/ServerVersion.cs) ignore at your own risk. A string in the form `<major>.<minor>.<patch>`
92 
93 - `Database:ConnectionString`: Connection string for your database. Click [here](https://www.developerfusion.com/tools/sql-connection-string/) for an SQL Server generator or see [here](https://www.connectionstrings.com/mysql/) for a MySQL guide ([You should probably use '127.0.0.1' instead of 'localhost'](https://stackoverflow.com/questions/19712307/mysql-localhost-127-0-0-1)).
94 
95 - `ControlPanel:Enable`: Enable the javascript based control panel to be served from the server via /index.html
96 
97 - `ControlPanel:AllowAnyOrigin`: Set the Access-Control-Allow-Origin header to * for all responses (also enables all headers and methods)
98 
99 - `ControlPanel:AllowedOrigins`: Set the Access-Control-Allow-Origin headers to this list of origins for all responses (also enables all headers and methods). This is overridden by `ControlPanel:AllowAnyOrigin`
100 
101 ### Database Configuration
102 
103 If using a MariaDB/MySQL server, our client library [recommends you set 'utf8mb4' as your default charset](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1-recommended-server-charset) disregard at your own risk.
104 
105 The user created for the application will need the privilege to create databases on the first run, do not create the database for it. Once the initial set of migrations is run, the create right may be revoked. The user should maintain DDL rights though for applying future migrations
106 
107 Note that the ratio of application installations to databases is 1:1. Do not attempt to share a database amongst multiple TGS installations. (We know SQLite would be perfect for this, but it does not handle the high level of concurrency the server uses)
108 
109 ### Starting
110 
111 For the Windows service version start the `tgstation-server-4` service. If it fails to start, check the Windows event log under Windows/Application for entries from tgstation-server-4 for errors.
112 
113 For the console version run `dotnet Tgstation.Server.Host.Console.dll` in the installation directory. The `tgs.bat` and `tgs.sh` shell scripts are shortcuts for this. If on Windows, you must do this as admin to give the server permission to install the required DirectX dependency for certain 512 BYOND versions as well as create symlinks.
114 
115 For the docker version run `docker start <your container name>`
116 
117 Test your server is running by visiting the local port in your browser. You should receive a 401 Unauthorized response (You may need to view the developer console). Otherwise an error page will be present.
118 
119 ### Stopping
120 
121 Note that the live detach for DreamDaemon servers is only supported for updates or restarts via the API at this time. Stopping tgstation-server will TERMINATE ALL CHILD DREAMDAEMON SERVERS.
122 
123 For the Windows service version stop the `tgstation-server-4` service
124 
125 For the console version press `Ctrl+C` or send a SIGQUIT to the ORIGINAL dotnet process
126 
127 For the docker version run `docker stop <your container name>`
128 
129 ## Integrating
130 
131 tgstation-server 4 now REQUIRES the DMAPI to be integrated into any BYOND codebase which plans on being used by it. The integration process is a fairly simple set of code changes.
132 
133 1. Copy the [DMAPI](https://github.com/tgstation/tgstation-server/tree/master/src/DMAPI) files anywhere in your code base. `tgs.dm` can be seperated from the `tgs` folder, but do not modify or move the contents of the `tgs` folder
134 2. Modify your `.dme`(s) to include the `tgs.dm` and `tgs/includes.dm` files (ORDER OF APPEARANCE IS MANDATORY)
135 3. Follow the instructions in `tgs.dm` to integrate the API with your codebase.
136 
137 The DMAPI is fully backwards compatible and should function with any tgstation-server version to date. Updates can be performed in the same manner. Using the `TGS_EXTERNAL_CONFIGURATION` is recommended in order to make the process as easy as replacing `tgs.dm` and the `tgs` folder with a newer version
138 
139 ### Example
140 
141 Here is a bare minimum example project that implements the essential code changes for integrating the DMAPI
142 
143 Before `tgs.dm`:
144 ```dm
145 //Remember, every codebase is different, you probably have better methods for these defines than the ones given here
146 #define TGS_EXTERNAL_CONFIGURATION
147 #define TGS_DEFINE_AND_SET_GLOBAL(Name, Value) var/global/##Name = ##Value
148 #define TGS_READ_GLOBAL(Name) global.##Name
149 #define TGS_WRITE_GLOBAL(Name, Value) global.##Name = ##Value
150 #define TGS_WORLD_ANNOUNCE(message) world << ##message
151 #define TGS_INFO_LOG(message) world.log << "TGS Info: [##message]"
152 #define TGS_ERROR_LOG(message) world.log << "TGS Error: [##message]"
153 #define TGS_NOTIFY_ADMINS(event) world.log << "TGS Admin Message: [##event]"
154 #define TGS_CLIENT_COUNT global.client_cout
155 #define TGS_PROTECT_DATUM(Path) // Leave blank if your codebase doesn't give administrators code reflection capabilities
156 ```
157 
158 Anywhere else:
159 ```dm
160 var/global/client_count = 0
161 
162 /world/New()
163  ..()
164  TgsNew()
165  TgsInitializationsComplete()
166 
167 /world/Reboot()
168  TgsReboot()
169  ..()
170 
171 /world/Topic()
172  TGS_TOPIC
173  ..()
174 
175 /client/New()
176  ..()
177  ++global.client_count
178 
179 /client/Del()
180  ..()
181  --global.client_count
182 
183 ```
184 
185 ## Remote Access
186 
187 tgstation-server is an [ASP.Net Core](https://docs.microsoft.com/en-us/aspnet/core/) app based on the Kestrel web server. This section is meant to serve as a general use case overview, but the entire Kestrel configuration can be modified to your liking with the configuration JSON. See [the official documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel) for details.
188 
189 Exposing the builtin Kestrel server to the internet directly over HTTP is highly not reccommended due to the lack of security. The recommended way to expose tgstation-server to the internet is to host it through a reverse proxy with HTTPS support. Here are some step by step examples to achieve this for major web servers.
190 
191 System administrators will most likely have their own configuration plans, but here are some basic guides for beginners.
192 
193 Once complete, test that your configuration worked by visiting your proxy site from a browser on a different computer. You should recieve a 401 Unauthorized response.
194 
195 ### IIS (Reccommended for Windows)
196 
197 1. Acquire an HTTPS certificate. The easiet free way for Windows is [win-acme](https://github.com/PKISharp/win-acme) (requires you to set up the website first)
198 2. Install the [Web Platform Installer](https://www.microsoft.com/web/downloads/platform.aspx)
199 3. Open the web platform installer in the IIS Manager and install the Application Request Routing 3.0 module
200 4. Create a new website, bind it to HTTPS only with your chosen certificate and exposed port. The physical path won't matter since it won't be used. Use `Require Server Name Indication` if you want to limit requests to a specific URL prefix.
201 5. Close and reopen the IIS Manager
202 5. Open the site and navigate to the `URL Rewrite` module
203 6. In the `Actions` Pane on the right click `Add Rule(s)...`
204 7. For the rule template, select `Reverse Proxy` under `Inbound and Outbound Rules` and click `OK`
205 8. You may get a prompt about enabling proxy functionality. Click `OK`
206 9. In the window that appears set the `Inbound Rules` textbox to the URL of your tgstation-server i.e. `http://localhost:5000`. Ensure `Enable SSL Offloading` is checked, then click `OK`
207 
208 ### Caddy (Reccommended for Linux, or those unfamilar with configuring NGINX or Apache)
209 
210 1. Setup a basic website configuration. Instructions on how to do so are out of scope.
211 2. In your Caddyfile, under a server entry, add the following (replace 8080 with the port TGS is hosted on):
212 ```
213 proxy /tgs localhost:8080 {
214  transparent
215 }
216 ```
217 
218 See https://caddyserver.com/docs/proxy
219 
220 ### NGINX (Reccommended for Linux)
221 
222 1. Setup a basic website configuration. Instructions on how to do so are out of scope.
223 2. Acquire an HTTPS certificate, likely via Let's Encrypt, and configure NGINX to use it.
224 3. Setup a path under a server like the following (replace 8080 with the port TGS is hosted on):
225 ```
226 location /tgs {
227  proxy_pass http://127.0.0.1:8080;
228  break;
229 }
230 ```
231 
232 See https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/
233 
234 ### Apache
235 
236 1. Ensure the `mod_proxy` extension is installed.
237 2. Setup a basic website configuration. Instructions on how to do so are out of scope.
238 3. Acquire an HTTPS certificate, likely via Let's Encrypt, and configure Apache to use it.
239 4. Under a VirtualHost entry, setup the following (replace 8080 with the port TGS is hosted on):
240 ```
241 ProxyPass / http://127.0.0.1:8080
242 ProxyPassReverse / http://127.0.0.1:8080
243 ```
244 
245 See https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html
246 
247 ## Usage
248 
249 tgstation-server v4 is controlled via a RESTful HTTP json API. Documentation on this API can be found [here](https://tgstation.github.io/tgstation-server/api.html). This section serves to document the concepts of the server.
250 
251 ### Users
252 
253 All actions apart from logging in must be taken by a user. TGS installs with one default user whose credentials can be found [here](https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Api/Models/User.cs). It is recommended to disable this user ASAP as it is used to create Jobs that are started by the system. If access to all users is lost, the default user can be reset using the `Database:ResetAdminPassword` configuration setting.
254 
255 Users can be enabled/disabled and have a very granular set of rights associated to them that determine the actions they are allowed to take (i.e. Modify the user list or create instances). Users can be _database based_ or _system based_. Database users are your standard web users with a username and password. System users, on the otherhand, are authenticated with the host OS. These users cannot have their password or names changed by TGS as they are managed by the system (and in reverse, login tokens don't expire when their password changes). The benefit to having these users is it allows the use of system ACLs for static file control. More on that later.
256 
257 ### Instances
258 
259 A TGS deployment is made up with a set of instances, which each represent a production BYOND server. As many instances as desired can be created. Be aware, however, due to the nature of BYOND, this will quickly result in system resource exhaustion.
260 
261 An instance is stored in a single folder anywhere on a system and is made up of several components: The source code git repository, the BYOND installations, the compiler, the watchdog, chat bots, and static file management systems.
262 
263 ##### Instance Users
264 
265 All users with access to an instance have an InstanceUser object associated with the two that defines more rights specific to that instance (i.e. Deploy code, modify bots, edit other InstanceUsers).
266 
267 #### Repository
268 
269 The `Repository` folder is a git repository containing the code of the game you wish to host. It can be cloned from any public or private remote repository and has capabilities to affect changes back to it. All the standard benefits of git are utilized (i.e. check out any revision or reference).
270 
271 Additional features become available if the remote repository is hosted on https://github.com/. Namely the Test Merge feature, which allows you to take a pull request opened on the repository and compile it into a game deployment for testing. Information about test merges is available in game via the DMAPI and via the main API as well.
272 
273 Manual operations on the repository while an instance is running may lead to git data corruption. Thankfully, it's simple enough to delete and reclone the repository via the API.
274 
275 #### Byond
276 
277 The `Byond` folder contains installations of [BYOND](https://secure.byond.com/) versions. The version which is used by your game code can be changed on a whim (Note that only versions >= 511.1385 have been thouroughly tested. Lower versions should work but if one doesn't function, please open an issue report) and the server will take care of installing it.
278 
279 #### Compiler
280 
281 The compiler deploys code from the `Repository` folder to the `Game` folder and compiles it either by autodetecting the `.dme` or having it set by configuration. Several other step are also run such as validating the DMAPI version and creating symlinks for static files are done at this point. The compiler also applies server side code modifications and duplicates compiled code for the watchdog as well (See following section).
282 
283 #### Watchdog
284 
285 The watchdog is responsible for starting and keeping your server running. It functions by launching two servers which are hot-swapped on `/world/Reboot`s and during crashes to prevent downtime. This hot swapping feature is also what allows TGS to deploy updates to live servers without bringing them down.
286 
287 DreamDaemon can be finicky and will crash with several high load games or bad DM code. The watchdog has several failure prevention methods to keep at least one server running while these issues are sorted out.
288 
289 #### Chat Bots
290 
291 TGS supports creating infinite chat bots for notifying staff or players of things like code deployments and uptime in. Currently the following providers are supported
292 
293 - Internet Relay Chat (IRC)
294 - Discord
295 
296 More can be added by providing a new implementation of the [IProvider](https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs) interface
297 
298 Bots have a set of built-in commands that can be triggered via `!tgs`, mentioning, or private messaging them. Along with these, custom commands can be defined using the DMAPI by creating a subtype of the `/datum/tgs_chat_command` type (See `tgs.dm` for details). Invocation for custom commands can be restricted to certain channels.
299 
300 #### Static Files
301 
302 All files in game code deployments are considered transient by default, meaning when new code is deployed, changes will be lost. Static files allow you to specify which files and folders stick around throughout all deployments.
303 
304 The `StaticFiles` folder contains 3 root folders which cannot be deleted and operate under special rules
305  - `CodeModifications`
306  - `EventScripts`
307  - `GameStaticFiles`
308 
309 These files can be modified either in host mode or system user mode. In host mode, TGS itself is responsible for reading and writing the files. In system user mode read and write actions are performed using the system account of the logged on User, enabling the use of ACLs to control access to files. Database users will not be able to use the static file system if this mode is configured for an instance.
310 
311 This folder may be freely modified manually just beware this may cause in-progress deployments to error if done on Windows systems.
312 
313 #### CodeModifications
314 
315 When a deployment is made by the compiler, all the contents of this folder are copied over the repository contents. Then one of two code change modes are selected based on the prescense of certain files.
316 
317 If `<target dme>.dm` is present, that .dme will be used instead of the repository's `.dme`
318 
319 Otherwise the files `HeadInclude.dm` and `TailInclude.dm` are searched for and added as include lines to the top and bottom of the target `.dme` repsectively if they exist. These files can contain any valid DreamMaker code (Including `#include`ing other `.dm` files!) allowing you to modify the a repository's code on a per instance basis
320 
321 #### EventScripts
322 
323 This folder can contain anything. But, when certain events occur in the instance, TGS will look here for `.bat` or `.sh` files with the same name and run those with corresponding arguments. List of supported events can be found [here](https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs#L28) (subject to expansion) list of event parameters can be found [here](https://github.com/tgstation/tgstation-server/blob/master/src/Tgstation.Server.Host/Components/EventType.cs)
324 
325 #### GameStaticFiles
326 
327 Any files and folders contained in this root level of this folder will be symbolically linked to all deployments at the time they are created. This allows persistent game data (BYOND `.sav`s or code configuration files for example) to persist across all deployments. This folder contains a .tgsignore file which can be used to prevent symlinks from being generated by entering the names of files and folders (1 per line)
328 
329 ### Updating
330 
331 TGS 4 can self update without stopping your DreamDaemon servers. Any V4 release made to this repository is bound by a contract that allows changes of the runtime assemblies without stopping your servers. Database migrations are automatically applied as well. Because of this REVERTING TO LOWER VERSIONS IS NOT OFFICIALLY SUPPORTED, do so at your own risk (check changes made to `/src/Tgstation.Server.Host/Models/Migrations`).
332 
333 ### Clients
334 
335 Here are tools for interacting with the TGS 4 web API
336 
337 - [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel): Official client. A cross platform GUI for using tgstation-server
338 - [Tgstation.Server.Client](https://www.nuget.org/packages/Tgstation.Server.Client): A nuget .NET Standard 2.0 TAP based library for communicating with tgstation-server
339 - [Tgstation.Server.Api](https://www.nuget.org/packages/Tgstation.Server.Api): A nuget .NET Standard 2.0 library containing API definitions for tgstation-server
340 - [Postman](https://www.getpostman.com/): This repository contains [TGS.postman_collection.json](https://github.com/tgstation/tgstation-server/blob/master/tools/TGS.postman_collection.json) which is used during development for testing. Contains example requests for all endpoints but takes some knowledge to use (Note that the pre-request script is configured to login the default admin user for every request)
341 
342 Contact project maintainers to get your client added to this list
343 
344 ## Backup/Restore
345 
346 Note that tgstation-server is NOT a backup solution, the onus is on the server runners.
347 
348 The `Repository` folder should, by the nature of git, not need to be backed up or should be done so on the remote server if necessary.
349 
350 The `BYOND` and `Game` folders should never be backed up due.
351 
352 The `Configuration` folder should and database be fully backed up
353 
354 To restore an installation from backups, first restore the instance `Configuration` folders in their new homes. Then restore the database, modifying the `Path` column in the `Instances` table where necessary to point to the new instances. Then start the server pointed at the new database.
355 
356 Should you end up with a lost database for some reason or want to reattach a detached instance you can reattach an existing folder by creating an empty file named `TGS4_ALLOW_INSTANCE_ATTACH` inside it (This is automatically created when detaching instances). Then create a new instance with that path, this will bypass the empty folder check. Note that this will not restore things such as user permissions, server config options, or deployment metadata. Those must be reconfigured manually
357 
358 ## Troubleshooting
359 
360 Feel free to ask for help at the coderbus discord in #tooling-questions: https://discord.gg/Vh8TJp9. Cyberboss#8246 can answer most questions.
361 
362 ## Contributing
363 
364 * See [CONTRIBUTING.md](https://github.com/tgstation/tgstation-server/blob/master/.github/CONTRIBUTING.md)
365 
366 ## Licensing
367 
368 * The DMAPI for the project is licensed under the MIT license.
369 * The /tg/station 13 icon is licensed under [Creative Commons 3.0 BY-SA](http://creativecommons.org/licenses/by-sa/3.0/).
370 * The remainder of the project is licensed under [GNU AGPL v3](http://www.gnu.org/licenses/agpl-3.0.html)
371 
372 See the files in the `/src/DMAPI` tree for the MIT license
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class RateLimitException : ClientException
11  {
12  DateTimeOffset RetryAfter { get; }
13 
17  public RateLimitException() { }
18 
23  public RateLimitException(string secondsString) : base(new ErrorMessage
24  {
25  Message = "GitHub rate limit reached!",
26  SeverApiVersion = null
27  }, (HttpStatusCode)429)
28  {
29  if (Int32.TryParse(secondsString, out var seconds) && seconds >= 0)
30  RetryAfter = DateTimeOffset.Now.AddSeconds(seconds);
31  else
32  RetryAfter = DateTimeOffset.Now;
33  }
34 
40  public RateLimitException(string message, Exception innerException) : base(message, innerException) { }
41  }
42 }
- +Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class RateLimitException : ClientException
11  {
12  DateTimeOffset RetryAfter { get; }
13 
17  public RateLimitException() { }
18 
23  public RateLimitException(string secondsString) : base(new ErrorMessage
24  {
25  Message = "GitHub rate limit reached!",
26  SeverApiVersion = null
27  }, (HttpStatusCode)429)
28  {
29  if (Int32.TryParse(secondsString, out var seconds) && seconds >= 0)
30  RetryAfter = DateTimeOffset.Now.AddSeconds(seconds);
31  else
32  RetryAfter = DateTimeOffset.Now;
33  }
34 
40  public RateLimitException(string message, Exception innerException) : base(message, innerException) { }
41  }
42 }
Occurs when a GitHub rate limit occurs
diff --git a/_release_notes_8_assembly_info_8cs.html b/_release_notes_8_assembly_info_8cs.html index 638a7353fc..85ccdc6409 100644 --- a/_release_notes_8_assembly_info_8cs.html +++ b/_release_notes_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: tools/ReleaseNotes/obj/Release/netcoreapp2.1/ReleaseNotes.AssemblyInfo.cs File Reference +tgstation-server: tools/ReleaseNotes/obj/Debug/netcoreapp2.1/ReleaseNotes.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_release_notes_8_assembly_info_8cs_source.html b/_release_notes_8_assembly_info_8cs_source.html index 2af530f94b..9a5ab05d0e 100644 --- a/_release_notes_8_assembly_info_8cs_source.html +++ b/_release_notes_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tools/ReleaseNotes/obj/Release/netcoreapp2.1/ReleaseNotes.AssemblyInfo.cs Source File +tgstation-server: tools/ReleaseNotes/obj/Debug/netcoreapp2.1/ReleaseNotes.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ReleaseNotes.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("ReleaseNotes")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("ReleaseNotes")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("ReleaseNotes")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("ReleaseNotes")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("ReleaseNotes")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("ReleaseNotes")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
-Go to the documentation of this file.
1 using LibGit2Sharp;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Threading;
5 using System.Threading.Tasks;
8 using Tgstation.Server.Host.IO;
9 
10 namespace Tgstation.Server.Host.Components.Repository
11 {
14  {
16  public bool InUse => semaphore.CurrentCount == 0;
17 
19  public bool CloneInProgress { get; private set; }
20 
25 
30 
35 
39  readonly ILogger<Repository> repositoryLogger;
40 
44  readonly ILogger<RepositoryManager> logger;
45 
50 
54  readonly SemaphoreSlim semaphore;
55 
65  public RepositoryManager(RepositorySettings repositorySettings, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger<Repository> repositoryLogger, ILogger<RepositoryManager> logger)
66  {
67  this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings));
68  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
69  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
70  this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
71  this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger));
72  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  semaphore = new SemaphoreSlim(1);
74  }
75 
77  public void Dispose()
78  {
79  logger.LogTrace("Disposing...");
80  semaphore.Dispose();
81  }
82 
84  public async Task<IRepository> CloneRepository(Uri url, string initialBranch, string username, string password, Action<int> progressReporter, CancellationToken cancellationToken)
85  {
86  if (url == null)
87  throw new ArgumentNullException(nameof(url));
88  if (progressReporter == null)
89  throw new ArgumentNullException(nameof(progressReporter));
90 
91  logger.LogInformation("Begin clone {0} (Branch: {1})", url, initialBranch);
92  lock (this)
93  {
94  if (CloneInProgress)
95  throw new InvalidOperationException("The repository is already being cloned!");
96  CloneInProgress = true;
97  }
98  try
99  {
100  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
101  {
102  logger.LogTrace("Semaphore acquired");
103  if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
104  try
105  {
106  await Task.Factory.StartNew(() =>
107  {
108  string path = null;
109  try
110  {
111  path = LibGit2Sharp.Repository.Clone(url.ToString(), ioManager.ResolvePath("."), new CloneOptions
112  {
113  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
114  OnTransferProgress = (a) =>
115  {
116  var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
117  progressReporter((int)percentage);
118  return !cancellationToken.IsCancellationRequested;
119  },
120  RecurseSubmodules = true,
121  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
122  RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
123  BranchName = initialBranch,
124  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
125  });
126  }
127  catch (UserCancelledException) { }
128  cancellationToken.ThrowIfCancellationRequested();
129  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
130  }
131  catch
132  {
133  try
134  {
135  logger.LogTrace("Deleting partially cloned repository...");
136  await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
137  }
138  catch (Exception e)
139  {
140  logger.LogDebug("Error deleting partially cloned repository! Exception: {0}", e);
141  }
142  throw;
143  }
144  else
145  {
146  logger.LogDebug("Repository exists, clone aborted!");
147  return null;
148  }
149  }
150  logger.LogInformation("Clone complete!");
151  }
152  finally
153  {
154  CloneInProgress = false;
155  }
156  return await LoadRepository(cancellationToken).ConfigureAwait(false);
157  }
158 
160  public async Task<IRepository> LoadRepository(CancellationToken cancellationToken)
161  {
162  logger.LogTrace("Begin LoadRepository...");
163  lock (this)
164  if (CloneInProgress)
165  throw new InvalidOperationException("The repository is being cloned!");
166  await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
167  LibGit2Sharp.Repository repo = null;
168  await Task.Factory.StartNew(() =>
169  {
170  try
171  {
172  logger.LogTrace("Creating LibGit2Sharp.Repository...");
173  repo = new LibGit2Sharp.Repository(ioManager.ResolvePath("."));
174  }
175  catch (RepositoryNotFoundException e)
176  {
177  logger.LogDebug("Repository not found!");
178  logger.LogTrace("Exception: {0}", e);
179  }
180  catch
181  {
182  semaphore.Release();
183  throw;
184  }
185  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
186  if (repo == null)
187  {
188  semaphore.Release();
189  return null;
190  }
191  return new Repository(repo, ioManager, eventConsumer, credentialsProvider, repositoryLogger, () =>
192  {
193  logger.LogTrace("Releasing semaphore due to Repository disposal...");
194  semaphore.Release();
195  });
196  }
197 
199  public async Task DeleteRepository(CancellationToken cancellationToken)
200  {
201  logger.LogInformation("Deleting repository...");
202  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
203  {
204  logger.LogTrace("Semaphore acquired, deleting Repository directory...");
205  await ioManager.DeleteDirectory(".", cancellationToken).ConfigureAwait(false);
206  }
207  }
208  }
209 }
-
Represents configurable settings for a Repository
+Go to the documentation of this file.
1 using LibGit2Sharp;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Threading;
5 using System.Threading.Tasks;
8 using Tgstation.Server.Host.IO;
9 
10 namespace Tgstation.Server.Host.Components.Repository
11 {
14  {
16  public bool InUse => semaphore.CurrentCount == 0;
17 
19  public bool CloneInProgress { get; private set; }
20 
25 
30 
35 
39  readonly ILogger<Repository> repositoryLogger;
40 
44  readonly ILogger<RepositoryManager> logger;
45 
50 
54  readonly SemaphoreSlim semaphore;
55 
65  public RepositoryManager(RepositorySettings repositorySettings, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger<Repository> repositoryLogger, ILogger<RepositoryManager> logger)
66  {
67  this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings));
68  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
69  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
70  this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
71  this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger));
72  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  semaphore = new SemaphoreSlim(1);
74  }
75 
77  public void Dispose()
78  {
79  logger.LogTrace("Disposing...");
80  semaphore.Dispose();
81  }
82 
84  public async Task<IRepository> CloneRepository(Uri url, string initialBranch, string username, string password, Action<int> progressReporter, CancellationToken cancellationToken)
85  {
86  if (url == null)
87  throw new ArgumentNullException(nameof(url));
88  if (progressReporter == null)
89  throw new ArgumentNullException(nameof(progressReporter));
90 
91  logger.LogInformation("Begin clone {0} (Branch: {1})", url, initialBranch);
92  lock (this)
93  {
94  if (CloneInProgress)
95  throw new InvalidOperationException("The repository is already being cloned!");
96  CloneInProgress = true;
97  }
98  try
99  {
100  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
101  {
102  logger.LogTrace("Semaphore acquired");
103  if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
104  try
105  {
106  await Task.Factory.StartNew(() =>
107  {
108  string path = null;
109  try
110  {
111  path = LibGit2Sharp.Repository.Clone(url.ToString(), ioManager.ResolvePath("."), new CloneOptions
112  {
113  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
114  OnTransferProgress = (a) =>
115  {
116  var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
117  progressReporter((int)percentage);
118  return !cancellationToken.IsCancellationRequested;
119  },
120  RecurseSubmodules = true,
121  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
122  RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
123  BranchName = initialBranch,
124  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
125  });
126  }
127  catch (UserCancelledException) { }
128  cancellationToken.ThrowIfCancellationRequested();
129  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
130  }
131  catch
132  {
133  try
134  {
135  logger.LogTrace("Deleting partially cloned repository...");
136  await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
137  }
138  catch (Exception e)
139  {
140  logger.LogDebug("Error deleting partially cloned repository! Exception: {0}", e);
141  }
142  throw;
143  }
144  else
145  {
146  logger.LogDebug("Repository exists, clone aborted!");
147  return null;
148  }
149  }
150  logger.LogInformation("Clone complete!");
151  }
152  finally
153  {
154  CloneInProgress = false;
155  }
156  return await LoadRepository(cancellationToken).ConfigureAwait(false);
157  }
158 
160  public async Task<IRepository> LoadRepository(CancellationToken cancellationToken)
161  {
162  logger.LogTrace("Begin LoadRepository...");
163  lock (this)
164  if (CloneInProgress)
165  throw new InvalidOperationException("The repository is being cloned!");
166  await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
167  LibGit2Sharp.Repository repo = null;
168  await Task.Factory.StartNew(() =>
169  {
170  try
171  {
172  logger.LogTrace("Creating LibGit2Sharp.Repository...");
173  repo = new LibGit2Sharp.Repository(ioManager.ResolvePath("."));
174  }
175  catch (RepositoryNotFoundException e)
176  {
177  logger.LogDebug("Repository not found!");
178  logger.LogTrace("Exception: {0}", e);
179  }
180  catch
181  {
182  semaphore.Release();
183  throw;
184  }
185  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
186  if (repo == null)
187  {
188  semaphore.Release();
189  return null;
190  }
191  return new Repository(repo, ioManager, eventConsumer, credentialsProvider, repositoryLogger, () =>
192  {
193  logger.LogTrace("Releasing semaphore due to Repository disposal...");
194  semaphore.Release();
195  });
196  }
197 
199  public async Task DeleteRepository(CancellationToken cancellationToken)
200  {
201  logger.LogInformation("Deleting repository...");
202  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
203  {
204  logger.LogTrace("Semaphore acquired, deleting Repository directory...");
205  await ioManager.DeleteDirectory(".", cancellationToken).ConfigureAwait(false);
206  }
207  }
208  }
209 }
Represents configurable settings for a Repository
Consumes EventTypes and takes the appropriate actions
diff --git a/_request_timeout_exception_8cs_source.html b/_request_timeout_exception_8cs_source.html index 6a599cdf1e..4fe6d43046 100644 --- a/_request_timeout_exception_8cs_source.html +++ b/_request_timeout_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RequestTimeoutException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
11  {
16  {
17  Message = "The request timed out!",
18  SeverApiVersion = null
19  }, HttpStatusCode.RequestTimeout)
20  { }
21 
26  public RequestTimeoutException(string message) : base(message) { }
27 
33  public RequestTimeoutException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
-
RequestTimeoutException()
Construct an RequestTimeoutException
+Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
11  {
16  {
17  Message = "The request timed out!",
18  SeverApiVersion = null
19  }, HttpStatusCode.RequestTimeout)
20  { }
21 
26  public RequestTimeoutException(string message) : base(message) { }
27 
33  public RequestTimeoutException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
RequestTimeoutException()
Construct an RequestTimeoutException
diff --git a/_server_8cs_source.html b/_server_8cs_source.html index e24e7e710f..c8b33b1f8e 100644 --- a/_server_8cs_source.html +++ b/_server_8cs_source.html @@ -92,36 +92,36 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Server.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.DependencyInjection;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.IO;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host
14 {
16 #pragma warning disable CA1001 // Types that own disposable fields should be disposable
17  sealed class Server : IServer, IServerControl
18 #pragma warning restore CA1001 // Types that own disposable fields should be disposable
19  {
21  public bool RestartRequested { get; private set; }
22 
24  public bool WatchdogPresent => updatePath != null;
25 
29  readonly IWebHostBuilder webHostBuilder;
30 
34  readonly List<IRestartHandler> restartHandlers;
35 
39  readonly string updatePath;
40 
44  ILogger<Server> logger;
45 
49  CancellationTokenSource cancellationTokenSource;
50 
54  bool updating;
55 
61  public Server(IWebHostBuilder webHostBuilder, string updatePath)
62  {
63  this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder));
64  this.updatePath = updatePath;
65 
66  restartHandlers = new List<IRestartHandler>();
67  }
68 
73  void CheckSanity(bool checkWatchdog)
74  {
75  if (checkWatchdog && !WatchdogPresent)
76  throw new InvalidOperationException("Server restarts are not supported");
77 
78  if (cancellationTokenSource == null || logger == null)
79  throw new InvalidOperationException("Tried to control a non-running Server!");
80  }
81 
83  public async Task RunAsync(CancellationToken cancellationToken)
84  {
85  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
86  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
87  {
88  if (fsWatcher != null)
89  {
90  fsWatcher.Created += (a, b) =>
91  {
92  if (b.FullPath == updatePath && File.Exists(b.FullPath))
93  cancellationTokenSource.Cancel();
94  };
95  fsWatcher.EnableRaisingEvents = true;
96  }
97  using (var webHost = webHostBuilder
98  .UseStartup<Application>()
99  .ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this))
100  .Build()
101  )
102  {
103  logger = webHost.Services.GetRequiredService<ILogger<Server>>();
104  await webHost.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
105  }
106  }
107  }
108 
110  public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
111  {
112  if (version == null)
113  throw new ArgumentNullException(nameof(version));
114  if (updateZipUrl == null)
115  throw new ArgumentNullException(nameof(updateZipUrl));
116  if (ioManager == null)
117  throw new ArgumentNullException(nameof(ioManager));
118 
119  CheckSanity(true);
120 
121  logger.LogTrace("Begin ApplyUpdate...");
122 
123  lock (this)
124  {
125  if (updating || RestartRequested)
126  {
127  logger.LogTrace("Aborted due to concurrency conflict!");
128  return false;
129  }
130  updating = true;
131  }
132 
133  async void RunUpdate()
134  {
135  try
136  {
137  logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl);
138 
139  if (cancellationTokenSource == null)
140  throw new InvalidOperationException("Tried to update a non-running Server!");
141  var cancellationToken = cancellationTokenSource.Token;
142 
143  logger.LogTrace("Downloading zip package...");
144  var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false);
145 
146  try
147  {
148  logger.LogTrace("Exctracting zip package to {0}...", updatePath);
149  await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false);
150  }
151  catch (Exception e)
152  {
153  updating = false;
154  try
155  {
156  //important to not leave this directory around if possible
157  await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false);
158  }
159  catch (Exception e2)
160  {
161  throw new AggregateException(e, e2);
162  }
163  throw;
164  }
165 
166  await Restart(version).ConfigureAwait(false);
167  }
168  catch (OperationCanceledException)
169  {
170  logger.LogInformation("Server update cancelled!");
171  }
172  catch (Exception e)
173  {
174  logger.LogError("Error updating server! Exception: {0}", e);
175  }
176  finally
177  {
178  updating = false;
179  }
180  }
181 
182  RunUpdate();
183  return true;
184  }
185 
188  {
189  if (handler == null)
190  throw new ArgumentNullException(nameof(handler));
191 
192  CheckSanity(false);
193 
194  lock (this)
195  if (!RestartRequested)
196  {
197  logger.LogTrace("Registering restart handler {0}...", handler);
198  restartHandlers.Add(handler);
199  return new RestartRegistration(() =>
200  {
201  lock (this)
202  if (!RestartRequested)
203  restartHandlers.Remove(handler);
204  });
205  }
206  return new RestartRegistration(() => { });
207  }
208 
210  public Task Restart() => Restart(null);
211 
217  async Task Restart(Version newVersion)
218  {
219  CheckSanity(true);
220 
221  logger.LogTrace("Begin Restart...");
222 
223  lock (this)
224  {
225  if ((updating && newVersion == null) || RestartRequested)
226  {
227  logger.LogTrace("Aborted due to concurrency conflict!");
228  return;
229  }
230  RestartRequested = true;
231  }
232 
233  logger.LogInformation("Restarting server...");
234 
235  using (var cts = new CancellationTokenSource())
236  {
237  logger.LogTrace("Running restart handlers...");
238  var cancellationToken = cts.Token;
239  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
240  //YA GOT 10 SECONDS
241  var expiryTask = Task.Delay(TimeSpan.FromSeconds(10));
242  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
243  logger.LogTrace("Joining restart handlers...");
244  cts.Cancel();
245  try
246  {
247  await eventsTask.ConfigureAwait(false);
248  }
249  catch (OperationCanceledException) { }
250  catch (Exception e)
251  {
252  logger.LogError("Restart handlers error! Exception: {0}", e);
253  }
254  }
255 
256  logger.LogTrace("Stopping host...");
257  cancellationTokenSource.Cancel();
258  }
259  }
260 }
-
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.DependencyInjection;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.IO;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host
14 {
16 #pragma warning disable CA1001 // Types that own disposable fields should be disposable
17  sealed class Server : IServer, IServerControl
18 #pragma warning restore CA1001 // Types that own disposable fields should be disposable
19  {
21  public bool RestartRequested { get; private set; }
22 
24  public bool WatchdogPresent => updatePath != null;
25 
29  readonly IWebHostBuilder webHostBuilder;
30 
34  readonly List<IRestartHandler> restartHandlers;
35 
39  readonly string updatePath;
40 
44  ILogger<Server> logger;
45 
49  CancellationTokenSource cancellationTokenSource;
50 
55 
59  bool updating;
60 
66  public Server(IWebHostBuilder webHostBuilder, string updatePath)
67  {
68  this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder));
69  this.updatePath = updatePath;
70 
71  webHostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
72 
73  restartHandlers = new List<IRestartHandler>();
74  }
75 
80  void CheckSanity(bool checkWatchdog)
81  {
82  if (checkWatchdog && !WatchdogPresent && propagatedException == null)
83  throw new InvalidOperationException("Server restarts are not supported");
84 
85  if (cancellationTokenSource == null || logger == null)
86  throw new InvalidOperationException("Tried to control a non-running Server!");
87  }
88 
93  {
94  if (propagatedException != null)
95  throw propagatedException;
96  }
97 
99  public async Task RunAsync(CancellationToken cancellationToken)
100  {
101  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
102  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
103  {
104  if (fsWatcher != null)
105  {
106  fsWatcher.Created += (a, b) =>
107  {
108  if (b.FullPath == updatePath && File.Exists(b.FullPath))
109  cancellationTokenSource.Cancel();
110  };
111  fsWatcher.EnableRaisingEvents = true;
112  }
113 
114  using (var webHost = webHostBuilder.Build())
115  try
116  {
117  logger = webHost.Services.GetRequiredService<ILogger<Server>>();
118  await webHost.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
119  }
120  catch (OperationCanceledException)
121  {
122  CheckExceptionPropagation();
123  throw;
124  }
125  }
126  CheckExceptionPropagation();
127  }
128 
130  public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
131  {
132  if (version == null)
133  throw new ArgumentNullException(nameof(version));
134  if (updateZipUrl == null)
135  throw new ArgumentNullException(nameof(updateZipUrl));
136  if (ioManager == null)
137  throw new ArgumentNullException(nameof(ioManager));
138 
139  CheckSanity(true);
140 
141  logger.LogTrace("Begin ApplyUpdate...");
142 
143  lock (this)
144  {
145  if (updating || RestartRequested)
146  {
147  logger.LogTrace("Aborted due to concurrency conflict!");
148  return false;
149  }
150  updating = true;
151  }
152 
153  async void RunUpdate()
154  {
155  try
156  {
157  logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl);
158 
159  if (cancellationTokenSource == null)
160  throw new InvalidOperationException("Tried to update a non-running Server!");
161  var cancellationToken = cancellationTokenSource.Token;
162 
163  logger.LogTrace("Downloading zip package...");
164  var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false);
165 
166  try
167  {
168  logger.LogTrace("Exctracting zip package to {0}...", updatePath);
169  await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false);
170  }
171  catch (Exception e)
172  {
173  updating = false;
174  try
175  {
176  //important to not leave this directory around if possible
177  await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false);
178  }
179  catch (Exception e2)
180  {
181  throw new AggregateException(e, e2);
182  }
183  throw;
184  }
185 
186  await Restart(version, null).ConfigureAwait(false);
187  }
188  catch (OperationCanceledException)
189  {
190  logger.LogInformation("Server update cancelled!");
191  }
192  catch (Exception e)
193  {
194  logger.LogError("Error updating server! Exception: {0}", e);
195  }
196  finally
197  {
198  updating = false;
199  }
200  }
201 
202  RunUpdate();
203  return true;
204  }
205 
208  {
209  if (handler == null)
210  throw new ArgumentNullException(nameof(handler));
211 
212  CheckSanity(false);
213 
214  lock (this)
215  if (!RestartRequested)
216  {
217  logger.LogTrace("Registering restart handler {0}...", handler);
218  restartHandlers.Add(handler);
219  return new RestartRegistration(() =>
220  {
221  lock (this)
222  if (!RestartRequested)
223  restartHandlers.Remove(handler);
224  });
225  }
226  return new RestartRegistration(() => { });
227  }
228 
230  public Task Restart() => Restart(null, null);
231 
238  async Task Restart(Version newVersion, Exception exception)
239  {
240  CheckSanity(true);
241 
242  logger.LogTrace("Begin Restart...");
243 
244  lock (this)
245  {
246  if ((updating && newVersion == null) || RestartRequested)
247  {
248  logger.LogTrace("Aborted due to concurrency conflict!");
249  return;
250  }
251  RestartRequested = true;
252  propagatedException = exception;
253  }
254 
255  if (exception == null)
256  using (var cts = new CancellationTokenSource())
257  {
258  logger.LogInformation("Restarting server...");
259  var cancellationToken = cts.Token;
260  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
261  //YA GOT 10 SECONDS
262  var expiryTask = Task.Delay(TimeSpan.FromSeconds(10));
263  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
264  logger.LogTrace("Joining restart handlers...");
265  cts.Cancel();
266  try
267  {
268  await eventsTask.ConfigureAwait(false);
269  }
270  catch (OperationCanceledException) { }
271  catch (Exception e)
272  {
273  logger.LogError("Restart handlers error! Exception: {0}", e);
274  }
275  }
276 
277  logger.LogTrace("Stopping host...");
278  cancellationTokenSource.Cancel();
279  }
280 
282  public Task Die(Exception exception) => Restart(null, exception);
283  }
284 }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
-
async Task RunAsync(CancellationToken cancellationToken)
Runs the IServer
Definition: Server.cs:83
-
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:110
+
async Task RunAsync(CancellationToken cancellationToken)
Runs the IServer
Definition: Server.cs:99
+
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:130
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts
Definition: Server.cs:34
+
async Task Restart(Version newVersion, Exception exception)
Implements Restart()
Definition: Server.cs:238
Task< byte[]> DownloadFile(Uri url, CancellationToken cancellationToken)
Downloads a file from url
-
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
Definition: Server.cs:187
+
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
Definition: Server.cs:207
Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken)
Extract a set of zipFileBytes to a given path
- +
void CheckExceptionPropagation()
Re-throw propagatedException if it exists
Definition: Server.cs:92
readonly string updatePath
The absolute path to install updates to
Definition: Server.cs:39
Represents the host
Definition: IServer.cs:9
-
Server(IWebHostBuilder webHostBuilder, string updatePath)
Construct a Server
Definition: Server.cs:61
+
Server(IWebHostBuilder webHostBuilder, string updatePath)
Construct a Server
Definition: Server.cs:66
CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the Server
Definition: Server.cs:49
-
bool updating
If a server update has been or is being applied
Definition: Server.cs:54
+
bool updating
If a server update has been or is being applied
Definition: Server.cs:59
+
Exception propagatedException
The Exception to propagate when the server terminates
Definition: Server.cs:54
Interface for using filesystems
Definition: IIOManager.cs:11
-
async Task Restart(Version newVersion)
Implements Restart()
Definition: Server.cs:217
Represents the lifetime of a IRestartHandler registration
readonly IWebHostBuilder webHostBuilder
The IWebHostBuilder for the Server
Definition: Server.cs:29
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
-
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used
Definition: Server.cs:73
+
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used
Definition: Server.cs:80
diff --git a/_server_error_exception_8cs_source.html b/_server_error_exception_8cs_source.html index d4ced35b44..414629c949 100644 --- a/_server_error_exception_8cs_source.html +++ b/_server_error_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServerErrorException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class ServerErrorException : ClientException
11  {
15  public string Html { get; }
16 
20  public ServerErrorException() { }
21 
26  public ServerErrorException(string html) : base(new ErrorMessage
27  {
28  Message = "An internal server error occurred!",
29  SeverApiVersion = null
30  }, HttpStatusCode.InternalServerError)
31  {
32  Html = html;
33  }
34 
40  public ServerErrorException(string message, Exception innerException) : base(message, innerException) { }
41  }
42 }
- +Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class ServerErrorException : ClientException
11  {
15  public string Html { get; }
16 
20  public ServerErrorException() { }
21 
26  public ServerErrorException(string html) : base(new ErrorMessage
27  {
28  Message = "An internal server error occurred!",
29  SeverApiVersion = null
30  }, HttpStatusCode.InternalServerError)
31  {
32  Html = html;
33  }
34 
40  public ServerErrorException(string message, Exception innerException) : base(message, innerException) { }
41  }
42 }
ServerErrorException(string message, Exception innerException)
Construct an ServerErrorException with a message and innerException
diff --git a/_server_factory_8cs_source.html b/_server_factory_8cs_source.html index 8e8d746199..68962d13be 100644 --- a/_server_factory_8cs_source.html +++ b/_server_factory_8cs_source.html @@ -92,12 +92,17 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServerFactory.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore;
2 using System;
3 
4 namespace Tgstation.Server.Host
5 {
7  public sealed class ServerFactory : IServerFactory
8  {
10  public IServer CreateServer(string[] args, string updatePath) => new Server(WebHost.CreateDefaultBuilder(args ?? throw new ArgumentNullException(nameof(args))), updatePath);
11  }
12 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore;
2 using Microsoft.AspNetCore.Hosting;
3 using Microsoft.Extensions.Configuration;
4 using System;
5 using System.IO;
6 using System.Reflection;
8 
9 namespace Tgstation.Server.Host
10 {
12  public sealed class ServerFactory : IServerFactory
13  {
15  public IServer CreateServer(string[] args, string updatePath) {
16 
17  var webHost = WebHost.CreateDefaultBuilder(args ?? throw new ArgumentNullException(nameof(args)))
18  .ConfigureAppConfiguration((context, configurationBuilder) => configurationBuilder.SetBasePath(Directory.GetCurrentDirectory()))
19  .UseStartup<Application>()
20  .SuppressStatusMessages(true)
21  .UseShutdownTimeout(TimeSpan.FromMinutes(1));
22 
23  if(updatePath != null)
24  webHost.UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
25 
26  return new Server(webHost, updatePath);
27  }
28  }
29 }
- + + +
Represents the host
Definition: IServer.cs:9
+
IServer CreateServer(string[] args, string updatePath)
Create a IServer
+ +
diff --git a/_server_service_8cs_source.html b/_server_service_8cs_source.html index cfee5334e1..045897ffdd 100644 --- a/_server_service_8cs_source.html +++ b/_server_service_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServerService.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.Extensions.Logging.EventLog;
3 using Microsoft.Extensions.Logging.EventLog.Internal;
4 using System;
5 using System.Diagnostics;
6 using System.Diagnostics.CodeAnalysis;
7 using System.Globalization;
8 using System.ServiceProcess;
9 using System.Threading;
10 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Service
14 {
19  {
23  public const string Name = "tgstation-server-4";
24 
28  readonly IWatchdog watchdog;
29 
34 
38  CancellationTokenSource cancellationTokenSource;
39 
46  public ServerService(IWatchdogFactory watchdogFactory, ILoggerFactory loggerFactory, LogLevel minumumLogLevel)
47  {
48  if (watchdogFactory == null)
49  throw new ArgumentNullException(nameof(watchdogFactory));
50  if (loggerFactory == null)
51  throw new ArgumentNullException(nameof(loggerFactory));
52 
53  loggerFactory.AddEventLog(new EventLogSettings
54  {
55  EventLog = this,
56  Filter = (message, logLevel) => logLevel >= minumumLogLevel
57  });
58 
59  ServiceName = Name;
60  watchdog = watchdogFactory.CreateWatchdog(loggerFactory);
61  }
62 
64  public int MaxMessageSize => (int)EventLog.MaximumKilobytes * 1024;
65 
67  public void WriteEntry(string message, EventLogEntryType type, int eventID, short category) => EventLog.WriteEntry(message, type, eventID, category);
68 
75  async Task RunWatchdog(string[] args, CancellationToken cancellationToken)
76  {
77  await watchdog.RunAsync(false, args, cancellationTokenSource.Token).ConfigureAwait(false);
78 
79  void StopServiceAsync()
80  {
81  try
82  {
83  Task.Run(Stop, cancellationToken);
84  }
85  catch (OperationCanceledException) { }
86  catch (Exception e)
87  {
88  EventLog.WriteEntry(String.Format(CultureInfo.InvariantCulture, "Error stopping service! Exception: {0}", e));
89  }
90  }
91  StopServiceAsync();
92  }
93 
95  [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "cancellationTokenSource")]
96  protected override void Dispose(bool disposing)
97  {
98  cancellationTokenSource?.Dispose();
99  base.Dispose(disposing);
100  }
101 
103  protected override void OnStart(string[] args)
104  {
105  cancellationTokenSource?.Dispose();
106  cancellationTokenSource = new CancellationTokenSource();
107  watchdogTask = RunWatchdog(args, cancellationTokenSource.Token);
108  }
109 
111  protected override void OnStop()
112  {
113  cancellationTokenSource.Cancel();
114  watchdogTask.GetAwaiter().GetResult();
115  }
116  }
117 }
-
async Task RunWatchdog(string[] args, CancellationToken cancellationToken)
Executes the watchdog, stopping the service if it exits
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.Extensions.Logging.EventLog;
3 using Microsoft.Extensions.Logging.EventLog.Internal;
4 using System;
5 using System.Diagnostics;
6 using System.Diagnostics.CodeAnalysis;
7 using System.Globalization;
8 using System.ServiceProcess;
9 using System.Threading;
10 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Service
14 {
19  {
23  public const string Name = "tgstation-server-4";
24 
28  readonly IWatchdog watchdog;
29 
34 
38  CancellationTokenSource cancellationTokenSource;
39 
46  public ServerService(IWatchdogFactory watchdogFactory, ILoggerFactory loggerFactory, LogLevel minumumLogLevel)
47  {
48  if (watchdogFactory == null)
49  throw new ArgumentNullException(nameof(watchdogFactory));
50  if (loggerFactory == null)
51  throw new ArgumentNullException(nameof(loggerFactory));
52 
53  loggerFactory.AddEventLog(new EventLogSettings
54  {
55  EventLog = this,
56  Filter = (message, logLevel) => logLevel >= minumumLogLevel
57  });
58 
59  ServiceName = Name;
60  watchdog = watchdogFactory.CreateWatchdog(loggerFactory);
61  }
62 
64  public int MaxMessageSize => (int)EventLog.MaximumKilobytes * 1024;
65 
67  public void WriteEntry(string message, EventLogEntryType type, int eventID, short category) => EventLog.WriteEntry(message, type, eventID, category);
68 
75  async Task RunWatchdog(string[] args, CancellationToken cancellationToken)
76  {
77  await watchdog.RunAsync(false, args, cancellationToken).ConfigureAwait(false);
78 
79  void StopServiceAsync()
80  {
81  try
82  {
83  Task.Run(Stop, cancellationToken);
84  }
85  catch (OperationCanceledException) { }
86  catch (Exception e)
87  {
88  EventLog.WriteEntry(String.Format(CultureInfo.InvariantCulture, "Error stopping service! Exception: {0}", e));
89  }
90  }
91  StopServiceAsync();
92  }
93 
95  [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "cancellationTokenSource")]
96  protected override void Dispose(bool disposing)
97  {
98  cancellationTokenSource?.Dispose();
99  base.Dispose(disposing);
100  }
101 
103  protected override void OnStart(string[] args)
104  {
105  cancellationTokenSource?.Dispose();
106  cancellationTokenSource = new CancellationTokenSource();
107  watchdogTask = RunWatchdog(args, cancellationTokenSource.Token);
108  }
109 
111  protected override void OnStop()
112  {
113  cancellationTokenSource.Cancel();
114  watchdogTask.GetAwaiter().GetResult();
115  }
116  }
117 }
async Task RunWatchdog(string[] args, CancellationToken cancellationToken)
Executes the watchdog, stopping the service if it exits
IWatchdog CreateWatchdog(ILoggerFactory loggerFactory)
Create a IWatchdog
override void OnStart(string[] args)
diff --git a/_service_collection_extensions_8cs.html b/_service_collection_extensions_8cs.html new file mode 100644 index 0000000000..61261d6947 --- /dev/null +++ b/_service_collection_extensions_8cs.html @@ -0,0 +1,120 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Core/ServiceCollectionExtensions.cs File Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
ServiceCollectionExtensions.cs File Reference
+
+
+ +

Go to the source code of this file.

+ + + + + +

+Classes

class  Tgstation.Server.Host.Core.ServiceCollectionExtensions
 Extensions for IServiceCollection More...
 
+ + + +

+Namespaces

namespace  Tgstation.Server.Host.Core
 
+
+ + + + diff --git a/_service_collection_extensions_8cs_source.html b/_service_collection_extensions_8cs_source.html new file mode 100644 index 0000000000..a31815b0c6 --- /dev/null +++ b/_service_collection_extensions_8cs_source.html @@ -0,0 +1,112 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Core/ServiceCollectionExtensions.cs Source File + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
ServiceCollectionExtensions.cs
+
+
+Go to the documentation of this file.
1 using Microsoft.Extensions.Configuration;
2 using Microsoft.Extensions.DependencyInjection;
3 using System;
4 using System.Globalization;
6 
7 namespace Tgstation.Server.Host.Core
8 {
13  {
21  public static IServiceCollection UseStandardConfig<TConfig>(this IServiceCollection serviceCollection, IConfiguration configuration) where TConfig: class
22  {
23  if (serviceCollection == null)
24  throw new ArgumentNullException(nameof(serviceCollection));
25  if (configuration == null)
26  throw new ArgumentNullException(nameof(configuration));
27 
28  const string SectionFieldName = nameof(GeneralConfiguration.Section);
29 
30  var configType = typeof(TConfig);
31  var sectionField = configType.GetField(SectionFieldName);
32  if (sectionField == null)
33  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has no {1} field!", configType, SectionFieldName));
34 
35  var stringType = typeof(string);
36  if (sectionField.FieldType != stringType)
37  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has invalid {1} field type, must be {2}!", configType, SectionFieldName, stringType));
38 
39  var sectionName = (string)sectionField.GetValue(null);
40 
41  return serviceCollection.Configure<TConfig>(configuration.GetSection(sectionName));
42  }
43  }
44 }
+ +
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
+ + + + + + +
+ + + + diff --git a/_service_unavailable_exception_8cs_source.html b/_service_unavailable_exception_8cs_source.html index 9308fa3e3c..1dcd155763 100644 --- a/_service_unavailable_exception_8cs_source.html +++ b/_service_unavailable_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServiceUnavailableException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
11  {
16  {
17  Message = "The server is currently starting or stopping and is unable to process the request!",
18  SeverApiVersion = null
19  }, HttpStatusCode.ServiceUnavailable)
20  { }
21 
26  public ServiceUnavailableException(string message) : base(message) { }
27 
33  public ServiceUnavailableException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
- +Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
11  {
16  {
17  Message = "The server is currently starting or stopping and is unable to process the request!",
18  SeverApiVersion = null
19  }, HttpStatusCode.ServiceUnavailable)
20  { }
21 
26  public ServiceUnavailableException(string message) : base(message) { }
27 
33  public ServiceUnavailableException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
diff --git a/_session_controller_8cs_source.html b/_session_controller_8cs_source.html index 60f23e9727..11b4ff52f6 100644 --- a/_session_controller_8cs_source.html +++ b/_session_controller_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SessionController.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Newtonsoft.Json.Serialization;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Net;
9 using System.Threading;
10 using System.Threading.Tasks;
16 
17 namespace Tgstation.Server.Host.Components.Watchdog
18 {
21  {
23  public bool IsPrimary
24  {
25  get
26  {
27  CheckDisposed();
28  return reattachInformation.IsPrimary;
29  }
30  }
31 
34  {
35  get
36  {
37  if (!Lifetime.IsCompleted)
38  throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
39  return apiValidationStatus;
40  }
41  }
42 
44  public IDmbProvider Dmb
45  {
46  get
47  {
48  CheckDisposed();
49  return reattachInformation.Dmb;
50  }
51  }
52 
54  public ushort? Port
55  {
56  get
57  {
58  CheckDisposed();
59  if (portClosedForReboot)
60  return null;
61  return reattachInformation.Port;
62  }
63  }
64 
67  {
68  get
69  {
70  CheckDisposed();
71  return reattachInformation.RebootState;
72  }
73  }
74 
76  public bool ClosePortOnReboot { get; set; }
77 
79  public bool TerminationWasRequested { get; private set; }
80 
82  public Task<LaunchResult> LaunchResult { get; }
83 
85  public Task<int> Lifetime => process.Lifetime;
86 
88  public Task OnReboot => rebootTcs.Task;
89 
94 
98  readonly IByondTopicSender byondTopicSender;
99 
104 
108  readonly IProcess process;
109 
114 
119 
123  readonly IChat chat;
124 
128  readonly ILogger<SessionController> logger;
129 
134 
138  TaskCompletionSource<bool> portAssignmentTcs;
142  ushort? nextPort;
143 
147  TaskCompletionSource<object> rebootTcs;
148 
153 
157  bool disposed;
158 
163 
167  bool released;
168 
182  public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger<SessionController> logger, DreamDaemonSecurity? launchSecurityLevel, uint? startupTimeout)
183  {
184  this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid
185  this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
186  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
187  this.process = process ?? throw new ArgumentNullException(nameof(process));
188  this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
189  this.interopContext = interopContext ?? throw new ArgumentNullException(nameof(interopContext));
190  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
191  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
192 
193  this.launchSecurityLevel = launchSecurityLevel;
194 
195  interopContext.RegisterHandler(this);
196 
197  portClosedForReboot = false;
198  disposed = false;
199  apiValidationStatus = ApiValidationStatus.NeverValidated;
200  released = false;
201 
202  rebootTcs = new TaskCompletionSource<object>();
203 
204  process.Lifetime.ContinueWith(x => chatJsonTrackingContext.Active = false, TaskScheduler.Current);
205 
206  async Task<LaunchResult> GetLaunchResult()
207  {
208  var startTime = DateTimeOffset.Now;
209  Task toAwait = process.Startup;
210 
211  if (startupTimeout.HasValue)
212  toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
213 
214  await toAwait.ConfigureAwait(false);
215 
216  var result = new LaunchResult
217  {
218  ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
219  StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
220  };
221  return result;
222  };
223  LaunchResult = GetLaunchResult();
224 
225  logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port);
226  }
227 
232 #pragma warning disable CA1821 // Remove empty Finalizers //TODO remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
233  ~SessionController() => Dispose(false);
234 #pragma warning restore CA1821 // Remove empty Finalizers
235 
237  public void Dispose()
238  {
239  Dispose(true);
240  GC.SuppressFinalize(this);
241  }
242 
244  void Dispose(bool disposing)
245  {
246  lock (this)
247  {
248  if (disposed)
249  return;
250  if (disposing)
251  {
252  if (!released)
253  {
254  process.Terminate();
255  byondLock.Dispose();
256  }
257  process.Dispose();
258  interopContext.Dispose();
259  Dmb?.Dispose(); //will be null when released
260  chatJsonTrackingContext.Dispose();
261  disposed = true;
262  }
263  else
264  {
265  if (logger != null)
266  logger.LogError("Being disposed via finalizer!");
267  if (!released)
268  if (process != null)
269  process.Terminate();
270  else if (logger != null)
271  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
272  }
273  }
274  }
275 
277  public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
278  {
279  if (command == null)
280  throw new ArgumentNullException(nameof(command));
281 
282  var query = command.Parameters;
283 
284  object content;
285  Action postRespond = null;
286  ushort? overrideResponsePort = null;
287  if (query.TryGetValue(Constants.DMParameterCommand, out var method))
288  {
289  content = new object();
290  switch (method)
291  {
293  try
294  {
295  var message = JsonConvert.DeserializeObject<Response>(command.RawJson, new JsonSerializerSettings
296  {
297  ContractResolver = new CamelCasePropertyNamesContractResolver()
298  });
299  if (message.ChannelIds == null)
300  throw new InvalidOperationException("Missing ChannelIds field!");
301  if (message.Message == null)
302  throw new InvalidOperationException("Missing Message field!");
303  await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false);
304  }
305  catch (Exception e)
306  {
307  logger.LogDebug("Exception while decoding chat message! Exception: {0}", e);
308  goto default;
309  }
310  break;
312  //currently unused, maybe in the future
313  break;
315  TerminationWasRequested = true;
316  process.Terminate();
317  return;
319  lock (this)
320  {
321  if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort))
322  {
324  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
325  content = new ErrorMessage { Message = "Missing stringified port as data parameter!" };
326  break;
327  }
328 
329  if (!nextPort.HasValue)
330  //not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
331  reattachInformation.Port = currentPort;
332  else
333  {
334  //nextPort is ready, tell DD to switch to that
335  //if it fails it'll kill itself
336  content = new Dictionary<string, ushort> { { Constants.DMParameterData, nextPort.Value } };
337  reattachInformation.Port = nextPort.Value;
338  overrideResponsePort = currentPort;
339  nextPort = null;
340 
341  //we'll also get here from SetPort so complete that task
342  var tmpTcs = portAssignmentTcs;
343  portAssignmentTcs = null;
344  if (tmpTcs != null)
345  postRespond = () => tmpTcs.SetResult(true);
346  }
347 
348  portClosedForReboot = false;
349  }
350  break;
352  if (!launchSecurityLevel.HasValue)
353  {
354  logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!");
355  apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest;
356  content = new ErrorMessage { Message = "Invalid API validation request!" };
357  break;
358  }
359  if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse<DreamDaemonSecurity>(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel))
360  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
361  else
362  switch (minimumSecurityLevel)
363  {
364  case DreamDaemonSecurity.Safe:
365  apiValidationStatus = ApiValidationStatus.RequiresSafe;
366  break;
367  case DreamDaemonSecurity.Ultrasafe:
368  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
369  break;
370  case DreamDaemonSecurity.Trusted:
371  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
372  break;
373  default:
374  throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!");
375  }
376  break;
378  if (ClosePortOnReboot)
379  {
380  chatJsonTrackingContext.Active = false;
381  content = new Dictionary<string, int> { { Constants.DMParameterData, 0 } };
382  portClosedForReboot = true;
383  }
384  var oldTcs = rebootTcs;
385  rebootTcs = new TaskCompletionSource<object>();
386  postRespond = () => oldTcs.SetResult(null);
387  break;
388  default:
389  content = new ErrorMessage { Message = "Requested command not supported!" };
390  break;
391  }
392  }
393  else
394  content = new ErrorMessage { Message = "Missing command parameter!" };
395 
396  var json = JsonConvert.SerializeObject(content);
397  var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false);
398 
399  if (response != Constants.DMResponseSuccess)
400  logger.LogWarning("Received error response while responding to interop: {0}", response);
401 
402  postRespond?.Invoke();
403  }
404 
409  {
410  if (disposed)
411  throw new ObjectDisposedException(nameof(SessionController));
412  }
413 
415  public void EnableCustomChatCommands() => chatJsonTrackingContext.Active = true;
416 
419  {
420  CheckDisposed();
421  //we still don't want to dispose the dmb yet, even though we're keeping it alive
422  var tmpProvider = reattachInformation.Dmb;
423  reattachInformation.Dmb = null;
424  released = true;
425  Dispose();
426  byondLock.DoNotDeleteThisSession();
427  tmpProvider.KeepAlive();
428  reattachInformation.Dmb = tmpProvider;
429  return reattachInformation;
430  }
431 
433  public Task<string> SendCommand(string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken);
434 
435  async Task<string> SendCommand(string command, ushort? overridePort, CancellationToken cancellationToken)
436  {
437  try
438  {
439  var commandString = String.Format(CultureInfo.InvariantCulture,
440  "?{0}={1}&{2}={3}",
441  byondTopicSender.SanitizeString(Constants.DMInteropAccessIdentifier),
442  byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier),
443  byondTopicSender.SanitizeString(Constants.DMParameterCommand),
444  //intentionally don't sanitize command, that's up to the caller
445  command);
446 
447  var targetPort = overridePort ?? reattachInformation.Port;
448  logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString);
449 
450  return await byondTopicSender.SendTopic(
451  new IPEndPoint(IPAddress.Loopback, targetPort),
452  commandString,
453  cancellationToken).ConfigureAwait(false);
454  }
455  catch (OperationCanceledException)
456  {
457  throw;
458  }
459  catch (Exception e)
460  {
461  logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message);
462  return null;
463  }
464  }
465 
467  public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
468  {
469  CheckDisposed();
470 
471  if (port == 0)
472  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
473 
474  async Task<bool> ImmediateTopicPortChange()
475  {
476  var commandResult = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangePort), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(false);
477 
478  if (commandResult != Constants.DMResponseSuccess)
479  {
480  logger.LogWarning("Failed port change! DD says: {0}", commandResult);
481  return false;
482  }
483 
484  reattachInformation.Port = port;
485  return true;
486  }
487 
488  lock (this)
489  if (portClosedForReboot)
490  {
491  if (portAssignmentTcs != null)
492  throw new InvalidOperationException("A port change operation is already in progress!");
493  nextPort = port;
494  portAssignmentTcs = new TaskCompletionSource<bool>();
495  return portAssignmentTcs.Task;
496  }
497  else
498  return ImmediateTopicPortChange();
499  }
500 
502  public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
503  {
504  if (RebootState == newRebootState)
505  return true;
506  reattachInformation.RebootState = newRebootState;
507  return await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangeReboot), byondTopicSender.SanitizeString(Constants.DMParameterData), (int)newRebootState), cancellationToken).ConfigureAwait(false) == Constants.DMResponseSuccess;
508  }
509 
511  public void ResetRebootState()
512  {
513  CheckDisposed();
514  reattachInformation.RebootState = RebootState.Normal;
515  }
516 
518  public void SetHighPriority() => process.SetHighPriority();
519  }
520 }
-
bool disposed
If the SessionController has been disposed
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Newtonsoft.Json.Serialization;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Net;
9 using System.Threading;
10 using System.Threading.Tasks;
16 
17 namespace Tgstation.Server.Host.Components.Watchdog
18 {
21  {
23  public bool IsPrimary
24  {
25  get
26  {
27  CheckDisposed();
28  return reattachInformation.IsPrimary;
29  }
30  }
31 
34  {
35  get
36  {
37  if (!Lifetime.IsCompleted)
38  throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
39  return apiValidationStatus;
40  }
41  }
42 
44  public IDmbProvider Dmb
45  {
46  get
47  {
48  CheckDisposed();
49  return reattachInformation.Dmb;
50  }
51  }
52 
54  public ushort? Port
55  {
56  get
57  {
58  CheckDisposed();
59  if (portClosedForReboot)
60  return null;
61  return reattachInformation.Port;
62  }
63  }
64 
67  {
68  get
69  {
70  CheckDisposed();
71  return reattachInformation.RebootState;
72  }
73  }
74 
76  public bool ClosePortOnReboot { get; set; }
77 
79  public bool TerminationWasRequested { get; private set; }
80 
82  public Task<LaunchResult> LaunchResult { get; }
83 
85  public Task<int> Lifetime => process.Lifetime;
86 
88  public Task OnReboot => rebootTcs.Task;
89 
94 
98  readonly IByondTopicSender byondTopicSender;
99 
104 
108  readonly IProcess process;
109 
114 
119 
123  readonly IChat chat;
124 
128  readonly ILogger<SessionController> logger;
129 
134 
138  TaskCompletionSource<bool> portAssignmentTcs;
142  ushort? nextPort;
143 
147  TaskCompletionSource<object> rebootTcs;
148 
153 
157  bool disposed;
158 
163 
167  bool released;
168 
182  public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger<SessionController> logger, DreamDaemonSecurity? launchSecurityLevel, uint? startupTimeout)
183  {
184  this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid
185  this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
186  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
187  this.process = process ?? throw new ArgumentNullException(nameof(process));
188  this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
189  this.interopContext = interopContext ?? throw new ArgumentNullException(nameof(interopContext));
190  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
191  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
192 
193  this.launchSecurityLevel = launchSecurityLevel;
194 
195  interopContext.RegisterHandler(this);
196 
197  portClosedForReboot = false;
198  disposed = false;
199  apiValidationStatus = ApiValidationStatus.NeverValidated;
200  released = false;
201 
202  rebootTcs = new TaskCompletionSource<object>();
203 
204  process.Lifetime.ContinueWith(x => chatJsonTrackingContext.Active = false, TaskScheduler.Current);
205 
206  async Task<LaunchResult> GetLaunchResult()
207  {
208  var startTime = DateTimeOffset.Now;
209  Task toAwait = process.Startup;
210 
211  if (startupTimeout.HasValue)
212  toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
213 
214  await toAwait.ConfigureAwait(false);
215 
216  var result = new LaunchResult
217  {
218  ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
219  StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
220  };
221  return result;
222  };
223  LaunchResult = GetLaunchResult();
224 
225  logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port);
226  }
227 
232 #pragma warning disable CA1821 // Remove empty Finalizers //TODO remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
233  ~SessionController() => Dispose(false);
234 #pragma warning restore CA1821 // Remove empty Finalizers
235 
237  public void Dispose()
238  {
239  Dispose(true);
240  GC.SuppressFinalize(this);
241  }
242 
244  void Dispose(bool disposing)
245  {
246  lock (this)
247  {
248  if (disposed)
249  return;
250  if (disposing)
251  {
252  if (!released)
253  {
254  process.Terminate();
255  byondLock.Dispose();
256  }
257  process.Dispose();
258  interopContext.Dispose();
259  Dmb?.Dispose(); //will be null when released
260  chatJsonTrackingContext.Dispose();
261  disposed = true;
262  }
263  else
264  {
265  if (logger != null)
266  logger.LogError("Being disposed via finalizer!");
267  if (!released)
268  if (process != null)
269  process.Terminate();
270  else if (logger != null)
271  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
272  }
273  }
274  }
275 
277  public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
278  {
279  if (command == null)
280  throw new ArgumentNullException(nameof(command));
281 
282  var query = command.Parameters;
283 
284  object content;
285  Action postRespond = null;
286  ushort? overrideResponsePort = null;
287  if (query.TryGetValue(Constants.DMParameterCommand, out var method))
288  {
289  content = new object();
290  switch (method)
291  {
293  try
294  {
295  var message = JsonConvert.DeserializeObject<Response>(command.RawJson, new JsonSerializerSettings
296  {
297  ContractResolver = new CamelCasePropertyNamesContractResolver()
298  });
299  if (message.ChannelIds == null)
300  throw new InvalidOperationException("Missing ChannelIds field!");
301  if (message.Message == null)
302  throw new InvalidOperationException("Missing Message field!");
303  await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false);
304  }
305  catch (Exception e)
306  {
307  logger.LogDebug("Exception while decoding chat message! Exception: {0}", e);
308  goto default;
309  }
310  break;
312  //currently unused, maybe in the future
313  break;
315  TerminationWasRequested = true;
316  process.Terminate();
317  return;
319  lock (this)
320  {
321  if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort))
322  {
324  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
325  content = new ErrorMessage { Message = "Missing stringified port as data parameter!" };
326  break;
327  }
328 
329  if (!nextPort.HasValue)
330  //not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
331  reattachInformation.Port = currentPort;
332  else
333  {
334  //nextPort is ready, tell DD to switch to that
335  //if it fails it'll kill itself
336  content = new Dictionary<string, ushort> { { Constants.DMParameterData, nextPort.Value } };
337  reattachInformation.Port = nextPort.Value;
338  overrideResponsePort = currentPort;
339  nextPort = null;
340 
341  //we'll also get here from SetPort so complete that task
342  var tmpTcs = portAssignmentTcs;
343  portAssignmentTcs = null;
344  if (tmpTcs != null)
345  postRespond = () => tmpTcs.SetResult(true);
346  }
347 
348  portClosedForReboot = false;
349  }
350  break;
352  if (!launchSecurityLevel.HasValue)
353  {
354  logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!");
355  apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest;
356  content = new ErrorMessage { Message = "Invalid API validation request!" };
357  break;
358  }
359  if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse<DreamDaemonSecurity>(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel))
360  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
361  else
362  switch (minimumSecurityLevel)
363  {
364  case DreamDaemonSecurity.Safe:
365  apiValidationStatus = ApiValidationStatus.RequiresSafe;
366  break;
367  case DreamDaemonSecurity.Ultrasafe:
368  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
369  break;
370  case DreamDaemonSecurity.Trusted:
371  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
372  break;
373  default:
374  throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!");
375  }
376  break;
378  if (ClosePortOnReboot)
379  {
380  chatJsonTrackingContext.Active = false;
381  content = new Dictionary<string, int> { { Constants.DMParameterData, 0 } };
382  portClosedForReboot = true;
383  }
384  var oldTcs = rebootTcs;
385  rebootTcs = new TaskCompletionSource<object>();
386  postRespond = () => oldTcs.SetResult(null);
387  break;
388  default:
389  content = new ErrorMessage { Message = "Requested command not supported!" };
390  break;
391  }
392  }
393  else
394  content = new ErrorMessage { Message = "Missing command parameter!" };
395 
396  var json = JsonConvert.SerializeObject(content);
397  var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false);
398 
399  if (response != Constants.DMResponseSuccess)
400  logger.LogWarning("Received error response while responding to interop: {0}", response);
401 
402  postRespond?.Invoke();
403  }
404 
409  {
410  if (disposed)
411  throw new ObjectDisposedException(nameof(SessionController));
412  }
413 
415  public void EnableCustomChatCommands() => chatJsonTrackingContext.Active = true;
416 
419  {
420  CheckDisposed();
421  //we still don't want to dispose the dmb yet, even though we're keeping it alive
422  var tmpProvider = reattachInformation.Dmb;
423  reattachInformation.Dmb = null;
424  released = true;
425  Dispose();
426  byondLock.DoNotDeleteThisSession();
427  tmpProvider.KeepAlive();
428  reattachInformation.Dmb = tmpProvider;
429  return reattachInformation;
430  }
431 
433  public Task<string> SendCommand(string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken);
434 
435  async Task<string> SendCommand(string command, ushort? overridePort, CancellationToken cancellationToken)
436  {
437  try
438  {
439  var commandString = String.Format(CultureInfo.InvariantCulture,
440  "?{0}={1}&{2}={3}",
441  byondTopicSender.SanitizeString(Constants.DMInteropAccessIdentifier),
442  byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier),
443  byondTopicSender.SanitizeString(Constants.DMParameterCommand),
444  //intentionally don't sanitize command, that's up to the caller
445  command);
446 
447  var targetPort = overridePort ?? reattachInformation.Port;
448  logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString);
449 
450  return await byondTopicSender.SendTopic(
451  new IPEndPoint(IPAddress.Loopback, targetPort),
452  commandString,
453  cancellationToken).ConfigureAwait(false);
454  }
455  catch (OperationCanceledException)
456  {
457  throw;
458  }
459  catch (Exception e)
460  {
461  logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message);
462  return null;
463  }
464  }
465 
467  public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
468  {
469  CheckDisposed();
470 
471  if (port == 0)
472  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
473 
474  async Task<bool> ImmediateTopicPortChange()
475  {
476  var commandResult = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangePort), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(false);
477 
478  if (commandResult != Constants.DMResponseSuccess)
479  {
480  logger.LogWarning("Failed port change! DD says: {0}", commandResult);
481  return false;
482  }
483 
484  reattachInformation.Port = port;
485  return true;
486  }
487 
488  lock (this)
489  if (portClosedForReboot)
490  {
491  if (portAssignmentTcs != null)
492  throw new InvalidOperationException("A port change operation is already in progress!");
493  nextPort = port;
494  portAssignmentTcs = new TaskCompletionSource<bool>();
495  return portAssignmentTcs.Task;
496  }
497  else
498  return ImmediateTopicPortChange();
499  }
500 
502  public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
503  {
504  if (RebootState == newRebootState)
505  return true;
506  reattachInformation.RebootState = newRebootState;
507  return await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangeReboot), byondTopicSender.SanitizeString(Constants.DMParameterData), (int)newRebootState), cancellationToken).ConfigureAwait(false) == Constants.DMResponseSuccess;
508  }
509 
511  public void ResetRebootState()
512  {
513  CheckDisposed();
514  reattachInformation.RebootState = RebootState.Normal;
515  }
516 
518  public void SetHighPriority() => process.SetHighPriority();
519  }
520 }
bool disposed
If the SessionController has been disposed
readonly ICommContext interopContext
The ICommContext for the SessionController
diff --git a/_setup_wizard_8cs_source.html b/_setup_wizard_8cs_source.html index d23c4c6b46..2c2e08ce32 100644 --- a/_setup_wizard_8cs_source.html +++ b/_setup_wizard_8cs_source.html @@ -92,8 +92,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SetupWizard.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using MySql.Data.MySqlClient;
5 using Newtonsoft.Json;
6 using System;
7 using System.Collections.Generic;
8 using System.Data.Common;
9 using System.Data.SqlClient;
10 using System.Globalization;
11 using System.Runtime.InteropServices;
12 using System.Text;
13 using System.Threading;
14 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
17 
18 namespace Tgstation.Server.Host.Core
19 {
21  sealed class SetupWizard : ISetupWizard
22  {
27 
31  readonly IConsole console;
32 
36  readonly IHostingEnvironment hostingEnvironment;
37 
42 
47 
52 
57 
61  readonly ILogger<SetupWizard> logger;
62 
67 
80  public SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, IDBConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, ILogger<SetupWizard> logger, IOptions<GeneralConfiguration> generalConfigurationOptions)
81  {
82  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
83  this.console = console ?? throw new ArgumentNullException(nameof(console));
84  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
85  this.application = application ?? throw new ArgumentNullException(nameof(application));
86  this.dbConnectionFactory = dbConnectionFactory ?? throw new ArgumentNullException(nameof(dbConnectionFactory));
87  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
88  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
89  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
90  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
91  }
92 
99  async Task<bool> PromptYesNo(string question, CancellationToken cancellationToken)
100  {
101  do
102  {
103  await console.WriteAsync(question, false, cancellationToken).ConfigureAwait(false);
104  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
105  var upperResponse = responseString.ToUpperInvariant();
106  if (upperResponse == "Y" || upperResponse == "YES")
107  return true;
108  else if (upperResponse == "N" || upperResponse == "NO")
109  return false;
110  await console.WriteAsync("Invalid response!", true, cancellationToken).ConfigureAwait(false);
111  }
112  while (true);
113  }
114 
120  async Task<ushort?> PromptForHostingPort(CancellationToken cancellationToken)
121  {
122  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
123  await console.WriteAsync("What port would you like to connect to TGS on?", true, cancellationToken).ConfigureAwait(false);
124  await console.WriteAsync("Note: If this is a docker container with the default port already mapped, use the default.", true, cancellationToken).ConfigureAwait(false);
125 
126  do
127  {
128  await console.WriteAsync("API Port (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
129  var portString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
130  if (String.IsNullOrWhiteSpace(portString))
131  return null;
132  if (UInt16.TryParse(portString, out var port) && port != 0)
133  return port;
134  await console.WriteAsync("Invalid port! Please enter a value between 1 and 65535", true, cancellationToken).ConfigureAwait(false);
135  }
136  while (true);
137  }
138 
144  async Task<DatabaseConfiguration> ConfigureDatabase(CancellationToken cancellationToken)
145  {
146  do
147  {
148  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
149  await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
150 
151  var databaseConfiguration = new DatabaseConfiguration();
152  do
153  {
154  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Please enter one of {0}, {1}, or {2}: ", DatabaseType.MariaDB, DatabaseType.SqlServer, DatabaseType.MySql), false, cancellationToken).ConfigureAwait(false);
155  var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
156  if (Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
157  {
158  databaseConfiguration.DatabaseType = databaseType;
159  break;
160  }
161  await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
162  }
163  while (true);
164 
165  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
166  await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false);
167  var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
168  if (String.IsNullOrWhiteSpace(serverAddress))
169  serverAddress = null;
170 
171  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
172  await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
173  string databaseName;
174 
175  do
176  {
177  databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
178  if (!String.IsNullOrWhiteSpace(databaseName))
179  break;
180  await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
181  }
182  while (true);
183 
184  var dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false);
185 
186  bool useWinAuth;
187  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
188  useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
189  else
190  useWinAuth = false;
191 
192  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
193 
194  string username = null;
195  string password = null;
196  if (!useWinAuth)
197  {
198  await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
199  username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
200  await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
201  password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
202  }
203  else
204  {
205  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);
206  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);
207  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);
208  }
209  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
210 
211  DbConnection testConnection;
212  void CreateTestConnection(string connectionString)
213  {
214  testConnection = dbConnectionFactory.CreateConnection(connectionString, databaseConfiguration.DatabaseType);
215  }
216 
217  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
218  {
219  var csb = new SqlConnectionStringBuilder
220  {
221  ApplicationName = application.VersionPrefix,
222  DataSource = serverAddress ?? "(local)"
223  };
224  if (useWinAuth)
225  csb.IntegratedSecurity = true;
226  else
227  {
228  csb.UserID = username;
229  csb.Password = password;
230  }
231 
232  CreateTestConnection(csb.ConnectionString);
233  csb.InitialCatalog = databaseName;
234  databaseConfiguration.ConnectionString = csb.ConnectionString;
235  }
236  else
237  {
238  var csb = new MySqlConnectionStringBuilder
239  {
240  Server = serverAddress ?? "127.0.0.1",
241  UserID = username,
242  Password = password
243  };
244 
245  CreateTestConnection(csb.ConnectionString);
246  csb.Database = databaseName;
247  databaseConfiguration.ConnectionString = csb.ConnectionString;
248  }
249 
250  try
251  {
252  using (testConnection)
253  {
254  await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
255  await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
256  await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
257 
258  if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
259  {
260  await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
261  using (var command = testConnection.CreateCommand())
262  {
263  command.CommandText = "SELECT VERSION()";
264  var fullVersion = (string)(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
265  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
266  var splits = fullVersion.Split('-');
267  databaseConfiguration.MySqlServerVersion = splits[0];
268  }
269  }
270 
271  if (!dbExists)
272  {
273  await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
274  using (var command = testConnection.CreateCommand())
275  {
276  command.CommandText = String.Format(CultureInfo.InvariantCulture, "CREATE DATABASE {0}", databaseName);
277  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
278  }
279  await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
280  await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
281  using (var command = testConnection.CreateCommand())
282  {
283  command.CommandText = String.Format(CultureInfo.InvariantCulture, "DROP DATABASE {0}", databaseName);
284  try
285  {
286  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
287  }
288  catch (OperationCanceledException)
289  {
290  throw;
291  }
292  catch (Exception e)
293  {
294  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
295  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
296  await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
297  await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
298  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
299  }
300  }
301  }
302  }
303 
304  return databaseConfiguration;
305  }
306  catch (OperationCanceledException)
307  {
308  throw;
309  }
310  catch (Exception e)
311  {
312  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
313  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
314  await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
315  }
316  } while (true);
317  }
318 
324  async Task<GeneralConfiguration> ConfigureGeneral(CancellationToken cancellationToken)
325  {
326  var newGeneralConfiguration = new GeneralConfiguration
327  {
329  };
330  do
331  {
332  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
333  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
334  var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
335  if (String.IsNullOrWhiteSpace(passwordLengthString))
336  break;
337  if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
338  {
339  newGeneralConfiguration.MinimumPasswordLength = passwordLength;
340  break;
341  }
342  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
343  }
344  while (true);
345 
346  do
347  {
348  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
349  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);
350  var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
351  if (String.IsNullOrWhiteSpace(topicTimeoutString))
352  break;
353  if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
354  {
355  newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
356  break;
357  }
358  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
359  }
360  while (true);
361 
362  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
363  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);
364  await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
365  newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
366  if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
367  newGeneralConfiguration.GitHubAccessToken = null;
368  return newGeneralConfiguration;
369  }
370 
376  async Task<FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
377  {
378  var fileLoggingConfiguration = new FileLoggingConfiguration();
379  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
380  fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);
381 
382  if (!fileLoggingConfiguration.Disable)
383  {
384  do
385  {
386  await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
387  fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
388  if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
389  {
390  fileLoggingConfiguration.Directory = null;
391  break;
392  }
393  //test a write of it
394  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
395  await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);
396  try
397  {
398  await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);
399  var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
400  await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
401  try
402  {
403  await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
404  }
405  catch (OperationCanceledException)
406  {
407  throw;
408  }
409  catch (Exception e)
410  {
411  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);
412  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
413  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
414  }
415  break;
416  }
417  catch (OperationCanceledException)
418  {
419  throw;
420  }
421  catch (Exception e)
422  {
423  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
424  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
425  await console.WriteAsync("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
426  }
427  } while (true);
428 
429  async Task<LogLevel?> PromptLogLevel(string question)
430  {
431  do
432  {
433  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
434  await console.WriteAsync(question, true, cancellationToken).ConfigureAwait(false);
435  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);
436  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
437  if (String.IsNullOrWhiteSpace(responseString))
438  return null;
439  if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
440  return logLevel;
441  await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
442  } while (true);
443  }
444 
445  fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;
446  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;
447  }
448  return fileLoggingConfiguration;
449  }
450 
461  async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, CancellationToken cancellationToken)
462  {
463  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
464 
465  var map = new Dictionary<string, object>()
466  {
467  { DatabaseConfiguration.Section, databaseConfiguration },
468  { GeneralConfiguration.Section, newGeneralConfiguration },
469  { FileLoggingConfiguration.Section, fileLoggingConfiguration }
470  };
471 
472  if (hostingPort.HasValue)
473  map.Add("Kestrel", new
474  {
475  EndPoints = new
476  {
477  Http = new
478  {
479  Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort)
480  }
481  }
482  });
483 
484  var json = JsonConvert.SerializeObject(map, Formatting.Indented);
485  var configBytes = Encoding.UTF8.GetBytes(json);
486 
487  try
488  {
489  await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
490  }
491  catch (OperationCanceledException)
492  {
493  throw;
494  }
495  catch (Exception e)
496  {
497  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
498  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
499  await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
500  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
501  await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
502  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
503  await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
504  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
505  throw new OperationCanceledException();
506  }
507 
508  await console.WriteAsync("Waiting for configuration changes to reload...", true, cancellationToken).ConfigureAwait(false);
509 
510  //we need to wait for the configuration's file system watcher to read and reload the changes
511  await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
512  }
513 
520  async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
521  {
522  //welcome message
523  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
524  await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
525  await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
526 
527  var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
528 
529  var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
530 
531  var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
532 
533  var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false);
534 
535  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
536 
537  await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, cancellationToken).ConfigureAwait(false);
538  }
539 
541  public async Task<bool> CheckRunWizard(CancellationToken cancellationToken)
542  {
543  var setupWizardMode = generalConfiguration.SetupWizardMode;
544  logger.LogTrace("Checking if setup wizard should run. SetupWizardMode: {0}", setupWizardMode);
545 
546  if (setupWizardMode == SetupWizardMode.Never)
547  {
548  logger.LogTrace("Skipping due to configuration...");
549  return false;
550  }
551 
552  var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
553  if (!console.Available)
554  {
555  if (forceRun)
556  throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!");
557  logger.LogTrace("Skipping due to console not being available...");
558  return false;
559  }
560 
561  var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
562  var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);
563 
564  bool shouldRunBasedOnAutodetect;
565  if (exists)
566  {
567  var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);
568  var contents = Encoding.UTF8.GetString(bytes);
569  var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents);
570  logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty);
571  shouldRunBasedOnAutodetect = existingConfigIsEmpty;
572  }
573  else
574  {
575  shouldRunBasedOnAutodetect = true;
576  logger.LogTrace("No configuration json detected");
577  }
578 
579 
580  if (!shouldRunBasedOnAutodetect)
581  {
582  if (forceRun)
583  {
584  logger.LogTrace("Asking user to bypass due to force run request...");
585  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);
586 
587  forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
588  }
589  if (!forceRun)
590  return false;
591  }
592 
593  //flush the logs to prevent console conflicts
594  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
595 
596  await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
597  return true;
598  }
599  }
600 }
-
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using MySql.Data.MySqlClient;
5 using Newtonsoft.Json;
6 using System;
7 using System.Collections.Generic;
8 using System.Data.Common;
9 using System.Data.SqlClient;
10 using System.Globalization;
11 using System.Linq;
12 using System.Text;
13 using System.Threading;
14 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
17 
18 namespace Tgstation.Server.Host.Core
19 {
21  sealed class SetupWizard : ISetupWizard
22  {
27 
31  readonly IConsole console;
32 
36  readonly IHostingEnvironment hostingEnvironment;
37 
42 
47 
52 
57 
61  readonly ILogger<SetupWizard> logger;
62 
67 
80  public SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, IDBConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, ILogger<SetupWizard> logger, IOptions<GeneralConfiguration> generalConfigurationOptions)
81  {
82  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
83  this.console = console ?? throw new ArgumentNullException(nameof(console));
84  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
85  this.application = application ?? throw new ArgumentNullException(nameof(application));
86  this.dbConnectionFactory = dbConnectionFactory ?? throw new ArgumentNullException(nameof(dbConnectionFactory));
87  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
88  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
89  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
90  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
91  }
92 
99  async Task<bool> PromptYesNo(string question, CancellationToken cancellationToken)
100  {
101  do
102  {
103  await console.WriteAsync(question, false, cancellationToken).ConfigureAwait(false);
104  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
105  var upperResponse = responseString.ToUpperInvariant();
106  if (upperResponse == "Y" || upperResponse == "YES")
107  return true;
108  else if (upperResponse == "N" || upperResponse == "NO")
109  return false;
110  await console.WriteAsync("Invalid response!", true, cancellationToken).ConfigureAwait(false);
111  }
112  while (true);
113  }
114 
120  async Task<ushort?> PromptForHostingPort(CancellationToken cancellationToken)
121  {
122  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
123  await console.WriteAsync("What port would you like to connect to TGS on?", true, cancellationToken).ConfigureAwait(false);
124  await console.WriteAsync("Note: If this is a docker container with the default port already mapped, use the default.", true, cancellationToken).ConfigureAwait(false);
125 
126  do
127  {
128  await console.WriteAsync("API Port (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
129  var portString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
130  if (String.IsNullOrWhiteSpace(portString))
131  return null;
132  if (UInt16.TryParse(portString, out var port) && port != 0)
133  return port;
134  await console.WriteAsync("Invalid port! Please enter a value between 1 and 65535", true, cancellationToken).ConfigureAwait(false);
135  }
136  while (true);
137  }
138 
144  async Task<DatabaseConfiguration> ConfigureDatabase(CancellationToken cancellationToken)
145  {
146  do
147  {
148  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
149  await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
150 
151  var databaseConfiguration = new DatabaseConfiguration();
152  do
153  {
154  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Please enter one of {0}, {1}, or {2}: ", DatabaseType.MariaDB, DatabaseType.SqlServer, DatabaseType.MySql), false, cancellationToken).ConfigureAwait(false);
155  var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
156  if (Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
157  {
158  databaseConfiguration.DatabaseType = databaseType;
159  break;
160  }
161  await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
162  }
163  while (true);
164 
165  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
166  await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false);
167  var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
168  if (String.IsNullOrWhiteSpace(serverAddress))
169  serverAddress = null;
170 
171  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
172  await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
173  string databaseName;
174 
175  do
176  {
177  databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
178  if (!String.IsNullOrWhiteSpace(databaseName))
179  break;
180  await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
181  }
182  while (true);
183 
184  var dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false);
185 
186  bool useWinAuth;
187  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
188  useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
189  else
190  useWinAuth = false;
191 
192  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
193 
194  string username = null;
195  string password = null;
196  if (!useWinAuth)
197  {
198  await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
199  username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
200  await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
201  password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
202  }
203  else
204  {
205  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);
206  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);
207  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);
208  }
209  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
210 
211  DbConnection testConnection;
212  void CreateTestConnection(string connectionString)
213  {
214  testConnection = dbConnectionFactory.CreateConnection(connectionString, databaseConfiguration.DatabaseType);
215  }
216 
217  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
218  {
219  var csb = new SqlConnectionStringBuilder
220  {
221  ApplicationName = application.VersionPrefix,
222  DataSource = serverAddress ?? "(local)"
223  };
224  if (useWinAuth)
225  csb.IntegratedSecurity = true;
226  else
227  {
228  csb.UserID = username;
229  csb.Password = password;
230  }
231 
232  CreateTestConnection(csb.ConnectionString);
233  csb.InitialCatalog = databaseName;
234  databaseConfiguration.ConnectionString = csb.ConnectionString;
235  }
236  else
237  {
238  var csb = new MySqlConnectionStringBuilder
239  {
240  Server = serverAddress ?? "127.0.0.1",
241  UserID = username,
242  Password = password
243  };
244 
245  CreateTestConnection(csb.ConnectionString);
246  csb.Database = databaseName;
247  databaseConfiguration.ConnectionString = csb.ConnectionString;
248  }
249 
250  try
251  {
252  using (testConnection)
253  {
254  await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
255  await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
256  await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
257 
258  if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
259  {
260  await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
261  using (var command = testConnection.CreateCommand())
262  {
263  command.CommandText = "SELECT VERSION()";
264  var fullVersion = (string)(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
265  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
266  var splits = fullVersion.Split('-');
267  databaseConfiguration.MySqlServerVersion = splits[0];
268  }
269  }
270 
271  if (!dbExists)
272  {
273  await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
274  using (var command = testConnection.CreateCommand())
275  {
276  command.CommandText = String.Format(CultureInfo.InvariantCulture, "CREATE DATABASE {0}", databaseName);
277  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
278  }
279  await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
280  await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
281  using (var command = testConnection.CreateCommand())
282  {
283  command.CommandText = String.Format(CultureInfo.InvariantCulture, "DROP DATABASE {0}", databaseName);
284  try
285  {
286  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
287  }
288  catch (OperationCanceledException)
289  {
290  throw;
291  }
292  catch (Exception e)
293  {
294  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
295  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
296  await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
297  await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
298  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
299  }
300  }
301  }
302  }
303 
304  return databaseConfiguration;
305  }
306  catch (OperationCanceledException)
307  {
308  throw;
309  }
310  catch (Exception e)
311  {
312  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
313  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
314  await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
315  }
316  } while (true);
317  }
318 
324  async Task<GeneralConfiguration> ConfigureGeneral(CancellationToken cancellationToken)
325  {
326  var newGeneralConfiguration = new GeneralConfiguration
327  {
329  };
330 
331  do
332  {
333  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
334  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
335  var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
336  if (String.IsNullOrWhiteSpace(passwordLengthString))
337  break;
338  if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
339  {
340  newGeneralConfiguration.MinimumPasswordLength = passwordLength;
341  break;
342  }
343  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
344  }
345  while (true);
346 
347  do
348  {
349  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
350  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);
351  var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
352  if (String.IsNullOrWhiteSpace(topicTimeoutString))
353  break;
354  if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
355  {
356  newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
357  break;
358  }
359  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
360  }
361  while (true);
362 
363  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
364  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);
365  await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
366  newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
367  if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
368  newGeneralConfiguration.GitHubAccessToken = null;
369  return newGeneralConfiguration;
370  }
371 
377  async Task<FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
378  {
379  var fileLoggingConfiguration = new FileLoggingConfiguration();
380  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
381  fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);
382 
383  if (!fileLoggingConfiguration.Disable)
384  {
385  do
386  {
387  await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
388  fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
389  if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
390  {
391  fileLoggingConfiguration.Directory = null;
392  break;
393  }
394  //test a write of it
395  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
396  await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);
397  try
398  {
399  await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);
400  var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
401  await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
402  try
403  {
404  await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
405  }
406  catch (OperationCanceledException)
407  {
408  throw;
409  }
410  catch (Exception e)
411  {
412  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);
413  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
414  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
415  }
416  break;
417  }
418  catch (OperationCanceledException)
419  {
420  throw;
421  }
422  catch (Exception e)
423  {
424  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
425  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
426  await console.WriteAsync("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
427  }
428  } while (true);
429 
430  async Task<LogLevel?> PromptLogLevel(string question)
431  {
432  do
433  {
434  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
435  await console.WriteAsync(question, true, cancellationToken).ConfigureAwait(false);
436  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);
437  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
438  if (String.IsNullOrWhiteSpace(responseString))
439  return null;
440  if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
441  return logLevel;
442  await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
443  } while (true);
444  }
445 
446  fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;
447  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;
448  }
449  return fileLoggingConfiguration;
450  }
451 
457  async Task<ControlPanelConfiguration> ConfigureControlPanel(CancellationToken cancellationToken)
458  {
459  var config = new ControlPanelConfiguration
460  {
461  Enable = await PromptYesNo("Enable the web control panel? (y/n): ", cancellationToken).ConfigureAwait(false),
462  AllowAnyOrigin = await PromptYesNo("Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *) (y/n): ", cancellationToken).ConfigureAwait(false)
463  };
464 
465  if (!config.AllowAnyOrigin)
466  {
467  await console.WriteAsync("Enter a comma seperated list of CORS allowed origins (optional): ", false, cancellationToken).ConfigureAwait(false);
468  var commaSeperatedOrigins = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
469  if (!String.IsNullOrWhiteSpace(commaSeperatedOrigins))
470  {
471  var splits = commaSeperatedOrigins.Split(',');
472  config.AllowedOrigins = new List<string>(splits.Select(x => x.Trim()));
473  }
474  }
475 
476  return config;
477  }
478 
490  async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
491  {
492  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
493 
494  var map = new Dictionary<string, object>()
495  {
496  { DatabaseConfiguration.Section, databaseConfiguration },
497  { GeneralConfiguration.Section, newGeneralConfiguration },
498  { FileLoggingConfiguration.Section, fileLoggingConfiguration },
499  { ControlPanelConfiguration.Section, controlPanelConfiguration }
500  };
501 
502  if (hostingPort.HasValue)
503  map.Add("Kestrel", new
504  {
505  EndPoints = new
506  {
507  Http = new
508  {
509  Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort)
510  }
511  }
512  });
513 
514  var json = JsonConvert.SerializeObject(map, Formatting.Indented);
515  var configBytes = Encoding.UTF8.GetBytes(json);
516 
517  try
518  {
519  await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
520  }
521  catch (OperationCanceledException)
522  {
523  throw;
524  }
525  catch (Exception e)
526  {
527  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
528  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
529  await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
530  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
531  await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
532  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
533  await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
534  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
535  throw new OperationCanceledException();
536  }
537 
538  await console.WriteAsync("Waiting for configuration changes to reload...", true, cancellationToken).ConfigureAwait(false);
539 
540  //we need to wait for the configuration's file system watcher to read and reload the changes
541  await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
542  }
543 
550  async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
551  {
552  //welcome message
553  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
554  await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
555  await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
556 
557  var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
558 
559  var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
560 
561  var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
562 
563  var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false);
564 
565  var controlPanelConfiguration = await ConfigureControlPanel(cancellationToken).ConfigureAwait(false);
566 
567  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
568 
569  await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, controlPanelConfiguration, cancellationToken).ConfigureAwait(false);
570  }
571 
573  public async Task<bool> CheckRunWizard(CancellationToken cancellationToken)
574  {
575  var setupWizardMode = generalConfiguration.SetupWizardMode;
576  logger.LogTrace("Checking if setup wizard should run. SetupWizardMode: {0}", setupWizardMode);
577 
578  if (setupWizardMode == SetupWizardMode.Never)
579  {
580  logger.LogTrace("Skipping due to configuration...");
581  return false;
582  }
583 
584  var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
585  if (!console.Available)
586  {
587  if (forceRun)
588  throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!");
589  logger.LogTrace("Skipping due to console not being available...");
590  return false;
591  }
592 
593  var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
594  var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);
595 
596  bool shouldRunBasedOnAutodetect;
597  if (exists)
598  {
599  var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);
600  var contents = Encoding.UTF8.GetString(bytes);
601  var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}";
602  logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty);
603  shouldRunBasedOnAutodetect = existingConfigIsEmpty;
604  }
605  else
606  {
607  shouldRunBasedOnAutodetect = true;
608  logger.LogTrace("No configuration json detected");
609  }
610 
611 
612  if (!shouldRunBasedOnAutodetect)
613  {
614  if (forceRun)
615  {
616  logger.LogTrace("Asking user to bypass due to force run request...");
617  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);
618 
619  forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
620  }
621  if (!forceRun)
622  return false;
623  }
624 
625  //flush the logs to prevent console conflicts
626  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
627 
628  await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
629  return true;
630  }
631  }
632 }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+
List< string > AllowedOrigins
Origins allowed for CORS requests
+
async Task< ControlPanelConfiguration > ConfigureControlPanel(CancellationToken cancellationToken)
Prompts the user to create a ControlPanelConfiguration
Definition: SetupWizard.cs:457
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
SetupWizardMode
Determines if the Core.ISetupWizard will run
@@ -109,19 +110,21 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the SetupWizard
Definition: SetupWizard.cs:66
SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, IDBConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, ILogger< SetupWizard > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct a SetupWizard
Definition: SetupWizard.cs:80
async Task< bool > PromptYesNo(string question, CancellationToken cancellationToken)
A prompt for a yes or no value
Definition: SetupWizard.cs:99
- +
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the ControlPanelConfiguratio...
+ +
async Task< DatabaseConfiguration > ConfigureDatabase(CancellationToken cancellationToken)
Prompts the user to create a DatabaseConfiguration
Definition: SetupWizard.cs:144
-
async Task SaveConfiguration(string userConfigFileName, ushort?hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, CancellationToken cancellationToken)
Saves a given Configuration set to userConfigFileName
Definition: SetupWizard.cs:461
async Task< GeneralConfiguration > ConfigureGeneral(CancellationToken cancellationToken)
Prompts the user to create a GeneralConfiguration
Definition: SetupWizard.cs:324
+
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:490
readonly IHostingEnvironment hostingEnvironment
The IHostingEnvironment for the SetupWizard
Definition: SetupWizard.cs:36
async Task< ushort?> PromptForHostingPort(CancellationToken cancellationToken)
Prompts the user to enter the port to host TGS on
Definition: SetupWizard.cs:120
Configuration options for the Models.DatabaseContext<TParentContext>
-
async Task< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration
Definition: SetupWizard.cs:376
+
async Task< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration
Definition: SetupWizard.cs:377
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the SetupWizard
Definition: SetupWizard.cs:56
-
async Task< bool > CheckRunWizard(CancellationToken cancellationToken)
Run the setup wizard if necessary
Definition: SetupWizard.cs:541
+
async Task< bool > CheckRunWizard(CancellationToken cancellationToken)
Run the setup wizard if necessary
Definition: SetupWizard.cs:573
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SetupWizard
Definition: SetupWizard.cs:51
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
@@ -132,7 +135,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Interface for using filesystems
Definition: IIOManager.cs:11
For identifying the current platform
readonly ILogger< SetupWizard > logger
The ILogger for the SetupWizard
Definition: SetupWizard.cs:61
-
async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
Runs the SetupWizard
Definition: SetupWizard.cs:520
+
async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
Runs the SetupWizard
Definition: SetupWizard.cs:550
uint MinimumPasswordLength
Minimum length of database user passwords
diff --git a/_sql_server_database_context_8cs_source.html b/_sql_server_database_context_8cs_source.html index a2fef729fc..327ef219f1 100644 --- a/_sql_server_database_context_8cs_source.html +++ b/_sql_server_database_context_8cs_source.html @@ -97,7 +97,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For initially seeding a database
DatabaseContext<TParentContext> for Sqlserver
- +
SqlServerDatabaseContext(DbContextOptions< SqlServerDatabaseContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger< SqlServerDatabaseContext > logger)
Construct a SqlServerDatabaseContext
Configuration options for the Models.DatabaseContext<TParentContext>
diff --git a/_test_application_8cs_source.html b/_test_application_8cs_source.html index 132322bc56..605eb1fda1 100644 --- a/_test_application_8cs_source.html +++ b/_test_application_8cs_source.html @@ -92,18 +92,19 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TestApplication.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Builder;
2 using Microsoft.AspNetCore.Hosting;
3 using Microsoft.Extensions.Configuration;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Options;
6 using Microsoft.VisualStudio.TestTools.UnitTesting;
7 using Moq;
8 using System;
9 using System.Collections.Generic;
10 using System.Threading;
11 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Core.Tests
16 {
20  [TestClass]
21  public sealed class TestApplication
22  {
23  [TestMethod]
24  public void TestMethodThrows()
25  {
26  Assert.ThrowsException<ArgumentNullException>(() => new Application(null, null));
27  var mockConfiguration = new Mock<IConfiguration>();
28  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
29 
30  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
31 
32  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
33 
34  Assert.ThrowsException<ArgumentNullException>(() => app.ConfigureServices(null));
35  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null));
36 
37  var mockAppBuilder = new Mock<IApplicationBuilder>();
38  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, null, null, null));
39 
40  var mockServerControl = new Mock<IServerControl>();
41  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null));
42 
43  var mockTokenFactory = new Mock<ITokenFactory>();
44  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null));
45  }
46 
48  {
49  public Task<bool> CheckRunWizard(CancellationToken cancellationToken) => Task.FromResult(true);
50  }
51 
53  {
54  public CancellationToken ApplicationStarted => default;
55 
56  public CancellationToken ApplicationStopping => default;
57 
58  public CancellationToken ApplicationStopped => default;
59 
60  public void StopApplication() { }
61  }
62 
63  [TestMethod]
65  {
66  var mockConfiguration = new Mock<IConfiguration>();
67  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
68 
69  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
70 
71  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
72 
73  var mockOptions = new Mock<IOptions<GeneralConfiguration>>();
74  mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration
75  {
77  }).Verifiable();
78 
79  var fakeServiceDescriptor = new List<ServiceDescriptor>()
80  {
81  new ServiceDescriptor(typeof(IApplicationLifetime), typeof(MockApplicationLifetime), ServiceLifetime.Singleton),
82  new ServiceDescriptor(typeof(ISetupWizard), typeof(MockSetupWizard), ServiceLifetime.Singleton),
83  new ServiceDescriptor(typeof(IOptions<GeneralConfiguration>), mockOptions.Object)
84  };
85 
86  var mockServiceCollection = new Mock<IServiceCollection>();
87 
88  var mockConfigSection = new Mock<IConfigurationSection>();
89 
90  mockConfiguration.Setup(x => x.GetSection(It.IsNotNull<string>())).Returns(mockConfigSection.Object).Verifiable();
91  mockServiceCollection.Setup(x => x.GetEnumerator()).Returns(() => fakeServiceDescriptor.GetEnumerator()).Verifiable();
92 
93  Assert.ThrowsException<OperationCanceledException>(() => app.ConfigureServices(mockServiceCollection.Object));
94 
95  mockOptions.VerifyAll();
96  mockConfiguration.VerifyAll();
97  mockServiceCollection.VerifyAll();
98  }
99  }
100 }
SetupWizardMode
Determines if the Core.ISetupWizard will run
- +Go to the documentation of this file.
1 using Microsoft.AspNetCore.Builder;
2 using Microsoft.AspNetCore.Hosting;
3 using Microsoft.Extensions.Configuration;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Options;
6 using Microsoft.VisualStudio.TestTools.UnitTesting;
7 using Moq;
8 using System;
9 using System.Collections.Generic;
10 using System.Threading;
11 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Core.Tests
16 {
20  [TestClass]
21  public sealed class TestApplication
22  {
23  [TestMethod]
24  public void TestMethodThrows()
25  {
26  Assert.ThrowsException<ArgumentNullException>(() => new Application(null, null));
27  var mockConfiguration = new Mock<IConfiguration>();
28  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
29 
30  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
31 
32  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
33 
34  Assert.ThrowsException<ArgumentNullException>(() => app.ConfigureServices(null));
35  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null, null));
36 
37  var mockAppBuilder = new Mock<IApplicationBuilder>();
38  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, null, null, null, null));
39 
40  var mockServerControl = new Mock<IServerControl>();
41  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null));
42 
43  var mockTokenFactory = new Mock<ITokenFactory>();
44  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null));
45 
46  var mockControlPanelOptions = new Mock<IOptions<ControlPanelConfiguration>>();
47  mockControlPanelOptions.SetupGet(x => x.Value).Returns(new ControlPanelConfiguration()).Verifiable();
48  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockControlPanelOptions.Object, null));
49  mockControlPanelOptions.VerifyAll();
50  }
51 
53  {
54  public Task<bool> CheckRunWizard(CancellationToken cancellationToken) => Task.FromResult(true);
55  }
56 
58  {
59  public CancellationToken ApplicationStarted => default;
60 
61  public CancellationToken ApplicationStopping => default;
62 
63  public CancellationToken ApplicationStopped => default;
64 
65  public void StopApplication() { }
66  }
67 
68  [TestMethod]
70  {
71  var mockConfiguration = new Mock<IConfiguration>();
72  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
73 
74  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
75 
76  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
77 
78  var mockOptions = new Mock<IOptions<GeneralConfiguration>>();
79  mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration
80  {
82  }).Verifiable();
83 
84  var fakeServiceDescriptor = new List<ServiceDescriptor>()
85  {
86  new ServiceDescriptor(typeof(IApplicationLifetime), typeof(MockApplicationLifetime), ServiceLifetime.Singleton),
87  new ServiceDescriptor(typeof(ISetupWizard), typeof(MockSetupWizard), ServiceLifetime.Singleton),
88  new ServiceDescriptor(typeof(IOptions<GeneralConfiguration>), mockOptions.Object)
89  };
90 
91  var mockServiceCollection = new Mock<IServiceCollection>();
92 
93  var mockConfigSection = new Mock<IConfigurationSection>();
94 
95  mockConfiguration.Setup(x => x.GetSection(It.IsNotNull<string>())).Returns(mockConfigSection.Object).Verifiable();
96  mockServiceCollection.Setup(x => x.GetEnumerator()).Returns(() => fakeServiceDescriptor.GetEnumerator()).Verifiable();
97 
98  Assert.ThrowsException<OperationCanceledException>(() => app.ConfigureServices(mockServiceCollection.Object));
99 
100  mockOptions.VerifyAll();
101  mockConfiguration.VerifyAll();
102  mockServiceCollection.VerifyAll();
103  }
104  }
105 }
SetupWizardMode
Determines if the Core.ISetupWizard will run
+ - - - + + + + - +
The command line Configuration setup wizard
Definition: ISetupWizard.cs:9
diff --git a/_test_d_b_connection_factory_8cs_source.html b/_test_d_b_connection_factory_8cs_source.html index 9ffd778250..24b539411d 100644 --- a/_test_d_b_connection_factory_8cs_source.html +++ b/_test_d_b_connection_factory_8cs_source.html @@ -96,7 +96,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
diff --git a/_test_job_exception_8cs_source.html b/_test_job_exception_8cs_source.html index c6e6c1aff9..9b9f5ba37c 100644 --- a/_test_job_exception_8cs_source.html +++ b/_test_job_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TestJobException.cs
-Go to the documentation of this file.
1 using Microsoft.VisualStudio.TestTools.UnitTesting;
2 using System;
3 
4 namespace Tgstation.Server.Host.Core.Tests
5 {
6  [TestClass]
7  public sealed class TestJobException
8  {
9  [TestMethod]
10  public void TestConstruction()
11  {
12  new JobException();
13  new JobException("Message");
14  new JobException("Message", new Exception());
15  }
16  }
17 }
- +Go to the documentation of this file.
1 using Microsoft.VisualStudio.TestTools.UnitTesting;
2 using System;
3 
4 namespace Tgstation.Server.Host.Core.Tests
5 {
6  [TestClass]
7  public sealed class TestJobException
8  {
9  [TestMethod]
10  public void TestConstruction()
11  {
12  new JobException();
13  new JobException("Message");
14  new JobException("Message", new Exception());
15  }
16  }
17 }
diff --git a/_test_posix_byond_installer_8cs_source.html b/_test_posix_byond_installer_8cs_source.html index 548df6b243..a484b5ad2d 100644 --- a/_test_posix_byond_installer_8cs_source.html +++ b/_test_posix_byond_installer_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TestPosixByondInstaller.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Threading.Tasks;
6 using Tgstation.Server.Host.IO;
7 
8 namespace Tgstation.Server.Host.Components.Byond.Tests
9 {
10  [TestClass]
11  public sealed class TestPosixByondInstaller
12  {
13  [TestMethod]
14  public void TestConstruction()
15  {
16  Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(null, null, null));
17  var mockIOManager = new Mock<IIOManager>();
18  Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, null, null));
19  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
20  Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, null));
21 
22  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
23  new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
24  }
25 
26  [TestMethod]
27  public async Task TestCacheClean()
28  {
29  var mockIOManager = new Mock<IIOManager>();
30  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
31  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
32  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
33 
34  const string ByondCachePath = "~/.byond/cache";
35 
36  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Returns(Task.CompletedTask).Verifiable();
37 
38  await installer.CleanCache(default);
39 
40  mockIOManager.Verify();
41 
42 
43  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new OperationCanceledException()).Verifiable();
44 
45  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => installer.CleanCache(default)).ConfigureAwait(false);
46 
47  mockIOManager.Verify();
48 
49  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new Exception()).Verifiable();
50 
51  await installer.CleanCache(default).ConfigureAwait(false);
52 
53  mockIOManager.Verify();
54  }
55 
56 
57  [TestMethod]
58  public async Task TestDownload()
59  {
60  var mockIOManager = new Mock<IIOManager>();
61  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
62  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
63  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
64 
65  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.DownloadVersion(null, default)).ConfigureAwait(false);
66 
67  var ourArray = Array.Empty<byte>();
68  mockIOManager.Setup(x => x.DownloadFile(It.Is<Uri>(uri => uri == new Uri("https://secure.byond.com/download/build/511/511.1385_byond_linux.zip")), default)).Returns(Task.FromResult(ourArray)).Verifiable();
69 
70  var result = await installer.DownloadVersion(new Version(511, 1385), default).ConfigureAwait(false);
71 
72  Assert.AreSame(ourArray, result);
73  mockIOManager.Verify();
74  }
75 
76  [TestMethod]
77  public async Task TestInstallByond()
78  {
79  var mockIOManager = new Mock<IIOManager>();
80  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
81  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
82  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
83 
84  const string FakePath = "fake";
85  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(null, null, default)).ConfigureAwait(false);
86  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(FakePath, null, default)).ConfigureAwait(false);
87 
88  await installer.InstallByond(FakePath, new Version(511, 1385), default).ConfigureAwait(false);
89 
90  mockIOManager.Verify(x => x.ConcatPath(It.IsAny<string>(), It.IsNotNull<string>()), Times.Exactly(5));
91  mockIOManager.Verify(x => x.WriteAllBytes(It.IsAny<string>(), It.IsNotNull<byte[]>(), default), Times.Exactly(2));
92  mockPostWriteHandler.Verify(x => x.HandleWrite(It.IsAny<string>()), Times.Exactly(4));
93  }
94  }
95 }
- +Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Threading.Tasks;
6 using Tgstation.Server.Host.IO;
7 
8 namespace Tgstation.Server.Host.Components.Byond.Tests
9 {
10  [TestClass]
11  public sealed class TestPosixByondInstaller
12  {
13  [TestMethod]
14  public void TestConstruction()
15  {
16  Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(null, null, null));
17  var mockIOManager = new Mock<IIOManager>();
18  Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, null, null));
19  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
20  Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, null));
21 
22  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
23  new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
24  }
25 
26  [TestMethod]
27  public async Task TestCacheClean()
28  {
29  var mockIOManager = new Mock<IIOManager>();
30  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
31  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
32  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
33 
34  const string ByondCachePath = "~/.byond/cache";
35 
36  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Returns(Task.CompletedTask).Verifiable();
37 
38  await installer.CleanCache(default);
39 
40  mockIOManager.Verify();
41 
42 
43  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new OperationCanceledException()).Verifiable();
44 
45  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => installer.CleanCache(default)).ConfigureAwait(false);
46 
47  mockIOManager.Verify();
48 
49  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new Exception()).Verifiable();
50 
51  await installer.CleanCache(default).ConfigureAwait(false);
52 
53  mockIOManager.Verify();
54  }
55 
56 
57  [TestMethod]
58  public async Task TestDownload()
59  {
60  var mockIOManager = new Mock<IIOManager>();
61  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
62  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
63  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
64 
65  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.DownloadVersion(null, default)).ConfigureAwait(false);
66 
67  var ourArray = Array.Empty<byte>();
68  mockIOManager.Setup(x => x.DownloadFile(It.Is<Uri>(uri => uri == new Uri("https://secure.byond.com/download/build/511/511.1385_byond_linux.zip")), default)).Returns(Task.FromResult(ourArray)).Verifiable();
69 
70  var result = await installer.DownloadVersion(new Version(511, 1385), default).ConfigureAwait(false);
71 
72  Assert.AreSame(ourArray, result);
73  mockIOManager.Verify();
74  }
75 
76  [TestMethod]
77  public async Task TestInstallByond()
78  {
79  var mockIOManager = new Mock<IIOManager>();
80  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
81  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
82  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
83 
84  const string FakePath = "fake";
85  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(null, null, default)).ConfigureAwait(false);
86  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(FakePath, null, default)).ConfigureAwait(false);
87 
88  await installer.InstallByond(FakePath, new Version(511, 1385), default).ConfigureAwait(false);
89 
90  mockIOManager.Verify(x => x.ConcatPath(It.IsAny<string>(), It.IsNotNull<string>()), Times.Exactly(5));
91  mockIOManager.Verify(x => x.WriteAllBytes(It.IsAny<string>(), It.IsNotNull<byte[]>(), default), Times.Exactly(2));
92  mockPostWriteHandler.Verify(x => x.HandleWrite(It.IsAny<string>()), Times.Exactly(4));
93  }
94  }
95 }
diff --git a/_test_server_factory_8cs_source.html b/_test_server_factory_8cs_source.html index 29728e1715..d301d84e5b 100644 --- a/_test_server_factory_8cs_source.html +++ b/_test_server_factory_8cs_source.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
1 using Microsoft.VisualStudio.TestTools.UnitTesting;
2 using System;
3 
4 namespace Tgstation.Server.Host.Tests
5 {
9  [TestClass]
10  public sealed class TestServerFactory
11  {
12  [TestMethod]
14  {
15  var factory = new ServerFactory();
16 
17  Assert.ThrowsException<ArgumentNullException>(() => factory.CreateServer(null, null));
18  factory.CreateServer(Array.Empty<string>(), null);
19  }
20 
21  [TestMethod]
23  {
24  var factory = new ServerFactory();
25  const string Path = "/test";
26 
27  Assert.ThrowsException<ArgumentNullException>(() => factory.CreateServer(null, null));
28  Assert.ThrowsException<ArgumentNullException>(() => factory.CreateServer(null, Path));
29  factory.CreateServer(Array.Empty<string>(), Path);
30  }
31  }
32 }
- + diff --git a/_test_server_service_8cs_source.html b/_test_server_service_8cs_source.html index f66f5e91a2..3d33a53bc7 100644 --- a/_test_server_service_8cs_source.html +++ b/_test_server_service_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TestServerService.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Reflection;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Service.Tests
11 {
15  [TestClass]
16  public sealed class TestServerService
17  {
18  [TestMethod]
20  {
21  Assert.ThrowsException<ArgumentNullException>(() => new ServerService(null, null, default));
22  var mockWatchdogFactory = new Mock<IWatchdogFactory>();
23  Assert.ThrowsException<ArgumentNullException>(() => new ServerService(mockWatchdogFactory.Object, null, default));
24  var mockLoggerFactory = new LoggerFactory();
25  new ServerService(mockWatchdogFactory.Object, mockLoggerFactory, default).Dispose();
26  }
27 
28  [TestMethod]
29  public void TestRun()
30  {
31  var type = typeof(ServerService);
32  var onStart = type.GetMethod("OnStart", BindingFlags.Instance | BindingFlags.NonPublic);
33  var onStop = type.GetMethod("OnStop", BindingFlags.Instance | BindingFlags.NonPublic);
34 
35  var mockWatchdog = new Mock<IWatchdog>();
36  var args = Array.Empty<string>();
37  CancellationToken cancellationToken;
38  mockWatchdog.Setup(x => x.RunAsync(false, args, It.IsAny<CancellationToken>())).Callback((bool x, string[] _, CancellationToken token) => cancellationToken = token).Returns(Task.CompletedTask).Verifiable();
39  var mockWatchdogFactory = new Mock<IWatchdogFactory>();
40  var mockLoggerFactory = new LoggerFactory();
41  mockWatchdogFactory.Setup(x => x.CreateWatchdog(mockLoggerFactory)).Returns(mockWatchdog.Object).Verifiable();
42 
43  using (var service = new ServerService(mockWatchdogFactory.Object, mockLoggerFactory, default))
44  {
45  onStart.Invoke(service, new object[] { args });
46 
47  Assert.IsFalse(cancellationToken.IsCancellationRequested);
48 
49  onStop.Invoke(service, Array.Empty<object>());
50  Assert.IsTrue(cancellationToken.IsCancellationRequested);
51  mockWatchdog.VerifyAll();
52  }
53 
54  mockWatchdogFactory.VerifyAll();
55  }
56  }
57 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Reflection;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Service.Tests
11 {
15  [TestClass]
16  public sealed class TestServerService
17  {
18  [TestMethod]
20  {
21  Assert.ThrowsException<ArgumentNullException>(() => new ServerService(null, null, default));
22  var mockWatchdogFactory = new Mock<IWatchdogFactory>();
23  Assert.ThrowsException<ArgumentNullException>(() => new ServerService(mockWatchdogFactory.Object, null, default));
24  var mockLoggerFactory = new LoggerFactory();
25  new ServerService(mockWatchdogFactory.Object, mockLoggerFactory, default).Dispose();
26  }
27 
28  [TestMethod]
29  public void TestRun()
30  {
31  var type = typeof(ServerService);
32  var onStart = type.GetMethod("OnStart", BindingFlags.Instance | BindingFlags.NonPublic);
33  var onStop = type.GetMethod("OnStop", BindingFlags.Instance | BindingFlags.NonPublic);
34 
35  var mockWatchdog = new Mock<IWatchdog>();
36  var args = Array.Empty<string>();
37  CancellationToken cancellationToken, originalToken;
38  using(var cts = new CancellationTokenSource())
39  {
40  originalToken = cts.Token;
41  cancellationToken = originalToken;
42  }
43  var cancelled = false;
44 
45  mockWatchdog.Setup(x => x.RunAsync(false, args, It.IsAny<CancellationToken>())).Callback((bool x, string[] _, CancellationToken token) => token.Register(() => cancelled = true)).Returns(Task.CompletedTask).Verifiable();
46  var mockWatchdogFactory = new Mock<IWatchdogFactory>();
47  var mockLoggerFactory = new LoggerFactory();
48  mockWatchdogFactory.Setup(x => x.CreateWatchdog(mockLoggerFactory)).Returns(mockWatchdog.Object).Verifiable();
49 
50  using (var service = new ServerService(mockWatchdogFactory.Object, mockLoggerFactory, default))
51  {
52  Assert.IsFalse(cancelled);
53  onStart.Invoke(service, new object[] { args });
54  mockWatchdog.VerifyAll();
55 
56  Assert.AreNotSame(cancellationToken, originalToken);
57 
58  onStop.Invoke(service, Array.Empty<object>());
59  Assert.IsTrue(cancelled);
60  }
61  mockWatchdogFactory.VerifyAll();
62  }
63  }
64 }
override void Dispose(bool disposing)
diff --git a/_test_service_collection_extensions_8cs.html b/_test_service_collection_extensions_8cs.html new file mode 100644 index 0000000000..32e0cf5bb1 --- /dev/null +++ b/_test_service_collection_extensions_8cs.html @@ -0,0 +1,127 @@ + + + + + + +tgstation-server: tests/Tgstation.Server.Host.Tests/Core/TestServiceCollectionExtensions.cs File Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
TestServiceCollectionExtensions.cs File Reference
+
+ + + + + diff --git a/_test_service_collection_extensions_8cs_source.html b/_test_service_collection_extensions_8cs_source.html new file mode 100644 index 0000000000..4614e5674a --- /dev/null +++ b/_test_service_collection_extensions_8cs_source.html @@ -0,0 +1,114 @@ + + + + + + +tgstation-server: tests/Tgstation.Server.Host.Tests/Core/TestServiceCollectionExtensions.cs Source File + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
TestServiceCollectionExtensions.cs
+
+
+Go to the documentation of this file.
1 using Microsoft.Extensions.Configuration;
2 using Microsoft.Extensions.DependencyInjection;
3 using Microsoft.VisualStudio.TestTools.UnitTesting;
4 using Moq;
5 using System;
6 
7 namespace Tgstation.Server.Host.Core.Tests
8 {
9  [TestClass]
10  public sealed class TestServiceCollectionExtensions
11  {
12  class GoodConfig
13  {
14  public const string Section = "asdf";
15  }
16 
17  class BadConfig1
18  {
19  const string Section = "asdf";
20  }
21 
22  class BadConfig2
23  {
24  public const bool Section = false;
25  }
26 
27  class BadConfig3
28  {
29  //nah
30  }
31 
32  [TestMethod]
33  public void TestUseStandardConfig()
34  {
35  var serviceCollection = new ServiceCollection();
36  var mockConfig = new Mock<IConfigurationSection>();
37  mockConfig.Setup(x => x.GetSection(It.IsNotNull<string>())).Returns(mockConfig.Object).Verifiable();
38  Assert.ThrowsException<ArgumentNullException>(() => ServiceCollectionExtensions.UseStandardConfig<GoodConfig>(null, null));
39  Assert.ThrowsException<ArgumentNullException>(() => serviceCollection.UseStandardConfig<GoodConfig>(null));
40  serviceCollection.UseStandardConfig<GoodConfig>(mockConfig.Object);
41  Assert.ThrowsException<InvalidOperationException>(() => serviceCollection.UseStandardConfig<BadConfig1>(mockConfig.Object));
42  Assert.ThrowsException<InvalidOperationException>(() => serviceCollection.UseStandardConfig<BadConfig2>(mockConfig.Object));
43  Assert.ThrowsException<InvalidOperationException>(() => serviceCollection.UseStandardConfig<BadConfig3>(mockConfig.Object));
44  mockConfig.Verify();
45  }
46  }
47 }
+ + + + + + + + + + +
+ + + + diff --git a/_test_setup_wizard_8cs_source.html b/_test_setup_wizard_8cs_source.html index 9e76d5c2a9..d06a532fa7 100644 --- a/_test_setup_wizard_8cs_source.html +++ b/_test_setup_wizard_8cs_source.html @@ -92,11 +92,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TestSetupWizard.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using Microsoft.VisualStudio.TestTools.UnitTesting;
5 using Moq;
6 using Moq.Protected;
7 using System;
8 using System.Collections.Generic;
9 using System.Data.Common;
10 using System.Runtime.InteropServices;
11 using System.Text;
12 using System.Threading;
13 using System.Threading.Tasks;
15 using Tgstation.Server.Host.IO;
16 
17 namespace Tgstation.Server.Host.Core.Tests
18 {
19  [TestClass]
20  public sealed class TestSetupWizard
21  {
22  [TestMethod]
23  public void TestConstructionThrows()
24  {
25  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(null, null, null, null, null, null, null, null, null));
26  var mockIOManager = new Mock<IIOManager>();
27  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null));
28  var mockConsole = new Mock<IConsole>();
29  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null));
30  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
31  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null));
32  var mockApplication = new Mock<IApplication>();
33  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, null, null, null, null, null));
34  var mockDBConnectionFactory = new Mock<IDBConnectionFactory>();
35  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, null, null, null, null));
36  var mockPlatformIdentifier = new Mock<IPlatformIdentifier>();
37  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null));
38  var mockAsyncDelayer = new Mock<IAsyncDelayer>();
39  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null));
40  var mockLogger = new Mock<ILogger<SetupWizard>>();
41  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLogger.Object, null));
42  }
43 
44  [TestMethod]
45  public async Task TestWithUserStupiditiy()
46  {
47  var mockIOManager = new Mock<IIOManager>();
48  var mockConsole = new Mock<IConsole>();
49  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
50  var mockApplication = new Mock<IApplication>();
51  var mockDBConnectionFactory = new Mock<IDBConnectionFactory>();
52  var mockLogger = new Mock<ILogger<SetupWizard>>();
53  var mockGeneralConfigurationOptions = new Mock<IOptions<GeneralConfiguration>>();
54  var mockPlatformIdentifier = new Mock<IPlatformIdentifier>();
55  var mockAsyncDelayer = new Mock<IAsyncDelayer>();
56 
57  var testGeneralConfig = new GeneralConfiguration
58  {
60  };
61  mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(testGeneralConfig).Verifiable();
62 
63  var wizard = new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLogger.Object, mockGeneralConfigurationOptions.Object);
64 
65  mockPlatformIdentifier.SetupGet(x => x.IsWindows).Returns(true).Verifiable();
66  mockAsyncDelayer.Setup(x => x.Delay(It.IsAny<TimeSpan>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
67 
68  Assert.IsFalse(await wizard.CheckRunWizard(default).ConfigureAwait(false));
69 
70  testGeneralConfig.SetupWizardMode = SetupWizardMode.Force;
71  await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
72 
73  testGeneralConfig.SetupWizardMode = SetupWizardMode.Only;
74  await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
75 
76  mockConsole.SetupGet(x => x.Available).Returns(true).Verifiable();
77  mockIOManager.Setup(x => x.FileExists(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(true)).Verifiable();
78  mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(Encoding.UTF8.GetBytes("cucked"))).Verifiable();
79  mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
80 
81  var mockSuccessCommand = new Mock<DbCommand>();
82  mockSuccessCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(0)).Verifiable();
83  mockSuccessCommand.Setup(x => x.ExecuteScalarAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult<object>("1.2.3")).Verifiable();
84  var mockFailCommand = new Mock<DbCommand>();
85  mockFailCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
86 
87  void SetDbCommandCreator(Mock<DbConnection> mock, Func<DbCommand> creator) => mock.Protected().Setup<DbCommand>("CreateDbCommand").Returns(creator).Verifiable();
88 
89  var mockGoodDbConnection = new Mock<DbConnection>();
90  mockGoodDbConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
91  SetDbCommandCreator(mockGoodDbConnection, () => mockSuccessCommand.Object);
92 
93  var mockBadDbConnection = new Mock<DbConnection>();
94  mockBadDbConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
95  var invokeTimes = 0;
96  var mockUglyDbConnection = new Mock<DbConnection>();
97  SetDbCommandCreator(mockUglyDbConnection, () =>
98  {
99  if (invokeTimes < 2)
100  {
101  ++invokeTimes;
102  return mockSuccessCommand.Object;
103  }
104  else
105  return mockFailCommand.Object;
106  });
107 
108  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.SqlServer)).Returns(mockBadDbConnection.Object).Verifiable();
109  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.MariaDB)).Returns(mockGoodDbConnection.Object).Verifiable();
110  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.MySql)).Returns(mockUglyDbConnection.Object).Verifiable();
111 
112  var finalInputSequence = new List<string>()
113  {
114  //first run, just say no to the force prompt after testing it
115  "fake",
116  "n",
117  //second run say yes to the force prompt
118  "y",
119  //first normal run
120  "bad port number",
121  "0",
122  "666",
123  "FakeDBType",
124  nameof(DatabaseType.SqlServer),
125  "this isn't validated",
126  "nor is this",
127  "no",
128  //test winauth
129  "yes",
130  //sql server will always fail so reconfigure with maria
131  nameof(DatabaseType.MariaDB),
132  "bleh",
133  "blah",
134  "NO",
135  "user",
136  "pass",
137  //general config
138  "four",
139  "-12",
140  "16",
141  "eight",
142  "-27",
143  "5000",
144  "fake token",
145  //logging config
146  "no",
147  //saved, now for second run
148  //this time use defaults amap
149  String.Empty,
150  //test MySQL errors
151  nameof(DatabaseType.MySql),
152  String.Empty,
153  String.Empty,
154  "DbName",
155  "n",
156  "user",
157  "pass",
158  //general config
159  String.Empty,
160  String.Empty,
161  String.Empty,
162  //logging config
163  "y",
164  "not actually verified because lol mocks /../!@#$%^&*()/..///.",
165  "Warning",
166  String.Empty,
167  //third run, we already hit all the code coverage so just get through it
168  String.Empty,
169  nameof(DatabaseType.MariaDB),
170  String.Empty,
171  "dbname",
172  "y",
173  "user",
174  "pass",
175  String.Empty,
176  String.Empty,
177  String.Empty,
178  "y",
179  "will faile",
180  String.Empty,
181  String.Empty,
182  "fake",
183  "None",
184  "Critical"
185  };
186 
187  var inputPos = 0;
188 
189  mockApplication.SetupGet(x => x.VersionPrefix).Returns("sumfuk").Verifiable();
190 
191  mockConsole.Setup(x => x.PressAnyKeyAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
192  mockConsole.Setup(x => x.ReadLineAsync(It.IsAny<bool>(), It.IsAny<CancellationToken>())).Returns(() =>
193  {
194  if (inputPos == finalInputSequence.Count)
195  Assert.Fail("Exhausted input sequence!");
196  var res = finalInputSequence[inputPos++];
197  return Task.FromResult(res);
198  }).Verifiable();
199  mockConsole.Setup(x => x.WriteAsync(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
200 
201  Assert.IsFalse(await wizard.CheckRunWizard(default).ConfigureAwait(false));
202  //first real run
203  Assert.IsTrue(await wizard.CheckRunWizard(default).ConfigureAwait(false));
204 
205  //second run
206  mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(Encoding.UTF8.GetBytes(String.Empty))).Verifiable();
207  Assert.IsTrue(await wizard.CheckRunWizard(default).ConfigureAwait(false));
208 
209  //third run
210  testGeneralConfig.SetupWizardMode = SetupWizardMode.Autodetect;
211  mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
212  var firstRun = true;
213  mockIOManager.Setup(x => x.CreateDirectory(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(() =>
214  {
215  if (firstRun)
216  {
217  firstRun = false;
218  throw new Exception();
219  }
220  return Task.CompletedTask;
221  }).Verifiable();
222 
223  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
224 
225  Assert.AreEqual(finalInputSequence.Count, inputPos);
226  mockFailCommand.VerifyAll();
227  mockSuccessCommand.VerifyAll();
228  mockIOManager.VerifyAll();
229  mockGeneralConfigurationOptions.VerifyAll();
230  mockConsole.VerifyAll();
231  mockGoodDbConnection.VerifyAll();
232  mockBadDbConnection.VerifyAll();
233  mockUglyDbConnection.VerifyAll();
234  mockDBConnectionFactory.VerifyAll();
235  mockApplication.VerifyAll();
236  mockPlatformIdentifier.VerifyAll();
237  mockAsyncDelayer.VerifyAll();
238  }
239  }
240 }
- +Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using Microsoft.VisualStudio.TestTools.UnitTesting;
5 using Moq;
6 using Moq.Protected;
7 using System;
8 using System.Collections.Generic;
9 using System.Data.Common;
10 using System.Runtime.InteropServices;
11 using System.Text;
12 using System.Threading;
13 using System.Threading.Tasks;
15 using Tgstation.Server.Host.IO;
16 
17 namespace Tgstation.Server.Host.Core.Tests
18 {
19  [TestClass]
20  public sealed class TestSetupWizard
21  {
22  [TestMethod]
23  public void TestConstructionThrows()
24  {
25  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(null, null, null, null, null, null, null, null, null));
26  var mockIOManager = new Mock<IIOManager>();
27  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, null, null, null, null, null, null, null, null));
28  var mockConsole = new Mock<IConsole>();
29  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, null, null, null, null, null, null, null));
30  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
31  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, null, null, null, null, null, null));
32  var mockApplication = new Mock<IApplication>();
33  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, null, null, null, null, null));
34  var mockDBConnectionFactory = new Mock<IDBConnectionFactory>();
35  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, null, null, null, null));
36  var mockPlatformIdentifier = new Mock<IPlatformIdentifier>();
37  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, null, null, null));
38  var mockAsyncDelayer = new Mock<IAsyncDelayer>();
39  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, null, null));
40  var mockLogger = new Mock<ILogger<SetupWizard>>();
41  Assert.ThrowsException<ArgumentNullException>(() => new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLogger.Object, null));
42  }
43 
44  [TestMethod]
45  public async Task TestWithUserStupiditiy()
46  {
47  var mockIOManager = new Mock<IIOManager>();
48  var mockConsole = new Mock<IConsole>();
49  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
50  var mockApplication = new Mock<IApplication>();
51  var mockDBConnectionFactory = new Mock<IDBConnectionFactory>();
52  var mockLogger = new Mock<ILogger<SetupWizard>>();
53  var mockGeneralConfigurationOptions = new Mock<IOptions<GeneralConfiguration>>();
54  var mockPlatformIdentifier = new Mock<IPlatformIdentifier>();
55  var mockAsyncDelayer = new Mock<IAsyncDelayer>();
56 
57  var testGeneralConfig = new GeneralConfiguration
58  {
60  };
61  mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(testGeneralConfig).Verifiable();
62 
63  var wizard = new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLogger.Object, mockGeneralConfigurationOptions.Object);
64 
65  mockPlatformIdentifier.SetupGet(x => x.IsWindows).Returns(true).Verifiable();
66  mockAsyncDelayer.Setup(x => x.Delay(It.IsAny<TimeSpan>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
67 
68  Assert.IsFalse(await wizard.CheckRunWizard(default).ConfigureAwait(false));
69 
70  testGeneralConfig.SetupWizardMode = SetupWizardMode.Force;
71  await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
72 
73  testGeneralConfig.SetupWizardMode = SetupWizardMode.Only;
74  await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
75 
76  mockConsole.SetupGet(x => x.Available).Returns(true).Verifiable();
77  mockIOManager.Setup(x => x.FileExists(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(true)).Verifiable();
78  mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(Encoding.UTF8.GetBytes("cucked"))).Verifiable();
79  mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
80 
81  var mockSuccessCommand = new Mock<DbCommand>();
82  mockSuccessCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(0)).Verifiable();
83  mockSuccessCommand.Setup(x => x.ExecuteScalarAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult<object>("1.2.3")).Verifiable();
84  var mockFailCommand = new Mock<DbCommand>();
85  mockFailCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
86 
87  void SetDbCommandCreator(Mock<DbConnection> mock, Func<DbCommand> creator) => mock.Protected().Setup<DbCommand>("CreateDbCommand").Returns(creator).Verifiable();
88 
89  var mockGoodDbConnection = new Mock<DbConnection>();
90  mockGoodDbConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
91  SetDbCommandCreator(mockGoodDbConnection, () => mockSuccessCommand.Object);
92 
93  var mockBadDbConnection = new Mock<DbConnection>();
94  mockBadDbConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
95  var invokeTimes = 0;
96  var mockUglyDbConnection = new Mock<DbConnection>();
97  SetDbCommandCreator(mockUglyDbConnection, () =>
98  {
99  if (invokeTimes < 2)
100  {
101  ++invokeTimes;
102  return mockSuccessCommand.Object;
103  }
104  else
105  return mockFailCommand.Object;
106  });
107 
108  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.SqlServer)).Returns(mockBadDbConnection.Object).Verifiable();
109  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.MariaDB)).Returns(mockGoodDbConnection.Object).Verifiable();
110  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.MySql)).Returns(mockUglyDbConnection.Object).Verifiable();
111 
112  var finalInputSequence = new List<string>()
113  {
114  //first run, just say no to the force prompt after testing it
115  "fake",
116  "n",
117  //second run say yes to the force prompt
118  "y",
119  //first normal run
120  "bad port number",
121  "0",
122  "666",
123  "FakeDBType",
124  nameof(DatabaseType.SqlServer),
125  "this isn't validated",
126  "nor is this",
127  "no",
128  //test winauth
129  "yes",
130  //sql server will always fail so reconfigure with maria
131  nameof(DatabaseType.MariaDB),
132  "bleh",
133  "blah",
134  "NO",
135  "user",
136  "pass",
137  //general config
138  "four",
139  "-12",
140  "16",
141  "eight",
142  "-27",
143  "5000",
144  "fake token",
145  //logging config
146  "no",
147  //cp config
148  "y",
149  "y",
150  //saved, now for second run
151  //this time use defaults amap
152  String.Empty,
153  //test MySQL errors
154  nameof(DatabaseType.MySql),
155  String.Empty,
156  String.Empty,
157  "DbName",
158  "n",
159  "user",
160  "pass",
161  //general config
162  "y",
163  String.Empty,
164  String.Empty,
165  String.Empty,
166  //logging config
167  "y",
168  "not actually verified because lol mocks /../!@#$%^&*()/..///.",
169  "Warning",
170  String.Empty,
171  //cp config
172  "y",
173  "n",
174  String.Empty,
175  //third run, we already hit all the code coverage so just get through it
176  String.Empty,
177  nameof(DatabaseType.MariaDB),
178  String.Empty,
179  "dbname",
180  "y",
181  "user",
182  "pass",
183  //general
184  "y",
185  String.Empty,
186  String.Empty,
187  String.Empty,
188  "y",
189  "will faile",
190  String.Empty,
191  String.Empty,
192  "fake",
193  "None",
194  "Critical",
195  //cp config
196  "y",
197  "n",
198  "http://fake.com, https://example.org",
199  };
200 
201  var inputPos = 0;
202 
203  mockApplication.SetupGet(x => x.VersionPrefix).Returns("sumfuk").Verifiable();
204 
205  mockConsole.Setup(x => x.PressAnyKeyAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
206  mockConsole.Setup(x => x.ReadLineAsync(It.IsAny<bool>(), It.IsAny<CancellationToken>())).Returns(() =>
207  {
208  if (inputPos == finalInputSequence.Count)
209  Assert.Fail("Exhausted input sequence!");
210  var res = finalInputSequence[inputPos++];
211  return Task.FromResult(res);
212  }).Verifiable();
213  mockConsole.Setup(x => x.WriteAsync(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
214 
215  Assert.IsFalse(await wizard.CheckRunWizard(default).ConfigureAwait(false));
216  //first real run
217  Assert.IsTrue(await wizard.CheckRunWizard(default).ConfigureAwait(false));
218 
219  //second run
220  mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(Encoding.UTF8.GetBytes(String.Empty))).Verifiable();
221  Assert.IsTrue(await wizard.CheckRunWizard(default).ConfigureAwait(false));
222 
223  //third run
224  testGeneralConfig.SetupWizardMode = SetupWizardMode.Autodetect;
225  mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
226  var firstRun = true;
227  mockIOManager.Setup(x => x.CreateDirectory(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(() =>
228  {
229  if (firstRun)
230  {
231  firstRun = false;
232  throw new Exception();
233  }
234  return Task.CompletedTask;
235  }).Verifiable();
236 
237  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
238 
239  Assert.AreEqual(finalInputSequence.Count, inputPos);
240  mockFailCommand.VerifyAll();
241  mockSuccessCommand.VerifyAll();
242  mockIOManager.VerifyAll();
243  mockGeneralConfigurationOptions.VerifyAll();
244  mockConsole.VerifyAll();
245  mockGoodDbConnection.VerifyAll();
246  mockBadDbConnection.VerifyAll();
247  mockUglyDbConnection.VerifyAll();
248  mockDBConnectionFactory.VerifyAll();
249  mockApplication.VerifyAll();
250  mockPlatformIdentifier.VerifyAll();
251  mockAsyncDelayer.VerifyAll();
252  }
253  }
254 }
SetupWizardMode
Determines if the Core.ISetupWizard will run
- +
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
diff --git a/_testing_server_8cs_source.html b/_testing_server_8cs_source.html index 1a5b127021..6b0ef1433d 100644 --- a/_testing_server_8cs_source.html +++ b/_testing_server_8cs_source.html @@ -98,10 +98,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
Represents the host
Definition: IServer.cs:9
- -
IServer CreateServer(string[] args, string updatePath)
Create a IServer
+ +
IServer CreateServer(string[] args, string updatePath)
Create a IServer
diff --git a/_tgstation_8_server_8_api_8_assembly_info_8cs.html b/_tgstation_8_server_8_api_8_assembly_info_8cs.html index 88fc90fce7..823d805b2e 100644 --- a/_tgstation_8_server_8_api_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_api_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Api/obj/Release/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Api/obj/Debug/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html index 3f1185acce..c303cd7fb3 100644 --- a/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Api/obj/Release/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Api/obj/Debug/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Api.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("API definitions for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.0.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.0.0")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.0.0")]
23 
24 // Generated by the MSBuild WriteCodeFragment class.
25 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("API definitions for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.0.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.0.0")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.0.0")]
23 
24 // Generated by the MSBuild WriteCodeFragment class.
25 
diff --git a/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html index f0cb13b64e..8aba9d97b3 100644 --- a/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Api.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Api.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Api.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Api.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Api.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html index f735cdd6bf..e0e4ccf475 100644 --- a/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Api.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Api.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_client_8_assembly_info_8cs.html b/_tgstation_8_server_8_client_8_assembly_info_8cs.html index c1b2251f60..fb681e6583 100644 --- a/_tgstation_8_server_8_client_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_client_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Client/obj/Release/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Client/obj/Debug/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html index b98b2e720e..5c3ccaf7e9 100644 --- a/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Client/obj/Release/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Client/obj/Debug/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Client.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station 13")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("Client library for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.2.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.2")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.2.0")]
23 [assembly: System.Resources.NeutralResourcesLanguageAttribute("en-CA")]
24 
25 // Generated by the MSBuild WriteCodeFragment class.
26 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station 13")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("Client library for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.2.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.2")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.2.0")]
23 [assembly: System.Resources.NeutralResourcesLanguageAttribute("en-CA")]
24 
25 // Generated by the MSBuild WriteCodeFragment class.
26 
diff --git a/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html index 3eef0f64ad..3c2a097db5 100644 --- a/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Client.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Client.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Client.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Client.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Client.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html index 0bb889fa5e..9ef26d4cd5 100644 --- a/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Client.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Client.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_2_components_2_compiler_2_dream_maker_8cs_source.html b/_tgstation_8_server_8_host_2_components_2_compiler_2_dream_maker_8cs_source.html index 656ab93136..99dceefe03 100644 --- a/_tgstation_8_server_8_host_2_components_2_compiler_2_dream_maker_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_compiler_2_dream_maker_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamMaker.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.Linq;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
17 
18 namespace Tgstation.Server.Host.Components.Compiler
19 {
21  sealed class DreamMaker : IDreamMaker
22  {
26  public const string ADirectoryName = "A";
30  public const string BDirectoryName = "B";
34  public const string DmbExtension = ".dmb";
38  const string DmeExtension = "dme";
39 
43  readonly IByondManager byond;
51  readonly StaticFiles.IConfiguration configuration;
71  readonly IChat chat;
79  readonly IWatchdog watchdog;
83  readonly ILogger<DreamMaker> logger;
84 
88  bool compiling;
89 
104  public DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, IProcessExecutor processExecutor, IWatchdog watchdog, ILogger<DreamMaker> logger)
105  {
106  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
107  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
108  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
109  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
110  this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
111  this.application = application ?? throw new ArgumentNullException(nameof(application));
112  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
113  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
114  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
115  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
116  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
117  }
118 
129  async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
130  {
131  logger.LogTrace("Verifying DMAPI...");
132  var launchParameters = new DreamDaemonLaunchParameters
133  {
134  AllowWebClient = false,
135  PrimaryPort = portToUse,
136  SecurityLevel = securityLevel, //all it needs to read the file and exit
137  StartupTimeout = timeout
138  };
139 
140  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
141 
142  job.MinimumSecurityLevel = securityLevel; //needed for the TempDmbProvider
143  var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job);
144 
145  var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);
146  using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false))
147  {
148  var launchResult = await controller.LaunchResult.ConfigureAwait(false);
149 
150  var now = DateTimeOffset.Now;
151  if (now < timeoutAt && launchResult.StartupTime.HasValue)
152  {
153  var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken);
154 
155  await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);
156  cancellationToken.ThrowIfCancellationRequested();
157  }
158 
159  if (controller.Lifetime.IsCompleted)
160  {
161  var validationStatus = controller.ApiValidationStatus;
162  logger.LogTrace("API validation status: {0}", validationStatus);
163  switch (validationStatus)
164  {
165  case ApiValidationStatus.RequiresUltrasafe:
166  job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
167  return;
168  case ApiValidationStatus.RequiresSafe:
169  if (securityLevel == DreamDaemonSecurity.Ultrasafe)
170  throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!");
171  job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
172  return;
173  case ApiValidationStatus.RequiresTrusted:
174  if (securityLevel != DreamDaemonSecurity.Trusted)
175  throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!");
176  job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
177  return;
178  case ApiValidationStatus.NeverValidated:
179  break;
180  case ApiValidationStatus.BadValidationRequest:
181  throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!");
182  case ApiValidationStatus.UnaskedValidationRequest:
183  default:
184  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus));
185  }
186  }
187 
188  throw new JobException("DMAPI validation timed out!");
189  }
190  }
191 
199  async Task<int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
200  {
201  using (var dm = processExecutor.LaunchProcess(dreamMakerPath, ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)), String.Format(CultureInfo.InvariantCulture, "-clean {0}.{1}", job.DmeName, DmeExtension), true, true))
202  {
203  int exitCode;
204  using (cancellationToken.Register(() => dm.Terminate()))
205  exitCode = await dm.Lifetime.ConfigureAwait(false);
206  cancellationToken.ThrowIfCancellationRequested();
207 
208  logger.LogDebug("DreamMaker exit code: {0}", exitCode);
209  job.Output = dm.GetCombinedOutput();
210  logger.LogTrace("DreamMaker output: {0}{1}", Environment.NewLine, job.Output);
211  return exitCode;
212  }
213  }
214 
221  async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
222  {
223  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
224  var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
225  var dmePath = ioManager.ConcatPath(dirA, dmeFileName);
226  var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
227 
228  var dmeModificationsTask = configuration.CopyDMFilesTo(dmeFileName, ioManager.ResolvePath(dirA), cancellationToken);
229 
230  var dmeBytes = await dmeReadTask.ConfigureAwait(false);
231  var dme = Encoding.UTF8.GetString(dmeBytes);
232 
233  var dmeModifications = await dmeModificationsTask.ConfigureAwait(false);
234 
235  if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
236  {
237  if (dmeModifications != null)
238  logger.LogDebug(".dme replacement configured!");
239  else
240  logger.LogTrace("No .dme modifications required.");
241  return;
242  }
243 
244  if (dmeModifications.HeadIncludeLine != null)
245  logger.LogDebug("Head .dme include line: {0}", dmeModifications.HeadIncludeLine);
246  if (dmeModifications.TailIncludeLine != null)
247  logger.LogDebug("Tail .dme include line: {0}", dmeModifications.TailIncludeLine);
248 
249  var dmeLines = new List<string>(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None));
250  for (var I = 0; I < dmeLines.Count; ++I)
251  {
252  var line = dmeLines[I];
253  if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
254  {
255  dmeLines.Insert(I + 1, dmeModifications.HeadIncludeLine);
256  ++I;
257  }
258  else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
259  {
260  dmeLines.Insert(I, dmeModifications.TailIncludeLine);
261  break;
262  }
263  }
264 
265  dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines));
266  await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false);
267  }
268 
270  public async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
271  {
272  if (revisionInformation == null)
273  throw new ArgumentNullException(nameof(revisionInformation));
274 
275  if (dreamMakerSettings == null)
276  throw new ArgumentNullException(nameof(dreamMakerSettings));
277 
278  if (repository == null)
279  throw new ArgumentNullException(nameof(repository));
280 
281  if (progressReporter == null)
282  throw new ArgumentNullException(nameof(progressReporter));
283 
284  if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
285  throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!");
286 
287  logger.LogTrace("Begin Compile");
288 
289  var job = new Models.CompileJob
290  {
291  DirectoryName = Guid.NewGuid(),
292  DmeName = dreamMakerSettings.ProjectName,
293  RevisionInformation = revisionInformation
294  };
295 
296  logger.LogTrace("Compile output GUID: {0}", job.DirectoryName);
297 
298  lock (this)
299  {
300  if (compiling)
301  throw new JobException("There is already a compile job in progress!");
302  compiling = true;
303  }
304 
305  using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
306  {
307  async Task ProgressTask()
308  {
309  if (!estimatedDuration.HasValue)
310  return;
311 
312  progressReporter(0);
313  var ct = progressCts.Token;
314  var sleepInterval = estimatedDuration.Value / 100;
315 
316  try
317  {
318  for (var I = 0; I < 99; ++I)
319  {
320  await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false);
321  progressReporter(I + 1);
322  }
323  }
324  catch (OperationCanceledException) { }
325  }
326 
327  var progressTask = ProgressTask();
328  try
329  {
330 
331 
332  var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
333  string remoteCommitInsert;
334  if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
335  {
336  commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
337  remoteCommitInsert = String.Empty;
338  }
339  else
340  remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
341 
342  var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
343  String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
344  {
345  var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
346  if (x.Comment != null)
347  result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
348  return result;
349  })));
350 
351  using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false))
352  {
353  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false);
354 
355  async Task CleanupFailedCompile(bool cancelled)
356  {
357  logger.LogTrace("Cleaning compile directory...");
358  var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);
359  try
360  {
361  await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
362  }
363  catch (Exception e)
364  {
365  logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e);
366  }
367 
368  await chatTask.ConfigureAwait(false);
369  };
370 
371  try
372  {
373  await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
374 
375  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
376  var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
377 
378  logger.LogTrace("Copying repository to game directory...");
379  //copy the repository
380  var fullDirA = ioManager.ResolvePath(dirA);
381  var repoOrigin = repository.Origin;
382  using (repository)
383  await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
384 
385  //run precompile scripts
386  var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
387  await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
388 
389  //determine the dme
390  if (job.DmeName == null)
391  {
392  logger.LogTrace("Searching for available .dmes...");
393  var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
394  if (path == default)
395  throw new JobException("Unable to find any .dme!");
396  var dmeWithExtension = ioManager.GetFileName(path);
397  job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
398  }
399  else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false))
400  throw new JobException("Unable to locate specified .dme!");
401 
402  logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);
403 
404  await ModifyDme(job, cancellationToken).ConfigureAwait(false);
405 
406  //run compiler, verify api
407  job.ByondVersion = byondLock.Version.ToString();
408 
409  var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
410 
411  try
412  {
413  if (exitCode != 0)
414  throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
415 
416  await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
417  }
418  catch (JobException)
419  {
420  //server never validated or compile failed
421  await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
422  throw;
423  }
424 
425  logger.LogTrace("Running post compile event...");
426  await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false);
427 
428  logger.LogTrace("Duplicating compiled game...");
429 
430  //duplicate the dmb et al
431  await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
432 
433  logger.LogTrace("Applying static game file symlinks...");
434 
435  //symlink in the static data
436  var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
437  var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
438 
439  await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
440 
441  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false);
442 
443  logger.LogDebug("Compile complete!");
444  return job;
445  }
446  catch (Exception e)
447  {
448  await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false);
449  throw;
450  }
451  }
452  }
453  catch (OperationCanceledException)
454  {
455  await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);
456  throw;
457  }
458  finally
459  {
460  compiling = false;
461  progressCts.Cancel();
462  await progressTask.ConfigureAwait(false);
463  }
464  }
465  }
466  }
467 }
- +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.Linq;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
17 
18 namespace Tgstation.Server.Host.Components.Compiler
19 {
21  sealed class DreamMaker : IDreamMaker
22  {
26  public const string ADirectoryName = "A";
30  public const string BDirectoryName = "B";
34  public const string DmbExtension = ".dmb";
38  const string DmeExtension = "dme";
39 
43  readonly IByondManager byond;
51  readonly StaticFiles.IConfiguration configuration;
71  readonly IChat chat;
79  readonly IWatchdog watchdog;
83  readonly ILogger<DreamMaker> logger;
84 
88  bool compiling;
89 
104  public DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, IProcessExecutor processExecutor, IWatchdog watchdog, ILogger<DreamMaker> logger)
105  {
106  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
107  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
108  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
109  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
110  this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
111  this.application = application ?? throw new ArgumentNullException(nameof(application));
112  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
113  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
114  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
115  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
116  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
117  }
118 
129  async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
130  {
131  logger.LogTrace("Verifying DMAPI...");
132  var launchParameters = new DreamDaemonLaunchParameters
133  {
134  AllowWebClient = false,
135  PrimaryPort = portToUse,
136  SecurityLevel = securityLevel, //all it needs to read the file and exit
137  StartupTimeout = timeout
138  };
139 
140  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
141 
142  job.MinimumSecurityLevel = securityLevel; //needed for the TempDmbProvider
143  var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job);
144 
145  var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);
146  using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false))
147  {
148  var launchResult = await controller.LaunchResult.ConfigureAwait(false);
149 
150  var now = DateTimeOffset.Now;
151  if (now < timeoutAt && launchResult.StartupTime.HasValue)
152  {
153  var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken);
154 
155  await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);
156  cancellationToken.ThrowIfCancellationRequested();
157  }
158 
159  if (controller.Lifetime.IsCompleted)
160  {
161  var validationStatus = controller.ApiValidationStatus;
162  logger.LogTrace("API validation status: {0}", validationStatus);
163  switch (validationStatus)
164  {
165  case ApiValidationStatus.RequiresUltrasafe:
166  job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
167  return;
168  case ApiValidationStatus.RequiresSafe:
169  if (securityLevel == DreamDaemonSecurity.Ultrasafe)
170  throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!");
171  job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
172  return;
173  case ApiValidationStatus.RequiresTrusted:
174  if (securityLevel != DreamDaemonSecurity.Trusted)
175  throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!");
176  job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
177  return;
178  case ApiValidationStatus.NeverValidated:
179  break;
180  case ApiValidationStatus.BadValidationRequest:
181  throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!");
182  case ApiValidationStatus.UnaskedValidationRequest:
183  default:
184  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus));
185  }
186  }
187 
188  throw new JobException("DMAPI validation timed out!");
189  }
190  }
191 
199  async Task<int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
200  {
201  using (var dm = processExecutor.LaunchProcess(dreamMakerPath, ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)), String.Format(CultureInfo.InvariantCulture, "-clean {0}.{1}", job.DmeName, DmeExtension), true, true))
202  {
203  int exitCode;
204  using (cancellationToken.Register(() => dm.Terminate()))
205  exitCode = await dm.Lifetime.ConfigureAwait(false);
206  cancellationToken.ThrowIfCancellationRequested();
207 
208  logger.LogDebug("DreamMaker exit code: {0}", exitCode);
209  job.Output = dm.GetCombinedOutput();
210  logger.LogTrace("DreamMaker output: {0}{1}", Environment.NewLine, job.Output);
211  return exitCode;
212  }
213  }
214 
221  async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
222  {
223  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
224  var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
225  var dmePath = ioManager.ConcatPath(dirA, dmeFileName);
226  var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
227 
228  var dmeModificationsTask = configuration.CopyDMFilesTo(dmeFileName, ioManager.ResolvePath(dirA), cancellationToken);
229 
230  var dmeBytes = await dmeReadTask.ConfigureAwait(false);
231  var dme = Encoding.UTF8.GetString(dmeBytes);
232 
233  var dmeModifications = await dmeModificationsTask.ConfigureAwait(false);
234 
235  if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
236  {
237  if (dmeModifications != null)
238  logger.LogDebug(".dme replacement configured!");
239  else
240  logger.LogTrace("No .dme modifications required.");
241  return;
242  }
243 
244  if (dmeModifications.HeadIncludeLine != null)
245  logger.LogDebug("Head .dme include line: {0}", dmeModifications.HeadIncludeLine);
246  if (dmeModifications.TailIncludeLine != null)
247  logger.LogDebug("Tail .dme include line: {0}", dmeModifications.TailIncludeLine);
248 
249  var dmeLines = new List<string>(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None));
250  for (var I = 0; I < dmeLines.Count; ++I)
251  {
252  var line = dmeLines[I];
253  if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
254  {
255  dmeLines.Insert(I + 1, dmeModifications.HeadIncludeLine);
256  ++I;
257  }
258  else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
259  {
260  dmeLines.Insert(I, dmeModifications.TailIncludeLine);
261  break;
262  }
263  }
264 
265  dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines));
266  await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false);
267  }
268 
270  public async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
271  {
272  if (revisionInformation == null)
273  throw new ArgumentNullException(nameof(revisionInformation));
274 
275  if (dreamMakerSettings == null)
276  throw new ArgumentNullException(nameof(dreamMakerSettings));
277 
278  if (repository == null)
279  throw new ArgumentNullException(nameof(repository));
280 
281  if (progressReporter == null)
282  throw new ArgumentNullException(nameof(progressReporter));
283 
284  if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
285  throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!");
286 
287  logger.LogTrace("Begin Compile");
288 
289  var job = new Models.CompileJob
290  {
291  DirectoryName = Guid.NewGuid(),
292  DmeName = dreamMakerSettings.ProjectName,
293  RevisionInformation = revisionInformation
294  };
295 
296  logger.LogTrace("Compile output GUID: {0}", job.DirectoryName);
297 
298  lock (this)
299  {
300  if (compiling)
301  throw new JobException("There is already a compile job in progress!");
302  compiling = true;
303  }
304 
305  using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
306  {
307  async Task ProgressTask()
308  {
309  if (!estimatedDuration.HasValue)
310  return;
311 
312  progressReporter(0);
313  var ct = progressCts.Token;
314  var sleepInterval = estimatedDuration.Value / 100;
315 
316  try
317  {
318  for (var I = 0; I < 99; ++I)
319  {
320  await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false);
321  progressReporter(I + 1);
322  }
323  }
324  catch (OperationCanceledException) { }
325  }
326 
327  var progressTask = ProgressTask();
328  try
329  {
330 
331 
332  var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
333  string remoteCommitInsert;
334  if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
335  {
336  commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
337  remoteCommitInsert = String.Empty;
338  }
339  else
340  remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
341 
342  var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
343  String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
344  {
345  var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
346  if (x.Comment != null)
347  result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
348  return result;
349  })));
350 
351  using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false))
352  {
353  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false);
354 
355  async Task CleanupFailedCompile(bool cancelled)
356  {
357  logger.LogTrace("Cleaning compile directory...");
358  var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);
359  try
360  {
361  await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
362  }
363  catch (Exception e)
364  {
365  logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e);
366  }
367 
368  await chatTask.ConfigureAwait(false);
369  };
370 
371  try
372  {
373  await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
374 
375  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
376  var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
377 
378  logger.LogTrace("Copying repository to game directory...");
379  //copy the repository
380  var fullDirA = ioManager.ResolvePath(dirA);
381  var repoOrigin = repository.Origin;
382  using (repository)
383  await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
384 
385  //run precompile scripts
386  var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
387  await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
388 
389  //determine the dme
390  if (job.DmeName == null)
391  {
392  logger.LogTrace("Searching for available .dmes...");
393  var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
394  if (path == default)
395  throw new JobException("Unable to find any .dme!");
396  var dmeWithExtension = ioManager.GetFileName(path);
397  job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
398  }
399  else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false))
400  throw new JobException("Unable to locate specified .dme!");
401 
402  logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);
403 
404  await ModifyDme(job, cancellationToken).ConfigureAwait(false);
405 
406  //run compiler, verify api
407  job.ByondVersion = byondLock.Version.ToString();
408 
409  var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
410 
411  try
412  {
413  if (exitCode != 0)
414  throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
415 
416  await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
417  }
418  catch (JobException)
419  {
420  //server never validated or compile failed
421  await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
422  throw;
423  }
424 
425  logger.LogTrace("Running post compile event...");
426  await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false);
427 
428  logger.LogTrace("Duplicating compiled game...");
429 
430  //duplicate the dmb et al
431  await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
432 
433  logger.LogTrace("Applying static game file symlinks...");
434 
435  //symlink in the static data
436  var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
437  var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
438 
439  await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
440 
441  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false);
442 
443  logger.LogDebug("Compile complete!");
444  return job;
445  }
446  catch (Exception e)
447  {
448  await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false);
449  throw;
450  }
451  }
452  }
453  catch (OperationCanceledException)
454  {
455  await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);
456  throw;
457  }
458  finally
459  {
460  compiling = false;
461  progressCts.Cancel();
462  await progressTask.ConfigureAwait(false);
463  }
464  }
465  }
466  }
467 }
readonly ILogger< DreamMaker > logger
The ILogger for DreamMaker
Definition: DreamMaker.cs:83
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 49b8ec6190..954f0fc938 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 @@ -92,10 +92,9 @@ 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 Octokit;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
18 
19 namespace Tgstation.Server.Host.Components
20 {
22  sealed class Instance : IInstance
23  {
26 
28  public IByondManager ByondManager { get; }
29 
31  public IDreamMaker DreamMaker { get; }
32 
34  public IWatchdog Watchdog { get; }
35 
37  public IChat Chat { get; }
38 
40  public StaticFiles.IConfiguration Configuration { get; }
41 
43  public ICompileJobConsumer CompileJobConsumer { get; }
44 
49 
54 
59 
64 
69 
73  readonly ILogger<Instance> logger;
74 
78  readonly Api.Models.Instance metadata;
79 
83  Task timerTask;
87  CancellationTokenSource timerCts;
88 
106  public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IGitHubClientFactory gitHubClientFactory, ILogger<Instance> logger)
107  {
108  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
109  RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
110  ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
111  DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
112  Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
113  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
114  Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
115  CompileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
116  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
117  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
118  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
119  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
120  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
121  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
122  }
123 
125  public void Dispose()
126  {
127  timerCts?.Dispose();
128  CompileJobConsumer.Dispose();
129  Configuration.Dispose();
130  Chat.Dispose();
131  Watchdog.Dispose();
133  }
134 
136  public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action<int> progressReporter, CancellationToken cancellationToken)
137  {
138  //DO NOT FOLLOW THE SUGGESTION FOR A THROW EXPRESSION HERE
139  if (job == null)
140  throw new ArgumentNullException(nameof(job));
141  if (databaseContext == null)
142  throw new ArgumentNullException(nameof(databaseContext));
143  if (progressReporter == null)
144  throw new ArgumentNullException(nameof(progressReporter));
145 
146  var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings
147  {
148  StartupTimeout = x.StartupTimeout,
149  }).FirstOrDefaultAsync(cancellationToken);
150 
151  var compileJobsTask = databaseContext.CompileJobs
152  .Where(x => x.Job.Instance.Id == metadata.Id)
153  .OrderByDescending(x => x.Job.StoppedAt)
154  //TODO: Replace with this select when the issues linked in https://github.com/tgstation/tgstation-server/issues/737 are fixed
155  //.Select(x => x.Job.StoppedAt.Value - x.Job.StartedAt.Value)
156  .Select(x => new Job
157  {
158  StoppedAt = x.Job.StoppedAt,
159  StartedAt = x.Job.StartedAt
160  })
161  .Take(10)
162  .ToListAsync(cancellationToken);
163 
164  var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
165  if (dreamMakerSettings == default)
166  throw new JobException("Missing DreamMakerSettings in DB!");
167  var ddSettings = await ddSettingsTask.ConfigureAwait(false);
168  if (ddSettings == default)
169  throw new JobException("Missing DreamDaemonSettings in DB!");
170 
171  Task<RepositorySettings> repositorySettingsTask = null;
172  string repoOwner = null;
173  string repoName = null;
174  CompileJob compileJob;
175  RevisionInformation revInfo;
176  using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
177  {
178  if (repo == null)
179  throw new JobException("Missing Repository!");
180 
181  if (repo.IsGitHubRepository)
182  {
183  repoOwner = repo.GitHubOwner;
184  repoName = repo.GitHubRepoName;
185  repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).Select(x => new RepositorySettings
186  {
187  AccessToken = x.AccessToken,
188  ShowTestMergeCommitters = x.ShowTestMergeCommitters
189  }).FirstOrDefaultAsync(cancellationToken);
190  }
191 
192  var repoSha = repo.Head;
193  revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy).FirstOrDefaultAsync().ConfigureAwait(false);
194 
195  if (revInfo == default)
196  {
197  revInfo = new RevisionInformation
198  {
199  CommitSha = repoSha,
200  OriginCommitSha = repoSha,
201  Instance = new Models.Instance
202  {
203  Id = metadata.Id
204  }
205  };
206  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
207  databaseContext.Instances.Attach(revInfo.Instance);
208  }
209 
210  TimeSpan? averageSpan = null;
211  var previousCompileJobs = await compileJobsTask.ConfigureAwait(false);
212  if(previousCompileJobs.Count != 0)
213  {
214  var totalSpan = TimeSpan.Zero;
215  foreach (var I in previousCompileJobs)
216  totalSpan += I.StoppedAt.Value - I.StartedAt.Value;
217  averageSpan = totalSpan / previousCompileJobs.Count;
218  }
219 
220  compileJob = await DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, progressReporter, averageSpan, cancellationToken).ConfigureAwait(false);
221  }
222 
223  compileJob.Job = job;
224 
225  databaseContext.CompileJobs.Add(compileJob); //will be saved by job context
226 
227  job.PostComplete = ct => CompileJobConsumer.LoadCompileJob(compileJob, ct);
228 
229  if (repositorySettingsTask != null)
230  {
231  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
232  if (repositorySettings == default)
233  throw new JobException("Missing repository settings!");
234 
235  if (repositorySettings.AccessToken != null)
236  {
237  //potential for commenting on a test merge change
238  var outgoingCompileJob = LatestCompileJob();
239 
240  if(outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha != compileJob.RevisionInformation.CommitSha)
241  {
242  var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken);
243 
244  async Task CommentOnPR(int prNumber, string comment)
245  {
246  try
247  {
248  await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
249  }
250  catch (ApiException e)
251  {
252  logger.LogWarning("Error posting GitHub comment! Exception: {0}", e);
253  }
254  }
255 
256  var tasks = new List<Task>();
257 
258  string FormatTestMerge(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}",
259  Environment.NewLine,
260  repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty,
261  testMerge.PullRequestRevision,
262  testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty,
263  updated ? "Updated" : "Deployed",
264  metadata.Name,
266  compileJob.RevisionInformation.CommitSha
267  );
268 
269  //added prs
270  foreach (var I in compileJob
272  .ActiveTestMerges
273  .Select(x => x.TestMerge)
274  .Where(x => !outgoingCompileJob
276  .ActiveTestMerges
277  .Any(y => y.TestMerge.Number == x.Number)))
278  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, false)));
279 
280  //removed prs
281  foreach (var I in outgoingCompileJob
283  .ActiveTestMerges
284  .Select(x => x.TestMerge)
285  .Where(x => !compileJob
288  .Any(y => y.TestMerge.Number == x.Number)))
289  tasks.Add(CommentOnPR(I.Number.Value, "#### Test Merge Removed"));
290 
291  //updated prs
292  foreach(var I in compileJob
294  .ActiveTestMerges
295  .Select(x => x.TestMerge)
296  .Where(x => outgoingCompileJob
297  .RevisionInformation
298  .ActiveTestMerges
299  .Any(y => y.TestMerge.Number == x.Number)))
300  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, true)));
301 
302  if (tasks.Any())
303  await Task.WhenAll(tasks).ConfigureAwait(false);
304  }
305  }
306  }
307  }
308 
315  async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
316  {
317  while (true)
318  try
319  {
320  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
321  logger.LogDebug("Beginning auto update...");
322  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
323  try
324  {
325  Models.User user = null;
326  await databaseContextFactory.UseContext(async (db) => user = await db.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
327  var repositoryUpdateJob = new Job
328  {
329  Instance = new Models.Instance
330  {
331  Id = metadata.Id
332  },
333  Description = "Scheduled repository update",
334  CancelRightsType = RightsType.Repository,
335  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
336  StartedBy = user
337  };
338 
339  string deploySha = null;
340  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) =>
341  {
342  var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
343 
344  //assume 5 steps with synchronize
345  const int ProgressSections = 7;
346  const int ProgressStep = 100 / ProgressSections;
347 
348 
349  const int NumSteps = 3;
350  var doneSteps = 0;
351 
352  Action<int> NextProgressReporter()
353  {
354  var tmpDoneSteps = doneSteps;
355  ++doneSteps;
356  return progress => progressReporter((progress + 100 * tmpDoneSteps) / NumSteps);
357  };
358 
359  using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false))
360  {
361  if (repo == null)
362  {
363  logger.LogTrace("Aborting repo update, no repository!");
364  return;
365  }
366 
367  var startSha = repo.Head;
368  if (!repo.Tracking)
369  {
370  logger.LogTrace("Aborting repo update, not tracking origin!");
371  deploySha = startSha;
372  return;
373  }
374 
375  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
376 
377  //the main point of auto update is to pull the remote
378  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
379 
380  RevisionInformation currentRevInfo = null;
381  bool hasDbChanges = false;
382 
383  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
384  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
385  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
386  .FirstOrDefaultAsync(cancellationToken);
387 
388  async Task UpdateRevInfo(string currentHead, bool onOrigin)
389  {
390  if(currentRevInfo == null)
391  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
392 
393  if (currentRevInfo == default)
394  {
395  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
396  onOrigin = true;
397  }
398 
399  var attachedInstance = new Models.Instance
400  {
401  Id = metadata.Id
402  };
403  var oldRevInfo = currentRevInfo;
404  currentRevInfo = new RevisionInformation
405  {
406  CommitSha = currentHead,
407  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
408  Instance = attachedInstance
409  };
410  if (!onOrigin)
411  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
412 
413  databaseContext.Instances.Attach(attachedInstance);
414  databaseContext.RevisionInformations.Add(currentRevInfo);
415  hasDbChanges = true;
416  }
417 
418  //take appropriate auto update actions
419  bool shouldSyncTracked;
420  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
421  {
422  logger.LogTrace("Preserving test merges...");
423 
424  var currentRevInfoTask = LoadRevInfo();
425 
426  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
427 
428  if (!result.HasValue)
429  throw new JobException("Merge conflict while preserving test merges!");
430 
431  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
432 
433  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
434  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
435 
436  var currentHead = repo.Head;
437  if (currentHead != startSha)
438  {
439  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
440  shouldSyncTracked = stillOnOrigin;
441  }
442  else
443  shouldSyncTracked = false;
444  }
445  else
446  {
447  logger.LogTrace("Not preserving test merges...");
448  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
449 
450  var currentHead = repo.Head;
451 
452  currentRevInfo = await databaseContext.RevisionInformations
453  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
454  .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
455 
456  if (currentHead != startSha && currentRevInfo != default)
457  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
458 
459  shouldSyncTracked = true;
460  }
461 
462  //synch if necessary
463  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
464  {
465  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
466  var currentHead = repo.Head;
467  if (currentHead != currentRevInfo.CommitSha)
468  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
469  }
470 
471  if(hasDbChanges)
472  try
473  {
474  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
475  }
476  catch
477  {
478  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
479  throw;
480  }
481 
482  progressReporter(5 * ProgressStep);
483  deploySha = repo.Head;
484  }
485  }, cancellationToken).ConfigureAwait(false);
486 
487  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
488 
489  if (deploySha == null)
490  {
491  logger.LogTrace("Aborting auto update, repository error!");
492  continue;
493  }
494 
495  if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
496  {
497  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
498  continue;
499  }
500 
501  //finally set up the job
502  var compileProcessJob = new Job
503  {
504  StartedBy = user,
505  Instance = repositoryUpdateJob.Instance,
506  Description = "Scheduled code deployment",
507  CancelRightsType = RightsType.DreamMaker,
508  CancelRight = (ulong)DreamMakerRights.CancelCompile
509  };
510 
511  await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false);
512 
513  await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
514  }
515  catch (OperationCanceledException)
516  {
517  logger.LogDebug("Cancelled auto update job!");
518  throw;
519  }
520  catch (Exception e)
521  {
522  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
523  continue;
524  }
525  }
526  catch (OperationCanceledException)
527  {
528  break;
529  }
530  logger.LogTrace("Leaving auto update loop...");
531  }
532 
534  public void Rename(string newName)
535  {
536  if (String.IsNullOrWhiteSpace(newName))
537  throw new ArgumentNullException(nameof(newName));
538  metadata.Name = newName;
539  }
540 
542  public async Task StartAsync(CancellationToken cancellationToken)
543  {
544  await Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value), Configuration.StartAsync(cancellationToken), ByondManager.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), CompileJobConsumer.StartAsync(cancellationToken)).ConfigureAwait(false);
545 
546  //dependent on so many things, its just safer this way
547  await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
548 
549  CompileJob latestCompileJob = null;
550  await databaseContextFactory.UseContext(async db =>
551  {
552  latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
553  }).ConfigureAwait(false);
554  await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false);
555  }
556 
558  public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(0), Configuration.StopAsync(cancellationToken), ByondManager.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), CompileJobConsumer.StopAsync(cancellationToken));
559 
561  public async Task SetAutoUpdateInterval(uint newInterval)
562  {
563  Task toWait;
564  lock (this)
565  {
566  if (timerTask != null)
567  {
568  timerCts.Cancel();
569  toWait = timerTask;
570  }
571  else
572  toWait = Task.CompletedTask;
573  }
574  await toWait.ConfigureAwait(false);
575  if (newInterval == 0)
576  return;
577  lock (this)
578  {
579  //race condition, just quit
580  if (timerTask != null)
581  return;
582  timerCts?.Dispose();
583  timerCts = new CancellationTokenSource();
584  timerTask = TimerLoop(newInterval, timerCts.Token);
585  }
586  }
587 
590  {
591  if (!dmbFactory.DmbAvailable)
592  return null;
593  return dmbFactory.LockNextDmb(0)?.CompileJob;
594  }
595  }
596 }
+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.Threading;
9 using System.Threading.Tasks;
18 
19 namespace Tgstation.Server.Host.Components
20 {
22  sealed class Instance : IInstance
23  {
26 
28  public IByondManager ByondManager { get; }
29 
31  public IDreamMaker DreamMaker { get; }
32 
34  public IWatchdog Watchdog { get; }
35 
37  public IChat Chat { get; }
38 
40  public StaticFiles.IConfiguration Configuration { get; }
41 
43  public ICompileJobConsumer CompileJobConsumer { get; }
44 
49 
54 
59 
64 
69 
73  readonly ILogger<Instance> logger;
74 
78  readonly Api.Models.Instance metadata;
79 
83  Task timerTask;
87  CancellationTokenSource timerCts;
88 
106  public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IGitHubClientFactory gitHubClientFactory, ILogger<Instance> logger)
107  {
108  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
109  RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
110  ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
111  DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
112  Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
113  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
114  Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
115  CompileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
116  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
117  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
118  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
119  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
120  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
121  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
122  }
123 
125  public void Dispose()
126  {
127  timerCts?.Dispose();
128  CompileJobConsumer.Dispose();
129  Configuration.Dispose();
130  Chat.Dispose();
131  Watchdog.Dispose();
133  }
134 
136  public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action<int> progressReporter, CancellationToken cancellationToken)
137  {
138  //DO NOT FOLLOW THE SUGGESTION FOR A THROW EXPRESSION HERE
139  if (job == null)
140  throw new ArgumentNullException(nameof(job));
141  if (databaseContext == null)
142  throw new ArgumentNullException(nameof(databaseContext));
143  if (progressReporter == null)
144  throw new ArgumentNullException(nameof(progressReporter));
145 
146  var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings
147  {
148  StartupTimeout = x.StartupTimeout,
149  }).FirstOrDefaultAsync(cancellationToken);
150 
151  var compileJobsTask = databaseContext.CompileJobs
152  .Where(x => x.Job.Instance.Id == metadata.Id)
153  .OrderByDescending(x => x.Job.StoppedAt)
154  //TODO: Replace with this select when the issues linked in https://github.com/tgstation/tgstation-server/issues/737 are fixed
155  //.Select(x => x.Job.StoppedAt.Value - x.Job.StartedAt.Value)
156  .Select(x => new Job
157  {
158  StoppedAt = x.Job.StoppedAt,
159  StartedAt = x.Job.StartedAt
160  })
161  .Take(10)
162  .ToListAsync(cancellationToken);
163 
164  var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
165  if (dreamMakerSettings == default)
166  throw new JobException("Missing DreamMakerSettings in DB!");
167  var ddSettings = await ddSettingsTask.ConfigureAwait(false);
168  if (ddSettings == default)
169  throw new JobException("Missing DreamDaemonSettings in DB!");
170 
171  Task<RepositorySettings> repositorySettingsTask = null;
172  string repoOwner = null;
173  string repoName = null;
174  CompileJob compileJob;
175  RevisionInformation revInfo;
176  using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
177  {
178  if (repo == null)
179  throw new JobException("Missing Repository!");
180 
181  if (repo.IsGitHubRepository)
182  {
183  repoOwner = repo.GitHubOwner;
184  repoName = repo.GitHubRepoName;
185  repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).Select(x => new RepositorySettings
186  {
187  AccessToken = x.AccessToken,
188  ShowTestMergeCommitters = x.ShowTestMergeCommitters
189  }).FirstOrDefaultAsync(cancellationToken);
190  }
191 
192  var repoSha = repo.Head;
193  revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy).FirstOrDefaultAsync().ConfigureAwait(false);
194 
195  if (revInfo == default)
196  {
197  revInfo = new RevisionInformation
198  {
199  CommitSha = repoSha,
200  OriginCommitSha = repoSha,
201  Instance = new Models.Instance
202  {
203  Id = metadata.Id
204  }
205  };
206  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
207  databaseContext.Instances.Attach(revInfo.Instance);
208  }
209 
210  TimeSpan? averageSpan = null;
211  var previousCompileJobs = await compileJobsTask.ConfigureAwait(false);
212  if(previousCompileJobs.Count != 0)
213  {
214  var totalSpan = TimeSpan.Zero;
215  foreach (var I in previousCompileJobs)
216  totalSpan += I.StoppedAt.Value - I.StartedAt.Value;
217  averageSpan = totalSpan / previousCompileJobs.Count;
218  }
219 
220  compileJob = await DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, progressReporter, averageSpan, cancellationToken).ConfigureAwait(false);
221  }
222 
223  compileJob.Job = job;
224 
225  databaseContext.CompileJobs.Add(compileJob); //will be saved by job context
226 
227  job.PostComplete = ct => CompileJobConsumer.LoadCompileJob(compileJob, ct);
228 
229  if (repositorySettingsTask != null)
230  {
231  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
232  if (repositorySettings == default)
233  throw new JobException("Missing repository settings!");
234 
235  if (repositorySettings.AccessToken != null)
236  {
237  //potential for commenting on a test merge change
238  var outgoingCompileJob = LatestCompileJob();
239 
240  if(outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha != compileJob.RevisionInformation.CommitSha)
241  {
242  var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken);
243 
244  async Task CommentOnPR(int prNumber, string comment)
245  {
246  try
247  {
248  await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
249  }
250  catch (ApiException e)
251  {
252  logger.LogWarning("Error posting GitHub comment! Exception: {0}", e);
253  }
254  }
255 
256  var tasks = new List<Task>();
257 
258  string FormatTestMerge(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}",
259  Environment.NewLine,
260  repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty,
261  testMerge.PullRequestRevision,
262  testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty,
263  updated ? "Updated" : "Deployed",
264  metadata.Name,
266  compileJob.RevisionInformation.CommitSha
267  );
268 
269  //added prs
270  foreach (var I in compileJob
272  .ActiveTestMerges
273  .Select(x => x.TestMerge)
274  .Where(x => !outgoingCompileJob
276  .ActiveTestMerges
277  .Any(y => y.TestMerge.Number == x.Number)))
278  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, false)));
279 
280  //removed prs
281  foreach (var I in outgoingCompileJob
283  .ActiveTestMerges
284  .Select(x => x.TestMerge)
285  .Where(x => !compileJob
288  .Any(y => y.TestMerge.Number == x.Number)))
289  tasks.Add(CommentOnPR(I.Number.Value, "#### Test Merge Removed"));
290 
291  //updated prs
292  foreach(var I in compileJob
294  .ActiveTestMerges
295  .Select(x => x.TestMerge)
296  .Where(x => outgoingCompileJob
297  .RevisionInformation
298  .ActiveTestMerges
299  .Any(y => y.TestMerge.Number == x.Number)))
300  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, true)));
301 
302  if (tasks.Any())
303  await Task.WhenAll(tasks).ConfigureAwait(false);
304  }
305  }
306  }
307  }
308 
315  async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
316  {
317  while (true)
318  try
319  {
320  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
321  logger.LogDebug("Beginning auto update...");
322  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
323  try
324  {
325  Models.User user = null;
326  await databaseContextFactory.UseContext(async (db) => user = await db.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
327  var repositoryUpdateJob = new Job
328  {
329  Instance = new Models.Instance
330  {
331  Id = metadata.Id
332  },
333  Description = "Scheduled repository update",
334  CancelRightsType = RightsType.Repository,
335  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
336  StartedBy = user
337  };
338 
339  string deploySha = null;
340  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) =>
341  {
342  var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
343 
344  //assume 5 steps with synchronize
345  const int ProgressSections = 7;
346  const int ProgressStep = 100 / ProgressSections;
347 
348 
349  const int NumSteps = 3;
350  var doneSteps = 0;
351 
352  Action<int> NextProgressReporter()
353  {
354  var tmpDoneSteps = doneSteps;
355  ++doneSteps;
356  return progress => progressReporter((progress + 100 * tmpDoneSteps) / NumSteps);
357  };
358 
359  using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false))
360  {
361  if (repo == null)
362  {
363  logger.LogTrace("Aborting repo update, no repository!");
364  return;
365  }
366 
367  var startSha = repo.Head;
368  if (!repo.Tracking)
369  {
370  logger.LogTrace("Aborting repo update, not tracking origin!");
371  deploySha = startSha;
372  return;
373  }
374 
375  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
376 
377  //the main point of auto update is to pull the remote
378  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
379 
380  RevisionInformation currentRevInfo = null;
381  bool hasDbChanges = false;
382 
383  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
384  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
385  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
386  .FirstOrDefaultAsync(cancellationToken);
387 
388  async Task UpdateRevInfo(string currentHead, bool onOrigin)
389  {
390  if(currentRevInfo == null)
391  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
392 
393  if (currentRevInfo == default)
394  {
395  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
396  onOrigin = true;
397  }
398 
399  var attachedInstance = new Models.Instance
400  {
401  Id = metadata.Id
402  };
403  var oldRevInfo = currentRevInfo;
404  currentRevInfo = new RevisionInformation
405  {
406  CommitSha = currentHead,
407  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
408  Instance = attachedInstance
409  };
410  if (!onOrigin)
411  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
412 
413  databaseContext.Instances.Attach(attachedInstance);
414  databaseContext.RevisionInformations.Add(currentRevInfo);
415  hasDbChanges = true;
416  }
417 
418  //take appropriate auto update actions
419  bool shouldSyncTracked;
420  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
421  {
422  logger.LogTrace("Preserving test merges...");
423 
424  var currentRevInfoTask = LoadRevInfo();
425 
426  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
427 
428  if (!result.HasValue)
429  throw new JobException("Merge conflict while preserving test merges!");
430 
431  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
432 
433  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
434  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
435 
436  var currentHead = repo.Head;
437  if (currentHead != startSha)
438  {
439  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
440  shouldSyncTracked = stillOnOrigin;
441  }
442  else
443  shouldSyncTracked = false;
444  }
445  else
446  {
447  logger.LogTrace("Not preserving test merges...");
448  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
449 
450  var currentHead = repo.Head;
451 
452  currentRevInfo = await databaseContext.RevisionInformations
453  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
454  .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
455 
456  if (currentHead != startSha && currentRevInfo != default)
457  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
458 
459  shouldSyncTracked = true;
460  }
461 
462  //synch if necessary
463  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
464  {
465  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
466  var currentHead = repo.Head;
467  if (currentHead != currentRevInfo.CommitSha)
468  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
469  }
470 
471  if(hasDbChanges)
472  try
473  {
474  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
475  }
476  catch
477  {
478  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
479  throw;
480  }
481 
482  progressReporter(5 * ProgressStep);
483  deploySha = repo.Head;
484  }
485  }, cancellationToken).ConfigureAwait(false);
486 
487  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
488 
489  if (deploySha == null)
490  {
491  logger.LogTrace("Aborting auto update, repository error!");
492  continue;
493  }
494 
495  if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
496  {
497  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
498  continue;
499  }
500 
501  //finally set up the job
502  var compileProcessJob = new Job
503  {
504  StartedBy = user,
505  Instance = repositoryUpdateJob.Instance,
506  Description = "Scheduled code deployment",
507  CancelRightsType = RightsType.DreamMaker,
508  CancelRight = (ulong)DreamMakerRights.CancelCompile
509  };
510 
511  await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false);
512 
513  await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
514  }
515  catch (OperationCanceledException)
516  {
517  logger.LogDebug("Cancelled auto update job!");
518  throw;
519  }
520  catch (Exception e)
521  {
522  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
523  continue;
524  }
525  }
526  catch (OperationCanceledException)
527  {
528  break;
529  }
530  logger.LogTrace("Leaving auto update loop...");
531  }
532 
534  public void Rename(string newName)
535  {
536  if (String.IsNullOrWhiteSpace(newName))
537  throw new ArgumentNullException(nameof(newName));
538  metadata.Name = newName;
539  }
540 
542  public async Task StartAsync(CancellationToken cancellationToken)
543  {
544  await Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value), Configuration.StartAsync(cancellationToken), ByondManager.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), CompileJobConsumer.StartAsync(cancellationToken)).ConfigureAwait(false);
545 
546  //dependent on so many things, its just safer this way
547  await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
548 
549  CompileJob latestCompileJob = null;
550  await databaseContextFactory.UseContext(async db =>
551  {
552  latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
553  }).ConfigureAwait(false);
554  await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false);
555  }
556 
558  public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(0), Configuration.StopAsync(cancellationToken), ByondManager.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), CompileJobConsumer.StopAsync(cancellationToken));
559 
561  public async Task SetAutoUpdateInterval(uint newInterval)
562  {
563  Task toWait;
564  lock (this)
565  {
566  if (timerTask != null)
567  {
568  timerCts.Cancel();
569  toWait = timerTask;
570  }
571  else
572  toWait = Task.CompletedTask;
573  }
574  await toWait.ConfigureAwait(false);
575  if (newInterval == 0)
576  return;
577  lock (this)
578  {
579  //race condition, just quit
580  if (timerTask != null)
581  return;
582  timerCts?.Dispose();
583  timerCts = new CancellationTokenSource();
584  timerTask = TimerLoop(newInterval, timerCts.Token);
585  }
586  }
587 
590  {
591  if (!dmbFactory.DmbAvailable)
592  return null;
593  return dmbFactory.LockNextDmb(0)?.CompileJob;
594  }
595  }
596 }
string Name
The name of the User
Definition: User.cs:38
-
Manages the runtime of Jobs
Definition: IJobManager.cs:12
DbSet< Instance > Instances
The Instances in the IDatabaseContext
async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action< int > progressReporter, CancellationToken cancellationToken)
Run the compile job and insert it into the database. Meant to be called by a Core.IJobManager
Definition: Instance.cs:136
diff --git a/_tgstation_8_server_8_host_8_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_assembly_info_8cs.html index df4548a1f4..43c0f7534c 100644 --- a/_tgstation_8_server_8_host_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_assembly_info_8cs_source.html index c45aa1af40..a3aa5b9790 100644 --- a/_tgstation_8_server_8_host_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_console_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_console_8_assembly_info_8cs_source.html index 52a84c0f9a..ac265aa2be 100644 --- a/_tgstation_8_server_8_host_8_console_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_console_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Console/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Console.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Host.Console/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Console.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Console.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html index 76fc39106d..5d18b1948e 100644 --- a/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html index 37466fca50..53fc29de6a 100644 --- a/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html index 3203ae4cf7..22f2c188b8 100644 --- a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html index c53270b03d..38a995518c 100644 --- a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html index b50f09579f..13e5231737 100644 --- a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs File Reference +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html index 4e22852711..76db250606 100644 --- a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html index bd9abbdcce..6f4b8b8668 100644 --- a/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html index 33f02c41de..1966731549 100644 --- a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Release/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Debug/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html index 11fec32519..96cc6a8bf5 100644 --- a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Release/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Debug/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Watchdog.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Watchdog")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Watchdog")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Watchdog")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Watchdog")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Watchdog")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Watchdog")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_watchdog_8_tests_8_assembly_info_8cs_source.html index 6b02753515..1bc613db7b 100644 --- a/_tgstation_8_server_8_host_8_watchdog_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_watchdog_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Watchdog.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Watchdog.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Watchdog.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Watchdog.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Watchdog.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Watchdog.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Watchdog.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Watchdog.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Watchdog.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_host_8_watchdog_8_tests_8_program_8cs_source.html index 05521465ef..30b70e2f1d 100644 --- a/_tgstation_8_server_8_host_8_watchdog_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_host_8_watchdog_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Watchdog.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Watchdog.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_tests_8_assembly_info_8cs.html b/_tgstation_8_server_8_tests_8_assembly_info_8cs.html index 6ffe9f992d..26e4c9ba82 100644 --- a/_tgstation_8_server_8_tests_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_tests_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Tests.AssemblyInfo.cs File Reference +tgstation-server: tests/Tgstation.Server.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Tests.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_tests_8_assembly_info_8cs_source.html index 4ac2885489..ce315e8a0d 100644 --- a/_tgstation_8_server_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_tests_8_program_8cs_source.html index ba2161b75f..fa15cfe690 100644 --- a/_tgstation_8_server_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_unauthorized_exception_8cs_source.html b/_unauthorized_exception_8cs_source.html index c5a3883af0..7cb97d19d3 100644 --- a/_unauthorized_exception_8cs_source.html +++ b/_unauthorized_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
UnauthorizedException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class UnauthorizedException : ClientException
11  {
15  public UnauthorizedException() : base(new ErrorMessage
16  {
17  Message = "Invalid credentials!",
18  SeverApiVersion = null
19  }, HttpStatusCode.Unauthorized)
20  { }
21 
26  public UnauthorizedException(string message) : base(message) { }
27 
33  public UnauthorizedException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
- +Go to the documentation of this file.
1 using System;
2 using System.Net;
4 
5 namespace Tgstation.Server.Client
6 {
10  public sealed class UnauthorizedException : ClientException
11  {
15  public UnauthorizedException() : base(new ErrorMessage
16  {
17  Message = "Invalid credentials!",
18  SeverApiVersion = null
19  }, HttpStatusCode.Unauthorized)
20  { }
21 
26  public UnauthorizedException(string message) : base(message) { }
27 
33  public UnauthorizedException(string message, Exception innerException) : base(message, innerException) { }
34  }
35 }
UnauthorizedException(string message)
Construct an UnauthorizedException with a message
diff --git a/_unrecognized_response_exception_8cs_source.html b/_unrecognized_response_exception_8cs_source.html index 38362a183b..f6a6bab804 100644 --- a/_unrecognized_response_exception_8cs_source.html +++ b/_unrecognized_response_exception_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
UnrecognizedResponseException.cs
-Go to the documentation of this file.
1 using System;
2 using System.Globalization;
3 using System.Net;
5 
6 namespace Tgstation.Server.Client
7 {
12  {
18  public UnrecognizedResponseException(string data, HttpStatusCode statusCode) : base(new ErrorMessage
19  {
20  Message = String.Format(CultureInfo.InvariantCulture, "Unrecognized response body: {0}", data),
21  SeverApiVersion = null
22  }, statusCode)
23  { }
24 
29 
34  public UnrecognizedResponseException(string message) : base(message) { }
35 
41  public UnrecognizedResponseException(string message, Exception innerException) : base(message, innerException) { }
42  }
43 }
-
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 System;
2 using System.Globalization;
3 using System.Net;
5 
6 namespace Tgstation.Server.Client
7 {
12  {
18  public UnrecognizedResponseException(string data, HttpStatusCode statusCode) : base(new ErrorMessage
19  {
20  Message = String.Format(CultureInfo.InvariantCulture, "Unrecognized response body: {0}", data),
21  SeverApiVersion = null
22  }, statusCode)
23  { }
24 
29 
34  public UnrecognizedResponseException(string message) : base(message) { }
35 
41  public UnrecognizedResponseException(string message, Exception innerException) : base(message, innerException) { }
42  }
43 }
Occurs when a response is received that did not deserialize to one of the expected Api...
diff --git a/_user_controller_8cs_source.html b/_user_controller_8cs_source.html index 01dda92f66..d6e9de464d 100644 --- a/_user_controller_8cs_source.html +++ b/_user_controller_8cs_source.html @@ -118,7 +118,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the UserController
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
InstanceManagerRights
Rights for managing Models.Instances
- +
override async Task< IActionResult > Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
diff --git a/_watchdog_2_watchdog_8cs_source.html b/_watchdog_2_watchdog_8cs_source.html index d09726a134..52d47a2719 100644 --- a/_watchdog_2_watchdog_8cs_source.html +++ b/_watchdog_2_watchdog_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Watchdog.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.IO;
7 using System.Linq;
8 using System.Reflection;
9 using System.Runtime.InteropServices;
10 using System.Threading;
11 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Watchdog
14 {
16  sealed class Watchdog : IWatchdog
17  {
18 
22  readonly ILogger<Watchdog> logger;
23 
28  public Watchdog(ILogger<Watchdog> logger)
29  {
30  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
31  }
32 
34  public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
35  {
36  logger.LogInformation("Host watchdog starting...");
37  logger.LogDebug("PID: {0}", Process.GetCurrentProcess().Id);
38  string updateDirectory = null;
39  try
40  {
41  var enviromentPath = Environment.GetEnvironmentVariable("PATH");
42  var paths = enviromentPath.Split(';');
43  var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
44 
45  var exeName = "dotnet";
46  IEnumerable<string> enumerator;
47  if (isWindows)
48  {
49  exeName += ".exe";
50  enumerator = paths;
51  }
52  else
53  enumerator = paths.Select(x => x.Split(':')).SelectMany(x => x);
54 
55  enumerator = enumerator.Select(x => Path.Combine(x, exeName));
56 
57  var dotnetPath = enumerator
58  .Where(x =>
59  {
60  logger.LogTrace("Checking for dotnet at {0}", x);
61  return File.Exists(x);
62  })
63  .FirstOrDefault();
64 
65  if (dotnetPath == default)
66  {
67  logger.LogCritical("Unable to locate dotnet executable in PATH! Please ensure the .NET Core runtime is installed and is in your PATH!");
68  return;
69  }
70  logger.LogInformation("Detected dotnet executable at {0}", dotnetPath);
71 
72  var rootLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
73 
74  var assemblyStoragePath = Path.Combine(rootLocation, "lib"); //always always next to watchdog
75 #if DEBUG
76  Directory.CreateDirectory(assemblyStoragePath);
77 #endif
78  var defaultAssemblyPath = Path.GetFullPath(Path.Combine(assemblyStoragePath, "Default"));
79 #if DEBUG
80  //just copy the shit where it belongs
81  Directory.Delete(assemblyStoragePath, true);
82  Directory.CreateDirectory(defaultAssemblyPath);
83 
84  var sourcePath = "../../../Tgstation.Server.Host/bin/Debug/netcoreapp2.1";
85  foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
86  Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath));
87 
88  foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
89  File.Copy(newPath, newPath.Replace(sourcePath, defaultAssemblyPath), true);
90 
91  const string AppSettingsJson = "appsettings.json";
92  var rootJson = Path.Combine(rootLocation, AppSettingsJson);
93  File.Delete(rootJson);
94  File.Move(Path.Combine(defaultAssemblyPath, AppSettingsJson), rootJson);
95 #endif
96 
97  var assemblyName = String.Join(".", nameof(Tgstation), nameof(Server), nameof(Host), "dll");
98  var assemblyPath = Path.Combine(defaultAssemblyPath, assemblyName);
99 
100  if (assemblyPath.Contains("\""))
101  {
102  logger.LogCritical("Running from paths with \"'s in the name is not supported!");
103  return;
104  }
105 
106  if (!File.Exists(assemblyPath))
107  {
108  logger.LogCritical("Unable to locate host assembly!");
109  return;
110  }
111 
112  while (!cancellationToken.IsCancellationRequested)
113  using (logger.BeginScope("Host invocation"))
114  {
115  updateDirectory = Path.GetFullPath(Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString()));
116  logger.LogInformation("Update path set to {0}", updateDirectory);
117  using (var process = new Process())
118  {
119  process.StartInfo.FileName = dotnetPath;
120  process.StartInfo.WorkingDirectory = rootLocation; //for appsettings
121 
122  var arguments = new List<string>
123  {
124  '"' + assemblyPath + '"',
125  '"' + updateDirectory + '"'
126  };
127 
128  if (Environment.GetCommandLineArgs().Any(x => x == "--attach-host-debugger"))
129  arguments.Add("--attach-debugger");
130 
131  if (runConfigure)
132  {
133  logger.LogInformation("Running configuration check and wizard if necessary...");
134  arguments.Add("General:SetupWizardMode=Only");
135  }
136 
137  arguments.AddRange(args);
138 
139  process.StartInfo.Arguments = String.Join(" ", arguments);
140 
141  process.StartInfo.UseShellExecute = false; //runs in the same console
142 
143  var tcs = new TaskCompletionSource<object>();
144  process.Exited += (a, b) =>
145  {
146  tcs.TrySetResult(null);
147  };
148  process.EnableRaisingEvents = true;
149 
150  logger.LogInformation("Launching host...");
151 
152  var killedHostProcess = false;
153  try
154  {
155  process.Start();
156 
157  using (var processCts = new CancellationTokenSource())
158  using (processCts.Token.Register(() => tcs.TrySetResult(null)))
159  using (cancellationToken.Register(() =>
160  {
161  if (!Directory.Exists(updateDirectory))
162  {
163  logger.LogInformation("Cancellation requested! Writing shutdown lock file...");
164  File.WriteAllBytes(updateDirectory, Array.Empty<byte>());
165  }
166  else
167  logger.LogWarning("Cancellation requested while update directory exists!");
168 
169  logger.LogInformation("Will force close host process if it doesn't exit in 10 seconds...");
170 
171  try
172  {
173  processCts.CancelAfter(TimeSpan.FromSeconds(10));
174  }
175  catch (ObjectDisposedException) { } //race conditions
176  }))
177  await tcs.Task.ConfigureAwait(false);
178  }
179  catch (InvalidOperationException) { }
180  finally
181  {
182  try
183  {
184  if (!process.HasExited)
185  {
186  killedHostProcess = true;
187  process.Kill();
188  process.WaitForExit();
189  }
190  }
191  catch (InvalidOperationException) { }
192  logger.LogInformation("Host exited!");
193  }
194 
195  if (runConfigure)
196  {
197  logger.LogInformation("Exiting due to configuration check...");
198  return;
199  }
200 
201  switch (process.ExitCode)
202  {
203  case 0:
204  return;
205  case 1:
206  if (!cancellationToken.IsCancellationRequested)
207  //just a restart
208  logger.LogInformation("Watchdog will restart host...");
209  else
210  logger.LogWarning("Host requested restart but watchdog shutdown is in progress!");
211  break;
212  case 2:
213  //update path is now an exception document
214  logger.LogCritical("Host crashed, propagating exception dump...");
215  var data = File.ReadAllText(updateDirectory);
216  try
217  {
218  File.Delete(updateDirectory);
219  }
220  catch (Exception e)
221  {
222  logger.LogWarning("Unable to delete exception dump file at {0}! Exception: {1}", updateDirectory, e);
223  }
224  throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host propagated exception: {0}", data));
225  default:
226  if (killedHostProcess)
227  {
228  logger.LogWarning("Watchdog forced to kill host process!");
229  cancellationToken.ThrowIfCancellationRequested();
230  }
231  throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host crashed with exit code {0}!", process.ExitCode));
232  }
233  }
234 
235  //HEY YOU
236  //BE WARNED THAT IF YOU DEBUGGED THE HOST PROCESS THAT JUST LAUNCHED THE DEBUGGER WILL HOLD A LOCK ON THE DIRECTORY
237  //THIS MEANS THE FIRST DIRECTORY.MOVE WILL THROW
238  if (Directory.Exists(updateDirectory))
239  {
240  logger.LogInformation("Applying server update...");
241  if (isWindows)
242  {
243  //windows dick sucking resource unlocking
244  GC.Collect(Int32.MaxValue, GCCollectionMode.Default, true);
245  await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
246  }
247  var tempPath = Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString());
248  try
249  {
250  Directory.Move(defaultAssemblyPath, tempPath);
251  try
252  {
253  Directory.Move(updateDirectory, defaultAssemblyPath);
254  logger.LogInformation("Server update complete, deleting old server...");
255  try
256  {
257  Directory.Delete(tempPath, true);
258  }
259  catch (Exception e)
260  {
261  logger.LogWarning("Error deleting old server at {0}! Exception: {1}", tempPath, e);
262  }
263  }
264  catch (Exception e)
265  {
266  logger.LogError("Error moving updated server directory, attempting revert! Exception: {0}", e);
267  Directory.Delete(defaultAssemblyPath, true);
268  Directory.Move(tempPath, defaultAssemblyPath);
269  logger.LogInformation("Revert successful!");
270  }
271  }
272  catch (Exception e)
273  {
274  logger.LogWarning("Failed to move out active host assembly! Exception: {0}", e);
275  }
276  }
277  }
278  }
279  catch (OperationCanceledException)
280  {
281  logger.LogDebug("Exiting due to cancellation...");
282  if (!Directory.Exists(updateDirectory))
283  File.Delete(updateDirectory);
284  else
285  Directory.Delete(updateDirectory, true);
286  }
287  catch (Exception e)
288  {
289  logger.LogCritical("Watchdog error! Exception: {0}", e);
290  }
291  finally
292  {
293  logger.LogInformation("Host watchdog exiting...");
294  }
295  }
296  }
297 }
- +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.IO;
7 using System.Linq;
8 using System.Reflection;
9 using System.Runtime.InteropServices;
10 using System.Threading;
11 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Watchdog
14 {
16  sealed class Watchdog : IWatchdog
17  {
18 
22  readonly ILogger<Watchdog> logger;
23 
28  public Watchdog(ILogger<Watchdog> logger)
29  {
30  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
31  }
32 
34  public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
35  {
36  logger.LogInformation("Host watchdog starting...");
37  logger.LogDebug("PID: {0}", Process.GetCurrentProcess().Id);
38  string updateDirectory = null;
39  try
40  {
41  var enviromentPath = Environment.GetEnvironmentVariable("PATH");
42  var paths = enviromentPath.Split(';');
43  var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
44 
45  var exeName = "dotnet";
46  IEnumerable<string> enumerator;
47  if (isWindows)
48  {
49  exeName += ".exe";
50  enumerator = paths;
51  }
52  else
53  enumerator = paths.Select(x => x.Split(':')).SelectMany(x => x);
54 
55  enumerator = enumerator.Select(x => Path.Combine(x, exeName));
56 
57  var dotnetPath = enumerator
58  .Where(x =>
59  {
60  logger.LogTrace("Checking for dotnet at {0}", x);
61  return File.Exists(x);
62  })
63  .FirstOrDefault();
64 
65  if (dotnetPath == default)
66  {
67  logger.LogCritical("Unable to locate dotnet executable in PATH! Please ensure the .NET Core runtime is installed and is in your PATH!");
68  return;
69  }
70  logger.LogInformation("Detected dotnet executable at {0}", dotnetPath);
71 
72  var rootLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
73 
74  var assemblyStoragePath = Path.Combine(rootLocation, "lib"); //always always next to watchdog
75 #if DEBUG
76  Directory.CreateDirectory(assemblyStoragePath);
77 #endif
78  var defaultAssemblyPath = Path.GetFullPath(Path.Combine(assemblyStoragePath, "Default"));
79 #if DEBUG
80  //just copy the shit where it belongs
81  Directory.Delete(assemblyStoragePath, true);
82  Directory.CreateDirectory(defaultAssemblyPath);
83 
84  var sourcePath = "../../../Tgstation.Server.Host/bin/Debug/netcoreapp2.1";
85  foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
86  Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath));
87 
88  foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
89  File.Copy(newPath, newPath.Replace(sourcePath, defaultAssemblyPath), true);
90 
91  const string AppSettingsJson = "appsettings.json";
92  var rootJson = Path.Combine(rootLocation, AppSettingsJson);
93  File.Delete(rootJson);
94  File.Move(Path.Combine(defaultAssemblyPath, AppSettingsJson), rootJson);
95 #endif
96 
97  var assemblyName = String.Join(".", nameof(Tgstation), nameof(Server), nameof(Host), "dll");
98  var assemblyPath = Path.Combine(defaultAssemblyPath, assemblyName);
99 
100  if (assemblyPath.Contains("\""))
101  {
102  logger.LogCritical("Running from paths with \"'s in the name is not supported!");
103  return;
104  }
105 
106  if (!File.Exists(assemblyPath))
107  {
108  logger.LogCritical("Unable to locate host assembly!");
109  return;
110  }
111 
112  while (!cancellationToken.IsCancellationRequested)
113  using (logger.BeginScope("Host invocation"))
114  {
115  updateDirectory = Path.GetFullPath(Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString()));
116  logger.LogInformation("Update path set to {0}", updateDirectory);
117  using (var process = new Process())
118  {
119  process.StartInfo.FileName = dotnetPath;
120  process.StartInfo.WorkingDirectory = rootLocation; //for appsettings
121 
122  var arguments = new List<string>
123  {
124  '"' + assemblyPath + '"',
125  '"' + updateDirectory + '"'
126  };
127 
128  if (Environment.GetCommandLineArgs().Any(x => x == "--attach-host-debugger"))
129  arguments.Add("--attach-debugger");
130 
131  if (runConfigure)
132  {
133  logger.LogInformation("Running configuration check and wizard if necessary...");
134  arguments.Add("General:SetupWizardMode=Only");
135  }
136 
137  arguments.AddRange(args);
138 
139  process.StartInfo.Arguments = String.Join(" ", arguments);
140 
141  process.StartInfo.UseShellExecute = false; //runs in the same console
142 
143  var tcs = new TaskCompletionSource<object>();
144  process.Exited += (a, b) =>
145  {
146  tcs.TrySetResult(null);
147  };
148  process.EnableRaisingEvents = true;
149 
150  logger.LogInformation("Launching host...");
151 
152  var killedHostProcess = false;
153  try
154  {
155  process.Start();
156 
157  using (var processCts = new CancellationTokenSource())
158  using (processCts.Token.Register(() => tcs.TrySetResult(null)))
159  using (cancellationToken.Register(() =>
160  {
161  if (!Directory.Exists(updateDirectory))
162  {
163  logger.LogInformation("Cancellation requested! Writing shutdown lock file...");
164  File.WriteAllBytes(updateDirectory, Array.Empty<byte>());
165  }
166  else
167  logger.LogWarning("Cancellation requested while update directory exists!");
168 
169  logger.LogInformation("Will force close host process if it doesn't exit in 10 seconds...");
170 
171  try
172  {
173  processCts.CancelAfter(TimeSpan.FromSeconds(10));
174  }
175  catch (ObjectDisposedException) { } //race conditions
176  }))
177  await tcs.Task.ConfigureAwait(false);
178  }
179  catch (InvalidOperationException) { }
180  finally
181  {
182  try
183  {
184  if (!process.HasExited)
185  {
186  killedHostProcess = true;
187  process.Kill();
188  process.WaitForExit();
189  }
190  }
191  catch (InvalidOperationException) { }
192  logger.LogInformation("Host exited!");
193  }
194 
195  if (runConfigure)
196  {
197  logger.LogInformation("Exiting due to configuration check...");
198  return;
199  }
200 
201  switch (process.ExitCode)
202  {
203  case 0:
204  return;
205  case 1:
206  if (!cancellationToken.IsCancellationRequested)
207  //just a restart
208  logger.LogInformation("Watchdog will restart host...");
209  else
210  logger.LogWarning("Host requested restart but watchdog shutdown is in progress!");
211  break;
212  case 2:
213  //update path is now an exception document
214  logger.LogCritical("Host crashed, propagating exception dump...");
215  var data = File.ReadAllText(updateDirectory);
216  try
217  {
218  File.Delete(updateDirectory);
219  }
220  catch (Exception e)
221  {
222  logger.LogWarning("Unable to delete exception dump file at {0}! Exception: {1}", updateDirectory, e);
223  }
224  throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host propagated exception: {0}", data));
225  default:
226  if (killedHostProcess)
227  {
228  logger.LogWarning("Watchdog forced to kill host process!");
229  cancellationToken.ThrowIfCancellationRequested();
230  }
231  throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host crashed with exit code {0}!", process.ExitCode));
232  }
233  }
234 
235  //HEY YOU
236  //BE WARNED THAT IF YOU DEBUGGED THE HOST PROCESS THAT JUST LAUNCHED THE DEBUGGER WILL HOLD A LOCK ON THE DIRECTORY
237  //THIS MEANS THE FIRST DIRECTORY.MOVE WILL THROW
238  if (Directory.Exists(updateDirectory))
239  {
240  logger.LogInformation("Applying server update...");
241  if (isWindows)
242  {
243  //windows dick sucking resource unlocking
244  GC.Collect(Int32.MaxValue, GCCollectionMode.Default, true);
245  await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
246  }
247  var tempPath = Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString());
248  try
249  {
250  Directory.Move(defaultAssemblyPath, tempPath);
251  try
252  {
253  Directory.Move(updateDirectory, defaultAssemblyPath);
254  logger.LogInformation("Server update complete, deleting old server...");
255  try
256  {
257  Directory.Delete(tempPath, true);
258  }
259  catch (Exception e)
260  {
261  logger.LogWarning("Error deleting old server at {0}! Exception: {1}", tempPath, e);
262  }
263  }
264  catch (Exception e)
265  {
266  logger.LogError("Error moving updated server directory, attempting revert! Exception: {0}", e);
267  Directory.Delete(defaultAssemblyPath, true);
268  Directory.Move(tempPath, defaultAssemblyPath);
269  logger.LogInformation("Revert successful!");
270  }
271  }
272  catch (Exception e)
273  {
274  logger.LogWarning("Failed to move out active host assembly! Exception: {0}", e);
275  }
276  }
277  }
278  }
279  catch (OperationCanceledException)
280  {
281  logger.LogDebug("Exiting due to cancellation...");
282  if (!Directory.Exists(updateDirectory))
283  File.Delete(updateDirectory);
284  else
285  Directory.Delete(updateDirectory, true);
286  }
287  catch (Exception e)
288  {
289  logger.LogCritical("Watchdog error! Exception: {0}", e);
290  }
291  finally
292  {
293  logger.LogInformation("Host watchdog exiting...");
294  }
295  }
296  }
297 }
async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
Run the IWatchdog
Definition: Watchdog.cs:34
diff --git a/_windows_byond_installer_8cs_source.html b/_windows_byond_installer_8cs_source.html index 4e1e9c6fab..59f49e13dd 100644 --- a/_windows_byond_installer_8cs_source.html +++ b/_windows_byond_installer_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WindowsByondInstaller.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
4 using System.Text;
5 using System.Threading;
6 using System.Threading.Tasks;
8 using Tgstation.Server.Host.IO;
9 
10 namespace Tgstation.Server.Host.Components.Byond
11 {
16  {
20  const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
24  const string ByondConfigDir = "byond/cfg";
28  const string ByondDDConfig = "daemon.txt";
32  const string ByondNoPromptTrustedMode = "trusted-check 0";
36  const string ByondDXDir = "byond/directx";
37 
39  public string DreamDaemonName => "dreamdaemon.exe";
40 
42  public string DreamMakerName => "dm.exe";
43 
48 
53 
57  readonly ILogger<WindowsByondInstaller> logger;
58 
62  readonly SemaphoreSlim semaphore;
63 
68 
75  public WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger<WindowsByondInstaller> logger)
76  {
77  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
78  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
79  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
80 
81  semaphore = new SemaphoreSlim(1);
82  installedDirectX = false;
83  }
84 
86  public void Dispose() => semaphore.Dispose();
87 
89  public async Task CleanCache(CancellationToken cancellationToken)
90  {
91  try
92  {
93  await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
94  }
95  catch(OperationCanceledException)
96  {
97  throw;
98  }
99  catch (Exception e)
100  {
101  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
102  }
103  }
104 
106  public Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
107  {
108  var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor);
109 
110  return ioManager.DownloadFile(new Uri(url), cancellationToken);
111  }
112 
114  public async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
115  {
116  async Task SetNoPromptTrusted()
117  {
118  var configPath = ioManager.ConcatPath(path, ByondConfigDir);
119  await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
120  await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
121  };
122 
123  var setNoPromptTrustedModeTask = SetNoPromptTrusted();
124 
125  //after this version lummox made DD depend of directx lol
126  //but then he became amazing and not only fixed it but also gave us 30s compiles \[T]/
127  if (version.Major == 512 && version.Minor >= 1427 && version.Minor < 1452 && !installedDirectX)
128  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
129  //check again because race conditions
130  if (!installedDirectX)
131  {
132  //always install it, it's pretty fast and will do better redundancy checking than us
133  var rbdx = ioManager.ConcatPath(path, ByondDXDir);
134  //noShellExecute because we aren't doing runas shennanigans
135  IProcess directXInstaller;
136  try
137  {
138  directXInstaller = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
139  }
140  catch (Exception e)
141  {
142  throw new JobException("Unable to start DirectX installer process! Is the server running with admin privileges?", e);
143  }
144  using (directXInstaller)
145  {
146  int exitCode;
147  using (cancellationToken.Register(() => directXInstaller.Terminate()))
148  exitCode = await directXInstaller.Lifetime.ConfigureAwait(false);
149  cancellationToken.ThrowIfCancellationRequested();
150 
151  if (exitCode != 0)
152  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Failed to install included DirectX! Exit code: {0}", exitCode));
153  installedDirectX = true;
154  }
155  }
156 
157  await setNoPromptTrustedModeTask.ConfigureAwait(false);
158  }
159  }
160 }
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the WindowsByondInstaller
- +Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
4 using System.Text;
5 using System.Threading;
6 using System.Threading.Tasks;
8 using Tgstation.Server.Host.IO;
9 
10 namespace Tgstation.Server.Host.Components.Byond
11 {
16  {
20  const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
24  const string ByondConfigDir = "byond/cfg";
28  const string ByondDDConfig = "daemon.txt";
32  const string ByondNoPromptTrustedMode = "trusted-check 0";
36  const string ByondDXDir = "byond/directx";
37 
39  public string DreamDaemonName => "dreamdaemon.exe";
40 
42  public string DreamMakerName => "dm.exe";
43 
48 
53 
57  readonly ILogger<WindowsByondInstaller> logger;
58 
62  readonly SemaphoreSlim semaphore;
63 
68 
75  public WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger<WindowsByondInstaller> logger)
76  {
77  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
78  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
79  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
80 
81  semaphore = new SemaphoreSlim(1);
82  installedDirectX = false;
83  }
84 
86  public void Dispose() => semaphore.Dispose();
87 
89  public async Task CleanCache(CancellationToken cancellationToken)
90  {
91  try
92  {
93  await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
94  }
95  catch(OperationCanceledException)
96  {
97  throw;
98  }
99  catch (Exception e)
100  {
101  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
102  }
103  }
104 
106  public Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
107  {
108  var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor);
109 
110  return ioManager.DownloadFile(new Uri(url), cancellationToken);
111  }
112 
114  public async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
115  {
116  async Task SetNoPromptTrusted()
117  {
118  var configPath = ioManager.ConcatPath(path, ByondConfigDir);
119  await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
120  await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
121  };
122 
123  var setNoPromptTrustedModeTask = SetNoPromptTrusted();
124 
125  //after this version lummox made DD depend of directx lol
126  //but then he became amazing and not only fixed it but also gave us 30s compiles \[T]/
127  if (version.Major == 512 && version.Minor >= 1427 && version.Minor < 1452 && !installedDirectX)
128  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
129  //check again because race conditions
130  if (!installedDirectX)
131  {
132  //always install it, it's pretty fast and will do better redundancy checking than us
133  var rbdx = ioManager.ConcatPath(path, ByondDXDir);
134  //noShellExecute because we aren't doing runas shennanigans
135  IProcess directXInstaller;
136  try
137  {
138  directXInstaller = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
139  }
140  catch (Exception e)
141  {
142  throw new JobException("Unable to start DirectX installer process! Is the server running with admin privileges?", e);
143  }
144  using (directXInstaller)
145  {
146  int exitCode;
147  using (cancellationToken.Register(() => directXInstaller.Terminate()))
148  exitCode = await directXInstaller.Lifetime.ConfigureAwait(false);
149  cancellationToken.ThrowIfCancellationRequested();
150 
151  if (exitCode != 0)
152  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Failed to install included DirectX! Exit code: {0}", exitCode));
153  installedDirectX = true;
154  }
155  }
156 
157  await setNoPromptTrustedModeTask.ConfigureAwait(false);
158  }
159  }
160 }
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the WindowsByondInstaller
Task< byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
Download a given BYOND version
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
readonly IProcessExecutor processExecutor
The IProcessExecutor for the WindowsByondInstaller
diff --git a/_windows_system_identity_factory_8cs_source.html b/_windows_system_identity_factory_8cs_source.html index d8e498e22f..d2029a23a8 100644 --- a/_windows_system_identity_factory_8cs_source.html +++ b/_windows_system_identity_factory_8cs_source.html @@ -92,8 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WindowsSystemIdentityFactory.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.Win32.SafeHandles;
3 using System;
4 using System.DirectoryServices.AccountManagement;
5 using System.Security.Principal;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Security
11 {
16  {
20  readonly ILogger<WindowsSystemIdentityFactory> logger;
21 
28  static void GetUserAndDomainName(string input, out string username, out string domainName)
29  {
30  var splits = input.Split('\\');
31  username = splits.Length > 1 ? splits[1] : splits[0];
32  domainName = splits.Length > 1 ? splits[0] : null;
33  }
34 
39  public WindowsSystemIdentityFactory(ILogger<WindowsSystemIdentityFactory> logger)
40  {
41  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
42  }
43 
45  public Task<ISystemIdentity> CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
46  {
47  if (user == null)
48  throw new ArgumentNullException(nameof(user));
49 
50  if (user.SystemIdentifier == null)
51  throw new InvalidOperationException("User's SystemIdentifier must not be null!");
52 
53  PrincipalContext pc = null;
54  UserPrincipal principal = null;
55 
56  bool TryGetPrincipalFromContextType(ContextType contextType)
57  {
58  try
59  {
60  pc = new PrincipalContext(contextType);
61  cancellationToken.ThrowIfCancellationRequested();
62  principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
63  }
64  catch (OperationCanceledException)
65  {
66  throw;
67  }
68  catch (Exception e)
69  {
70  logger.LogWarning("Error loading user for context type {0}! Exception: {1}", contextType, e);
71  }
72  finally
73  {
74  if (principal == null)
75  {
76  pc?.Dispose();
77  cancellationToken.ThrowIfCancellationRequested();
78  }
79  }
80  return principal != null;
81  };
82 
83  if (!TryGetPrincipalFromContextType(ContextType.Machine) && !TryGetPrincipalFromContextType(ContextType.Domain))
84  return null;
85  return (ISystemIdentity)new WindowsSystemIdentity(principal);
86  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
87 
89  public Task<ISystemIdentity> CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
90  {
91  if (username == null)
92  throw new ArgumentNullException(nameof(username));
93  if (password == null)
94  throw new ArgumentNullException(nameof(password));
95 
96  var originalUsername = username;
97  GetUserAndDomainName(originalUsername, out username, out var domainName);
98 
99  var res = NativeMethods.LogonUser(username, domainName, password, 3 /*LOGON32_LOGON_NETWORK*/, 0 /*LOGON32_PROVIDER_DEFAULT*/, out var token);
100  if (!res)
101  {
102  logger.LogTrace("Failed to log in username {0}!", originalUsername);
103  return null;
104  }
105 
106  logger.LogTrace("Successfully logged in username {0}!", originalUsername);
107 
108  using (var handle = new SafeAccessTokenHandle(token)) //checked internally, windows identity always duplicates the handle when constructed with a userToken
109  return (ISystemIdentity)new WindowsSystemIdentity(new WindowsIdentity(handle.DangerousGetHandle())); //https://github.com/dotnet/corefx/blob/6ed61acebe3214fcf79b4274f2bb9b55c0604a4d/src/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L271
110  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
111  }
112 }
- +Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.Win32.SafeHandles;
3 using System;
4 using System.DirectoryServices.AccountManagement;
5 using System.Security.Principal;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Security
11 {
16  {
20  readonly ILogger<WindowsSystemIdentityFactory> logger;
21 
28  static void GetUserAndDomainName(string input, out string username, out string domainName)
29  {
30  var splits = input.Split('\\');
31  username = splits.Length > 1 ? splits[1] : splits[0];
32  domainName = splits.Length > 1 ? splits[0] : null;
33  }
34 
39  public WindowsSystemIdentityFactory(ILogger<WindowsSystemIdentityFactory> logger)
40  {
41  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
42  }
43 
45  public Task<ISystemIdentity> CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
46  {
47  if (user == null)
48  throw new ArgumentNullException(nameof(user));
49 
50  if (user.SystemIdentifier == null)
51  throw new InvalidOperationException("User's SystemIdentifier must not be null!");
52 
53  PrincipalContext pc = null;
54  UserPrincipal principal = null;
55 
56  bool TryGetPrincipalFromContextType(ContextType contextType)
57  {
58  try
59  {
60  pc = new PrincipalContext(contextType);
61  cancellationToken.ThrowIfCancellationRequested();
62  principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
63  }
64  catch (OperationCanceledException)
65  {
66  throw;
67  }
68  catch (Exception e)
69  {
70  logger.LogWarning("Error loading user for context type {0}! Exception: {1}", contextType, e);
71  }
72  finally
73  {
74  if (principal == null)
75  {
76  pc?.Dispose();
77  cancellationToken.ThrowIfCancellationRequested();
78  }
79  }
80  return principal != null;
81  };
82 
83  if (!TryGetPrincipalFromContextType(ContextType.Machine) && !TryGetPrincipalFromContextType(ContextType.Domain))
84  return null;
85  return (ISystemIdentity)new WindowsSystemIdentity(principal);
86  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
87 
89  public Task<ISystemIdentity> CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
90  {
91  if (username == null)
92  throw new ArgumentNullException(nameof(username));
93  if (password == null)
94  throw new ArgumentNullException(nameof(password));
95 
96  var originalUsername = username;
97  GetUserAndDomainName(originalUsername, out username, out var domainName);
98 
99  var res = NativeMethods.LogonUser(username, domainName, password, 3 /*LOGON32_LOGON_NETWORK*/, 0 /*LOGON32_PROVIDER_DEFAULT*/, out var token);
100  if (!res)
101  {
102  logger.LogTrace("Failed to log in username {0}!", originalUsername);
103  return null;
104  }
105 
106  logger.LogTrace("Successfully logged in username {0}!", originalUsername);
107 
108  using (var handle = new SafeAccessTokenHandle(token)) //checked internally, windows identity always duplicates the handle when constructed with a userToken
109  return (ISystemIdentity)new WindowsSystemIdentity(new WindowsIdentity(handle.DangerousGetHandle())); //https://github.com/dotnet/corefx/blob/6ed61acebe3214fcf79b4274f2bb9b55c0604a4d/src/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L271
110  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
111  }
112 }
Represents a user on the current System.Runtime.InteropServices.OSPlatform
diff --git a/annotated.html b/annotated.html index b5006d001b..ad354e2d2a 100644 --- a/annotated.html +++ b/annotated.html @@ -303,10 +303,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  CInstanceManager  CReattachInfoHandler  NConfiguration - CDatabaseConfigurationConfiguration options for the Models.DatabaseContext<TParentContext> - CFileLoggingConfigurationFile logging configuration options - CGeneralConfigurationGeneral configuration options - CUpdatesConfigurationConfiguration for the automatic update system + CControlPanelConfigurationConfiguration options for the web control panel + CDatabaseConfigurationConfiguration options for the Models.DatabaseContext<TParentContext> + CFileLoggingConfigurationFile logging configuration options + CGeneralConfigurationGeneral configuration options + CUpdatesConfigurationConfiguration for the automatic update system  NConsole  NTests  CTestProgram @@ -339,7 +340,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  CTestJobException  CTestJobHandler  CTestPlatformIdentifier - CTestSetupWizard + CTestServiceCollectionExtensions + CBadConfig1 + CBadConfig2 + CBadConfig3 + CGoodConfig + CTestSetupWizard  CApplication  CApplicationBuilderExtensionsExtensions for IApplicationBuilder  CAsyncDelayer @@ -368,7 +374,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  CProcessExecutor  CRestartRegistration  CSemaphoreSlimContextAsync lock context helper - CSetupWizard + CServiceCollectionExtensionsExtensions for IServiceCollection + CSetupWizard  NIO  NTests  CTestConsole @@ -480,16 +487,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  CAutoGeneratedProgram  CController  CDbContext - CException - CIApplicationLifetime - CIDesignTimeDbContextFactory - CIDisposable - CIEventLog - CIHostedService - CJsonConverter - CMigration - CModelSnapshot - CServiceBase + CIApplicationLifetime + CIDesignTimeDbContextFactory + CIDisposable + CIEventLog + CIHostedService + CJsonConverter + CMigration + CModelSnapshot + CServiceBase
diff --git a/architecture.html b/architecture.html index 84553477eb..c6c3d932d2 100644 --- a/architecture.html +++ b/architecture.html @@ -135,7 +135,7 @@ Main Server

Server Initialization

The server's entrypoint is in the Tgstation.Server.Host.Program class. This class mainly determines if the Host watchdog is present and creates and runs the Tgstation.Server.Host.Server class. That class then builds an ASP.NET Core web host using the Tgstation.Server.Host.Core.Application class.

-

The Tgstation.Server.Host.Core.Application class has two methods called by the framework. First the Tgstation.Server.Host.Core.Application.ConfigureServices method sets up dependency injection of interfaces for Controllers, the Tgstation.Server.Host.Models.DatabaseContext, and the component factories of the server. The framework handles constructing these things once the application starts. Configuration is loaded from the appropriate appSettings.json into the Tgstation.Server.Host.Configuration classes for injection as well. Then Tgstation.Server.Host.Core.Application.Configure method is run which sets up the web request pipeline which currently has the following stack of handlers:

+

The Tgstation.Server.Host.Core.Application class has two methods called by the framework. First the Tgstation.Server.Host.Core.Application.ConfigureServices method sets up dependency injection of interfaces for Controllers, the Tgstation.Server.Host.Models.DatabaseContext, and the component factories of the server. The framework handles constructing these things once the application starts. Configuration is loaded from the appropriate appSettings.json into the Tgstation.Server.Host.Configuration classes for injection as well. Then Tgstation.Server.Host.Core.Application.Configure method is run which sets up the web request pipeline which currently has the following stack of handlers:

  • Catch any exceptions and respond with 500 and detailed HTML error page
  • Respond with 503 if the application is still starting or shutting down
  • diff --git a/class_auto_generated_program.html b/class_auto_generated_program.html index 180eeab0e0..126ab2dcb0 100644 --- a/class_auto_generated_program.html +++ b/class_auto_generated_program.html @@ -295,12 +295,12 @@ Static Private Member Functions

The documentation for this class was generated from the following files:
diff --git a/class_exception.png b/class_exception.png deleted file mode 100644 index 7abe28cc8e18e65aa9992a337dc406fe0cd37fdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9718 zcmeHt2~<<(+Ah{(si1OO5obBJ35rYtDrL-ZK-o-MltHG5h?Ih$K$w%%Uy&**x)B)^ zQZ+&?5L9FcQxFTtm>MKuNC?OfLm**D2qDw`g4*G%cAftJI_uuM&boW8tRy=-d+%@W z=Xu`eeZGD9i0h%18XGj!)YMi&4hP)S)IK7DSLG*5!J3S#x{2Uz-4W+w_DZD^e0)10 z!lX6a0G}3L_4W13E;Dt(*H5C|T#u@OjZ{l_>oGGmHDoe$VDGWGC6lblm`=5-ZYww1YEn(RCo|k|V!!yaRo`#a zl>S_Map~Kp2D@mJA+@R}kF{7%Uw-@k=M%(hwpjV|qd3-P&tyjp^_{nfQ#to+e;}S! zTk5zVn$9+(9v8{ZEr{Y33Q;Ki@(klMj^A}0q&pPD;&64%jp;*%Zvo9d6G$V6gM zP`2mVR=au4{i@9_*^DF;gU=c0%MPutW=0@iJ8CHPKeibuN$aZWBxIp8v=v{hz`W$p za5sgsoQERANk+5%fRhh08E-3i`b6-!6P(ZyC_oqN{~yR?tm>p#UOg#%L06C8D{+HD&^<8U>O}A9u7o*U%x8a zA|SS^`0-knOHXdN!GwM6>X?Fljl@^1i!5$=ga5z%XO3SvCMQ26$u2)6r>UUd%BAa> zbv{BLnqjmfU0@i2cL8s;KMN0V^gClFdWfAy_VHFh2#=T zDDP=AU*WEYy(=Fwg-<>Alpc-|wH)>D$8~WwnokD3We3wPh-b@)>`#Q-L*SazX)M-W z8oGq$xx<=M+)Ih#S$41nDkQ0i3M{uW%sVERZP#2?S5M)Yvpb}Zi~Y3>TwvX&sYG=F zN7h6?XnfFUcI3rJio?}XF^nrlSay)^Mp901CIz2O9ONDBKvavpG%K%)I_yeIld`^! z>czX~&D{{bl`}QSeJ?TJV?OuNZFp5(ok|po1_*JXtl2ZR$m6C;$-a8~%}PH4I$~-R z>0c|MURRdvc;QyY29yASW4w2KOF8XAFuk9x;7SDmw1&|?+J%bOhriB ze;f+iXpwMDj~gH*yFM{N)*gu4!BqywiqnNb@!n^ep1LvjV33@Hw|#?i*mLoQ4n8Ha z?^OFw9V%&+@7KW^bDyiuE7d}-ey6IZ=vr~M2wR3~35l^S8k&9^-navdH2`7zHHrb- z)ii0H&s#gK%W(xZk*6@JmU_aZ7z=3NvfNXi3D-$* zdf9%~b}!gY3$f&FkV3!dmJlm8Z<*NONDGAz!{U=aJ1>+=vrolbLx#Y&{MxYatq;(O zRYA)h;E(kP;BPI!UrYhOpT7!!xoFt6Yga}{PCg}AkGsp|Xm)VqreP*6G% z(Skk-q)j!@{mA5{DxPU&Xs`Mn4AbtI+n3*hOCeQhU!4(`E6Y3;!@?^T%Bw6kW+K=_ zv{(cB$$aC;YyAtnrUa*H|5=q#KDt2GpYo|y` zc|m25;;3BF^RuHy^HdZ>T|YiMY;NA6$n~K{GH1H|NKswE6@{qM{EQT>C;a78G^tsm znI}oTeW{=4m-zL@vn7|#*Z~Aw2XZ>^N9MEL6TMN0 z7AI95AUIkbjqo^5*u?KndqHHHSp&;Hw%y}w{e^c>U9ZletXA(OG8|RR)V+n@i3%;c#CSthu?#UP3Ja5Vl zY5YA-R%$?nzZUf-g8e<*utau%mQf7Nq&<;-7W0W0xOF%~u2!iNylF2o88wW$0h!pF zL5jUQ^kwUn*CGiBB&74(8~vw907y3j;=FX~V}PO~gA@Rl{}%TCMH%|H>?>m1dnWcR ziCa2wn6_9){?h0K4u4ob%CbUUCCTN{%X}VGY|(3xC9E-l@)r4!4e3%RE?e=oux%zn z*~R>d@~(xs#=_XJgcP7LZ$`G1lMGXAbw=-9lL73r=v9? zqeJ_P68ox!bM*gX7jvnUWc3;6Y@9$`;&a6QJv*dV+{r&38 zmb#IQ@vq+F8_n=-YZD>66EOWlTXddD;@HLtK;Xd?c;K*v#Gr>zcfT(WG)Yp6zGgYX z??BJ|IVd+%o7`GR^{;MRjchSjAzjzO>Zrii8NHd?edV`E_txtQB2$#UW1a4m4~e|> z%5E5!l$2Dn8vi-BEt1mi%5#o$!Z}%5m~k(-&t_L3U0mCE?v>4nX$p(<1{75b>36DA z*P&`k`h-c9+8KyrS2E<0t#hLOvlX6W?}3lMOK@0-za)N|VBALFBpD1D$c;xOk-^5? z`=AaOH{K_qMq2F|k{>W+iqJ1vn!{7{bd1fM^Lg<~FaXU}X#4Hl&Z}65$5)6krhK29E#_1?*V*sds+N1zt2{DwA;!&`726 zDPV)0E?vifHHlFh__iWlTSFH618`ejDp9`;EZC)oKFlD@ z%Fg0obhv(4-V!RsWOJrV8)>$mJ>C|Mg>8%!vdxf z_=VA1(%rnXn1nnYse-R-Vc`KA2c8igXR#LM)=f69mIX|g0Pl?7#`Qa2ZRNA0{TGPa zPvdSx%T0Y=G1y6p;sB~nE`t3)C5p8~bJ6L>xt4fz2D!O%RT7eGG^OjfFa=7VCfweMs!(C-#D^iTTDnBIfLzMh%$@I|?{$bKa5 zT!-JvOz1Ph7MNL5 z?BZZX$>SeDChyM*qBYmzw>H+u@7(s6hGXzXT>D8xeA8}z1v?87Ke+J$I{FcSmlo{* z8Qn6rLkWCI9rJ0&Z~2DfEr=NBLh#!zSnD}vx&fDbL@dggZo zJml)+@o}fyTvJlTy26@;35=iWh)d%>=ic95XP2YCm$U{y@N6ppe14*)xB}^?Ub#)2 zTvYndWPHJ+fl_kLnA>zfV8m?(i}kcKEprr?9pr(T(c?OHUUOdfd~bN_Gk&GVbU?$} z5aCe3o;JD1zB78s$}cv;#%3HXgx{t`LnHSfQ%*D0A;RChTFCDF!!5GDr1})k$@c=U z)C`4C)HnX}PIu94O*k^?%yTj~GC2G3X%~jw>Xnhj{)4SV)R(IhPbuIr4~lD zbF6V(hVadR1;Vc*X;Pe9X0zySTrxp%d2GZj=|TlREcu(cG`7VZy*WaM)yao1zqdEN z7@TwyFQ<7-xAd~fd6F*YlhhEBW(~5te+T1%i6mxCm_(=J5OZxMc}a%$^}wSvCpZTU z7}8;7_^fKZxo%`=-;LCVGq64f1+vsTrprN6;bOH z#R%oikLBdSMOn(Et=#*A;Z6%*2S(=jl`cbdQl8|j{FKTS+2IN~r7J<^^1n(Dxtqmg zt2(O(JX3a{uX-IU)QLNOw&c=)fO3PswLW6Oq%3J&&8=wGD0=IATDUFJBHZJ8yW{je zK#QtQrY1!Bg0;U%UQYBWMUlc@JATZsSSJI zhtlh9tS^DX8JabA^jaSI8JkV({Vp`CST(5ap^Ns%y0oYdnmLpdkp$S$b4zFnAzG~6 zpnygSBHjNRKe)ysL)$}^*b16lgk;YjHG2PCRZisOKXUHcHK|s!Bx0Ms_PpT8U&xgH z50{g+ehB17A8eX9eHY{i%Xa^j(E*$MhyKTJgGZ=WJ0-00Qm=OmXlkP;p&27pk*1n+ zhao!$7@=9$M|o6VCh!oGFZz_)#qY1%EkFWtLS#(z;g3Qya zN9JNH=ce)tFVs~B4?J69ya36cZw_`=%Iua&h3H&%-%KKM1$^wB+I<-&MnP)Vj%7Gk67=0!jYm=nMH`~^LF_xIV~iMND8nNR2& zSk){9)~hAyIVW7pmQ$;H8aMrn(ov`4MKwH-CL(pxr#pYO{CgD{FUN+AkDqzKaieie zOY+gfojSsHd5XF-tP%mx2zI`87Q zqhUDsheTBrF8C=8p^9?2U%MX(_BVPT<*X9lfH+I?PSLhmMkbEgb56U>r$|V|)ON{I zXe|zPTY9KqPpjOF8E+~^0+T1ojl|<`HCw#B{e61Z#E-xne2cm?JW@S4H3b$MrY9|BVd7eW64 z`h$-L-!{sQ%T$Cgg7Aa`@Q4yn05yuvflSJFLYF}AK$sC2EjGCNe?oV>}`+G(3Wov zSlGU2^ReN!1r0v;t8(X%vA>u0S*$tJ@U`dT#EZSGe(Q7O(wy*FaF8{sg8(0}IRH45 zMWf@JTX(&dY`c1TAkc_wMyED(`1;3q8=n!W9VSdl-^scct$G&-D_13Lb%eWMRan`{ zk%m{I*H7jAE&VQ?$?wd^XYqT75iLhm^+^}9iW>D5pmWE`-*y(0$us+aus7z`>o`gS~ z_Zh1DM%+;>`nHWFwYK0*i4dhJNk7{`d8*hMq{^*ygoH7jndYHND)qQpG)K5*V+N=x zDIl@goIZjE=$@XB2R$#0_-n%VpDpVbhy}A~xEG_8@cPFG!hGSR6eo7E7vmCVs60h> zn%{UVGotj;wL`9;-L;Daoo8<=F|A7p8eDQPJhO369qzJ=x0;W$IK2Y`}=z%~HHGPam?ILGN zqe5gh&wZAoxv~0B-yy&BGn>Na=67_G;|-YDmlJ_yFu8{lM2*1OHlE~1g-|GWOW!g& zeW)OM3%M>7fu zOZZxp0`Q+B5XqJR8B!z%uTQ9C$WiMZsRjxFp1; znFN+R5*Lmd%ooY6`giX~3pxCC<>yT@{y>f*C23UuUGy%>&H5%W;Ayw?Q}i z{89G^V{htQ@4oVcGtP5wmQd~#09@BEcCntpyduu8U91(`ocqnp$o3isnlQy}T zMMrLyQxzQbMLNk`>`g0f7yHtRzfDUws@j#_uukSovmXOnYEpFDzb_ru_K-dznGQ-_F6t)=ux-4waKtPdNbO&&SkU!x;DN$=eR$n(3~HY&5l8 zm3%YJxWnZp2p3o#ce1-ou==559}RIlD)(#M(e(C`U_X^ye2EOkdpQ;X*Ir%J?Cv8 zKm7~(_g&5K;GNZa&Jk}ZP_&b@KFw42T2Wo=PaR`17Z0V+b*_!7#T#=kXfdv!BKmF! zb>?H?0qeTR3GZ}^D2Zd$HZIPx%aMi|5#JVqr(1?|du|JZ$Y{MR_sccVA_>CBT8cry z`M?N?U13$hZ+t2Gyqr${PDY0te5U{Y&igc;2i`uOjWQIsiHbZcqQ6FRu>;$>P(E!x z4lnk$*Ms%B9Mkr3wr8%?=0{Kpcv0b1%xtHsudCJ!_mYi&6x2>{O(mVK#1&=KdQ6P6 zdXF(?Y|~1&iy7sra!{J8U|fq=4XXKUdbGcG`l)C|ST$OEmci%s+`}yZV2D|qVvinoA1S~@vbs|1s>8;gX~=o KkoKMUum263`1#QQ diff --git a/class_release_notes_1_1_program.html b/class_release_notes_1_1_program.html index 6e9c8b2ab9..dd0e28e823 100644 --- a/class_release_notes_1_1_program.html +++ b/class_release_notes_1_1_program.html @@ -138,8 +138,7 @@ Static Private Member Functions

The entrypoint for the Program

Definition at line 20 of file Program.cs.

-
21  {
22  if (args.Length < 1)
23  {
24  Console.WriteLine("Missing version argument!");
25  return 1;
26  }
27 
28  var versionString = args[0];
29  if (!Version.TryParse(versionString, out var version))
30  {
31  Console.WriteLine("Invalid version: " + versionString);
32  return 2;
33  }
34 
35  var doNotCloseMilestone = args.Length >= 2 && args[1].ToUpperInvariant() == "--NO-CLOSE";
36 
37  const string ReleaseNotesEnvVar = "TGS_RELEASE_NOTES_TOKEN";
38  var githubToken = Environment.GetEnvironmentVariable(ReleaseNotesEnvVar);
39  if (String.IsNullOrWhiteSpace(githubToken))
40  {
41  Console.WriteLine("Missing " + ReleaseNotesEnvVar + " environment variable!");
42  return 3;
43  }
44 
45  try
46  {
47  var client = new GitHubClient(new ProductHeaderValue("tgs_release_notes"));
48  client.Credentials = new Credentials(githubToken);
49 
50  const string RepoOwner = "tgstation";
51  const string RepoName = "tgstation-server";
52 
53  var releasesTask = client.Repository.Release.GetAll(RepoOwner, RepoName);
54 
55  Console.WriteLine("Getting pull requests in milestone " + versionString + "...");
56  var milestonePRs = await client.Search.SearchIssues(new SearchIssuesRequest
57  {
58  Milestone = versionString,
59  Type = IssueTypeQualifier.PullRequest,
60  Repos = { { RepoOwner, RepoName } }
61  }).ConfigureAwait(false);
62 
63  if (milestonePRs.IncompleteResults)
64  {
65  Console.WriteLine("Incomplete results for milestone PRs query!");
66  return 5;
67  }
68  Console.WriteLine(milestonePRs.Items.Count + " total pull requests");
69 
70  Task<Milestone> milestoneTask = null;
71  var milestoneTaskLock = new object();
72  var releaseDictionary = new Dictionary<int, List<string>>();
73  var authorizedUsers = new Dictionary<long, Task<bool>>();
74 
75  async Task GetReleaseNotesFromPR(Issue pullRequest)
76  {
77  //need to check it was merged
78  var fullPR = await client.Repository.PullRequest.Get(RepoOwner, RepoName, pullRequest.Number).ConfigureAwait(false);
79 
80  async Task<Milestone> GetMilestone()
81  {
82  if (fullPR.Milestone == null)
83  return null;
84  return await client.Issue.Milestone.Get(RepoOwner, RepoName, fullPR.Milestone.Number);
85  };
86 
87  lock (milestoneTaskLock)
88  if (milestoneTask == null)
89  milestoneTask = GetMilestone();
90 
91  if (!fullPR.Merged)
92  return;
93 
94  async Task BuildNotesFromComment(string comment, User user)
95  {
96  var commentSplits = comment.Split('\n');
97  var notesOpen = false;
98  var notesClosed = false;
99  var notes = new List<string>();
100  foreach (var line in commentSplits)
101  {
102  var trimmedLine = line.Trim();
103  if (!notesOpen)
104  {
105  notesOpen = trimmedLine.StartsWith(":cl:", StringComparison.Ordinal);
106  notesClosed = false;
107  continue;
108  }
109  if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal))
110  {
111  notesClosed = true;
112  notesOpen = false;
113  continue;
114  }
115  if (trimmedLine.Length == 0)
116  continue;
117  notes.Add(trimmedLine);
118  }
119  if (!notesClosed || notes.Count == 0)
120  return;
121 
122  Task<bool> authTask;
123  TaskCompletionSource<bool> ourTcs = null;
124  lock (authorizedUsers)
125  {
126  if (!authorizedUsers.TryGetValue(user.Id, out authTask))
127  {
128  ourTcs = new TaskCompletionSource<bool>();
129  authTask = ourTcs.Task;
130  authorizedUsers.Add(user.Id, authTask);
131  }
132  }
133 
134  if (ourTcs != null)
135  try
136  {
137  //check if the user has access
138  var perm = await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false);
139  ourTcs.SetResult(perm.Permission == PermissionLevel.Write || perm.Permission == PermissionLevel.Admin);
140  }
141  catch
142  {
143  ourTcs.SetResult(false);
144  throw;
145  }
146 
147  var authorized = await authTask.ConfigureAwait(false);
148  if (!authorized)
149  return;
150 
151  lock (releaseDictionary)
152  {
153  foreach (var I in notes)
154  Console.WriteLine("#" + fullPR.Number + " - " + I + " (@" + user.Login + ")");
155  if (releaseDictionary.TryGetValue(fullPR.Number, out var currentValues))
156  currentValues.AddRange(notes);
157  else
158  releaseDictionary.Add(fullPR.Number, notes);
159  }
160  }
161 
162  var comments = await client.Issue.Comment.GetAllForIssue(RepoOwner, RepoName, fullPR.Number).ConfigureAwait(false);
163  await Task.WhenAll(BuildNotesFromComment(fullPR.Body, fullPR.User), Task.WhenAll(comments.Select(x => BuildNotesFromComment(x.Body, x.User)))).ConfigureAwait(false);
164  }
165 
166  var tasks = new List<Task>();
167  foreach (var I in milestonePRs.Items)
168  tasks.Add(GetReleaseNotesFromPR(I));
169 
170  var releases = await releasesTask.ConfigureAwait(false);
171 
172  var releasingSuite = version.Major;
173 
174  Version highestReleaseVersion = null;
175  Release highestRelease = null;
176  foreach (var I in releases)
177  {
178  if (!Version.TryParse(I.TagName.Replace("tgstation-server-v", String.Empty), out var currentReleaseVersion))
179  {
180  Console.WriteLine("WARNING: Unable to determine version of release " + I.HtmlUrl);
181  continue;
182  }
183 
184  if (currentReleaseVersion.Major == releasingSuite && (highestReleaseVersion == null || currentReleaseVersion > highestReleaseVersion))
185  {
186  highestReleaseVersion = currentReleaseVersion;
187  highestRelease = I;
188  }
189  }
190 
191  if (highestReleaseVersion == null)
192  {
193  Console.WriteLine("Unable to determine highest release version for suite " + releasingSuite + "!");
194  return 6;
195  }
196 
197  var oldNotes = highestRelease.Body;
198 
199  var splits = new List<string>(oldNotes.Split('\n'));
200  //trim away all the lines that don't start with #
201 
202  string keepThisRelease;
203  if (version.Revision == 0)
204  if (version.Build == 0)
205  keepThisRelease = "# ";
206  else
207  keepThisRelease = "## ";
208  else
209  keepThisRelease = "### ";
210 
211  for (; !splits[0].StartsWith(keepThisRelease, StringComparison.Ordinal); splits.RemoveAt(0))
212  if (splits.Count == 1)
213  {
214  Console.WriteLine("Error formatting release notes: Can't detemine notes start!");
215  return 7;
216  }
217 
218  oldNotes = String.Join('\n', splits);
219 
220  string prefix;
221  switch (releasingSuite)
222  {
223  case 3:
224  prefix = "The /tg/station server suite";
225  break;
226  default:
227  prefix = "See https://tgstation.github.io/tgstation-server for installation instructions";
228  break;
229  }
230 
231  var newNotes = new StringBuilder(prefix);
232  newNotes.Append(Environment.NewLine);
233  newNotes.Append(Environment.NewLine);
234  if (version.Revision == 0)
235  if (version.Build == 0)
236  {
237  newNotes.Append("# [Version ");
238  newNotes.Append(version.Minor);
239  }
240  else
241  {
242  newNotes.Append("## [Changelog for ");
243  newNotes.Append(version.Build);
244  newNotes.Append(".x");
245  }
246  else
247  {
248  newNotes.Append("### [Patch ");
249  newNotes.Append(version.Revision);
250  }
251  newNotes.Append("](");
252  var milestone = await milestoneTask.ConfigureAwait(false);
253  if (milestone == null)
254  {
255  Console.WriteLine("Unable to detemine milestone!");
256  return 9;
257  }
258  newNotes.Append(milestone.HtmlUrl);
259  newNotes.Append("?closed=1)");
260  newNotes.Append(Environment.NewLine);
261 
262  await Task.WhenAll(tasks).ConfigureAwait(false);
263 
264  if (releaseDictionary.Count == 0)
265  {
266  Console.WriteLine("No release notes for this milestone!");
267  return 8;
268  }
269 
270  foreach (var I in releaseDictionary)
271  foreach (var note in I.Value)
272  {
273  newNotes.Append(Environment.NewLine);
274  newNotes.Append("- ");
275  newNotes.Append(note);
276  newNotes.Append(" (#");
277  newNotes.Append(I.Key);
278  newNotes.Append(')');
279  }
280 
281  newNotes.Append(Environment.NewLine);
282  newNotes.Append(Environment.NewLine);
283  newNotes.Append(oldNotes);
284 
285  Console.WriteLine("Writing out new release notes...");
286  var releaseNotes = newNotes.ToString();
287  await File.WriteAllTextAsync("release_notes.md", releaseNotes).ConfigureAwait(false);
288 
289  if (doNotCloseMilestone)
290  Console.WriteLine("Not closing milestone due to parameter!");
291  else
292  {
293  Console.WriteLine("Closing milestone...");
294  await client.Issue.Milestone.Update(RepoOwner, RepoName, milestone.Number, new MilestoneUpdate
295  {
296  State = ItemState.Closed
297  }).ConfigureAwait(false);
298  }
299 
300  return 0;
301  }
302  catch (Exception e)
303  {
304  Console.WriteLine(e);
305  return 4;
306  }
307  }
-
+
21  {
22  if (args.Length < 1)
23  {
24  Console.WriteLine("Missing version argument!");
25  return 1;
26  }
27 
28  var versionString = args[0];
29  if (!Version.TryParse(versionString, out var version))
30  {
31  Console.WriteLine("Invalid version: " + versionString);
32  return 2;
33  }
34 
35  var doNotCloseMilestone = args.Length >= 2 && args[1].ToUpperInvariant() == "--NO-CLOSE";
36 
37  const string ReleaseNotesEnvVar = "TGS_RELEASE_NOTES_TOKEN";
38  var githubToken = Environment.GetEnvironmentVariable(ReleaseNotesEnvVar);
39  if (String.IsNullOrWhiteSpace(githubToken))
40  {
41  Console.WriteLine("Missing " + ReleaseNotesEnvVar + " environment variable!");
42  return 3;
43  }
44 
45  try
46  {
47  var client = new GitHubClient(new ProductHeaderValue("tgs_release_notes"));
48  client.Credentials = new Credentials(githubToken);
49 
50  const string RepoOwner = "tgstation";
51  const string RepoName = "tgstation-server";
52 
53  var releasesTask = client.Repository.Release.GetAll(RepoOwner, RepoName);
54 
55  Console.WriteLine("Getting pull requests in milestone " + versionString + "...");
56  var milestonePRs = await client.Search.SearchIssues(new SearchIssuesRequest
57  {
58  Milestone = versionString,
59  Type = IssueTypeQualifier.PullRequest,
60  Repos = { { RepoOwner, RepoName } }
61  }).ConfigureAwait(false);
62 
63  if (milestonePRs.IncompleteResults)
64  {
65  Console.WriteLine("Incomplete results for milestone PRs query!");
66  return 5;
67  }
68  Console.WriteLine(milestonePRs.Items.Count + " total pull requests");
69 
70  Task<Milestone> milestoneTask = null;
71  var milestoneTaskLock = new object();
72  var releaseDictionary = new Dictionary<int, List<string>>();
73  var authorizedUsers = new Dictionary<long, Task<bool>>();
74 
75  async Task GetReleaseNotesFromPR(Issue pullRequest)
76  {
77  //need to check it was merged
78  var fullPR = await client.Repository.PullRequest.Get(RepoOwner, RepoName, pullRequest.Number).ConfigureAwait(false);
79 
80  async Task<Milestone> GetMilestone()
81  {
82  if (fullPR.Milestone == null)
83  return null;
84  return await client.Issue.Milestone.Get(RepoOwner, RepoName, fullPR.Milestone.Number);
85  };
86 
87  lock (milestoneTaskLock)
88  if (milestoneTask == null)
89  milestoneTask = GetMilestone();
90 
91  if (!fullPR.Merged)
92  return;
93 
94  async Task BuildNotesFromComment(string comment, User user)
95  {
96  var commentSplits = comment.Split('\n');
97  var notesOpen = false;
98  var notesClosed = false;
99  var notes = new List<string>();
100  foreach (var line in commentSplits)
101  {
102  var trimmedLine = line.Trim();
103  if (!notesOpen)
104  {
105  notesOpen = trimmedLine.StartsWith(":cl:", StringComparison.Ordinal);
106  notesClosed = false;
107  continue;
108  }
109  if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal))
110  {
111  notesClosed = true;
112  notesOpen = false;
113  continue;
114  }
115  if (trimmedLine.Length == 0)
116  continue;
117  notes.Add(trimmedLine);
118  }
119  if (!notesClosed || notes.Count == 0)
120  return;
121 
122  Task<bool> authTask;
123  TaskCompletionSource<bool> ourTcs = null;
124  lock (authorizedUsers)
125  {
126  if (!authorizedUsers.TryGetValue(user.Id, out authTask))
127  {
128  ourTcs = new TaskCompletionSource<bool>();
129  authTask = ourTcs.Task;
130  authorizedUsers.Add(user.Id, authTask);
131  }
132  }
133 
134  if (ourTcs != null)
135  try
136  {
137  //check if the user has access
138  var perm = await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false);
139  ourTcs.SetResult(perm.Permission == PermissionLevel.Write || perm.Permission == PermissionLevel.Admin);
140  }
141  catch
142  {
143  ourTcs.SetResult(false);
144  throw;
145  }
146 
147  var authorized = await authTask.ConfigureAwait(false);
148  if (!authorized)
149  return;
150 
151  lock (releaseDictionary)
152  {
153  foreach (var I in notes)
154  Console.WriteLine("#" + fullPR.Number + " - " + I + " (@" + user.Login + ")");
155  if (releaseDictionary.TryGetValue(fullPR.Number, out var currentValues))
156  currentValues.AddRange(notes);
157  else
158  releaseDictionary.Add(fullPR.Number, notes);
159  }
160  }
161 
162  var comments = await client.Issue.Comment.GetAllForIssue(RepoOwner, RepoName, fullPR.Number).ConfigureAwait(false);
163  await Task.WhenAll(BuildNotesFromComment(fullPR.Body, fullPR.User), Task.WhenAll(comments.Select(x => BuildNotesFromComment(x.Body, x.User)))).ConfigureAwait(false);
164  }
165 
166  var tasks = new List<Task>();
167  foreach (var I in milestonePRs.Items)
168  tasks.Add(GetReleaseNotesFromPR(I));
169 
170  var releases = await releasesTask.ConfigureAwait(false);
171 
172  var releasingSuite = version.Major;
173 
174  Version highestReleaseVersion = null;
175  Release highestRelease = null;
176  foreach (var I in releases)
177  {
178  if (!Version.TryParse(I.TagName.Replace("tgstation-server-v", String.Empty), out var currentReleaseVersion))
179  {
180  Console.WriteLine("WARNING: Unable to determine version of release " + I.HtmlUrl);
181  continue;
182  }
183 
184  if (currentReleaseVersion.Major == releasingSuite && (highestReleaseVersion == null || currentReleaseVersion > highestReleaseVersion))
185  {
186  highestReleaseVersion = currentReleaseVersion;
187  highestRelease = I;
188  }
189  }
190 
191  if (highestReleaseVersion == null)
192  {
193  Console.WriteLine("Unable to determine highest release version for suite " + releasingSuite + "!");
194  return 6;
195  }
196 
197  var oldNotes = highestRelease.Body;
198 
199  var splits = new List<string>(oldNotes.Split('\n'));
200  //trim away all the lines that don't start with #
201 
202  string keepThisRelease;
203  if (version.Revision == 0)
204  if (version.Build == 0)
205  keepThisRelease = "# ";
206  else
207  keepThisRelease = "## ";
208  else
209  keepThisRelease = "### ";
210 
211  for (; !splits[0].StartsWith(keepThisRelease, StringComparison.Ordinal); splits.RemoveAt(0))
212  if (splits.Count == 1)
213  {
214  Console.WriteLine("Error formatting release notes: Can't detemine notes start!");
215  return 7;
216  }
217 
218  oldNotes = String.Join('\n', splits);
219 
220  string prefix;
221  switch (releasingSuite)
222  {
223  case 3:
224  prefix = "The /tg/station server suite";
225  break;
226  default:
227  prefix = "See https://tgstation.github.io/tgstation-server for installation instructions";
228  break;
229  }
230 
231  var newNotes = new StringBuilder(prefix);
232  newNotes.Append(Environment.NewLine);
233  newNotes.Append(Environment.NewLine);
234  if (version.Revision == 0)
235  if (version.Build == 0)
236  {
237  newNotes.Append("# [Version ");
238  newNotes.Append(version.Minor);
239  }
240  else
241  {
242  newNotes.Append("## [Changelog for ");
243  newNotes.Append(version.Build);
244  newNotes.Append(".x");
245  }
246  else
247  {
248  newNotes.Append("### [Patch ");
249  newNotes.Append(version.Revision);
250  }
251  newNotes.Append("](");
252  var milestone = await milestoneTask.ConfigureAwait(false);
253  if (milestone == null)
254  {
255  Console.WriteLine("Unable to detemine milestone!");
256  return 9;
257  }
258  newNotes.Append(milestone.HtmlUrl);
259  newNotes.Append("?closed=1)");
260  newNotes.Append(Environment.NewLine);
261 
262  await Task.WhenAll(tasks).ConfigureAwait(false);
263 
264  if (releaseDictionary.Count == 0)
265  {
266  Console.WriteLine("No release notes for this milestone!");
267  return 8;
268  }
269 
270  foreach (var I in releaseDictionary)
271  foreach (var note in I.Value)
272  {
273  newNotes.Append(Environment.NewLine);
274  newNotes.Append("- ");
275  newNotes.Append(note);
276  newNotes.Append(" (#");
277  newNotes.Append(I.Key);
278  newNotes.Append(')');
279  }
280 
281  newNotes.Append(Environment.NewLine);
282  newNotes.Append(Environment.NewLine);
283  newNotes.Append(oldNotes);
284 
285  Console.WriteLine("Writing out new release notes...");
286  var releaseNotes = newNotes.ToString();
287  await File.WriteAllTextAsync("release_notes.md", releaseNotes).ConfigureAwait(false);
288 
289  if (doNotCloseMilestone)
290  Console.WriteLine("Not closing milestone due to parameter!");
291  else
292  {
293  Console.WriteLine("Closing milestone...");
294  await client.Issue.Milestone.Update(RepoOwner, RepoName, milestone.Number, new MilestoneUpdate
295  {
296  State = ItemState.Closed
297  }).ConfigureAwait(false);
298  }
299 
300  return 0;
301  }
302  catch (Exception e)
303  {
304  Console.WriteLine(e);
305  return 4;
306  }
307  }

The documentation for this class was generated from the following file:
@@ -602,7 +602,7 @@ Static Private Attributes

Definition at line 91 of file ApiHeaders.cs.

-

Referenced by Tgstation.Server.Host.Controllers.HomeController.CreateToken().

+

Referenced by Tgstation.Server.Host.Controllers.HomeController.CreateToken().

@@ -727,7 +727,7 @@ Static Private Attributes

Definition at line 71 of file ApiHeaders.cs.

-

Referenced by Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync().

+

Referenced by Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync().

@@ -753,7 +753,7 @@ Static Private Attributes

Definition at line 61 of file ApiHeaders.cs.

-

Referenced by Tgstation.Server.Host.Security.ClaimsInjector.InjectClaimsIntoContext(), and Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync().

+

Referenced by Tgstation.Server.Host.Security.ClaimsInjector.InjectClaimsIntoContext(), and Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync().

@@ -779,7 +779,7 @@ Static Private Attributes

Definition at line 86 of file ApiHeaders.cs.

-

Referenced by Tgstation.Server.Host.Controllers.HomeController.CreateToken().

+

Referenced by Tgstation.Server.Host.Controllers.HomeController.CreateToken().

@@ -831,7 +831,7 @@ Static Private Attributes

Definition at line 66 of file ApiHeaders.cs.

-

Referenced by Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync().

+

Referenced by Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync().

@@ -857,7 +857,7 @@ Static Private Attributes

Definition at line 81 of file ApiHeaders.cs.

-

Referenced by Tgstation.Server.Host.Controllers.HomeController.CreateToken().

+

Referenced by Tgstation.Server.Host.Controllers.HomeController.CreateToken().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_error_message.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_error_message.html index 28c920ce28..fc5af70b69 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_error_message.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_error_message.html @@ -138,7 +138,7 @@ Properties

Definition at line 13 of file ErrorMessage.cs.

-

Referenced by Tgstation.Server.Host.Controllers.ChatController.Create(), Tgstation.Server.Host.Controllers.InstanceController.Create(), Tgstation.Server.Host.Controllers.ConfigurationController.File(), Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync(), and Tgstation.Server.Host.Controllers.ConfigurationController.Update().

+

Referenced by Tgstation.Server.Host.Controllers.ChatController.Create(), Tgstation.Server.Host.Controllers.InstanceController.Create(), Tgstation.Server.Host.Controllers.ConfigurationController.File(), Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync(), and Tgstation.Server.Host.Controllers.ConfigurationController.Update().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html index ad4e094cf8..4a03e91cb3 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html @@ -240,7 +240,7 @@ Properties

Definition at line 15 of file User.cs.

-

Referenced by Tgstation.Server.Host.Security.IdentityCache.CacheSystemIdentity(), Tgstation.Server.Host.Core.JobManager.CancelJob(), Tgstation.Server.Host.Controllers.HomeController.CreateToken(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.UserController.GetId(), Tgstation.Server.Host.Controllers.InstanceController.GetId(), Tgstation.Server.Host.Controllers.InstanceController.List(), Tgstation.Server.Host.Security.IdentityCache.LoadCachedIdentity(), Tgstation.Server.Host.Controllers.InstanceController.NormalizeModelPath(), Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync(), Tgstation.Server.Host.Core.JobManager.RunJob(), Tgstation.Server.Host.Controllers.ByondController.Update(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), Tgstation.Server.Host.Controllers.RepositoryController.Update(), and Tgstation.Server.Client.UsersClient.UsersClient().

+

Referenced by Tgstation.Server.Host.Security.IdentityCache.CacheSystemIdentity(), Tgstation.Server.Host.Core.JobManager.CancelJob(), Tgstation.Server.Host.Controllers.HomeController.CreateToken(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.UserController.GetId(), Tgstation.Server.Host.Controllers.InstanceController.GetId(), Tgstation.Server.Host.Controllers.InstanceController.List(), Tgstation.Server.Host.Security.IdentityCache.LoadCachedIdentity(), Tgstation.Server.Host.Controllers.InstanceController.NormalizeModelPath(), Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync(), Tgstation.Server.Host.Core.JobManager.RunJob(), Tgstation.Server.Host.Controllers.ByondController.Update(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), Tgstation.Server.Host.Controllers.RepositoryController.Update(), and Tgstation.Server.Client.UsersClient.UsersClient().

diff --git a/class_tgstation_1_1_server_1_1_client_1_1_api_conflict_exception.html b/class_tgstation_1_1_server_1_1_client_1_1_api_conflict_exception.html index f3a4980659..29d9570d41 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_api_conflict_exception.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_api_conflict_exception.html @@ -108,7 +108,6 @@ Inheritance diagram for Tgstation.Server.Client.ApiConflictException:
Tgstation.Server.Client.ClientException -Exception
@@ -123,7 +122,7 @@ Public Member Functions - +
 ApiConflictException (string message)
 Construct an ApiConflictException with a message More...
 
 ApiConflictException (string message, Exception innerException)
 ApiConflictException (string message, Exception innerException)
 Construct an ApiConflictException with a message and innerException More...
 
@@ -139,7 +138,7 @@ Additional Inherited Members - + @@ -229,7 +228,7 @@ Additional Inherited Members

Construct an ApiConflictException with a message

Parameters
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
- Properties inherited from Tgstation.Server.Client.ClientException
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -251,7 +250,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -265,8 +264,8 @@ Additional Inherited Members

Construct an ApiConflictException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_api_conflict_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_api_conflict_exception.png index d3098730703de5a9f407e1fcacbf7ceaf42309dc..6de14591647d03a00cc60476c5c92959fd928ed4 100644 GIT binary patch delta 1111 zcmbQoxrB2rvX%Izg`G zN4J2}1f?Cj@5i<}O0*oj8`<*ktVEJ~S^4}|k6*ja{^Z8c+8M}W=fBAb=K-*qzmi)oFt;g=%{U4|Qg7>acy6qFlpK-;aO$^SkPWns@*9(eDS^{qVBEj@|LA4k_>) zdhkx?&;)}PfkSUa8P;9XXPCfs*zcjYOv}Om0rtdzZWae69-uOYh!&tWMuu(!j%EXf z1VOe$0X7C5kbZ`SdRGacB*O-v3EXk?*smgTUS=|KzfbMW$u0iUcGTCf@5-4q*DTxS$L&o^lV4)r{fp)BocS|v zs=xL&T;gPG{JZ{Tw&9GKcOAe9=1fG3V&aW$xx4o=9k}|7l_4nchGtD?bG?BG*CPd< ztceUBxFZQ$96@8&j?tMz(L<2o!8FL*x**&Z0voIvv=D6=$HFeg* zk3t)FUKivGVmM%Qx_Q;Y|8|_~x`pc*&Y9lqJEPh0?Q=zE^Yh9k29IZzCK7$xUuM7R zGyC7YxbWwhJwAQT_tP|;q_T|O)=c%aS<|oj>TjO<2IXD9jNc{yO}nMiFLgMNn?;C4 zM6ubW*Id0;tA9^O^TKaOBTn)szfkX~`^v2|NvoJmqU-pi11F4|IT)m#UT1BqI$tec zFWdIb(Q(c!($5SJI-Csr%WF7=qpRPfuvO7|oxz1A^OIkcEvxvW+V*teipcG| zJ>NE7mJe}`uDyC{|M3gD7m8}R4p-(&B(LE3*4~tx-o7YdMUR@W+TMltxwEo4b-{_xoJ+QEGJL*ud)l>*!!)cg`cU_a7&E-|^-l6h8PX7h3Xj{v( zHVLn_i+u~ld9u50E=vCk9lh<|Key`>~Pac(>%jfTw^ekF%EP>~KTce78XtDuY zvck$F9!cjO!+UXz53*_*8m>x+aq$-$v^gD=={cw?)X)KPEkl4I8&ENW7EtRk28W{( zJqINiRsba=nClrtLB=pFNa6uXGK2tSdKeo{nJ07@x@jwU-iz%83N$Y1GCX6@^2I%9 z$sW6U)B7oMp7SnVEqjtJJ>z!IB$bt$W@~y*{CoGp`E>>pQkI|o;mLQiEM?wtz8pUn ztC?p6+tlBrS_)PFobGlxC|7&Y>KXUK4_B=)yL$d)cwGG><=J0P_^tGk+l%tXw#@hM79{hi#(Cz6ggdfK>V5Tk$&sw> z@;7t#CUW08Iy>c8F;7s8m8kJ^%l(g({pTd!knuOzVphb)dicPrSczrri{d)dw&zK> z-CbXH=iNT%+5ZB1>dlT;8_0ZbGi>WhsMoks_C7%7=9NJIFKzdeF3fg{%D-Iy^+U$? zbyZz_snWjwwYht9@-}~KJLzk9Zi-6w-QyoFub!^5-=luoSH`y5I+dSie}(Z>E}r=0 zzxVy_MwNU}Tpe==Ix1n*CXu{nFVlgmzZe)+8|sM2JxkzeZL*YTu8&q>5X2G#T?FEd zVQT&X0Vj?y7KRuQPue&acvRN@lY0D0o$)|K`0a`7wStUo_$JP(p8IeC z3xig5>504-``@cvf6B>ld{(aev-(Lczbj>uo>s;vFiiYwCB^ev^w*+Y3-10~dQqe< zIZkc)!s(Ic0@`MIS;waehQBs-*?aFTOQ}lu|F-Jh`#!fhAhh9s4Iern>uAt32NYj7TqE!*$6O{_}U$Gd|G&`c_lMBFAvc zTYd(;3#$#EtYu*kfAv&?;Xz#%G Tgstation.Server.Client.ClientException -Exception
@@ -123,7 +122,7 @@ Public Member Functions - +
 ApiMismatchException (string message)
 Construct an ApiMismatchException with a message More...
 
 ApiMismatchException (string message, Exception innerException)
 ApiMismatchException (string message, Exception innerException)
 Construct an ApiMismatchException with a message and innerException More...
 
@@ -139,7 +138,7 @@ Additional Inherited Members - + @@ -217,7 +216,7 @@ Additional Inherited Members

Construct an ApiMismatchException with a message

Parameters
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
- Properties inherited from Tgstation.Server.Client.ClientException
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -239,7 +238,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -253,8 +252,8 @@ Additional Inherited Members

Construct an ApiMismatchException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_api_mismatch_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_api_mismatch_exception.png index 6fd4f3583909a4a56741eee20aff979373a49921..52e2f40a33e44ffdb0c80cd62bccbba8555eeea4 100644 GIT binary patch literal 1336 zcmeAS@N?(olHy`uVBq!ia0y~yU=#qdJ2;quq{K9ry&x*UC&cyt|NlVdi#K0O`wvY3 zvcUMjfdj0acjSRwj*=k1U~9&eY#jesx0S-*7M9dAzI7oDsjdJIjMPrRmG+#w(HiW~bgW8;M`#Hhr?5 z-AJ~-Ir`b_{(DCCmJ`mLvHjiS0(HkKex^w?f7TgKI1?i2k+upZ%03~;YvWq}1MW9u z52PtDDDS@S%d)~qz~Re-L?t$+4cW&SSMaa|NOCLyilr(voN{28+Q^{A%oxJPM23VN z+ljYjyKlZatgCcrn+=)w2^v)<;rc6HsnB;C_>&y4HiH=b4I*<%JY=t~*v3F9;uLDx>L!iXQb zwc&k63twII-_~1t(fwMY@dbmok7G*(J!E$-vpnT6aZ1MvpLl+?+jClt=WlIW)0}tA z`h0%_qBO>0`kOrXLt5+;jI^!5~@jw^E_qeOc_>JpJ_!ZeKU? z-g$o_c>ddolDSjtd7tcQ>uEI6+3xOE`@QeXOn>WFtx})vH@?tSPTjrwZq=48m&~`i z&lZ#S4M|>m_5F?Xb&BlU&sbdU=xChF@bg-zoZv^vZ@p{QGgu}{34UDo&DmjEu7u#n z|C{DB2?>6TJ_8KOR0Y9>MiV*$Bt2YK$T{R~zkPO_9fN#gIm7HmhMqTd=7I}=5qYt> zy;FkYz&c5G>O?Lio&1teSUM$XH9zZtZSVM*8C~G%B&tD2Rcyto_5J^kJ=ivv;h5R) zz9ds+)3c|4=dmrmxA-K(s=XD!=)GTl>5Z+PP-Kn!yGbtlHBL7%zPTaBabV-y^DCH* z;*Z30+9&n?m;njN2-`Mhh{Ii8A^3$w2YWvOnUtZTa zILFic(Yc-|g<>u(uFyvpbRKXy?!Ka#)3{}$$c7`8UV>+;{1%+glgXQWp`dE(c?OT( zh~-9IYdp9v%-ko(a5JP!?--khaN-A}{LadkyZ>(5e&m%Yi_+yeW>4%U%yR7ASYpGp zi7l_eV|m%+E$(jT?h2ep+nV(zpUKk3s5$u7F^NWrHxthClsqvM&{*4dD0hEuWIRJr zobxdUscE;B7w@ZfU*@;-`(lG@nLA1=`?5~h3G7W>B2#V}**`7p!?JG~mme+WG)Yh1 z%q|uF_Nx5MXN$73PaM=Wo_F!Zz5Qj~%zu{rP2f^rSglZdkfoxzmTy98HRGO(b{rfB n^39NQXcdFzQjy!g{xI9C-Y(lSFX0rhm}BsC^>bP0l+XkKJ-9`s literal 1324 zcmeAS@N?(olHy`uVBq!ia0y~yU=#qdJ2;quq{K9ry&x*UC&cyt|NlVdi#K0O`wvY3 zvcUMjfdj0acjSRwj*=k1Utw&%-2do30@2-v6`jDJ=+FZpTWv?Vvbv2>_}H+U4WLa4nGC%#muyu9!( z{=BTp%ja*dI;gz7bIT2S-xE1oW$&qbTb!pAZ}h4BKT~D; zR?kb_Z(>bTV*k$#YF(bJ>Z!k}{OSa*6HG6|nKd`d$TZ`fjgyK41D~!1LwEzjItIoVMy4H1ECs9_54Z#lh$=K_JCGr9{W!zS=aqd^ z+ao6>-4+Hq3E`Mhwv;G2&t;#g{;&U;_E+U)*V=uHBdQPYJg?#Dx$dx!>dKAk)=T<> z87D3|C-+ud_(qNUyXf1(^SPB{mlcYhsAZJ%-m<3sZ)DW;r?#Du} z#%-zf&olsry#h-rMrCVv$_-`nudY0h9j4e+vdCrR@=jqy|iJ*JV%GoG;F) zXRtez$FPxsQR3!XF(rq<28LzU{Q{f<4bI$rWXB?w1o73-VOi#^q^F5Y9dCatJ1_*p z(~OxybSG=qsn>t^8}8UAeBegd{lGO@OIB4@+n-6eQf(r6pj701naBUX3TLl9R+@OW z{7IhYmhMfu5(gT;Ixw7jTj9n#;j7X=uLq2k|2J>Z^WL*O+)Cd!KJT^Ohh=H!V%Kb% zv--Q$k;j+$QchJjcYV_%+N4u&tfd+xVO}2A$!pAq61>j z72P+yGHf*4UARMU;lnRp^BE@yZS+YMxjE6tA^Bew(~{L9zCC;%qKO|i<#m?6oc*_G z%aM1cEJ>GR%$^udnB>^GvDAin6Z^V`h~-t2uecvOcUQn;dz9)ob5OJ&)Lfg~A(G`H9J^n5kBv(F~o^oc~reWS3x z*|XlbpVqDjt=^OQ`6J7!%@^M`p1NAS)qUBn6|ba8hbCXp zov-u(Mt4J_IiJYD@< J);T3K0RVboNL&B_ diff --git a/class_tgstation_1_1_server_1_1_client_1_1_client_exception.html b/class_tgstation_1_1_server_1_1_client_1_1_client_exception.html index d7fed1a25d..917c27b4dc 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_client_exception.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_client_exception.html @@ -108,7 +108,6 @@ Inheritance diagram for Tgstation.Server.Client.ClientException:
-Exception Tgstation.Server.Client.ApiConflictException Tgstation.Server.Client.ApiMismatchException Tgstation.Server.Client.ConflictException @@ -134,7 +133,7 @@ Protected Member Functions  ClientException (string message)  Construct a ClientException with a message More...
  - ClientException (string message, Exception innerException) + ClientException (string message, Exception innerException)  Construct a ClientException with a message and innerException More...
  @@ -255,7 +254,7 @@ Properties

Construct a ClientException with a message

Parameters
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -280,7 +279,7 @@ Properties - Exception  + Exception  innerException  @@ -299,8 +298,8 @@ Properties

Construct a ClientException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_client_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_client_exception.png index 06d42632bf8b301388688700bfe6d9152e044166..f7880deb09a4e68f26f5aed920d895bbad97f8d5 100644 GIT binary patch literal 9509 zcmeHNc~nz(w}q{GfP#QZ z1i2wWgn$r26f39<(I6m0h=_n8gn$eQ3B&u{pdG%iU9WGw_4>zKy=#ReH+S9NaL?Ii z?|r!Vo%P`bN^6u96ciSq%@5isD11Q%uZQzyfip>QYDDn1>boNjX4BKt;Nyq>(V&FN zpTH;i>(Qe}b1t^4fv@ujcGmU^;3DK`eBq#{prBraKKQLesG^XI54lsj^iXHbNvdRe zdI){TeE3e*S2w*fOAFuFrrjyKZ3wxiTj$vC27lI_RgeW3WwLwEm}atBn@N?52RXyz z5gq5vmF6x+vwMfb)HZ6a-KTB>E{VKAKBI7TB*HXPya8M|qR!{bMBtm~V^}e-Sqq#Z zXz<4)Oi|f_N;Pz+g3+}RSr_GBZg5|B*T{I-_|zn2wK2GlDRLhZ49C)jbvjhGJDn|j z6_?KL4P1H9ZueQ5!MIE$t0|||UvnKNf=Rh92Db{&BL|{&-yO1(9Og5)ek)#^D@`w7 zXCa;RcN!aS7;j&Hb@|kivex&OIF^NUbl52T>X@ucOBA$?R2iL-VE`tTDH>WRi2L0F zeqHIH4k-gnK!i-dJRA%vLk8W5EJk!dJ!aMga_{hX`J$`K7k-UgIBbes*uWz^5R>Lrt9MGF1}^6QFHc(>)$9TmT8dopY=t4 zaYjxhgUkQ#PgSC7_ynUEFxghcdG~e0?%!V&mV8Fxzk5eT-p{E~OZAYBkK>Jpr5oS$ ztYIVx38-K6SqSFk7+9N;l zdt&Au{qu?Z(_b9QsmA&F8j`G*SSt3GX2f}Q#5W#HtC@m^6Ts-k=0+{sa|=(IQTSge zw`MMyeRFgO^)#ZX8h6o0FF$J_Z@C*1r~T?)SsizIs8;E#49)7$&P`~)i)!X&aX0Eq zNWKTn>^>wh)g#{w^g=$b&ij#cFh#Z+DM_+~9fDvM^xY;+LhV9Qy;iR>seZz(L}oDN z#+bIRjc9txUnX3FzHJ$`mEY`UBb?#{up%DP2VHA}qivQjF&0n*5dZXInulFS!w43*GtD0KD@-GM$>M;t;kNnfyViY2mqBrcT zK4?4drS?_uizWDli<~M!F8@1D)wH3F=08~+dtz0vLdRKgViBz608OUK!G;2lA&=`I z2Zm>mrU>?)0Us*dir~>;5jm&E9n!BJ86IwX=Mv}&HSTorC<&;;I?ea98QNoDIWp?e zr2@ky109EQ2CZl{#tH*eRx^gUI zq*4idq>fed!R(4NGxdGh{K%O2sa*$@7de(@e-RpAVZ9?isYbc=V3$%`N~h8Ey9af$ zd$4pFz=XG_(5#RLeXrLB!Sa}Gt2D?+HTB)NO+CHW`>x-|A`(sajkGx9?PDuu)5Ina z<2LH)V?+)P$8)x_f#|m;$MP(FcbgRoq8JQx+zL4mmr9+bPP;oiX@vz?7> z#tQVxVhl8?AM9Sa@GE4oHKdB8t}uM%Yf>d!GFOXFM}wb1s!zm4XK@{&7#LnH2UZLu zoWWWMnt@+0s8J5I=1veD!QSJmVmHixvb|rST+wff=4?sGg})a{{|b0_3)R=fo|yM! z`+Q>szeh5(6SM+`qpajLaiHR;pRLl1Epy7S=~)b*#8(%`<;v;yZ+?9RH_YF>!r z{h-mNItPfj_k|-AB+rO{X}F47)&(UmvGE;S#aI!gYOHPJ?n=DZ?gX-^dgAAo=1SFr zByy%fL<&PU4U53AU8p^lB+wgK5ll@MzN~x#(Q^A;v-0gDGRx3LG{I8B#kBmwQr|Hr z7sm(J{fDo>@Oq>ex?LTC&4{mav$MXszM?Gcx=scOtEPC#Wd zn0j5+xv&Wc#~`po+@hS~aIYG7Sp=?Ad6Fg^56?~_80@Mlva6L!VNCu0HcGN^Y7N6& zY|=c^s*hW1@5cqm;(-*dM3RmV6_(=GKKzk)_X>hN=F$kOT-YeiT#5CT<%-Uy7QlVF zOVOPxCwki=A?P0Lv<#sc{~qVR#KQ&hr;q`7S-E}wlZB5|2ZI5Jhz8I8Z=eo;CEkCf zG#%EQ6RTpuEbq@6{IWa~O#L?8*DPmLpYr!V*Y(TN)y%`oCLk#A2nxm0uGepdAd@Hh z!CRa0$BlWtZ~EbUHB|u>9(&060K=Xa!QP@MWWj>;W3~|e9dQD^tm+MQ(BH?H2MHZV zWz5@GmtW~LB)MH`YOZ!H@Sbu#q+8K$anxB+mz95R_;%Q7qLhToyIAL3u(!=kUnfU@ z+NcHKxtSwmf@G>BhRjQI4)VTY)JWK+nAL!%s@hno85*jP>I3mVZ>#r>boL~knTzt7 zLhvqxWUX|h&nNfgFF{_PDz@NsA1=HjVg*l8YX-E-;J!UbgorfouW_lu^vU%}Hv-~T znvzD6CFe;rHhh_93dy?$^)A3w2PNUR^DT%#eWbmeUJkcw>0LGUuv)t379?Z zKM{JwW!#biaqFZWFPn_h_Varx@rS(=%$3rIl+i4MP&t5WJf_CzjxfeX1UE38mY1s< z)-?32NgLV7i=}i`Rv61pK`FBfYkX}fVByUD$ii=sm7OVY$2Ai_0M?2V`#G5^H(}WK z0{P`0RxY`@;uthqe9-R0P4{9M3d3)VO&^dlm`rB z_WFh~yWK@!2^jyQRw3moYWxA30?qw@7Gn}#LIsZ8W|UzeCU zeW?SvSV?Zh(R{ImhzNABlb{}K;O7NYe9iY^Gz%-@BVlgBiVzKc`fTMrC9RVH*bV&7 zjKcA1Iq(=D)X^t20b4)96>%Wen4qaLMa3nS=GilcvU-h2YhC*2`|dFtX4sI@t`zO1 zyO?E>mIHKIcU@e_PHb5RaoKLu#L=f9<+H95PT(LYv^y8smb>KP*6*Mdpn#ZRl15tJ z!7xZARy~H14=X`XKC%b@WXbNcH~yHryUwi|LLG@vz4_FHnH=`g@j*N>AK<)90pMlT z1CsBHGh&yWQ{=lI{TRtN{6^*$b=TUxRVIbWbWmUt#t)bS*m{#Z5-8S%_>4!prfKv( z<3a2zyoi?cVQasWcH3vP_YfZ5Ii&we;{SYm8rJ14^agB2uwhAw4doj#iQ4gFCe- zqY9QD0#QIDS#yqrmI%^NOx2KYK~PIYIXrEvtxC0P;KxiH_+KpCwOGrd!+o(|QoR8Jpg`ik-W zlff)5cI7O?O(L{3@}x>c371FG=G}L$qk^fa$4slHk)rS~UJvNT58=9ewIgox=sE7} zB2s;9)%D<%HWz2Dc+2aHTn=Yn^!2ZEqG^mf`KBnZ^}{JKi^llcc-pE4A7nx2A6!Gy zqS1J#s1<@x?@9#-`OhW`HUedE<)GcsK{Zf9+#I3N4&)2ZODtyyUf@CRQ~#v!7Nh@6 z1U}k6|G=u)6K@nlXLVTH%^MUg{ySysUlYpzJ8^sG&k{rhcfAg!26kDS0k?Jyv{K*< z#6Ku^LpK|jlE|=kl~|If6j$KfN&V5eup{h1Myf0&76fH%riJuaazzdIu7%uyu?wZR z8YpiLDg$8h?U3KXWx{=5$r+J4{xxG4_pqu-ncn9dxI?-!8NQ-eo8RYpBYR4$J}Hq%LbpU|%1gqr z%QWIlNdW5BSQy2~=HbN7bM%vzOS^i1$MSs{ylW#DEsUb~R3+F$KwW&esAeIh7uqve zBW?Ly)^EKU?{JA#l0ozP2Es>T(~kb8M>bHy(29?d*zm`xjV?=l^1*zOKz$^TbNq0> zU<0Ac-EFSenT z6sOkxedgd_X82*o=<~m7kSHKnfoymGi|S>)#UGRVpyM73AP8q&GG}_n1g;F#%Ow5) zz9ajbsl^XZ#DZa{lJ(I6A=QB1+j2Y`nA=5xQszmyXJgI5`AfC-@)~=`M0(+xF(nTO zZEd)_QY8kaCioLE613Mbhgzu-C6eb-+n=4E2~*oRZIZCpPgL{ZR)pg2)!>U`q4d-& zYyEXrk3Ni8<-NLf=x5E7n*6=Zc3GPJoB7@v!tZrYP@Wlt>kt3HnDiKp2kL`uVT2cu z>zl@kQ!`-iZ)L;h$c3>J$DXwkn&ILlds0TC>R*lQ@GHDU9LV>*4|}6QDutSH4lylR z(M9L8zO=tGRC$=xu~b*fD^TddNji-~JMQUCBIxk-Bq+^K(w0PC)HYeZR(p+iSL2cT zc#E>U_8J1kqIL5_d$j2FGvN)-$GPOelrRkESa-y?oEtn~SO$A%aoG*7Dm%4v!HU>L zGCros? zw>73TnKEOnUW=L9J%BwqN)qMz!07FVrxV+vku_OxP%E#(@bspu%cEXIgA5>^fFR)G zOEjms!S0CnF#z=LKLiLcgkE(M*C`m@lbxB#Q|V0=z;H$<@YHc@Lv^@KPMIb$N$@AD zC4)v5;M0J0glOC6lk0(NneSnfEz3FwD39c-T-} zbV7X1@U3xD>{G^8+O6^!HqtIl$7j1+ zg^0lINi|Hlujod6GR2_I= z9vQ~~&zkj6m+$E{t`p*JP=^>dcLy=izGcfm@kjVQ5qCHqcYuQPu`;6sm4Q84z=X6rCh8D~Dsei$Px_$ua;!yP&Ra+|xV)%K8=(1|QflkX&2jlTO48L0D z_~!QLd7$NK)1EYf3&{?S_KJ5m4wYM$R%7AWbZ>*#MQ^j`8XDBC8OD_HNj)9~BuszJ zgEo6!$c{7_r!^;~@VV)ie%ugwW2W2edQfM2h{Hk*1RckC~bGrM~k?XIxx@C>(E6V&zr*YZ1bzufVDXtcFp zBq||di(gfBOO=vI1`f1_ZuSyGZ6HXDxTLz~QYI0~>6y{9Mka2=K*kdPOGhn?*z^GJ z9pj;|e9DhK!@REg*COA+7Nd3Y!ayKl?L2Y$zqY21g=A>)Pkt26X|HT*h=Q7Nk?GxK zvUha@xp3b?!~zWh4Jw_xn+YVV{6KV(><;HcLMMH!Z?p+ zWg03+Ba&wwm_um>a|ivdg>U!Fw5trq8z+&T^-A@~vO5kCQ}gQb@UA3_aC{sDO+%m* zR9zNUAk9j={67Q!vFy)Y{w?~qhn@@qeGr4@H&mDAbE1TtVSHYoL9zc~2#P)_K!cuD zJ8>YEa+ERYRH{?PykN* zGJ>ckzbgRUse65C>xQ?Prar_qx@5ZMkzFSyFg+f_^t>*S=@lT(wxxX4*!LiHgt8ft_AXMSRUa5*LF3 z%=^8tbo8}*V^C6O)b}l?N}2^7>4E!SJcPO;c9Mb@w z{^x1*hz;EI092;e2EN`f(Q>9&>tiXs&t)su-ZAS?cYOxhR)&K27>&s9UiFY@iN#GO z4+BasP=o6ASak9rChwD-gO?ztKMIhg(QS@~A*VO+cZt&ZBI>Cnp>prPdS}WAPBnE( z%}J qq)JAC?~I!RGUb2b=A4^WbX!;&aNK-t3izKU1+96-jjS5XcqSU`{=L`6Ylk|~TKMJv=|K@Wy` zs2V^;5(XiJFgYMaNK%6k5W);GggJ~!fVWT7j<@%At+!U+TKAr{ScC(dea_zB|NYwJNBq+oKQ`?-nx@gU zgwAS}eE9YHwvTq*)oZUUqTCj4Og5dxH|;-uJ~cSKo$YE$sLPtC!9HI_x& zS+~Y~fL@Ty?M|tC5GFe)lNd!VNe*nbfOU{d?X=^%T467S%pXLFU3C*thfY9eRz|~9 zqA^iJ5fyW*azYP^{vO=K^;1w1ysb8$G9JH|!0ghvM_#|EzT(!DsIOpD9CI$x)_U$J z7}M1g3rXh1`@?R{i26`t9uMIsq|BSB<8b(Olq>A@CAhkV2V4Fq2DLEctADrmM_b*di|LIM0#pOo^`tBzFJ?e}5loO|IBlR6EZr3E+ zT7R}@&&lPVFZDC8tP}^@- z#K0_zcP&-EHt!dlJ#LIFwyIHUyM+q;WDUO0iX9rqSH#9Is106Aak*dz)_u+uB5im; zvnD)rDkjyvbWLH}7Kbi3S~OXXiAlUIZmM$@OwKWro91I*Zlr%146b<_T(iQAn-W$RxpYCqnQ;zC*N#95(G$xco(@kO z0O{S)P`~~n4@GPpO*-k-9HR|W?H+xJ=8>gz9r{RQ-uHIapC+rAmETM6x)Inj>&?un z>-3zhj&)#{8xT(exP1y^$x{I|+Q?$7KbBbbXq+6Gn;B`aFDXzbFfOHGA17#(SAA`M zXt*wW%Tc!gl9qqzVp`JQs*vIcsl7tvutFCeXoC67GHJ!;8IRNNQO1>J6IYB&PK0P{ zs8=i}7L0F_>^`$na=Pchl*5y?Q;(l`zA65C>zm)KXZNLpTVC{BI#{9b*|_%P2g?<& zn*q?>ot*d*An|(yb{f@N^0QBzKBl3*{?lX)#pTi2@ZY1(Ab356oJs^B{!31U8bY}q zsW0{)Z&Lb7uX_PhC5$2+`vsAFjy?|a5C-^Njpm|_w=BVpaUu@B%TD0NL)Y(1KTG%5 za&uhcYc=8Nb@Pq5?zt8!MKySAClra0)0`!`l3!^!)N$$-s#>sxxgm0+)LOfKx;3-+ zoHTe^T6mkhex^qXZ_|7wsB=G`x?mW$ZJ_K`UY>K^ZB9Wkw)i$VMvp;VKE9rqde%Xc zr_%UIhW$a(MoTdb(_Rsen%@d2vAZ3P969Hoyj8znj$4jW`~0ivz8z1@x#y{jsbZr5 z$XB~nLNI)MB~9WYrUAyo-kd7$vac+iAyS3g>%X1ntb{S50q?lY5Kpz(G;)EJ^eCvU zQfdarAl#H?ZeZiv;xOhl+7|B_C%zhwszkY%88F{S;m!&@-t{Gzq6&_&XYptR$w3!! zt2i*VzvCW7^rA9(UI2FdMMl+v3*ofIQx3xB-ixE3?*H=5b2$C z?Am(SJ<6UHU5QfxdI+QwvB5I#6MIRaPtwDEy@)O@V` zha*fY001SxDSy1~=BX$eQp?Ah_LDtQb5s<3YuX{j<%?VYQsDl%Qu#l?+a2{s@Lruo z>cTy4X?D^}I2UR3A_*0Yv`s5JZo4t_F595Do@G+!a~1!5t*B`iZ<|K2rUz^^xJ%x4 zeIZFhG|hk3qp7#cZ|any_6z3$gSEuyj0Xb|7Y2HcQ$5=ZO|x?541#V>RZv}j)bU}k z+k#?wy^rAL>uJHaQ0tJPx^7NUYGprc=ea5ud={vpBS>;CDJKV}cw!cIa{!l421Y<7 zE__0H+H&vC&L&nhSgi;ft_Xvle*JockrRW`9K@=c8DweepL?80iCw)JYnd&!-C-Xz z-RVg$yNhBRMXEK5vTr($HT5zvGrw%`Ab!q_Yteg^QXHFD8`w5V!zPskkbTsJVb7Ny zy#u_JoVuGmaI3Uyebog=(3OQ3O~j>?`ZJ+M=E32kElYORb5d|8rUSBQic+<2`-0Jo z3$yxSSzN>R_B!7toJ)$hZhML<5E2I9Ap_RYyr%)~XXk%|dra*eTGy)=rf*~qf|%G^ zDwoI#B96Fu8s&DD-$jKY{1sx&CFetvyjH(eq{MP$0*-P8p62lI5%g7TcQp>O%GZGw zcYu+;T8faPo{kjvdvO1uz)H^X&;XK5TcPtSdct}|nc-h3BL1vwnIyoLKNqJIN=2ce z#pGY=t3P~9?2V~QGK;;2R4j?;l#zx@LUAsA%YPBHv6VgR>E+ifi2rU{4N$sD28v@1 zxk* zSe0g9(>G?*?SEmT0hZVpM7>r_KT9M;Hizm5h^-RxG40NQu+eb@fys&vu;iG zfg>X%in6YKp(rpvbuXX`#vMrp(r-{AI z!}10O@(+@>!1F-X?pget%UM%ZDs^>Qywxp-aG2yhb^A0L@fZ+o(z`6orPbaYOC3r+ z?bn7La(f9)0wx`<;h=DBsVLtUn!bqEmPJvpS_h%MtI6wE2&^=O_kGps+ra`{S}fr2 zg6>laRDbILen;SYjyKunc%-^0RD9sOOMU$uV6DGeZTw5p9vDGo{9ItO3Hu>)Sf=yO zjLuJw$%ePZk)XFyIWM4C1ov-5w6f*0S!<%pY;0{9kSesVR(dkR-63jS%OSJ9*in;f z=8DWcc@#QlF>1YdVwaXNAZ~2p6omkH^rg=V_0%Arklvw{+mWt$glqHIpoKdB7R4nD zueSRNfVs)3YqSmta^o570g>pHu*f2{Zy0~3xWVTWXB6(?Kt!AV!0w4&H_THpPZ%*o zxRbUEKtr_!-X^`cma$DPQZV0z*oiP2`ORfF15-qs0^DbsdHH`6n-hr~OJDwWhOw@I ziR-;`RcjNXqFQfEl{@VVDos!E2s0}>yC{yLi06G)l%gwF8J(BOEc{Ou?Y(4Iq&LXy zKh@kEP80P)2(uZ&nAG0Be8xxp9TS2>n_-uaeaq>WNKV|K%btGt9}Ju%cWAyFWTH(1 z*yq?(Cmb0O5mE8U>{~VN*1QX-aYVzcQ|vD_6V;S_tM`e?XLt4VirYr%iWfE2WvXpA z0>9L+-Yz`Ll=J$10*P9t2X{gRa6lek6`n5)y`o2ZJ>PeuG2UEpNu>pj9IY*pZeU*4BloiS-Jj}Sr_90*E}~Pm^0KO{ z^4Tn^1?+yX)@Q(AO?n+^-^BS#AI(lVCVr7suRCg7FDsKOfJuE`4*R^gG~3}NgDg0O zPCf3B)u{1y;l;s#j(wgCz&hK+X*U3;7HA1+e5_qI?ZbW2;0Gu1Q7W;R@ZblBv(DsR z)K_(kW?QKSWQba2uDTQIFX#J`&*-p1qmu4~ISfdKY$|bRrz%siCRM2;LIIcnR~xR) z%~Vx+btpUHuBYJrH@?0BisRz||I<1XEb~E_jaBy4$Cs8#T#E9^#vz;Zn(1JHI})Ze zSSM-uBWa%Riz7CgsU^(+uKo<6Q8~hJ>G?(7!$yfb(X`Xe*=?VruV#dxgkH>e{=~~| z8T9%0%^+#btk;`q*)pNe$1=6Z`va_63y8OUX%&PuF)``976Y4j%{KTK`=4w1R>j}P zmK!wSVkWsBsJCI(1ERZCgGQEF?TeMbM% zCWd9;wY2;JTi9xVP+24P#vO{!UyvlSsRwi0!od6y&T9!@w4g%W6LE%6QbUiTo}``iWiNyT5<)CQ_v??3qhCM{p)B zN=gm%m>4LtXFZcF+F#)K^z7n5jXtZTD4{W)>1hS$%9S{cLN^TE3BkMPHj%8O0*3A8 zoS)=7j8$y6VlBkTC4ZK=ByTs7%ycEo{j5b#HUamygXCgPflW#Jx_7Okc6^7>e~9r6 zFJ#+i2H^2uHKpfuq=8Wc$SBxbQ=aw-Plx4-{}Ms6qor*mcYEq;} z=_#%^cctEB!XO@zWf)=5m|$Ae67`eXFTXV|9&>5Eh|cYcsDIGiNqnpfuD=VpK9DlU zDqWk1k6yalQ}x*@Ch!y-auVV!NYuQXxM{%y2GiBMgmvIBoOp};)RLic70CWA@~fIg z>7J5GLiNcLPS6=6F!nEn5uNXAE;)?GRc#T(AKHrr5zn)A=!K3pPbPMzox`vGWlFPO z)T458kU=%USw*tK)-v~xAM!UgIaI}V#uv0*j9XR8%S2fL%&x4`mgC#kg#f$)p*sd% zyvIe=e+43-lq@^0&C_FWFh~!7kbMEDAE+e!d-(noF#c8mpZ*1*T3UZLK6c_S4ha8* zwr^;M41U&dW4k=yTc~;0KjqbpZvA6fzp3@qA-Kc8Z1)_!C#z|}WQ@Q080>_!(lHI; zh?*A2gEm(^&+6bK$adhmHxXSs1yj;Ec~GMdS=9=Q$0JT=H2_UixZdRj_q?wad1&9T zt(M?W!;kYqS86~8vn;rZpxK39ytv@-%;JZE)CR6#qTybQUNUMk(z9|=3$F|M_Za&B zFrBn?`O_iCE3NmVUIWQP%B4av~xbT`}xJpMJn@ znPDF=MmZpvBiHoM;>jNdg_bA1clxUf7t-CEIHB%J<$^`l8nyaVME7rn+=sSG7w#Gx zd0t}qRgK@IBt6SBH((Ofh2LGE!=3%kcHC@=sC$X>&eGE5`e)!m^9gqst#=uYWv~q- zST$2Q zGIsF0c)d;nTsGk1^1~L76%3rk*sD58r?t{e ziTw79$p2sMg35~yKr2p<)yj605U*>_?NYgkn#Tg=lPf!_DZH9jf80{;G7Eb&cet3A5JYn~>2qOiTOIp%=r}41~Q)ifX<$I>gOotLU;4VznoGlh+CQP|y)&>HrtJszJ9^Cm) zO82-I^RX$T>QK_hr!SgFbZLiO2Hc@Y;ijZylwGU#Z3ZFZ6D5X z*^!56++dgnfA7SIzaF+UC)K?yX#p6)X(CXjNk?Iq*)f1^6LzJ+J1WoCUf0C(%CabT zdBZA7${pkq7g>S|hlMLrDy3jobwc3FjU>8*V4>=LFqusWZo(ZXcVRqi6=X)NO{Gh- zgNs+ks<@^sRI9zyE>4hYsq{SGA&8xFQb}J=Ls;&=1PbEK5w7RUO7&`|TECJa)-c%5 zoCTiXml_5b8%w%h{qsA63AA$Q^Nuv%Ckrtrpq`~?I0zdocs&}QKM}A}TPq^F>wbME zyD{H!HOq1Q;YyB|NNzH@FAEk1m{lvl7}VI#xO5E`uGhnvGpgfs*|)Wpd}%{E8!xyW?au?y3OpoDX* zfScGokKiXJASA{Q2=Y3%=Di$#8_*vZDteh)G`*4VAW?k#{2SSn4v+B$XKaEC$5i5X z;_Kb}1a0gMk_aC)A-ZR8*HbW*6dB6bOi~gKY+e5ik&eNV#v4Ds*sR=Vew^jX2wq;{YseXI%*c?GeBQu5Bu~>Ip&fHa9GMd=f;dgI;0I+ zHllI$j4vS`Ib2IEGw~y&ru_xgh5=>@ezxFMet4l$Wgim zH-q%1yM(XWJ}JQ99p}ow*&1UhmoS($p*hctc-cxcw}WiMB`k9$Y{H`?*MaStt7M6# zSxCw=+A=yTEFpi80vV2{Q#73t`m5_z`FJGojiy-H19;m2=6@axJgN?0ezLjZ-3gYQ z`E?Pu@}C}b#{G)<1)u1tpzC$t13y>n{V+hM)bA)XX;_+h7@qeV^F!chfvzMDD*>jD z9y=8jS9$l9s#z+^ll@8Mk$IH3&joBxZ#@M`@}eVvQANKusy{jtM7H53Pc57MO&+bz z$TAr;u&3tL6?<|*?Np1)Y?>>KnU3`l6Aexdaauh8&7@iRSlJZ3(~X~7fXW8859e@& zyakkh)aO#(7S>|lm07tJ=j^}aqSrG2KZ{{V+7H6d!jV|MqfoEur~Llx!$*#}OvnTq zY&h*6R9Sxs`?$BLK@{ZM$*MzM9MMH*+z%d}okdqp^t_KXmn^xS-8<{p#-YWDGz!yZ zG-+Qx3g;f_`!2ijmUn?Dhms#+DVj(ZaLed;Kv}2p+M3%=2@q{_X1F|SF;GeAaZ|5vjB7w)xEMj`;aC?6L+R8b~@Tc$=`Ho&B_K6Xpe}5CyoZ>VYDLh zIeu>f-7vzlZ@AaprR8s`c#qRG%2Rt}g?2L?g-P!2HW}@+Mw~nRthzXm-LNseoOOdU z$o$2`Qp7pQsJr6g^NVe)g~1^tT)P$c^818y$0Dm5%@xO*U;Dckgx<@dZqoSaec$eo@;ksgN(zvvZ>S4 diff --git a/class_tgstation_1_1_server_1_1_client_1_1_conflict_exception.html b/class_tgstation_1_1_server_1_1_client_1_1_conflict_exception.html index bcd9122694..18582f1d53 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_conflict_exception.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_conflict_exception.html @@ -108,7 +108,6 @@ Inheritance diagram for Tgstation.Server.Client.ConflictException:
Tgstation.Server.Client.ClientException -Exception
@@ -123,7 +122,7 @@ Public Member Functions - +
 ConflictException (string message)
 Construct an ConflictException with a message More...
 
 ConflictException (string message, Exception innerException)
 ConflictException (string message, Exception innerException)
 Construct an ConflictException with a message and innerException More...
 
@@ -139,7 +138,7 @@ Additional Inherited Members - + @@ -228,7 +227,7 @@ Additional Inherited Members

Construct an ConflictException with a message

Parameters
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
- Properties inherited from Tgstation.Server.Client.ClientException
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -250,7 +249,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -264,8 +263,8 @@ Additional Inherited Members

Construct an ConflictException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_conflict_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_conflict_exception.png index 08f379e3fe3c6f842ba98ea40a75bcde6d6d235d..116300934739e3fb37154422c33fab7c9f56b360 100644 GIT binary patch delta 1021 zcmey$(ZD$&rk>x^#WAFU@$KBV(>`l)xNR?4TlBxY#>!O6 zw13HzODRe#SNSbX2n+qX9!zXEe(tw&)vndYFZZkc`+H{9s^h;(mezLtto(WN!k*_p zC&^v^^Sd(rSxD&pW&Ll9^&a?y>U&=0GhV*xmtO5V8~xo8!Z8<95+?J7UM+uF{CsEP zxyldc_B8cwyRG{9(qX+h>HKGQ_VNGvcHaK>_WkoCLreYZx0D#HUR4)hY_xjSzs9D> z(5w5wgxnd;r6$uE{<)PmoHSAV{MSzYOUoJl$!D3Y>UZ7E+kWzhuS<#+>*kJ*dN5&n zM)QAXhve(GkHf#8*`uyh?jHLLWie@<_%6wX@r_TQ7=M>ovP@_qaG&FY+cGtYk7la>~ln6S0; zOT*8fvpkHP>nE)_EHOo7YI*i5pEIjn&tLwvDZ=~w#3ya5y=`)6 zo%Ai^gzEIm>~BIgZrm6VzxAffZJp;o7v%RV-%Z{3qf=P$r~Rv-E6&e+(_CEkIjuLI zUL`2_Q|~V`Fx=u1foD4-fBWrRJ;p!1?-(X$Iz9VcYxjb4ru>P^j^|Iv)Ji9?u^Knj z!*M+_2P^x)@hL%GlRH@qrZ!0)*bl;7#V<1sSY5k)e0^cwHhspPtvC8=u6AzRe<$m9 z0ozBWvNx=`-ztCaiDXGw{QQsWizI6!{o>?$?`7vQ8H~R*znd|A{dIq?`IT$_f69rg z-=}utoqu7tl1cE|m9=wfr^*_8zMFq*`x*6ZYDJN2s&my}O#7M1eUeA$wE>%|Ynl3~ z#b1M_znT#p&pi9x%NN(L-DYfzO+CD)Dn?nda@!To7b*GtGiH0;HTW(r9B6sI^zX&& z!wbr1@OG|Tqxf0s;H$-ZDvrd+?mw~TkUPUyenarlV{8ev zSNSggl3pHeba{Z7dRcx@A>(Mxn_3V$(Syd2iaev(-@v}r7`>m jVJ-RZQ-Wr>{TF3));K(cbP0l+XkK>wgcI delta 1019 zcmZqR{K_#Qrk=&q#WAFU@$KA))0#DS-1O%^|MFM9(mK>YA!+&Uw>i$wKJ-Q$u;*z`xAe5{K-lTdyX#-SKR$QnZfV}ISL zTsPyoO-1^>gH^ZRPO5yJ=szc&|4gLkC{^Hq$$-TuA)+007+N%JnI-ToY^cFb(Ag6hpesm${E z{mVo*tH^$tQ7>e5``ek%cUsR1Yxny8I`iGcPH1o1)j!ec*L^j1Zu`CYUVhuxklZZ! z_4__Oi@jN&bKW~`o2XS-y9fK3{_9Ip+9rKVquo?*UfLampNwVWt?w#J2zB*L-5UG$C7HNrWtR$dG7Oq{O`)Psdb-()jj{a z+lrP>|2aX@^B33Nb8}w1PTJ@F9~4|#uyA{QZja7sqv;I)+{_zJnkasLYbO7tz3-IE z{rk7FUKdz>WGb7(n)~hNk3CT=4;CE%Gx@^JZRzu#|N6JhC-yQ!+8p+~2Kn*p=Zn~V ziukv7^XpHl2g~X=?Uq?MbCvJg$5+Ge8BTukZDsB0nj~+f?MG)__iI{LKU03&gfj{o z`I9FEr<5z%-_lQA*|ziB)s30+zSYU@<>h#z#s2K>V>!?Bo0s}IC|6r2tke2>;7**6 z%TJr>U#r)bHrpkdsVv*H>r6Gv?*2cII(AyrP5e3GbM0C2neAI+)wiwOa_COB|JKLb z*2N{ho7;QeIDYZSIT{PTX(_q^V4$P2ihf!7mVcX{xjTP7H{-iiSb9&-)WKu nb~Z^K*bl;=j@vGsvh1S$ZJ(qMvp3%jWdH(CS3j3^P6 Tgstation.Server.Client.ClientException -Exception
@@ -120,7 +119,7 @@ Public Member Functions - +
 InsufficientPermissionsException (string message)
 Construct an InsufficientPermissionsException with a message More...
 
 InsufficientPermissionsException (string message, Exception innerException)
 InsufficientPermissionsException (string message, Exception innerException)
 Construct an InsufficientPermissionsException with a message and innerException More...
 
@@ -136,7 +135,7 @@ Additional Inherited Members - + @@ -189,7 +188,7 @@ Additional Inherited Members

Construct an InsufficientPermissionsException with a message

Parameters
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
- Properties inherited from Tgstation.Server.Client.ClientException
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -211,7 +210,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -225,8 +224,8 @@ Additional Inherited Members

Construct an InsufficientPermissionsException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_insufficient_permissions_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_insufficient_permissions_exception.png index 090a78a265d9ed82828f354d936fb87340df927c..31a40a82617b47a77ffa9ce98bbc48c85e06676c 100644 GIT binary patch literal 1486 zcmb_cYcShc9Q`L)rS)jEwS7p_H0)}%sHj~_QHg}LRA?)GpwdR|1GOblE3_y9 zy5TY*AQ%8e?}p@Y$RPkQIO~h?3ZX)jWm7SWuA2;SXmf#X=LmZEwspV?y*=S-Ftr3z z)tKiyWzQ)@`q^GE&EfOoDCJ&Ag~Eb?r)cz1PZjPE`ejPss1H)w*{{$Z=;u>XW+|{K z6|MJ1?IaBuzT2awnLN1?#4dlMk#YwnR5ZhwV&)Ipx;yy=ui)e!P_x?)E zS14lJtu!LP@qH)3TiPn^l|hkymHbufrE`lyxGhtqa(@X=y7awnEw{P}^N6p@Sq&uo z?`J(5bd|gpR2ktb`N>ew2RjW^eKEI%=x2MYkWd9Ya5n=W4p|tec#$eXwWDE14*kz6 z{Gnnb6R^ea^@PHV7-<+N3c&=7@TgBpQ;&}G>~cekF0j&tV*g@RFM>%XgGO0IC@h*> zl!hr`SyA2-&u{1kYHL{yYtX(}B%-bPfrZ_* z+8+As{ffC(rMK8}FDF!N{`QwKZei$1>T+{a-BkQ}thjPXvn|v`a&uliBxRG!Bb!#U zR6k{Sg}4MeHHje56ZA}~wX>OQ#6s!( z!YX&avv<#|~r1C*9-icvlQlqPt}jj@GsczLe{} zptIN>sEpfmIO?1n`s#0@^4|UE0{X_h6sziv1aT8CtaRufRtZb{s}b`?kW)h&X5nrW zHlW4ZsX@p5lP>QOif3Xkxqxqb6xk~#Fz`+dV55rdAau0Tx1+Z4#3HO+56>SGmkbA> z5vFwg;>mf2Z(1}87QwE@vGj|vNC<)9lctY?As}>pyeAR@W8gn2UC$5^{5Eq64;dPG z0^*Natv**?03U7XEANjbjcL6BQj;tw{B$S3j>fL-69yTk8{`$mI}hNY=3I(R$IQyA zrA2`}>d|tuf*3v-uZ&S<iAmRe>m8bfeT zGE2WMdKu7WLDVF4wQO&F>pG1fudz0sCU1Go9iwWu*xPKY9;Uk`Z|d23Z~tvOPw%y) zJ-mIIrgL_@jq)pQn$ZdRMcc~th?a^18;=oANZyW|yT+rZ&NpYbJ|aEy9Dg!D5V%C^ zLcTQZL)K(muQ}=Lwy(54*Z`Y;RbZJ!979*gfkUb!0Qwcz?8GUi(hG`M t_ZnyS0g*naG+^24qqqoUeu&B(9q>ii?KlD>leK~7))Sjg$o*yrNrF(N{&6MNMKmGlwy~XpA?2be? zy|24h+_LWyS${#z)BHhS^^;loyYISXKaZG{cR=?03AZOo*MTND|GFfny3EzG>nRH@7`pUPfU*>At{JJ5xM`&kxjp8R6;j&2zMFOSr6Q?h|n|65Rv$Bbc zixot^q6 zJ)&aG`vmcuXHu(kUK^F%(MikiTVJ#LD!+-r#jcpFjzfik7QmDt9(!VD?at>f%av~L z>zg6Ic>caVs}9NfCx>Q9vi{1ph_gT1-Fb9nxl+H};ZJU_xwN-EpxRrq-SOpm?WApY`(Zd$~x#Yg^x zJo#-~q_b&eQ2Z^!Z;LP8Gx~m3&Qj#(qAlD Tgstation.Server.Client.ClientException -Exception @@ -120,7 +119,7 @@ Public Member Functions - +
 MethodNotSupportedException (string message)
 Construct an MethodNotSupportedException with a message More...
 
 MethodNotSupportedException (string message, Exception innerException)
 MethodNotSupportedException (string message, Exception innerException)
 Construct an MethodNotSupportedException with a message and innerException More...
 
@@ -136,7 +135,7 @@ Additional Inherited Members - + @@ -189,7 +188,7 @@ Additional Inherited Members

Construct an MethodNotSupportedException with a message

Parameters
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
- Properties inherited from Tgstation.Server.Client.ClientException
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -211,7 +210,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -225,8 +224,8 @@ Additional Inherited Members

Construct an MethodNotSupportedException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_method_not_supported_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_method_not_supported_exception.png index 127df8755a6ef5a06b00520d4a93a4dbad475b66..af69c2a3bf3551b50bcf59bd7b923b7baa9fc976 100644 GIT binary patch literal 1462 zcmcIkX;4#F6n=yNVnr-vaU;;8v<$Q=VH2VOC9()e5kX|DGA4sSK@bB4^AJR#vMH1j zs77S~6(Q1y0TT_?V5o#iRfGtHrx3uJkeEaQS$OHQGyd>*r+4Pw``vTCnRDlyZ@%)# zh#-rV>sJB*U=b1=7zF@Ecm$WNFhB$B;A{o)8IM(xImwbx4)Z=QYO<8*1@nsPm}7Z#xJY`c<(AgPtWp z&wTuj1_jDwOac;C4bWHx7)=gY;Dm(oF>t60p)M-aw8&3o> zM!RW^W4YdlT_QCSU%(gPlx8(SFrY+87>)6O6A0vsEgh@%|0UUbpOf0?ye<5BUGbm% zumn$TcX>RzEb%&tc(Qc!$>EVhSr}~SGyYU= z!wu?7LbfUs%9s}L(|HX2kq4e9t&;|mxCw9m&dATy7Oaxzp!m)9@KB3Nd~nYHH*b~7 zN@&}CRk1J;b19mBq<*n|rsZ6~@OS&MjCS^GIkh;>{8yXt&NY#_qxag?@0Q~O<*gZ# zBVlXrCI&salhdx*0mfe&kFB(-GCfbtfqvE@{MRm z58%_&nqzP(rdQzJdZoZn)Lz8bwQA@}k7I>+A0rpmp zUWe$=+JZZj*eMG2&;OR_>O-~cO5v0^F%f)(o(lMgi zEF1Q{KqNw3PQ$_m_@Wb$s6H%vP46_vGUQXvNIzluhlXM_SnQw^NF?Tq1$;NbBgiAX zwxKok36Mpi0BHY}8slJ<%IP!^hfH^9Hw}&2gbbftv*Ujezz1D17h^+RuGK|FMIz-< zpGxZn{5BPpKB`_?@>H|w=&i(Z`|fV*VWpity)=AcCSmLf^w<u)l9OIw>7U-{aMy6MIS-R9S)nfZ(et#E7p(#D_lO0vG|iuOomQ^Icj zZfl|M&gj^-_or|kE9@F4FD?7Vi^j2_%1U>?uBW7l&VEY4x`7JUHo$mlx|~Basa&2X z1enJL=RpR?rqj*$JkOo6)Jo7Bmp-HNpDzoc`aeeYbmYxuX7nTm9GFzYTwdpCJ4HuV z;><|0gYs*zO~UlsI{too(gU}jWm6!Q|6DD?-Irz#>vl4n#Wy*lI!Kt@OyN*cqzbJ) zy{Y8eG;e%knF1yY58{_dI;e}22cNBN^P^1p%Bz^dXED(!jiLu}`*ieMF7M~^ecYRR zY6f3wSf@s8ed#Z1Rb;m-3wDdxqAhKBwUHJ3I;4{bOpD}VEc{k%vh?Flv86wduy`9-A6yMHC{^ma-&F6J!&J;!+R^ z7{DSZv z$=?;a-Nwqn3IG5baK{!Z0HDZb&a*_D_d4;4G4t`|uHf*kCX>mGlbYE>MIFUv`VeBV z7?VHZXueveP(#B2bJD`-YC1#2u4}xrM931v zq{#Cea^s(d%&R>Fii8B-2F!Qd)6OmpWbk1un_DVHoZX6yAhoJe?_N&^H;&KiNE9yO zoAwqDsz!E=LWXt*8B4Dy=;M*JecFLA*OD3!)~$Ks+TI5}Om}MA=5x;3zqLZ_8BJtO zs&`s~%T((K-J~{zghD&HvY~qA=)ok+))73*p6Y7 zY@LdXsBu2|2uZiNSc(ACgN~GyF#3Kh8_3Z7mq$5d?2o*5RiIcJ4tjD+#s9QyC0tTN z(&51jJLKUzyxV^Di zwXLi@|IeO2>2$!mxfP=W7UkRg43&YGAMm@5`Xtw>d>cGoefvUvmFInRRMMrzJ!07& zyAoB$O)0ZAv{Q=%o5LHUSN%T4ID~_q!vlG|%GaAH(GDb^_Ky^455AA~(6CJ-G!nb> zsL;3nuZ=}<2bz-aU9`uF+=YUqcPhdMwK-m8H6kZf@@gtK2WyH^IO>Bq<<8mX4(k&W zH*<4vM6=mUX_}qf>5ooW&|fuGuZRrc34Q{ay=(z=aR&nQl2+PV7E2ajcCQn~4o2%O zp8gLC|GRNmhuO8I2Ls()Fc>l5-0+F9swV0$d@au870GF_aySr(ofzD`kG092-S?I{ zD2bBQ+P;rZdIM3~I2Om}iCr^qqFhC;4H8aArO3NnYouo~e%Zqug3&&z2=P6o7e9Ut zt(sI+nDEGptx)7ciN;dQ42a|3k%VDWb6c zXcMDV!n9%zACPL(mai>@zL`UWtJp8KDMrQ@T>-{sqK*7X_bLDBG>Mc^6|Ez_%C6pv zWA-ayW3r|`OhgkAbm>OUK=+-qf=b7zyXhvsq$EFm9IWJv(vg!TaV*2#L$`fL4+bUf zE|^M%8UB#yW^IGNyz|VE)KhM?rPgd9fSHT!eB3QF&r|^18oK4i<^#EZ1Ki^B AnE(I) diff --git a/class_tgstation_1_1_server_1_1_client_1_1_rate_limit_exception.html b/class_tgstation_1_1_server_1_1_client_1_1_rate_limit_exception.html index 44d1327d32..ef29bc157e 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_rate_limit_exception.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_rate_limit_exception.html @@ -109,7 +109,6 @@ Inheritance diagram for Tgstation.Server.Client.RateLimitException: Tgstation.Server.Client.ClientException -Exception @@ -121,7 +120,7 @@ Public Member Functions - +
 RateLimitException (string secondsString)
 Construct an RateLimitException with a secondsString More...
 
 RateLimitException (string message, Exception innerException)
 RateLimitException (string message, Exception innerException)
 Construct an RateLimitException with a message and innerException More...
 
@@ -149,7 +148,7 @@ Additional Inherited Members - +
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
@@ -194,7 +193,7 @@ Additional Inherited Members

Construct an RateLimitException with a secondsString

Parameters
- +
secondsStringThe message for the Exception
secondsStringThe message for the Exception
@@ -218,7 +217,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -232,8 +231,8 @@ Additional Inherited Members

Construct an RateLimitException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_rate_limit_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_rate_limit_exception.png index 0bb125be2d581be6e4b336349d431658344d4833..481e4adc860b6426f53082e0268a06b9314deb4a 100644 GIT binary patch delta 1095 zcmZqYoWeOFre4|8#WAFU@$KB#MW3xWT&JJ#srYYh*Owak;*s}$zsZ-K+;?}TwnT7D z+`nYXB>#yap|)BVLPPgzf{2&0FLusfwMs6l^7l983hrI2UhT9Ctjc$Zw~sqMTU()i z$y4L{`1xn%ta{b|YVPuRtE~>dmG`{LXS{sXugO)fWAC3UIbZKSxkFKV)hhdy+wXm4 zvt53ev+C(=*?;kF_ix?l-TklqLUG~zk{{RYZ$JFc12li4-Hz%7t5?;z^j!=MwO`^8 z9{P0|m|(SCIwgkdL2(t+k16qAo?q789si~Ej`r^4FE2hY{1bV>a1T4K$0{Lz;qk|c zA0qsMg8f`2f`a*ALh3^BKVd=RSC2oftxmtz*>UG$n)T#2>!hP zGVeJ9!sUF+@cZu;#S zf1-ZxCTq8TKK=*(jeHM$iN_xpmx7WbgN@b&#yy%qymG&L$vOdS@N{!^>oICp0BmYeJ9*(>*N{a*M+_Sd)K z@28&3I`=nz-r3{Zp6}E<7p%YeZOYq_?5z0o_@@ubzLaIpo^>@ga;N+L<)ybbf36j% z?%sFH$*9={6jxdN+f&Xw2-jmE(cd1lf*1mB0m%8Wjs(o{%JudxUGkt;gr5OF^b-KTPeJtD) z&6*?s^5@Rl`1xn%sMM~Gt9%t5nqP3-ebO6);z?iTR=te9fBwsfdiSXvdfE{F`U~4j zEFAS~79ak9`bG8Z-G$fx3;YVZGnw!I$Ma=B?3?vG)2oj2S1n)%+8qpcgm7)-w4+NI z?k#6;@c-#rZ*ynUZ`b-s)v4t@a_D*0R` zo}ZTS+2+X1{+cJJ$c^ zV_vOyDf>(5)+uKKf+UYAq-(9WHA-{R7t;-U`+8aL$%^e44c<5|J#*o-oa%y$abk9! zSt;`B5uvK_H)YEI-q>$t^QbC(*E+>zIW1>$0g>kg1-dLQ+fIP_30No zJ5*klzkK~-(wF6;FD5OgkQ05ua8Cq?r(7()%zO$IEssUMb~7BimFvFmTc=uE z*8eJ{-IA@mHwwjeTYmjlyWP}bY3V<;>TR+!qqkgKr(U^#>-Wkxyubbxf1h}=-gNHq z{kdh<+mz3zel+`$gof zqY&6;cw(m2p|g_46-O7juCLw7GB+}E7h8Pm73;YT5$Up3A2W*YR&V>jnE{Nd7pAjz z?nr+z;mrx}S&uAi&9Y5h%pIa{&Rsm6qpQDuPeEIqMV_B+cuu;R+k3wSIe+_KTyA>* zJLqWmy4|jFbE=|K)miP=?cdxaFZ(YR5^0b))BPb`E*oU~Ys&d|zbtpX{jki@EcQ|y z&+XvVa*4}*M7PWCdtLT(Y3{$JEy7bami=6OvETZS#W$(HpV|c;n7?dV?3y1falknL xs`|y^lMH{#YQjy}fX6^En0}@O1TaS?83{1OV}8DBl18 diff --git a/class_tgstation_1_1_server_1_1_client_1_1_request_timeout_exception.html b/class_tgstation_1_1_server_1_1_client_1_1_request_timeout_exception.html index d09b1ccbb7..9645384095 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_request_timeout_exception.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_request_timeout_exception.html @@ -108,7 +108,6 @@ Inheritance diagram for Tgstation.Server.Client.RequestTimeoutException: Tgstation.Server.Client.ClientException -Exception @@ -120,7 +119,7 @@ Public Member Functions - +
 RequestTimeoutException (string message)
 Construct an RequestTimeoutException with a message More...
 
 RequestTimeoutException (string message, Exception innerException)
 RequestTimeoutException (string message, Exception innerException)
 Construct an RequestTimeoutException with a message and innerException More...
 
@@ -136,7 +135,7 @@ Additional Inherited Members - + @@ -189,7 +188,7 @@ Additional Inherited Members

Construct an RequestTimeoutException with a message

Parameters
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
- Properties inherited from Tgstation.Server.Client.ClientException
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -211,7 +210,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -225,8 +224,8 @@ Additional Inherited Members

Construct an RequestTimeoutException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_request_timeout_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_request_timeout_exception.png index ec665b42ea5108b54468bff68ed4155f113a23d1..08565eb9bffb0e40186ed8d9391745fa5a996b38 100644 GIT binary patch literal 1405 zcmb_ceNfV89Dj_NEl?>?s;Q)a=?D}78E(7V_3v)a-Sa%3`@Y*vHV4G!<{dqed)a)ZQ{w}2Z)QB!p%JHbgv<} z|JxUfOQT^z4&iceC1)u|hP%eav@;`CueDs`{#wtI;nL8OM05?!= zKbTUHS=2Inhu7Jebu+Z_{%MXi(>gmHKOmLa4@Hq6DSEG0Ix46^bSsC;6-%q9cr-@( zOJq`ci%g~+R@m^r(~hS=eDm56KJ-EN03xe$XG?j9zAG;yYn@{zeTMa!jHn4&YI`PA zs_-dQBNttaGS1C0F|1gv2^{m|%?s_H#YzKc>2jcAR1jH?_2CDGq^Hjy{m* zz+w*@R-iXiAp`ZyS7W}uy znb8|`I~IH5hjFru#tyt7m`d&-y&t7xGG@5dy6~T*>G95zR_V~u%*1ZnA2xs0VA>Xa z=gy=faStz_iqeKg<@jM4+^yJ^gA=Cr=wFBl9F-u1()&QvntE3vj9Jn-*u)60DlMty z?|8P?YKJSAo7zJtM&+vyvqsD zU!gIZF5>M=sJDhmR)4IcnWr%K$_H*vtcUn_h4kq}K^S#rPDYf)lriBbK<>mmTZs-bBQ9 zh&yth&Uiz<>=>-3Ks(vcm+p!^3A5(2rjMxb*z*!58okEigSc7GW5K%&w9=|)E*aRM z*@q0~Q&9zk~JM zZ%XqS+5E3TP;$zddZ3J2#exG*R&djR{P`O()N9}EBsEnvj^@h2VN3Yc?d%NtJHaC>!Dc4LtX7VCWT zIE_IsiWHoUKGutvqlMypD!n57GJh55IYQZv=^{23-NjMwhT32HfyD#8Ish~u z9U3Qdx)C5O)0kyLD%W--QK5GD1d`iKZ^urS4q>*XM=`7k! zrP1SXU~`AW$nEYK@rMp+=$pT`HgbQlveIk_9V!+xjzOn$#cIXyoF7AGg$_=p!oq2eBL<54!&4vd|ZtoAC^tK0OqnR-qn)v z#%Joy1fxUJP60xVDrtG#R^wwFw=Pw#c)^25uU2PM2w&w8hW&}u(g-*EL!(Y4;~M_M zq;)FMn$*4qsU$taTu;VWh7{RB*Sv~(ymXafVvr6QA`DfuMdJMPd`B1thb8B9{)Yx~ z>vk3xVuCO8@ght5sV4uL+K&{i#;x)dJ?Q6ea!p}vp&bp{kV5lO(shl%S8}Iy811YZ zl8%hBPJ4$`i~2*&X`~(_w}Bf2|14D{)JrSx>V?V5Tx%8yM*1)=1$%h=3t+E@@V<-( z-n|o#<>Cy!I*9#JJ%(PPtpjmMx-5@(KpV~)DGxL?98Tg8FAQsIyAwfkt~MINPKuV= z(Z5RSJotA>V!8Kc!tB>%IIWWhaLo>IT(kZRfU%Qrm@Hfc>_CC?rQ7}=+<&bWwr)=p zo_C3IEe=>9JNu8?Nqku)*;JEaf#Ko7QPVP6`8;fL3)VUB;y%^K?2_jbl?u&PW#~rY za};=eup`iw`U&l+fGg;0`nB4Jn%c~Ac86B(m=juNSQb0;MKx}1);Wsa$585*@u9DZ zm^?)#pU@c0GgZ<6XoYmEGQ93Y-J<~$aSC*1l8gy!yCTz?ujK7j%dc-j+FDi~)%Pm0 zr(0ImAzs1hp2@fK?`=MH7~$7v?EHe;)V5ud*d^pGz#PtuMH%Z;ALDKn)ZQCni?S{^ z$_KDxKN)>Op)1LOi#sj-Axcd`3PD%9msLvYk~DF7?C-g}Zpf;Kidq7Zu>C2c`6^IZ ze?i{ysTHHv+`N@$)o+_&SWC--G8-04r__iw#MpboN1;$w+&2eXdKljYR*5EOj;GnQ zkV+@$?{^ LGA5!rJoU_PuxZWx diff --git a/class_tgstation_1_1_server_1_1_client_1_1_server_error_exception.html b/class_tgstation_1_1_server_1_1_client_1_1_server_error_exception.html index 62467e1045..4895048555 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_server_error_exception.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_server_error_exception.html @@ -109,7 +109,6 @@ Inheritance diagram for Tgstation.Server.Client.ServerErrorException: Tgstation.Server.Client.ClientException -Exception @@ -121,7 +120,7 @@ Public Member Functions - +
 ServerErrorException (string html)
 Construct an ServerErrorException with html More...
 
 ServerErrorException (string message, Exception innerException)
 ServerErrorException (string message, Exception innerException)
 Construct an ServerErrorException with a message and innerException More...
 
@@ -150,7 +149,7 @@ Additional Inherited Members - +
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
@@ -219,7 +218,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -233,8 +232,8 @@ Additional Inherited Members

Construct an ServerErrorException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_server_error_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_server_error_exception.png index 05645b97047b8c488ed113fc69017c66805b49a0..a402e6d2b419ffba86523ffe2e31b6e9b766ba20 100644 GIT binary patch literal 1235 zcmeAS@N?(olHy`uVBq!ia0y~yVB`g|J2;quIz>rW{OH}-S^5_Rc+0>0EK-zu_ipCC>2A7r z-o2YXtLnbF{e7RqclWN}u6K3&g&Fek-3!)D{-tqw^S}A`oo4U;{r!b)_Ts$uS?|AT zy!*(P`|jQSMdnv~Q=gf;@Xi+2DF2>+arP@W=fy(5_L_W~vRU=h=?tCFk_CBxa->Sb zOU_MMKfhwxpA?{#;lEleT%1kr+%>M{1G=N!+r;JYUE9eS9c{a-mvy>nF7^1z^n<~c zQRN~-#@8CI_6&wOCIKa=AZ`jM+3+k3^Re|F&K5WWiX@& z@m^#&p(2G%;m1xBx&GtMQF?M|X_sn%s%Okh1u8c%K3(Fw_{!-shxYFG`_EVV(As#j zpzgMr>(?&KpUj$>_Hkd&l{0HX%X8<7#8wJBy?4iyz*$(zN&P)$3Ld%gwz1AIp(c+-T*Rl~#7aw)f`3NY3q| zx8EmhI267Au0mT*S$FcXwd);c8*nafytHBW>&%X0c5md8&1#+mo_Vw2$E}lFZZegd zMc=)_X4`wc^U9f7)ic~qY|NW_Pa=E9?*6Zb<$BK+wjaBwmUG5IdE>9cCcCVc&YgZ( z=w3z&>!#A0nC|Un&*x6kooMg%cwgQV6R8Q$=L+SnFFQ5Y*ZP&i-5>oXk7f#WeOVZE zTvqgTRd9aN($K$J%I6eMh6s+Gb5LE9((+;f`$?4rRf@_$JG!L-UdnuO5l{B z-Me)e({E5D7G*u}XD9;2;nu`6v0|??^3CUNFgy2`m$zZ^vu}%#ZQI%nqp8&_r>d-p1P?rF#JgIC|=d>5!P|KWW; zaYuJal+7|A7=0#GxCvsLx(zjT#)eTeBtW7Sl)1M8}kk`alU-{EgZaj-VIrTcQ1Y+=X^>bP0l+XkK D!w*vQ delta 1148 zcmcc2d6IKNOnro>i(^OyCx}0&_VbVTt{bh=txqJDK?#?(9 zA3uA+o5`O{eeOT~{zw0d=cVi~^Y#YzIiD>5Ew$uFi=Agx=e-c~`e{?|3xwwQEl_!> zS`~bEMTUse{^`f&6>W2^lk8pERd%I!zVG5WUB~8r`YQN-%6uQ^N$b8`S{*V;2<(Ps z=NBq^zMqykqfI5%i1+e@B|gHvg);7|O&a(wFds1DZHVe`FPy=4z*Cs#u>o&`OIo57 z!%UyUCJaVE?F=bEE%gj1QlyqKPB>}e!>lkla|W9O$X^XU%P>@e6wdcZmN|YgN@}r6 zWU(>WKA^fuQ(n$G7BFkchd1@hMHT-n^&Iv$=MclLx#$};_a?53xy z^CZ7nzpc~bbB#Z5eSd6O^0IeZ>t8N1TsU8R`5KoW8hcY8Zu{Dq9raJYtu#DWs=a%C z>B09o(?0gzG;&TV?FyO1w`;kf^^~>`85jPsG>hK*^^VOjWplKQ@!HzbTP>~@zEj%n z-tmt$`Ih-zV)IMoe#c9WAM=$jzv8%aGp96p_7{_Qam>BUlRqE{Ip!KH{x${jnF-2P2y*nB``?KfCZ71TF7RH+wNr|5L z?Af_3T>a^}W%IT!&ikX-Td}-FsdmMck1j`d#a*3!W9zLW8jIJjy|XoU+OCT`fC8`%wUn(lqds?rxQBF#1qa4eJq^GbJ!rlJDFj_sdEOrml^6kz|o(% zbmO&!)f%On2_H?6)t9rysp#ealDvP4DLlSLO#xcJ3EGo;7=>e(l_M zdBLn1FRkV>_dY%Mx5--go}E{I|B7DzGK#)tUv_m_;f^0s*Ed~`EikpdZQF8@ z_n%SGjCV?NXC2GocCL`PF+X9$q3HYdcNN-t%Da_&Z(T2b$F$|v3cj}!4y5M&w&v`$ z-DTT<8yddjK7GWL@@=Me+*GO|eV1E@U#jrmNk!^fU+7cz!T>8g6tv$Eyu7;lk P0}yz+`njxgN@xNAK{_>D diff --git a/class_tgstation_1_1_server_1_1_client_1_1_service_unavailable_exception.html b/class_tgstation_1_1_server_1_1_client_1_1_service_unavailable_exception.html index 8e1547ade7..8a9c26d707 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_service_unavailable_exception.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_service_unavailable_exception.html @@ -108,7 +108,6 @@ Inheritance diagram for Tgstation.Server.Client.ServiceUnavailableException: Tgstation.Server.Client.ClientException -Exception @@ -120,7 +119,7 @@ Public Member Functions - +
 ServiceUnavailableException (string message)
 Construct an ServiceUnavailableException with a message More...
 
 ServiceUnavailableException (string message, Exception innerException)
 ServiceUnavailableException (string message, Exception innerException)
 Construct an ServiceUnavailableException with a message and innerException More...
 
@@ -136,7 +135,7 @@ Additional Inherited Members - + @@ -189,7 +188,7 @@ Additional Inherited Members

Construct an ServiceUnavailableException with a message

Parameters
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
- Properties inherited from Tgstation.Server.Client.ClientException
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -211,7 +210,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -225,8 +224,8 @@ Additional Inherited Members

Construct an ServiceUnavailableException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_service_unavailable_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_service_unavailable_exception.png index 7758e0c8d4fe4fa79b4020a5333866fcd885a794..7b5e6b91605b5e2a4f3e248ec8409f31163988d8 100644 GIT binary patch literal 1437 zcmbtUYfw{17`=!Ri!wv8DxyrNP~QRbFhUTamSDnbY>H6LC_baX-lQ}_(4#r@$}rxMwx12IDg`=Yg%n)HbMk_b3H|zG`NNFYJ+v$9kg0aLs2{< zEKDr9dcW**uEUmkQzP#p%8HTLKquL-nZ)EcGW5HfvNdsrraodY_1x>`q68!>suyCv zt)Q1KhMkyqtD=Zuo~Rn4MSoaXTnO~n?f@1>xG`PZCNkPqM|iyf%5`iYMB>r!8sOn~ zS?MAvAZ8J|+^$3!fG*1F2p^^^#UBKilJ_Ay${OOj3As#B)zVl7qp96O>S8d2QHJ*R zE{eExpAQp{k16U&91h1Xc{)8gD_5;7$K&CqP^jb-=-oO+Vlu`_mYULMA+sq)tsJ=v z4e2xM#W7z6@tL8yssg@S!FSY%f`R_WYWex^gJP_#mK3xJpbE~ zK1Y(uw*7QPde}BRtjjSRWD_?J+&*GlzF@_}hYH!~Rtu(LFU_p>UdA}(s9-gA$4qlT z`aE=RYHn5Ya{Q?a;izb-p1VR}1z*N+ZL6nx+AWt%%q6(ty`(?eapjoSiGGSvlyO6T z`2*gr&L_EZLTF&E{DgX6m<;M;C9h}6oCJoJ!XrC;K{Jn@Oh3-oQjS_4uMNwqF`Yfk zcGfAIiwJAtI%Ypw$=T5%4Z{*K*wkR`Q#CjLXdlnmA28}ba{8PBFNfn5^ULPr>%x14 z*U@-I=-6H`WOLh zwll76AAf9ZUBq3UcT$CjtVfuFKg70xl?PmThR+fCIzD} z_WKCB?dXs*T)WiK*x9)X=UI0%H7CVrGHJYS3`U!bzaPw4L&N#%vJlT=TZdT6FuOK- z@C0A4Riy=%qzRxLbJg{qx#Z?55}~i0gDKicS&0uMJMTV*S}aah5?A)et;uVb;3v}; zv%Lb{-#VewgsIRcn$^X%@po)2$4!TnAsbn@!LyFh>kHCbK%J&NRkG+LVhBTe=pGl+ z{lKRNgTSgjV7lldd=AxU{B2mNOfEIoLvj>d@Zw$pv@I1j`AUrHJ7N2h?6(Uc#c{f% zA@|BpA}PseoK=(bPZAwe_1T8)2B@j`>dCq+QF)retSqyEIp5~z!3QMR z>5~P5ZeI%+S1r;E^V!Bn9rfq^!SMi(94e>6B5X;YYL2yUg^nv9HPq()avCloNa$=g zJ*RO<{)$QpYP&}@pt*7kD-bv*T85HC=`d<0FQ5Ta={{ESAjQ=a@PGHO57+!FTfht| YIb*JJaOvy_(n|p*P-wK24E)1{Qv*} literal 1421 zcmcIkeN>WH9DYgDtk@s3)cmR~)T3*frBuhy@&jIHT1Zx)^r4wZZ)hsTK%`tVn_A*W zhG{#6*;GoySImzjt=#NGgun?2XIeHC#4_2+ioNWtKlb;|?m73~=ic8vzw_MR^W0ms zBMygN>bw*H05ph?Bme*ki{LX$tdNnU*yMvuPCH_WQD(ClfxqkLbI>1FOH&LIHvqt5S1@ub@dV0HYv4`zEMHs`u}fnvqkEP@Ctnrb5@R*| zjW6mjdmN+=vlmFTKv>B?=>tadZlplGiU%r;cL@vhPv&dH=0u|x~q;}zPQHLSkxWe7e(S)TfW2x_g@8^rOhm= z%I5q76(HN=67Ed1!lyt0wHdvp1V#y3vD^UVe+f=LQDq1fPf0YJM$9U(v-DHF5dPHW z?K~LkZHs0QpuCtJyAS< zR#5uqTsSCt_vBzz52VYHYwzMcbj4wEht$}nH|gu=*h<&A0-UPqo~`O#b&HoZRB)Y6 z4N4LiYGx9Lu0U;SanZt5XvwIrZ0B$uPW@JcRVtmdzA~pSF@ETvisGk-^1mSmgZ;x# zYvZ&02jtD8_9=2#`v&{$uUUU&Jz~*=HiH8QbJ}w__ajEPZ`{0UzEJhrh~v#jcEgWP z`+;T$t-=%Rb+UNyQ%x-_@fu-tX$3VTo(MA3EeG?UHweiaZZ zk^L{CTfFpaFWI}knh^6#IBGpG@^2sU_o~_9z%=}k&2gC?3$+aJH@U#GotB^fFUEj(>lFlgDI4;!o8s));3d8hUKQ1UXD>&Q~8zmhTik@6Q+s2 z4}(an>-d~L&B~QED(MNyEyt13lX$ zN0=N&tJ{OF=r<}=(ghh@B<^?P6n9RwpFMDD(W|M+5NUf_l2`Dc>tPbt;ste@t&fDe z`FSEaO!~M*P2Snxt!BXDmm$$;IB=fb47uIqr*LNRYF>xd$dCymD3^~keMw?^Wo=p=A9v^(Da< zW5+gLulwGO4#mr;Tiq`SCXg$SPcE8m+L9%uO^aDvEuNrGY6N;OHe&&+% Tgstation.Server.Client.ClientException -Exception @@ -120,7 +119,7 @@ Public Member Functions - +
 UnauthorizedException (string message)
 Construct an UnauthorizedException with a message More...
 
 UnauthorizedException (string message, Exception innerException)
 UnauthorizedException (string message, Exception innerException)
 Construct an UnauthorizedException with a message and innerException More...
 
@@ -136,7 +135,7 @@ Additional Inherited Members - + @@ -189,7 +188,7 @@ Additional Inherited Members

Construct an UnauthorizedException with a message

Parameters
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
- Properties inherited from Tgstation.Server.Client.ClientException
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -211,7 +210,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -225,8 +224,8 @@ Additional Inherited Members

Construct an UnauthorizedException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_unauthorized_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_unauthorized_exception.png index 857a4f5b8805cff89e23148e2841f90f487fe72d..64648733984f13dd4cb2f649984f9b4f43a07155 100644 GIT binary patch literal 1361 zcmeAS@N?(olHy`uVBq!ia0y~yU=#wfJ2;quq)hUZ1|TI7;1lBd|Nnm=^TnI5rTvE{ z09jys;J^Xa&O7ozE=Ng_UoZnu5eQs86=KA|!1BP;#WAFU@$KB3MUSm`SZ5bZ{rJDW zM)vfS0+p4Ns!t!b}ZPoFV4ovJ!>X4BM^G_J5oGc_YT)0Ta> z>a}dnyqP;sKYq7e@A7o*lV@z6?cV;@T(h#SHm5!FxKI4skG6m7{%M!aoEd+t{OzQd zB|ekq?T<>+U8-mNdG(KNF`sTRZ&nxFvT^FnpP@C;dV;Exm&LD3nfX0q^B*_W^BggsFS=W}-_-wr_bWS@o zy!_XHt}9x+Yqu}o_UqF8qkG<+e-vlSv&c-jx8p{E=gG}iJHouSM0jz&VEes!^GTzn zs!6u7xv!IaB0GKE4=2ulvu3r|w0+YX`;G-H-O>>e_+QQ^-0w)_nK|Ci{LKH#_LwL+ z{S;PBU3B@R<*OC_rJ0NEE0(0>{ct#GwQSDiMJWPb^{2?l$;2+a+Ba`%kI{0oJ@=+A zH@w|!d`qW3{$!<|q_*Jf+s(nr-}%l(_Iv}yBCqP#-r%4gTK(6V!#3}GvLNbe@~eRC z=wdmWzRIVQn$6EuED;n`J~yT2-Bmfkk76}xSEf&yQ`y^5;s0;uq}*Rx3o{X{kF{bDN_aiJBDLk4BvL#f6KDss277Kzj7JVlq6LKZ5`QJ>1NW~bQB`{%t*zTWyL@yE)_ ze>1X{r_TGU&+i{9pSXT*z-p5%_vYPt#l0({+V=MLtDB$QtXTPe@7KH6-;1giH{ZH< za!$xeW3ebrlW9RryAJ+)le5XEx-ξx;qmh|+uVlYSXZ2)wdjb2H~P$DAoT&M|CO zSv$Ql4|M(d?$G(>bJeybpFc7@+mO4xSZ`+gcHWyaFZWF{y7=wrcaYb=sCT7Po6OKF3}13^lx=`Z{)Jm{HW#TGhpD+vj?o2vvQ) zGi8lSP9DEs@7{`nZ<9_u=y>-;D0b1}m`&@K>?`z2ic5X|^_t%MhFh0{O!MEqm3B`H zJ9v#a}JTZ;4+T~wVIwZ7-EMO&4jS5U-yjfpDCUMeR$gglKFxlHO=A7->i z&(rEj<#!v0Np=(U!<9Ur*R1Y;AN=XpuL!=&T{HK!J^%jY_t*7TJTFcE7`r;UFL1yV&Xc5ToVSrFT9N*O+N` z@?_`dkAePkihBPa@Zs*mrfPQKqb#()|wNSm@6PT3FyMdD^p~S8&2VVU|Ye^ zqQPkK=wpZQf@vCzYixRta0YlyVaVwaU{LmA=+R&_Sj2LmLx^(Dfhggw(bm{H+hNmnT>n5P3!tR0en^)p~mNs{Ha__Uu(t+ppaYj(&SJ{^*(S;)TWhb64E; zP|ZB9tGYgH=|U0SC~wXTw%eP#PZ}*XO|p*7f1T!2DZIOH*WrB;k>yj@)UIcK+q=+9 zQYCjqz5lXo>qlB?Csnf#f0;s~+ii-+Btg|lDK=9GcK$XGWMy`-*2w@7wj)nW6UCjkxIIqpx4D*!xeS>Z1I_xi1;E%A8%fX42o{?QBA8Th3}P46Dr8YrFMo+>5N- zx^23v!_Th%7<9iXd-eLe+FsMx_U`Sr+&L*+hi9q5hG{|_^&h^>Am$W=Z_L0?vxYMDh7;=D-KIMkh`s&kG@A(rrZiV!kO7`_Qc+L%Ckau zZRqJs_vTxeb#J;Eb*7~J=a<7dH=B10r{7WmB?`ZFPd*)B`@tD=CVzFxY%9h(srnU5 vbPXHkpPH}5_~R~0)Y?qtIc(5U^p}0@z5Llcaa&@5 Tgstation.Server.Client.ClientException -Exception @@ -123,7 +122,7 @@ Public Member Functions - +
 UnrecognizedResponseException (string message)
 Construct an UnrecognizedResponseException with a message More...
 
 UnrecognizedResponseException (string message, Exception innerException)
 UnrecognizedResponseException (string message, Exception innerException)
 Construct an UnrecognizedResponseException with a message and innerException More...
 
@@ -139,7 +138,7 @@ Additional Inherited Members - + @@ -229,7 +228,7 @@ Additional Inherited Members

Construct an UnrecognizedResponseException with a message

Parameters
 ClientException (string message)
 Construct a ClientException with a message More...
 
 ClientException (string message, Exception innerException)
 ClientException (string message, Exception innerException)
 Construct a ClientException with a message and innerException More...
 
- Properties inherited from Tgstation.Server.Client.ClientException
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -251,7 +250,7 @@ Additional Inherited Members - Exception  + Exception  innerException  @@ -265,8 +264,8 @@ Additional Inherited Members

Construct an UnrecognizedResponseException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the base Exception
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_unrecognized_response_exception.png b/class_tgstation_1_1_server_1_1_client_1_1_unrecognized_response_exception.png index 3ceebd68cef228c8d6bbb534a9cb65d54ac83611..ed3fd45f9ffc3c707c55d526ae7cac70dfd7facf 100644 GIT binary patch literal 1472 zcmeAS@N?(olHy`uVBq!ia0y~yVDtsDJ2;quWJrytE0B^1@CkAK|NlRb`Qpvj(*8pe zfGjXRaNq!I=N)+BeOJEz_dor#%Xk0I@YY}IeXFMCkLbgg<)7Vm)}OmCzu#&0?%$?6 z(sIw8uKc#OPUL$Bho-i`UEmhJw{cfR)X1E2rD#m|=jCH~KeKM@RfOWkpxf~yl@&feoW@vh+ z_MQE`x?bdX4Cll9cdW$EMagem_U!)o1G`r8^=^N@aavQg=;uBAt?JJibi6V$HU2%} zvza{S%&7&D8{cvSSfJE! zcDwf8b=_&4>&)EQeQXPMi^U!LqFo!ZX!@p~Rrh6XuZeTne<}IRJ+FGv;PbzZ?fYqe zyC=QMLS^=3qjwx1W7dAVePhd!Bg=b|*B(Dwc&7Nijm72TpkO$sIlDK^cyqjx{p-(W z=l24`@qf4Wt(dU>nK$zbJ!hw-IRV2rXd-I(<`vIc=IL|UWG?#y_8p8I4J@DpaNF04 zu?QI3D({Sz3M;Szqu*JN>d}vsuzrZnUOGj|OH-ARtAW9yH-{-jaW+Hb2{i|?O`H>s zCKX#wZg_jE?%w|1wcqD590Mf)TWLA@`TOq$_V16h^IYfm*>%6I>_W5Ozo+^(^!dxx zciyf1CTzwSHLbJg9bLT!^wtqwV-=oW{OzzD) z9+j0SEcZ+Av5CCmyU6yxchx4(sQIf=8a>xrzJ9^tZ9h9&h4i>?i!zD*%szM8o8#EF zj%#VQVc%J*54YCby7%|)zOAn|{a@2;!~ZDx-`ulW`?GEw3;qz<(Dvi=fwZqIXFqR0 zT-toR`tOy~|E}9!KKEtg?I`x@&wQnCZt(OyTe>fQvv*D8n~j+Xz;rV8tnLSoW1D2! zS1;bUd2_?7nNqXgo{+z36Z5k9>{7Ypl)`}7hfhggedgGE#q77^>g4?W>9db$oX>hv zp1bd!`Ej$=XMRnUOWq&z^6=b^H?8dNn3o5=;(C14Zg;Zryq6FEWbtjR5)DnSlF7`D zx}I3S%>1~Y`QeW<<;?rmpZih~vMyPt{oA>jcGm)L_q=9&@Tz9|vz_hL7k6*HInRJW z{K;>V|0~Zf-N3l+%=OHF(X$zt9&jT?BdC0MXXXtm98XpLW3GF`yTq-_Y#Xq+W$<+M Kb6Mw<&;$S_pT=we literal 1455 zcmb_ceNa+a9DS&jTBg-jT7o*Fn`t?k``iR6ODCU&HcB6AX6lBaU?RF&Sd!(Mt)zj8 z?#q};*lh|wQqwKVHdD$LYx5%ng^Ctq@T*W)1!P~l(@eX6cW3VJp7Xo+&N*}E%q`j- zzQe|PjWqxOHc)7ABmkg@2tI3Rj#N@E6oM@FyLXX7OePZo-;GRVl{_p(&_DCRg9i(W z2AvRUnHd=#1t5n$j=nCEF90kcK*2$z9Mou)HmxDRE`--CxNmah`&rn>I};tMO2Y0Y zepBQiZ%?F}1pP5YQIq@Di&$5PS5Jhx9)fNji{)k2UeqYgtrni-gMnMU9h3dc9IJ_^ zZ-P0Q-6!@a`qVnrT}xg-9HC3an4LGQlwmcP!3L1`XX}iNaPF(x=k8GRE1_O};{({i zmIpOrhw|Yum@Vn3qlLZry4dr?-mAAgpm9pX6{isxL5o_aCEQI$eLTL07R4b9hxcPC zKhVM=S0Q`R1~ej@{n1a|=LRugs;I9wMwa z$SoeXZ$u&YSYFf>&WjQH5d5Na#`9knsb(a|my_x6>E%8-{ZT3Rddja(AKl`lyu-Z0 z#~RP<8=>A5y~dNvtE+R+b1VNF{dk@uq zIh~Ht$^0X_Cyxm^lbdu9m?Q~SKD*P3RF z3zl!R)~__%I~&`r-D%l5eO5un^-oyrXfMc}Q>cdOn=~1Qn_PUvo22>E68AQ;v^#1d znst6g_lT^s7}EXKZgX19RP}1-{qa4&P1eizF500B&vKM5IbSX~No9oQ#*Nfd5*ce} zsw9CEyE^YU{(R$F1EY>5s|_0o`Ie78G?xH&NQtdU5O8+tSk1{kDQX^2n%&LaF1krdM|0{)%Pf| zg{n#g8rQyLIQjVCwVgqm_C3_LF?I zDTT3t%EIZiex`UBSomu7|6+lsmPelR5R_>Riv*NHkz;{h5c0bMP)K-i&9+1Ne*@>X BztjK# diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_byond_manager.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_byond_manager.html index ff77999478..a49ff8c98a 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_byond_manager.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_byond_manager.html @@ -362,8 +362,7 @@ Private Attributes
Returns
A Task representing the running operation

Definition at line 109 of file ByondManager.cs.

-
110  {
111  var ourTcs = new TaskCompletionSource<object>();
112  Task inProgressTask;
113 
114  var versionKey = VersionKey(version);
115  bool installed;
116  lock (installedVersions)
117  {
118  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
119  if (!installed)
120  installedVersions.Add(versionKey, ourTcs.Task);
121  }
122  if (installed)
123  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
124  {
125  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
126  cancellationToken.ThrowIfCancellationRequested();
127  return;
128  }
129  //okay up to us to install it then
130  try
131  {
132  await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
133  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
134 
135  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
136  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
137 
138  try
139  {
140  var download = await downloadTask.ConfigureAwait(false);
141  await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
142  await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
143 
144  //make sure to do this last because this is what tells us we have a valid version in the future
145  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
146  }
147  catch (WebException e)
148  {
149  //since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
150  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message));
151  }
152  catch (OperationCanceledException)
153  {
154  throw;
155  }
156  catch
157  {
158  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
159  throw;
160  }
161 
162  ourTcs.SetResult(null);
163  }
164  catch (Exception e)
165  {
166  if (!(e is OperationCanceledException))
167  await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken).ConfigureAwait(false);
168  lock (installedVersions)
169  installedVersions.Remove(versionKey);
170  ourTcs.SetException(e);
171  throw;
172  }
173  }
-
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+
110  {
111  var ourTcs = new TaskCompletionSource<object>();
112  Task inProgressTask;
113 
114  var versionKey = VersionKey(version);
115  bool installed;
116  lock (installedVersions)
117  {
118  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
119  if (!installed)
120  installedVersions.Add(versionKey, ourTcs.Task);
121  }
122  if (installed)
123  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
124  {
125  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
126  cancellationToken.ThrowIfCancellationRequested();
127  return;
128  }
129  //okay up to us to install it then
130  try
131  {
132  await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
133  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
134 
135  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
136  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
137 
138  try
139  {
140  var download = await downloadTask.ConfigureAwait(false);
141  await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
142  await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
143 
144  //make sure to do this last because this is what tells us we have a valid version in the future
145  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
146  }
147  catch (WebException e)
148  {
149  //since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
150  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message));
151  }
152  catch (OperationCanceledException)
153  {
154  throw;
155  }
156  catch
157  {
158  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
159  throw;
160  }
161 
162  ourTcs.SetResult(null);
163  }
164  catch (Exception e)
165  {
166  if (!(e is OperationCanceledException))
167  await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken).ConfigureAwait(false);
168  lock (installedVersions)
169  installedVersions.Remove(versionKey);
170  ourTcs.SetException(e);
171  throw;
172  }
173  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:48
Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content
Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken)
Extract a set of zipFileBytes to a given path
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_posix_byond_installer.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_posix_byond_installer.html index 63ae6bf453..87c00d01a3 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_posix_byond_installer.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_posix_byond_installer.html @@ -239,8 +239,7 @@ Additional Inherited Members

Implements Tgstation.Server.Host.Components.Byond.IByondInstaller.

Definition at line 66 of file PosixByondInstaller.cs.

-
67  {
68  try
69  {
70  await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
71  }
72  catch (OperationCanceledException)
73  {
74  throw;
75  }
76  catch (Exception e)
77  {
78  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
79  }
80  }
-
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+
67  {
68  try
69  {
70  await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
71  }
72  catch (OperationCanceledException)
73  {
74  throw;
75  }
76  catch (Exception e)
77  {
78  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
79  }
80  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
readonly ILogger< PosixByondInstaller > logger
The ILogger for the PosixByondInstaller
readonly IIOManager ioManager
The IIOManager for the PosixByondInstaller
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_tests_1_1_test_posix_byond_installer.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_tests_1_1_test_posix_byond_installer.html index e213056112..0a76919dc0 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_tests_1_1_test_posix_byond_installer.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_tests_1_1_test_posix_byond_installer.html @@ -128,8 +128,7 @@ Public Member Functions

Definition at line 27 of file TestPosixByondInstaller.cs.

-
28  {
29  var mockIOManager = new Mock<IIOManager>();
30  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
31  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
32  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
33 
34  const string ByondCachePath = "~/.byond/cache";
35 
36  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Returns(Task.CompletedTask).Verifiable();
37 
38  await installer.CleanCache(default);
39 
40  mockIOManager.Verify();
41 
42 
43  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new OperationCanceledException()).Verifiable();
44 
45  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => installer.CleanCache(default)).ConfigureAwait(false);
46 
47  mockIOManager.Verify();
48 
49  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new Exception()).Verifiable();
50 
51  await installer.CleanCache(default).ConfigureAwait(false);
52 
53  mockIOManager.Verify();
54  }
-
+
28  {
29  var mockIOManager = new Mock<IIOManager>();
30  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
31  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
32  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
33 
34  const string ByondCachePath = "~/.byond/cache";
35 
36  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Returns(Task.CompletedTask).Verifiable();
37 
38  await installer.CleanCache(default);
39 
40  mockIOManager.Verify();
41 
42 
43  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new OperationCanceledException()).Verifiable();
44 
45  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => installer.CleanCache(default)).ConfigureAwait(false);
46 
47  mockIOManager.Verify();
48 
49  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new Exception()).Verifiable();
50 
51  await installer.CleanCache(default).ConfigureAwait(false);
52 
53  mockIOManager.Verify();
54  }
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_windows_byond_installer.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_windows_byond_installer.html index 07b0163248..3632cca8f8 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_windows_byond_installer.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_windows_byond_installer.html @@ -253,8 +253,7 @@ Additional Inherited Members

Implements Tgstation.Server.Host.Components.Byond.IByondInstaller.

Definition at line 89 of file WindowsByondInstaller.cs.

-
90  {
91  try
92  {
93  await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
94  }
95  catch(OperationCanceledException)
96  {
97  throw;
98  }
99  catch (Exception e)
100  {
101  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
102  }
103  }
-
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+
90  {
91  try
92  {
93  await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
94  }
95  catch(OperationCanceledException)
96  {
97  throw;
98  }
99  catch (Exception e)
100  {
101  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
102  }
103  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
readonly ILogger< WindowsByondInstaller > logger
The ILogger for the WindowsByondInstaller
readonly IIOManager ioManager
The IIOManager for the WindowsByondInstaller
string ConcatPath(params string[] paths)
Combines an array of strings into a path
@@ -350,8 +349,7 @@ Additional Inherited Members

Definition at line 114 of file WindowsByondInstaller.cs.

References Tgstation.Server.Host.Core.IProcessBase.Lifetime, Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock(), and Tgstation.Server.Host.Core.IProcess.Terminate().

-
115  {
116  async Task SetNoPromptTrusted()
117  {
118  var configPath = ioManager.ConcatPath(path, ByondConfigDir);
119  await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
120  await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
121  };
122 
123  var setNoPromptTrustedModeTask = SetNoPromptTrusted();
124 
125  //after this version lummox made DD depend of directx lol
126  //but then he became amazing and not only fixed it but also gave us 30s compiles \[T]/
127  if (version.Major == 512 && version.Minor >= 1427 && version.Minor < 1452 && !installedDirectX)
128  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
129  //check again because race conditions
130  if (!installedDirectX)
131  {
132  //always install it, it's pretty fast and will do better redundancy checking than us
133  var rbdx = ioManager.ConcatPath(path, ByondDXDir);
134  //noShellExecute because we aren't doing runas shennanigans
135  IProcess directXInstaller;
136  try
137  {
138  directXInstaller = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
139  }
140  catch (Exception e)
141  {
142  throw new JobException("Unable to start DirectX installer process! Is the server running with admin privileges?", e);
143  }
144  using (directXInstaller)
145  {
146  int exitCode;
147  using (cancellationToken.Register(() => directXInstaller.Terminate()))
148  exitCode = await directXInstaller.Lifetime.ConfigureAwait(false);
149  cancellationToken.ThrowIfCancellationRequested();
150 
151  if (exitCode != 0)
152  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Failed to install included DirectX! Exit code: {0}", exitCode));
153  installedDirectX = true;
154  }
155  }
156 
157  await setNoPromptTrustedModeTask.ConfigureAwait(false);
158  }
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the WindowsByondInstaller
- +
115  {
116  async Task SetNoPromptTrusted()
117  {
118  var configPath = ioManager.ConcatPath(path, ByondConfigDir);
119  await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
120  await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
121  };
122 
123  var setNoPromptTrustedModeTask = SetNoPromptTrusted();
124 
125  //after this version lummox made DD depend of directx lol
126  //but then he became amazing and not only fixed it but also gave us 30s compiles \[T]/
127  if (version.Major == 512 && version.Minor >= 1427 && version.Minor < 1452 && !installedDirectX)
128  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
129  //check again because race conditions
130  if (!installedDirectX)
131  {
132  //always install it, it's pretty fast and will do better redundancy checking than us
133  var rbdx = ioManager.ConcatPath(path, ByondDXDir);
134  //noShellExecute because we aren't doing runas shennanigans
135  IProcess directXInstaller;
136  try
137  {
138  directXInstaller = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
139  }
140  catch (Exception e)
141  {
142  throw new JobException("Unable to start DirectX installer process! Is the server running with admin privileges?", e);
143  }
144  using (directXInstaller)
145  {
146  int exitCode;
147  using (cancellationToken.Register(() => directXInstaller.Terminate()))
148  exitCode = await directXInstaller.Lifetime.ConfigureAwait(false);
149  cancellationToken.ThrowIfCancellationRequested();
150 
151  if (exitCode != 0)
152  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Failed to install included DirectX! Exit code: {0}", exitCode));
153  installedDirectX = true;
154  }
155  }
156 
157  await setNoPromptTrustedModeTask.ConfigureAwait(false);
158  }
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the WindowsByondInstaller
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
readonly IProcessExecutor processExecutor
The IProcessExecutor for the WindowsByondInstaller
Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat.html index 450021ad56..2799bf8500 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat.html @@ -565,8 +565,7 @@ Private Attributes
Returns
A Task representing the running operation

Definition at line 332 of file Chat.cs.

-
333  {
334  var messageTasks = new Dictionary<IProvider, Task<Message>>();
335  try
336  {
337  while (!cancellationToken.IsCancellationRequested)
338  {
339  //prune disconnected providers
340  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
341  messageTasks.Remove(I.Key);
342 
343  //add new ones
344  Task updatedTask;
345  lock (this)
346  updatedTask = connectionsUpdated.Task;
347  lock (providers)
348  foreach (var I in providers)
349  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
350  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
351 
352  if (messageTasks.Count == 0)
353  {
354  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
355  continue;
356  }
357 
358  //wait for a message
359  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
360 
361  //process completed ones
362  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
363  {
364  var message = await I.Value.ConfigureAwait(false);
365 
366  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
367 
368  messageTasks.Remove(I.Key);
369  }
370  }
371  }
372  catch (OperationCanceledException) { }
373  catch (Exception e)
374  {
375  logger.LogError("Message monitor crashed!: Exception: {0}", e);
376  }
377  }
-
readonly Dictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBot.Id
Definition: Chat.cs:65
+
333  {
334  var messageTasks = new Dictionary<IProvider, Task<Message>>();
335  try
336  {
337  while (!cancellationToken.IsCancellationRequested)
338  {
339  //prune disconnected providers
340  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
341  messageTasks.Remove(I.Key);
342 
343  //add new ones
344  Task updatedTask;
345  lock (this)
346  updatedTask = connectionsUpdated.Task;
347  lock (providers)
348  foreach (var I in providers)
349  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
350  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
351 
352  if (messageTasks.Count == 0)
353  {
354  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
355  continue;
356  }
357 
358  //wait for a message
359  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
360 
361  //process completed ones
362  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
363  {
364  var message = await I.Value.ConfigureAwait(false);
365 
366  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
367 
368  messageTasks.Remove(I.Key);
369  }
370  }
371  }
372  catch (OperationCanceledException) { }
373  catch (Exception e)
374  {
375  logger.LogError("Message monitor crashed!: Exception: {0}", e);
376  }
377  }
readonly Dictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBot.Id
Definition: Chat.cs:65
bool Connected(long connectionId)
If a given set of ChatBot is connected
Definition: Chat.cs:567
Task Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan
TaskCompletionSource< object > connectionsUpdated
The TaskCompletionSource<TResult> that completes when ChatBots change
Definition: Chat.cs:100
@@ -628,8 +627,7 @@ Private Attributes

Definition at line 191 of file Chat.cs.

References Tgstation.Server.Host.Components.Chat.Providers.IProvider.BotMention, Tgstation.Server.Host.Components.Chat.User.Channel, Tgstation.Server.Host.Components.Chat.Providers.IProvider.Connected, Tgstation.Server.Host.Components.Chat.Providers.Message.Content, Tgstation.Server.Host.Components.Chat.Channel.Id, Tgstation.Server.Host.Components.Chat.Channel.IsAdminChannel, Tgstation.Server.Host.Components.Chat.Channel.IsPrivateChannel, Tgstation.Server.Host.Components.Chat.Channel.RealId, Tgstation.Server.Host.Components.Chat.Channel.Tag, and Tgstation.Server.Host.Components.Chat.Providers.Message.User.

-
192  {
193  //map the channel if it's private and we haven't seen it
194  lock (providers)
195  {
196  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
197  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
198  if (message.User.Channel.IsPrivateChannel)
199  lock (mappedChannels)
200  {
201  if (!provider.Connected)
202  return;
203  if (!enumerable.Any())
204  {
205  ulong newId;
206  lock (this)
207  newId = channelIdCounter++;
208  mappedChannels.Add(newId, new ChannelMapping
209  {
210  IsWatchdogChannel = false,
211  ProviderChannelId = message.User.Channel.RealId,
212  ProviderId = providerId,
213  Channel = message.User.Channel
214  });
215  message.User.Channel.RealId = newId;
216  }
217  else
218  message.User.Channel.RealId = enumerable.First().Key;
219  }
220  else
221  {
222  //need to add tag and isAdminChannel
223  var mapping = enumerable.First().Value;
224  message.User.Channel.Id = mapping.Channel.Id;
225  message.User.Channel.Tag = mapping.Channel.Tag;
226  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
227  }
228  }
229 
230  var splits = new List<string>(message.Content.Trim().Split(' '));
231  var address = splits[0];
232  if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ','))
233  address = address.Substring(0, address.Length - 1);
234 
235  address = address.ToUpperInvariant();
236 
237  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
238 
239  if (!addressed && !message.User.Channel.IsPrivateChannel)
240  //no mention
241  return;
242 
243  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
244 
245  if (addressed)
246  splits.RemoveAt(0);
247 
248  if (splits.Count == 0)
249  {
250  //just a mention
251  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
252  return;
253  }
254 
255  var command = splits[0].ToUpperInvariant();
256  splits.RemoveAt(0);
257  var arguments = String.Join(" ", splits);
258 
259  try
260  {
261  async Task<ICommand> GetCommand(string commandName)
262  {
263  if (!builtinCommands.TryGetValue(commandName, out var handler))
264  {
265  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken));
266  await Task.WhenAll(tasks).ConfigureAwait(false);
267  handler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == commandName).FirstOrDefault();
268  }
269  return handler;
270  };
271 
272  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
273 
274  if (command == "HELP" || command == "?")
275  {
276  string helpText;
277  if (splits.Count == 0)
278  {
279  var allCommands = builtinCommands.Select(x => x.Value).ToList();
280  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)).ToList();
281  await Task.WhenAll(tasks).ConfigureAwait(false);
282  allCommands.AddRange(tasks.SelectMany(x => x.Result));
283  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)));
284  }
285  else
286  {
287  var helpHandler = await GetCommand(splits[0].ToUpperInvariant()).ConfigureAwait(false);
288  if (helpHandler != default)
289  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
290  else
291  helpText = UnknownCommandMessage;
292  }
293  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
294  return;
295  }
296 
297  var commandHandler = await GetCommand(command).ConfigureAwait(false);
298 
299  if (commandHandler == default)
300  {
301  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
302  return;
303  }
304 
305  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
306  {
307  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
308  return;
309  }
310 
311  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
312  if (result != null)
313  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
314  }
315  catch (OperationCanceledException)
316  {
317  throw;
318  }
319  catch (Exception e)
320  {
321  //error bc custom commands should reply about why it failed
322  logger.LogError("Error processing chat command: {0}", e);
323  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
324  }
325  }
-
User User
The Components.Chat.User who sent the Message
Definition: Message.cs:16
+
192  {
193  //map the channel if it's private and we haven't seen it
194  lock (providers)
195  {
196  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
197  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
198  if (message.User.Channel.IsPrivateChannel)
199  lock (mappedChannels)
200  {
201  if (!provider.Connected)
202  return;
203  if (!enumerable.Any())
204  {
205  ulong newId;
206  lock (this)
207  newId = channelIdCounter++;
208  mappedChannels.Add(newId, new ChannelMapping
209  {
210  IsWatchdogChannel = false,
211  ProviderChannelId = message.User.Channel.RealId,
212  ProviderId = providerId,
213  Channel = message.User.Channel
214  });
215  message.User.Channel.RealId = newId;
216  }
217  else
218  message.User.Channel.RealId = enumerable.First().Key;
219  }
220  else
221  {
222  //need to add tag and isAdminChannel
223  var mapping = enumerable.First().Value;
224  message.User.Channel.Id = mapping.Channel.Id;
225  message.User.Channel.Tag = mapping.Channel.Tag;
226  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
227  }
228  }
229 
230  var splits = new List<string>(message.Content.Trim().Split(' '));
231  var address = splits[0];
232  if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ','))
233  address = address.Substring(0, address.Length - 1);
234 
235  address = address.ToUpperInvariant();
236 
237  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
238 
239  if (!addressed && !message.User.Channel.IsPrivateChannel)
240  //no mention
241  return;
242 
243  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
244 
245  if (addressed)
246  splits.RemoveAt(0);
247 
248  if (splits.Count == 0)
249  {
250  //just a mention
251  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
252  return;
253  }
254 
255  var command = splits[0].ToUpperInvariant();
256  splits.RemoveAt(0);
257  var arguments = String.Join(" ", splits);
258 
259  try
260  {
261  async Task<ICommand> GetCommand(string commandName)
262  {
263  if (!builtinCommands.TryGetValue(commandName, out var handler))
264  {
265  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken));
266  await Task.WhenAll(tasks).ConfigureAwait(false);
267  handler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == commandName).FirstOrDefault();
268  }
269  return handler;
270  };
271 
272  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
273 
274  if (command == "HELP" || command == "?")
275  {
276  string helpText;
277  if (splits.Count == 0)
278  {
279  var allCommands = builtinCommands.Select(x => x.Value).ToList();
280  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)).ToList();
281  await Task.WhenAll(tasks).ConfigureAwait(false);
282  allCommands.AddRange(tasks.SelectMany(x => x.Result));
283  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)));
284  }
285  else
286  {
287  var helpHandler = await GetCommand(splits[0].ToUpperInvariant()).ConfigureAwait(false);
288  if (helpHandler != default)
289  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
290  else
291  helpText = UnknownCommandMessage;
292  }
293  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
294  return;
295  }
296 
297  var commandHandler = await GetCommand(command).ConfigureAwait(false);
298 
299  if (commandHandler == default)
300  {
301  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
302  return;
303  }
304 
305  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
306  {
307  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
308  return;
309  }
310 
311  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
312  if (result != null)
313  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
314  }
315  catch (OperationCanceledException)
316  {
317  throw;
318  }
319  catch (Exception e)
320  {
321  //error bc custom commands should reply about why it failed
322  logger.LogError("Error processing chat command: {0}", e);
323  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
324  }
325  }
User User
The Components.Chat.User who sent the Message
Definition: Message.cs:16
Channel Channel
The Components.Chat.Channel the user spoke from
Definition: User.cs:40
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_json_tracking_context.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_json_tracking_context.html index c2791f18ac..1d1c2b7a2b 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_json_tracking_context.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_json_tracking_context.html @@ -259,8 +259,7 @@ Private Attributes

Implements Tgstation.Server.Host.Components.Chat.IJsonTrackingContext.

Definition at line 64 of file JsonTrackingContext.cs.

-
65  {
66  try
67  {
68  if (Active && await ioManager.FileExists(commandsPath, cancellationToken).ConfigureAwait(false))
69  {
70  var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false);
71  var resultJson = Encoding.UTF8.GetString(resultBytes);
72  logger.LogTrace("Read commands JSON: {0}", resultJson);
73  var result = JsonConvert.DeserializeObject<List<CustomCommand>>(resultJson, new JsonSerializerSettings
74  {
75  ContractResolver = new DefaultContractResolver
76  {
77  NamingStrategy = new SnakeCaseNamingStrategy()
78  }
79  });
80  foreach (var I in result)
81  I.SetHandler(customCommandHandler);
82  return result;
83  }
84  }
85  catch (OperationCanceledException)
86  {
87  throw;
88  }
89  catch (Exception e)
90  {
91  logger.LogWarning("Error retrieving custom commands! Exception: {0}", e);
92  }
93  return new List<CustomCommand>();
94  }
- +
65  {
66  try
67  {
68  if (Active && await ioManager.FileExists(commandsPath, cancellationToken).ConfigureAwait(false))
69  {
70  var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false);
71  var resultJson = Encoding.UTF8.GetString(resultBytes);
72  logger.LogTrace("Read commands JSON: {0}", resultJson);
73  var result = JsonConvert.DeserializeObject<List<CustomCommand>>(resultJson, new JsonSerializerSettings
74  {
75  ContractResolver = new DefaultContractResolver
76  {
77  NamingStrategy = new SnakeCaseNamingStrategy()
78  }
79  });
80  foreach (var I in result)
81  I.SetHandler(customCommandHandler);
82  return result;
83  }
84  }
85  catch (OperationCanceledException)
86  {
87  throw;
88  }
89  catch (Exception e)
90  {
91  logger.LogWarning("Error retrieving custom commands! Exception: {0}", e);
92  }
93  return new List<CustomCommand>();
94  }
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_discord_provider.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_discord_provider.html index d4ddaadbaa..18922fd71a 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_discord_provider.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_discord_provider.html @@ -323,8 +323,7 @@ Additional Inherited Members

Implements Tgstation.Server.Host.Components.Chat.Providers.Provider.

Definition at line 114 of file DiscordProvider.cs.

-
115  {
116  if (Connected)
117  return true;
118 
119  try
120  {
121  await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
122 
123  cancellationToken.ThrowIfCancellationRequested();
124 
125  await client.StartAsync().ConfigureAwait(false);
126 
127  var channelsAvailable = new TaskCompletionSource<object>();
128  client.Ready += () =>
129  {
130  channelsAvailable.TrySetResult(null);
131  return Task.CompletedTask;
132  };
133  using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
134  await channelsAvailable.Task.ConfigureAwait(false);
135  }
136  catch (OperationCanceledException)
137  {
138  throw;
139  }
140  catch (Exception e)
141  {
142  logger.LogWarning("Error connecting to Discord: {0}", e);
143  return false;
144  }
145 
146  return true;
147  }
- +
115  {
116  if (Connected)
117  return true;
118 
119  try
120  {
121  await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
122 
123  cancellationToken.ThrowIfCancellationRequested();
124 
125  await client.StartAsync().ConfigureAwait(false);
126 
127  var channelsAvailable = new TaskCompletionSource<object>();
128  client.Ready += () =>
129  {
130  channelsAvailable.TrySetResult(null);
131  return Task.CompletedTask;
132  };
133  using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
134  await channelsAvailable.Task.ConfigureAwait(false);
135  }
136  catch (OperationCanceledException)
137  {
138  throw;
139  }
140  catch (Exception e)
141  {
142  logger.LogWarning("Error connecting to Discord: {0}", e);
143  return false;
144  }
145 
146  return true;
147  }
readonly ILogger< DiscordProvider > logger
The ILogger for the DiscordProvider
readonly string botToken
The token used for connecting to discord
readonly DiscordSocketClient client
The DiscordSocketClient for the DiscordProvider
@@ -358,8 +357,7 @@ Additional Inherited Members

Implements Tgstation.Server.Host.Components.Chat.Providers.Provider.

Definition at line 150 of file DiscordProvider.cs.

-
151  {
152  if (!Connected)
153  return;
154 
155  try
156  {
157  await client.StopAsync().ConfigureAwait(false);
158  cancellationToken.ThrowIfCancellationRequested();
159  await client.LogoutAsync().ConfigureAwait(false);
160  }
161  catch (OperationCanceledException)
162  {
163  throw;
164  }
165  catch (Exception e)
166  {
167  logger.LogWarning("Error disconnecting from discord: {0}", e);
168  }
169  }
- +
151  {
152  if (!Connected)
153  return;
154 
155  try
156  {
157  await client.StopAsync().ConfigureAwait(false);
158  cancellationToken.ThrowIfCancellationRequested();
159  await client.LogoutAsync().ConfigureAwait(false);
160  }
161  catch (OperationCanceledException)
162  {
163  throw;
164  }
165  catch (Exception e)
166  {
167  logger.LogWarning("Error disconnecting from discord: {0}", e);
168  }
169  }
readonly ILogger< DiscordProvider > logger
The ILogger for the DiscordProvider
readonly DiscordSocketClient client
The DiscordSocketClient for the DiscordProvider
@@ -519,8 +517,7 @@ Additional Inherited Members

Implements Tgstation.Server.Host.Components.Chat.Providers.Provider.

Definition at line 211 of file DiscordProvider.cs.

-
212  {
213  try
214  {
215  var channel = client.GetChannel(channelId) as IMessageChannel;
216  await (channel?.SendMessageAsync(message, false, null, new RequestOptions
217  {
218  CancelToken = cancellationToken
219  }) ?? Task.CompletedTask).ConfigureAwait(false);
220  }
221  catch (OperationCanceledException)
222  {
223  throw;
224  }
225  catch (Exception e)
226  {
227  logger.LogWarning("Error sending discord message: {0}", e);
228  }
229  }
-
readonly ILogger< DiscordProvider > logger
The ILogger for the DiscordProvider
+
212  {
213  try
214  {
215  var channel = client.GetChannel(channelId) as IMessageChannel;
216  await (channel?.SendMessageAsync(message, false, null, new RequestOptions
217  {
218  CancelToken = cancellationToken
219  }) ?? Task.CompletedTask).ConfigureAwait(false);
220  }
221  catch (OperationCanceledException)
222  {
223  throw;
224  }
225  catch (Exception e)
226  {
227  logger.LogWarning("Error sending discord message: {0}", e);
228  }
229  }
readonly ILogger< DiscordProvider > logger
The ILogger for the DiscordProvider
readonly DiscordSocketClient client
The DiscordSocketClient for the DiscordProvider
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_irc_provider.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_irc_provider.html index 7994f05087..9d10cf3467 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_irc_provider.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_irc_provider.html @@ -480,8 +480,7 @@ Additional Inherited Members

Definition at line 329 of file IrcProvider.cs.

References Tgstation.Server.Host.Components.Chat.Channel.IsAdminChannel.

-
330  {
331  if (!Connected)
332  return;
333  try
334  {
335  await Task.Factory.StartNew(() =>
336  {
337  try
338  {
339  client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise it wont go through
340  }
341  catch (Exception e)
342  {
343  logger.LogWarning("Error quitting IRC: {0}", e);
344  }
345  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
346  Dispose();
347  await listenTask.ConfigureAwait(false);
348  }
349  catch (OperationCanceledException)
350  {
351  throw;
352  }
353  catch (Exception e)
354  {
355  logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
356  }
357  }
-
readonly IrcFeatures client
The IrcFeatures client
Definition: IrcProvider.cs:41
+
330  {
331  if (!Connected)
332  return;
333  try
334  {
335  await Task.Factory.StartNew(() =>
336  {
337  try
338  {
339  client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise it wont go through
340  }
341  catch (Exception e)
342  {
343  logger.LogWarning("Error quitting IRC: {0}", e);
344  }
345  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
346  Dispose();
347  await listenTask.ConfigureAwait(false);
348  }
349  catch (OperationCanceledException)
350  {
351  throw;
352  }
353  catch (Exception e)
354  {
355  logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
356  }
357  }
readonly IrcFeatures client
The IrcFeatures client
Definition: IrcProvider.cs:41
readonly ILogger< IrcProvider > logger
The ILogger for the IrcProvider
Definition: IrcProvider.cs:36
Task listenTask
The Task used for IrcConnection.Listen(bool)
Definition: IrcProvider.cs:82
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dmb_factory.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dmb_factory.html index 813717f01a..9db2bd58cd 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dmb_factory.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dmb_factory.html @@ -332,8 +332,7 @@ Private Attributes

Definition at line 231 of file DmbFactory.cs.

References Tgstation.Server.Api.Models.Internal.CompileJob.DirectoryName.

-
232  {
233  List<long> jobIdsToSkip;
234  //don't clean locked directories
235  lock (this)
236  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
237 
238  List<string> jobUidsToNotErase = null;
239 
240  //find the uids of locked directories
241  await databaseContextFactory.UseContext(async db =>
242  {
243  jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
244  }).ConfigureAwait(false);
245 
246  //add the other exemption
247  if (exceptThisOne != null)
248  jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
249 
250  //cleanup
251  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
252  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
253  int deleting = 0;
254  var tasks = directories.Select(async x =>
255  {
256  var nameOnly = ioManager.GetFileName(x);
257  if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
258  return;
259  try
260  {
261  ++deleting;
262  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
263  }
264  catch (OperationCanceledException)
265  {
266  throw;
267  }
268  catch (Exception e)
269  {
270  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
271  }
272  }).ToList();
273  if (deleting > 0)
274  {
275  logger.LogDebug("Cleaning {0} unused game folders...", deleting);
276  await Task.WhenAll().ConfigureAwait(false);
277  }
278  }
-
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+
232  {
233  List<long> jobIdsToSkip;
234  //don't clean locked directories
235  lock (this)
236  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
237 
238  List<string> jobUidsToNotErase = null;
239 
240  //find the uids of locked directories
241  await databaseContextFactory.UseContext(async db =>
242  {
243  jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
244  }).ConfigureAwait(false);
245 
246  //add the other exemption
247  if (exceptThisOne != null)
248  jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
249 
250  //cleanup
251  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
252  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
253  int deleting = 0;
254  var tasks = directories.Select(async x =>
255  {
256  var nameOnly = ioManager.GetFileName(x);
257  if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
258  return;
259  try
260  {
261  ++deleting;
262  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
263  }
264  catch (OperationCanceledException)
265  {
266  throw;
267  }
268  catch (Exception e)
269  {
270  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
271  }
272  }).ToList();
273  if (deleting > 0)
274  {
275  logger.LogDebug("Cleaning {0} unused game folders...", deleting);
276  await Task.WhenAll().ConfigureAwait(false);
277  }
278  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
Task< IReadOnlyList< string > > GetDirectories(string path, CancellationToken cancellationToken)
Returns directory names in a given path
readonly IIOManager ioManager
The IIOManager for the DmbFactory
Definition: DmbFactory.cs:39
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dream_maker.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dream_maker.html index bec0bb6782..f348ce069f 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dream_maker.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dream_maker.html @@ -362,8 +362,7 @@ Private Attributes

References Tgstation.Server.Host.Components.Repository.IRepository.CopyTo(), and Tgstation.Server.Host.Components.Repository.IRepository.Origin.

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess().

-
271  {
272  if (revisionInformation == null)
273  throw new ArgumentNullException(nameof(revisionInformation));
274 
275  if (dreamMakerSettings == null)
276  throw new ArgumentNullException(nameof(dreamMakerSettings));
277 
278  if (repository == null)
279  throw new ArgumentNullException(nameof(repository));
280 
281  if (progressReporter == null)
282  throw new ArgumentNullException(nameof(progressReporter));
283 
284  if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
285  throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!");
286 
287  logger.LogTrace("Begin Compile");
288 
289  var job = new Models.CompileJob
290  {
291  DirectoryName = Guid.NewGuid(),
292  DmeName = dreamMakerSettings.ProjectName,
293  RevisionInformation = revisionInformation
294  };
295 
296  logger.LogTrace("Compile output GUID: {0}", job.DirectoryName);
297 
298  lock (this)
299  {
300  if (compiling)
301  throw new JobException("There is already a compile job in progress!");
302  compiling = true;
303  }
304 
305  using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
306  {
307  async Task ProgressTask()
308  {
309  if (!estimatedDuration.HasValue)
310  return;
311 
312  progressReporter(0);
313  var ct = progressCts.Token;
314  var sleepInterval = estimatedDuration.Value / 100;
315 
316  try
317  {
318  for (var I = 0; I < 99; ++I)
319  {
320  await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false);
321  progressReporter(I + 1);
322  }
323  }
324  catch (OperationCanceledException) { }
325  }
326 
327  var progressTask = ProgressTask();
328  try
329  {
330 
331 
332  var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
333  string remoteCommitInsert;
334  if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
335  {
336  commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
337  remoteCommitInsert = String.Empty;
338  }
339  else
340  remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
341 
342  var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
343  String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
344  {
345  var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
346  if (x.Comment != null)
347  result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
348  return result;
349  })));
350 
351  using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false))
352  {
353  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false);
354 
355  async Task CleanupFailedCompile(bool cancelled)
356  {
357  logger.LogTrace("Cleaning compile directory...");
358  var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);
359  try
360  {
361  await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
362  }
363  catch (Exception e)
364  {
365  logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e);
366  }
367 
368  await chatTask.ConfigureAwait(false);
369  };
370 
371  try
372  {
373  await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
374 
375  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
376  var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
377 
378  logger.LogTrace("Copying repository to game directory...");
379  //copy the repository
380  var fullDirA = ioManager.ResolvePath(dirA);
381  var repoOrigin = repository.Origin;
382  using (repository)
383  await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
384 
385  //run precompile scripts
386  var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
387  await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
388 
389  //determine the dme
390  if (job.DmeName == null)
391  {
392  logger.LogTrace("Searching for available .dmes...");
393  var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
394  if (path == default)
395  throw new JobException("Unable to find any .dme!");
396  var dmeWithExtension = ioManager.GetFileName(path);
397  job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
398  }
399  else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false))
400  throw new JobException("Unable to locate specified .dme!");
401 
402  logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);
403 
404  await ModifyDme(job, cancellationToken).ConfigureAwait(false);
405 
406  //run compiler, verify api
407  job.ByondVersion = byondLock.Version.ToString();
408 
409  var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
410 
411  try
412  {
413  if (exitCode != 0)
414  throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
415 
416  await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
417  }
418  catch (JobException)
419  {
420  //server never validated or compile failed
421  await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
422  throw;
423  }
424 
425  logger.LogTrace("Running post compile event...");
426  await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false);
427 
428  logger.LogTrace("Duplicating compiled game...");
429 
430  //duplicate the dmb et al
431  await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
432 
433  logger.LogTrace("Applying static game file symlinks...");
434 
435  //symlink in the static data
436  var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
437  var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
438 
439  await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
440 
441  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false);
442 
443  logger.LogDebug("Compile complete!");
444  return job;
445  }
446  catch (Exception e)
447  {
448  await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false);
449  throw;
450  }
451  }
452  }
453  catch (OperationCanceledException)
454  {
455  await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);
456  throw;
457  }
458  finally
459  {
460  compiling = false;
461  progressCts.Cancel();
462  await progressTask.ConfigureAwait(false);
463  }
464  }
465  }
-
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+
271  {
272  if (revisionInformation == null)
273  throw new ArgumentNullException(nameof(revisionInformation));
274 
275  if (dreamMakerSettings == null)
276  throw new ArgumentNullException(nameof(dreamMakerSettings));
277 
278  if (repository == null)
279  throw new ArgumentNullException(nameof(repository));
280 
281  if (progressReporter == null)
282  throw new ArgumentNullException(nameof(progressReporter));
283 
284  if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
285  throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!");
286 
287  logger.LogTrace("Begin Compile");
288 
289  var job = new Models.CompileJob
290  {
291  DirectoryName = Guid.NewGuid(),
292  DmeName = dreamMakerSettings.ProjectName,
293  RevisionInformation = revisionInformation
294  };
295 
296  logger.LogTrace("Compile output GUID: {0}", job.DirectoryName);
297 
298  lock (this)
299  {
300  if (compiling)
301  throw new JobException("There is already a compile job in progress!");
302  compiling = true;
303  }
304 
305  using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
306  {
307  async Task ProgressTask()
308  {
309  if (!estimatedDuration.HasValue)
310  return;
311 
312  progressReporter(0);
313  var ct = progressCts.Token;
314  var sleepInterval = estimatedDuration.Value / 100;
315 
316  try
317  {
318  for (var I = 0; I < 99; ++I)
319  {
320  await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false);
321  progressReporter(I + 1);
322  }
323  }
324  catch (OperationCanceledException) { }
325  }
326 
327  var progressTask = ProgressTask();
328  try
329  {
330 
331 
332  var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
333  string remoteCommitInsert;
334  if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
335  {
336  commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
337  remoteCommitInsert = String.Empty;
338  }
339  else
340  remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
341 
342  var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
343  String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
344  {
345  var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
346  if (x.Comment != null)
347  result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
348  return result;
349  })));
350 
351  using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false))
352  {
353  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false);
354 
355  async Task CleanupFailedCompile(bool cancelled)
356  {
357  logger.LogTrace("Cleaning compile directory...");
358  var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);
359  try
360  {
361  await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
362  }
363  catch (Exception e)
364  {
365  logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e);
366  }
367 
368  await chatTask.ConfigureAwait(false);
369  };
370 
371  try
372  {
373  await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
374 
375  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
376  var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
377 
378  logger.LogTrace("Copying repository to game directory...");
379  //copy the repository
380  var fullDirA = ioManager.ResolvePath(dirA);
381  var repoOrigin = repository.Origin;
382  using (repository)
383  await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
384 
385  //run precompile scripts
386  var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
387  await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
388 
389  //determine the dme
390  if (job.DmeName == null)
391  {
392  logger.LogTrace("Searching for available .dmes...");
393  var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
394  if (path == default)
395  throw new JobException("Unable to find any .dme!");
396  var dmeWithExtension = ioManager.GetFileName(path);
397  job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
398  }
399  else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false))
400  throw new JobException("Unable to locate specified .dme!");
401 
402  logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);
403 
404  await ModifyDme(job, cancellationToken).ConfigureAwait(false);
405 
406  //run compiler, verify api
407  job.ByondVersion = byondLock.Version.ToString();
408 
409  var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
410 
411  try
412  {
413  if (exitCode != 0)
414  throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
415 
416  await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
417  }
418  catch (JobException)
419  {
420  //server never validated or compile failed
421  await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
422  throw;
423  }
424 
425  logger.LogTrace("Running post compile event...");
426  await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false);
427 
428  logger.LogTrace("Duplicating compiled game...");
429 
430  //duplicate the dmb et al
431  await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
432 
433  logger.LogTrace("Applying static game file symlinks...");
434 
435  //symlink in the static data
436  var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
437  var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
438 
439  await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
440 
441  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false);
442 
443  logger.LogDebug("Compile complete!");
444  return job;
445  }
446  catch (Exception e)
447  {
448  await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false);
449  throw;
450  }
451  }
452  }
453  catch (OperationCanceledException)
454  {
455  await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);
456  throw;
457  }
458  finally
459  {
460  compiling = false;
461  progressCts.Cancel();
462  await progressTask.ConfigureAwait(false);
463  }
464  }
465  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
Task< List< string > > GetFilesWithExtension(string path, string extension, CancellationToken cancellationToken)
Gets a list of files in path with the given extension
readonly ILogger< DreamMaker > logger
The ILogger for DreamMaker
Definition: DreamMaker.cs:83
readonly StaticFiles.IConfiguration configuration
The StaticFiles.IConfiguration for DreamMaker
Definition: DreamMaker.cs:51
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance.html index c199521cf9..774e22eda5 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance.html @@ -635,8 +635,7 @@ Private Attributes

Definition at line 315 of file Instance.cs.

References Tgstation.Server.Host.Models.RevisionInformation.ActiveTestMerges, Tgstation.Server.Api.Models.Internal.RevisionInformation.CommitSha, Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.RevisionInformation.Instance, Tgstation.Server.Host.Components.Instance.Instance(), Tgstation.Server.Host.Components.Repository.RepositoryManager.LoadRepository(), Tgstation.Server.Api.Models.Internal.RevisionInformation.OriginCommitSha, and Tgstation.Server.Host.Components.Repository.Repository.Repository().

-
316  {
317  while (true)
318  try
319  {
320  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
321  logger.LogDebug("Beginning auto update...");
322  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
323  try
324  {
325  Models.User user = null;
326  await databaseContextFactory.UseContext(async (db) => user = await db.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
327  var repositoryUpdateJob = new Job
328  {
329  Instance = new Models.Instance
330  {
331  Id = metadata.Id
332  },
333  Description = "Scheduled repository update",
334  CancelRightsType = RightsType.Repository,
335  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
336  StartedBy = user
337  };
338 
339  string deploySha = null;
340  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) =>
341  {
342  var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
343 
344  //assume 5 steps with synchronize
345  const int ProgressSections = 7;
346  const int ProgressStep = 100 / ProgressSections;
347 
348 
349  const int NumSteps = 3;
350  var doneSteps = 0;
351 
352  Action<int> NextProgressReporter()
353  {
354  var tmpDoneSteps = doneSteps;
355  ++doneSteps;
356  return progress => progressReporter((progress + 100 * tmpDoneSteps) / NumSteps);
357  };
358 
359  using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false))
360  {
361  if (repo == null)
362  {
363  logger.LogTrace("Aborting repo update, no repository!");
364  return;
365  }
366 
367  var startSha = repo.Head;
368  if (!repo.Tracking)
369  {
370  logger.LogTrace("Aborting repo update, not tracking origin!");
371  deploySha = startSha;
372  return;
373  }
374 
375  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
376 
377  //the main point of auto update is to pull the remote
378  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
379 
380  RevisionInformation currentRevInfo = null;
381  bool hasDbChanges = false;
382 
383  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
384  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
385  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
386  .FirstOrDefaultAsync(cancellationToken);
387 
388  async Task UpdateRevInfo(string currentHead, bool onOrigin)
389  {
390  if(currentRevInfo == null)
391  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
392 
393  if (currentRevInfo == default)
394  {
395  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
396  onOrigin = true;
397  }
398 
399  var attachedInstance = new Models.Instance
400  {
401  Id = metadata.Id
402  };
403  var oldRevInfo = currentRevInfo;
404  currentRevInfo = new RevisionInformation
405  {
406  CommitSha = currentHead,
407  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
408  Instance = attachedInstance
409  };
410  if (!onOrigin)
411  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
412 
413  databaseContext.Instances.Attach(attachedInstance);
414  databaseContext.RevisionInformations.Add(currentRevInfo);
415  hasDbChanges = true;
416  }
417 
418  //take appropriate auto update actions
419  bool shouldSyncTracked;
420  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
421  {
422  logger.LogTrace("Preserving test merges...");
423 
424  var currentRevInfoTask = LoadRevInfo();
425 
426  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
427 
428  if (!result.HasValue)
429  throw new JobException("Merge conflict while preserving test merges!");
430 
431  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
432 
433  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
434  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
435 
436  var currentHead = repo.Head;
437  if (currentHead != startSha)
438  {
439  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
440  shouldSyncTracked = stillOnOrigin;
441  }
442  else
443  shouldSyncTracked = false;
444  }
445  else
446  {
447  logger.LogTrace("Not preserving test merges...");
448  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
449 
450  var currentHead = repo.Head;
451 
452  currentRevInfo = await databaseContext.RevisionInformations
453  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
454  .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
455 
456  if (currentHead != startSha && currentRevInfo != default)
457  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
458 
459  shouldSyncTracked = true;
460  }
461 
462  //synch if necessary
463  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
464  {
465  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
466  var currentHead = repo.Head;
467  if (currentHead != currentRevInfo.CommitSha)
468  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
469  }
470 
471  if(hasDbChanges)
472  try
473  {
474  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
475  }
476  catch
477  {
478  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
479  throw;
480  }
481 
482  progressReporter(5 * ProgressStep);
483  deploySha = repo.Head;
484  }
485  }, cancellationToken).ConfigureAwait(false);
486 
487  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
488 
489  if (deploySha == null)
490  {
491  logger.LogTrace("Aborting auto update, repository error!");
492  continue;
493  }
494 
496  {
497  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
498  continue;
499  }
500 
501  //finally set up the job
502  var compileProcessJob = new Job
503  {
504  StartedBy = user,
505  Instance = repositoryUpdateJob.Instance,
506  Description = "Scheduled code deployment",
507  CancelRightsType = RightsType.DreamMaker,
508  CancelRight = (ulong)DreamMakerRights.CancelCompile
509  };
510 
511  await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false);
512 
513  await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
514  }
515  catch (OperationCanceledException)
516  {
517  logger.LogDebug("Cancelled auto update job!");
518  throw;
519  }
520  catch (Exception e)
521  {
522  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
523  continue;
524  }
525  }
526  catch (OperationCanceledException)
527  {
528  break;
529  }
530  logger.LogTrace("Leaving auto update loop...");
531  }
-
async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action< int > progressReporter, CancellationToken cancellationToken)
Run the compile job and insert it into the database. Meant to be called by a Core.IJobManager
Definition: Instance.cs:136
+
316  {
317  while (true)
318  try
319  {
320  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
321  logger.LogDebug("Beginning auto update...");
322  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
323  try
324  {
325  Models.User user = null;
326  await databaseContextFactory.UseContext(async (db) => user = await db.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
327  var repositoryUpdateJob = new Job
328  {
329  Instance = new Models.Instance
330  {
331  Id = metadata.Id
332  },
333  Description = "Scheduled repository update",
334  CancelRightsType = RightsType.Repository,
335  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
336  StartedBy = user
337  };
338 
339  string deploySha = null;
340  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) =>
341  {
342  var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
343 
344  //assume 5 steps with synchronize
345  const int ProgressSections = 7;
346  const int ProgressStep = 100 / ProgressSections;
347 
348 
349  const int NumSteps = 3;
350  var doneSteps = 0;
351 
352  Action<int> NextProgressReporter()
353  {
354  var tmpDoneSteps = doneSteps;
355  ++doneSteps;
356  return progress => progressReporter((progress + 100 * tmpDoneSteps) / NumSteps);
357  };
358 
359  using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false))
360  {
361  if (repo == null)
362  {
363  logger.LogTrace("Aborting repo update, no repository!");
364  return;
365  }
366 
367  var startSha = repo.Head;
368  if (!repo.Tracking)
369  {
370  logger.LogTrace("Aborting repo update, not tracking origin!");
371  deploySha = startSha;
372  return;
373  }
374 
375  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
376 
377  //the main point of auto update is to pull the remote
378  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
379 
380  RevisionInformation currentRevInfo = null;
381  bool hasDbChanges = false;
382 
383  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
384  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
385  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
386  .FirstOrDefaultAsync(cancellationToken);
387 
388  async Task UpdateRevInfo(string currentHead, bool onOrigin)
389  {
390  if(currentRevInfo == null)
391  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
392 
393  if (currentRevInfo == default)
394  {
395  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
396  onOrigin = true;
397  }
398 
399  var attachedInstance = new Models.Instance
400  {
401  Id = metadata.Id
402  };
403  var oldRevInfo = currentRevInfo;
404  currentRevInfo = new RevisionInformation
405  {
406  CommitSha = currentHead,
407  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
408  Instance = attachedInstance
409  };
410  if (!onOrigin)
411  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
412 
413  databaseContext.Instances.Attach(attachedInstance);
414  databaseContext.RevisionInformations.Add(currentRevInfo);
415  hasDbChanges = true;
416  }
417 
418  //take appropriate auto update actions
419  bool shouldSyncTracked;
420  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
421  {
422  logger.LogTrace("Preserving test merges...");
423 
424  var currentRevInfoTask = LoadRevInfo();
425 
426  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
427 
428  if (!result.HasValue)
429  throw new JobException("Merge conflict while preserving test merges!");
430 
431  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
432 
433  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
434  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
435 
436  var currentHead = repo.Head;
437  if (currentHead != startSha)
438  {
439  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
440  shouldSyncTracked = stillOnOrigin;
441  }
442  else
443  shouldSyncTracked = false;
444  }
445  else
446  {
447  logger.LogTrace("Not preserving test merges...");
448  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
449 
450  var currentHead = repo.Head;
451 
452  currentRevInfo = await databaseContext.RevisionInformations
453  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
454  .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
455 
456  if (currentHead != startSha && currentRevInfo != default)
457  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
458 
459  shouldSyncTracked = true;
460  }
461 
462  //synch if necessary
463  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
464  {
465  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
466  var currentHead = repo.Head;
467  if (currentHead != currentRevInfo.CommitSha)
468  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
469  }
470 
471  if(hasDbChanges)
472  try
473  {
474  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
475  }
476  catch
477  {
478  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
479  throw;
480  }
481 
482  progressReporter(5 * ProgressStep);
483  deploySha = repo.Head;
484  }
485  }, cancellationToken).ConfigureAwait(false);
486 
487  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
488 
489  if (deploySha == null)
490  {
491  logger.LogTrace("Aborting auto update, repository error!");
492  continue;
493  }
494 
496  {
497  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
498  continue;
499  }
500 
501  //finally set up the job
502  var compileProcessJob = new Job
503  {
504  StartedBy = user,
505  Instance = repositoryUpdateJob.Instance,
506  Description = "Scheduled code deployment",
507  CancelRightsType = RightsType.DreamMaker,
508  CancelRight = (ulong)DreamMakerRights.CancelCompile
509  };
510 
511  await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false);
512 
513  await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
514  }
515  catch (OperationCanceledException)
516  {
517  logger.LogDebug("Cancelled auto update job!");
518  throw;
519  }
520  catch (Exception e)
521  {
522  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
523  continue;
524  }
525  }
526  catch (OperationCanceledException)
527  {
528  break;
529  }
530  logger.LogTrace("Leaving auto update loop...");
531  }
async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action< int > progressReporter, CancellationToken cancellationToken)
Run the compile job and insert it into the database. Meant to be called by a Core.IJobManager
Definition: Instance.cs:136
CompileJob LatestCompileJob()
The latest staged CompileJob
Definition: Instance.cs:589
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager-members.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager-members.html index a14d407723..75d01cb50e 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager-members.html @@ -115,8 +115,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)Tgstation.Server.Host.Components.InstanceManager Tgstation::Server::Host::Components::IInstanceManager.OfflineInstance(Models.Instance metadata, User user, CancellationToken cancellationToken)Tgstation.Server.Host.Components.IInstanceManager OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)Tgstation.Server.Host.Components.InstanceManager - StartAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Components.InstanceManager - StopAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Components.InstanceManager + serverControlTgstation.Server.Host.Components.InstanceManagerprivate + StartAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Components.InstanceManager + StopAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Components.InstanceManager
- + @@ -162,6 +162,9 @@ Private Attributes + + + @@ -241,18 +244,18 @@ Private Attributes - +

Public Member Functions

 InstanceManager (IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger< InstanceManager > logger)
 InstanceManager (IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger< InstanceManager > logger)
 Construct an InstanceManager More...
 
void Dispose ()
readonly IJobManager jobManager
 The IJobManager for the InstanceManager More...
 
readonly IServerControl serverControl
 The IServerControl for the InstanceManager More...
 
readonly ILogger< InstanceManagerlogger
 The ILogger for the InstanceManager More...
 
databaseContextFactoryThe value of databaseContextFactory
applicationThe value of application
jobManagerThe value of jobManager
serverControlThe IServerControl used to register the InstanceManager as a IRestartHandler
serverControlThe value of serverControl
loggerThe value of logger
-

Definition at line 72 of file InstanceManager.cs.

+

Definition at line 77 of file InstanceManager.cs.

References Tgstation.Server.Host.Core.IServerControl.RegisterForRestart().

-
73  {
74  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
75  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
76  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
77  this.application = application ?? throw new ArgumentNullException(nameof(application));
78  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
79  if (serverControl == null)
80  throw new ArgumentNullException(nameof(serverControl));
81  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
82 
83  serverControl.RegisterForRestart(this);
84 
85  instances = new Dictionary<long, IInstance>();
86  }
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
+
78  {
79  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
80  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
81  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
82  this.application = application ?? throw new ArgumentNullException(nameof(application));
83  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
84  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
85  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
86 
87  serverControl.RegisterForRestart(this);
88 
89  instances = new Dictionary<long, IInstance>();
90  }
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
-
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
+
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
@@ -273,9 +276,9 @@ Private Attributes

-

Definition at line 89 of file InstanceManager.cs.

-
90  {
91  lock (this)
92  {
93  if (disposed)
94  return;
95  disposed = true;
96  }
97  foreach (var I in instances)
98  I.Value.Dispose();
99  }
bool disposed
If the InstanceManager has been Disposed
-
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
+

Definition at line 93 of file InstanceManager.cs.

+
94  {
95  lock (this)
96  {
97  if (disposed)
98  return;
99  disposed = true;
100  }
101  foreach (var I in instances)
102  I.Value.Dispose();
103  }
bool disposed
If the InstanceManager has been Disposed
+
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
@@ -297,8 +300,8 @@ Private Attributes

Implements Tgstation.Server.Host.Components.IInstanceManager.

-

Definition at line 102 of file InstanceManager.cs.

-
103  {
104  if (metadata == null)
105  throw new ArgumentNullException(nameof(metadata));
106  lock (this)
107  {
108  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
109  throw new InvalidOperationException("Instance not online!");
110  return instance;
111  }
112  }
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
+

Definition at line 106 of file InstanceManager.cs.

+
107  {
108  if (metadata == null)
109  throw new ArgumentNullException(nameof(metadata));
110  lock (this)
111  {
112  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
113  throw new InvalidOperationException("Instance not online!");
114  return instance;
115  }
116  }
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
@@ -330,8 +333,8 @@ Private Attributes

Implements Tgstation.Server.Host.Core.IRestartHandler.

-

Definition at line 245 of file InstanceManager.cs.

-
246  {
247  downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
248  return Task.CompletedTask;
249  }
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
+

Definition at line 257 of file InstanceManager.cs.

+
258  {
259  downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
260  return Task.CompletedTask;
261  }
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
readonly IApplication application
The IApplication for the InstanceManager
Version Version
The version of the Application
Definition: IApplication.cs:23
@@ -371,8 +374,8 @@ Private Attributes

Implements Tgstation.Server.Host.Components.IInstanceManager.

-

Definition at line 115 of file InstanceManager.cs.

-
116  {
117  if (newPath == null)
118  throw new ArgumentNullException(nameof(newPath));
119  if (instance.Online.Value)
120  throw new InvalidOperationException("Cannot move an online instance!");
121  var oldPath = instance.Path;
122  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
124  {
125  var targetInstance = new Models.Instance
126  {
127  Id = instance.Id
128  };
129  db.Instances.Attach(targetInstance);
130  targetInstance.Path = newPath;
131  return db.Save(cancellationToken);
132  }).ConfigureAwait(false);
133  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
134  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+

Definition at line 119 of file InstanceManager.cs.

+
120  {
121  if (newPath == null)
122  throw new ArgumentNullException(nameof(newPath));
123  if (instance.Online.Value)
124  throw new InvalidOperationException("Cannot move an online instance!");
125  var oldPath = instance.Path;
126  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
128  {
129  var targetInstance = new Models.Instance
130  {
131  Id = instance.Id
132  };
133  db.Instances.Attach(targetInstance);
134  targetInstance.Path = newPath;
135  return db.Save(cancellationToken);
136  }).ConfigureAwait(false);
137  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
138  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
Task CopyDirectory(string src, string dest, IEnumerable< string > ignore, CancellationToken cancellationToken)
Copies a directory from src to dest
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
Task UseContext(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext
@@ -412,12 +415,12 @@ Private Attributes

-

Definition at line 137 of file InstanceManager.cs.

-
138  {
139  if (metadata == null)
140  throw new ArgumentNullException(nameof(metadata));
141  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
142  IInstance instance;
143  lock (this)
144  {
145  if (!instances.TryGetValue(metadata.Id, out instance))
146  throw new InvalidOperationException("Instance not online!");
147  instances.Remove(metadata.Id);
148  }
149  try
150  {
151  //we are the one responsible for cancelling his jobs
152  var tasks = new List<Task>();
153  await databaseContextFactory.UseContext(async db =>
154  {
155  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
156  {
157  Id = x.Id
158  }).ToAsyncEnumerable();
159  await jobs.ForEachAsync(job =>
160  {
161  lock (tasks)
162  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
163  }, cancellationToken).ConfigureAwait(false);
164  }).ConfigureAwait(false);
165 
166  await Task.WhenAll(tasks).ConfigureAwait(false);
167 
168  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
169  }
170  finally
171  {
172  instance.Dispose();
173  }
174  }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
+

Definition at line 141 of file InstanceManager.cs.

+
142  {
143  if (metadata == null)
144  throw new ArgumentNullException(nameof(metadata));
145  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
146  IInstance instance;
147  lock (this)
148  {
149  if (!instances.TryGetValue(metadata.Id, out instance))
150  throw new InvalidOperationException("Instance not online!");
151  instances.Remove(metadata.Id);
152  }
153  try
154  {
155  //we are the one responsible for cancelling his jobs
156  var tasks = new List<Task>();
157  await databaseContextFactory.UseContext(async db =>
158  {
159  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
160  {
161  Id = x.Id
162  }).ToAsyncEnumerable();
163  await jobs.ForEachAsync(job =>
164  {
165  lock (tasks)
166  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
167  }, cancellationToken).ConfigureAwait(false);
168  }).ConfigureAwait(false);
169 
170  await Task.WhenAll(tasks).ConfigureAwait(false);
171 
172  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
173  }
174  finally
175  {
176  instance.Dispose();
177  }
178  }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
readonly IJobManager jobManager
The IJobManager for the InstanceManager
-
async Task StopAsync(CancellationToken cancellationToken)
-
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
-
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
+
async Task StopAsync(CancellationToken cancellationToken)
+
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
+
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
Task UseContext(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext
@@ -450,11 +453,11 @@ Private Attributes

Implements Tgstation.Server.Host.Components.IInstanceManager.

-

Definition at line 177 of file InstanceManager.cs.

-
178  {
179  if (metadata == null)
180  throw new ArgumentNullException(nameof(metadata));
181  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
182  var instance = instanceFactory.CreateInstance(metadata);
183  try
184  {
185  lock (this)
186  {
187  if (instances.ContainsKey(metadata.Id))
188  throw new InvalidOperationException("Instance already online!");
189  instances.Add(metadata.Id, instance);
190  }
191  }
192  catch
193  {
194  instance.Dispose();
195  throw;
196  }
197  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
198  }
IInstance CreateInstance(Models.Instance metadata)
Create an IInstance
-
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
+

Definition at line 181 of file InstanceManager.cs.

+
182  {
183  if (metadata == null)
184  throw new ArgumentNullException(nameof(metadata));
185  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
186  var instance = instanceFactory.CreateInstance(metadata);
187  try
188  {
189  lock (this)
190  {
191  if (instances.ContainsKey(metadata.Id))
192  throw new InvalidOperationException("Instance already online!");
193  instances.Add(metadata.Id, instance);
194  }
195  }
196  catch
197  {
198  instance.Dispose();
199  throw;
200  }
201  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
202  }
IInstance CreateInstance(Models.Instance metadata)
Create an IInstance
+
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
-
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
+
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
@@ -492,12 +495,12 @@ Private Attributes

-

Definition at line 233 of file InstanceManager.cs.

-
234  {
235  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
236  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
237  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
238 
239  //downgrade the db if necessary
240  if (downgradeVersion != null)
241  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
242  }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
+

Definition at line 245 of file InstanceManager.cs.

+
246  {
247  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
248  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
249  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
250 
251  //downgrade the db if necessary
252  if (downgradeVersion != null)
253  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
254  }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
readonly IJobManager jobManager
The IJobManager for the InstanceManager
-
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
+
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
-
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
+
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
Task UseContext(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext
@@ -571,7 +574,7 @@ Private Attributes

If the InstanceManager has been Disposed

-

Definition at line 60 of file InstanceManager.cs.

+

Definition at line 65 of file InstanceManager.cs.

@@ -595,7 +598,7 @@ Private Attributes

Used in StopAsync(CancellationToken) to determine if database downgrades must be made

-

Definition at line 55 of file InstanceManager.cs.

+

Definition at line 60 of file InstanceManager.cs.

@@ -643,7 +646,7 @@ Private Attributes

Map of Api.Models.Instance.Ids to respective IInstances

-

Definition at line 50 of file InstanceManager.cs.

+

Definition at line 55 of file InstanceManager.cs.

@@ -715,6 +718,30 @@ Private Attributes

The ILogger for the InstanceManager

+

Definition at line 50 of file InstanceManager.cs.

+ +
+
+ +
+
+ + + + + +
+ + + + +
readonly IServerControl Tgstation.Server.Host.Components.InstanceManager.serverControl
+
+private
+
+ +

The IServerControl for the InstanceManager

+

Definition at line 45 of file InstanceManager.cs.

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_context.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_context.html index 199bd978eb..0594e881eb 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_context.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_context.html @@ -284,8 +284,7 @@ Private Attributes

Definition at line 98 of file CommContext.cs.

-
99  {
100  try
101  {
102  var fileBytes = await ioManager.ReadAllBytes(e.FullPath, cancellationToken).ConfigureAwait(false);
103  var file = Encoding.UTF8.GetString(fileBytes);
104 
105  logger.LogTrace("Read interop command json: {0}", file);
106 
107  CommCommand command;
108  try
109  {
110  command = new CommCommand
111  {
112  Parameters = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, object>>(file),
113  RawJson = file
114  };
115  }
116  catch (JsonException ex)
117  {
118  //file not fully written yet
119  logger.LogDebug("Suppressing json convert exception for command file write: {0}", ex);
120  return;
121  }
122 
123  await (handler?.HandleInterop(command, cancellationToken) ?? Task.CompletedTask).ConfigureAwait(false);
124  }
125  catch (OperationCanceledException) { }
126  catch (Exception ex)
127  {
128  logger.LogDebug("Exception while trying to handle command json write: {0}", ex);
129  }
130  }
-
readonly CancellationToken cancellationToken
The CancellationToken for the CommContext
Definition: CommContext.cs:39
+
99  {
100  try
101  {
102  var fileBytes = await ioManager.ReadAllBytes(e.FullPath, cancellationToken).ConfigureAwait(false);
103  var file = Encoding.UTF8.GetString(fileBytes);
104 
105  logger.LogTrace("Read interop command json: {0}", file);
106 
107  CommCommand command;
108  try
109  {
110  command = new CommCommand
111  {
112  Parameters = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, object>>(file),
113  RawJson = file
114  };
115  }
116  catch (JsonException ex)
117  {
118  //file not fully written yet
119  logger.LogDebug("Suppressing json convert exception for command file write: {0}", ex);
120  return;
121  }
122 
123  await (handler?.HandleInterop(command, cancellationToken) ?? Task.CompletedTask).ConfigureAwait(false);
124  }
125  catch (OperationCanceledException) { }
126  catch (Exception ex)
127  {
128  logger.LogDebug("Exception while trying to handle command json write: {0}", ex);
129  }
130  }
readonly CancellationToken cancellationToken
The CancellationToken for the CommContext
Definition: CommContext.cs:39
readonly ILogger< CommContext > logger
The ILogger for the CommContext
Definition: CommContext.cs:24
ICommHandler handler
The ICommHandler for the CommContext
Definition: CommContext.cs:44
readonly IIOManager ioManager
The IIOManager for the CommContext
Definition: CommContext.cs:19
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_repository_1_1_repository_manager.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_repository_1_1_repository_manager.html index e9b32fc036..679b77234b 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_repository_1_1_repository_manager.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_repository_1_1_repository_manager.html @@ -303,8 +303,7 @@ Private Attributes

Definition at line 84 of file RepositoryManager.cs.

References Tgstation.Server.Host.Components.Repository.CredentialsProvider.GenerateHandler(), and Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock().

-
85  {
86  if (url == null)
87  throw new ArgumentNullException(nameof(url));
88  if (progressReporter == null)
89  throw new ArgumentNullException(nameof(progressReporter));
90 
91  logger.LogInformation("Begin clone {0} (Branch: {1})", url, initialBranch);
92  lock (this)
93  {
94  if (CloneInProgress)
95  throw new InvalidOperationException("The repository is already being cloned!");
96  CloneInProgress = true;
97  }
98  try
99  {
100  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
101  {
102  logger.LogTrace("Semaphore acquired");
103  if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
104  try
105  {
106  await Task.Factory.StartNew(() =>
107  {
108  string path = null;
109  try
110  {
111  path = LibGit2Sharp.Repository.Clone(url.ToString(), ioManager.ResolvePath("."), new CloneOptions
112  {
113  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
114  OnTransferProgress = (a) =>
115  {
116  var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
117  progressReporter((int)percentage);
118  return !cancellationToken.IsCancellationRequested;
119  },
120  RecurseSubmodules = true,
121  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
122  RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
123  BranchName = initialBranch,
124  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
125  });
126  }
127  catch (UserCancelledException) { }
128  cancellationToken.ThrowIfCancellationRequested();
129  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
130  }
131  catch
132  {
133  try
134  {
135  logger.LogTrace("Deleting partially cloned repository...");
136  await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
137  }
138  catch (Exception e)
139  {
140  logger.LogDebug("Error deleting partially cloned repository! Exception: {0}", e);
141  }
142  throw;
143  }
144  else
145  {
146  logger.LogDebug("Repository exists, clone aborted!");
147  return null;
148  }
149  }
150  logger.LogInformation("Clone complete!");
151  }
152  finally
153  {
154  CloneInProgress = false;
155  }
156  return await LoadRepository(cancellationToken).ConfigureAwait(false);
157  }
-
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+
85  {
86  if (url == null)
87  throw new ArgumentNullException(nameof(url));
88  if (progressReporter == null)
89  throw new ArgumentNullException(nameof(progressReporter));
90 
91  logger.LogInformation("Begin clone {0} (Branch: {1})", url, initialBranch);
92  lock (this)
93  {
94  if (CloneInProgress)
95  throw new InvalidOperationException("The repository is already being cloned!");
96  CloneInProgress = true;
97  }
98  try
99  {
100  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
101  {
102  logger.LogTrace("Semaphore acquired");
103  if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
104  try
105  {
106  await Task.Factory.StartNew(() =>
107  {
108  string path = null;
109  try
110  {
111  path = LibGit2Sharp.Repository.Clone(url.ToString(), ioManager.ResolvePath("."), new CloneOptions
112  {
113  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
114  OnTransferProgress = (a) =>
115  {
116  var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
117  progressReporter((int)percentage);
118  return !cancellationToken.IsCancellationRequested;
119  },
120  RecurseSubmodules = true,
121  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
122  RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
123  BranchName = initialBranch,
124  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
125  });
126  }
127  catch (UserCancelledException) { }
128  cancellationToken.ThrowIfCancellationRequested();
129  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
130  }
131  catch
132  {
133  try
134  {
135  logger.LogTrace("Deleting partially cloned repository...");
136  await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
137  }
138  catch (Exception e)
139  {
140  logger.LogDebug("Error deleting partially cloned repository! Exception: {0}", e);
141  }
142  throw;
143  }
144  else
145  {
146  logger.LogDebug("Repository exists, clone aborted!");
147  return null;
148  }
149  }
150  logger.LogInformation("Clone complete!");
151  }
152  finally
153  {
154  CloneInProgress = false;
155  }
156  return await LoadRepository(cancellationToken).ConfigureAwait(false);
157  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
readonly ILogger< RepositoryManager > logger
The ILogger for the RepositoryManager
readonly SemaphoreSlim semaphore
Used for controlling single access to the IRepository
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_session_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_session_controller.html index da517b5b7f..e6baac1406 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_session_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_session_controller.html @@ -553,8 +553,7 @@ Private Attributes

Definition at line 277 of file SessionController.cs.

References Tgstation.Server.Host.Components.Interop.Constants.DMCommandApiValidate, Tgstation.Server.Host.Components.Interop.Constants.DMCommandChat, Tgstation.Server.Host.Components.Interop.Constants.DMCommandEndProcess, Tgstation.Server.Host.Components.Interop.Constants.DMCommandNewPort, Tgstation.Server.Host.Components.Interop.Constants.DMCommandServerPrimed, Tgstation.Server.Host.Components.Interop.Constants.DMCommandWorldReboot, Tgstation.Server.Host.Components.Interop.Constants.DMParameterCommand, Tgstation.Server.Host.Components.Interop.Constants.DMParameterData, Tgstation.Server.Host.Components.Interop.Constants.DMResponseSuccess, Tgstation.Server.Host.Components.Interop.Constants.DMTopicInteropResponse, Tgstation.Server.Host.Components.Interop.CommCommand.Parameters, and Tgstation.Server.Host.Components.Interop.CommCommand.RawJson.

-
278  {
279  if (command == null)
280  throw new ArgumentNullException(nameof(command));
281 
282  var query = command.Parameters;
283 
284  object content;
285  Action postRespond = null;
286  ushort? overrideResponsePort = null;
287  if (query.TryGetValue(Constants.DMParameterCommand, out var method))
288  {
289  content = new object();
290  switch (method)
291  {
293  try
294  {
295  var message = JsonConvert.DeserializeObject<Response>(command.RawJson, new JsonSerializerSettings
296  {
297  ContractResolver = new CamelCasePropertyNamesContractResolver()
298  });
299  if (message.ChannelIds == null)
300  throw new InvalidOperationException("Missing ChannelIds field!");
301  if (message.Message == null)
302  throw new InvalidOperationException("Missing Message field!");
303  await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false);
304  }
305  catch (Exception e)
306  {
307  logger.LogDebug("Exception while decoding chat message! Exception: {0}", e);
308  goto default;
309  }
310  break;
312  //currently unused, maybe in the future
313  break;
316  process.Terminate();
317  return;
319  lock (this)
320  {
321  if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort))
322  {
324  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
325  content = new ErrorMessage { Message = "Missing stringified port as data parameter!" };
326  break;
327  }
328 
329  if (!nextPort.HasValue)
330  //not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
331  reattachInformation.Port = currentPort;
332  else
333  {
334  //nextPort is ready, tell DD to switch to that
335  //if it fails it'll kill itself
336  content = new Dictionary<string, ushort> { { Constants.DMParameterData, nextPort.Value } };
338  overrideResponsePort = currentPort;
339  nextPort = null;
340 
341  //we'll also get here from SetPort so complete that task
342  var tmpTcs = portAssignmentTcs;
343  portAssignmentTcs = null;
344  if (tmpTcs != null)
345  postRespond = () => tmpTcs.SetResult(true);
346  }
347 
348  portClosedForReboot = false;
349  }
350  break;
352  if (!launchSecurityLevel.HasValue)
353  {
354  logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!");
355  apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest;
356  content = new ErrorMessage { Message = "Invalid API validation request!" };
357  break;
358  }
359  if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse<DreamDaemonSecurity>(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel))
360  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
361  else
362  switch (minimumSecurityLevel)
363  {
364  case DreamDaemonSecurity.Safe:
366  break;
367  case DreamDaemonSecurity.Ultrasafe:
368  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
369  break;
370  case DreamDaemonSecurity.Trusted:
371  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
372  break;
373  default:
374  throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!");
375  }
376  break;
378  if (ClosePortOnReboot)
379  {
381  content = new Dictionary<string, int> { { Constants.DMParameterData, 0 } };
382  portClosedForReboot = true;
383  }
384  var oldTcs = rebootTcs;
385  rebootTcs = new TaskCompletionSource<object>();
386  postRespond = () => oldTcs.SetResult(null);
387  break;
388  default:
389  content = new ErrorMessage { Message = "Requested command not supported!" };
390  break;
391  }
392  }
393  else
394  content = new ErrorMessage { Message = "Missing command parameter!" };
395 
396  var json = JsonConvert.SerializeObject(content);
397  var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false);
398 
399  if (response != Constants.DMResponseSuccess)
400  logger.LogWarning("Received error response while responding to interop: {0}", response);
401 
402  postRespond?.Invoke();
403  }
- +
278  {
279  if (command == null)
280  throw new ArgumentNullException(nameof(command));
281 
282  var query = command.Parameters;
283 
284  object content;
285  Action postRespond = null;
286  ushort? overrideResponsePort = null;
287  if (query.TryGetValue(Constants.DMParameterCommand, out var method))
288  {
289  content = new object();
290  switch (method)
291  {
293  try
294  {
295  var message = JsonConvert.DeserializeObject<Response>(command.RawJson, new JsonSerializerSettings
296  {
297  ContractResolver = new CamelCasePropertyNamesContractResolver()
298  });
299  if (message.ChannelIds == null)
300  throw new InvalidOperationException("Missing ChannelIds field!");
301  if (message.Message == null)
302  throw new InvalidOperationException("Missing Message field!");
303  await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false);
304  }
305  catch (Exception e)
306  {
307  logger.LogDebug("Exception while decoding chat message! Exception: {0}", e);
308  goto default;
309  }
310  break;
312  //currently unused, maybe in the future
313  break;
316  process.Terminate();
317  return;
319  lock (this)
320  {
321  if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort))
322  {
324  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
325  content = new ErrorMessage { Message = "Missing stringified port as data parameter!" };
326  break;
327  }
328 
329  if (!nextPort.HasValue)
330  //not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
331  reattachInformation.Port = currentPort;
332  else
333  {
334  //nextPort is ready, tell DD to switch to that
335  //if it fails it'll kill itself
336  content = new Dictionary<string, ushort> { { Constants.DMParameterData, nextPort.Value } };
338  overrideResponsePort = currentPort;
339  nextPort = null;
340 
341  //we'll also get here from SetPort so complete that task
342  var tmpTcs = portAssignmentTcs;
343  portAssignmentTcs = null;
344  if (tmpTcs != null)
345  postRespond = () => tmpTcs.SetResult(true);
346  }
347 
348  portClosedForReboot = false;
349  }
350  break;
352  if (!launchSecurityLevel.HasValue)
353  {
354  logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!");
355  apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest;
356  content = new ErrorMessage { Message = "Invalid API validation request!" };
357  break;
358  }
359  if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse<DreamDaemonSecurity>(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel))
360  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
361  else
362  switch (minimumSecurityLevel)
363  {
364  case DreamDaemonSecurity.Safe:
366  break;
367  case DreamDaemonSecurity.Ultrasafe:
368  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
369  break;
370  case DreamDaemonSecurity.Trusted:
371  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
372  break;
373  default:
374  throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!");
375  }
376  break;
378  if (ClosePortOnReboot)
379  {
381  content = new Dictionary<string, int> { { Constants.DMParameterData, 0 } };
382  portClosedForReboot = true;
383  }
384  var oldTcs = rebootTcs;
385  rebootTcs = new TaskCompletionSource<object>();
386  postRespond = () => oldTcs.SetResult(null);
387  break;
388  default:
389  content = new ErrorMessage { Message = "Requested command not supported!" };
390  break;
391  }
392  }
393  else
394  content = new ErrorMessage { Message = "Missing command parameter!" };
395 
396  var json = JsonConvert.SerializeObject(content);
397  var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false);
398 
399  if (response != Constants.DMResponseSuccess)
400  logger.LogWarning("Received error response while responding to interop: {0}", response);
401 
402  postRespond?.Invoke();
403  }
IReadOnlyDictionary< string, object > Parameters
The dictionary of the CommCommand
Definition: CommCommand.cs:13
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
@@ -717,8 +716,7 @@ Private Attributes

Definition at line 435 of file SessionController.cs.

References Tgstation.Server.Host.Components.Interop.Constants.DMInteropAccessIdentifier, and Tgstation.Server.Host.Components.Interop.Constants.DMParameterCommand.

-
436  {
437  try
438  {
439  var commandString = String.Format(CultureInfo.InvariantCulture,
440  "?{0}={1}&{2}={3}",
444  //intentionally don't sanitize command, that's up to the caller
445  command);
446 
447  var targetPort = overridePort ?? reattachInformation.Port;
448  logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString);
449 
450  return await byondTopicSender.SendTopic(
451  new IPEndPoint(IPAddress.Loopback, targetPort),
452  commandString,
453  cancellationToken).ConfigureAwait(false);
454  }
455  catch (OperationCanceledException)
456  {
457  throw;
458  }
459  catch (Exception e)
460  {
461  logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message);
462  return null;
463  }
464  }
-
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
+
436  {
437  try
438  {
439  var commandString = String.Format(CultureInfo.InvariantCulture,
440  "?{0}={1}&{2}={3}",
444  //intentionally don't sanitize command, that's up to the caller
445  command);
446 
447  var targetPort = overridePort ?? reattachInformation.Port;
448  logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString);
449 
450  return await byondTopicSender.SendTopic(
451  new IPEndPoint(IPAddress.Loopback, targetPort),
452  commandString,
453  cancellationToken).ConfigureAwait(false);
454  }
455  catch (OperationCanceledException)
456  {
457  throw;
458  }
459  catch (Exception e)
460  {
461  logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message);
462  return null;
463  }
464  }
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
ushort Port
The port DreamDaemon was last listening on
readonly ILogger< SessionController > logger
The ILogger for the SessionController
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog.html index 3f59b5f3cd..036cbdbff3 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog.html @@ -724,8 +724,7 @@ Private Attributes

Definition at line 256 of file Watchdog.cs.

References Tgstation.Server.Host.Components.Watchdog.MonitorState.ActiveServer, Tgstation.Server.Host.Components.Watchdog.ISessionController.ClosePortOnReboot, Tgstation.Server.Host.Components.IDmbProvider.CompileJob, Tgstation.Server.Host.Components.Watchdog.ISessionController.Dmb, Tgstation.Server.Host.Components.Watchdog.ISessionController.EnableCustomChatCommands(), Tgstation.Server.Api.Models.Internal.CompileJob.Id, Tgstation.Server.Host.Components.Watchdog.MonitorState.InactiveServer, Tgstation.Server.Host.Components.Watchdog.MonitorState.InactiveServerCritFail, Tgstation.Server.Host.Components.Watchdog.ISessionController.IsPrimary, Tgstation.Server.Host.Components.Watchdog.MonitorState.NextAction, Tgstation.Server.Host.Components.Watchdog.MonitorState.RebootingInactiveServer, Tgstation.Server.Host.Components.Watchdog.ISessionController.RebootState, Tgstation.Server.Host.Components.Watchdog.ISessionController.ResetRebootState(), Tgstation.Server.Host.Core.IProcessBase.SetHighPriority(), Tgstation.Server.Host.Components.Watchdog.ISessionController.SetPort(), and Tgstation.Server.Host.Components.Watchdog.ISessionController.TerminationWasRequested.

-
257  {
258  logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
259 
260  //this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
261  //I'll do my best to walk you through it
262 
263  //returns true if the inactive server can't be used immediately
264  //also sets monitor to restart if the above holds
265  bool FullRestartDeadInactive()
266  {
267  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
268  {
269  logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
270  monitorState.NextAction = MonitorAction.Restart; //will dispose server
271  return true;
272  }
273  return false;
274  };
275 
276  //trys to set inactive server's port to the public game port
277  //doesn't handle closing active server's port
278  //returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
279  //on failure, sets monitor to restart
280  async Task<bool> MakeInactiveActive()
281  {
282  logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
283  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
284 
285  if (!result)
286  {
287  logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
288  monitorState.NextAction = MonitorAction.Restart; //will dispose server
289  return false;
290  }
291 
292  //inactive server should always be using active launch parameters
294 
295  var tmp = monitorState.ActiveServer;
296  monitorState.ActiveServer = monitorState.InactiveServer;
297  monitorState.InactiveServer = tmp;
299  monitorState.ActiveServer.EnableCustomChatCommands();
300  return true;
301  }
302 
303  // Kills and tries to launch inactive server with the latest dmb
304  // falls back to current dmb on failure
305  // Sets critfail on inactive server failing that
306  // returns false if the backup dmb was used successfully, true otherwise
307  async Task UpdateAndRestartInactiveServer(bool breakAfter)
308  {
309  activeParametersUpdated = new TaskCompletionSource<object>();
310  monitorState.InactiveServer.Dispose(); //kill or recycle it
311  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
312  monitorState.NextAction = desiredNextAction;
313 
314  logger.LogInformation("Rebooting inactive server...");
315  var newDmb = dmbFactory.LockNextDmb(1);
316  try
317  {
318  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, newDmb, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
319  monitorState.InactiveServer.SetHighPriority();
320  }
321  catch (OperationCanceledException)
322  {
323  throw;
324  }
325  catch (Exception e)
326  {
327  logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
328  //ahh jeez, what do we do here?
329  //this is our fault, so it should never happen but
330  //idk maybe a database error while handling the newest dmb?
331  //either way try to start it using the active server's dmb as a backup
332  try
333  {
334  var dmbBackup = await dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
335 
336  if (dmbBackup == null) //NANI!?
337  //just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
338  throw new JobException("Creating backup DMB provider failed!");
339 
340  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
341  monitorState.InactiveServer.SetHighPriority();
342  await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
343  }
344  catch (OperationCanceledException)
345  {
346  throw;
347  }
348  catch (Exception e2)
349  {
350  //fuuuuucckkk
351  logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
352  monitorState.InactiveServerCritFail = true;
353  await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
354  return;
355  }
356  }
357 
358  logger.LogInformation("Successfully relaunched inactive server!");
359  monitorState.RebootingInactiveServer = true;
360  }
361 
362  string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
363 
364  //reason handling
365  switch (activationReason)
366  {
367  case MonitorActivationReason.ActiveServerCrashed:
368  if (monitorState.ActiveServer.RebootState == Components.Watchdog.RebootState.Shutdown)
369  {
370  //the time for graceful shutdown is now
371  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Exiting due to graceful termination request...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
373  monitorState.NextAction = MonitorAction.Exit;
374  break;
375  }
376 
377  if (FullRestartDeadInactive())
378  {
379  //tell chat about it and go ahead
380  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Inactive server unable to online!", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
381  //we've already been set to restart
382  break;
383  }
384 
385  //tell chat about it
386  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Onlining inactive server...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
387 
388  //try to active the inactive server
389  if (!await MakeInactiveActive().ConfigureAwait(false))
390  //failing that, we've already been set to restart
391  break;
392 
393  //bring up another inactive server
394  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
395  break;
396  case MonitorActivationReason.InactiveServerCrashed:
397  //just announce and try to bring it back
398  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Inactive server {0}! Rebooting...", ExitWord(monitorState.InactiveServer)), cancellationToken).ConfigureAwait(false);
399  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
400  break;
401  case MonitorActivationReason.ActiveServerRebooted:
402  //ideal goal: active server just closed its port
403  //tell inactive server to open it's port and that's now the active server
404  var rebootState = monitorState.ActiveServer.RebootState;
405  monitorState.ActiveServer.ResetRebootState(); //the DMAPI has already done this internally
406 
407  if (FullRestartDeadInactive() && rebootState != Components.Watchdog.RebootState.Shutdown)
408  //full restart if the inactive server is being fucky
409  break;
410 
411  //what matters here is the RebootState
412  var restartOnceSwapped = false;
413 
414  switch (rebootState)
415  {
416  case Components.Watchdog.RebootState.Normal:
417  //life as normal
418  break;
419  case Components.Watchdog.RebootState.Restart:
420  //reboot the current active server once the inactive one activates
421  restartOnceSwapped = true;
422  break;
423  case Components.Watchdog.RebootState.Shutdown:
424  //graceful shutdown time
425  await chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
427  monitorState.NextAction = MonitorAction.Exit;
428  return;
429  }
430 
431  //are both servers now running the same CompileJob?
432  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
433 
434  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
435  //need a new launch to update either settings or compile job
436  restartOnceSwapped = true;
437 
438  if (restartOnceSwapped)
439  //we need to manually restart active server
440  //just kill it here, easier that way
441  monitorState.ActiveServer.Dispose();
442 
443  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
444 
445  if (activeServerStillHasPortOpen)
446  //we didn't want active server to swap for some reason and it still has it's port open
447  //just continue as normal
448  break;
449 
450  if (!await MakeInactiveActive().ConfigureAwait(false))
451  //monitor will restart
452  break;
453 
454  //servers now swapped
455 
456  //enable this now if inactive server is not still valid
457  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
458 
459  if (!restartOnceSwapped)
460  //now try to reopen it on the private port
461  //failing that, just reboot it
462  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
463 
464  //break either way because any issues past this point would be solved by the reboot
465  if (restartOnceSwapped)
466  //for one reason or another
467  //update and reboot
468  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
469  else
470  //only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
471  monitorState.NextAction = MonitorAction.Skip;
472  break;
473  case MonitorActivationReason.InactiveServerRebooted:
474  //just don't let the active server close it's port if the inactive server isn't ready
475  monitorState.RebootingInactiveServer = true;
476  monitorState.InactiveServer.ResetRebootState();
477  monitorState.ActiveServer.ClosePortOnReboot = false;
478  monitorState.NextAction = MonitorAction.Continue;
479  break;
480  case MonitorActivationReason.InactiveServerStartupComplete:
481  //opposite of above case
482  monitorState.RebootingInactiveServer = false;
483  monitorState.ActiveServer.ClosePortOnReboot = true;
484  monitorState.NextAction = MonitorAction.Continue;
485  break;
486  case MonitorActivationReason.NewDmbAvailable:
487  case MonitorActivationReason.ActiveLaunchParametersUpdated:
488  //just reload the inactive server and wait for a swap to apply the changes
489  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
490  break;
491  }
492  }
- +
257  {
258  logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
259 
260  //this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
261  //I'll do my best to walk you through it
262 
263  //returns true if the inactive server can't be used immediately
264  //also sets monitor to restart if the above holds
265  bool FullRestartDeadInactive()
266  {
267  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
268  {
269  logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
270  monitorState.NextAction = MonitorAction.Restart; //will dispose server
271  return true;
272  }
273  return false;
274  };
275 
276  //trys to set inactive server's port to the public game port
277  //doesn't handle closing active server's port
278  //returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
279  //on failure, sets monitor to restart
280  async Task<bool> MakeInactiveActive()
281  {
282  logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
283  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
284 
285  if (!result)
286  {
287  logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
288  monitorState.NextAction = MonitorAction.Restart; //will dispose server
289  return false;
290  }
291 
292  //inactive server should always be using active launch parameters
294 
295  var tmp = monitorState.ActiveServer;
296  monitorState.ActiveServer = monitorState.InactiveServer;
297  monitorState.InactiveServer = tmp;
299  monitorState.ActiveServer.EnableCustomChatCommands();
300  return true;
301  }
302 
303  // Kills and tries to launch inactive server with the latest dmb
304  // falls back to current dmb on failure
305  // Sets critfail on inactive server failing that
306  // returns false if the backup dmb was used successfully, true otherwise
307  async Task UpdateAndRestartInactiveServer(bool breakAfter)
308  {
309  activeParametersUpdated = new TaskCompletionSource<object>();
310  monitorState.InactiveServer.Dispose(); //kill or recycle it
311  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
312  monitorState.NextAction = desiredNextAction;
313 
314  logger.LogInformation("Rebooting inactive server...");
315  var newDmb = dmbFactory.LockNextDmb(1);
316  try
317  {
318  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, newDmb, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
319  monitorState.InactiveServer.SetHighPriority();
320  }
321  catch (OperationCanceledException)
322  {
323  throw;
324  }
325  catch (Exception e)
326  {
327  logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
328  //ahh jeez, what do we do here?
329  //this is our fault, so it should never happen but
330  //idk maybe a database error while handling the newest dmb?
331  //either way try to start it using the active server's dmb as a backup
332  try
333  {
334  var dmbBackup = await dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
335 
336  if (dmbBackup == null) //NANI!?
337  //just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
338  throw new JobException("Creating backup DMB provider failed!");
339 
340  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
341  monitorState.InactiveServer.SetHighPriority();
342  await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
343  }
344  catch (OperationCanceledException)
345  {
346  throw;
347  }
348  catch (Exception e2)
349  {
350  //fuuuuucckkk
351  logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
352  monitorState.InactiveServerCritFail = true;
353  await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
354  return;
355  }
356  }
357 
358  logger.LogInformation("Successfully relaunched inactive server!");
359  monitorState.RebootingInactiveServer = true;
360  }
361 
362  string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
363 
364  //reason handling
365  switch (activationReason)
366  {
367  case MonitorActivationReason.ActiveServerCrashed:
368  if (monitorState.ActiveServer.RebootState == Components.Watchdog.RebootState.Shutdown)
369  {
370  //the time for graceful shutdown is now
371  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Exiting due to graceful termination request...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
373  monitorState.NextAction = MonitorAction.Exit;
374  break;
375  }
376 
377  if (FullRestartDeadInactive())
378  {
379  //tell chat about it and go ahead
380  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Inactive server unable to online!", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
381  //we've already been set to restart
382  break;
383  }
384 
385  //tell chat about it
386  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Onlining inactive server...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
387 
388  //try to active the inactive server
389  if (!await MakeInactiveActive().ConfigureAwait(false))
390  //failing that, we've already been set to restart
391  break;
392 
393  //bring up another inactive server
394  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
395  break;
396  case MonitorActivationReason.InactiveServerCrashed:
397  //just announce and try to bring it back
398  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Inactive server {0}! Rebooting...", ExitWord(monitorState.InactiveServer)), cancellationToken).ConfigureAwait(false);
399  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
400  break;
401  case MonitorActivationReason.ActiveServerRebooted:
402  //ideal goal: active server just closed its port
403  //tell inactive server to open it's port and that's now the active server
404  var rebootState = monitorState.ActiveServer.RebootState;
405  monitorState.ActiveServer.ResetRebootState(); //the DMAPI has already done this internally
406 
407  if (FullRestartDeadInactive() && rebootState != Components.Watchdog.RebootState.Shutdown)
408  //full restart if the inactive server is being fucky
409  break;
410 
411  //what matters here is the RebootState
412  var restartOnceSwapped = false;
413 
414  switch (rebootState)
415  {
416  case Components.Watchdog.RebootState.Normal:
417  //life as normal
418  break;
419  case Components.Watchdog.RebootState.Restart:
420  //reboot the current active server once the inactive one activates
421  restartOnceSwapped = true;
422  break;
423  case Components.Watchdog.RebootState.Shutdown:
424  //graceful shutdown time
425  await chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
427  monitorState.NextAction = MonitorAction.Exit;
428  return;
429  }
430 
431  //are both servers now running the same CompileJob?
432  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
433 
434  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
435  //need a new launch to update either settings or compile job
436  restartOnceSwapped = true;
437 
438  if (restartOnceSwapped)
439  //we need to manually restart active server
440  //just kill it here, easier that way
441  monitorState.ActiveServer.Dispose();
442 
443  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
444 
445  if (activeServerStillHasPortOpen)
446  //we didn't want active server to swap for some reason and it still has it's port open
447  //just continue as normal
448  break;
449 
450  if (!await MakeInactiveActive().ConfigureAwait(false))
451  //monitor will restart
452  break;
453 
454  //servers now swapped
455 
456  //enable this now if inactive server is not still valid
457  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
458 
459  if (!restartOnceSwapped)
460  //now try to reopen it on the private port
461  //failing that, just reboot it
462  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
463 
464  //break either way because any issues past this point would be solved by the reboot
465  if (restartOnceSwapped)
466  //for one reason or another
467  //update and reboot
468  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
469  else
470  //only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
471  monitorState.NextAction = MonitorAction.Skip;
472  break;
473  case MonitorActivationReason.InactiveServerRebooted:
474  //just don't let the active server close it's port if the inactive server isn't ready
475  monitorState.RebootingInactiveServer = true;
476  monitorState.InactiveServer.ResetRebootState();
477  monitorState.ActiveServer.ClosePortOnReboot = false;
478  monitorState.NextAction = MonitorAction.Continue;
479  break;
480  case MonitorActivationReason.InactiveServerStartupComplete:
481  //opposite of above case
482  monitorState.RebootingInactiveServer = false;
483  monitorState.ActiveServer.ClosePortOnReboot = true;
484  monitorState.NextAction = MonitorAction.Continue;
485  break;
486  case MonitorActivationReason.NewDmbAvailable:
487  case MonitorActivationReason.ActiveLaunchParametersUpdated:
488  //just reload the inactive server and wait for a swap to apply the changes
489  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
490  break;
491  }
492  }
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:101
Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
void DisposeAndNullControllers()
Call IDisposable.Dispose on alphaServer and bravoServer and set them to
Definition: Watchdog.cs:213
@@ -833,8 +832,7 @@ Private Attributes

Definition at line 697 of file Watchdog.cs.

References Tgstation.Server.Host.Components.Watchdog.WatchdogReattachInformation.Alpha, Tgstation.Server.Host.Models.WatchdogReattachInformationBase.AlphaIsActive, Tgstation.Server.Host.Components.Watchdog.WatchdogReattachInformation.Bravo, Tgstation.Server.Host.Components.Watchdog.ReattachInformation.Dmb, Tgstation.Server.Host.Components.Watchdog.ISessionController.EnableCustomChatCommands(), Tgstation.Server.Host.Components.Watchdog.ISessionController.LaunchResult, and Tgstation.Server.Host.Core.IProcessBase.SetHighPriority().

-
698  {
699  logger.LogTrace("Begin LaunchNoLock");
700 
701  if (Running)
702  throw new JobException("Watchdog already running!");
703 
704  Task chatTask;
705  //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
706  if (startMonitor && await StopMonitor().ConfigureAwait(false))
707  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
708  else if (announce)
709  //simple announce
710  chatTask = chat.SendWatchdogMessage(reattachInfo == null ? "Starting..." : "Reattaching...", cancellationToken);
711  else
712  //no announce
713  chatTask = Task.CompletedTask;
714 
715  //since neither server is running, this is safe to do
717 
718  //for when we call ourself and want to not catch thrown exceptions
719  var ignoreNestedException = false;
720  try
721  {
722  //good ole sanity, should never fucking trigger but i don't trust myself even though I should
723  if (alphaServer != null || bravoServer != null)
724  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
725 
726  //don't need a new dmb if reattaching
727  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
728  var dmbToUse = doesntNeedNewDmb ? null : dmbFactory.LockNextDmb(2);
729 
730  //if this try catches something, both servers are killed
731  try
732  {
733  //start the alpha server task, either by launch a new process or attaching to an existing one
734  //The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
735  //The tasks pertaining to server startup times are in the ISessionControllers
736  Task<ISessionController> alphaServerTask;
737  if (!doesntNeedNewDmb)
738  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken);
739  else
740  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
741 
742  //retrieve the session controller
743  var startTime = DateTimeOffset.Now;
744  alphaServer = await alphaServerTask.ConfigureAwait(false);
745  //failed reattaches will return null
747 
748  //extra delay for total ordering
749  var now = DateTimeOffset.Now;
750  var delay = now - startTime;
751 
752  //definitely not if reattaching though
753  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
754  await asyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
755 
756  //now bring bravo up
757  if (!doesntNeedNewDmb)
758  bravoServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken).ConfigureAwait(false);
759  else
760  bravoServer = await sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
761  //failed reattaches will return null
763 
764  //possiblity of null servers due to failed reattaches
765  if (alphaServer == null || bravoServer == null)
766  {
767  await chatTask.ConfigureAwait(false);
768  var bothServersDead = alphaServer == null && bravoServer == null;
769  if (bothServersDead
770  || (alphaServer == null && reattachInfo.AlphaIsActive)
771  || (bravoServer == null && !reattachInfo.AlphaIsActive))
772  {
773  //we lost the active server, just restart entirely
775  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
776  logger.LogWarning(FailReattachMessage);
777  logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
778  chatTask = chat.SendWatchdogMessage(FailReattachMessage, cancellationToken);
779  ignoreNestedException = true;
780  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
781  return;
782  }
783 
784  //we still have the active server but the other one is dead to us, hand it off to the monitor to restart
785  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
786  chatTask = chat.SendWatchdogMessage(InactiveReattachFailureMessage, cancellationToken);
787  logger.LogWarning(InactiveReattachFailureMessage);
788 
789  if (reattachInfo.AlphaIsActive)
790  bravoServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
791  else
792  alphaServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
793  }
794 
795  //throws a JobException if something went wrong with a launch
796  //Dead sessions won't trigger this
797  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
798  {
799  var launch = await controller.LaunchResult.ConfigureAwait(false);
800  if (launch.ExitCode.HasValue)
801  //you killed us ray...
802  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
803  if (!launch.StartupTime.HasValue)
804  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server timed out on startup: {0}s", launch.ToString(), ActiveLaunchParameters.StartupTimeout.Value));
805  return launch;
806  }
807 
808  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
809  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
810  //this task completes when both serers have finished booting
811  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
812 
813  //don't forget about the cancellationToken
814  var cancelTcs = new TaskCompletionSource<object>();
815  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
816  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
817  cancellationToken.ThrowIfCancellationRequested();
818 
819  await allTask.ConfigureAwait(false);
820 
821  //both servers are now running, alpha is the active server(unless reattach), huzzah
822  AlphaIsActive = reattachInfo?.AlphaIsActive ?? true;
823 
824  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
825  activeServer.EnableCustomChatCommands();
826  activeServer.ClosePortOnReboot = true;
827 
828  logger.LogInformation("Launched servers successfully");
829  Running = true;
830 
831  if (startMonitor)
832  {
833  monitorCts = new CancellationTokenSource();
835  }
836  }
837  catch
838  {
839  if (dmbToUse != null)
840  {
841  //we locked 2 dmbs
842  if (bravoServer == null)
843  {
844  //bravo didn't get control of his
845  dmbToUse.Dispose();
846  if (alphaServer == null)
847  //alpha didn't get control of his
848  dmbToUse.Dispose();
849  }
850  }
851  else if (doesntNeedNewDmb)
852  //we have reattachInfo
853  if (bravoServer == null)
854  {
855  //bravo didn't get control of his
856  reattachInfo.Bravo?.Dmb.Dispose();
857  if (alphaServer == null)
858  //alpha didn't get control of his
859  reattachInfo.Alpha?.Dmb.Dispose();
860  }
861  //kill the controllers
863  throw;
864  }
865  }
866  catch (Exception e)
867  {
868  //don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
869  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
870  {
871  var originalChatTask = chatTask;
872  async Task ChainChatTaskWithErrorMessage()
873  {
874  await originalChatTask.ConfigureAwait(false);
875  await chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false);
876  }
877  chatTask = ChainChatTaskWithErrorMessage();
878  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
879  }
880  throw;
881  }
882  finally
883  {
884  //finish the chat task that's in flight
885  try
886  {
887  await chatTask.ConfigureAwait(false);
888  }
889  catch (OperationCanceledException) { }
890  }
891  }
- +
698  {
699  logger.LogTrace("Begin LaunchNoLock");
700 
701  if (Running)
702  throw new JobException("Watchdog already running!");
703 
704  Task chatTask;
705  //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
706  if (startMonitor && await StopMonitor().ConfigureAwait(false))
707  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
708  else if (announce)
709  //simple announce
710  chatTask = chat.SendWatchdogMessage(reattachInfo == null ? "Starting..." : "Reattaching...", cancellationToken);
711  else
712  //no announce
713  chatTask = Task.CompletedTask;
714 
715  //since neither server is running, this is safe to do
717 
718  //for when we call ourself and want to not catch thrown exceptions
719  var ignoreNestedException = false;
720  try
721  {
722  //good ole sanity, should never fucking trigger but i don't trust myself even though I should
723  if (alphaServer != null || bravoServer != null)
724  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
725 
726  //don't need a new dmb if reattaching
727  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
728  var dmbToUse = doesntNeedNewDmb ? null : dmbFactory.LockNextDmb(2);
729 
730  //if this try catches something, both servers are killed
731  try
732  {
733  //start the alpha server task, either by launch a new process or attaching to an existing one
734  //The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
735  //The tasks pertaining to server startup times are in the ISessionControllers
736  Task<ISessionController> alphaServerTask;
737  if (!doesntNeedNewDmb)
738  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken);
739  else
740  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
741 
742  //retrieve the session controller
743  var startTime = DateTimeOffset.Now;
744  alphaServer = await alphaServerTask.ConfigureAwait(false);
745  //failed reattaches will return null
747 
748  //extra delay for total ordering
749  var now = DateTimeOffset.Now;
750  var delay = now - startTime;
751 
752  //definitely not if reattaching though
753  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
754  await asyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
755 
756  //now bring bravo up
757  if (!doesntNeedNewDmb)
758  bravoServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken).ConfigureAwait(false);
759  else
760  bravoServer = await sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
761  //failed reattaches will return null
763 
764  //possiblity of null servers due to failed reattaches
765  if (alphaServer == null || bravoServer == null)
766  {
767  await chatTask.ConfigureAwait(false);
768  var bothServersDead = alphaServer == null && bravoServer == null;
769  if (bothServersDead
770  || (alphaServer == null && reattachInfo.AlphaIsActive)
771  || (bravoServer == null && !reattachInfo.AlphaIsActive))
772  {
773  //we lost the active server, just restart entirely
775  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
776  logger.LogWarning(FailReattachMessage);
777  logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
778  chatTask = chat.SendWatchdogMessage(FailReattachMessage, cancellationToken);
779  ignoreNestedException = true;
780  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
781  return;
782  }
783 
784  //we still have the active server but the other one is dead to us, hand it off to the monitor to restart
785  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
786  chatTask = chat.SendWatchdogMessage(InactiveReattachFailureMessage, cancellationToken);
787  logger.LogWarning(InactiveReattachFailureMessage);
788 
789  if (reattachInfo.AlphaIsActive)
790  bravoServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
791  else
792  alphaServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
793  }
794 
795  //throws a JobException if something went wrong with a launch
796  //Dead sessions won't trigger this
797  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
798  {
799  var launch = await controller.LaunchResult.ConfigureAwait(false);
800  if (launch.ExitCode.HasValue)
801  //you killed us ray...
802  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
803  if (!launch.StartupTime.HasValue)
804  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server timed out on startup: {0}s", launch.ToString(), ActiveLaunchParameters.StartupTimeout.Value));
805  return launch;
806  }
807 
808  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
809  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
810  //this task completes when both serers have finished booting
811  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
812 
813  //don't forget about the cancellationToken
814  var cancelTcs = new TaskCompletionSource<object>();
815  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
816  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
817  cancellationToken.ThrowIfCancellationRequested();
818 
819  await allTask.ConfigureAwait(false);
820 
821  //both servers are now running, alpha is the active server(unless reattach), huzzah
822  AlphaIsActive = reattachInfo?.AlphaIsActive ?? true;
823 
824  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
825  activeServer.EnableCustomChatCommands();
826  activeServer.ClosePortOnReboot = true;
827 
828  logger.LogInformation("Launched servers successfully");
829  Running = true;
830 
831  if (startMonitor)
832  {
833  monitorCts = new CancellationTokenSource();
835  }
836  }
837  catch
838  {
839  if (dmbToUse != null)
840  {
841  //we locked 2 dmbs
842  if (bravoServer == null)
843  {
844  //bravo didn't get control of his
845  dmbToUse.Dispose();
846  if (alphaServer == null)
847  //alpha didn't get control of his
848  dmbToUse.Dispose();
849  }
850  }
851  else if (doesntNeedNewDmb)
852  //we have reattachInfo
853  if (bravoServer == null)
854  {
855  //bravo didn't get control of his
856  reattachInfo.Bravo?.Dmb.Dispose();
857  if (alphaServer == null)
858  //alpha didn't get control of his
859  reattachInfo.Alpha?.Dmb.Dispose();
860  }
861  //kill the controllers
863  throw;
864  }
865  }
866  catch (Exception e)
867  {
868  //don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
869  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
870  {
871  var originalChatTask = chatTask;
872  async Task ChainChatTaskWithErrorMessage()
873  {
874  await originalChatTask.ConfigureAwait(false);
875  await chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false);
876  }
877  chatTask = ChainChatTaskWithErrorMessage();
878  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
879  }
880  throw;
881  }
882  finally
883  {
884  //finish the chat task that's in flight
885  try
886  {
887  await chatTask.ConfigureAwait(false);
888  }
889  catch (OperationCanceledException) { }
890  }
891  }
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:101
Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
ISessionController CreateDeadSession(IDmbProvider dmbProvider)
Creates a ISessionController that appears to have started and died with exit code -1 ...
@@ -900,8 +898,7 @@ Private Attributes

Definition at line 499 of file Watchdog.cs.

References Tgstation.Server.Host.Core.IProcessBase.Lifetime, Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock(), and Tgstation.Server.Host.Components.Watchdog.MonitorState.NextAction.

-
500  {
501  logger.LogTrace("Entered MonitorLifetimes");
502 
503  //this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
504 
505  var iteration = 1;
506  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
507  {
508  //always start out with continue
509  monitorState.NextAction = MonitorAction.Continue;
510 
511  //dump some info to the logs
512  logger.LogDebug("Iteration {0} of monitor loop", iteration);
513  try
514  {
515  if (AlphaIsActive)
516  logger.LogDebug("Alpha is the active server");
517  else
518  logger.LogDebug("Bravo is the active server");
519 
520 
521  if (monitorState.RebootingInactiveServer)
522  logger.LogDebug("Inactive server is rebooting");
523 
524  //update the monitor state with the inactive/active servers
525  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
526  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
527 
528  if (monitorState.ActiveServer.ClosePortOnReboot)
529  logger.LogDebug("Active server will close port on reboot");
530  if (monitorState.InactiveServer.ClosePortOnReboot)
531  logger.LogDebug("Inactive server will close port on reboot");
532 
533  logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
534  logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
535 
536  //load the activation tasks into local variables
537  Task activeServerLifetime = monitorState.ActiveServer.Lifetime;
538  Task inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
539  var activeServerReboot = monitorState.ActiveServer.OnReboot;
540  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
541  Task inactiveServerStartup = monitorState.RebootingInactiveServer ? monitorState.InactiveServer.LaunchResult : null;
542  Task activeLaunchParametersChanged = activeParametersUpdated.Task;
543  var newDmbAvailable = dmbFactory.OnNewerDmb;
544 
545  //cancel waiting if requested
546  var cancelTcs = new TaskCompletionSource<object>();
547  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
548  {
549  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
550  if (monitorState.RebootingInactiveServer)
551  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
552  //wait for something to happen
553  await toWaitOn.ConfigureAwait(false);
554  cancellationToken.ThrowIfCancellationRequested();
555  }
556 
557  var chatTask = Task.CompletedTask;
558  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
559  {
560  //always run HandleMonitorWakeup from the context of the semaphore lock
561  //multiple things may have happened, handle them one at a time
562  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
563  {
564  MonitorActivationReason activationReason = default; //this will always be assigned before being used
565 
566  //process the tasks in this order and call HandlerMonitorWakup for each
567 
568  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
569  {
570  var taskCompleted = task?.IsCompleted == true;
571  task = null;
572  if (monitorState.NextAction == MonitorAction.Skip)
573  monitorState.NextAction = MonitorAction.Continue;
574  else if (taskCompleted)
575  {
576  activationReason = testActivationReason;
577  return true;
578  }
579  return false;
580  };
581 
582  if (CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
583  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
584  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
585  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
586  || CheckActivationReason(ref inactiveServerStartup, MonitorActivationReason.InactiveServerStartupComplete)
587  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
588  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated))
589  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
590  else
591  moreActivationsToProcess = false;
592  }
593 
594  //writeback alphaServer and bravoServer from monitor state in case they changesd
595  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
596  bravoServer = !AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
597  }
598 
599  //full reboot required
600  if (monitorState.NextAction == MonitorAction.Restart)
601  {
602  logger.LogDebug("Next state action is to restart");
604  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
605 
606  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
607  {
608  Exception launchException = null;
609  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
610  try
611  {
612  //use LaunchImplNoLock without announcements or restarting the monitor
613  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
614  if (Running)
615  {
616  logger.LogDebug("Relaunch successful, resetting monitor state...");
617  monitorState = new MonitorState(); //clean the slate and continue
618  }
619  }
620  catch (OperationCanceledException)
621  {
622  throw;
623  }
624  catch (Exception e)
625  {
626  launchException = e;
627  }
628 
629  await chatTask.ConfigureAwait(false);
630  if (!Running)
631  {
632  if (launchException == null)
633  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
634  else
635  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
636  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); //max of one hour, increasing by a power of 2 each time
637  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
638  await Task.WhenAll(asyncDelayer.Delay(TimeSpan.FromSeconds(retryDelay), cancellationToken), chatTask).ConfigureAwait(false);
639  }
640  }
641  }
642  }
643  catch (OperationCanceledException)
644  {
645  logger.LogDebug("Monitor cancelled");
646  break;
647  }
648  catch (Exception e)
649  {
650  //really, this should NEVER happen
651  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}, Exception: {2}", iteration, JsonConvert.SerializeObject(monitorState), e);
652  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Monitor crashed, this should NEVER happen! Please report this, full details in logs! Restarting monitor... Error: {0}", e.Message), cancellationToken).ConfigureAwait(false);
653  }
654  }
655  logger.LogTrace("Monitor exiting...");
656  }
- +
500  {
501  logger.LogTrace("Entered MonitorLifetimes");
502 
503  //this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
504 
505  var iteration = 1;
506  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
507  {
508  //always start out with continue
509  monitorState.NextAction = MonitorAction.Continue;
510 
511  //dump some info to the logs
512  logger.LogDebug("Iteration {0} of monitor loop", iteration);
513  try
514  {
515  if (AlphaIsActive)
516  logger.LogDebug("Alpha is the active server");
517  else
518  logger.LogDebug("Bravo is the active server");
519 
520 
521  if (monitorState.RebootingInactiveServer)
522  logger.LogDebug("Inactive server is rebooting");
523 
524  //update the monitor state with the inactive/active servers
525  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
526  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
527 
528  if (monitorState.ActiveServer.ClosePortOnReboot)
529  logger.LogDebug("Active server will close port on reboot");
530  if (monitorState.InactiveServer.ClosePortOnReboot)
531  logger.LogDebug("Inactive server will close port on reboot");
532 
533  logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
534  logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
535 
536  //load the activation tasks into local variables
537  Task activeServerLifetime = monitorState.ActiveServer.Lifetime;
538  Task inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
539  var activeServerReboot = monitorState.ActiveServer.OnReboot;
540  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
541  Task inactiveServerStartup = monitorState.RebootingInactiveServer ? monitorState.InactiveServer.LaunchResult : null;
542  Task activeLaunchParametersChanged = activeParametersUpdated.Task;
543  var newDmbAvailable = dmbFactory.OnNewerDmb;
544 
545  //cancel waiting if requested
546  var cancelTcs = new TaskCompletionSource<object>();
547  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
548  {
549  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
550  if (monitorState.RebootingInactiveServer)
551  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
552  //wait for something to happen
553  await toWaitOn.ConfigureAwait(false);
554  cancellationToken.ThrowIfCancellationRequested();
555  }
556 
557  var chatTask = Task.CompletedTask;
558  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
559  {
560  //always run HandleMonitorWakeup from the context of the semaphore lock
561  //multiple things may have happened, handle them one at a time
562  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
563  {
564  MonitorActivationReason activationReason = default; //this will always be assigned before being used
565 
566  //process the tasks in this order and call HandlerMonitorWakup for each
567 
568  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
569  {
570  var taskCompleted = task?.IsCompleted == true;
571  task = null;
572  if (monitorState.NextAction == MonitorAction.Skip)
573  monitorState.NextAction = MonitorAction.Continue;
574  else if (taskCompleted)
575  {
576  activationReason = testActivationReason;
577  return true;
578  }
579  return false;
580  };
581 
582  if (CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
583  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
584  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
585  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
586  || CheckActivationReason(ref inactiveServerStartup, MonitorActivationReason.InactiveServerStartupComplete)
587  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
588  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated))
589  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
590  else
591  moreActivationsToProcess = false;
592  }
593 
594  //writeback alphaServer and bravoServer from monitor state in case they changesd
595  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
596  bravoServer = !AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
597  }
598 
599  //full reboot required
600  if (monitorState.NextAction == MonitorAction.Restart)
601  {
602  logger.LogDebug("Next state action is to restart");
604  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
605 
606  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
607  {
608  Exception launchException = null;
609  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
610  try
611  {
612  //use LaunchImplNoLock without announcements or restarting the monitor
613  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
614  if (Running)
615  {
616  logger.LogDebug("Relaunch successful, resetting monitor state...");
617  monitorState = new MonitorState(); //clean the slate and continue
618  }
619  }
620  catch (OperationCanceledException)
621  {
622  throw;
623  }
624  catch (Exception e)
625  {
626  launchException = e;
627  }
628 
629  await chatTask.ConfigureAwait(false);
630  if (!Running)
631  {
632  if (launchException == null)
633  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
634  else
635  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
636  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); //max of one hour, increasing by a power of 2 each time
637  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
638  await Task.WhenAll(asyncDelayer.Delay(TimeSpan.FromSeconds(retryDelay), cancellationToken), chatTask).ConfigureAwait(false);
639  }
640  }
641  }
642  }
643  catch (OperationCanceledException)
644  {
645  logger.LogDebug("Monitor cancelled");
646  break;
647  }
648  catch (Exception e)
649  {
650  //really, this should NEVER happen
651  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}, Exception: {2}", iteration, JsonConvert.SerializeObject(monitorState), e);
652  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Monitor crashed, this should NEVER happen! Please report this, full details in logs! Restarting monitor... Error: {0}", e.Message), cancellationToken).ConfigureAwait(false);
653  }
654  }
655  logger.LogTrace("Monitor exiting...");
656  }
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:101
Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:697
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_control_panel_configuration-members.html b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_control_panel_configuration-members.html new file mode 100644 index 0000000000..d77d1cc7c1 --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_control_panel_configuration-members.html @@ -0,0 +1,113 @@ + + + + + + +tgstation-server: Member List + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
Tgstation.Server.Host.Configuration.ControlPanelConfiguration Member List
+
+ + + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_control_panel_configuration.html b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_control_panel_configuration.html new file mode 100644 index 0000000000..ac366fb681 --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_control_panel_configuration.html @@ -0,0 +1,235 @@ + + + + + + +tgstation-server: Tgstation.Server.Host.Configuration.ControlPanelConfiguration Class Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
Tgstation.Server.Host.Configuration.ControlPanelConfiguration Class Reference
+
+
+ +

Configuration options for the web control panel + More...

+ + + + + +

+Public Attributes

const string Section = "ControlPanel"
 The key for the Microsoft.Extensions.Configuration.IConfigurationSection the ControlPanelConfiguration resides in More...
 
+ + + + + + + + + + +

+Properties

bool Enable [get, set]
 If the control panel is enabled More...
 
bool AllowAnyOrigin [get, set]
 If any origin is allowed for CORS requests. This overrides AllowedOrigins More...
 
List< string > AllowedOrigins [get, set]
 Origins allowed for CORS requests More...
 
+

Detailed Description

+

Configuration options for the web control panel

+ +

Definition at line 8 of file ControlPanelConfiguration.cs.

+

Member Data Documentation

+ +
+
+ + + + +
const string Tgstation.Server.Host.Configuration.ControlPanelConfiguration.Section = "ControlPanel"
+
+ +

The key for the Microsoft.Extensions.Configuration.IConfigurationSection the ControlPanelConfiguration resides in

+ +

Definition at line 13 of file ControlPanelConfiguration.cs.

+ +

Referenced by Tgstation.Server.Host.Core.SetupWizard.SaveConfiguration().

+ +
+
+

Property Documentation

+ +
+
+ + + + + +
+ + + + +
bool Tgstation.Server.Host.Configuration.ControlPanelConfiguration.AllowAnyOrigin
+
+getset
+
+ +

If any origin is allowed for CORS requests. This overrides AllowedOrigins

+ +

Definition at line 23 of file ControlPanelConfiguration.cs.

+ +

Referenced by Tgstation.Server.Host.Core.Application.ConfigureServices().

+ +
+
+ +
+
+ + + + + +
+ + + + +
List<string> Tgstation.Server.Host.Configuration.ControlPanelConfiguration.AllowedOrigins
+
+getset
+
+
+ +
+
+ + + + + +
+ + + + +
bool Tgstation.Server.Host.Configuration.ControlPanelConfiguration.Enable
+
+getset
+
+ +

If the control panel is enabled

+ +

Definition at line 18 of file ControlPanelConfiguration.cs.

+ +
+
+
The documentation for this class was generated from the following file: +
+ + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_database_configuration.html b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_database_configuration.html index cc08f49d69..4e530546e4 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_database_configuration.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_database_configuration.html @@ -146,7 +146,7 @@ Properties

Definition at line 14 of file DatabaseConfiguration.cs.

-

Referenced by Tgstation.Server.Host.Core.Application.ConfigureServices(), Tgstation.Server.Host.Models.Migrations.DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), and Tgstation.Server.Host.Core.SetupWizard.SaveConfiguration().

+

Referenced by Tgstation.Server.Host.Models.Migrations.DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), and Tgstation.Server.Host.Core.SetupWizard.SaveConfiguration().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_file_logging_configuration.html b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_file_logging_configuration.html index 22dc1f5bd2..fb4aa8ca8d 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_file_logging_configuration.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_file_logging_configuration.html @@ -201,7 +201,7 @@ Private Attributes

Definition at line 15 of file FileLoggingConfiguration.cs.

-

Referenced by Tgstation.Server.Host.Core.Application.ConfigureServices(), and Tgstation.Server.Host.Core.SetupWizard.SaveConfiguration().

+

Referenced by Tgstation.Server.Host.Core.SetupWizard.SaveConfiguration().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_general_configuration.html b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_general_configuration.html index 929e44e16f..44d6070c18 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_general_configuration.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_general_configuration.html @@ -201,7 +201,7 @@ Private Attributes

Definition at line 14 of file GeneralConfiguration.cs.

-

Referenced by Tgstation.Server.Host.Core.Application.ConfigureServices(), and Tgstation.Server.Host.Core.SetupWizard.SaveConfiguration().

+

Referenced by Tgstation.Server.Host.Core.SetupWizard.SaveConfiguration(), and Tgstation.Server.Host.Core.ServiceCollectionExtensions.UseStandardConfig< TConfig >().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_updates_configuration.html b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_updates_configuration.html index a5cde1f481..c931b38740 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_updates_configuration.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_updates_configuration.html @@ -225,8 +225,6 @@ Private Attributes

Definition at line 11 of file UpdatesConfiguration.cs.

-

Referenced by Tgstation.Server.Host.Core.Application.ConfigureServices().

-

Property Documentation

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_api_controller-members.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_api_controller-members.html index baac9a7a29..820f481db8 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_api_controller-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_api_controller-members.html @@ -98,14 +98,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

This is the complete list of members for Tgstation.Server.Host.Controllers.ApiController, including all inherited members.

- + - + +
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)Tgstation.Server.Host.Controllers.ApiController
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)Tgstation.Server.Host.Controllers.ApiController
ApiHeadersTgstation.Server.Host.Controllers.ApiControllerprotected
AuthenticationContextTgstation.Server.Host.Controllers.ApiControllerprotected
DatabaseContextTgstation.Server.Host.Controllers.ApiControllerprotected
InstanceTgstation.Server.Host.Controllers.ApiControllerprotected
LoggerTgstation.Server.Host.Controllers.ApiControllerprotected
OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)Tgstation.Server.Host.Controllers.ApiController
requireInstanceTgstation.Server.Host.Controllers.ApiControllerprivate
requireHeadersTgstation.Server.Host.Controllers.ApiControllerprivate
requireInstanceTgstation.Server.Host.Controllers.ApiControllerprivate
- - - + + +

Public Member Functions

 ApiController (IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
 Construct an ApiController More...
 
 ApiController (IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
 Construct an ApiController More...
 
override async Task OnActionExecutionAsync (ActionExecutingContext context, ActionExecutionDelegate next)
 
@@ -146,13 +146,16 @@ Private Attributes + + +
readonly bool requireInstance
 If IAuthenticationContext.InstanceUser permissions are required to access the ApiController More...
 
readonly bool requireHeaders
 If ApiHeaders are required More...
 

Detailed Description

A Controller for API functions

Definition at line 22 of file ApiController.cs.

Constructor & Destructor Documentation

- +
@@ -178,7 +181,13 @@ Private Attributes - + + + + + + + @@ -195,16 +204,18 @@ Private Attributes +
bool requireInstance requireInstance,
bool requireHeaders 
authenticationContextFactoryThe IAuthenticationContextFactory for the ApiController
loggerThe value of Logger
requireInstanceThe value of requireInstance
requireHeadersThe value of requireHeaders
-

Definition at line 61 of file ApiController.cs.

+

Definition at line 67 of file ApiController.cs.

References Tgstation.Server.Host.Security.IAuthenticationContextFactory.CurrentAuthenticationContext, and Tgstation.Server.Host.Security.AuthenticationContext.InstanceUser.

-
62  {
63  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
64  if (authenticationContextFactory == null)
65  throw new ArgumentNullException(nameof(authenticationContextFactory));
66  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
70  }
+
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;
77  }
+
readonly bool requireHeaders
If ApiHeaders are required
ILogger Logger
The ILogger for the ApiController
readonly bool requireInstance
If IAuthenticationContext.InstanceUser permissions are required to access the ApiController ...
Metadata about a server instance
Definition: Instance.cs:9
@@ -239,10 +250,10 @@ Private Attributes

-

Definition at line 73 of file ApiController.cs.

+

Definition at line 80 of file ApiController.cs.

References Tgstation.Server.Api.ApiHeaders.ApiVersion, Tgstation.Server.Api.ApiHeaders.Compatible(), Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.ApiHeaders.InstanceId, Tgstation.Server.Host.Security.AuthenticationContext.InstanceUser, Tgstation.Server.Api.Models.ErrorMessage.Message, Tgstation.Server.Host.Security.AuthenticationContext.User, and Tgstation.Server.Api.ApiHeaders.UserAgent.

-
74  {
75  //ALL valid token and login requests that match a route go through this function
76  //404 is returned before
77 
78  if (AuthenticationContext != null && AuthenticationContext.User == null)
79  {
80  //valid token, expired password
81  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
82  return;
83  }
84 
85  //validate the headers
86  try
87  {
88  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
89 
90  if (!ApiHeaders.Compatible())
91  {
92  await StatusCode((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
93  {
94  Message = "Provided API version is incompatible with server version!"
95  }).ExecuteResultAsync(context).ConfigureAwait(false);
96  return;
97  }
98 
99  if (requireInstance)
100  {
101  if (!ApiHeaders.InstanceId.HasValue)
102  {
103  await BadRequest(new ErrorMessage { Message = "Missing Instance header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
104  return;
105  }
107  {
108  //accessing an instance they don't have access to or one that's disabled
109  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
110  return;
111  }
112  }
113  }
114  catch (InvalidOperationException e)
115  {
116  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
117  return;
118  }
119 
120  if (ModelState?.IsValid == false)
121  {
122  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
123  //HACK
124  //do some fuckery to remove RequiredAttribute errors
125  for (var I = 0; I < errorMessages.Count; ++I)
126  {
127  var message = errorMessages[I];
128  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
129  {
130  errorMessages.RemoveAt(I);
131  --I;
132  }
133  }
134  if (errorMessages.Count > 0)
135  {
136  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
137  return;
138  }
139  }
140 
141  Logger.LogDebug("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path, Request.QueryString, ApiHeaders.InstanceId);
142 
143  try
144  {
145  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
146  }
147  catch (OperationCanceledException e)
148  {
149  Logger.LogDebug("Request cancelled! Exception: {0}", e);
150  throw;
151  }
152  }
+
81  {
82  //ALL valid token and login requests that match a route go through this function
83  //404 is returned before
84 
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((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
100  {
101  Message = "Provided API version is incompatible with server version!"
102  }).ExecuteResultAsync(context).ConfigureAwait(false);
103  return;
104  }
105 
106  if (requireInstance)
107  {
108  if (!ApiHeaders.InstanceId.HasValue)
109  {
110  await BadRequest(new ErrorMessage { Message = "Missing Instance header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
111  return;
112  }
114  {
115  //accessing an instance they don't have access to or one that's disabled
116  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
117  return;
118  }
119  }
120  }
121  catch (InvalidOperationException e)
122  {
123  if (requireHeaders)
124  {
125  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
126  return;
127  }
128  }
129 
130  if (ModelState?.IsValid == false)
131  {
132  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
133  //HACK
134  //do some fuckery to remove RequiredAttribute errors
135  for (var I = 0; I < errorMessages.Count; ++I)
136  {
137  var message = errorMessages[I];
138  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
139  {
140  errorMessages.RemoveAt(I);
141  --I;
142  }
143  }
144  if (errorMessages.Count > 0)
145  {
146  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
147  return;
148  }
149  }
150 
151  if (ApiHeaders != null)
152  Logger.LogDebug("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path, Request.QueryString, ApiHeaders.InstanceId);
153 
154  try
155  {
156  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
157  }
158  catch (OperationCanceledException e)
159  {
160  Logger.LogDebug("Request cancelled! Exception: {0}", e);
161  throw;
162  }
163  }
Version ApiVersion
The client&#39;s API version
Definition: ApiHeaders.cs:71
bool Compatible()
Checks if the ApiVersion is compatible with Version
ProductHeaderValue UserAgent
The client&#39;s user agent
Definition: ApiHeaders.cs:66
@@ -251,6 +262,7 @@ Private Attributes
string Message
A human readable description of the error
Definition: ErrorMessage.cs:13
+
readonly bool requireHeaders
If ApiHeaders are required
long Id
The ID of the User
Definition: User.cs:15
ILogger Logger
The ILogger for the ApiController
readonly bool requireInstance
If IAuthenticationContext.InstanceUser permissions are required to access the ApiController ...
@@ -261,6 +273,30 @@ Private Attributes

Member Data Documentation

+ +
+
+ + + + + +
+ + + + +
readonly bool Tgstation.Server.Host.Controllers.ApiController.requireHeaders
+
+private
+
+ +

If ApiHeaders are required

+ +

Definition at line 57 of file ApiController.cs.

+ +
+
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller-members.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller-members.html index 84cb5fd1bb..92374c3b66 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller-members.html @@ -98,15 +98,17 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

This is the complete list of members for Tgstation.Server.Host.Controllers.HomeController, including all inherited members.

- + + + - - + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller.html index 95e109cf58..3edd99e59a 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller.html @@ -115,19 +115,19 @@ Inheritance diagram for Tgstation.Server.Host.Controllers.HomeController:
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)Tgstation.Server.Host.Controllers.ApiController
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)Tgstation.Server.Host.Controllers.ApiController
ApiHeadersTgstation.Server.Host.Controllers.ApiControllerprotected
applicationTgstation.Server.Host.Controllers.HomeControllerprivate
AuthenticationContextTgstation.Server.Host.Controllers.ApiControllerprotected
browserResolverTgstation.Server.Host.Controllers.HomeControllerprivate
controlPanelConfigurationTgstation.Server.Host.Controllers.HomeControllerprivate
CreateToken(CancellationToken cancellationToken)Tgstation.Server.Host.Controllers.HomeController
cryptographySuiteTgstation.Server.Host.Controllers.HomeControllerprivate
DatabaseContextTgstation.Server.Host.Controllers.ApiControllerprotected
Home()Tgstation.Server.Host.Controllers.HomeController
HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, ILogger< HomeController > logger)Tgstation.Server.Host.Controllers.HomeController
Home()Tgstation.Server.Host.Controllers.HomeController
HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, IBrowserResolver browserResolver, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< HomeController > logger)Tgstation.Server.Host.Controllers.HomeController
identityCacheTgstation.Server.Host.Controllers.HomeControllerprivate
InstanceTgstation.Server.Host.Controllers.ApiControllerprotected
LoggerTgstation.Server.Host.Controllers.ApiControllerprotected
- - - - - - + + + + + + - - - + + +

Public Member Functions

 HomeController (IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, ILogger< HomeController > logger)
 Construct a HomeController More...
 
JsonResult Home ()
 Returns the version of the Application More...
 
 HomeController (IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, IBrowserResolver browserResolver, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< HomeController > logger)
 Construct a HomeController More...
 
IActionResult Home ()
 Main page of the Application More...
 
async Task< IActionResult > CreateToken (CancellationToken cancellationToken)
 Attempt to authenticate a User using ApiController.ApiHeaders More...
 
- Public Member Functions inherited from Tgstation.Server.Host.Controllers.ApiController
 ApiController (IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
 Construct an ApiController More...
 
 ApiController (IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
 Construct an ApiController More...
 
override async Task OnActionExecutionAsync (ActionExecutingContext context, ActionExecutionDelegate next)
 
@@ -148,6 +148,12 @@ Private Attributes + + + + + +
readonly IIdentityCache identityCache
 The IIdentityCache for the HomeController More...
 
readonly IBrowserResolver browserResolver
 The IBrowserResolver for the HomeController More...
 
readonly ControlPanelConfiguration controlPanelConfiguration
 The ControlPanelConfiguration for the HomeController More...
 
@@ -171,9 +177,9 @@ Additional Inherited Members

Detailed Description

Main ApiController for the Application

-

Definition at line 20 of file HomeController.cs.

+

Definition at line 24 of file HomeController.cs.

Constructor & Destructor Documentation

- +

Additional Inherited Members

@@ -219,6 +225,18 @@ Additional Inherited Members + + + + + + + + + + + + @@ -243,13 +261,17 @@ Additional Inherited Members + +
IIdentityCache  identityCache,
IBrowserResolver browserResolver,
IOptions< ControlPanelConfigurationcontrolPanelConfigurationOptions,
cryptographySuiteThe value of cryptographySuite
applicationThe value of application
identityCacheThe value of identityCache
browserResolverThe value of browserResolver
controlPanelConfigurationOptionsThe IOptions<TOptions> containing the value of controlPanelConfiguration
loggerThe ILogger for the ApiController
-

Definition at line 54 of file HomeController.cs.

-
54  : base(databaseContext, authenticationContextFactory, logger, false)
55  {
56  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
57  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
58  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
59  this.application = application ?? throw new ArgumentNullException(nameof(application));
60  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
61  }
+

Definition at line 70 of file HomeController.cs.

+
70  : base(databaseContext, authenticationContextFactory, logger, false, false)
71  {
72  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
73  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
74  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
75  this.application = application ?? throw new ArgumentNullException(nameof(application));
76  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
77  this.browserResolver = browserResolver ?? throw new ArgumentNullException(nameof(browserResolver));
78  controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
79  }
readonly ControlPanelConfiguration controlPanelConfiguration
The ControlPanelConfiguration for the HomeController
+
readonly IBrowserResolver browserResolver
The IBrowserResolver for the HomeController
+

Member Function Documentation

@@ -276,21 +298,21 @@ Additional Inherited Members
Returns
A Task<TResult> resulting in the IActionResult of the operation
-

Definition at line 81 of file HomeController.cs.

+

Definition at line 113 of file HomeController.cs.

References Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.ApiHeaders.IsTokenAuthentication, Tgstation.Server.Api.ApiHeaders.Password, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), Tgstation.Server.Host.Security.ISystemIdentity.Uid, Tgstation.Server.Host.Security.ISystemIdentity.Username, Tgstation.Server.Api.ApiHeaders.Username, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Users.

-
82  {
84  return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
85 
86  ISystemIdentity identity;
87  try
88  {
89  //trust the system over the database because a user's name can change while still having the same SID
90  identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
91  }
92  catch (NotImplementedException)
93  {
94  identity = null;
95  }
96  using (identity)
97  {
98  IQueryable<User> query;
99  if (identity == null)
100  query = DatabaseContext.Users.Where(x => x.CanonicalName == ApiHeaders.Username.ToUpperInvariant());
101  else
102  query = DatabaseContext.Users.Where(x => x.SystemIdentifier == identity.Uid);
103  var user = await query.Select(x => new User
104  {
105  Id = x.Id,
106  PasswordHash = x.PasswordHash,
107  Enabled = x.Enabled,
108  Name = x.Name
109  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
110 
111  if (user == null)
112  return Unauthorized();
113 
114  if (identity == null)
115  {
116  var originalHash = user.PasswordHash;
118  return Unauthorized();
119  if (user.PasswordHash != originalHash)
120  {
121  var updatedUser = new User
122  {
123  Id = user.Id
124  };
125  DatabaseContext.Users.Attach(updatedUser);
126  updatedUser.PasswordHash = user.PasswordHash;
127  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
128  }
129  }
130  //check if the name changed and updoot accordingly
131  else if (identity.Username != user.Name)
132  {
133  DatabaseContext.Users.Attach(user);
134  user.Name = identity.Username;
135  user.CanonicalName = user.Name.ToUpperInvariant();
136  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
137  }
138 
139  if (!user.Enabled.Value)
140  return Forbid();
141 
142  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
143  if (identity != null)
144  {
145  //expire the identity slightly after the auth token in case of lag
146  var identExpiry = token.ExpiresAt.Value;
147  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
148  identExpiry += TimeSpan.FromSeconds(15);
149  identityCache.CacheSystemIdentity(user, identity, identExpiry);
150  }
151 
152  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
153 
154  return Json(token);
155  }
156  }
bool CheckUserPassword(User user, string password)
Checks a given password matches a given user &#39;s User.PasswordHash. This may result in User...
+
114  {
115  if (ApiHeaders == null)
116  return BadRequest(new Api.Models.ErrorMessage { Message = "Missing API headers!" });
117 
119  return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
120 
121  ISystemIdentity identity;
122  try
123  {
124  //trust the system over the database because a user's name can change while still having the same SID
125  identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
126  }
127  catch (NotImplementedException)
128  {
129  identity = null;
130  }
131  using (identity)
132  {
133  IQueryable<User> query;
134  if (identity == null)
135  query = DatabaseContext.Users.Where(x => x.CanonicalName == ApiHeaders.Username.ToUpperInvariant());
136  else
137  query = DatabaseContext.Users.Where(x => x.SystemIdentifier == identity.Uid);
138  var user = await query.Select(x => new User
139  {
140  Id = x.Id,
141  PasswordHash = x.PasswordHash,
142  Enabled = x.Enabled,
143  Name = x.Name
144  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
145 
146  if (user == null)
147  return Unauthorized();
148 
149  if (identity == null)
150  {
151  var originalHash = user.PasswordHash;
153  return Unauthorized();
154  if (user.PasswordHash != originalHash)
155  {
156  var updatedUser = new User
157  {
158  Id = user.Id
159  };
160  DatabaseContext.Users.Attach(updatedUser);
161  updatedUser.PasswordHash = user.PasswordHash;
162  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
163  }
164  }
165  //check if the name changed and updoot accordingly
166  else if (identity.Username != user.Name)
167  {
168  DatabaseContext.Users.Attach(user);
169  user.Name = identity.Username;
170  user.CanonicalName = user.Name.ToUpperInvariant();
171  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
172  }
173 
174  if (!user.Enabled.Value)
175  return Forbid();
176 
177  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
178  if (identity != null)
179  {
180  //expire the identity slightly after the auth token in case of lag
181  var identExpiry = token.ExpiresAt.Value;
182  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
183  identExpiry += TimeSpan.FromSeconds(15);
184  identityCache.CacheSystemIdentity(user, identity, identExpiry);
185  }
186 
187  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
188 
189  return Json(token);
190  }
191  }
bool CheckUserPassword(User user, string password)
Checks a given password matches a given user &#39;s User.PasswordHash. This may result in User...
Task< ISystemIdentity > CreateSystemIdentity(User user, CancellationToken cancellationToken)
Create a ISystemIdentity for a given user
Task< Token > CreateToken(Models.User user, CancellationToken cancellationToken)
Create a Token for a given user
Represents a user on the current System.Runtime.InteropServices.OSPlatform
string Username
The client&#39;s username
Definition: ApiHeaders.cs:81
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
-
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the HomeController
+
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the HomeController
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:16
string Password
The client&#39;s password
Definition: ApiHeaders.cs:86
-
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController
+
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController
bool IsTokenAuthentication
If the header uses password or JWT authentication
Definition: ApiHeaders.cs:91
@@ -298,18 +320,18 @@ Additional Inherited Members
long Id
The ID of the User
Definition: User.cs:15
ILogger Logger
The ILogger for the ApiController
void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry)
Keep a user &#39;s systemIdentity alive until an expiry time
-
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the HomeController
-
readonly ITokenFactory tokenFactory
The ITokenFactory for the HomeController
+
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the HomeController
+
readonly ITokenFactory tokenFactory
The ITokenFactory for the HomeController
string Uid
A unique identifier for the user
- +
- + @@ -317,9 +339,16 @@ Additional Inherited Members
JsonResult Tgstation.Server.Host.Controllers.HomeController.Home IActionResult Tgstation.Server.Host.Controllers.HomeController.Home ( )
-

Returns the version of the Application

-
Returns
Application.Version
+

Main page of the Application

+
Returns
The Api.Models.ServerInformation of the Application if a properly authenticated API request, the web control panel if on a browser and enabled, UnauthorizedResult otherwise
+

Definition at line 88 of file HomeController.cs.

+
89  {
90  if (AuthenticationContext != null)
91  return Json(new Api.Models.ServerInformation
92  {
93  Version = application.Version,
94  ApiVersion = ApiHeaders.Version
95  });
96 
97  //if we are using a browser and the control panel, soft redirect to the app page
98  if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic)
99  {
100  Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), loading control panel...", browserResolver.UserAgent);
101  return File("~/index.html", MediaTypeNames.Text.Html);
102  }
103 
104  return Unauthorized();
105  }
+ +
readonly ControlPanelConfiguration controlPanelConfiguration
The ControlPanelConfiguration for the HomeController
+
ILogger Logger
The ILogger for the ApiController
+
readonly IBrowserResolver browserResolver
The IBrowserResolver for the HomeController
+

Member Data Documentation

@@ -343,7 +372,55 @@ Additional Inherited Members

The IApplication for the HomeController

-

Definition at line 37 of file HomeController.cs.

+

Definition at line 41 of file HomeController.cs.

+ +
+
+ +
+
+ + + + + +
+ + + + +
readonly IBrowserResolver Tgstation.Server.Host.Controllers.HomeController.browserResolver
+
+private
+
+ +

The IBrowserResolver for the HomeController

+ +

Definition at line 50 of file HomeController.cs.

+ +
+
+ +
+
+ + + + + +
+ + + + +
readonly ControlPanelConfiguration Tgstation.Server.Host.Controllers.HomeController.controlPanelConfiguration
+
+private
+
+ +

The ControlPanelConfiguration for the HomeController

+ +

Definition at line 55 of file HomeController.cs.

@@ -367,7 +444,7 @@ Additional Inherited Members

The ICryptographySuite for the HomeController

-

Definition at line 33 of file HomeController.cs.

+

Definition at line 37 of file HomeController.cs.

@@ -391,7 +468,7 @@ Additional Inherited Members

The IIdentityCache for the HomeController

-

Definition at line 41 of file HomeController.cs.

+

Definition at line 45 of file HomeController.cs.

@@ -415,7 +492,7 @@ Additional Inherited Members

The ISystemIdentityFactory for the HomeController

-

Definition at line 29 of file HomeController.cs.

+

Definition at line 33 of file HomeController.cs.

@@ -439,7 +516,7 @@ Additional Inherited Members

The ITokenFactory for the HomeController

-

Definition at line 25 of file HomeController.cs.

+

Definition at line 29 of file HomeController.cs.

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_instance_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_instance_controller.html index 446ca43242..6f8f06fd6b 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_instance_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_instance_controller.html @@ -557,8 +557,7 @@ Private Attributes

Definition at line 252 of file InstanceController.cs.

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances.

-
253  {
254  var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id);
255 
256  var moveJob = await instanceQuery
257  .SelectMany(x => x.Jobs).
258 #pragma warning disable CA1307 // Specify StringComparison
259  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
260 #pragma warning restore CA1307 // Specify StringComparison
261  .Select(x => new Models.Job
262  {
263  Id = x.Id
264  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
265 
266  if (moveJob != default)
267  //cancel it now
268  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false);
269 
270  var usersInstanceUserTask = instanceQuery.SelectMany(x => x.InstanceUsers).Where(x => x.UserId == AuthenticationContext.User.Id).FirstOrDefaultAsync(cancellationToken);
271 
272  var originalModel = await instanceQuery
273  .Include(x => x.RepositorySettings)
274  .Include(x => x.ChatSettings)
275  .ThenInclude(x => x.Channels)
276  .Include(x => x.DreamDaemonSettings) //need these for onlining
277  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
278  if (originalModel == default(Models.Instance))
279  return StatusCode((int)HttpStatusCode.Gone);
280 
281  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
282  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
283  {
284  var memberSelectorExpression = (MemberExpression)expression.Body;
285  var property = (PropertyInfo)memberSelectorExpression.Member;
286 
287  var newVal = property.GetValue(model);
288  if (newVal == null)
289  return false;
290  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
291  return true;
292 
293  property.SetValue(originalModel, newVal);
294  return false;
295  };
296 
297  string originalModelPath = null;
298  string rawPath = null;
299  if (model.Path != null)
300  {
301  NormalizeModelPath(model, out rawPath);
302 
303  if (model.Path != originalModel.Path)
304  {
305  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
306  return Forbid();
307  if (originalModel.Online.Value && model.Online != true)
308  return Conflict(new ErrorMessage { Message = "Cannot relocate an online instance!" });
309 
310  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
311  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
312  return Conflict(new ErrorMessage { Message = "Path not empty!" });
313 
314  originalModelPath = originalModel.Path;
315  originalModel.Path = model.Path;
316  }
317  }
318 
319  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
320  var originalOnline = originalModel.Online.Value;
321  var renamed = model.Name != null && originalModel.Name != model.Name;
322 
323  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
324  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
325  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
326  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline))
327  return Forbid();
328 
329  //ensure the current user has write privilege on the instance
330  var usersInstanceUser = await usersInstanceUserTask.ConfigureAwait(false);
331  if (usersInstanceUser == default)
332  {
333  var instanceAdminUser = InstanceAdminUser();
334  instanceAdminUser.InstanceId = originalModel.Id;
335  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
336  }
337  else
338  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
339 
340  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
341 
342  if (renamed)
343  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
344 
345  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
346  try
347  {
348  if (originalOnline && model.Online == false)
349  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
350  else if (!originalOnline && model.Online == true)
351  {
352  //force autostart false here because we don't want any long running jobs right now
353  //remember to document this
354  originalModel.DreamDaemonSettings.AutoStart = false;
355  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
356  }
357  }
358  catch (Exception e)
359  {
360  if(!(e is OperationCanceledException))
361  Logger.LogError("Error changing instance online state! Exception: {0}", e);
362  originalModel.Online = originalOnline;
363  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
364  if (originalModelPath != null)
365  originalModel.Path = originalModelPath;
366  await DatabaseContext.Save(default).ConfigureAwait(false);
367  throw;
368  }
369 
370  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
371  {
372  Id = originalModel.Id
373  };
374  if (originalModelPath != null)
375  {
376  var job = new Models.Job
377  {
378  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
379  Instance = originalModel,
380  CancelRightsType = RightsType.InstanceManager,
381  CancelRight = (ulong)InstanceManagerRights.Relocate,
382  StartedBy = AuthenticationContext.User
383  };
384 
385  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
386  api.MoveJob = job.ToApi();
387  }
388 
389  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
390  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
391 
392  return Json(api);
393  }
- +
253  {
254  var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id);
255 
256  var moveJob = await instanceQuery
257  .SelectMany(x => x.Jobs).
258 #pragma warning disable CA1307 // Specify StringComparison
259  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
260 #pragma warning restore CA1307 // Specify StringComparison
261  .Select(x => new Models.Job
262  {
263  Id = x.Id
264  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
265 
266  if (moveJob != default)
267  //cancel it now
268  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false);
269 
270  var usersInstanceUserTask = instanceQuery.SelectMany(x => x.InstanceUsers).Where(x => x.UserId == AuthenticationContext.User.Id).FirstOrDefaultAsync(cancellationToken);
271 
272  var originalModel = await instanceQuery
273  .Include(x => x.RepositorySettings)
274  .Include(x => x.ChatSettings)
275  .ThenInclude(x => x.Channels)
276  .Include(x => x.DreamDaemonSettings) //need these for onlining
277  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
278  if (originalModel == default(Models.Instance))
279  return StatusCode((int)HttpStatusCode.Gone);
280 
281  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
282  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
283  {
284  var memberSelectorExpression = (MemberExpression)expression.Body;
285  var property = (PropertyInfo)memberSelectorExpression.Member;
286 
287  var newVal = property.GetValue(model);
288  if (newVal == null)
289  return false;
290  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
291  return true;
292 
293  property.SetValue(originalModel, newVal);
294  return false;
295  };
296 
297  string originalModelPath = null;
298  string rawPath = null;
299  if (model.Path != null)
300  {
301  NormalizeModelPath(model, out rawPath);
302 
303  if (model.Path != originalModel.Path)
304  {
305  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
306  return Forbid();
307  if (originalModel.Online.Value && model.Online != true)
308  return Conflict(new ErrorMessage { Message = "Cannot relocate an online instance!" });
309 
310  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
311  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
312  return Conflict(new ErrorMessage { Message = "Path not empty!" });
313 
314  originalModelPath = originalModel.Path;
315  originalModel.Path = model.Path;
316  }
317  }
318 
319  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
320  var originalOnline = originalModel.Online.Value;
321  var renamed = model.Name != null && originalModel.Name != model.Name;
322 
323  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
324  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
325  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
326  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline))
327  return Forbid();
328 
329  //ensure the current user has write privilege on the instance
330  var usersInstanceUser = await usersInstanceUserTask.ConfigureAwait(false);
331  if (usersInstanceUser == default)
332  {
333  var instanceAdminUser = InstanceAdminUser();
334  instanceAdminUser.InstanceId = originalModel.Id;
335  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
336  }
337  else
338  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
339 
340  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
341 
342  if (renamed)
343  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
344 
345  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
346  try
347  {
348  if (originalOnline && model.Online == false)
349  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
350  else if (!originalOnline && model.Online == true)
351  {
352  //force autostart false here because we don't want any long running jobs right now
353  //remember to document this
354  originalModel.DreamDaemonSettings.AutoStart = false;
355  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
356  }
357  }
358  catch (Exception e)
359  {
360  if(!(e is OperationCanceledException))
361  Logger.LogError("Error changing instance online state! Exception: {0}", e);
362  originalModel.Online = originalOnline;
363  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
364  if (originalModelPath != null)
365  originalModel.Path = originalModelPath;
366  await DatabaseContext.Save(default).ConfigureAwait(false);
367  throw;
368  }
369 
370  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
371  {
372  Id = originalModel.Id
373  };
374  if (originalModelPath != null)
375  {
376  var job = new Models.Job
377  {
378  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
379  Instance = originalModel,
380  CancelRightsType = RightsType.InstanceManager,
381  CancelRight = (ulong)InstanceManagerRights.Relocate,
382  StartedBy = AuthenticationContext.User
383  };
384 
385  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
386  api.MoveJob = job.ToApi();
387  }
388 
389  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
390  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
391 
392  return Json(api);
393  }
Task MoveInstance(Models.Instance metadata, string newPath, CancellationToken cancellationToken)
Move an IInstance
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_model_controller-members.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_model_controller-members.html index a4a090e581..f6928bca9b 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_model_controller-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_model_controller-members.html @@ -98,7 +98,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

This is the complete list of members for Tgstation.Server.Host.Controllers.ModelController< TModel >, including all inherited members.

- + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_model_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_model_controller.html index ed4dd99b77..7a962635bf 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_model_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_model_controller.html @@ -136,9 +136,9 @@ Public Member Functions - - - + + +
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)Tgstation.Server.Host.Controllers.ApiController
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)Tgstation.Server.Host.Controllers.ApiController
ApiHeadersTgstation.Server.Host.Controllers.ApiControllerprotected
AuthenticationContextTgstation.Server.Host.Controllers.ApiControllerprotected
Create([FromBody]TModel model, CancellationToken cancellationToken)Tgstation.Server.Host.Controllers.ModelController< TModel >virtual
 Attempt to list entries of the TModel More...
 
- Public Member Functions inherited from Tgstation.Server.Host.Controllers.ApiController
 ApiController (IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
 Construct an ApiController More...
 
 ApiController (IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
 Construct an ApiController More...
 
override async Task OnActionExecutionAsync (ActionExecutingContext context, ActionExecutionDelegate next)
 
@@ -228,7 +228,7 @@ Additional Inherited Members

Definition at line 23 of file ModelController.cs.

-
23 : base(databaseContext, authenticationContextFactory, logger, requireInstance) { }
readonly bool requireInstance
If IAuthenticationContext.InstanceUser permissions are required to access the ApiController ...
+
23 : base(databaseContext, authenticationContextFactory, logger, requireInstance, true) { }
readonly bool requireInstance
If IAuthenticationContext.InstanceUser permissions are required to access the ApiController ...
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application-members.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application-members.html index f20e8b7cf3..94a706c630 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application-members.html @@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
- + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application.html index 749aa08709..522027a533 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application.html @@ -122,10 +122,10 @@ Public Member Functions - - - - + + + +
Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)Tgstation.Server.Host.Core.Application
configurationTgstation.Server.Host.Core.Applicationprivate
Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, ILogger< Application > logger)Tgstation.Server.Host.Core.Application
Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< Application > logger)Tgstation.Server.Host.Core.Application
ConfigureServices(IServiceCollection services)Tgstation.Server.Host.Core.Application
hostingEnvironmentTgstation.Server.Host.Core.Applicationprivate
Ready(Exception initializationError)Tgstation.Server.Host.Core.Application
void ConfigureServices (IServiceCollection services)
 Configure dependency injected services More...
 
void Configure (IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, ILogger< Application > logger)
 Configure the Application More...
 
void Ready (Exception initializationError)
void Configure (IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< Application > logger)
 Configure the Application More...
 
void Ready (Exception initializationError)
 Mark the IApplication as ready to run More...
 
@@ -214,7 +214,7 @@ Private Attributes

Member Function Documentation

- +
@@ -236,6 +236,12 @@ Private Attributes + + + + + + @@ -256,13 +262,14 @@ Private Attributes +
ITokenFactory  tokenFactory,
IOptions< ControlPanelConfigurationcontrolPanelConfigurationOptions,
applicationBuilderThe IApplicationBuilder to configure
serverControlThe IServerControl for the Application
tokenFactoryThe value of tokenFactory
controlPanelConfigurationOptionsThe IOptions<TOptions> containing the ControlPanelConfiguration to use
loggerThe Microsoft.Extensions.Logging.ILogger for the Application

inheritdoc />

-

Definition at line 315 of file Application.cs.

+

Definition at line 327 of file Application.cs.

References Tgstation.Server.Host.Core.IServerControl.Restart(), and Tgstation.Server.Host.Core.IServerControl.WatchdogPresent.

@@ -292,8 +299,8 @@ Private Attributes

Definition at line 88 of file Application.cs.

-

References Tgstation.Server.Host.Core.ISetupWizard.CheckRunWizard(), Tgstation.Server.Host.IO.IIOManager.ConcatPath(), Tgstation.Server.Host.Configuration.DatabaseConfiguration.DatabaseType, Tgstation.Server.Host.Configuration.FileLoggingConfiguration.Directory, Tgstation.Server.Host.Configuration.FileLoggingConfiguration.Disable, Tgstation.Server.Host.Core.IPlatformIdentifier.IsWindows, Tgstation.Server.Host.Configuration.FileLoggingConfiguration.LogLevel, Tgstation.Server.Host.Configuration.FileLoggingConfiguration.MicrosoftLogLevel, Tgstation.Server.Host.Configuration.UpdatesConfiguration.Section, Tgstation.Server.Host.Configuration.GeneralConfiguration.Section, Tgstation.Server.Host.Configuration.DatabaseConfiguration.Section, Tgstation.Server.Host.Configuration.FileLoggingConfiguration.Section, and Tgstation.Server.Host.Security.ITokenFactory.ValidationParameters.

-
89  {
90  if (services == null)
91  throw new ArgumentNullException(nameof(services));
92 
93  //needful
94  services.AddSingleton<IApplication>(this);
95 
96  //configure configuration
97  services.Configure<UpdatesConfiguration>(configuration.GetSection(UpdatesConfiguration.Section));
98  services.Configure<DatabaseConfiguration>(configuration.GetSection(DatabaseConfiguration.Section));
99  services.Configure<GeneralConfiguration>(configuration.GetSection(GeneralConfiguration.Section));
101 
102  //enable options which give us config reloading
103  services.AddOptions();
104 
105  //other stuff needed for for setup wizard and configuration
106  services.AddSingleton<IIOManager, DefaultIOManager>();
107  services.AddSingleton<IConsole, IO.Console>();
108  services.AddSingleton<IDBConnectionFactory, DBConnectionFactory>();
109  services.AddSingleton<ISetupWizard, SetupWizard>();
110  services.AddSingleton<IPlatformIdentifier, PlatformIdentifier>();
111  services.AddSingleton<IAsyncDelayer, AsyncDelayer>();
112 
113  GeneralConfiguration generalConfiguration;
114  DatabaseConfiguration databaseConfiguration;
115  FileLoggingConfiguration fileLoggingConfiguration;
116  IIOManager ioManager;
117  IPlatformIdentifier platformIdentifier;
118 
119  //temporarily build the service provider in it's current state
120  //do it here so we can run the setup wizard if necessary
121  //also allows us to get some options and other services we need for continued configuration
122  using (var provider = services.BuildServiceProvider())
123  {
124  //run the wizard if necessary
125  var setupWizard = provider.GetRequiredService<ISetupWizard>();
126  var applicationLifetime = provider.GetRequiredService<Microsoft.AspNetCore.Hosting.IApplicationLifetime>();
127  var setupWizardRan = setupWizard.CheckRunWizard(applicationLifetime.ApplicationStopping).GetAwaiter().GetResult();
128 
129  //load the configuration options we need
130  var generalOptions = provider.GetRequiredService<IOptions<GeneralConfiguration>>();
131  generalConfiguration = generalOptions.Value;
132 
133  //unless this is set, in which case, we leave
134  if (setupWizardRan && generalConfiguration.SetupWizardMode == SetupWizardMode.Only)
135  //we don't inject a logger in the constuctor to log this because it's not yet configured
136  throw new OperationCanceledException("Exiting due to SetupWizardMode configuration!");
137 
138  var dbOptions = provider.GetRequiredService<IOptions<DatabaseConfiguration>>();
139  databaseConfiguration = dbOptions.Value;
140 
141  var loggingOptions = provider.GetRequiredService<IOptions<FileLoggingConfiguration>>();
142  fileLoggingConfiguration = loggingOptions.Value;
143 
144  ioManager = provider.GetRequiredService<IIOManager>();
145  platformIdentifier = provider.GetRequiredService<IPlatformIdentifier>();
146  }
147 
148  //setup file logging via serilog
149  if (!fileLoggingConfiguration.Disable)
150  services.AddLogging(builder =>
151  {
152  //common app data is C:/ProgramData on windows, else /usr/shar
153  var logPath = !String.IsNullOrEmpty(fileLoggingConfiguration.Directory) ? fileLoggingConfiguration.Directory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
154 
155  logPath = ioManager.ConcatPath(logPath, "tgs-{Date}.log");
156 
157  LogEventLevel? ConvertLogLevel(LogLevel logLevel)
158  {
159  switch (logLevel)
160  {
161  case LogLevel.Critical:
162  return LogEventLevel.Fatal;
163  case LogLevel.Debug:
164  return LogEventLevel.Debug;
165  case LogLevel.Error:
166  return LogEventLevel.Error;
167  case LogLevel.Information:
168  return LogEventLevel.Information;
169  case LogLevel.Trace:
170  return LogEventLevel.Verbose;
171  case LogLevel.Warning:
172  return LogEventLevel.Warning;
173  case LogLevel.None:
174  return null;
175  default:
176  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel));
177  }
178  };
179 
180  var logEventLevel = ConvertLogLevel(fileLoggingConfiguration.LogLevel);
181  var microsoftEventLevel = ConvertLogLevel(fileLoggingConfiguration.MicrosoftLogLevel);
182 
183  var formatter = new MessageTemplateTextFormatter("{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", null);
184 
185  var configuration = new LoggerConfiguration()
186  .Enrich.FromLogContext()
187  .WriteTo.Async(w => w.RollingFile(formatter, logPath, shared: true, flushToDiskInterval: TimeSpan.FromSeconds(2)));
188 
189  if (logEventLevel.HasValue)
190  configuration.MinimumLevel.Is(logEventLevel.Value);
191 
192  if (microsoftEventLevel.HasValue)
193  configuration.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
194 
195  builder.AddSerilog(configuration.CreateLogger(), true);
196  });
197 
198  //configure bearer token validation
199  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
200  {
201  //this line isn't actually run until the first request is made
202  //at that point tokenFactory will be populated
203  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
204  jwtBearerOptions.Events = new JwtBearerEvents
205  {
206  //Application is our composition root so this monstrosity of a line is okay
207  OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService<IClaimsInjector>().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted)
208  };
209  });
210  //fucking converts 'sub' to M$ bs
211  //can't be done in the above lambda, that's too late
212  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
213 
214  //add mvc, configure the json serializer settings
215  services.AddMvc().AddJsonOptions(options =>
216  {
217  options.AllowInputFormatterExceptionMessages = true;
218  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
219  options.SerializerSettings.CheckAdditionalContent = true;
220  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
221  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
222  options.SerializerSettings.Converters = new[] { new VersionConverter() };
223  });
224 
225  void AddTypedContext<TContext>() where TContext : DatabaseContext<TContext>
226  {
227  services.AddDbContext<TContext>(builder =>
228  {
229  if (hostingEnvironment.IsDevelopment())
230  builder.EnableSensitiveDataLogging();
231  });
232  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
233  }
234 
235  //add the correct database context type
236  var dbType = databaseConfiguration.DatabaseType;
237  switch (dbType)
238  {
239  case DatabaseType.MySql:
240  case DatabaseType.MariaDB:
241  AddTypedContext<MySqlDatabaseContext>();
242  break;
243  case DatabaseType.SqlServer:
244  AddTypedContext<SqlServerDatabaseContext>();
245  break;
246  default:
247  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
248  }
249 
250  //configure other database services
251  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
252  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
253 
254  //configure security services
256  services.AddScoped<IClaimsInjector, ClaimsInjector>();
257  services.AddSingleton<IIdentityCache, IdentityCache>();
258  services.AddSingleton<ICryptographySuite, CryptographySuite>();
259  services.AddSingleton<ITokenFactory, TokenFactory>();
260  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
261 
262  //configure platform specific services
263  if (platformIdentifier.IsWindows)
264  {
265  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
266  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
267  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
268  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
269 
270  services.AddSingleton<WindowsNetworkPromptReaper>();
271  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
272  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
273  }
274  else
275  {
276  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
277  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
278  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
279  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
280  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
281  }
282 
283  //configure misc services
284  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
285  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
286  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
287  services.AddSingleton<IByondTopicSender>(new ByondTopicSender
288  {
289  ReceiveTimeout = generalConfiguration.ByondTopicTimeout,
290  SendTimeout = generalConfiguration.ByondTopicTimeout
291  });
292 
293  //configure component services
294  services.AddSingleton<ICredentialsProvider, CredentialsProvider>();
295  services.AddSingleton<IProviderFactory, ProviderFactory>();
296  services.AddSingleton<IChatFactory, ChatFactory>();
297  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
298  services.AddSingleton<IInstanceFactory, InstanceFactory>();
299 
300  //configure root services
301  services.AddSingleton<InstanceManager>();
302  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
303  services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
304 
305  services.AddSingleton<IJobManager, JobManager>();
306  }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
+

References Tgstation.Server.Host.Configuration.ControlPanelConfiguration.AllowAnyOrigin, Tgstation.Server.Host.Configuration.ControlPanelConfiguration.AllowedOrigins, Tgstation.Server.Host.Core.ISetupWizard.CheckRunWizard(), Tgstation.Server.Host.IO.IIOManager.ConcatPath(), Tgstation.Server.Host.Configuration.DatabaseConfiguration.DatabaseType, Tgstation.Server.Host.Configuration.FileLoggingConfiguration.Directory, Tgstation.Server.Host.Configuration.FileLoggingConfiguration.Disable, Tgstation.Server.Host.Core.IPlatformIdentifier.IsWindows, Tgstation.Server.Host.Configuration.FileLoggingConfiguration.LogLevel, Tgstation.Server.Host.Configuration.FileLoggingConfiguration.MicrosoftLogLevel, and Tgstation.Server.Host.Security.ITokenFactory.ValidationParameters.

+
89  {
90  if (services == null)
91  throw new ArgumentNullException(nameof(services));
92 
93  //needful
94  services.AddSingleton<IApplication>(this);
95 
96  //configure configuration
97  services.UseStandardConfig<UpdatesConfiguration>(configuration);
98  services.UseStandardConfig<DatabaseConfiguration>(configuration);
99  services.UseStandardConfig<GeneralConfiguration>(configuration);
100  services.UseStandardConfig<FileLoggingConfiguration>(configuration);
101  services.UseStandardConfig<ControlPanelConfiguration>(configuration);
102 
103  //enable options which give us config reloading
104  services.AddOptions();
105 
106  //other stuff needed for for setup wizard and configuration
107  services.AddSingleton<IIOManager, DefaultIOManager>();
108  services.AddSingleton<IConsole, IO.Console>();
109  services.AddSingleton<IDBConnectionFactory, DBConnectionFactory>();
110  services.AddSingleton<ISetupWizard, SetupWizard>();
111  services.AddSingleton<IPlatformIdentifier, PlatformIdentifier>();
112  services.AddSingleton<IAsyncDelayer, AsyncDelayer>();
113 
114  GeneralConfiguration generalConfiguration;
115  DatabaseConfiguration databaseConfiguration;
116  FileLoggingConfiguration fileLoggingConfiguration;
117  ControlPanelConfiguration controlPanelConfiguration;
118  IIOManager ioManager;
119  IPlatformIdentifier platformIdentifier;
120 
121  //temporarily build the service provider in it's current state
122  //do it here so we can run the setup wizard if necessary
123  //also allows us to get some options and other services we need for continued configuration
124  using (var provider = services.BuildServiceProvider())
125  {
126  //run the wizard if necessary
127  var setupWizard = provider.GetRequiredService<ISetupWizard>();
128  var applicationLifetime = provider.GetRequiredService<Microsoft.AspNetCore.Hosting.IApplicationLifetime>();
129  var setupWizardRan = setupWizard.CheckRunWizard(applicationLifetime.ApplicationStopping).GetAwaiter().GetResult();
130 
131  //load the configuration options we need
132  var generalOptions = provider.GetRequiredService<IOptions<GeneralConfiguration>>();
133  generalConfiguration = generalOptions.Value;
134 
135  //unless this is set, in which case, we leave
136  if (setupWizardRan && generalConfiguration.SetupWizardMode == SetupWizardMode.Only)
137  //we don't inject a logger in the constuctor to log this because it's not yet configured
138  throw new OperationCanceledException("Exiting due to SetupWizardMode configuration!");
139 
140  var dbOptions = provider.GetRequiredService<IOptions<DatabaseConfiguration>>();
141  databaseConfiguration = dbOptions.Value;
142 
143  var loggingOptions = provider.GetRequiredService<IOptions<FileLoggingConfiguration>>();
144  fileLoggingConfiguration = loggingOptions.Value;
145 
146  var controlPanelOptions = provider.GetRequiredService<IOptions<ControlPanelConfiguration>>();
147  controlPanelConfiguration = controlPanelOptions.Value;
148 
149  ioManager = provider.GetRequiredService<IIOManager>();
150  platformIdentifier = provider.GetRequiredService<IPlatformIdentifier>();
151  }
152 
153  //setup file logging via serilog
154  if (!fileLoggingConfiguration.Disable)
155  services.AddLogging(builder =>
156  {
157  //common app data is C:/ProgramData on windows, else /usr/shar
158  var logPath = !String.IsNullOrEmpty(fileLoggingConfiguration.Directory) ? fileLoggingConfiguration.Directory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
159 
160  logPath = ioManager.ConcatPath(logPath, "tgs-{Date}.log");
161 
162  LogEventLevel? ConvertLogLevel(LogLevel logLevel)
163  {
164  switch (logLevel)
165  {
166  case LogLevel.Critical:
167  return LogEventLevel.Fatal;
168  case LogLevel.Debug:
169  return LogEventLevel.Debug;
170  case LogLevel.Error:
171  return LogEventLevel.Error;
172  case LogLevel.Information:
173  return LogEventLevel.Information;
174  case LogLevel.Trace:
175  return LogEventLevel.Verbose;
176  case LogLevel.Warning:
177  return LogEventLevel.Warning;
178  case LogLevel.None:
179  return null;
180  default:
181  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel));
182  }
183  };
184 
185  var logEventLevel = ConvertLogLevel(fileLoggingConfiguration.LogLevel);
186  var microsoftEventLevel = ConvertLogLevel(fileLoggingConfiguration.MicrosoftLogLevel);
187 
188  var formatter = new MessageTemplateTextFormatter("{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", null);
189 
190  var configuration = new LoggerConfiguration()
191  .Enrich.FromLogContext()
192  .WriteTo.Async(w => w.RollingFile(formatter, logPath, shared: true, flushToDiskInterval: TimeSpan.FromSeconds(2)));
193 
194  if (logEventLevel.HasValue)
195  configuration.MinimumLevel.Is(logEventLevel.Value);
196 
197  if (microsoftEventLevel.HasValue)
198  configuration.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
199 
200  builder.AddSerilog(configuration.CreateLogger(), true);
201  });
202 
203  //configure bearer token validation
204  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
205  {
206  //this line isn't actually run until the first request is made
207  //at that point tokenFactory will be populated
208  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
209  jwtBearerOptions.Events = new JwtBearerEvents
210  {
211  //Application is our composition root so this monstrosity of a line is okay
212  OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService<IClaimsInjector>().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted)
213  };
214  });
215  //fucking converts 'sub' to M$ bs
216  //can't be done in the above lambda, that's too late
217  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
218 
219  //add mvc, configure the json serializer settings
220  services.AddMvc().AddJsonOptions(options =>
221  {
222  options.AllowInputFormatterExceptionMessages = true;
223  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
224  options.SerializerSettings.CheckAdditionalContent = true;
225  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
226  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
227  options.SerializerSettings.Converters = new[] { new VersionConverter() };
228  });
229 
230  //enable browser detection
231  services.AddDetectionCore().AddBrowser();
232 
233  //enable CORS if necessary
234  if (controlPanelConfiguration.AllowAnyOrigin || controlPanelConfiguration.AllowedOrigins?.Count > 0)
235  services.AddCors();
236 
237  void AddTypedContext<TContext>() where TContext : DatabaseContext<TContext>
238  {
239  services.AddDbContext<TContext>(builder =>
240  {
241  if (hostingEnvironment.IsDevelopment())
242  builder.EnableSensitiveDataLogging();
243  });
244  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
245  }
246 
247  //add the correct database context type
248  var dbType = databaseConfiguration.DatabaseType;
249  switch (dbType)
250  {
251  case DatabaseType.MySql:
252  case DatabaseType.MariaDB:
253  AddTypedContext<MySqlDatabaseContext>();
254  break;
255  case DatabaseType.SqlServer:
256  AddTypedContext<SqlServerDatabaseContext>();
257  break;
258  default:
259  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
260  }
261  //configure other database services
262  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
263  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
264 
265  //configure security services
267  services.AddScoped<IClaimsInjector, ClaimsInjector>();
268  services.AddSingleton<IIdentityCache, IdentityCache>();
269  services.AddSingleton<ICryptographySuite, CryptographySuite>();
270  services.AddSingleton<ITokenFactory, TokenFactory>();
271  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
272 
273  //configure platform specific services
274  if (platformIdentifier.IsWindows)
275  {
276  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
277  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
278  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
279  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
280 
281  services.AddSingleton<WindowsNetworkPromptReaper>();
282  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
283  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
284  }
285  else
286  {
287  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
288  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
289  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
290  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
291  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
292  }
293 
294  //configure misc services
295  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
296  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
297  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
298  services.AddSingleton<IByondTopicSender>(new ByondTopicSender
299  {
300  ReceiveTimeout = generalConfiguration.ByondTopicTimeout,
301  SendTimeout = generalConfiguration.ByondTopicTimeout
302  });
303 
304  //configure component services
305  services.AddSingleton<ICredentialsProvider, CredentialsProvider>();
306  services.AddSingleton<IProviderFactory, ProviderFactory>();
307  services.AddSingleton<IChatFactory, ChatFactory>();
308  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
309  services.AddSingleton<IInstanceFactory, InstanceFactory>();
310 
311  //configure root services
312  services.AddSingleton<InstanceManager>();
313  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
314  services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
315 
316  services.AddSingleton<IJobManager, JobManager>();
317  }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
@@ -302,6 +309,7 @@ Private Attributes
IPostWriteHandler for POSIX systems
+
List< string > AllowedOrigins
Origins allowed for CORS requests
SetupWizardMode
Determines if the Core.ISetupWizard will run
@@ -309,16 +317,15 @@ Private Attributes
LogLevel LogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs
ITokenFactory tokenFactory
The ITokenFactory for the Application
Definition: Application.cs:66
-
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
For initially seeding a database
-
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the DatabaseConfiguration re...
ISystemIdentityFactory for windows systems. Uses long running tasks due to potential networked domain...
Contains various cryptographic functions
+
bool AllowAnyOrigin
If any origin is allowed for CORS requests. This overrides AllowedOrigins
Configuration for the automatic update system
For downloading and installing BYOND extractions for a given system
For accessing the disk in a synchronous manner
@@ -327,9 +334,9 @@ Private Attributes -
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the UpdatesConfiguration res...
IPostWriteHandler for Windows systems
+ @@ -349,7 +356,6 @@ Private Attributes
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the FileLoggingConfiguration...
Interface for using filesystems
Definition: IIOManager.cs:11
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
@@ -371,7 +377,7 @@ Private Attributes void Tgstation.Server.Host.Core.Application.Ready ( - Exception  + Exception  initializationError) @@ -381,15 +387,15 @@ Private Attributes

Mark the IApplication as ready to run

Parameters
- +
initializationErrorThe Exception that put the application in a corrupted state if any
initializationErrorThe Exception that put the application in a corrupted state if any

Implements Tgstation.Server.Host.Core.IApplication.

-

Definition at line 361 of file Application.cs.

-
362  {
363  lock (startupTcs)
364  {
365  if (startupTcs.Task.IsCompleted)
366  throw new InvalidOperationException("Ready has already been called!");
367  if (initializationError == null)
368  startupTcs.SetResult(null);
369  else
370  startupTcs.SetException(initializationError);
371  }
372  }
readonly TaskCompletionSource< object > startupTcs
The TaskCompletionSource<TResult> used for determining when the Application is Ready(Exception) ...
Definition: Application.cs:61
+

Definition at line 406 of file Application.cs.

+
407  {
408  lock (startupTcs)
409  {
410  if (startupTcs.Task.IsCompleted)
411  throw new InvalidOperationException("Ready has already been called!");
412  if (initializationError == null)
413  startupTcs.SetResult(null);
414  else
415  startupTcs.SetException(initializationError);
416  }
417  }
readonly TaskCompletionSource< object > startupTcs
The TaskCompletionSource<TResult> used for determining when the Application is Ready(Exception) ...
Definition: Application.cs:61
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_exception.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_exception.html index 77ca37c1b1..90f3f726f6 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_exception.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_exception.html @@ -107,7 +107,6 @@ Inheritance diagram for Tgstation.Server.Host.Core.JobException:
-Exception
@@ -119,7 +118,7 @@ Public Member Functions - +
 JobException (string message)
 Construct a JobException with a message More...
 
 JobException (string message, Exception innerException)
 JobException (string message, Exception innerException)
 Construct a JobException with a message and innerException More...
 
@@ -164,7 +163,7 @@ Public Member Functions

Construct a JobException with a message

Parameters
- +
messageThe message for the Exception
messageThe message for the Exception
@@ -186,7 +185,7 @@ Public Member Functions - Exception  + Exception  innerException  @@ -200,8 +199,8 @@ Public Member Functions

Construct a JobException with a message and innerException

Parameters
- - + +
messageThe message for the Exception
innerExceptionThe inner Exception for the nase Exception
messageThe message for the Exception
innerExceptionThe inner Exception for the nase Exception
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_exception.png b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_exception.png index 39beaacdc1690d67a2262e4ff7aab12e1cd130dc..d411c7a940e6993276e151914e1bb08beb425a75 100644 GIT binary patch delta 641 zcmcb?dWm&HOnsuKi(^OyF)mbVoVuZdiEcy?fcub zP)_>~6MqT7;I~UD_1Xy_(1L#@v%?HG3C2A2gHOCH{7kGsph8_E_Dp`EJu} zcQCU5Jie^jXPQNV{60Ik9PjtjjybDJFE_`lM9i4dYJ9^gX;JdvSqh7 zR=v^t^mXz7BX+u<4(`j!{j!7k&1a?eMz+W0a;JaaIRE|ID#Nm7V~tOn_u876=--tq z;s1K`-n``Be(z>IT&TRQ^@d48{z^X6>YJsXZl$@k-TAw~clzGiODB)lZNK4PU&c`H z>MzlE<9HeC2l20JWtyT5``46jWUPzt-{EjnzE$uaw@rr&Y;p?dQ3vU2^i0>n{)O-TVAghpT8yy`IvfFIKTTwrju9 z72(^!eOu$@{jR;+x5&SFtAFqwSJkDH^7Y#$*RyU@nfvk=e_4X}rRpal(Ua~ig1f@{ z+a{fEYsNj^<_z_pI%{|DeDbZccKNpxZ$+%FEF0um%^Jk<;CeL4153R$RqK@)D}oc4 zD*{3I*g0N44fmaU<9iQZ-p^uiX&+xg|Koo<7oM8^$oRm@bAQZkzMEgp%@`7Oy#AQ& z@qdjA<)jnV74MgR^LO&Mjz?0b9_LHP{=dFFcGISvxn8Cr$f@^b&=$-g(3+>CV2VV)dodst6SMEd7ZuH7H^>R;j8 z{Vu|`O#J7kOY$3+yV~6d-`Xoz-e9Z$N$>LW8;k4TeyV=+yt=w4_90JK&9?3BZRfnV zv)ek^-oE|(#is50i(&qn`$^0-`jMiIL3_#%N M>gTe~DS@#G08|n-ZU6uP diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_manager.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_manager.html index 93f5431a01..581725e9f5 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_manager.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_job_manager.html @@ -432,8 +432,7 @@ Private Attributes

Definition at line 71 of file JobManager.cs.

References Tgstation.Server.Api.Models.Internal.Job.Cancelled, Tgstation.Server.Api.Models.Internal.Job.Description, Tgstation.Server.Api.Models.Internal.Job.ExceptionDetails, Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Models.Internal.Job.Id, Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Host.Models.Job.Instance, Tgstation.Server.Host.Models.Job.PostComplete, Tgstation.Server.Api.Models.Internal.Job.StartedAt, Tgstation.Server.Host.Models.Job.StartedBy, and Tgstation.Server.Api.Models.Internal.Job.StoppedAt.

-
72  {
73  try
74  {
75  await databaseContextFactory.UseContext(async databaseContext =>
76  {
77  async Task HandleExceptions(Task task)
78  {
79  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
80  try
81  {
82  await task.ConfigureAwait(false);
83  }
84  catch (OperationCanceledException)
85  {
86  logger.LogDebug("Job {0} cancelled!", job.Id);
87  job.Cancelled = true;
88  }
89  catch (JobException e)
90  {
91  job.ExceptionDetails = e.Message;
92  LogRegularException();
93  if (e.InnerException != null)
94  logger.LogDebug("Inner exception for job {0}: {1}", job.Id, e.InnerException);
95  }
96  catch (Exception e)
97  {
98  job.ExceptionDetails = e.ToString();
99  LogRegularException();
100  }
101  finally
102  {
103  job.StoppedAt = DateTimeOffset.Now;
104  }
105  }
106 
107  async Task RunJobInternal()
108  {
109  var oldJob = job;
110  job = new Job { Id = oldJob.Id };
111  databaseContext.Jobs.Attach(job);
112 
113  await operation(job, databaseContext, cancellationToken).ConfigureAwait(false);
114 
115  logger.LogDebug("Job {0} completed!", job.Id);
116  };
117 
118  await HandleExceptions(RunJobInternal()).ConfigureAwait(false);
119 
120  await databaseContext.Save(default).ConfigureAwait(false);
121 
122  bool JobErroredOrCancelled() => job.ExceptionDetails != null || job.Cancelled == true;
123 
124  //ok so, now it's time for the post commit step if it exists
125  if (!JobErroredOrCancelled() && job.PostComplete != null)
126  {
127  await HandleExceptions(job.PostComplete(cancellationToken)).ConfigureAwait(false);
128  if (JobErroredOrCancelled())
129  await databaseContext.Save(default).ConfigureAwait(false);
130  }
131  }).ConfigureAwait(false);
132  }
133  finally
134  {
135  lock (this)
136  {
137  var handler = jobs[job.Id];
138  jobs.Remove(job.Id);
139  handler.Dispose();
140  }
141  }
142  }
-
readonly Dictionary< long, JobHandler > jobs
Dictionary<TKey, TValue> of Api.Models.Internal.Job.Id to running JobHandlers
Definition: JobManager.cs:28
+
72  {
73  try
74  {
75  await databaseContextFactory.UseContext(async databaseContext =>
76  {
77  async Task HandleExceptions(Task task)
78  {
79  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
80  try
81  {
82  await task.ConfigureAwait(false);
83  }
84  catch (OperationCanceledException)
85  {
86  logger.LogDebug("Job {0} cancelled!", job.Id);
87  job.Cancelled = true;
88  }
89  catch (JobException e)
90  {
91  job.ExceptionDetails = e.Message;
92  LogRegularException();
93  if (e.InnerException != null)
94  logger.LogDebug("Inner exception for job {0}: {1}", job.Id, e.InnerException);
95  }
96  catch (Exception e)
97  {
98  job.ExceptionDetails = e.ToString();
99  LogRegularException();
100  }
101  finally
102  {
103  job.StoppedAt = DateTimeOffset.Now;
104  }
105  }
106 
107  async Task RunJobInternal()
108  {
109  var oldJob = job;
110  job = new Job { Id = oldJob.Id };
111  databaseContext.Jobs.Attach(job);
112 
113  await operation(job, databaseContext, cancellationToken).ConfigureAwait(false);
114 
115  logger.LogDebug("Job {0} completed!", job.Id);
116  };
117 
118  await HandleExceptions(RunJobInternal()).ConfigureAwait(false);
119 
120  await databaseContext.Save(default).ConfigureAwait(false);
121 
122  bool JobErroredOrCancelled() => job.ExceptionDetails != null || job.Cancelled == true;
123 
124  //ok so, now it's time for the post commit step if it exists
125  if (!JobErroredOrCancelled() && job.PostComplete != null)
126  {
127  await HandleExceptions(job.PostComplete(cancellationToken)).ConfigureAwait(false);
128  if (JobErroredOrCancelled())
129  await databaseContext.Save(default).ConfigureAwait(false);
130  }
131  }).ConfigureAwait(false);
132  }
133  finally
134  {
135  lock (this)
136  {
137  var handler = jobs[job.Id];
138  jobs.Remove(job.Id);
139  handler.Dispose();
140  }
141  }
142  }
readonly Dictionary< long, JobHandler > jobs
Dictionary<TKey, TValue> of Api.Models.Internal.Job.Id to running JobHandlers
Definition: JobManager.cs:28
DateTimeOffset StoppedAt
When the Job stopped
Definition: Job.cs:37
string ExceptionDetails
Details of any exceptions caught during the Job
Definition: Job.cs:26
Func< CancellationToken, Task > PostComplete
A Task to run after the job completes. This will not affect the Api.Models.Internal.Job.StoppedAt time, unless it is cancelled or errors
Definition: Job.cs:34
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_process.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_process.html index 0835577334..bcbdb145a0 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_process.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_process.html @@ -344,8 +344,7 @@ Private Attributes

Implements Tgstation.Server.Host.Core.IProcessBase.

Definition at line 106 of file Process.cs.

-
107  {
108  try
109  {
110  handle.PriorityClass = System.Diagnostics.ProcessPriorityClass.AboveNormal;
111  logger.LogTrace("Set to above normal priority", handle.Id);
112  }
113  catch (Exception e)
114  {
115  logger.LogWarning("Unable to raise process priority! Exception: {0}", e);
116  }
117  }
- +
107  {
108  try
109  {
110  handle.PriorityClass = System.Diagnostics.ProcessPriorityClass.AboveNormal;
111  logger.LogTrace("Set to above normal priority", handle.Id);
112  }
113  catch (Exception e)
114  {
115  logger.LogWarning("Unable to raise process priority! Exception: {0}", e);
116  }
117  }
readonly ILogger< Process > logger
The ILogger for the Process
Definition: Process.cs:29
readonly System.Diagnostics.Process handle
Definition: Process.cs:20
@@ -370,8 +369,7 @@ Private Attributes

Implements Tgstation.Server.Host.Core.IProcess.

Definition at line 90 of file Process.cs.

-
91  {
92  if (handle.HasExited)
93  return;
94  try
95  {
96  logger.LogTrace("Terminating process...");
97  handle.Kill();
98  handle.WaitForExit();
99  }
100  catch (Exception e)
101  {
102  logger.LogDebug("Process termination exception: {0}", e);
103  }
104  }
-
readonly ILogger< Process > logger
The ILogger for the Process
Definition: Process.cs:29
+
91  {
92  if (handle.HasExited)
93  return;
94  try
95  {
96  logger.LogTrace("Terminating process...");
97  handle.Kill();
98  handle.WaitForExit();
99  }
100  catch (Exception e)
101  {
102  logger.LogDebug("Process termination exception: {0}", e);
103  }
104  }
readonly ILogger< Process > logger
The ILogger for the Process
Definition: Process.cs:29
readonly System.Diagnostics.Process handle
Definition: Process.cs:20
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_process_executor.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_process_executor.html index dc96fefe61..d4bec3140b 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_process_executor.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_process_executor.html @@ -238,8 +238,7 @@ Private Attributes

Implements Tgstation.Server.Host.Core.IProcessExecutor.

Definition at line 58 of file ProcessExecutor.cs.

-
59  {
60  logger.LogDebug("Attaching to process {0}...", id);
61  System.Diagnostics.Process handle;
62  try
63  {
64  handle = System.Diagnostics.Process.GetProcessById(id);
65  }
66  catch(Exception e)
67  {
68  logger.LogDebug("Unable to get process {0}! Exception: {1}", id, e);
69  return null;
70  }
71  try
72  {
73  return new Process(handle, AttachExitHandler(handle), null, null, null, loggerFactory.CreateLogger<Process>(), true);
74  }
75  catch
76  {
77  handle.Dispose();
78  throw;
79  }
80  }
-
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProcessExecutor
+
59  {
60  logger.LogDebug("Attaching to process {0}...", id);
61  System.Diagnostics.Process handle;
62  try
63  {
64  handle = System.Diagnostics.Process.GetProcessById(id);
65  }
66  catch(Exception e)
67  {
68  logger.LogDebug("Unable to get process {0}! Exception: {1}", id, e);
69  return null;
70  }
71  try
72  {
73  return new Process(handle, AttachExitHandler(handle), null, null, null, loggerFactory.CreateLogger<Process>(), true);
74  }
75  catch
76  {
77  handle.Dispose();
78  throw;
79  }
80  }
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProcessExecutor
static Task< int > AttachExitHandler(System.Diagnostics.Process handle)
Create a Task<TResult> resulting in the exit code of a given handle
readonly ILogger< ProcessExecutor > logger
The ILogger for the ProcessExecutor
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_service_collection_extensions-members.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_service_collection_extensions-members.html new file mode 100644 index 0000000000..176fdf4e19 --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_service_collection_extensions-members.html @@ -0,0 +1,110 @@ + + + + + + +tgstation-server: Member List + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
Tgstation.Server.Host.Core.ServiceCollectionExtensions Member List
+
+
+ +

This is the complete list of members for Tgstation.Server.Host.Core.ServiceCollectionExtensions, including all inherited members.

+ + +
UseStandardConfig< TConfig >(this IServiceCollection serviceCollection, IConfiguration configuration)Tgstation.Server.Host.Core.ServiceCollectionExtensionsstatic
+ + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_service_collection_extensions.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_service_collection_extensions.html new file mode 100644 index 0000000000..1143b40e69 --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_service_collection_extensions.html @@ -0,0 +1,191 @@ + + + + + + +tgstation-server: Tgstation.Server.Host.Core.ServiceCollectionExtensions Class Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
Tgstation.Server.Host.Core.ServiceCollectionExtensions Class Reference
+
+
+ +

Extensions for IServiceCollection + More...

+ + + + + +

+Static Public Member Functions

static IServiceCollection UseStandardConfig< TConfig > (this IServiceCollection serviceCollection, IConfiguration configuration)
 Add a standard TConfig binding More...
 
+

Detailed Description

+

Extensions for IServiceCollection

+ +

Definition at line 12 of file ServiceCollectionExtensions.cs.

+

Member Function Documentation

+ +
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + +
static IServiceCollection Tgstation.Server.Host.Core.ServiceCollectionExtensions.UseStandardConfig< TConfig > (this IServiceCollection serviceCollection,
IConfiguration configuration 
)
+
+static
+
+ +

Add a standard TConfig binding

+
Template Parameters
+ + +
TConfigThe to bind. Must have a const/static string field named "Section"
+
+
+
Parameters
+ + + +
serviceCollectionThe IServiceCollection to configure
configurationThe IConfiguration containing the TConfig
+
+
+
Returns
serviceCollection
+
+
Type Constraints
+ + +
TConfig :class 
+
+
+
+ +

Definition at line 21 of file ServiceCollectionExtensions.cs.

+ +

References Tgstation.Server.Host.Configuration.GeneralConfiguration.Section.

+
21  : class
22  {
23  if (serviceCollection == null)
24  throw new ArgumentNullException(nameof(serviceCollection));
25  if (configuration == null)
26  throw new ArgumentNullException(nameof(configuration));
27 
28  const string SectionFieldName = nameof(GeneralConfiguration.Section);
29 
30  var configType = typeof(TConfig);
31  var sectionField = configType.GetField(SectionFieldName);
32  if (sectionField == null)
33  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has no {1} field!", configType, SectionFieldName));
34 
35  var stringType = typeof(string);
36  if (sectionField.FieldType != stringType)
37  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has invalid {1} field type, must be {2}!", configType, SectionFieldName, stringType));
38 
39  var sectionName = (string)sectionField.GetValue(null);
40 
41  return serviceCollection.Configure<TConfig>(configuration.GetSection(sectionName));
42  }
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
+ +
+
+
+
The documentation for this class was generated from the following file: +
+ + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_setup_wizard-members.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_setup_wizard-members.html index d462b8e696..aa6f1d6dc5 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_setup_wizard-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_setup_wizard-members.html @@ -101,21 +101,22 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); applicationTgstation.Server.Host.Core.SetupWizardprivate asyncDelayerTgstation.Server.Host.Core.SetupWizardprivate CheckRunWizard(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizard - ConfigureDatabase(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate - ConfigureGeneral(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate - ConfigureLogging(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate - consoleTgstation.Server.Host.Core.SetupWizardprivate - dbConnectionFactoryTgstation.Server.Host.Core.SetupWizardprivate - generalConfigurationTgstation.Server.Host.Core.SetupWizardprivate - hostingEnvironmentTgstation.Server.Host.Core.SetupWizardprivate - ioManagerTgstation.Server.Host.Core.SetupWizardprivate - loggerTgstation.Server.Host.Core.SetupWizardprivate - platformIdentifierTgstation.Server.Host.Core.SetupWizardprivate - PromptForHostingPort(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate - PromptYesNo(string question, CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate - RunWizard(string userConfigFileName, CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate - SaveConfiguration(string userConfigFileName, ushort?hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate - SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, IDBConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, ILogger< SetupWizard > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)Tgstation.Server.Host.Core.SetupWizard + ConfigureControlPanel(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate + ConfigureDatabase(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate + ConfigureGeneral(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate + ConfigureLogging(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate + consoleTgstation.Server.Host.Core.SetupWizardprivate + dbConnectionFactoryTgstation.Server.Host.Core.SetupWizardprivate + generalConfigurationTgstation.Server.Host.Core.SetupWizardprivate + hostingEnvironmentTgstation.Server.Host.Core.SetupWizardprivate + ioManagerTgstation.Server.Host.Core.SetupWizardprivate + loggerTgstation.Server.Host.Core.SetupWizardprivate + platformIdentifierTgstation.Server.Host.Core.SetupWizardprivate + PromptForHostingPort(CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate + PromptYesNo(string question, CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate + RunWizard(string userConfigFileName, CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate + SaveConfiguration(string userConfigFileName, ushort?hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)Tgstation.Server.Host.Core.SetupWizardprivate + SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, IDBConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, ILogger< SetupWizard > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)Tgstation.Server.Host.Core.SetupWizard
+ +
+
+ + + + + +
+ + + + + + + + +
async Task<ControlPanelConfiguration> Tgstation.Server.Host.Core.SetupWizard.ConfigureControlPanel (CancellationToken cancellationToken)
+
+private
+
+ +

Prompts the user to create a ControlPanelConfiguration

+
Parameters
+ + +
cancellationTokenThe CancellationToken for the operation
+
+
+
Returns
A Task<TResult> resulting in the new ControlPanelConfiguration
+ +

Definition at line 457 of file SetupWizard.cs.

+ +

References Tgstation.Server.Host.Configuration.ControlPanelConfiguration.AllowedOrigins.

+
458  {
459  var config = new ControlPanelConfiguration
460  {
461  Enable = await PromptYesNo("Enable the web control panel? (y/n): ", cancellationToken).ConfigureAwait(false),
462  AllowAnyOrigin = await PromptYesNo("Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *) (y/n): ", cancellationToken).ConfigureAwait(false)
463  };
464 
465  if (!config.AllowAnyOrigin)
466  {
467  await console.WriteAsync("Enter a comma seperated list of CORS allowed origins (optional): ", false, cancellationToken).ConfigureAwait(false);
468  var commaSeperatedOrigins = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
469  if (!String.IsNullOrWhiteSpace(commaSeperatedOrigins))
470  {
471  var splits = commaSeperatedOrigins.Split(',');
472  config.AllowedOrigins = new List<string>(splits.Select(x => x.Trim()));
473  }
474  }
475 
476  return config;
477  }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+
async Task< bool > PromptYesNo(string question, CancellationToken cancellationToken)
A prompt for a yes or no value
Definition: SetupWizard.cs:99
+
Task< string > ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken)
Read a line from the IConsole
+ +
Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken)
Write some text to the IConsole
@@ -343,8 +388,7 @@ Private Attributes
Returns
A Task<TResult> resulting in the new DatabaseConfiguration

Definition at line 144 of file SetupWizard.cs.

-
145  {
146  do
147  {
148  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
149  await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
150 
151  var databaseConfiguration = new DatabaseConfiguration();
152  do
153  {
154  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Please enter one of {0}, {1}, or {2}: ", DatabaseType.MariaDB, DatabaseType.SqlServer, DatabaseType.MySql), false, cancellationToken).ConfigureAwait(false);
155  var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
156  if (Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
157  {
158  databaseConfiguration.DatabaseType = databaseType;
159  break;
160  }
161  await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
162  }
163  while (true);
164 
165  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
166  await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false);
167  var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
168  if (String.IsNullOrWhiteSpace(serverAddress))
169  serverAddress = null;
170 
171  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
172  await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
173  string databaseName;
174 
175  do
176  {
177  databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
178  if (!String.IsNullOrWhiteSpace(databaseName))
179  break;
180  await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
181  }
182  while (true);
183 
184  var dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false);
185 
186  bool useWinAuth;
187  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
188  useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
189  else
190  useWinAuth = false;
191 
192  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
193 
194  string username = null;
195  string password = null;
196  if (!useWinAuth)
197  {
198  await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
199  username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
200  await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
201  password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
202  }
203  else
204  {
205  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);
206  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);
207  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);
208  }
209  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
210 
211  DbConnection testConnection;
212  void CreateTestConnection(string connectionString)
213  {
214  testConnection = dbConnectionFactory.CreateConnection(connectionString, databaseConfiguration.DatabaseType);
215  }
216 
217  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
218  {
219  var csb = new SqlConnectionStringBuilder
220  {
221  ApplicationName = application.VersionPrefix,
222  DataSource = serverAddress ?? "(local)"
223  };
224  if (useWinAuth)
225  csb.IntegratedSecurity = true;
226  else
227  {
228  csb.UserID = username;
229  csb.Password = password;
230  }
231 
232  CreateTestConnection(csb.ConnectionString);
233  csb.InitialCatalog = databaseName;
234  databaseConfiguration.ConnectionString = csb.ConnectionString;
235  }
236  else
237  {
238  var csb = new MySqlConnectionStringBuilder
239  {
240  Server = serverAddress ?? "127.0.0.1",
241  UserID = username,
242  Password = password
243  };
244 
245  CreateTestConnection(csb.ConnectionString);
246  csb.Database = databaseName;
247  databaseConfiguration.ConnectionString = csb.ConnectionString;
248  }
249 
250  try
251  {
252  using (testConnection)
253  {
254  await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
255  await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
256  await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
257 
258  if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
259  {
260  await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
261  using (var command = testConnection.CreateCommand())
262  {
263  command.CommandText = "SELECT VERSION()";
264  var fullVersion = (string)(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
265  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
266  var splits = fullVersion.Split('-');
267  databaseConfiguration.MySqlServerVersion = splits[0];
268  }
269  }
270 
271  if (!dbExists)
272  {
273  await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
274  using (var command = testConnection.CreateCommand())
275  {
276  command.CommandText = String.Format(CultureInfo.InvariantCulture, "CREATE DATABASE {0}", databaseName);
277  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
278  }
279  await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
280  await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
281  using (var command = testConnection.CreateCommand())
282  {
283  command.CommandText = String.Format(CultureInfo.InvariantCulture, "DROP DATABASE {0}", databaseName);
284  try
285  {
286  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
287  }
288  catch (OperationCanceledException)
289  {
290  throw;
291  }
292  catch (Exception e)
293  {
294  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
295  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
296  await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
297  await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
298  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
299  }
300  }
301  }
302  }
303 
304  return databaseConfiguration;
305  }
306  catch (OperationCanceledException)
307  {
308  throw;
309  }
310  catch (Exception e)
311  {
312  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
313  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
314  await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
315  }
316  } while (true);
317  }
-
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+
145  {
146  do
147  {
148  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
149  await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
150 
151  var databaseConfiguration = new DatabaseConfiguration();
152  do
153  {
154  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Please enter one of {0}, {1}, or {2}: ", DatabaseType.MariaDB, DatabaseType.SqlServer, DatabaseType.MySql), false, cancellationToken).ConfigureAwait(false);
155  var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
156  if (Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
157  {
158  databaseConfiguration.DatabaseType = databaseType;
159  break;
160  }
161  await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
162  }
163  while (true);
164 
165  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
166  await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false);
167  var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
168  if (String.IsNullOrWhiteSpace(serverAddress))
169  serverAddress = null;
170 
171  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
172  await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
173  string databaseName;
174 
175  do
176  {
177  databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
178  if (!String.IsNullOrWhiteSpace(databaseName))
179  break;
180  await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
181  }
182  while (true);
183 
184  var dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false);
185 
186  bool useWinAuth;
187  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
188  useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
189  else
190  useWinAuth = false;
191 
192  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
193 
194  string username = null;
195  string password = null;
196  if (!useWinAuth)
197  {
198  await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
199  username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
200  await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
201  password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
202  }
203  else
204  {
205  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);
206  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);
207  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);
208  }
209  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
210 
211  DbConnection testConnection;
212  void CreateTestConnection(string connectionString)
213  {
214  testConnection = dbConnectionFactory.CreateConnection(connectionString, databaseConfiguration.DatabaseType);
215  }
216 
217  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
218  {
219  var csb = new SqlConnectionStringBuilder
220  {
221  ApplicationName = application.VersionPrefix,
222  DataSource = serverAddress ?? "(local)"
223  };
224  if (useWinAuth)
225  csb.IntegratedSecurity = true;
226  else
227  {
228  csb.UserID = username;
229  csb.Password = password;
230  }
231 
232  CreateTestConnection(csb.ConnectionString);
233  csb.InitialCatalog = databaseName;
234  databaseConfiguration.ConnectionString = csb.ConnectionString;
235  }
236  else
237  {
238  var csb = new MySqlConnectionStringBuilder
239  {
240  Server = serverAddress ?? "127.0.0.1",
241  UserID = username,
242  Password = password
243  };
244 
245  CreateTestConnection(csb.ConnectionString);
246  csb.Database = databaseName;
247  databaseConfiguration.ConnectionString = csb.ConnectionString;
248  }
249 
250  try
251  {
252  using (testConnection)
253  {
254  await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
255  await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
256  await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
257 
258  if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
259  {
260  await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
261  using (var command = testConnection.CreateCommand())
262  {
263  command.CommandText = "SELECT VERSION()";
264  var fullVersion = (string)(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
265  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
266  var splits = fullVersion.Split('-');
267  databaseConfiguration.MySqlServerVersion = splits[0];
268  }
269  }
270 
271  if (!dbExists)
272  {
273  await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
274  using (var command = testConnection.CreateCommand())
275  {
276  command.CommandText = String.Format(CultureInfo.InvariantCulture, "CREATE DATABASE {0}", databaseName);
277  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
278  }
279  await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
280  await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
281  using (var command = testConnection.CreateCommand())
282  {
283  command.CommandText = String.Format(CultureInfo.InvariantCulture, "DROP DATABASE {0}", databaseName);
284  try
285  {
286  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
287  }
288  catch (OperationCanceledException)
289  {
290  throw;
291  }
292  catch (Exception e)
293  {
294  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
295  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
296  await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
297  await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
298  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
299  }
300  }
301  }
302  }
303 
304  return databaseConfiguration;
305  }
306  catch (OperationCanceledException)
307  {
308  throw;
309  }
310  catch (Exception e)
311  {
312  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
313  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
314  await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
315  }
316  } while (true);
317  }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
string VersionPrefix
Prefix to VersionString
Definition: IApplication.cs:13
DbConnection CreateConnection(string connectionString, DatabaseType databaseType)
Create a DbConnection
@@ -395,7 +439,7 @@ Private Attributes

Definition at line 324 of file SetupWizard.cs.

References Tgstation.Server.Host.Configuration.GeneralConfiguration.MinimumPasswordLength.

-
325  {
326  var newGeneralConfiguration = new GeneralConfiguration
327  {
329  };
330  do
331  {
332  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
333  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
334  var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
335  if (String.IsNullOrWhiteSpace(passwordLengthString))
336  break;
337  if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
338  {
339  newGeneralConfiguration.MinimumPasswordLength = passwordLength;
340  break;
341  }
342  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
343  }
344  while (true);
345 
346  do
347  {
348  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
349  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);
350  var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
351  if (String.IsNullOrWhiteSpace(topicTimeoutString))
352  break;
353  if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
354  {
355  newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
356  break;
357  }
358  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
359  }
360  while (true);
361 
362  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
363  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);
364  await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
365  newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
366  if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
367  newGeneralConfiguration.GitHubAccessToken = null;
368  return newGeneralConfiguration;
369  }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+
325  {
326  var newGeneralConfiguration = new GeneralConfiguration
327  {
329  };
330 
331  do
332  {
333  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
334  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
335  var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
336  if (String.IsNullOrWhiteSpace(passwordLengthString))
337  break;
338  if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
339  {
340  newGeneralConfiguration.MinimumPasswordLength = passwordLength;
341  break;
342  }
343  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
344  }
345  while (true);
346 
347  do
348  {
349  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
350  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);
351  var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
352  if (String.IsNullOrWhiteSpace(topicTimeoutString))
353  break;
354  if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
355  {
356  newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
357  break;
358  }
359  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
360  }
361  while (true);
362 
363  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
364  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);
365  await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
366  newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
367  if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
368  newGeneralConfiguration.GitHubAccessToken = null;
369  return newGeneralConfiguration;
370  }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
SetupWizardMode
Determines if the Core.ISetupWizard will run
Task< string > ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken)
Read a line from the IConsole
Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken)
Write some text to the IConsole
@@ -434,9 +478,8 @@ Private Attributes
Returns
A Task<TResult> resulting in the new FileLoggingConfiguration
-

Definition at line 376 of file SetupWizard.cs.

-
377  {
378  var fileLoggingConfiguration = new FileLoggingConfiguration();
379  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
380  fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);
381 
382  if (!fileLoggingConfiguration.Disable)
383  {
384  do
385  {
386  await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
387  fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
388  if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
389  {
390  fileLoggingConfiguration.Directory = null;
391  break;
392  }
393  //test a write of it
394  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
395  await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);
396  try
397  {
398  await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);
399  var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
400  await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
401  try
402  {
403  await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
404  }
405  catch (OperationCanceledException)
406  {
407  throw;
408  }
409  catch (Exception e)
410  {
411  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);
412  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
413  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
414  }
415  break;
416  }
417  catch (OperationCanceledException)
418  {
419  throw;
420  }
421  catch (Exception e)
422  {
423  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
424  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
425  await console.WriteAsync("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
426  }
427  } while (true);
428 
429  async Task<LogLevel?> PromptLogLevel(string question)
430  {
431  do
432  {
433  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
434  await console.WriteAsync(question, true, cancellationToken).ConfigureAwait(false);
435  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);
436  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
437  if (String.IsNullOrWhiteSpace(responseString))
438  return null;
439  if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
440  return logLevel;
441  await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
442  } while (true);
443  }
444 
445  fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;
446  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;
447  }
448  return fileLoggingConfiguration;
449  }
-
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+

Definition at line 377 of file SetupWizard.cs.

+
378  {
379  var fileLoggingConfiguration = new FileLoggingConfiguration();
380  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
381  fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);
382 
383  if (!fileLoggingConfiguration.Disable)
384  {
385  do
386  {
387  await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
388  fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
389  if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
390  {
391  fileLoggingConfiguration.Directory = null;
392  break;
393  }
394  //test a write of it
395  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
396  await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);
397  try
398  {
399  await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);
400  var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
401  await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
402  try
403  {
404  await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
405  }
406  catch (OperationCanceledException)
407  {
408  throw;
409  }
410  catch (Exception e)
411  {
412  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);
413  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
414  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
415  }
416  break;
417  }
418  catch (OperationCanceledException)
419  {
420  throw;
421  }
422  catch (Exception e)
423  {
424  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
425  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
426  await console.WriteAsync("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
427  }
428  } while (true);
429 
430  async Task<LogLevel?> PromptLogLevel(string question)
431  {
432  do
433  {
434  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
435  await console.WriteAsync(question, true, cancellationToken).ConfigureAwait(false);
436  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);
437  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
438  if (String.IsNullOrWhiteSpace(responseString))
439  return null;
440  if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
441  return logLevel;
442  await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
443  } while (true);
444  }
445 
446  fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;
447  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;
448  }
449  return fileLoggingConfiguration;
450  }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content
Task CreateDirectory(string path, CancellationToken cancellationToken)
Create a directory at path
readonly IIOManager ioManager
The IIOManager for the SetupWizard
Definition: SetupWizard.cs:26
@@ -578,18 +621,19 @@ Private Attributes
Returns
A Task representing the running operation
-

Definition at line 520 of file SetupWizard.cs.

-
521  {
522  //welcome message
523  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
524  await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
525  await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
526 
527  var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
528 
529  var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
530 
531  var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
532 
533  var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false);
534 
535  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
536 
537  await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, cancellationToken).ConfigureAwait(false);
538  }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+

Definition at line 550 of file SetupWizard.cs.

+
551  {
552  //welcome message
553  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
554  await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
555  await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
556 
557  var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
558 
559  var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
560 
561  var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
562 
563  var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false);
564 
565  var controlPanelConfiguration = await ConfigureControlPanel(cancellationToken).ConfigureAwait(false);
566 
567  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
568 
569  await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, controlPanelConfiguration, cancellationToken).ConfigureAwait(false);
570  }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+
async Task< ControlPanelConfiguration > ConfigureControlPanel(CancellationToken cancellationToken)
Prompts the user to create a ControlPanelConfiguration
Definition: SetupWizard.cs:457
async Task< DatabaseConfiguration > ConfigureDatabase(CancellationToken cancellationToken)
Prompts the user to create a DatabaseConfiguration
Definition: SetupWizard.cs:144
Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken)
Write some text to the IConsole
-
async Task SaveConfiguration(string userConfigFileName, ushort?hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, CancellationToken cancellationToken)
Saves a given Configuration set to userConfigFileName
Definition: SetupWizard.cs:461
async Task< GeneralConfiguration > ConfigureGeneral(CancellationToken cancellationToken)
Prompts the user to create a GeneralConfiguration
Definition: SetupWizard.cs:324
+
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:490
async Task< ushort?> PromptForHostingPort(CancellationToken cancellationToken)
Prompts the user to enter the port to host TGS on
Definition: SetupWizard.cs:120
-
async Task< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration
Definition: SetupWizard.cs:376
+
async Task< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration
Definition: SetupWizard.cs:377
- +
@@ -626,6 +670,12 @@ Private Attributes + + + + + + @@ -653,22 +703,24 @@ Private Attributes +
FileLoggingConfiguration  fileLoggingConfiguration,
ControlPanelConfiguration controlPanelConfiguration,
databaseConfigurationThe DatabaseConfiguration to save
newGeneralConfigurationThe GeneralConfiguration to save
fileLoggingConfigurationThe FileLoggingConfiguration to save
controlPanelConfigurationThe ControlPanelConfiguration to save
cancellationTokenThe CancellationToken for the operation
Returns
A Task representing the running operation
-

Definition at line 461 of file SetupWizard.cs.

+

Definition at line 490 of file SetupWizard.cs.

-

References Tgstation.Server.Host.Configuration.DatabaseConfiguration.Section, Tgstation.Server.Host.Configuration.GeneralConfiguration.Section, and Tgstation.Server.Host.Configuration.FileLoggingConfiguration.Section.

-
462  {
463  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
464 
465  var map = new Dictionary<string, object>()
466  {
467  { DatabaseConfiguration.Section, databaseConfiguration },
468  { GeneralConfiguration.Section, newGeneralConfiguration },
469  { FileLoggingConfiguration.Section, fileLoggingConfiguration }
470  };
471 
472  if (hostingPort.HasValue)
473  map.Add("Kestrel", new
474  {
475  EndPoints = new
476  {
477  Http = new
478  {
479  Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort)
480  }
481  }
482  });
483 
484  var json = JsonConvert.SerializeObject(map, Formatting.Indented);
485  var configBytes = Encoding.UTF8.GetBytes(json);
486 
487  try
488  {
489  await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
490  }
491  catch (OperationCanceledException)
492  {
493  throw;
494  }
495  catch (Exception e)
496  {
497  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
498  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
499  await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
500  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
501  await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
502  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
503  await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
504  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
505  throw new OperationCanceledException();
506  }
507 
508  await console.WriteAsync("Waiting for configuration changes to reload...", true, cancellationToken).ConfigureAwait(false);
509 
510  //we need to wait for the configuration's file system watcher to read and reload the changes
511  await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
512  }
-
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+

References Tgstation.Server.Host.Configuration.ControlPanelConfiguration.Section, Tgstation.Server.Host.Configuration.GeneralConfiguration.Section, Tgstation.Server.Host.Configuration.DatabaseConfiguration.Section, and Tgstation.Server.Host.Configuration.FileLoggingConfiguration.Section.

+
491  {
492  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
493 
494  var map = new Dictionary<string, object>()
495  {
496  { DatabaseConfiguration.Section, databaseConfiguration },
497  { GeneralConfiguration.Section, newGeneralConfiguration },
498  { FileLoggingConfiguration.Section, fileLoggingConfiguration },
499  { ControlPanelConfiguration.Section, controlPanelConfiguration }
500  };
501 
502  if (hostingPort.HasValue)
503  map.Add("Kestrel", new
504  {
505  EndPoints = new
506  {
507  Http = new
508  {
509  Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort)
510  }
511  }
512  });
513 
514  var json = JsonConvert.SerializeObject(map, Formatting.Indented);
515  var configBytes = Encoding.UTF8.GetBytes(json);
516 
517  try
518  {
519  await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
520  }
521  catch (OperationCanceledException)
522  {
523  throw;
524  }
525  catch (Exception e)
526  {
527  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
528  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
529  await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
530  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
531  await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
532  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
533  await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
534  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
535  throw new OperationCanceledException();
536  }
537 
538  await console.WriteAsync("Waiting for configuration changes to reload...", true, cancellationToken).ConfigureAwait(false);
539 
540  //we need to wait for the configuration's file system watcher to read and reload the changes
541  await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
542  }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the DatabaseConfiguration re...
readonly IIOManager ioManager
The IIOManager for the SetupWizard
Definition: SetupWizard.cs:26
+
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the ControlPanelConfiguratio...
Task Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan
+
Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken)
Write some text to the IConsole
Configuration options for the Models.DatabaseContext<TParentContext>
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application.html index 5104175d0b..020f17080e 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application.html @@ -135,8 +135,8 @@ Public Member Functions
-

Definition at line 64 of file TestApplication.cs.

-
65  {
66  var mockConfiguration = new Mock<IConfiguration>();
67  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
68 
69  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
70 
71  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
72 
73  var mockOptions = new Mock<IOptions<GeneralConfiguration>>();
74  mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration
75  {
77  }).Verifiable();
78 
79  var fakeServiceDescriptor = new List<ServiceDescriptor>()
80  {
81  new ServiceDescriptor(typeof(IApplicationLifetime), typeof(MockApplicationLifetime), ServiceLifetime.Singleton),
82  new ServiceDescriptor(typeof(ISetupWizard), typeof(MockSetupWizard), ServiceLifetime.Singleton),
83  new ServiceDescriptor(typeof(IOptions<GeneralConfiguration>), mockOptions.Object)
84  };
85 
86  var mockServiceCollection = new Mock<IServiceCollection>();
87 
88  var mockConfigSection = new Mock<IConfigurationSection>();
89 
90  mockConfiguration.Setup(x => x.GetSection(It.IsNotNull<string>())).Returns(mockConfigSection.Object).Verifiable();
91  mockServiceCollection.Setup(x => x.GetEnumerator()).Returns(() => fakeServiceDescriptor.GetEnumerator()).Verifiable();
92 
93  Assert.ThrowsException<OperationCanceledException>(() => app.ConfigureServices(mockServiceCollection.Object));
94 
95  mockOptions.VerifyAll();
96  mockConfiguration.VerifyAll();
97  mockServiceCollection.VerifyAll();
98  }
SetupWizardMode
Determines if the Core.ISetupWizard will run
+

Definition at line 69 of file TestApplication.cs.

+
70  {
71  var mockConfiguration = new Mock<IConfiguration>();
72  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
73 
74  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
75 
76  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
77 
78  var mockOptions = new Mock<IOptions<GeneralConfiguration>>();
79  mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration
80  {
82  }).Verifiable();
83 
84  var fakeServiceDescriptor = new List<ServiceDescriptor>()
85  {
86  new ServiceDescriptor(typeof(IApplicationLifetime), typeof(MockApplicationLifetime), ServiceLifetime.Singleton),
87  new ServiceDescriptor(typeof(ISetupWizard), typeof(MockSetupWizard), ServiceLifetime.Singleton),
88  new ServiceDescriptor(typeof(IOptions<GeneralConfiguration>), mockOptions.Object)
89  };
90 
91  var mockServiceCollection = new Mock<IServiceCollection>();
92 
93  var mockConfigSection = new Mock<IConfigurationSection>();
94 
95  mockConfiguration.Setup(x => x.GetSection(It.IsNotNull<string>())).Returns(mockConfigSection.Object).Verifiable();
96  mockServiceCollection.Setup(x => x.GetEnumerator()).Returns(() => fakeServiceDescriptor.GetEnumerator()).Verifiable();
97 
98  Assert.ThrowsException<OperationCanceledException>(() => app.ConfigureServices(mockServiceCollection.Object));
99 
100  mockOptions.VerifyAll();
101  mockConfiguration.VerifyAll();
102  mockServiceCollection.VerifyAll();
103  }
SetupWizardMode
Determines if the Core.ISetupWizard will run
@@ -156,7 +156,8 @@ Public Member Functions

Definition at line 24 of file TestApplication.cs.

-
25  {
26  Assert.ThrowsException<ArgumentNullException>(() => new Application(null, null));
27  var mockConfiguration = new Mock<IConfiguration>();
28  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
29 
30  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
31 
32  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
33 
34  Assert.ThrowsException<ArgumentNullException>(() => app.ConfigureServices(null));
35  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null));
36 
37  var mockAppBuilder = new Mock<IApplicationBuilder>();
38  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, null, null, null));
39 
40  var mockServerControl = new Mock<IServerControl>();
41  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null));
42 
43  var mockTokenFactory = new Mock<ITokenFactory>();
44  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null));
45  }
+
25  {
26  Assert.ThrowsException<ArgumentNullException>(() => new Application(null, null));
27  var mockConfiguration = new Mock<IConfiguration>();
28  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
29 
30  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
31 
32  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
33 
34  Assert.ThrowsException<ArgumentNullException>(() => app.ConfigureServices(null));
35  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null, null));
36 
37  var mockAppBuilder = new Mock<IApplicationBuilder>();
38  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, null, null, null, null));
39 
40  var mockServerControl = new Mock<IServerControl>();
41  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null));
42 
43  var mockTokenFactory = new Mock<ITokenFactory>();
44  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null));
45 
46  var mockControlPanelOptions = new Mock<IOptions<ControlPanelConfiguration>>();
47  mockControlPanelOptions.SetupGet(x => x.Value).Returns(new ControlPanelConfiguration()).Verifiable();
48  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockControlPanelOptions.Object, null));
49  mockControlPanelOptions.VerifyAll();
50  }
+

The documentation for this class was generated from the following file:
    diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application_1_1_mock_application_lifetime.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application_1_1_mock_application_lifetime.html index 3f5e743f97..bbd2013782 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application_1_1_mock_application_lifetime.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application_1_1_mock_application_lifetime.html @@ -125,7 +125,7 @@ Public Attributes

    Detailed Description

    -

    Definition at line 52 of file TestApplication.cs.

    +

    Definition at line 57 of file TestApplication.cs.

    Member Function Documentation

    @@ -140,8 +140,8 @@ Public Attributes
    -

    Definition at line 60 of file TestApplication.cs.

    -
    60 { }
    +

    Definition at line 65 of file TestApplication.cs.

    +
    65 { }

Member Data Documentation

@@ -155,7 +155,7 @@ Public Attributes
-

Definition at line 54 of file TestApplication.cs.

+

Definition at line 59 of file TestApplication.cs.

@@ -169,7 +169,7 @@ Public Attributes
-

Definition at line 58 of file TestApplication.cs.

+

Definition at line 63 of file TestApplication.cs.

@@ -183,7 +183,7 @@ Public Attributes
-

Definition at line 56 of file TestApplication.cs.

+

Definition at line 61 of file TestApplication.cs.

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application_1_1_mock_setup_wizard.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application_1_1_mock_setup_wizard.html index 78d1cab2d2..d9046666b7 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application_1_1_mock_setup_wizard.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_application_1_1_mock_setup_wizard.html @@ -116,7 +116,7 @@ Public Member Functions

Detailed Description

-

Definition at line 47 of file TestApplication.cs.

+

Definition at line 52 of file TestApplication.cs.

Member Function Documentation

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_job_exception.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_job_exception.html index 28e03a04bb..f8cf921013 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_job_exception.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_job_exception.html @@ -122,8 +122,7 @@ Public Member Functions

Definition at line 10 of file TestJobException.cs.

-
11  {
12  new JobException();
13  new JobException("Message");
14  new JobException("Message", new Exception());
15  }
-
+
11  {
12  new JobException();
13  new JobException("Message");
14  new JobException("Message", new Exception());
15  }

The documentation for this class was generated from the following file:
    diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions-members.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions-members.html new file mode 100644 index 0000000000..bf08927738 --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions-members.html @@ -0,0 +1,110 @@ + + + + + + +tgstation-server: Member List + + + + + + + + + + +
    +
    + + + + + + + +
    +
    tgstation-server +
    +
    The /tg/station 13 server suite
    +
    +
    + + + + + + +
    +
    + + +
    + +
    + + +
    +
    +
    +
    Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions Member List
    +
    + + + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions.html new file mode 100644 index 0000000000..93d74b1e2a --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions.html @@ -0,0 +1,151 @@ + + + + + + +tgstation-server: Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions Class Reference + + + + + + + + + + +
    +
    + + + + + + + +
    +
    tgstation-server +
    +
    The /tg/station 13 server suite
    +
    +
    + + + + + + +
    +
    + + +
    + +
    + + +
    +
    + +
    +
    Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions Class Reference
    +
    +
    + + + + + + + + + + +

    +Classes

    class  BadConfig1
     
    class  BadConfig2
     
    class  BadConfig3
     
    class  GoodConfig
     
    + + + +

    +Public Member Functions

    void TestUseStandardConfig ()
     
    +

    Detailed Description

    +
    +

    Definition at line 10 of file TestServiceCollectionExtensions.cs.

    +

    Member Function Documentation

    + +
    +
    + + + + + + + +
    void Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.TestUseStandardConfig ()
    +
    + +

    Definition at line 33 of file TestServiceCollectionExtensions.cs.

    +
    34  {
    35  var serviceCollection = new ServiceCollection();
    36  var mockConfig = new Mock<IConfigurationSection>();
    37  mockConfig.Setup(x => x.GetSection(It.IsNotNull<string>())).Returns(mockConfig.Object).Verifiable();
    38  Assert.ThrowsException<ArgumentNullException>(() => ServiceCollectionExtensions.UseStandardConfig<GoodConfig>(null, null));
    39  Assert.ThrowsException<ArgumentNullException>(() => serviceCollection.UseStandardConfig<GoodConfig>(null));
    40  serviceCollection.UseStandardConfig<GoodConfig>(mockConfig.Object);
    41  Assert.ThrowsException<InvalidOperationException>(() => serviceCollection.UseStandardConfig<BadConfig1>(mockConfig.Object));
    42  Assert.ThrowsException<InvalidOperationException>(() => serviceCollection.UseStandardConfig<BadConfig2>(mockConfig.Object));
    43  Assert.ThrowsException<InvalidOperationException>(() => serviceCollection.UseStandardConfig<BadConfig3>(mockConfig.Object));
    44  mockConfig.Verify();
    45  }
    +
    +
    +
    The documentation for this class was generated from the following file: +
    + + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config1-members.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config1-members.html new file mode 100644 index 0000000000..c811d42d1a --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config1-members.html @@ -0,0 +1,110 @@ + + + + + + +tgstation-server: Member List + + + + + + + + + + +
    +
    + + + + + + + +
    +
    tgstation-server +
    +
    The /tg/station 13 server suite
    +
    +
    + + + + + + +
    +
    + + +
    + +
    + + +
    +
    +
    +
    Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig1 Member List
    +
    + + + + + diff --git a/class_exception.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config1.html similarity index 52% rename from class_exception.html rename to class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config1.html index c27b347cb0..e484718a6b 100644 --- a/class_exception.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config1.html @@ -4,7 +4,7 @@ -tgstation-server: Exception Class Reference +tgstation-server: Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig1 Class Reference @@ -85,35 +85,53 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
+
+
-
Exception Class Reference
+
Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig1 Class Reference
-
-Inheritance diagram for Exception:
-
-
- - -Tgstation.Server.Client.ClientException -Tgstation.Server.Host.Core.JobException -Tgstation.Server.Client.ApiConflictException -Tgstation.Server.Client.ApiMismatchException -Tgstation.Server.Client.ConflictException -Tgstation.Server.Client.InsufficientPermissionsException -Tgstation.Server.Client.MethodNotSupportedException -Tgstation.Server.Client.RateLimitException -Tgstation.Server.Client.RequestTimeoutException -Tgstation.Server.Client.ServerErrorException -Tgstation.Server.Client.ServiceUnavailableException -Tgstation.Server.Client.UnauthorizedException -Tgstation.Server.Client.UnrecognizedResponseException - -
+ + + + +

+Private Attributes

const string Section = "asdf"
 
+

Detailed Description

+
+

Definition at line 17 of file TestServiceCollectionExtensions.cs.

+

Member Data Documentation

+ +
+
+ + + + + +
+ + + + +
const string Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig1.Section = "asdf"
+
+private
+
+ +

Definition at line 19 of file TestServiceCollectionExtensions.cs.

+ +
+

The documentation for this class was generated from the following file:
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config2-members.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config2-members.html new file mode 100644 index 0000000000..deba470efc --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config2-members.html @@ -0,0 +1,110 @@ + + + + + + +tgstation-server: Member List + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig2 Member List
+
+ + + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config2.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config2.html new file mode 100644 index 0000000000..65125da7f7 --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config2.html @@ -0,0 +1,136 @@ + + + + + + +tgstation-server: Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig2 Class Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig2 Class Reference
+
+
+ + + + +

+Public Attributes

const bool Section = false
 
+

Detailed Description

+
+

Definition at line 22 of file TestServiceCollectionExtensions.cs.

+

Member Data Documentation

+ +
+
+ + + + +
const bool Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig2.Section = false
+
+ +

Definition at line 24 of file TestServiceCollectionExtensions.cs.

+ +
+
+
The documentation for this class was generated from the following file: +
+ + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config3.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config3.html new file mode 100644 index 0000000000..480a7849bc --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_bad_config3.html @@ -0,0 +1,112 @@ + + + + + + +tgstation-server: Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig3 Class Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig3 Class Reference
+
+
+

Detailed Description

+
+

Definition at line 27 of file TestServiceCollectionExtensions.cs.

+

The documentation for this class was generated from the following file: +
+ + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_good_config-members.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_good_config-members.html new file mode 100644 index 0000000000..b9bc30c643 --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_good_config-members.html @@ -0,0 +1,110 @@ + + + + + + +tgstation-server: Member List + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.GoodConfig Member List
+
+ + + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_good_config.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_good_config.html new file mode 100644 index 0000000000..62d53b02cd --- /dev/null +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_service_collection_extensions_1_1_good_config.html @@ -0,0 +1,136 @@ + + + + + + +tgstation-server: Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.GoodConfig Class Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.GoodConfig Class Reference
+
+
+ + + + +

+Public Attributes

const string Section = "asdf"
 
+

Detailed Description

+
+

Definition at line 12 of file TestServiceCollectionExtensions.cs.

+

Member Data Documentation

+ +
+
+ + + + +
const string Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.GoodConfig.Section = "asdf"
+
+ +

Definition at line 14 of file TestServiceCollectionExtensions.cs.

+ +
+
+
The documentation for this class was generated from the following file: +
+ + + + diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_setup_wizard.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_setup_wizard.html index d58ca35c8e..fa0778a283 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_setup_wizard.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_setup_wizard.html @@ -141,8 +141,7 @@ Public Member Functions

Definition at line 45 of file TestSetupWizard.cs.

-
46  {
47  var mockIOManager = new Mock<IIOManager>();
48  var mockConsole = new Mock<IConsole>();
49  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
50  var mockApplication = new Mock<IApplication>();
51  var mockDBConnectionFactory = new Mock<IDBConnectionFactory>();
52  var mockLogger = new Mock<ILogger<SetupWizard>>();
53  var mockGeneralConfigurationOptions = new Mock<IOptions<GeneralConfiguration>>();
54  var mockPlatformIdentifier = new Mock<IPlatformIdentifier>();
55  var mockAsyncDelayer = new Mock<IAsyncDelayer>();
56 
57  var testGeneralConfig = new GeneralConfiguration
58  {
60  };
61  mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(testGeneralConfig).Verifiable();
62 
63  var wizard = new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLogger.Object, mockGeneralConfigurationOptions.Object);
64 
65  mockPlatformIdentifier.SetupGet(x => x.IsWindows).Returns(true).Verifiable();
66  mockAsyncDelayer.Setup(x => x.Delay(It.IsAny<TimeSpan>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
67 
68  Assert.IsFalse(await wizard.CheckRunWizard(default).ConfigureAwait(false));
69 
70  testGeneralConfig.SetupWizardMode = SetupWizardMode.Force;
71  await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
72 
73  testGeneralConfig.SetupWizardMode = SetupWizardMode.Only;
74  await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
75 
76  mockConsole.SetupGet(x => x.Available).Returns(true).Verifiable();
77  mockIOManager.Setup(x => x.FileExists(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(true)).Verifiable();
78  mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(Encoding.UTF8.GetBytes("cucked"))).Verifiable();
79  mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
80 
81  var mockSuccessCommand = new Mock<DbCommand>();
82  mockSuccessCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(0)).Verifiable();
83  mockSuccessCommand.Setup(x => x.ExecuteScalarAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult<object>("1.2.3")).Verifiable();
84  var mockFailCommand = new Mock<DbCommand>();
85  mockFailCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
86 
87  void SetDbCommandCreator(Mock<DbConnection> mock, Func<DbCommand> creator) => mock.Protected().Setup<DbCommand>("CreateDbCommand").Returns(creator).Verifiable();
88 
89  var mockGoodDbConnection = new Mock<DbConnection>();
90  mockGoodDbConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
91  SetDbCommandCreator(mockGoodDbConnection, () => mockSuccessCommand.Object);
92 
93  var mockBadDbConnection = new Mock<DbConnection>();
94  mockBadDbConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
95  var invokeTimes = 0;
96  var mockUglyDbConnection = new Mock<DbConnection>();
97  SetDbCommandCreator(mockUglyDbConnection, () =>
98  {
99  if (invokeTimes < 2)
100  {
101  ++invokeTimes;
102  return mockSuccessCommand.Object;
103  }
104  else
105  return mockFailCommand.Object;
106  });
107 
108  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.SqlServer)).Returns(mockBadDbConnection.Object).Verifiable();
109  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.MariaDB)).Returns(mockGoodDbConnection.Object).Verifiable();
110  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.MySql)).Returns(mockUglyDbConnection.Object).Verifiable();
111 
112  var finalInputSequence = new List<string>()
113  {
114  //first run, just say no to the force prompt after testing it
115  "fake",
116  "n",
117  //second run say yes to the force prompt
118  "y",
119  //first normal run
120  "bad port number",
121  "0",
122  "666",
123  "FakeDBType",
124  nameof(DatabaseType.SqlServer),
125  "this isn't validated",
126  "nor is this",
127  "no",
128  //test winauth
129  "yes",
130  //sql server will always fail so reconfigure with maria
131  nameof(DatabaseType.MariaDB),
132  "bleh",
133  "blah",
134  "NO",
135  "user",
136  "pass",
137  //general config
138  "four",
139  "-12",
140  "16",
141  "eight",
142  "-27",
143  "5000",
144  "fake token",
145  //logging config
146  "no",
147  //saved, now for second run
148  //this time use defaults amap
149  String.Empty,
150  //test MySQL errors
151  nameof(DatabaseType.MySql),
152  String.Empty,
153  String.Empty,
154  "DbName",
155  "n",
156  "user",
157  "pass",
158  //general config
159  String.Empty,
160  String.Empty,
161  String.Empty,
162  //logging config
163  "y",
164  "not actually verified because lol mocks /../!@#$%^&*()/..///.",
165  "Warning",
166  String.Empty,
167  //third run, we already hit all the code coverage so just get through it
168  String.Empty,
169  nameof(DatabaseType.MariaDB),
170  String.Empty,
171  "dbname",
172  "y",
173  "user",
174  "pass",
175  String.Empty,
176  String.Empty,
177  String.Empty,
178  "y",
179  "will faile",
180  String.Empty,
181  String.Empty,
182  "fake",
183  "None",
184  "Critical"
185  };
186 
187  var inputPos = 0;
188 
189  mockApplication.SetupGet(x => x.VersionPrefix).Returns("sumfuk").Verifiable();
190 
191  mockConsole.Setup(x => x.PressAnyKeyAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
192  mockConsole.Setup(x => x.ReadLineAsync(It.IsAny<bool>(), It.IsAny<CancellationToken>())).Returns(() =>
193  {
194  if (inputPos == finalInputSequence.Count)
195  Assert.Fail("Exhausted input sequence!");
196  var res = finalInputSequence[inputPos++];
197  return Task.FromResult(res);
198  }).Verifiable();
199  mockConsole.Setup(x => x.WriteAsync(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
200 
201  Assert.IsFalse(await wizard.CheckRunWizard(default).ConfigureAwait(false));
202  //first real run
203  Assert.IsTrue(await wizard.CheckRunWizard(default).ConfigureAwait(false));
204 
205  //second run
206  mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(Encoding.UTF8.GetBytes(String.Empty))).Verifiable();
207  Assert.IsTrue(await wizard.CheckRunWizard(default).ConfigureAwait(false));
208 
209  //third run
210  testGeneralConfig.SetupWizardMode = SetupWizardMode.Autodetect;
211  mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
212  var firstRun = true;
213  mockIOManager.Setup(x => x.CreateDirectory(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(() =>
214  {
215  if (firstRun)
216  {
217  firstRun = false;
218  throw new Exception();
219  }
220  return Task.CompletedTask;
221  }).Verifiable();
222 
223  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
224 
225  Assert.AreEqual(finalInputSequence.Count, inputPos);
226  mockFailCommand.VerifyAll();
227  mockSuccessCommand.VerifyAll();
228  mockIOManager.VerifyAll();
229  mockGeneralConfigurationOptions.VerifyAll();
230  mockConsole.VerifyAll();
231  mockGoodDbConnection.VerifyAll();
232  mockBadDbConnection.VerifyAll();
233  mockUglyDbConnection.VerifyAll();
234  mockDBConnectionFactory.VerifyAll();
235  mockApplication.VerifyAll();
236  mockPlatformIdentifier.VerifyAll();
237  mockAsyncDelayer.VerifyAll();
238  }
-
SetupWizardMode
Determines if the Core.ISetupWizard will run
+
46  {
47  var mockIOManager = new Mock<IIOManager>();
48  var mockConsole = new Mock<IConsole>();
49  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
50  var mockApplication = new Mock<IApplication>();
51  var mockDBConnectionFactory = new Mock<IDBConnectionFactory>();
52  var mockLogger = new Mock<ILogger<SetupWizard>>();
53  var mockGeneralConfigurationOptions = new Mock<IOptions<GeneralConfiguration>>();
54  var mockPlatformIdentifier = new Mock<IPlatformIdentifier>();
55  var mockAsyncDelayer = new Mock<IAsyncDelayer>();
56 
57  var testGeneralConfig = new GeneralConfiguration
58  {
60  };
61  mockGeneralConfigurationOptions.SetupGet(x => x.Value).Returns(testGeneralConfig).Verifiable();
62 
63  var wizard = new SetupWizard(mockIOManager.Object, mockConsole.Object, mockHostingEnvironment.Object, mockApplication.Object, mockDBConnectionFactory.Object, mockPlatformIdentifier.Object, mockAsyncDelayer.Object, mockLogger.Object, mockGeneralConfigurationOptions.Object);
64 
65  mockPlatformIdentifier.SetupGet(x => x.IsWindows).Returns(true).Verifiable();
66  mockAsyncDelayer.Setup(x => x.Delay(It.IsAny<TimeSpan>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
67 
68  Assert.IsFalse(await wizard.CheckRunWizard(default).ConfigureAwait(false));
69 
70  testGeneralConfig.SetupWizardMode = SetupWizardMode.Force;
71  await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
72 
73  testGeneralConfig.SetupWizardMode = SetupWizardMode.Only;
74  await Assert.ThrowsExceptionAsync<InvalidOperationException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
75 
76  mockConsole.SetupGet(x => x.Available).Returns(true).Verifiable();
77  mockIOManager.Setup(x => x.FileExists(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(true)).Verifiable();
78  mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(Encoding.UTF8.GetBytes("cucked"))).Verifiable();
79  mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
80 
81  var mockSuccessCommand = new Mock<DbCommand>();
82  mockSuccessCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(0)).Verifiable();
83  mockSuccessCommand.Setup(x => x.ExecuteScalarAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult<object>("1.2.3")).Verifiable();
84  var mockFailCommand = new Mock<DbCommand>();
85  mockFailCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
86 
87  void SetDbCommandCreator(Mock<DbConnection> mock, Func<DbCommand> creator) => mock.Protected().Setup<DbCommand>("CreateDbCommand").Returns(creator).Verifiable();
88 
89  var mockGoodDbConnection = new Mock<DbConnection>();
90  mockGoodDbConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
91  SetDbCommandCreator(mockGoodDbConnection, () => mockSuccessCommand.Object);
92 
93  var mockBadDbConnection = new Mock<DbConnection>();
94  mockBadDbConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
95  var invokeTimes = 0;
96  var mockUglyDbConnection = new Mock<DbConnection>();
97  SetDbCommandCreator(mockUglyDbConnection, () =>
98  {
99  if (invokeTimes < 2)
100  {
101  ++invokeTimes;
102  return mockSuccessCommand.Object;
103  }
104  else
105  return mockFailCommand.Object;
106  });
107 
108  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.SqlServer)).Returns(mockBadDbConnection.Object).Verifiable();
109  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.MariaDB)).Returns(mockGoodDbConnection.Object).Verifiable();
110  mockDBConnectionFactory.Setup(x => x.CreateConnection(It.IsAny<string>(), DatabaseType.MySql)).Returns(mockUglyDbConnection.Object).Verifiable();
111 
112  var finalInputSequence = new List<string>()
113  {
114  //first run, just say no to the force prompt after testing it
115  "fake",
116  "n",
117  //second run say yes to the force prompt
118  "y",
119  //first normal run
120  "bad port number",
121  "0",
122  "666",
123  "FakeDBType",
124  nameof(DatabaseType.SqlServer),
125  "this isn't validated",
126  "nor is this",
127  "no",
128  //test winauth
129  "yes",
130  //sql server will always fail so reconfigure with maria
131  nameof(DatabaseType.MariaDB),
132  "bleh",
133  "blah",
134  "NO",
135  "user",
136  "pass",
137  //general config
138  "four",
139  "-12",
140  "16",
141  "eight",
142  "-27",
143  "5000",
144  "fake token",
145  //logging config
146  "no",
147  //cp config
148  "y",
149  "y",
150  //saved, now for second run
151  //this time use defaults amap
152  String.Empty,
153  //test MySQL errors
154  nameof(DatabaseType.MySql),
155  String.Empty,
156  String.Empty,
157  "DbName",
158  "n",
159  "user",
160  "pass",
161  //general config
162  "y",
163  String.Empty,
164  String.Empty,
165  String.Empty,
166  //logging config
167  "y",
168  "not actually verified because lol mocks /../!@#$%^&*()/..///.",
169  "Warning",
170  String.Empty,
171  //cp config
172  "y",
173  "n",
174  String.Empty,
175  //third run, we already hit all the code coverage so just get through it
176  String.Empty,
177  nameof(DatabaseType.MariaDB),
178  String.Empty,
179  "dbname",
180  "y",
181  "user",
182  "pass",
183  //general
184  "y",
185  String.Empty,
186  String.Empty,
187  String.Empty,
188  "y",
189  "will faile",
190  String.Empty,
191  String.Empty,
192  "fake",
193  "None",
194  "Critical",
195  //cp config
196  "y",
197  "n",
198  "http://fake.com, https://example.org",
199  };
200 
201  var inputPos = 0;
202 
203  mockApplication.SetupGet(x => x.VersionPrefix).Returns("sumfuk").Verifiable();
204 
205  mockConsole.Setup(x => x.PressAnyKeyAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
206  mockConsole.Setup(x => x.ReadLineAsync(It.IsAny<bool>(), It.IsAny<CancellationToken>())).Returns(() =>
207  {
208  if (inputPos == finalInputSequence.Count)
209  Assert.Fail("Exhausted input sequence!");
210  var res = finalInputSequence[inputPos++];
211  return Task.FromResult(res);
212  }).Verifiable();
213  mockConsole.Setup(x => x.WriteAsync(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
214 
215  Assert.IsFalse(await wizard.CheckRunWizard(default).ConfigureAwait(false));
216  //first real run
217  Assert.IsTrue(await wizard.CheckRunWizard(default).ConfigureAwait(false));
218 
219  //second run
220  mockIOManager.Setup(x => x.ReadAllBytes(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(Encoding.UTF8.GetBytes(String.Empty))).Verifiable();
221  Assert.IsTrue(await wizard.CheckRunWizard(default).ConfigureAwait(false));
222 
223  //third run
224  testGeneralConfig.SetupWizardMode = SetupWizardMode.Autodetect;
225  mockIOManager.Setup(x => x.WriteAllBytes(It.IsNotNull<string>(), It.IsNotNull<byte[]>(), It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
226  var firstRun = true;
227  mockIOManager.Setup(x => x.CreateDirectory(It.IsNotNull<string>(), It.IsAny<CancellationToken>())).Returns(() =>
228  {
229  if (firstRun)
230  {
231  firstRun = false;
232  throw new Exception();
233  }
234  return Task.CompletedTask;
235  }).Verifiable();
236 
237  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => wizard.CheckRunWizard(default)).ConfigureAwait(false);
238 
239  Assert.AreEqual(finalInputSequence.Count, inputPos);
240  mockFailCommand.VerifyAll();
241  mockSuccessCommand.VerifyAll();
242  mockIOManager.VerifyAll();
243  mockGeneralConfigurationOptions.VerifyAll();
244  mockConsole.VerifyAll();
245  mockGoodDbConnection.VerifyAll();
246  mockBadDbConnection.VerifyAll();
247  mockUglyDbConnection.VerifyAll();
248  mockDBConnectionFactory.VerifyAll();
249  mockApplication.VerifyAll();
250  mockPlatformIdentifier.VerifyAll();
251  mockAsyncDelayer.VerifyAll();
252  }
SetupWizardMode
Determines if the Core.ISetupWizard will run
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context.html b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context.html index 52499ddcad..3d667c46a1 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context.html @@ -370,7 +370,7 @@ Private Attributes

Implements Tgstation.Server.Host.Models.IDatabaseContext.

-

Referenced by Tgstation.Server.Host.Controllers.InstanceUserController.Create(), Tgstation.Server.Host.Controllers.ChatController.Create(), Tgstation.Server.Host.Controllers.UserController.Create(), Tgstation.Server.Host.Controllers.InstanceController.Create(), Tgstation.Server.Host.Controllers.HomeController.CreateToken(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.InstanceController.Delete(), Tgstation.Server.Host.Controllers.RepositoryController.Read(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), Tgstation.Server.Host.Controllers.DreamMakerController.Update(), Tgstation.Server.Host.Controllers.UserController.Update(), Tgstation.Server.Host.Controllers.DreamDaemonController.Update(), Tgstation.Server.Host.Controllers.ChatController.Update(), and Tgstation.Server.Host.Controllers.RepositoryController.Update().

+

Referenced by Tgstation.Server.Host.Controllers.InstanceUserController.Create(), Tgstation.Server.Host.Controllers.ChatController.Create(), Tgstation.Server.Host.Controllers.UserController.Create(), Tgstation.Server.Host.Controllers.InstanceController.Create(), Tgstation.Server.Host.Controllers.HomeController.CreateToken(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.InstanceController.Delete(), Tgstation.Server.Host.Controllers.RepositoryController.Read(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), Tgstation.Server.Host.Controllers.DreamMakerController.Update(), Tgstation.Server.Host.Controllers.UserController.Update(), Tgstation.Server.Host.Controllers.DreamDaemonController.Update(), Tgstation.Server.Host.Controllers.ChatController.Update(), and Tgstation.Server.Host.Controllers.RepositoryController.Update().

@@ -403,8 +403,7 @@ Private Attributes

Implements Tgstation.Server.Host.Models.IDatabaseContext.

Definition at line 184 of file DatabaseContext.cs.

-
185  {
186  if (version == null)
187  throw new ArgumentNullException(nameof(version));
188  if (version < new Version(4, 0))
189  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
190 
191  string targetMigration = null;
192 
193  //Update this with new migrations as they are made
194  //Always use the MS class
195 
196  //TODO: Uncomment once #816 is merged
197  /*
198  if (version < new Version(4, 0, 2))
199  targetMigration = nameof(MSReattachCompileJobRequired);
200  */
201 
202  if (targetMigration == null)
203  return;
204 
205  if (UseMySQLMigrations())
206  targetMigration = String.Format(CultureInfo.InvariantCulture, "MY" + targetMigration.Substring(2));
207 
208  //even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
209  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
210  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
211 
212  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
213  try
214  {
215  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
216  }
217  catch (Exception e)
218  {
219  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
220  }
221  }
-
abstract bool UseMySQLMigrations()
If the MY_ class of migrations should be used instead of the MS_ class
+
185  {
186  if (version == null)
187  throw new ArgumentNullException(nameof(version));
188  if (version < new Version(4, 0))
189  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
190 
191  string targetMigration = null;
192 
193  //Update this with new migrations as they are made
194  //Always use the MS class
195 
196  //TODO: Uncomment once #816 is merged
197  /*
198  if (version < new Version(4, 0, 2))
199  targetMigration = nameof(MSReattachCompileJobRequired);
200  */
201 
202  if (targetMigration == null)
203  return;
204 
205  if (UseMySQLMigrations())
206  targetMigration = String.Format(CultureInfo.InvariantCulture, "MY" + targetMigration.Substring(2));
207 
208  //even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
209  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
210  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
211 
212  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
213  try
214  {
215  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
216  }
217  catch (Exception e)
218  {
219  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
220  }
221  }
abstract bool UseMySQLMigrations()
If the MY_ class of migrations should be used instead of the MS_ class
ILogger Logger
The ILogger for the DatabaseContext<TParentContext>
@@ -863,7 +862,7 @@ Private Attributes

Definition at line 20 of file DatabaseContext.cs.

-

Referenced by Tgstation.Server.Host.Controllers.UserController.Create(), Tgstation.Server.Host.Controllers.HomeController.CreateToken(), Tgstation.Server.Host.Controllers.UserController.GetId(), Tgstation.Server.Host.Controllers.UserController.List(), and Tgstation.Server.Host.Controllers.UserController.Update().

+

Referenced by Tgstation.Server.Host.Controllers.UserController.Create(), Tgstation.Server.Host.Controllers.HomeController.CreateToken(), Tgstation.Server.Host.Controllers.UserController.GetId(), Tgstation.Server.Host.Controllers.UserController.List(), and Tgstation.Server.Host.Controllers.UserController.Update().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_program.html b/class_tgstation_1_1_server_1_1_host_1_1_program.html index fd439bcec3..d890f47095 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_program.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_program.html @@ -147,8 +147,7 @@ Static Public Member Functions

Definition at line 25 of file Program.cs.

References Tgstation.Server.Host.IServerFactory.CreateServer(), and Tgstation.Server.Host.IServer.RunAsync().

-
26  {
27  var listArgs = new List<string>(args);
28  //first arg is 100% always the update path, starting it otherwise is solely for debugging purposes
29  string updatePath;
30  if (listArgs.Count > 0)
31  {
32  updatePath = listArgs[0];
33  listArgs.RemoveAt(0);
34  if (listArgs.Remove("--attach-debugger"))
35  Debugger.Launch();
36  }
37  else
38  updatePath = null;
39  try
40  {
41  var server = serverFactory.CreateServer(listArgs.ToArray(), updatePath);
42  try
43  {
44  using (var cts = new CancellationTokenSource())
45  {
46  void AppDomainHandler(object a, EventArgs b) => cts.Cancel();
47  AppDomain.CurrentDomain.ProcessExit += AppDomainHandler;
48  try
49  {
50  Console.CancelKeyPress += (a, b) =>
51  {
52  b.Cancel = true;
53  cts.Cancel();
54  };
55  await server.RunAsync(cts.Token).ConfigureAwait(false);
56  }
57  finally
58  {
59  AppDomain.CurrentDomain.ProcessExit -= AppDomainHandler;
60  }
61  }
62  }
63  catch (OperationCanceledException) { }
64  return server.RestartRequested ? 1 : 0;
65  }
66  catch (Exception e)
67  {
68  if (updatePath != null)
69  {
70  File.WriteAllText(updatePath, e.ToString());
71  return 2;
72  }
73  throw;
74  }
75  }
-
Task RunAsync(CancellationToken cancellationToken)
Runs the IServer
+
26  {
27  var listArgs = new List<string>(args);
28  //first arg is 100% always the update path, starting it otherwise is solely for debugging purposes
29  string updatePath;
30  if (listArgs.Count > 0)
31  {
32  updatePath = listArgs[0];
33  listArgs.RemoveAt(0);
34  if (listArgs.Remove("--attach-debugger"))
35  Debugger.Launch();
36  }
37  else
38  updatePath = null;
39  try
40  {
41  var server = serverFactory.CreateServer(listArgs.ToArray(), updatePath);
42  try
43  {
44  using (var cts = new CancellationTokenSource())
45  {
46  void AppDomainHandler(object a, EventArgs b) => cts.Cancel();
47  AppDomain.CurrentDomain.ProcessExit += AppDomainHandler;
48  try
49  {
50  Console.CancelKeyPress += (a, b) =>
51  {
52  b.Cancel = true;
53  cts.Cancel();
54  };
55  await server.RunAsync(cts.Token).ConfigureAwait(false);
56  }
57  finally
58  {
59  AppDomain.CurrentDomain.ProcessExit -= AppDomainHandler;
60  }
61  }
62  }
63  catch (OperationCanceledException) { }
64  return server.RestartRequested ? 1 : 0;
65  }
66  catch (Exception e)
67  {
68  if (updatePath != null)
69  {
70  File.WriteAllText(updatePath, e.ToString());
71  return 2;
72  }
73  throw;
74  }
75  }
Task RunAsync(CancellationToken cancellationToken)
Runs the IServer
IServer CreateServer(string[] args, string updatePath)
Create a IServer
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_authentication_context.html b/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_authentication_context.html index b74908b6a8..3831f2b4e8 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_authentication_context.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_authentication_context.html @@ -292,7 +292,7 @@ Properties

Definition at line 17 of file AuthenticationContext.cs.

-

Referenced by Tgstation.Server.Host.Controllers.ApiController.ApiController(), Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), and Tgstation.Server.Host.Controllers.DreamMakerController.Update().

+

Referenced by Tgstation.Server.Host.Controllers.ApiController.ApiController(), Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), and Tgstation.Server.Host.Controllers.DreamMakerController.Update().

@@ -344,7 +344,7 @@ Properties

Definition at line 14 of file AuthenticationContext.cs.

-

Referenced by Tgstation.Server.Host.Controllers.DreamDaemonController.Create(), Tgstation.Server.Host.Controllers.UserController.Create(), Tgstation.Server.Host.Controllers.DreamMakerController.Create(), Tgstation.Server.Host.Controllers.InstanceController.Create(), Tgstation.Server.Host.Controllers.RepositoryController.Create(), Tgstation.Server.Host.Controllers.JobController.Delete(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.UserController.GetId(), Tgstation.Server.Host.Controllers.InstanceController.GetId(), Tgstation.Server.Host.Controllers.InstanceController.List(), Tgstation.Server.Host.Controllers.InstanceController.NormalizeModelPath(), Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync(), Tgstation.Server.Host.Controllers.DreamDaemonController.Restart(), Tgstation.Server.Host.Controllers.ByondController.Update(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), Tgstation.Server.Host.Controllers.UserController.Update(), and Tgstation.Server.Host.Controllers.RepositoryController.Update().

+

Referenced by Tgstation.Server.Host.Controllers.DreamDaemonController.Create(), Tgstation.Server.Host.Controllers.UserController.Create(), Tgstation.Server.Host.Controllers.DreamMakerController.Create(), Tgstation.Server.Host.Controllers.InstanceController.Create(), Tgstation.Server.Host.Controllers.RepositoryController.Create(), Tgstation.Server.Host.Controllers.JobController.Delete(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.UserController.GetId(), Tgstation.Server.Host.Controllers.InstanceController.GetId(), Tgstation.Server.Host.Controllers.InstanceController.List(), Tgstation.Server.Host.Controllers.InstanceController.NormalizeModelPath(), Tgstation.Server.Host.Controllers.ApiController.OnActionExecutionAsync(), Tgstation.Server.Host.Controllers.DreamDaemonController.Restart(), Tgstation.Server.Host.Controllers.ByondController.Update(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), Tgstation.Server.Host.Controllers.UserController.Update(), and Tgstation.Server.Host.Controllers.RepositoryController.Update().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_claims_injector.html b/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_claims_injector.html index e851d31cdb..1309d5ad4a 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_claims_injector.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_claims_injector.html @@ -204,8 +204,7 @@ Private Attributes

Definition at line 41 of file ClaimsInjector.cs.

References Tgstation.Server.Api.ApiHeaders.InstanceId, Tgstation.Server.Api.Rights.RightsHelper.IsInstanceRight(), Tgstation.Server.Api.Rights.RightsHelper.RightToType(), and Tgstation.Server.Api.Rights.RightsHelper.RoleName().

-
42  {
43  if (tokenValidatedContext == null)
44  throw new ArgumentNullException(nameof(tokenValidatedContext));
45 
46  //Find the user id in the token
47  var userIdClaim = tokenValidatedContext.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
48  if (userIdClaim == default)
49  throw new InvalidOperationException("Missing required claim!");
50 
51  long userId;
52  try
53  {
54  userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
55  }
56  catch (Exception e)
57  {
58  throw new InvalidOperationException("Failed to parse user ID!", e);
59  }
60 
61  ApiHeaders apiHeaders;
62  try
63  {
64  apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders());
65  }
66  catch (InvalidOperationException)
67  {
68  //we are not responsible for handling header validation issues
69  return;
70  }
71 
72  //This populates the CurrentAuthenticationContext field for use by us and subsequent controllers
73  await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, tokenValidatedContext.SecurityToken.ValidFrom, cancellationToken).ConfigureAwait(false);
74 
76 
77  var enumerator = Enum.GetValues(typeof(RightsType));
78  var claims = new List<Claim>();
79  foreach (RightsType I in enumerator)
80  {
81  //if there's no instance user, do a weird thing and add all the instance roles
82  //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
83  //if user is null that means they got the token with an expired password
84  var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I);
85  var rightEnum = RightsHelper.RightToType(I);
86  var right = (Enum)Enum.ToObject(rightEnum, rightInt);
87  foreach (Enum J in Enum.GetValues(rightEnum))
88  if (right.HasFlag(J))
89  claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
90  }
91 
92  tokenValidatedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
93  }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
- +
42  {
43  if (tokenValidatedContext == null)
44  throw new ArgumentNullException(nameof(tokenValidatedContext));
45 
46  //Find the user id in the token
47  var userIdClaim = tokenValidatedContext.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
48  if (userIdClaim == default)
49  throw new InvalidOperationException("Missing required claim!");
50 
51  long userId;
52  try
53  {
54  userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
55  }
56  catch (Exception e)
57  {
58  throw new InvalidOperationException("Failed to parse user ID!", e);
59  }
60 
61  ApiHeaders apiHeaders;
62  try
63  {
64  apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders());
65  }
66  catch (InvalidOperationException)
67  {
68  //we are not responsible for handling header validation issues
69  return;
70  }
71 
72  //This populates the CurrentAuthenticationContext field for use by us and subsequent controllers
73  await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, tokenValidatedContext.SecurityToken.ValidFrom, cancellationToken).ConfigureAwait(false);
74 
76 
77  var enumerator = Enum.GetValues(typeof(RightsType));
78  var claims = new List<Claim>();
79  foreach (RightsType I in enumerator)
80  {
81  //if there's no instance user, do a weird thing and add all the instance roles
82  //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
83  //if user is null that means they got the token with an expired password
84  var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I);
85  var rightEnum = RightsHelper.RightToType(I);
86  var right = (Enum)Enum.ToObject(rightEnum, rightInt);
87  foreach (Enum J in Enum.GetValues(rightEnum))
88  if (right.HasFlag(J))
89  claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
90  }
91 
92  tokenValidatedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
93  }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_server-members.html b/class_tgstation_1_1_server_1_1_host_1_1_server-members.html index d8019e24ab..f906b9453e 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_server-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_server-members.html @@ -100,19 +100,22 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - + + + - - - - - - - - - - - + + + + + + + + + + + +
ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)Tgstation.Server.Host.Server
cancellationTokenSourceTgstation.Server.Host.Serverprivate
CheckSanity(bool checkWatchdog)Tgstation.Server.Host.Serverprivate
CheckExceptionPropagation()Tgstation.Server.Host.Serverprivate
CheckSanity(bool checkWatchdog)Tgstation.Server.Host.Serverprivate
Die(Exception exception)Tgstation.Server.Host.Server
loggerTgstation.Server.Host.Serverprivate
RegisterForRestart(IRestartHandler handler)Tgstation.Server.Host.Server
Restart()Tgstation.Server.Host.Server
Restart(Version newVersion)Tgstation.Server.Host.Serverprivate
restartHandlersTgstation.Server.Host.Serverprivate
RestartRequestedTgstation.Server.Host.Server
RunAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Server
Server(IWebHostBuilder webHostBuilder, string updatePath)Tgstation.Server.Host.Server
updatePathTgstation.Server.Host.Serverprivate
updatingTgstation.Server.Host.Serverprivate
WatchdogPresentTgstation.Server.Host.Server
webHostBuilderTgstation.Server.Host.Serverprivate
propagatedExceptionTgstation.Server.Host.Serverprivate
RegisterForRestart(IRestartHandler handler)Tgstation.Server.Host.Server
Restart()Tgstation.Server.Host.Server
Restart(Version newVersion, Exception exception)Tgstation.Server.Host.Serverprivate
restartHandlersTgstation.Server.Host.Serverprivate
RestartRequestedTgstation.Server.Host.Server
RunAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Server
Server(IWebHostBuilder webHostBuilder, string updatePath)Tgstation.Server.Host.Server
updatePathTgstation.Server.Host.Serverprivate
updatingTgstation.Server.Host.Serverprivate
WatchdogPresentTgstation.Server.Host.Server
webHostBuilderTgstation.Server.Host.Serverprivate
+ +

Re-throw propagatedException if it exists

+ +

Definition at line 92 of file Server.cs.

+
93  {
94  if (propagatedException != null)
95  throw propagatedException;
96  }
Exception propagatedException
The Exception to propagate when the server terminates
Definition: Server.cs:54
@@ -308,11 +345,32 @@ Private Attributes -

Definition at line 73 of file Server.cs.

-
74  {
75  if (checkWatchdog && !WatchdogPresent)
76  throw new InvalidOperationException("Server restarts are not supported");
77 
78  if (cancellationTokenSource == null || logger == null)
79  throw new InvalidOperationException("Tried to control a non-running Server!");
80  }
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
+

Definition at line 80 of file Server.cs.

+
81  {
82  if (checkWatchdog && !WatchdogPresent && propagatedException == null)
83  throw new InvalidOperationException("Server restarts are not supported");
84 
85  if (cancellationTokenSource == null || logger == null)
86  throw new InvalidOperationException("Tried to control a non-running Server!");
87  }
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the Server
Definition: Server.cs:49
+
Exception propagatedException
The Exception to propagate when the server terminates
Definition: Server.cs:54
+
+
+ +
+
+ + + + + + + + +
Task Tgstation.Server.Host.Server.Die (Exception exception)
+
+ +

Kill the server with a fatal exception

+ +

Implements Tgstation.Server.Host.Core.IServerControl.

+
@@ -333,12 +391,12 @@ Private Attributes

Implements Tgstation.Server.Host.Core.IServerControl.

-

Definition at line 187 of file Server.cs.

-
188  {
189  if (handler == null)
190  throw new ArgumentNullException(nameof(handler));
191 
192  CheckSanity(false);
193 
194  lock (this)
195  if (!RestartRequested)
196  {
197  logger.LogTrace("Registering restart handler {0}...", handler);
198  restartHandlers.Add(handler);
199  return new RestartRegistration(() =>
200  {
201  lock (this)
202  if (!RestartRequested)
203  restartHandlers.Remove(handler);
204  });
205  }
206  return new RestartRegistration(() => { });
207  }
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
+

Definition at line 207 of file Server.cs.

+
208  {
209  if (handler == null)
210  throw new ArgumentNullException(nameof(handler));
211 
212  CheckSanity(false);
213 
214  lock (this)
215  if (!RestartRequested)
216  {
217  logger.LogTrace("Registering restart handler {0}...", handler);
218  restartHandlers.Add(handler);
219  return new RestartRegistration(() =>
220  {
221  lock (this)
222  if (!RestartRequested)
223  restartHandlers.Remove(handler);
224  });
225  }
226  return new RestartRegistration(() => { });
227  }
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts
Definition: Server.cs:34
-
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used
Definition: Server.cs:73
+
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used
Definition: Server.cs:80
@@ -361,7 +419,7 @@ Private Attributes
- +
@@ -372,8 +430,18 @@ Private Attributes - + + + + + + + + + + +
async Task Tgstation.Server.Host.Server.Restart ( Version newVersion)newVersion,
Exception exception 
)
@@ -387,19 +455,20 @@ Private Attributes
Parameters
+
newVersionThe Version of any potential updates being applied
exceptionThe potential value of propagatedException
Returns
-

Definition at line 217 of file Server.cs.

-
218  {
219  CheckSanity(true);
220 
221  logger.LogTrace("Begin Restart...");
222 
223  lock (this)
224  {
225  if ((updating && newVersion == null) || RestartRequested)
226  {
227  logger.LogTrace("Aborted due to concurrency conflict!");
228  return;
229  }
230  RestartRequested = true;
231  }
232 
233  logger.LogInformation("Restarting server...");
234 
235  using (var cts = new CancellationTokenSource())
236  {
237  logger.LogTrace("Running restart handlers...");
238  var cancellationToken = cts.Token;
239  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
240  //YA GOT 10 SECONDS
241  var expiryTask = Task.Delay(TimeSpan.FromSeconds(10));
242  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
243  logger.LogTrace("Joining restart handlers...");
244  cts.Cancel();
245  try
246  {
247  await eventsTask.ConfigureAwait(false);
248  }
249  catch (OperationCanceledException) { }
250  catch (Exception e)
251  {
252  logger.LogError("Restart handlers error! Exception: {0}", e);
253  }
254  }
255 
256  logger.LogTrace("Stopping host...");
257  cancellationTokenSource.Cancel();
258  }
-
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
+

Definition at line 238 of file Server.cs.

+
239  {
240  CheckSanity(true);
241 
242  logger.LogTrace("Begin Restart...");
243 
244  lock (this)
245  {
246  if ((updating && newVersion == null) || RestartRequested)
247  {
248  logger.LogTrace("Aborted due to concurrency conflict!");
249  return;
250  }
251  RestartRequested = true;
252  propagatedException = exception;
253  }
254 
255  if (exception == null)
256  using (var cts = new CancellationTokenSource())
257  {
258  logger.LogInformation("Restarting server...");
259  var cancellationToken = cts.Token;
260  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
261  //YA GOT 10 SECONDS
262  var expiryTask = Task.Delay(TimeSpan.FromSeconds(10));
263  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
264  logger.LogTrace("Joining restart handlers...");
265  cts.Cancel();
266  try
267  {
268  await eventsTask.ConfigureAwait(false);
269  }
270  catch (OperationCanceledException) { }
271  catch (Exception e)
272  {
273  logger.LogError("Restart handlers error! Exception: {0}", e);
274  }
275  }
276 
277  logger.LogTrace("Stopping host...");
278  cancellationTokenSource.Cancel();
279  }
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts
Definition: Server.cs:34
CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the Server
Definition: Server.cs:49
-
bool updating
If a server update has been or is being applied
Definition: Server.cs:54
-
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used
Definition: Server.cs:73
+
bool updating
If a server update has been or is being applied
Definition: Server.cs:59
+
Exception propagatedException
The Exception to propagate when the server terminates
Definition: Server.cs:54
+
void CheckSanity(bool checkWatchdog)
Throws an InvalidOperationException if the IServerControl cannot be used
Definition: Server.cs:80
@@ -421,13 +490,12 @@ Private Attributes

Implements Tgstation.Server.Host.IServer.

-

Definition at line 83 of file Server.cs.

-
84  {
85  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
86  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
87  {
88  if (fsWatcher != null)
89  {
90  fsWatcher.Created += (a, b) =>
91  {
92  if (b.FullPath == updatePath && File.Exists(b.FullPath))
93  cancellationTokenSource.Cancel();
94  };
95  fsWatcher.EnableRaisingEvents = true;
96  }
97  using (var webHost = webHostBuilder
98  .UseStartup<Application>()
99  .ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this))
100  .Build()
101  )
102  {
103  logger = webHost.Services.GetRequiredService<ILogger<Server>>();
104  await webHost.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
105  }
106  }
107  }
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
- +

Definition at line 99 of file Server.cs.

+
100  {
101  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
102  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
103  {
104  if (fsWatcher != null)
105  {
106  fsWatcher.Created += (a, b) =>
107  {
108  if (b.FullPath == updatePath && File.Exists(b.FullPath))
109  cancellationTokenSource.Cancel();
110  };
111  fsWatcher.EnableRaisingEvents = true;
112  }
113 
114  using (var webHost = webHostBuilder.Build())
115  try
116  {
117  logger = webHost.Services.GetRequiredService<ILogger<Server>>();
118  await webHost.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
119  }
120  catch (OperationCanceledException)
121  {
123  throw;
124  }
125  }
127  }
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
+
void CheckExceptionPropagation()
Re-throw propagatedException if it exists
Definition: Server.cs:92
readonly string updatePath
The absolute path to install updates to
Definition: Server.cs:39
CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the Server
Definition: Server.cs:49
readonly IWebHostBuilder webHostBuilder
The IWebHostBuilder for the Server
Definition: Server.cs:29
-
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
@@ -478,6 +546,30 @@ Private Attributes

Definition at line 44 of file Server.cs.

+
+ + +
+
+ + + + + +
+ + + + +
Exception Tgstation.Server.Host.Server.propagatedException
+
+private
+
+ +

The Exception to propagate when the server terminates

+ +

Definition at line 54 of file Server.cs.

+
@@ -548,7 +640,7 @@ Private Attributes

If a server update has been or is being applied

-

Definition at line 54 of file Server.cs.

+

Definition at line 59 of file Server.cs.

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_server_factory.html b/class_tgstation_1_1_server_1_1_host_1_1_server_factory.html index 98f74a7c57..aad959b0ac 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_server_factory.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_server_factory.html @@ -119,7 +119,7 @@ Public Member Functions

Detailed Description

-

Definition at line 7 of file ServerFactory.cs.

+

Definition at line 12 of file ServerFactory.cs.

Member Function Documentation

@@ -149,8 +149,12 @@ Public Member Functions

Implements Tgstation.Server.Host.IServerFactory.

-

Referenced by Tgstation.Server.Tests.TestingServer.TestingServer().

+

Definition at line 15 of file ServerFactory.cs.

+

Referenced by Tgstation.Server.Tests.TestingServer.TestingServer().

+
15  {
16 
17  var webHost = WebHost.CreateDefaultBuilder(args ?? throw new ArgumentNullException(nameof(args)))
18  .ConfigureAppConfiguration((context, configurationBuilder) => configurationBuilder.SetBasePath(Directory.GetCurrentDirectory()))
19  .UseStartup<Application>()
20  .SuppressStatusMessages(true)
21  .UseShutdownTimeout(TimeSpan.FromMinutes(1));
22 
23  if(updatePath != null)
24  webHost.UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
25 
26  return new Server(webHost, updatePath);
27  }
+ +

The documentation for this class was generated from the following file:
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netcoreapp2.1
directory  netcoreapp2.1
 
diff --git a/dir_08963d262fa9e27215dce7b7c6e97f1b.html b/dir_08963d262fa9e27215dce7b7c6e97f1b.html index d2c2578eaf..8a40d4b732 100644 --- a/dir_08963d262fa9e27215dce7b7c6e97f1b.html +++ b/dir_08963d262fa9e27215dce7b7c6e97f1b.html @@ -95,6 +95,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); + + diff --git a/dir_53c056aa6a96f034cca75f793964be6e.html b/dir_0b6a380fe7fc1e1d640d1ac854fc3fb6.html similarity index 92% rename from dir_53c056aa6a96f034cca75f793964be6e.html rename to dir_0b6a380fe7fc1e1d640d1ac854fc3fb6.html index 221965305a..f668df272f 100644 --- a/dir_53c056aa6a96f034cca75f793964be6e.html +++ b/dir_0b6a380fe7fc1e1d640d1ac854fc3fb6.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release Directory Reference +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference

Files

file  ControlPanelConfiguration.cs [code]
 
file  DatabaseConfiguration.cs [code]
 
file  DatabaseType.cs [code]
- +

Directories

directory  netcoreapp2.1
directory  netcoreapp2.1
 
diff --git a/dir_0de875c2ee7edd4009e892c5045f7b3a.html b/dir_0de875c2ee7edd4009e892c5045f7b3a.html index d7609934bf..0f9a4d8c05 100644 --- a/dir_0de875c2ee7edd4009e892c5045f7b3a.html +++ b/dir_0de875c2ee7edd4009e892c5045f7b3a.html @@ -151,6 +151,8 @@ Files   file  SemaphoreSlimContext.cs [code]   +file  ServiceCollectionExtensions.cs [code] +  file  SetupWizard.cs [code]   diff --git a/dir_103b33cf1ab7043de6f87dfa61afa23f.html b/dir_103b33cf1ab7043de6f87dfa61afa23f.html index aa10758ae0..7501f6c633 100644 --- a/dir_103b33cf1ab7043de6f87dfa61afa23f.html +++ b/dir_103b33cf1ab7043de6f87dfa61afa23f.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_b2c89c850c962495793d09ea3a2ee385.html b/dir_1083fcf52a7d94a2efffb6ca88246acf.html similarity index 95% rename from dir_b2c89c850c962495793d09ea3a2ee385.html rename to dir_1083fcf52a7d94a2efffb6ca88246acf.html index 9e631c4d68..e8b31798d9 100644 --- a/dir_b2c89c850c962495793d09ea3a2ee385.html +++ b/dir_1083fcf52a7d94a2efffb6ca88246acf.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Release/netcoreapp2.1 Directory Reference +tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Debug/netcoreapp2.1 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_1302441b003a340c6a16c9bc1b51c94e.html b/dir_1302441b003a340c6a16c9bc1b51c94e.html index 80536a8f46..aa6d4b621c 100644 --- a/dir_1302441b003a340c6a16c9bc1b51c94e.html +++ b/dir_1302441b003a340c6a16c9bc1b51c94e.html @@ -111,6 +111,8 @@ Files   file  TestPlatformIdentifier.cs [code]   +file  TestServiceCollectionExtensions.cs [code] +  file  TestSetupWizard.cs [code]   diff --git a/dir_177c182c6d1f29bec543c3fdad3072d5.html b/dir_177c182c6d1f29bec543c3fdad3072d5.html index c5f1f80c35..de1657aed2 100644 --- a/dir_177c182c6d1f29bec543c3fdad3072d5.html +++ b/dir_177c182c6d1f29bec543c3fdad3072d5.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_53aae571c66a052729f8e64ccc397130.html b/dir_2553b13cbb871373b0c30fb90d66217c.html similarity index 94% rename from dir_53aae571c66a052729f8e64ccc397130.html rename to dir_2553b13cbb871373b0c30fb90d66217c.html index 41068033a6..fae236a9e2 100644 --- a/dir_53aae571c66a052729f8e64ccc397130.html +++ b/dir_2553b13cbb871373b0c30fb90d66217c.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1 Directory Reference +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_90400719279795a0c5ea912516a4c874.html b/dir_2b9d136d9ffdab429a63a74fca28b300.html similarity index 92% rename from dir_90400719279795a0c5ea912516a4c874.html rename to dir_2b9d136d9ffdab429a63a74fca28b300.html index 80c2dd4c01..5f8a13ad47 100644 --- a/dir_90400719279795a0c5ea912516a4c874.html +++ b/dir_2b9d136d9ffdab429a63a74fca28b300.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release Directory Reference +tgstation-server: src/Tgstation.Server.Host/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netcoreapp2.1
directory  netcoreapp2.1
 
diff --git a/dir_ec133cefc1442f53ec97e0d7a57cf129.html b/dir_2e39d68cad3f83629c74ea67329dead9.html similarity index 92% rename from dir_ec133cefc1442f53ec97e0d7a57cf129.html rename to dir_2e39d68cad3f83629c74ea67329dead9.html index c66705fb2e..448b1a1888 100644 --- a/dir_ec133cefc1442f53ec97e0d7a57cf129.html +++ b/dir_2e39d68cad3f83629c74ea67329dead9.html @@ -4,7 +4,7 @@ -tgstation-server: tools/ReleaseNotes/obj/Release Directory Reference +tgstation-server: tools/ReleaseNotes/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netcoreapp2.1
directory  netcoreapp2.1
 
diff --git a/dir_7ae0efc0613c36c848fa310214e0aee3.html b/dir_2e67631cb23401d696bcfdbe81f86f0f.html similarity index 92% rename from dir_7ae0efc0613c36c848fa310214e0aee3.html rename to dir_2e67631cb23401d696bcfdbe81f86f0f.html index 6f771886e7..dd30a6f223 100644 --- a/dir_7ae0efc0613c36c848fa310214e0aee3.html +++ b/dir_2e67631cb23401d696bcfdbe81f86f0f.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Api/obj/Release Directory Reference +tgstation-server: src/Tgstation.Server.Api/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netstandard2.0
directory  netstandard2.0
 
diff --git a/dir_446bdfc1cd19b69c11765b5665ebfd45.html b/dir_446bdfc1cd19b69c11765b5665ebfd45.html index e9184ffb05..00898166ed 100644 --- a/dir_446bdfc1cd19b69c11765b5665ebfd45.html +++ b/dir_446bdfc1cd19b69c11765b5665ebfd45.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_4122b363a9d577bed19bef337e757ff3.html b/dir_48968f849f7ddb061f7130e079fe66a6.html similarity index 92% rename from dir_4122b363a9d577bed19bef337e757ff3.html rename to dir_48968f849f7ddb061f7130e079fe66a6.html index b3a9b1e4ea..196118d239 100644 --- a/dir_4122b363a9d577bed19bef337e757ff3.html +++ b/dir_48968f849f7ddb061f7130e079fe66a6.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Client/obj/Release Directory Reference +tgstation-server: src/Tgstation.Server.Client/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netstandard2.0
directory  netstandard2.0
 
diff --git a/dir_d45f494620bc0048ee4a31fa4f7391e1.html b/dir_4c0c7e2f4fc60f51e2347a2d8dda94ce.html similarity index 94% rename from dir_d45f494620bc0048ee4a31fa4f7391e1.html rename to dir_4c0c7e2f4fc60f51e2347a2d8dda94ce.html index f7d7774036..2f3126d81e 100644 --- a/dir_d45f494620bc0048ee4a31fa4f7391e1.html +++ b/dir_4c0c7e2f4fc60f51e2347a2d8dda94ce.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Release/netcoreapp2.1 Directory Reference +tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Debug/netcoreapp2.1 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_685b5aa2d8a661ab5bc3f68b331887b0.html b/dir_685b5aa2d8a661ab5bc3f68b331887b0.html index 218f75929a..c1123aed64 100644 --- a/dir_685b5aa2d8a661ab5bc3f68b331887b0.html +++ b/dir_685b5aa2d8a661ab5bc3f68b331887b0.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_689f0584ee97ae9ca308532531d9c5d2.html b/dir_689f0584ee97ae9ca308532531d9c5d2.html index 6c9954610a..fd4c537aa3 100644 --- a/dir_689f0584ee97ae9ca308532531d9c5d2.html +++ b/dir_689f0584ee97ae9ca308532531d9c5d2.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_74017a19412e2f90cc6ad11fcac82495.html b/dir_74017a19412e2f90cc6ad11fcac82495.html index 7f8fa7d13d..6be6b1c3a2 100644 --- a/dir_74017a19412e2f90cc6ad11fcac82495.html +++ b/dir_74017a19412e2f90cc6ad11fcac82495.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_0e792d76844bf2d7c79795a56a50ebec.html b/dir_87039fa7b841d848ca75981d64237477.html similarity index 92% rename from dir_0e792d76844bf2d7c79795a56a50ebec.html rename to dir_87039fa7b841d848ca75981d64237477.html index 23a89b0749..adbdcbf855 100644 --- a/dir_0e792d76844bf2d7c79795a56a50ebec.html +++ b/dir_87039fa7b841d848ca75981d64237477.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Tests/obj/Release Directory Reference +tgstation-server: tests/Tgstation.Server.Tests/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netcoreapp2.1
directory  netcoreapp2.1
 
diff --git a/dir_87a4f2035ed18e30422f4b60aebc4381.html b/dir_87a4f2035ed18e30422f4b60aebc4381.html index 758e9f5842..bf16019436 100644 --- a/dir_87a4f2035ed18e30422f4b60aebc4381.html +++ b/dir_87a4f2035ed18e30422f4b60aebc4381.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_6db5af7fcfa17a34c7abf23e195af4db.html b/dir_9257d56e926145e494a5aa5279887044.html similarity index 94% rename from dir_6db5af7fcfa17a34c7abf23e195af4db.html rename to dir_9257d56e926145e494a5aa5279887044.html index c6f101d612..f1c9e65896 100644 --- a/dir_6db5af7fcfa17a34c7abf23e195af4db.html +++ b/dir_9257d56e926145e494a5aa5279887044.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Tests/obj/Release/netcoreapp2.1 Directory Reference +tgstation-server: tests/Tgstation.Server.Tests/obj/Debug/netcoreapp2.1 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_13bee0ed20ecc83b9bcd4b869b726d42.html b/dir_a3714ab037275c284988fc90b4d045a5.html similarity index 95% rename from dir_13bee0ed20ecc83b9bcd4b869b726d42.html rename to dir_a3714ab037275c284988fc90b4d045a5.html index 22a0a8b491..8da0204063 100644 --- a/dir_13bee0ed20ecc83b9bcd4b869b726d42.html +++ b/dir_a3714ab037275c284988fc90b4d045a5.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Release/netstandard2.0 Directory Reference +tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Debug/netstandard2.0 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_fcc41a8faa41492aba200a49fc7d0aed.html b/dir_a8807eef6592489c6a360d587f5b1a66.html similarity index 93% rename from dir_fcc41a8faa41492aba200a49fc7d0aed.html rename to dir_a8807eef6592489c6a360d587f5b1a66.html index 31f97c168e..884a51bfcb 100644 --- a/dir_fcc41a8faa41492aba200a49fc7d0aed.html +++ b/dir_a8807eef6592489c6a360d587f5b1a66.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Console/obj/Release/netcoreapp2.1 Directory Reference +tgstation-server: src/Tgstation.Server.Host.Console/obj/Debug/netcoreapp2.1 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_b524b00ec2259c3c83b44909fd47f6e3.html b/dir_b524b00ec2259c3c83b44909fd47f6e3.html index 4404253378..77ef1a1ebc 100644 --- a/dir_b524b00ec2259c3c83b44909fd47f6e3.html +++ b/dir_b524b00ec2259c3c83b44909fd47f6e3.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_900257602d294c4a4799e88420919068.html b/dir_b551a02288ef39e099594710382363a0.html similarity index 95% rename from dir_900257602d294c4a4799e88420919068.html rename to dir_b551a02288ef39e099594710382363a0.html index bc123d8ffe..e45deffdd4 100644 --- a/dir_900257602d294c4a4799e88420919068.html +++ b/dir_b551a02288ef39e099594710382363a0.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Release/netcoreapp2.1 Directory Reference +tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Debug/netcoreapp2.1 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_e928c7e494884ad322b5f6005c2954e1.html b/dir_b87cbb2c60cf682b61fbfcceb9bced43.html similarity index 94% rename from dir_e928c7e494884ad322b5f6005c2954e1.html rename to dir_b87cbb2c60cf682b61fbfcceb9bced43.html index f848936f1a..1a266fb6a8 100644 --- a/dir_e928c7e494884ad322b5f6005c2954e1.html +++ b/dir_b87cbb2c60cf682b61fbfcceb9bced43.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1 Directory Reference +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_40138fd4849573e743490d583f6d06d6.html b/dir_b92573f47b933d7a3661c26d1af2098b.html similarity index 93% rename from dir_40138fd4849573e743490d583f6d06d6.html rename to dir_b92573f47b933d7a3661c26d1af2098b.html index bcd2178ae4..8845501d3a 100644 --- a/dir_40138fd4849573e743490d583f6d06d6.html +++ b/dir_b92573f47b933d7a3661c26d1af2098b.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Release Directory Reference +tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netcoreapp2.1
directory  netcoreapp2.1
 
diff --git a/dir_18a5e30c14613227f95d12bd9fddc614.html b/dir_be9f3ed3485a880e3213506a311823d1.html similarity index 92% rename from dir_18a5e30c14613227f95d12bd9fddc614.html rename to dir_be9f3ed3485a880e3213506a311823d1.html index 2d245f3bd5..b2ae8d72ef 100644 --- a/dir_18a5e30c14613227f95d12bd9fddc614.html +++ b/dir_be9f3ed3485a880e3213506a311823d1.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Release Directory Reference +tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netcoreapp2.1
directory  netcoreapp2.1
 
diff --git a/dir_bf23e1e49f40264f2966f02ed35d8142.html b/dir_bf23e1e49f40264f2966f02ed35d8142.html index 628590894f..40d54d7173 100644 --- a/dir_bf23e1e49f40264f2966f02ed35d8142.html +++ b/dir_bf23e1e49f40264f2966f02ed35d8142.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_298f2a364723f4f4cea115538f944a94.html b/dir_c5f98c05147bcbba78e71856485f5049.html similarity index 95% rename from dir_298f2a364723f4f4cea115538f944a94.html rename to dir_c5f98c05147bcbba78e71856485f5049.html index 38023d24fb..aa66f45877 100644 --- a/dir_298f2a364723f4f4cea115538f944a94.html +++ b/dir_c5f98c05147bcbba78e71856485f5049.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Release/netcoreapp2.1 Directory Reference +tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Debug/netcoreapp2.1 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_83a6f600c846cd444f33e44bb9b4f9cb.html b/dir_d8a279f7113e0073f6cd65dd050c1475.html similarity index 94% rename from dir_83a6f600c846cd444f33e44bb9b4f9cb.html rename to dir_d8a279f7113e0073f6cd65dd050c1475.html index a8019a6a86..67523f1be3 100644 --- a/dir_83a6f600c846cd444f33e44bb9b4f9cb.html +++ b/dir_d8a279f7113e0073f6cd65dd050c1475.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Api/obj/Release/netstandard2.0 Directory Reference +tgstation-server: src/Tgstation.Server.Api/obj/Debug/netstandard2.0 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_e0c1bccb03249d93bc42ac0968ce43fd.html b/dir_e0c1bccb03249d93bc42ac0968ce43fd.html index 3c01320942..97505085b3 100644 --- a/dir_e0c1bccb03249d93bc42ac0968ce43fd.html +++ b/dir_e0c1bccb03249d93bc42ac0968ce43fd.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_c65d377d3c73fb4a07b3637f9c0caf4d.html b/dir_e97d380c3eebaf0856a4ec180d749620.html similarity index 93% rename from dir_c65d377d3c73fb4a07b3637f9c0caf4d.html rename to dir_e97d380c3eebaf0856a4ec180d749620.html index 8850253690..06713a10d6 100644 --- a/dir_c65d377d3c73fb4a07b3637f9c0caf4d.html +++ b/dir_e97d380c3eebaf0856a4ec180d749620.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Release Directory Reference +tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netcoreapp2.1
directory  netcoreapp2.1
 
diff --git a/dir_6c8d8f20501e8e412458f542d0998195.html b/dir_ea36b5ab01ad6e8b607438a02ae91964.html similarity index 93% rename from dir_6c8d8f20501e8e412458f542d0998195.html rename to dir_ea36b5ab01ad6e8b607438a02ae91964.html index 0a0cbe414e..a76bac9127 100644 --- a/dir_6c8d8f20501e8e412458f542d0998195.html +++ b/dir_ea36b5ab01ad6e8b607438a02ae91964.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Release Directory Reference +tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netstandard2.0
directory  netstandard2.0
 
diff --git a/dir_f0581cdd528a911f791df57d0f438bcb.html b/dir_f0581cdd528a911f791df57d0f438bcb.html index 92fb7d8e33..56a5aee82e 100644 --- a/dir_f0581cdd528a911f791df57d0f438bcb.html +++ b/dir_f0581cdd528a911f791df57d0f438bcb.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/dir_383ca23c40357ede4cca827b4a365c76.html b/dir_f88948f9bde0555374e8a59a663bdfc9.html similarity index 94% rename from dir_383ca23c40357ede4cca827b4a365c76.html rename to dir_f88948f9bde0555374e8a59a663bdfc9.html index f0da92e2cc..f4794e143b 100644 --- a/dir_383ca23c40357ede4cca827b4a365c76.html +++ b/dir_f88948f9bde0555374e8a59a663bdfc9.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Client/obj/Release/netstandard2.0 Directory Reference +tgstation-server: src/Tgstation.Server.Client/obj/Debug/netstandard2.0 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_7a9d035dcfcd6d9c7aa1efef4b6b58a4.html b/dir_fab1362e86740a2c573ef875dd474a67.html similarity index 92% rename from dir_7a9d035dcfcd6d9c7aa1efef4b6b58a4.html rename to dir_fab1362e86740a2c573ef875dd474a67.html index 445fc56436..1dedab1f09 100644 --- a/dir_7a9d035dcfcd6d9c7aa1efef4b6b58a4.html +++ b/dir_fab1362e86740a2c573ef875dd474a67.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Console/obj/Release Directory Reference +tgstation-server: src/Tgstation.Server.Host.Console/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

directory  netcoreapp2.1
directory  netcoreapp2.1
 
diff --git a/dir_fb12273b841438c2f2c9b5bc5398c636.html b/dir_fb12273b841438c2f2c9b5bc5398c636.html index a687d3898e..aaa3255164 100644 --- a/dir_fb12273b841438c2f2c9b5bc5398c636.html +++ b/dir_fb12273b841438c2f2c9b5bc5398c636.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

directory  Release
directory  Debug
 
diff --git a/files.html b/files.html index e398f9f2e7..35fdf6a3e3 100644 --- a/files.html +++ b/files.html @@ -133,8 +133,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  User.cs  UserUpdate.cs   obj -  Release -  netstandard2.0 +  Debug +  netstandard2.0  Tgstation.Server.Api.AssemblyInfo.cs   Rights  AdministrationRights.cs @@ -171,8 +171,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  JobsClient.cs  RepositoryClient.cs   obj -  Release -  netstandard2.0 +  Debug +  netstandard2.0  Tgstation.Server.Client.AssemblyInfo.cs  AdministrationClient.cs  ApiClient.cs @@ -309,12 +309,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  InstanceManager.cs  ReattachInfoHandler.cs   Configuration - DatabaseConfiguration.cs - DatabaseType.cs - FileLoggingConfiguration.cs - GeneralConfiguration.cs - SetupWizardMode.cs - UpdatesConfiguration.cs + ControlPanelConfiguration.cs + DatabaseConfiguration.cs + DatabaseType.cs + FileLoggingConfiguration.cs + GeneralConfiguration.cs + SetupWizardMode.cs + UpdatesConfiguration.cs   Controllers  AdministrationController.cs  ApiController.cs @@ -360,7 +361,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  ProcessExecutor.cs  RestartRegistration.cs  SemaphoreSlimContext.cs - SetupWizard.cs + ServiceCollectionExtensions.cs + SetupWizard.cs   IO  Console.cs  DefaultIOManager.cs @@ -422,8 +424,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  WatchdogReattachInformation.cs  WatchdogReattachInformationBase.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.AssemblyInfo.cs  Tgstation.Server.Host.RazorAssemblyInfo.cs   Security @@ -454,8 +456,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  ServerFactory.cs   Tgstation.Server.Host.Console   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.Console.AssemblyInfo.cs  AssemblyInfo.cs  Program.cs @@ -467,8 +469,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  ServerService.cs   Tgstation.Server.Host.Watchdog   obj -  Release -  netstandard2.0 +  Debug +  netstandard2.0  Tgstation.Server.Host.Watchdog.AssemblyInfo.cs  AssemblyInfo.cs  IWatchdog.cs @@ -480,8 +482,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');   Models  TestIrcConnectionStringBuilder.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Api.Tests.AssemblyInfo.cs  Tgstation.Server.Api.Tests.Program.cs   Rights @@ -491,16 +493,16 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');   Components  TestDreamDaemonClient.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Client.Tests.AssemblyInfo.cs  Tgstation.Server.Client.Tests.Program.cs  TestApiClient.cs  TestServerClientFactory.cs   Tgstation.Server.Host.Console.Tests   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs  Tgstation.Server.Host.Console.Tests.Program.cs  TestProgram.cs @@ -526,14 +528,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  TestJobException.cs  TestJobHandler.cs  TestPlatformIdentifier.cs - TestSetupWizard.cs + TestServiceCollectionExtensions.cs + TestSetupWizard.cs   IO  TestConsole.cs  TestPostWriteHandler.cs  TestSymlinkFactory.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.Tests.AssemblyInfo.cs  Tgstation.Server.Host.Tests.Program.cs   Security @@ -541,8 +544,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  TestServerFactory.cs   Tgstation.Server.Host.Watchdog.Tests   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.Watchdog.Tests.AssemblyInfo.cs  Tgstation.Server.Host.Watchdog.Tests.Program.cs  TestWatchdog.cs @@ -553,8 +556,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  ConfigurationTest.cs  InstanceTest.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Tests.AssemblyInfo.cs  Tgstation.Server.Tests.Program.cs  AdministrationTest.cs @@ -564,8 +567,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');   tools   ReleaseNotes   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  ReleaseNotes.AssemblyInfo.cs  Program.cs diff --git a/functions.html b/functions.html index a17990eda8..f71ad45613 100644 --- a/functions.html +++ b/functions.html @@ -228,6 +228,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); , Tgstation.Server.Host.Components.Chat.Commands.RevisionCommand , Tgstation.Server.Host.Components.Chat.Commands.VersionCommand +
  • AllowAnyOrigin +: Tgstation.Server.Host.Configuration.ControlPanelConfiguration +
  • +
  • AllowedOrigins +: Tgstation.Server.Host.Configuration.ControlPanelConfiguration +
  • AllowWebClient : Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters
  • @@ -273,10 +279,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Client.ServerClientFactory
  • ApiConflictException() -: Tgstation.Server.Client.ApiConflictException +: Tgstation.Server.Client.ApiConflictException
  • ApiController() -: Tgstation.Server.Host.Controllers.ApiController +: Tgstation.Server.Host.Controllers.ApiController
  • ApiHeaders() : Tgstation.Server.Api.ApiHeaders @@ -367,7 +373,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • AuthenticationContext : Tgstation.Server.Host.Controllers.ApiController -, Tgstation.Server.Host.Security.AuthenticationContext +, Tgstation.Server.Host.Security.AuthenticationContext
  • AuthenticationContextFactory() : Tgstation.Server.Host.Security.AuthenticationContextFactory diff --git a/functions_b.html b/functions_b.html index 9b79f9119d..302aa5b7ec 100644 --- a/functions_b.html +++ b/functions_b.html @@ -159,6 +159,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • bravoServer : Tgstation.Server.Host.Components.Watchdog.Watchdog
  • +
  • browserResolver +: Tgstation.Server.Host.Controllers.HomeController +
  • BuildModel() : Tgstation.Server.Host.Models.Migrations.MySqlDatabaseContextModelSnapshot , Tgstation.Server.Host.Models.Migrations.SqlServerDatabaseContextModelSnapshot diff --git a/functions_c.html b/functions_c.html index aa3a7f07c1..386c31d73c 100644 --- a/functions_c.html +++ b/functions_c.html @@ -292,6 +292,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • CheckDisposed() : Tgstation.Server.Host.Components.Watchdog.SessionController
  • +
  • CheckExceptionPropagation() +: Tgstation.Server.Host.Server +
  • CheckGetJob() : Tgstation.Server.Host.Core.JobManager
  • @@ -488,7 +491,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • ConfigurationFile : Tgstation.Server.Api.Routes -, Tgstation.Server.Client.Components.ConfigurationClient +, Tgstation.Server.Client.Components.ConfigurationClient
  • ConfigurationRights : Tgstation.Server.Api.Models.InstanceUser @@ -500,9 +503,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Api.Models.Instance
  • Configure() -: Tgstation.Server.Host.Core.Application +: Tgstation.Server.Host.Core.Application , Tgstation.Server.Host.Service.Program
  • +
  • ConfigureControlPanel() +: Tgstation.Server.Host.Core.SetupWizard +
  • ConfigureDatabase() : Tgstation.Server.Host.Core.SetupWizard
  • @@ -552,6 +558,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Api.Models.ConfigurationFile , Tgstation.Server.Host.Components.Chat.Providers.Message +
  • controlPanelConfiguration +: Tgstation.Server.Host.Controllers.HomeController +
  • ConvertApiChatChannel() : Tgstation.Server.Host.Controllers.ChatController
  • @@ -595,12 +604,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); , Tgstation.Server.Host.Controllers.UserController
  • Create< TBody, TResult >() -: Tgstation.Server.Client.ApiClient -, Tgstation.Server.Client.IApiClient +: Tgstation.Server.Client.ApiClient +, Tgstation.Server.Client.IApiClient
  • Create< TResult >() -: Tgstation.Server.Client.ApiClient -, Tgstation.Server.Client.IApiClient +: Tgstation.Server.Client.ApiClient +, Tgstation.Server.Client.IApiClient
  • CreateApiClient() : Tgstation.Server.Client.ApiClientFactory @@ -621,7 +630,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Client.IInstanceManagerClient , Tgstation.Server.Client.InstanceManagerClient , Tgstation.Server.Host.Core.GitHubClientFactory -, Tgstation.Server.Host.Core.IGitHubClientFactory +, Tgstation.Server.Host.Core.IGitHubClientFactory
  • CreateConnection() : Tgstation.Server.Host.Core.DBConnectionFactory @@ -672,8 +681,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); , Tgstation.Server.Host.ServerFactory
  • CreateServerClient() -: Tgstation.Server.Client.IServerClientFactory -, Tgstation.Server.Client.ServerClientFactory +: Tgstation.Server.Client.IServerClientFactory +, Tgstation.Server.Client.ServerClientFactory
  • CreateSymbolicLink() : Tgstation.Server.Host.IO.ISymlinkFactory @@ -689,8 +698,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • CreateSystemIdentity() : Tgstation.Server.Host.Security.ISystemIdentityFactory -, Tgstation.Server.Host.Security.PosixSystemIdentityFactory -, Tgstation.Server.Host.Security.WindowsSystemIdentityFactory +, Tgstation.Server.Host.Security.PosixSystemIdentityFactory +, Tgstation.Server.Host.Security.WindowsSystemIdentityFactory
  • CreateTestInstance() : Tgstation.Server.Tests.InstanceManagerTest diff --git a/functions_d.html b/functions_d.html index d8ea0d9f00..8e18d61c2f 100644 --- a/functions_d.html +++ b/functions_d.html @@ -266,6 +266,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • DevJson : Tgstation.Server.Host.Models.Migrations.DesignTimeDbContextFactoryHelpers
  • +
  • Die() +: Tgstation.Server.Host.Core.IServerControl +, Tgstation.Server.Host.Server +
  • Directory : Tgstation.Server.Host.Configuration.FileLoggingConfiguration , Tgstation.Server.Host.Controllers.ConfigurationController diff --git a/functions_e.html b/functions_e.html index a4f295fe0f..27efadbf97 100644 --- a/functions_e.html +++ b/functions_e.html @@ -128,6 +128,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
    Here is a list of all class members with links to the classes they belong to:

    - e -

    diff --git a/functions_h.html b/functions_h.html index e1d3584e79..4e9a60d80b 100644 --- a/functions_h.html +++ b/functions_h.html @@ -199,10 +199,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); , Tgstation.Server.Host.Components.Chat.Commands.VersionCommand
  • Home() -: Tgstation.Server.Host.Controllers.HomeController +: Tgstation.Server.Host.Controllers.HomeController
  • HomeController() -: Tgstation.Server.Host.Controllers.HomeController +: Tgstation.Server.Host.Controllers.HomeController
  • hostingEnvironment : Tgstation.Server.Host.Core.Application diff --git a/functions_i.html b/functions_i.html index af53fba658..21248e4834 100644 --- a/functions_i.html +++ b/functions_i.html @@ -389,7 +389,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Api.Models.ChatChannel
  • IrcConnectionStringBuilder() -: Tgstation.Server.Api.Models.IrcConnectionStringBuilder +: Tgstation.Server.Api.Models.IrcConnectionStringBuilder
  • IrcProvider() : Tgstation.Server.Host.Components.Chat.Providers.IrcProvider diff --git a/functions_p.html b/functions_p.html index e41e8f2f5d..a42d629871 100644 --- a/functions_p.html +++ b/functions_p.html @@ -277,6 +277,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • PromptYesNo() : Tgstation.Server.Host.Core.SetupWizard
  • +
  • propagatedException +: Tgstation.Server.Host.Server +
  • Provider : Tgstation.Server.Api.Models.Internal.ChatBot , Tgstation.Server.Host.Components.Chat.Providers.Provider diff --git a/functions_prop.html b/functions_prop.html index bd6289fcd0..0731c75829 100644 --- a/functions_prop.html +++ b/functions_prop.html @@ -178,6 +178,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Host.Components.Chat.Commands.CustomCommand , Tgstation.Server.Host.Components.Chat.Commands.ICommand
  • +
  • AllowAnyOrigin +: Tgstation.Server.Host.Configuration.ControlPanelConfiguration +
  • +
  • AllowedOrigins +: Tgstation.Server.Host.Configuration.ControlPanelConfiguration +
  • AllowWebClient : Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters
  • diff --git a/functions_prop_e.html b/functions_prop_e.html index 43b9476b44..a147313cbf 100644 --- a/functions_prop_e.html +++ b/functions_prop_e.html @@ -124,6 +124,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  

    - e -

    diff --git a/functions_vars_b.html b/functions_vars_b.html index 8cc379b055..6b10f80dc9 100644 --- a/functions_vars_b.html +++ b/functions_vars_b.html @@ -141,6 +141,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • bravoServer : Tgstation.Server.Host.Components.Watchdog.Watchdog
  • +
  • browserResolver +: Tgstation.Server.Host.Controllers.HomeController +
  • builtinCommands : Tgstation.Server.Host.Components.Chat.Chat
  • diff --git a/functions_vars_c.html b/functions_vars_c.html index 642fe018e2..cc297d75c1 100644 --- a/functions_vars_c.html +++ b/functions_vars_c.html @@ -242,6 +242,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • console : Tgstation.Server.Host.Core.SetupWizard
  • +
  • controlPanelConfiguration +: Tgstation.Server.Host.Controllers.HomeController +
  • counter : Tgstation.Server.Tests.InstanceManagerTest
  • diff --git a/functions_vars_p.html b/functions_vars_p.html index ff0eb288b9..a3ba421718 100644 --- a/functions_vars_p.html +++ b/functions_vars_p.html @@ -180,6 +180,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Api.Tests.TestApiHeaders , Tgstation.Server.Client.ServerClientFactory +
  • propagatedException +: Tgstation.Server.Host.Server +
  • providerFactory : Tgstation.Server.Host.Components.Chat.Chat , Tgstation.Server.Host.Components.Chat.ChatFactory diff --git a/functions_vars_r.html b/functions_vars_r.html index 28e22a8270..6b4acbe788 100644 --- a/functions_vars_r.html +++ b/functions_vars_r.html @@ -180,6 +180,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • requestLoggers : Tgstation.Server.Client.ApiClient
  • +
  • requireHeaders +: Tgstation.Server.Host.Controllers.ApiController +
  • requireInstance : Tgstation.Server.Host.Controllers.ApiController
  • diff --git a/functions_vars_s.html b/functions_vars_s.html index 97a86df2fe..28a586adb4 100644 --- a/functions_vars_s.html +++ b/functions_vars_s.html @@ -134,10 +134,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); , Tgstation.Server.Host.Components.Compiler.TemporaryDmbProvider
  • Section -: Tgstation.Server.Host.Configuration.DatabaseConfiguration +: Tgstation.Server.Host.Configuration.ControlPanelConfiguration +, Tgstation.Server.Host.Configuration.DatabaseConfiguration , Tgstation.Server.Host.Configuration.FileLoggingConfiguration , Tgstation.Server.Host.Configuration.GeneralConfiguration , Tgstation.Server.Host.Configuration.UpdatesConfiguration +, Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig1 +, Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig2 +, Tgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.GoodConfig
  • SecureStringLength : Tgstation.Server.Host.Security.CryptographySuite @@ -154,6 +158,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • serverControl : Tgstation.Server.Host.Components.Chat.ChatFactory +, Tgstation.Server.Host.Components.InstanceManager , Tgstation.Server.Host.Components.Watchdog.WatchdogFactory
  • serverUpdater diff --git a/hierarchy.html b/hierarchy.html index 5db76873da..8e6a47bf8d 100644 --- a/hierarchy.html +++ b/hierarchy.html @@ -100,387 +100,394 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  CAuthorizeAttribute  CTgstation.Server.Host.Controllers.TgsAuthorizeAttributeHelper for using the AuthorizeAttribute with the Api.Rights system  CAutoGeneratedProgram - CTgstation.Server.Api.Models.ByondRepresents a BYOND installation - CTgstation.Server.Tests.Instance.ByondTest - CTgstation.Server.Host.Components.Chat.ChannelRepresents a Providers.IProvider channel - CTgstation.Server.Host.Components.Chat.ChannelMappingRepresents a mapping of a Channel.RealId - CTgstation.Server.Api.Models.Internal.ChatBotManage the server chat bots - CTgstation.Server.Api.Models.ChatBot - CTgstation.Server.Host.Models.ChatBot - CTgstation.Server.Api.Models.ChatChannelIndicates a chat channel - CTgstation.Server.Host.Models.ChatChannel - CTgstation.Server.Host.Components.Interop.ChatCommand - CTgstation.Server.Api.Models.Internal.ChatConnectionStringBuilderHelper for building ChatBot.ConnectionStrings - CTgstation.Server.Api.Models.DiscordConnectionStringBuilderChatConnectionStringBuilder for ChatProvider.Discord - CTgstation.Server.Api.Models.IrcConnectionStringBuilderChatConnectionStringBuilder for ChatProvider.Irc - CTgstation.Server.Host.Components.Interop.CommCommandRepresents a command from DD - CTgstation.Server.Api.Models.Internal.CompileJobRepresents a run of DreamMaker - CTgstation.Server.Api.Models.CompileJob - CTgstation.Server.Host.Models.CompileJob - CTgstation.Server.Api.Models.ConfigurationFileRepresents a game configuration file. Create and delete actions uncerimonuously overwrite/delete files - CTgstation.Server.Tests.Instance.ConfigurationTest - CTgstation.Server.Host.Components.Interop.Constants - CController - CTgstation.Server.Host.Controllers.ApiControllerA Controller for API functions - CTgstation.Server.Host.Controllers.HomeControllerMain ApiController for the Application - CTgstation.Server.Host.Controllers.ModelController< TModel >An ApiController representing a TModel - CTgstation.Server.Host.Configuration.DatabaseConfigurationConfiguration options for the Models.DatabaseContext<TParentContext> - CTgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext > - CTgstation.Server.Host.Models.MySqlDatabaseContextDatabaseContext<TParentContext> for MySQL - CTgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext > - CTgstation.Server.Host.Models.SqlServerDatabaseContextDatabaseContext<TParentContext> for Sqlserver - CDbContext - CTgstation.Server.Host.Models.DatabaseContext< TParentContext > - CTgstation.Server.Host.Models.Migrations.DesignTimeDbContextFactoryHelpersContains helpers for creating design time Models.DatabaseContext<TParentContext>s - CTgstation.Server.Api.Models.Internal.DreamDaemonLaunchParametersLaunch settings for DreamDaemon - CTgstation.Server.Api.Models.Internal.DreamDaemonSettingsConfigurable settings for DreamDaemon - CTgstation.Server.Api.Models.DreamDaemonRepresents an instance of BYOND's DreamDaemon game server. Create action starts the server. Delete action shuts down the server - CTgstation.Server.Host.Models.DreamDaemonSettings - CTgstation.Server.Api.Models.DreamMakerRepresents the state of the DreamMaker compiler. Create action starts a new compile. Delete action cancels the current compile - CTgstation.Server.Host.Models.DreamMakerSettings - CTgstation.Server.Api.Models.ErrorMessageRepresents an error message returned by the server - CTgstation.Server.Host.Components.Interop.EventNotificationFor notifying DD of EventTypes - CException - CTgstation.Server.Client.ClientExceptionExceptions thrown by IServerClients - CTgstation.Server.Client.ApiConflictExceptionOccurs when the server returns an unknown response - CTgstation.Server.Client.ApiMismatchExceptionOccurs when the API version of the client is not compatible with the server's - CTgstation.Server.Client.ConflictExceptionOccurs when the client performs an action that would result in data conflict - CTgstation.Server.Client.InsufficientPermissionsExceptionOccurs when the client attempts to perform an action they do not have the rights for - CTgstation.Server.Client.MethodNotSupportedExceptionOccurs when the client tries to use a currently unsupported API - CTgstation.Server.Client.RateLimitExceptionOccurs when a GitHub rate limit occurs - CTgstation.Server.Client.RequestTimeoutExceptionOccurs when the client provides invalid credentials - CTgstation.Server.Client.ServerErrorExceptionOccurs when an error occurs in the server - CTgstation.Server.Client.ServiceUnavailableExceptionOccurs when the client makes a request while the server is starting or stopping - CTgstation.Server.Client.UnauthorizedExceptionOccurs when the client provides invalid credentials - CTgstation.Server.Client.UnrecognizedResponseExceptionOccurs when a response is received that did not deserialize to one of the expected Api.Models - CTgstation.Server.Host.Core.JobExceptionOperation exceptions thrown from the context of a Models.Job - CTgstation.Server.Host.Configuration.FileLoggingConfigurationFile logging configuration options - CTgstation.Server.Host.Configuration.GeneralConfigurationGeneral configuration options - CTgstation.Server.Client.IAdministrationClientFor managing server administration - CTgstation.Server.Client.AdministrationClient - CTgstation.Server.Client.IApiClientFactoryFor creating IApiClients - CTgstation.Server.Client.ApiClientFactory - CTgstation.Server.Host.Core.IApplicationConfigures the ASP.NET Core web application - CTgstation.Server.Host.Core.Application - CIApplicationLifetime - CTgstation.Server.Host.Core.Tests.TestApplication.MockApplicationLifetime - CTgstation.Server.Host.Core.IAsyncDelayerFor waiting asynchronously - CTgstation.Server.Host.Core.AsyncDelayer - CTgstation.Server.Host.Security.IAuthenticationContextFactoryFor creating and accessing authentication contexts - CTgstation.Server.Host.Security.AuthenticationContextFactory - CTgstation.Server.Client.Components.IByondClientFor managing the Byond installation - CTgstation.Server.Client.Components.ByondClient - CTgstation.Server.Host.Components.Byond.IByondInstallerFor downloading and installing BYOND extractions for a given system - CTgstation.Server.Host.Components.Byond.PosixByondInstallerIByondInstaller for Posix systems - CTgstation.Server.Host.Components.Byond.WindowsByondInstallerIByondInstaller for windows systems - CTgstation.Server.Client.Components.IChatBotsClientFor managing the chat bots - CTgstation.Server.Client.Components.ChatBotsClient - CTgstation.Server.Host.Components.Chat.IChatFactoryFor creating IChats - CTgstation.Server.Host.Components.Chat.ChatFactory - CTgstation.Server.Host.Security.IClaimsInjectorFor injecting System.Security.Claims.Claims that Controllers.TgsAuthorizeAttribute can look for - CTgstation.Server.Host.Security.ClaimsInjector - CTgstation.Server.Host.Components.Chat.Commands.ICommandRepresents a command that can be invoked by talking to chat bots - CTgstation.Server.Host.Components.Chat.Commands.ByondCommandFor displaying the installed Byond version - CTgstation.Server.Host.Components.Chat.Commands.CustomCommandRepresents a command made from DM code - CTgstation.Server.Host.Components.Chat.Commands.KekCommandkek - CTgstation.Server.Host.Components.Chat.Commands.PullRequestsCommandCommand for reading the active Api.Models.TestMerges - CTgstation.Server.Host.Components.Chat.Commands.RevisionCommandFor displaying Api.Models.Internal.RevisionInformation - CTgstation.Server.Host.Components.Chat.Commands.VersionCommandICommand to return the IApplication.VersionString - CTgstation.Server.Host.Components.Chat.Commands.ICommandFactoryFactory for built in ICommands - CTgstation.Server.Host.Components.Chat.Commands.CommandFactory - CTgstation.Server.Host.Components.Interop.ICommHandlerHandles CommCommands - CTgstation.Server.Host.Components.Watchdog.SessionController - CTgstation.Server.Client.Components.IConfigurationClientFor managing ConfigurationFile files - CTgstation.Server.Client.Components.ConfigurationClient - CTgstation.Server.Host.IO.IConsoleAbstraction for System.Console - CTgstation.Server.Host.IO.Console - CTgstation.Server.Host.Components.Repository.ICredentialsProviderFor generating CredentialsHandlers - CTgstation.Server.Host.Components.Repository.CredentialsProvider - CTgstation.Server.Host.Security.ICryptographySuiteContains various cryptographic functions - CTgstation.Server.Host.Security.CryptographySuite - CTgstation.Server.Host.Components.Chat.ICustomCommandHandlerHandles Commands.ICommands that map to those defined in a IJsonTrackingContext - CTgstation.Server.Host.Components.Watchdog.Watchdog - CTgstation.Server.Host.Models.IDatabaseContextRepresents the database - CTgstation.Server.Host.Models.DatabaseContext< TParentContext > - CTgstation.Server.Host.Core.IDatabaseContextFactoryFactory for scoping usage of IDatabaseContexts. Meant for use by Components - CTgstation.Server.Host.Core.DatabaseContextFactory - CTgstation.Server.Host.Models.IDatabaseSeederFor initially seeding a database - CTgstation.Server.Host.Models.DatabaseSeeder - CTgstation.Server.Host.Core.IDBConnectionFactoryFor creating DbConnection - CTgstation.Server.Host.Core.DBConnectionFactory - CIDesignTimeDbContextFactory - CTgstation.Server.Host.Models.Migrations.MySqlDesignTimeDbContextFactory - CTgstation.Server.Host.Models.Migrations.SqlServerDesignTimeDbContextFactory - CIDisposable - CTgstation.Server.Client.IApiClientWeb interface for the API - CTgstation.Server.Client.ApiClient - CTgstation.Server.Client.IHttpClientFor sending HTTP requests - CTgstation.Server.Client.HttpClient - CTgstation.Server.Client.IServerClientMain client for communicating with a server - CTgstation.Server.Client.ServerClient - CTgstation.Server.Host.Components.Byond.IByondExecutableLockRepresents usage of the two primary BYOND server executables - CTgstation.Server.Host.Components.Byond.ByondExecutableLock - CTgstation.Server.Host.Components.Byond.IByondManagerFor managing the BYOND installation - CTgstation.Server.Host.Components.Byond.ByondManager - CTgstation.Server.Host.Components.Byond.WindowsByondInstallerIByondInstaller for windows systems - CTgstation.Server.Host.Components.Chat.IChatFor managing connected chat services - CTgstation.Server.Host.Components.Chat.Chat - CTgstation.Server.Host.Components.Chat.IJsonTrackingContextRepresents a tracking of dynamic chat json files - CTgstation.Server.Host.Components.Chat.JsonTrackingContext - CTgstation.Server.Host.Components.Chat.Providers.IProviderFor interacting with a chat service - CTgstation.Server.Host.Components.Chat.Providers.Provider - CTgstation.Server.Host.Components.Chat.Providers.DiscordProviderIProvider for the Discord app - CTgstation.Server.Host.Components.Chat.Providers.IrcProviderIProvider for internet relay chat - CTgstation.Server.Host.Components.Compiler.ICompileJobConsumerSink for CompileJobs - CTgstation.Server.Host.Components.Compiler.DmbFactoryStandard IDmbFactory - CTgstation.Server.Host.Components.IDmbProviderProvides absolute paths to the latest compiled .dmbs - CTgstation.Server.Host.Components.Compiler.DmbProvider - CTgstation.Server.Host.Components.Compiler.TemporaryDmbProviderTemporary IDmbProvider - CTgstation.Server.Host.Components.IInstanceFor interacting with the instance services - CTgstation.Server.Host.Components.Instance - CTgstation.Server.Host.Components.InstanceManager - CTgstation.Server.Host.Components.Interop.ICommContextRepresents a registration of an interop session - CTgstation.Server.Host.Components.Interop.CommContext - CTgstation.Server.Host.Components.Repository.IRepositoryRepresents an on-disk git repository - CTgstation.Server.Host.Components.Repository.Repository - CTgstation.Server.Host.Components.Repository.IRepositoryManagerFactory for creating and loading IRepositorys - CTgstation.Server.Host.Components.Repository.RepositoryManager - CTgstation.Server.Host.Components.StaticFiles.IConfigurationFor managing the Configuration directory - CTgstation.Server.Host.Components.StaticFiles.Configuration - CTgstation.Server.Host.Components.Watchdog.IWatchdogRuns and monitors the twin server controllers - CTgstation.Server.Host.Components.Watchdog.Watchdog - CTgstation.Server.Host.Components.Watchdog.WindowsNetworkPromptReaper - CTgstation.Server.Host.Core.IProcessBaseRepresents process lifetime - CTgstation.Server.Host.Components.Watchdog.ISessionControllerHandles communication with a DreamDaemon IProcess - CTgstation.Server.Host.Components.Watchdog.DeadSessionControllerImplements a fake "dead" ISessionController - CTgstation.Server.Host.Components.Watchdog.SessionController - CTgstation.Server.Host.Core.IProcessAbstraction over a System.Diagnostics.Process - CTgstation.Server.Host.Core.Process - CTgstation.Server.Host.Core.IRestartRegistrationRepresents the lifetime of a IRestartHandler registration - CTgstation.Server.Host.Core.RestartRegistration - CTgstation.Server.Host.Core.JobHandlerClass for pairing Tasks with CancellationTokenSources - CTgstation.Server.Host.Core.JobManager - CTgstation.Server.Host.Core.SemaphoreSlimContextAsync lock context helper - CTgstation.Server.Host.Security.AuthenticationContextFactory - CTgstation.Server.Host.Security.IAuthenticationContextRepresents the currently authenticated Api.Models.User - CTgstation.Server.Host.Security.AuthenticationContextManages Api.Models.Users for a scope - CTgstation.Server.Host.Security.IdentityCache - CTgstation.Server.Host.Security.IdentityCacheObjectFor keeping a specific ISystemIdentity alive for a period of time - CTgstation.Server.Host.Security.ISystemIdentityRepresents a user on the current System.Runtime.InteropServices.OSPlatform - CTgstation.Server.Host.Security.WindowsSystemIdentityISystemIdentity for windows systems - CTgstation.Server.Host.Components.Compiler.IDmbFactoryFactory for IDmbProviders - CTgstation.Server.Host.Components.Compiler.DmbFactoryStandard IDmbFactory - CTgstation.Server.Client.Components.IDreamDaemonClientFor managing DreamDaemon - CTgstation.Server.Client.Components.DreamDaemonClient - CTgstation.Server.Host.Components.Compiler.IDreamMakerFor managing the compiler - CTgstation.Server.Host.Components.Compiler.DreamMaker - CTgstation.Server.Client.Components.IDreamMakerClientFor managing the compiler - CTgstation.Server.Client.Components.DreamMakerClient - CTgstation.Server.Host.Components.IEventConsumerConsumes EventTypes and takes the appropriate actions - CTgstation.Server.Host.Components.EventConsumer - CTgstation.Server.Host.Components.StaticFiles.IConfigurationFor managing the Configuration directory - CTgstation.Server.Host.Components.Watchdog.IWatchdogRuns and monitors the twin server controllers - CIEventLog - CTgstation.Server.Host.Service.ServerServiceRepresents a IWatchdog as a ServiceBase - CTgstation.Server.Host.Core.IGitHubClientFactoryFor creating IGitHubClients - CTgstation.Server.Host.Core.GitHubClientFactory - CIHostedService - CTgstation.Server.Host.Components.Byond.IByondManagerFor managing the BYOND installation - CTgstation.Server.Host.Components.Chat.IChatFor managing connected chat services - CTgstation.Server.Host.Components.Compiler.ICompileJobConsumerSink for CompileJobs - CTgstation.Server.Host.Components.IInstanceFor interacting with the instance services - CTgstation.Server.Host.Components.IInstanceFactoryFactory for creating IInstances - CTgstation.Server.Host.Components.InstanceFactory - CTgstation.Server.Host.Components.InstanceManager - CTgstation.Server.Host.Components.StaticFiles.IConfigurationFor managing the Configuration directory - CTgstation.Server.Host.Components.Watchdog.IWatchdogRuns and monitors the twin server controllers - CTgstation.Server.Host.Components.Watchdog.WindowsNetworkPromptReaper - CTgstation.Server.Host.Core.IJobManagerManages the runtime of Jobs - CTgstation.Server.Host.Core.JobManager - CTgstation.Server.Host.Security.IIdentityCacheFor caching ISystemIdentitys - CTgstation.Server.Host.Security.IdentityCache - CTgstation.Server.Client.Components.IInstanceClientFor managing a single Instance - CTgstation.Server.Client.Components.InstanceClient - CTgstation.Server.Host.Components.IInstanceManagerFor managing IInstances - CTgstation.Server.Host.Components.InstanceManager - CTgstation.Server.Client.IInstanceManagerClientFor managing Instances - CTgstation.Server.Client.InstanceManagerClient - CTgstation.Server.Client.Components.IInstanceUserClientFor managing InstanceUsers - CTgstation.Server.Client.Components.InstanceUserClient - CTgstation.Server.Host.IO.IIOManagerInterface for using filesystems - CTgstation.Server.Host.IO.DefaultIOManagerIIOManager that resolves paths to Environment.CurrentDirectory - CTgstation.Server.Host.IO.ResolvingIOManagerAn IIOManager that resolve relative paths from another IIOManager to a subdirectory of that - CTgstation.Server.Client.Components.IJobsClientAccess to running jobs - CTgstation.Server.Client.Components.JobsClient - CTgstation.Server.Host.Components.Watchdog.INetworkPromptReaperOn 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 - CTgstation.Server.Host.Components.Watchdog.PosixNetworkPromptReaper - CTgstation.Server.Host.Components.Watchdog.WindowsNetworkPromptReaper - CTgstation.Server.Api.Models.InstanceMetadata about a server instance - CTgstation.Server.Host.Models.InstanceRepresents an Api.Models.Instance in the database - CTgstation.Server.Tests.InstanceManagerTest - CTgstation.Server.Tests.Instance.InstanceTest - CTgstation.Server.Api.Models.InstanceUserRepresents a Users permissions in an Instance - CTgstation.Server.Host.Models.InstanceUser - CTgstation.Server.Tests.IntegrationTest - CTgstation.Server.Host.Core.IPlatformIdentifierFor identifying the current platform - CTgstation.Server.Host.Core.PlatformIdentifier - CTgstation.Server.Host.IO.IPostWriteHandler - CTgstation.Server.Host.IO.PosixPostWriteHandlerIPostWriteHandler for POSIX systems - CTgstation.Server.Host.IO.WindowsPostWriteHandlerIPostWriteHandler for Windows systems - CTgstation.Server.Host.Core.IProcessExecutorFor launching IProcess' - CTgstation.Server.Host.Core.ProcessExecutor - CTgstation.Server.Host.Components.Chat.IProviderFactoryFactory for IProviders - CTgstation.Server.Host.Components.Chat.ProviderFactory - CTgstation.Server.Host.Components.Watchdog.IReattachInfoHandlerHandles saving and loading WatchdogReattachInformation - CTgstation.Server.Host.Components.ReattachInfoHandler - CTgstation.Server.Client.Components.IRepositoryClientFor managing the Repository - CTgstation.Server.Client.Components.RepositoryClient - CTgstation.Server.Client.IRequestLoggerFor logging HTTP requests and responses - CTgstation.Server.Host.Core.IRestartHandlerHandler for server restarts - CTgstation.Server.Host.Components.Chat.Chat - CTgstation.Server.Host.Components.InstanceManager - CTgstation.Server.Host.Components.Watchdog.Watchdog - CTgstation.Server.Host.IServerRepresents the host - CTgstation.Server.Host.Server - CTgstation.Server.Tests.TestingServer - CTgstation.Server.Client.IServerClientFactoryFactory for creating IServerClients - CTgstation.Server.Client.ServerClientFactory - CTgstation.Server.Host.Core.IServerControlRepresents a service that may take an updated Host assembly and run it, stopping the current assembly in the process - CTgstation.Server.Host.Server - CTgstation.Server.Host.IServerFactoryFor creating IServers - CTgstation.Server.Host.ServerFactory - CTgstation.Server.Host.Components.Watchdog.ISessionControllerFactoryFactory for ISessionControllers - CTgstation.Server.Host.Components.Watchdog.SessionControllerFactory - CTgstation.Server.Host.Core.ISetupWizardThe command line Configuration setup wizard - CTgstation.Server.Host.Core.SetupWizard - CTgstation.Server.Host.Core.Tests.TestApplication.MockSetupWizard - CTgstation.Server.Host.IO.ISymlinkFactoryFor creating filesystem symbolic links - CTgstation.Server.Host.IO.PosixSymlinkFactoryISymlinkFactory for posix systems - CTgstation.Server.Host.IO.WindowsSymlinkFactoryISymlinkFactory for windows systems - CTgstation.Server.Host.IO.ISynchronousIOManagerFor accessing the disk in a synchronous manner - CTgstation.Server.Host.IO.SynchronousIOManager - CTgstation.Server.Host.Security.ISystemIdentityFactoryFactory for ISystemIdentitys - CTgstation.Server.Host.Security.PosixSystemIdentityFactoryISystemIdentityFactory for posix systems - CTgstation.Server.Host.Security.WindowsSystemIdentityFactoryISystemIdentityFactory for windows systems. Uses long running tasks due to potential networked domains - CTgstation.Server.Host.Security.ITokenFactoryFor creating Tokens - CTgstation.Server.Host.Security.TokenFactory - CTgstation.Server.Client.IUsersClientFor managing Users - CTgstation.Server.Client.UsersClient - CTgstation.Server.Host.Watchdog.IWatchdogThe watchdog for a Host - CTgstation.Server.Host.Watchdog.Watchdog - CTgstation.Server.Host.Components.Watchdog.IWatchdogFactoryFor creating IWatchdogs - CTgstation.Server.Host.Components.Watchdog.WatchdogFactory - CTgstation.Server.Host.Watchdog.IWatchdogFactoryFactory for creating IWatchdogs - CTgstation.Server.Host.Watchdog.WatchdogFactory - CTgstation.Server.Api.Models.Internal.JobRepresents a long running job - CTgstation.Server.Api.Models.JobRepresents a long running job on the server. Model is read-only, updates attempt to cancel the job - CTgstation.Server.Host.Models.Job - CJsonConverter - CTgstation.Server.Host.Components.Chat.Commands.BoolConverterJsonConverter for decoding bools returned by BYOND - CTgstation.Server.Host.Components.Interop.JsonFileRepresentation of the initial json passed to DreamDaemon - CTgstation.Server.Host.Components.Interop.JsonSubFileListInformation used in for reattaching and interop - CTgstation.Server.Host.Models.ReattachInformationBaseBase class for ReattachInformation - CTgstation.Server.Host.Components.Watchdog.ReattachInformationParameters necessary for duplicating a ISessionController session - CTgstation.Server.Host.Models.ReattachInformationDatabase representation of Components.Watchdog.ReattachInformation - CTgstation.Server.Host.Components.Watchdog.LaunchResultRepresents the result of trying to start a DD process - CTgstation.Server.Host.Components.Chat.Providers.MessageRepresents a message recieved by a IProvider - CMigration - CTgstation.Server.Host.Models.Migrations.MSAddMinimumSecurityAdd the Api.Models.Internal.CompileJob.MinimumSecurityLevel and DreamMaker.ApiValidationSecurityLevel columns for MSSQL - CTgstation.Server.Host.Models.Migrations.MSInitialCreateThe initial database migration for MSSQL - CTgstation.Server.Host.Models.Migrations.MSNullableAndForeignKeyCleanupCleans up of nullable columns and foreign keys MSSQL - CTgstation.Server.Host.Models.Migrations.MSReattachCompileJobRequiredMarks the CompileJobs of ReattachInformations as non-nullable for MSSQL - CTgstation.Server.Host.Models.Migrations.MYAddMinimumSecurityAdd the Api.Models.Internal.CompileJob.MinimumSecurityLevel and DreamMaker.ApiValidationSecurityLevel columns for MySQL/MariaDB - CTgstation.Server.Host.Models.Migrations.MYInitialCreateThe initial database migration for MySQL/MariaDB - CTgstation.Server.Host.Models.Migrations.MYNullableAndForeignKeyCleanupCleans up of nullable columns and foreign keys MySQL/MariaDB - CTgstation.Server.Host.Models.Migrations.MYReattachCompileJobRequiredMarks the CompileJobs of ReattachInformations as non-nullable for MySQL/MariaDB - CTgstation.Server.Host.Controllers.ModelController< Administration > - CTgstation.Server.Host.Controllers.AdministrationControllerModelController<TModel> for Administration - CTgstation.Server.Host.Controllers.ModelController< Api.Models.Byond > - CTgstation.Server.Host.Controllers.ByondControllerController for managing Api.Models.Byond.Versions - CTgstation.Server.Host.Controllers.ModelController< Api.Models.ChatBot > - CTgstation.Server.Host.Controllers.ChatControllerModelController<TModel> for managing Api.Models.ChatBots - CTgstation.Server.Host.Controllers.ModelController< Api.Models.DreamMaker > - CTgstation.Server.Host.Controllers.DreamMakerControllerController for managing the compiler - CTgstation.Server.Host.Controllers.ModelController< Api.Models.Instance > - CTgstation.Server.Host.Controllers.InstanceControllerController for managing Components.Instances - CTgstation.Server.Host.Controllers.ModelController< Api.Models.InstanceUser > - CTgstation.Server.Host.Controllers.InstanceUserControllerFor managing Users - CTgstation.Server.Host.Controllers.ModelController< Api.Models.Job > - CTgstation.Server.Host.Controllers.JobControllerModelController<TModel> for Api.Models.Jobs - CTgstation.Server.Host.Controllers.ModelController< ConfigurationFile > - CTgstation.Server.Host.Controllers.ConfigurationControllerThe ModelController<TModel> for ConfigurationFiles - CTgstation.Server.Host.Controllers.ModelController< DreamDaemon > - CTgstation.Server.Host.Controllers.DreamDaemonControllerModelController<TModel> for managing the DreamDaemon - CTgstation.Server.Host.Controllers.ModelController< Repository > - CTgstation.Server.Host.Controllers.RepositoryControllerController for managing the Repositorys - CTgstation.Server.Host.Controllers.ModelController< UserUpdate > - CTgstation.Server.Host.Controllers.UserControllerFor managing Users - CModelSnapshot - CTgstation.Server.Host.Models.Migrations.MySqlDatabaseContextModelSnapshot - CTgstation.Server.Host.Models.Migrations.SqlServerDatabaseContextModelSnapshot - CTgstation.Server.Host.Components.Watchdog.MonitorStateThe (absolute) state of the Watchdog - CTgstation.Server.Host.NativeMethodsNative methods used by the code - CTgstation.Server.Host.ProgramEntrypoint for the Process - CTgstation.Server.Host.Console.ProgramContains the entrypoint for the application - CTgstation.Server.Host.Service.ProgramContains the entrypoint for the application - CReleaseNotes.ProgramContains the application entrypoint - CTgstation.Server.Api.Models.Internal.RepositorySettingsRepresents configurable settings for a Repository - CTgstation.Server.Api.Models.RepositoryRepresents a git repository - CTgstation.Server.Host.Models.RepositorySettings - CTgstation.Server.Host.Components.Chat.ResponseRepresents a chat message requested by DD - CTgstation.Server.Host.Models.RevInfoTestMergeMany to many relationship for Models.RevisionInformation and Models.TestMerge - CTgstation.Server.Api.Models.Internal.RevisionInformationRepresents information about a current git revison - CTgstation.Server.Api.Models.RevisionInformation - CTgstation.Server.Host.Models.RevisionInformation - CTgstation.Server.Api.Rights.RightsHelperHelper for RightsTypes - CTgstation.Server.Api.RoutesRoutes to a server actions - CTgstation.Server.Api.Models.ServerInformationRepresents basic server information - CTgstation.Server.Host.Components.StaticFiles.ServerSideModificationsRepresents code modifications via configuration - CServiceBase - CTgstation.Server.Host.Service.ServerServiceRepresents a IWatchdog as a ServiceBase - CTgstation.Server.Client.Tests.TestApiClient - CTgstation.Server.Api.Tests.TestApiHeadersTests for ApiHeaders - CTgstation.Server.Host.Core.Tests.TestApplicationTests for Application - CTgstation.Server.Host.Core.Tests.TestAsyncDelayer - CTgstation.Server.Host.Security.Tests.TestAuthenticationContextTests for AuthenticationContext - CTgstation.Server.Host.IO.Tests.TestConsole - CTgstation.Server.Host.Core.Tests.TestDatabaseContextFactory - CTgstation.Server.Host.Core.Tests.TestDBConnectionFactory - CTgstation.Server.Host.Components.Chat.Providers.Tests.TestDiscordProvider - CTgstation.Server.Client.Components.Tests.TestDreamDaemonClient - CTgstation.Server.Host.Core.Tests.TestGitHubClientFactory - CTgstation.Server.Api.Models.Tests.TestIrcConnectionStringBuilder - CTgstation.Server.Host.Core.Tests.TestJobException - CTgstation.Server.Host.Core.Tests.TestJobHandler - CTgstation.Server.Api.Models.TestMergeParametersParameters for creating a TestMerge - CTgstation.Server.Api.Models.Internal.TestMergeBaseLayer of test merge data required internally - CTgstation.Server.Api.Models.Internal.TestMergeRepresents a merge of a GitHub pull request - CTgstation.Server.Api.Models.TestMerge - CTgstation.Server.Host.Models.TestMerge - CTgstation.Server.Host.Components.Interop.TestMergeThis model mirrors /datum/tgs_revision_information/test_merge - CTgstation.Server.Host.Core.Tests.TestPlatformIdentifier - CTgstation.Server.Host.Components.Byond.Tests.TestPosixByondInstaller - CTgstation.Server.Host.IO.Tests.TestPostWriteHandler - CTgstation.Server.Host.Console.Tests.TestProgram - CTgstation.Server.Api.Rights.Tests.TestRightsTests for Rights - CTgstation.Server.Client.Tests.TestServerClientFactory - CTgstation.Server.Host.Tests.TestServerFactoryTests for ServerFactory - CTgstation.Server.Host.Service.Tests.TestServerServiceTests for ServerService - CTgstation.Server.Host.Core.Tests.TestSetupWizard - CTgstation.Server.Host.IO.Tests.TestSymlinkFactory - CTgstation.Server.Host.Components.Watchdog.Tests.TestWatchdog - CTgstation.Server.Host.Watchdog.Tests.TestWatchdog - CTgstation.Server.Host.Watchdog.Tests.TestWatchdogFactoryTests for WatchdogFactory - CTgstation.Server.Api.Models.TokenRepresents a JWT returned by the API - CTgstation.Server.Host.Configuration.UpdatesConfigurationConfiguration for the automatic update system - CTgstation.Server.Api.Models.Internal.UserRepresents a server User - CTgstation.Server.Api.Models.User - CTgstation.Server.Api.Models.UserUpdateFor editing a given User. Will never be returned by the API - CTgstation.Server.Host.Models.User - CTgstation.Server.Host.Components.Chat.UserRepresents a tgs_chat_user datum - CTgstation.Server.Host.Models.WatchdogReattachInformationBaseBase class for WatchdogReattachInformation - CTgstation.Server.Host.Components.Watchdog.WatchdogReattachInformationReattach information for a IWatchdog - CTgstation.Server.Host.Models.WatchdogReattachInformationDatabase representation of Components.Watchdog.WatchdogReattachInformation + CTgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig1 + CTgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig2 + CTgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.BadConfig3 + CTgstation.Server.Api.Models.ByondRepresents a BYOND installation + CTgstation.Server.Tests.Instance.ByondTest + CTgstation.Server.Host.Components.Chat.ChannelRepresents a Providers.IProvider channel + CTgstation.Server.Host.Components.Chat.ChannelMappingRepresents a mapping of a Channel.RealId + CTgstation.Server.Api.Models.Internal.ChatBotManage the server chat bots + CTgstation.Server.Api.Models.ChatBot + CTgstation.Server.Host.Models.ChatBot + CTgstation.Server.Api.Models.ChatChannelIndicates a chat channel + CTgstation.Server.Host.Models.ChatChannel + CTgstation.Server.Host.Components.Interop.ChatCommand + CTgstation.Server.Api.Models.Internal.ChatConnectionStringBuilderHelper for building ChatBot.ConnectionStrings + CTgstation.Server.Api.Models.DiscordConnectionStringBuilderChatConnectionStringBuilder for ChatProvider.Discord + CTgstation.Server.Api.Models.IrcConnectionStringBuilderChatConnectionStringBuilder for ChatProvider.Irc + CTgstation.Server.Host.Components.Interop.CommCommandRepresents a command from DD + CTgstation.Server.Api.Models.Internal.CompileJobRepresents a run of DreamMaker + CTgstation.Server.Api.Models.CompileJob + CTgstation.Server.Host.Models.CompileJob + CTgstation.Server.Api.Models.ConfigurationFileRepresents a game configuration file. Create and delete actions uncerimonuously overwrite/delete files + CTgstation.Server.Tests.Instance.ConfigurationTest + CTgstation.Server.Host.Components.Interop.Constants + CController + CTgstation.Server.Host.Controllers.ApiControllerA Controller for API functions + CTgstation.Server.Host.Controllers.HomeControllerMain ApiController for the Application + CTgstation.Server.Host.Controllers.ModelController< TModel >An ApiController representing a TModel + CTgstation.Server.Host.Configuration.ControlPanelConfigurationConfiguration options for the web control panel + CTgstation.Server.Host.Configuration.DatabaseConfigurationConfiguration options for the Models.DatabaseContext<TParentContext> + CTgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext > + CTgstation.Server.Host.Models.MySqlDatabaseContextDatabaseContext<TParentContext> for MySQL + CTgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext > + CTgstation.Server.Host.Models.SqlServerDatabaseContextDatabaseContext<TParentContext> for Sqlserver + CDbContext + CTgstation.Server.Host.Models.DatabaseContext< TParentContext > + CTgstation.Server.Host.Models.Migrations.DesignTimeDbContextFactoryHelpersContains helpers for creating design time Models.DatabaseContext<TParentContext>s + CTgstation.Server.Api.Models.Internal.DreamDaemonLaunchParametersLaunch settings for DreamDaemon + CTgstation.Server.Api.Models.Internal.DreamDaemonSettingsConfigurable settings for DreamDaemon + CTgstation.Server.Api.Models.DreamDaemonRepresents an instance of BYOND's DreamDaemon game server. Create action starts the server. Delete action shuts down the server + CTgstation.Server.Host.Models.DreamDaemonSettings + CTgstation.Server.Api.Models.DreamMakerRepresents the state of the DreamMaker compiler. Create action starts a new compile. Delete action cancels the current compile + CTgstation.Server.Host.Models.DreamMakerSettings + CTgstation.Server.Api.Models.ErrorMessageRepresents an error message returned by the server + CTgstation.Server.Host.Components.Interop.EventNotificationFor notifying DD of EventTypes + CTgstation.Server.Host.Configuration.FileLoggingConfigurationFile logging configuration options + CTgstation.Server.Host.Configuration.GeneralConfigurationGeneral configuration options + CTgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions.GoodConfig + CTgstation.Server.Client.IAdministrationClientFor managing server administration + CTgstation.Server.Client.AdministrationClient + CTgstation.Server.Client.IApiClientFactoryFor creating IApiClients + CTgstation.Server.Client.ApiClientFactory + CTgstation.Server.Host.Core.IApplicationConfigures the ASP.NET Core web application + CTgstation.Server.Host.Core.Application + CIApplicationLifetime + CTgstation.Server.Host.Core.Tests.TestApplication.MockApplicationLifetime + CTgstation.Server.Host.Core.IAsyncDelayerFor waiting asynchronously + CTgstation.Server.Host.Core.AsyncDelayer + CTgstation.Server.Host.Security.IAuthenticationContextFactoryFor creating and accessing authentication contexts + CTgstation.Server.Host.Security.AuthenticationContextFactory + CTgstation.Server.Client.Components.IByondClientFor managing the Byond installation + CTgstation.Server.Client.Components.ByondClient + CTgstation.Server.Host.Components.Byond.IByondInstallerFor downloading and installing BYOND extractions for a given system + CTgstation.Server.Host.Components.Byond.PosixByondInstallerIByondInstaller for Posix systems + CTgstation.Server.Host.Components.Byond.WindowsByondInstallerIByondInstaller for windows systems + CTgstation.Server.Client.Components.IChatBotsClientFor managing the chat bots + CTgstation.Server.Client.Components.ChatBotsClient + CTgstation.Server.Host.Components.Chat.IChatFactoryFor creating IChats + CTgstation.Server.Host.Components.Chat.ChatFactory + CTgstation.Server.Host.Security.IClaimsInjectorFor injecting System.Security.Claims.Claims that Controllers.TgsAuthorizeAttribute can look for + CTgstation.Server.Host.Security.ClaimsInjector + CTgstation.Server.Host.Components.Chat.Commands.ICommandRepresents a command that can be invoked by talking to chat bots + CTgstation.Server.Host.Components.Chat.Commands.ByondCommandFor displaying the installed Byond version + CTgstation.Server.Host.Components.Chat.Commands.CustomCommandRepresents a command made from DM code + CTgstation.Server.Host.Components.Chat.Commands.KekCommandkek + CTgstation.Server.Host.Components.Chat.Commands.PullRequestsCommandCommand for reading the active Api.Models.TestMerges + CTgstation.Server.Host.Components.Chat.Commands.RevisionCommandFor displaying Api.Models.Internal.RevisionInformation + CTgstation.Server.Host.Components.Chat.Commands.VersionCommandICommand to return the IApplication.VersionString + CTgstation.Server.Host.Components.Chat.Commands.ICommandFactoryFactory for built in ICommands + CTgstation.Server.Host.Components.Chat.Commands.CommandFactory + CTgstation.Server.Host.Components.Interop.ICommHandlerHandles CommCommands + CTgstation.Server.Host.Components.Watchdog.SessionController + CTgstation.Server.Client.Components.IConfigurationClientFor managing ConfigurationFile files + CTgstation.Server.Client.Components.ConfigurationClient + CTgstation.Server.Host.IO.IConsoleAbstraction for System.Console + CTgstation.Server.Host.IO.Console + CTgstation.Server.Host.Components.Repository.ICredentialsProviderFor generating CredentialsHandlers + CTgstation.Server.Host.Components.Repository.CredentialsProvider + CTgstation.Server.Host.Security.ICryptographySuiteContains various cryptographic functions + CTgstation.Server.Host.Security.CryptographySuite + CTgstation.Server.Host.Components.Chat.ICustomCommandHandlerHandles Commands.ICommands that map to those defined in a IJsonTrackingContext + CTgstation.Server.Host.Components.Watchdog.Watchdog + CTgstation.Server.Host.Models.IDatabaseContextRepresents the database + CTgstation.Server.Host.Models.DatabaseContext< TParentContext > + CTgstation.Server.Host.Core.IDatabaseContextFactoryFactory for scoping usage of IDatabaseContexts. Meant for use by Components + CTgstation.Server.Host.Core.DatabaseContextFactory + CTgstation.Server.Host.Models.IDatabaseSeederFor initially seeding a database + CTgstation.Server.Host.Models.DatabaseSeeder + CTgstation.Server.Host.Core.IDBConnectionFactoryFor creating DbConnection + CTgstation.Server.Host.Core.DBConnectionFactory + CIDesignTimeDbContextFactory + CTgstation.Server.Host.Models.Migrations.MySqlDesignTimeDbContextFactory + CTgstation.Server.Host.Models.Migrations.SqlServerDesignTimeDbContextFactory + CIDisposable + CTgstation.Server.Client.IApiClientWeb interface for the API + CTgstation.Server.Client.ApiClient + CTgstation.Server.Client.IHttpClientFor sending HTTP requests + CTgstation.Server.Client.HttpClient + CTgstation.Server.Client.IServerClientMain client for communicating with a server + CTgstation.Server.Client.ServerClient + CTgstation.Server.Host.Components.Byond.IByondExecutableLockRepresents usage of the two primary BYOND server executables + CTgstation.Server.Host.Components.Byond.ByondExecutableLock + CTgstation.Server.Host.Components.Byond.IByondManagerFor managing the BYOND installation + CTgstation.Server.Host.Components.Byond.ByondManager + CTgstation.Server.Host.Components.Byond.WindowsByondInstallerIByondInstaller for windows systems + CTgstation.Server.Host.Components.Chat.IChatFor managing connected chat services + CTgstation.Server.Host.Components.Chat.Chat + CTgstation.Server.Host.Components.Chat.IJsonTrackingContextRepresents a tracking of dynamic chat json files + CTgstation.Server.Host.Components.Chat.JsonTrackingContext + CTgstation.Server.Host.Components.Chat.Providers.IProviderFor interacting with a chat service + CTgstation.Server.Host.Components.Chat.Providers.Provider + CTgstation.Server.Host.Components.Chat.Providers.DiscordProviderIProvider for the Discord app + CTgstation.Server.Host.Components.Chat.Providers.IrcProviderIProvider for internet relay chat + CTgstation.Server.Host.Components.Compiler.ICompileJobConsumerSink for CompileJobs + CTgstation.Server.Host.Components.Compiler.DmbFactoryStandard IDmbFactory + CTgstation.Server.Host.Components.IDmbProviderProvides absolute paths to the latest compiled .dmbs + CTgstation.Server.Host.Components.Compiler.DmbProvider + CTgstation.Server.Host.Components.Compiler.TemporaryDmbProviderTemporary IDmbProvider + CTgstation.Server.Host.Components.IInstanceFor interacting with the instance services + CTgstation.Server.Host.Components.Instance + CTgstation.Server.Host.Components.InstanceManager + CTgstation.Server.Host.Components.Interop.ICommContextRepresents a registration of an interop session + CTgstation.Server.Host.Components.Interop.CommContext + CTgstation.Server.Host.Components.Repository.IRepositoryRepresents an on-disk git repository + CTgstation.Server.Host.Components.Repository.Repository + CTgstation.Server.Host.Components.Repository.IRepositoryManagerFactory for creating and loading IRepositorys + CTgstation.Server.Host.Components.Repository.RepositoryManager + CTgstation.Server.Host.Components.StaticFiles.IConfigurationFor managing the Configuration directory + CTgstation.Server.Host.Components.StaticFiles.Configuration + CTgstation.Server.Host.Components.Watchdog.IWatchdogRuns and monitors the twin server controllers + CTgstation.Server.Host.Components.Watchdog.Watchdog + CTgstation.Server.Host.Components.Watchdog.WindowsNetworkPromptReaper + CTgstation.Server.Host.Core.IProcessBaseRepresents process lifetime + CTgstation.Server.Host.Components.Watchdog.ISessionControllerHandles communication with a DreamDaemon IProcess + CTgstation.Server.Host.Components.Watchdog.DeadSessionControllerImplements a fake "dead" ISessionController + CTgstation.Server.Host.Components.Watchdog.SessionController + CTgstation.Server.Host.Core.IProcessAbstraction over a System.Diagnostics.Process + CTgstation.Server.Host.Core.Process + CTgstation.Server.Host.Core.IRestartRegistrationRepresents the lifetime of a IRestartHandler registration + CTgstation.Server.Host.Core.RestartRegistration + CTgstation.Server.Host.Core.JobHandlerClass for pairing Tasks with CancellationTokenSources + CTgstation.Server.Host.Core.JobManager + CTgstation.Server.Host.Core.SemaphoreSlimContextAsync lock context helper + CTgstation.Server.Host.Security.AuthenticationContextFactory + CTgstation.Server.Host.Security.IAuthenticationContextRepresents the currently authenticated Api.Models.User + CTgstation.Server.Host.Security.AuthenticationContextManages Api.Models.Users for a scope + CTgstation.Server.Host.Security.IdentityCache + CTgstation.Server.Host.Security.IdentityCacheObjectFor keeping a specific ISystemIdentity alive for a period of time + CTgstation.Server.Host.Security.ISystemIdentityRepresents a user on the current System.Runtime.InteropServices.OSPlatform + CTgstation.Server.Host.Security.WindowsSystemIdentityISystemIdentity for windows systems + CTgstation.Server.Host.Components.Compiler.IDmbFactoryFactory for IDmbProviders + CTgstation.Server.Host.Components.Compiler.DmbFactoryStandard IDmbFactory + CTgstation.Server.Client.Components.IDreamDaemonClientFor managing DreamDaemon + CTgstation.Server.Client.Components.DreamDaemonClient + CTgstation.Server.Host.Components.Compiler.IDreamMakerFor managing the compiler + CTgstation.Server.Host.Components.Compiler.DreamMaker + CTgstation.Server.Client.Components.IDreamMakerClientFor managing the compiler + CTgstation.Server.Client.Components.DreamMakerClient + CTgstation.Server.Host.Components.IEventConsumerConsumes EventTypes and takes the appropriate actions + CTgstation.Server.Host.Components.EventConsumer + CTgstation.Server.Host.Components.StaticFiles.IConfigurationFor managing the Configuration directory + CTgstation.Server.Host.Components.Watchdog.IWatchdogRuns and monitors the twin server controllers + CIEventLog + CTgstation.Server.Host.Service.ServerServiceRepresents a IWatchdog as a ServiceBase + CTgstation.Server.Host.Core.IGitHubClientFactoryFor creating IGitHubClients + CTgstation.Server.Host.Core.GitHubClientFactory + CIHostedService + CTgstation.Server.Host.Components.Byond.IByondManagerFor managing the BYOND installation + CTgstation.Server.Host.Components.Chat.IChatFor managing connected chat services + CTgstation.Server.Host.Components.Compiler.ICompileJobConsumerSink for CompileJobs + CTgstation.Server.Host.Components.IInstanceFor interacting with the instance services + CTgstation.Server.Host.Components.IInstanceFactoryFactory for creating IInstances + CTgstation.Server.Host.Components.InstanceFactory + CTgstation.Server.Host.Components.InstanceManager + CTgstation.Server.Host.Components.StaticFiles.IConfigurationFor managing the Configuration directory + CTgstation.Server.Host.Components.Watchdog.IWatchdogRuns and monitors the twin server controllers + CTgstation.Server.Host.Components.Watchdog.WindowsNetworkPromptReaper + CTgstation.Server.Host.Core.IJobManagerManages the runtime of Jobs + CTgstation.Server.Host.Core.JobManager + CTgstation.Server.Host.Security.IIdentityCacheFor caching ISystemIdentitys + CTgstation.Server.Host.Security.IdentityCache + CTgstation.Server.Client.Components.IInstanceClientFor managing a single Instance + CTgstation.Server.Client.Components.InstanceClient + CTgstation.Server.Host.Components.IInstanceManagerFor managing IInstances + CTgstation.Server.Host.Components.InstanceManager + CTgstation.Server.Client.IInstanceManagerClientFor managing Instances + CTgstation.Server.Client.InstanceManagerClient + CTgstation.Server.Client.Components.IInstanceUserClientFor managing InstanceUsers + CTgstation.Server.Client.Components.InstanceUserClient + CTgstation.Server.Host.IO.IIOManagerInterface for using filesystems + CTgstation.Server.Host.IO.DefaultIOManagerIIOManager that resolves paths to Environment.CurrentDirectory + CTgstation.Server.Host.IO.ResolvingIOManagerAn IIOManager that resolve relative paths from another IIOManager to a subdirectory of that + CTgstation.Server.Client.Components.IJobsClientAccess to running jobs + CTgstation.Server.Client.Components.JobsClient + CTgstation.Server.Host.Components.Watchdog.INetworkPromptReaperOn 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 + CTgstation.Server.Host.Components.Watchdog.PosixNetworkPromptReaper + CTgstation.Server.Host.Components.Watchdog.WindowsNetworkPromptReaper + CTgstation.Server.Api.Models.InstanceMetadata about a server instance + CTgstation.Server.Host.Models.InstanceRepresents an Api.Models.Instance in the database + CTgstation.Server.Tests.InstanceManagerTest + CTgstation.Server.Tests.Instance.InstanceTest + CTgstation.Server.Api.Models.InstanceUserRepresents a Users permissions in an Instance + CTgstation.Server.Host.Models.InstanceUser + CTgstation.Server.Tests.IntegrationTest + CTgstation.Server.Host.Core.IPlatformIdentifierFor identifying the current platform + CTgstation.Server.Host.Core.PlatformIdentifier + CTgstation.Server.Host.IO.IPostWriteHandler + CTgstation.Server.Host.IO.PosixPostWriteHandlerIPostWriteHandler for POSIX systems + CTgstation.Server.Host.IO.WindowsPostWriteHandlerIPostWriteHandler for Windows systems + CTgstation.Server.Host.Core.IProcessExecutorFor launching IProcess' + CTgstation.Server.Host.Core.ProcessExecutor + CTgstation.Server.Host.Components.Chat.IProviderFactoryFactory for IProviders + CTgstation.Server.Host.Components.Chat.ProviderFactory + CTgstation.Server.Host.Components.Watchdog.IReattachInfoHandlerHandles saving and loading WatchdogReattachInformation + CTgstation.Server.Host.Components.ReattachInfoHandler + CTgstation.Server.Client.Components.IRepositoryClientFor managing the Repository + CTgstation.Server.Client.Components.RepositoryClient + CTgstation.Server.Client.IRequestLoggerFor logging HTTP requests and responses + CTgstation.Server.Host.Core.IRestartHandlerHandler for server restarts + CTgstation.Server.Host.Components.Chat.Chat + CTgstation.Server.Host.Components.InstanceManager + CTgstation.Server.Host.Components.Watchdog.Watchdog + CTgstation.Server.Host.IServerRepresents the host + CTgstation.Server.Host.Server + CTgstation.Server.Tests.TestingServer + CTgstation.Server.Client.IServerClientFactoryFactory for creating IServerClients + CTgstation.Server.Client.ServerClientFactory + CTgstation.Server.Host.Core.IServerControlRepresents a service that may take an updated Host assembly and run it, stopping the current assembly in the process + CTgstation.Server.Host.Server + CTgstation.Server.Host.IServerFactoryFor creating IServers + CTgstation.Server.Host.ServerFactory + CTgstation.Server.Host.Components.Watchdog.ISessionControllerFactoryFactory for ISessionControllers + CTgstation.Server.Host.Components.Watchdog.SessionControllerFactory + CTgstation.Server.Host.Core.ISetupWizardThe command line Configuration setup wizard + CTgstation.Server.Host.Core.SetupWizard + CTgstation.Server.Host.Core.Tests.TestApplication.MockSetupWizard + CTgstation.Server.Host.IO.ISymlinkFactoryFor creating filesystem symbolic links + CTgstation.Server.Host.IO.PosixSymlinkFactoryISymlinkFactory for posix systems + CTgstation.Server.Host.IO.WindowsSymlinkFactoryISymlinkFactory for windows systems + CTgstation.Server.Host.IO.ISynchronousIOManagerFor accessing the disk in a synchronous manner + CTgstation.Server.Host.IO.SynchronousIOManager + CTgstation.Server.Host.Security.ISystemIdentityFactoryFactory for ISystemIdentitys + CTgstation.Server.Host.Security.PosixSystemIdentityFactoryISystemIdentityFactory for posix systems + CTgstation.Server.Host.Security.WindowsSystemIdentityFactoryISystemIdentityFactory for windows systems. Uses long running tasks due to potential networked domains + CTgstation.Server.Host.Security.ITokenFactoryFor creating Tokens + CTgstation.Server.Host.Security.TokenFactory + CTgstation.Server.Client.IUsersClientFor managing Users + CTgstation.Server.Client.UsersClient + CTgstation.Server.Host.Watchdog.IWatchdogThe watchdog for a Host + CTgstation.Server.Host.Watchdog.Watchdog + CTgstation.Server.Host.Watchdog.IWatchdogFactoryFactory for creating IWatchdogs + CTgstation.Server.Host.Watchdog.WatchdogFactory + CTgstation.Server.Host.Components.Watchdog.IWatchdogFactoryFor creating IWatchdogs + CTgstation.Server.Host.Components.Watchdog.WatchdogFactory + CTgstation.Server.Api.Models.Internal.JobRepresents a long running job + CTgstation.Server.Api.Models.JobRepresents a long running job on the server. Model is read-only, updates attempt to cancel the job + CTgstation.Server.Host.Models.Job + CJsonConverter + CTgstation.Server.Host.Components.Chat.Commands.BoolConverterJsonConverter for decoding bools returned by BYOND + CTgstation.Server.Host.Components.Interop.JsonFileRepresentation of the initial json passed to DreamDaemon + CTgstation.Server.Host.Components.Interop.JsonSubFileListInformation used in for reattaching and interop + CTgstation.Server.Host.Models.ReattachInformationBaseBase class for ReattachInformation + CTgstation.Server.Host.Components.Watchdog.ReattachInformationParameters necessary for duplicating a ISessionController session + CTgstation.Server.Host.Models.ReattachInformationDatabase representation of Components.Watchdog.ReattachInformation + CTgstation.Server.Host.Components.Watchdog.LaunchResultRepresents the result of trying to start a DD process + CTgstation.Server.Host.Components.Chat.Providers.MessageRepresents a message recieved by a IProvider + CMigration + CTgstation.Server.Host.Models.Migrations.MSAddMinimumSecurityAdd the Api.Models.Internal.CompileJob.MinimumSecurityLevel and DreamMaker.ApiValidationSecurityLevel columns for MSSQL + CTgstation.Server.Host.Models.Migrations.MSInitialCreateThe initial database migration for MSSQL + CTgstation.Server.Host.Models.Migrations.MSNullableAndForeignKeyCleanupCleans up of nullable columns and foreign keys MSSQL + CTgstation.Server.Host.Models.Migrations.MSReattachCompileJobRequiredMarks the CompileJobs of ReattachInformations as non-nullable for MSSQL + CTgstation.Server.Host.Models.Migrations.MYAddMinimumSecurityAdd the Api.Models.Internal.CompileJob.MinimumSecurityLevel and DreamMaker.ApiValidationSecurityLevel columns for MySQL/MariaDB + CTgstation.Server.Host.Models.Migrations.MYInitialCreateThe initial database migration for MySQL/MariaDB + CTgstation.Server.Host.Models.Migrations.MYNullableAndForeignKeyCleanupCleans up of nullable columns and foreign keys MySQL/MariaDB + CTgstation.Server.Host.Models.Migrations.MYReattachCompileJobRequiredMarks the CompileJobs of ReattachInformations as non-nullable for MySQL/MariaDB + CTgstation.Server.Host.Controllers.ModelController< Administration > + CTgstation.Server.Host.Controllers.AdministrationControllerModelController<TModel> for Administration + CTgstation.Server.Host.Controllers.ModelController< Api.Models.Byond > + CTgstation.Server.Host.Controllers.ByondControllerController for managing Api.Models.Byond.Versions + CTgstation.Server.Host.Controllers.ModelController< Api.Models.ChatBot > + CTgstation.Server.Host.Controllers.ChatControllerModelController<TModel> for managing Api.Models.ChatBots + CTgstation.Server.Host.Controllers.ModelController< Api.Models.DreamMaker > + CTgstation.Server.Host.Controllers.DreamMakerControllerController for managing the compiler + CTgstation.Server.Host.Controllers.ModelController< Api.Models.Instance > + CTgstation.Server.Host.Controllers.InstanceControllerController for managing Components.Instances + CTgstation.Server.Host.Controllers.ModelController< Api.Models.InstanceUser > + CTgstation.Server.Host.Controllers.InstanceUserControllerFor managing Users + CTgstation.Server.Host.Controllers.ModelController< Api.Models.Job > + CTgstation.Server.Host.Controllers.JobControllerModelController<TModel> for Api.Models.Jobs + CTgstation.Server.Host.Controllers.ModelController< ConfigurationFile > + CTgstation.Server.Host.Controllers.ConfigurationControllerThe ModelController<TModel> for ConfigurationFiles + CTgstation.Server.Host.Controllers.ModelController< DreamDaemon > + CTgstation.Server.Host.Controllers.DreamDaemonControllerModelController<TModel> for managing the DreamDaemon + CTgstation.Server.Host.Controllers.ModelController< Repository > + CTgstation.Server.Host.Controllers.RepositoryControllerController for managing the Repositorys + CTgstation.Server.Host.Controllers.ModelController< UserUpdate > + CTgstation.Server.Host.Controllers.UserControllerFor managing Users + CModelSnapshot + CTgstation.Server.Host.Models.Migrations.MySqlDatabaseContextModelSnapshot + CTgstation.Server.Host.Models.Migrations.SqlServerDatabaseContextModelSnapshot + CTgstation.Server.Host.Components.Watchdog.MonitorStateThe (absolute) state of the Watchdog + CTgstation.Server.Host.NativeMethodsNative methods used by the code + CReleaseNotes.ProgramContains the application entrypoint + CTgstation.Server.Host.ProgramEntrypoint for the Process + CTgstation.Server.Host.Service.ProgramContains the entrypoint for the application + CTgstation.Server.Host.Console.ProgramContains the entrypoint for the application + CTgstation.Server.Api.Models.Internal.RepositorySettingsRepresents configurable settings for a Repository + CTgstation.Server.Api.Models.RepositoryRepresents a git repository + CTgstation.Server.Host.Models.RepositorySettings + CTgstation.Server.Host.Components.Chat.ResponseRepresents a chat message requested by DD + CTgstation.Server.Host.Models.RevInfoTestMergeMany to many relationship for Models.RevisionInformation and Models.TestMerge + CTgstation.Server.Api.Models.Internal.RevisionInformationRepresents information about a current git revison + CTgstation.Server.Api.Models.RevisionInformation + CTgstation.Server.Host.Models.RevisionInformation + CTgstation.Server.Api.Rights.RightsHelperHelper for RightsTypes + CTgstation.Server.Api.RoutesRoutes to a server actions + CTgstation.Server.Api.Models.ServerInformationRepresents basic server information + CTgstation.Server.Host.Components.StaticFiles.ServerSideModificationsRepresents code modifications via configuration + CServiceBase + CTgstation.Server.Host.Service.ServerServiceRepresents a IWatchdog as a ServiceBase + CTgstation.Server.Host.Core.ServiceCollectionExtensionsExtensions for IServiceCollection + CTgstation.Server.Client.Tests.TestApiClient + CTgstation.Server.Api.Tests.TestApiHeadersTests for ApiHeaders + CTgstation.Server.Host.Core.Tests.TestApplicationTests for Application + CTgstation.Server.Host.Core.Tests.TestAsyncDelayer + CTgstation.Server.Host.Security.Tests.TestAuthenticationContextTests for AuthenticationContext + CTgstation.Server.Host.IO.Tests.TestConsole + CTgstation.Server.Host.Core.Tests.TestDatabaseContextFactory + CTgstation.Server.Host.Core.Tests.TestDBConnectionFactory + CTgstation.Server.Host.Components.Chat.Providers.Tests.TestDiscordProvider + CTgstation.Server.Client.Components.Tests.TestDreamDaemonClient + CTgstation.Server.Host.Core.Tests.TestGitHubClientFactory + CTgstation.Server.Api.Models.Tests.TestIrcConnectionStringBuilder + CTgstation.Server.Host.Core.Tests.TestJobException + CTgstation.Server.Host.Core.Tests.TestJobHandler + CTgstation.Server.Api.Models.TestMergeParametersParameters for creating a TestMerge + CTgstation.Server.Api.Models.Internal.TestMergeBaseLayer of test merge data required internally + CTgstation.Server.Api.Models.Internal.TestMergeRepresents a merge of a GitHub pull request + CTgstation.Server.Api.Models.TestMerge + CTgstation.Server.Host.Models.TestMerge + CTgstation.Server.Host.Components.Interop.TestMergeThis model mirrors /datum/tgs_revision_information/test_merge + CTgstation.Server.Host.Core.Tests.TestPlatformIdentifier + CTgstation.Server.Host.Components.Byond.Tests.TestPosixByondInstaller + CTgstation.Server.Host.IO.Tests.TestPostWriteHandler + CTgstation.Server.Host.Console.Tests.TestProgram + CTgstation.Server.Api.Rights.Tests.TestRightsTests for Rights + CTgstation.Server.Client.Tests.TestServerClientFactory + CTgstation.Server.Host.Tests.TestServerFactoryTests for ServerFactory + CTgstation.Server.Host.Service.Tests.TestServerServiceTests for ServerService + CTgstation.Server.Host.Core.Tests.TestServiceCollectionExtensions + CTgstation.Server.Host.Core.Tests.TestSetupWizard + CTgstation.Server.Host.IO.Tests.TestSymlinkFactory + CTgstation.Server.Host.Components.Watchdog.Tests.TestWatchdog + CTgstation.Server.Host.Watchdog.Tests.TestWatchdog + CTgstation.Server.Host.Watchdog.Tests.TestWatchdogFactoryTests for WatchdogFactory + CTgstation.Server.Api.Models.TokenRepresents a JWT returned by the API + CTgstation.Server.Host.Configuration.UpdatesConfigurationConfiguration for the automatic update system + CTgstation.Server.Host.Components.Chat.UserRepresents a tgs_chat_user datum + CTgstation.Server.Api.Models.Internal.UserRepresents a server User + CTgstation.Server.Api.Models.User + CTgstation.Server.Api.Models.UserUpdateFor editing a given User. Will never be returned by the API + CTgstation.Server.Host.Models.User + CTgstation.Server.Host.Models.WatchdogReattachInformationBaseBase class for WatchdogReattachInformation + CTgstation.Server.Host.Components.Watchdog.WatchdogReattachInformationReattach information for a IWatchdog + CTgstation.Server.Host.Models.WatchdogReattachInformationDatabase representation of Components.Watchdog.WatchdogReattachInformation + CException + CTgstation.Server.Client.ClientExceptionExceptions thrown by IServerClients + CTgstation.Server.Client.ApiConflictExceptionOccurs when the server returns an unknown response + CTgstation.Server.Client.ApiMismatchExceptionOccurs when the API version of the client is not compatible with the server's + CTgstation.Server.Client.ConflictExceptionOccurs when the client performs an action that would result in data conflict + CTgstation.Server.Client.InsufficientPermissionsExceptionOccurs when the client attempts to perform an action they do not have the rights for + CTgstation.Server.Client.MethodNotSupportedExceptionOccurs when the client tries to use a currently unsupported API + CTgstation.Server.Client.RateLimitExceptionOccurs when a GitHub rate limit occurs + CTgstation.Server.Client.RequestTimeoutExceptionOccurs when the client provides invalid credentials + CTgstation.Server.Client.ServerErrorExceptionOccurs when an error occurs in the server + CTgstation.Server.Client.ServiceUnavailableExceptionOccurs when the client makes a request while the server is starting or stopping + CTgstation.Server.Client.UnauthorizedExceptionOccurs when the client provides invalid credentials + CTgstation.Server.Client.UnrecognizedResponseExceptionOccurs when a response is received that did not deserialize to one of the expected Api.Models + CTgstation.Server.Host.Core.JobExceptionOperation exceptions thrown from the context of a Models.Job diff --git a/index.html b/index.html index cbf356af2f..d57eaf7cbd 100644 --- a/index.html +++ b/index.html @@ -129,6 +129,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • Database:DatabaseType: Can be one of SqlServer, MariaDB, or MySql
  • Database:MySqlServerVersion: The version of MySql/MariaDB the database resides on, can be left as null for attempted auto detection. Used by the MySQL/MariaDB provider for selection of certain features ignore at your own risk. A string in the form <major>.<minor>.<patch>
  • Database:ConnectionString: Connection string for your database. Click here for an SQL Server generator or see here for a MySQL guide (You should probably use '127.0.0.1' instead of 'localhost').
  • +
  • ControlPanel:Enable: Enable the javascript based control panel to be served from the server via /index.html
  • +
  • ControlPanel:AllowAnyOrigin: Set the Access-Control-Allow-Origin header to * for all responses (also enables all headers and methods)
  • +
  • ControlPanel:AllowedOrigins: Set the Access-Control-Allow-Origin headers to this list of origins for all responses (also enables all headers and methods). This is overridden by ControlPanel:AllowAnyOrigin
  • Database Configuration

    If using a MariaDB/MySQL server, our client library recommends you set 'utf8mb4' as your default charset disregard at your own risk.

    diff --git a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_application.html b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_application.html index 09f7decd58..a93d9f7ca4 100644 --- a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_application.html +++ b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_application.html @@ -114,7 +114,7 @@ Inheritance diagram for Tgstation.Server.Host.Core.IApplication: - +

    Public Member Functions

    void Ready (Exception initializationError)
    void Ready (Exception initializationError)
     Mark the IApplication as ready to run More...
     
    @@ -142,7 +142,7 @@ Properties - + @@ -152,7 +152,7 @@ Properties

    Mark the IApplication as ready to run

    Parameters
    void Tgstation.Server.Host.Core.IApplication.Ready (Exception Exception  initializationError)
    - +
    initializationErrorThe Exception that put the application in a corrupted state if any
    initializationErrorThe Exception that put the application in a corrupted state if any
    diff --git a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_server_control-members.html b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_server_control-members.html index 1f8861826c..a65db40834 100644 --- a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_server_control-members.html +++ b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_server_control-members.html @@ -99,9 +99,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

    This is the complete list of members for Tgstation.Server.Host.Core.IServerControl, including all inherited members.

    - - - + + + +
    ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)Tgstation.Server.Host.Core.IServerControl
    RegisterForRestart(IRestartHandler handler)Tgstation.Server.Host.Core.IServerControl
    Restart()Tgstation.Server.Host.Core.IServerControl
    WatchdogPresentTgstation.Server.Host.Core.IServerControl
    Die(Exception exception)Tgstation.Server.Host.Core.IServerControl
    RegisterForRestart(IRestartHandler handler)Tgstation.Server.Host.Core.IServerControl
    Restart()Tgstation.Server.Host.Core.IServerControl
    WatchdogPresentTgstation.Server.Host.Core.IServerControl