From 30245dc01c74129315680c4229158054e67f80e4 Mon Sep 17 00:00:00 2001 From: tgstation-server Date: Tue, 7 Aug 2018 19:40:02 -0700 Subject: [PATCH] Deploy code docs to GitHub Pages for Appveyor build 845 Commit: aca74526f7e3b78b83146c067b1ec00338138444 --- _api_controller_8cs_source.html | 4 +- _app_8g_8cs.html | 4 +- _app_8g_8cs_source.html | 4 +- _application_8cs_source.html | 14 +- _byond_manager_8cs_source.html | 2 +- _chat_8cs_source.html | 2 +- _chat_controller_8cs_source.html | 6 +- _command_factory_8cs_source.html | 2 +- _compiler_status_8cs.html | 9 +- _compiler_status_8cs_source.html | 4 +- ...chdog_2_i_watchdog_factory_8cs_source.html | 2 +- ...dog_2_reattach_information_8cs_source.html | 9 +- ...ents_2_watchdog_2_watchdog_8cs_source.html | 42 +-- ...atchdog_2_watchdog_factory_8cs_source.html | 2 +- ...chdog_reattach_information_8cs_source.html | 12 +- _configuration_8cs_source.html | 28 +- _database_configuration_8cs_source.html | 2 +- _database_context_8cs_source.html | 15 +- _default_i_o_manager_8cs_source.html | 20 +- _dmb_factory_8cs_source.html | 37 +-- _dmb_provider_8cs_source.html | 3 +- _dream_daemon_controller_8cs_source.html | 10 +- _dream_daemon_security_8cs_source.html | 2 +- _dream_maker_controller_8cs_source.html | 15 +- _event_consumer_8cs_source.html | 2 +- _event_type_8cs_source.html | 6 +- _executor_8cs_source.html | 8 +- _home_controller_8cs_source.html | 6 +- _i_compile_job_consumer_8cs_source.html | 2 +- _i_dmb_factory_8cs_source.html | 3 +- _i_dream_maker_8cs_source.html | 6 +- _i_instance_8cs_source.html | 7 +- _i_instance_factory_8cs_source.html | 2 +- _i_reattach_info_handler_8cs_source.html | 2 +- _instance_controller_8cs_source.html | 10 +- _instance_factory_8cs_source.html | 8 +- _instance_manager_8cs_source.html | 42 +-- _instance_user_controller_8cs_source.html | 6 +- _interop_constants_8cs_source.html | 5 +- _interop_controller_8cs_source.html | 15 +- _job_controller_8cs_source.html | 13 +- _job_manager_8cs_source.html | 24 +- _json_tracking_context_8cs_source.html | 20 +- _launch_result_8cs_source.html | 2 +- _main_window_8g_8cs.html | 4 +- _main_window_8g_8cs_source.html | 4 +- _my_sql_database_context_8cs.html | 2 +- _my_sql_database_context_8cs_source.html | 8 +- ...gn_time_db_context_factory_8cs_source.html | 6 +- _pull_requests_command_8cs_source.html | 2 +- _reattach_info_handler_8cs_source.html | 12 +- _reattach_information_base_8cs_source.html | 2 +- _repository_controller_8cs_source.html | 6 +- _script_executor_8cs_source.html | 2 +- _session_8cs_source.html | 4 +- _session_controller_8cs_source.html | 46 +-- _session_controller_factory_8cs_source.html | 36 ++- _sql_server_database_context_8cs.html | 2 +- _sql_server_database_context_8cs_source.html | 8 +- ...gn_time_db_context_factory_8cs_source.html | 6 +- _sqlite_database_context_8cs.html | 2 +- _sqlite_database_context_8cs_source.html | 7 +- _temporary_dmb_provider_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 +- ...er_8_command_line_8_assembly_info_8cs.html | 4 +- ...mmand_line_8_assembly_info_8cs_source.html | 6 +- ...mand_line_8_tests_8_assembly_info_8cs.html | 4 +- ...ne_8_tests_8_assembly_info_8cs_source.html | 6 +- ..._8_command_line_8_tests_8_program_8cs.html | 4 +- ...and_line_8_tests_8_program_8cs_source.html | 4 +- ...s_2_compiler_2_dream_maker_8cs_source.html | 13 +- ...st_2_components_2_instance_8cs_source.html | 40 +-- ...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 +- ...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 +- _user_controller_8cs_source.html | 4 +- _windows_byond_installer_8cs_source.html | 2 +- annotated.html | 6 +- api.html | 41 ++- class_auto_generated_program.html | 12 +- class_controller.html | 6 +- class_controller.png | Bin 2569 -> 2457 bytes ...erver_1_1_api_1_1_models_1_1_instance.html | 2 +- ...1_models_1_1_internal_1_1_compile_job.html | 6 +- ...al_1_1_dream_daemon_launch_parameters.html | 10 +- ...1_api_1_1_models_1_1_internal_1_1_job.html | 12 +- ..._1_1_internal_1_1_repository_settings.html | 12 +- ..._api_1_1_models_1_1_internal_1_1_user.html | 2 +- ..._1_1_server_1_1_control_panel_1_1_app.html | 2 +- ...ver_1_1_control_panel_1_1_main_window.html | 2 +- ...omponents_1_1_byond_1_1_byond_manager.html | 6 +- ...1_1_byond_1_1_windows_byond_installer.html | 2 +- ...host_1_1_components_1_1_chat_1_1_chat.html | 6 +- ...ts_1_1_chat_1_1_json_tracking_context.html | 42 +-- ..._1_1_compiler_1_1_dmb_factory-members.html | 33 +-- ...mponents_1_1_compiler_1_1_dmb_factory.html | 272 +++++++++++------- ...1_1_compiler_1_1_dmb_provider-members.html | 1 - ...ponents_1_1_compiler_1_1_dmb_provider.html | 38 +-- ..._1_1_compiler_1_1_dream_maker-members.html | 4 +- ...mponents_1_1_compiler_1_1_dream_maker.html | 71 +++-- ...er_1_1_temporary_dmb_provider-members.html | 2 +- ...1_compiler_1_1_temporary_dmb_provider.html | 70 +++-- ...t_1_1_components_1_1_instance-members.html | 4 +- ..._1_1_host_1_1_components_1_1_instance.html | 147 ++++++---- ...t_1_1_components_1_1_instance_factory.html | 4 +- ...mponents_1_1_instance_manager-members.html | 37 ++- ...t_1_1_components_1_1_instance_manager.html | 104 +++---- ..._components_1_1_reattach_info_handler.html | 7 +- ...1_1_repository_1_1_repository_manager.html | 4 +- ...ts_1_1_static_files_1_1_configuration.html | 81 +++--- ..._1_1_static_files_1_1_script_executor.html | 2 +- ...nts_1_1_watchdog_1_1_executor-members.html | 2 + ..._components_1_1_watchdog_1_1_executor.html | 76 ++++- ...ts_1_1_watchdog_1_1_interop_constants.html | 72 ++--- ..._1_watchdog_1_1_launch_result-members.html | 5 +- ...onents_1_1_watchdog_1_1_launch_result.html | 29 +- ...onents_1_1_watchdog_1_1_monitor_state.html | 12 +- ...hdog_1_1_reattach_information-members.html | 2 +- ...1_1_watchdog_1_1_reattach_information.html | 23 +- ...1_components_1_1_watchdog_1_1_session.html | 6 +- ...tchdog_1_1_session_controller-members.html | 37 +-- ...s_1_1_watchdog_1_1_session_controller.html | 153 ++++++++-- ...tchdog_1_1_session_controller_factory.html | 73 +++-- ...omponents_1_1_watchdog_1_1_test_merge.html | 2 +- ..._components_1_1_watchdog_1_1_watchdog.html | 97 ++++--- ...1_watchdog_1_1_watchdog_launch_result.html | 4 +- ...watchdog_reattach_information-members.html | 2 +- ...dog_1_1_watchdog_reattach_information.html | 41 +-- ...on_1_1_database_configuration-members.html | 5 +- ...figuration_1_1_database_configuration.html | 27 ++ ...st_1_1_controllers_1_1_api_controller.html | 9 +- ...ost_1_1_controllers_1_1_api_controller.png | Bin 2561 -> 2111 bytes ...t_1_1_controllers_1_1_chat_controller.html | 32 +-- ...trollers_1_1_configuration_controller.html | 6 +- ...ntrollers_1_1_dream_daemon_controller.html | 16 +- ...ontrollers_1_1_dream_maker_controller.html | 26 +- ...t_1_1_controllers_1_1_home_controller.html | 9 +- ...1_controllers_1_1_instance_controller.html | 30 +- ...trollers_1_1_instance_user_controller.html | 30 +- ...ollers_1_1_interop_controller-members.html | 13 +- ..._1_controllers_1_1_interop_controller.html | 97 +++---- ...1_1_controllers_1_1_interop_controller.png | Bin 1399 -> 865 bytes ...ontrollers_1_1_job_controller-members.html | 2 +- ...st_1_1_controllers_1_1_job_controller.html | 68 +++-- ..._1_1_controllers_1_1_model_controller.html | 2 +- ...controllers_1_1_repository_controller.html | 26 +- ...t_1_1_controllers_1_1_user_controller.html | 24 +- ...ver_1_1_host_1_1_core_1_1_application.html | 12 +- ...ver_1_1_host_1_1_core_1_1_job_handler.html | 2 +- ...host_1_1_core_1_1_job_manager-members.html | 13 +- ...ver_1_1_host_1_1_core_1_1_job_manager.html | 102 +++++-- ...t_1_1_core_1_1_semaphore_slim_context.html | 2 +- ...1_i_o_1_1_default_i_o_manager-members.html | 2 +- ..._host_1_1_i_o_1_1_default_i_o_manager.html | 60 ++-- ...ost_1_1_i_o_1_1_resolving_i_o_manager.html | 4 +- ...r_1_1_host_1_1_models_1_1_compile_job.html | 2 +- ...1_models_1_1_database_context-members.html | 4 +- ..._host_1_1_models_1_1_database_context.html | 172 +++++------ ..._1_1_models_1_1_dream_daemon_settings.html | 2 - ..._1_server_1_1_host_1_1_models_1_1_job.html | 4 +- ...my_sql_design_time_db_context_factory.html | 2 +- ...server_design_time_db_context_factory.html | 2 +- ...s_1_1_my_sql_database_context-members.html | 25 +- ..._1_models_1_1_my_sql_database_context.html | 64 +++-- ..._models_1_1_reattach_information_base.html | 10 +- ...1_sql_server_database_context-members.html | 25 +- ...odels_1_1_sql_server_database_context.html | 64 +++-- ...s_1_1_sqlite_database_context-members.html | 4 +- ..._1_models_1_1_sqlite_database_context.html | 84 ++---- ...1_security_1_1_authentication_context.html | 4 +- ... dir_019c6d3af924151b681494cc466be1ac.html | 8 +- ... dir_0b6a380fe7fc1e1d640d1ac854fc3fb6.html | 8 +- dir_103b33cf1ab7043de6f87dfa61afa23f.html | 2 +- dir_1443dae955b59059fffc2ce8e4c78697.html | 2 +- dir_177c182c6d1f29bec543c3fdad3072d5.html | 2 +- ... dir_28befe6570ea1b64761728e289f5b293.html | 8 +- ... dir_2b9d136d9ffdab429a63a74fca28b300.html | 8 +- ... dir_2e67631cb23401d696bcfdbe81f86f0f.html | 8 +- dir_37d3dde17308ffc593336c61a3bc03d9.html | 2 +- ... dir_41d8b89bcec5066b22bb40db01221fa5.html | 4 +- dir_446bdfc1cd19b69c11765b5665ebfd45.html | 2 +- ... dir_48968f849f7ddb061f7130e079fe66a6.html | 8 +- ... dir_5af7de1413352a4fe88a28f48c07cbc2.html | 4 +- dir_685b5aa2d8a661ab5bc3f68b331887b0.html | 2 +- dir_689f0584ee97ae9ca308532531d9c5d2.html | 2 +- dir_74017a19412e2f90cc6ad11fcac82495.html | 2 +- dir_87a4f2035ed18e30422f4b60aebc4381.html | 2 +- ... dir_9001df4c867c54e409a37c0d680ebe27.html | 4 +- ... dir_928684424f22ad70cdc70cb4cd42d210.html | 8 +- ... dir_9aa71984c95727502875aeec10822088.html | 4 +- ... dir_9aedc62a69ee0ed357488302c2a7ddc7.html | 4 +- ... dir_a3714ab037275c284988fc90b4d045a5.html | 4 +- dir_b524b00ec2259c3c83b44909fd47f6e3.html | 2 +- ... dir_b5743443209c10c7366c6da2e3df804f.html | 6 +- ... dir_b92573f47b933d7a3661c26d1af2098b.html | 8 +- ... dir_be9f3ed3485a880e3213506a311823d1.html | 8 +- ... dir_c3ccbc778e7dc2ad3f7649c567722ab9.html | 4 +- ... dir_d8a279f7113e0073f6cd65dd050c1475.html | 4 +- ... dir_daeb063f9bb170acf64b90bbd817fcde.html | 4 +- dir_e0c1bccb03249d93bc42ac0968ce43fd.html | 2 +- dir_e4340cf3eb86a9b6135c5aa3fb3cf0ea.html | 2 +- ... dir_e97d380c3eebaf0856a4ec180d749620.html | 8 +- ... dir_ea36b5ab01ad6e8b607438a02ae91964.html | 8 +- ... dir_eca6d9a7be6772490ed1cc9215fd08cf.html | 4 +- ... dir_f88948f9bde0555374e8a59a663bdfc9.html | 4 +- ... dir_fa19fb0671b7c8d7e52fd04aee5fcddb.html | 4 +- ... dir_fab1362e86740a2c573ef875dd474a67.html | 8 +- dir_fb12273b841438c2f2c9b5bc5398c636.html | 2 +- files.html | 50 ++-- functions_0x7e.html | 6 +- functions_a.html | 3 - functions_c.html | 19 +- functions_d.html | 4 +- functions_e.html | 7 +- functions_f.html | 4 +- functions_func_0x7e.html | 6 +- functions_func_c.html | 18 +- functions_func_d.html | 4 +- functions_func_e.html | 3 + functions_func_f.html | 4 +- functions_func_i.html | 2 +- functions_func_j.html | 2 +- functions_func_l.html | 2 +- functions_func_m.html | 2 +- functions_func_o.html | 3 +- functions_func_r.html | 1 + functions_func_s.html | 4 +- functions_func_t.html | 2 +- functions_func_v.html | 2 +- functions_i.html | 2 +- functions_j.html | 2 +- functions_l.html | 16 +- functions_m.html | 2 +- functions_n.html | 5 +- functions_o.html | 3 +- functions_p.html | 3 - functions_prop_c.html | 5 + functions_prop_l.html | 1 + functions_prop_n.html | 3 + functions_prop_p.html | 3 - functions_r.html | 8 +- functions_s.html | 4 +- functions_t.html | 2 +- functions_v.html | 2 +- functions_vars_a.html | 3 - functions_vars_c.html | 4 - functions_vars_l.html | 5 +- functions_vars_n.html | 2 +- functions_vars_r.html | 3 + hierarchy.html | 10 +- ...1_1_byond_1_1_i_byond_executable_lock.html | 2 +- ...1_compiler_1_1_i_compile_job_consumer.html | 2 +- ..._1_compiler_1_1_i_dmb_factory-members.html | 7 +- ...onents_1_1_compiler_1_1_i_dmb_factory.html | 70 ++++- ..._1_compiler_1_1_i_dream_maker-members.html | 2 +- ...onents_1_1_compiler_1_1_i_dream_maker.html | 19 +- ...ost_1_1_components_1_1_i_dmb_provider.html | 8 +- ...1_1_components_1_1_i_instance-members.html | 15 +- ..._1_host_1_1_components_1_1_i_instance.html | 29 +- ...1_1_components_1_1_i_interop_consumer.html | 2 +- ..._1_components_1_1_i_interop_registrar.html | 2 +- ...nents_1_1_repository_1_1_i_repository.html | 4 +- ...nents_1_1_watchdog_1_1_i_session_base.html | 4 +- ...1_1_watchdog_1_1_i_session_controller.html | 18 +- ...ost_1_1_models_1_1_i_database_context.html | 4 +- ...st_1_1_security_1_1_i_system_identity.html | 2 +- ...station_1_1_server_1_1_api_1_1_models.html | 23 +- ...on_1_1_server_1_1_host_1_1_components.html | 10 +- ...tation_1_1_server_1_1_host_1_1_models.html | 6 +- search/all_1.js | 5 +- search/all_10.js | 7 +- search/all_12.js | 11 +- search/all_13.js | 4 +- search/all_14.js | 2 +- search/all_16.js | 2 +- search/all_17.js | 4 +- search/all_19.js | 4 +- search/all_3.js | 7 +- search/all_4.js | 6 +- search/all_5.js | 2 +- search/all_6.js | 2 +- search/all_9.js | 2 +- search/all_a.js | 2 +- search/all_c.js | 4 +- search/all_d.js | 2 +- search/all_e.js | 5 +- search/all_f.js | 2 +- search/enumvalues_10.js | 8 +- search/enumvalues_11.html | 26 ++ search/enumvalues_11.js | 10 + search/enumvalues_b.js | 19 +- search/enumvalues_c.js | 40 ++- search/enumvalues_d.js | 24 +- search/enumvalues_e.js | 3 +- search/enumvalues_f.js | 3 +- search/functions_10.js | 4 +- search/functions_11.js | 4 +- search/functions_12.js | 2 +- search/functions_14.js | 2 +- search/functions_15.js | 2 +- search/functions_17.js | 4 +- search/functions_2.js | 5 +- search/functions_3.js | 6 +- search/functions_4.js | 3 +- search/functions_5.js | 2 +- search/functions_8.js | 2 +- search/functions_9.js | 2 +- search/functions_b.js | 2 +- search/functions_c.js | 2 +- search/functions_e.js | 2 +- search/properties_2.js | 3 +- search/properties_a.js | 2 +- search/properties_c.js | 1 + search/properties_e.js | 1 - search/searchdata.js | 2 +- search/variables_1.js | 1 - search/variables_11.js | 1 + search/variables_3.js | 3 +- search/variables_b.js | 2 +- search/variables_d.js | 2 +- 346 files changed, 2497 insertions(+), 2016 deletions(-) rename dir_91e2bbee1291fcb0db7e0417b617d01e.html => dir_019c6d3af924151b681494cc466be1ac.html (93%) rename dir_53c056aa6a96f034cca75f793964be6e.html => dir_0b6a380fe7fc1e1d640d1ac854fc3fb6.html (92%) rename dir_009be6f9fdcd196c363d07190dd441da.html => dir_28befe6570ea1b64761728e289f5b293.html (93%) rename dir_90400719279795a0c5ea912516a4c874.html => dir_2b9d136d9ffdab429a63a74fca28b300.html (92%) rename dir_7ae0efc0613c36c848fa310214e0aee3.html => dir_2e67631cb23401d696bcfdbe81f86f0f.html (92%) rename dir_2266e7807ddb7cfdb837ecff50d18412.html => dir_41d8b89bcec5066b22bb40db01221fa5.html (95%) rename dir_4122b363a9d577bed19bef337e757ff3.html => dir_48968f849f7ddb061f7130e079fe66a6.html (92%) rename dir_5069f282fe9d0323372a5085da5fee49.html => dir_5af7de1413352a4fe88a28f48c07cbc2.html (95%) rename dir_78d37ae488054b1d483e4fd9348761bd.html => dir_9001df4c867c54e409a37c0d680ebe27.html (94%) rename dir_03cafb38a7caa4c529b3f77bf54ecc7c.html => dir_928684424f22ad70cdc70cb4cd42d210.html (92%) rename dir_677fea1e54d82b3586895a7b21db15e6.html => dir_9aa71984c95727502875aeec10822088.html (93%) rename dir_6deca23b0c5a69ee313e728b24a2ba8c.html => dir_9aedc62a69ee0ed357488302c2a7ddc7.html (93%) rename dir_13bee0ed20ecc83b9bcd4b869b726d42.html => dir_a3714ab037275c284988fc90b4d045a5.html (95%) rename dir_544d119314081d4fa660891e3cb1b2b0.html => dir_b5743443209c10c7366c6da2e3df804f.html (95%) rename dir_40138fd4849573e743490d583f6d06d6.html => dir_b92573f47b933d7a3661c26d1af2098b.html (93%) rename dir_18a5e30c14613227f95d12bd9fddc614.html => dir_be9f3ed3485a880e3213506a311823d1.html (92%) rename dir_85962924731471f7a9836641cd31e82a.html => dir_c3ccbc778e7dc2ad3f7649c567722ab9.html (95%) rename dir_83a6f600c846cd444f33e44bb9b4f9cb.html => dir_d8a279f7113e0073f6cd65dd050c1475.html (94%) rename dir_6824e70542450d573273f8d3973ed576.html => dir_daeb063f9bb170acf64b90bbd817fcde.html (94%) rename dir_c65d377d3c73fb4a07b3637f9c0caf4d.html => dir_e97d380c3eebaf0856a4ec180d749620.html (93%) rename dir_6c8d8f20501e8e412458f542d0998195.html => dir_ea36b5ab01ad6e8b607438a02ae91964.html (93%) rename dir_56301f73c488544f78326004b3ba69a6.html => dir_eca6d9a7be6772490ed1cc9215fd08cf.html (95%) rename dir_383ca23c40357ede4cca827b4a365c76.html => dir_f88948f9bde0555374e8a59a663bdfc9.html (93%) rename dir_4a67386b25ed7510b9ff12136421e5e0.html => dir_fa19fb0671b7c8d7e52fd04aee5fcddb.html (94%) rename dir_7a9d035dcfcd6d9c7aa1efef4b6b58a4.html => dir_fab1362e86740a2c573ef875dd474a67.html (92%) create mode 100644 search/enumvalues_11.html create mode 100644 search/enumvalues_11.js diff --git a/_api_controller_8cs_source.html b/_api_controller_8cs_source.html index a79632fe34..47619d2f77 100644 --- a/_api_controller_8cs_source.html +++ b/_api_controller_8cs_source.html @@ -91,7 +91,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authentication.JwtBearer;
2 using Microsoft.AspNetCore.Http;
3 using Microsoft.AspNetCore.Mvc;
4 using Microsoft.AspNetCore.Mvc.Filters;
5 using Microsoft.Extensions.DependencyInjection;
6 using Microsoft.Extensions.Logging;
7 using System;
8 using System.Collections.Generic;
9 using System.Globalization;
10 using System.IdentityModel.Tokens.Jwt;
11 using System.Linq;
12 using System.Security.Claims;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
19 
20 namespace Tgstation.Server.Host.Controllers
21 {
25  [Produces(ApiHeaders.ApplicationJson)]
26  [Consumes(ApiHeaders.ApplicationJson)]
27  public abstract class ApiController : Controller
28  {
32  protected ApiHeaders ApiHeaders { get; private set; }
33 
37  protected IDatabaseContext DatabaseContext { get; }
38 
43 
47  protected ILogger Logger { get; }
48 
52  protected Models.Instance Instance { get; }
53 
57  readonly bool requireInstance;
58 
64  public static async Task OnTokenValidated(TokenValidatedContext context)
65  {
66  var databaseContext = context.HttpContext.RequestServices.GetRequiredService<IDatabaseContext>();
67  var authenticationContextFactory = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationContextFactory>();
68 
69  var userIdClaim = context.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
70 
71  if (userIdClaim == default(Claim))
72  throw new InvalidOperationException("Missing required claim!");
73 
74  long userId;
75  try
76  {
77  userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
78  }
79  catch (Exception e)
80  {
81  throw new InvalidOperationException("Failed to parse user ID!", e);
82  }
83 
84  ApiHeaders apiHeaders;
85  try
86  {
87  apiHeaders = new ApiHeaders(context.HttpContext.Request.GetTypedHeaders());
88  }
89  catch
90  {
91  //let OnActionExecutionAsync handle the reponse
92  return;
93  }
94 
95  await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.SecurityToken.ValidFrom, context.HttpContext.RequestAborted).ConfigureAwait(false);
96 
97  var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
98 
99  var enumerator = Enum.GetValues(typeof(RightsType));
100  var claims = new List<Claim>();
101  foreach (RightsType I in enumerator)
102  {
103  //if there's no instance user, do a weird thing and add all the instance roles
104  //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
105  //if user is null that means they got the token with an expired password
106  var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0 : authenticationContext.GetRight(I);
107  var rightEnum = RightsHelper.RightToType(I);
108  var right = (Enum)Enum.ToObject(rightEnum, rightInt);
109  foreach (Enum J in Enum.GetValues(rightEnum))
110  if (right.HasFlag(J))
111  claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
112  }
113 
114  context.Principal.AddIdentity(new ClaimsIdentity(claims));
115  }
116 
124  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
125  {
126  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
127  if (authenticationContextFactory == null)
128  throw new ArgumentNullException(nameof(authenticationContextFactory));
129  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
130  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
132  this.requireInstance = requireInstance;
133  }
134 
136  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
137  {
138  if (AuthenticationContext != null && AuthenticationContext.User == null)
139  {
140  //valid token, expired password
141  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
142  return;
143  }
144 
145  //validate the headers
146  try
147  {
148  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
149 
150  if (requireInstance)
151  {
152  if(!ApiHeaders.InstanceId.HasValue)
153  {
154  await BadRequest(new ErrorMessage { Message = "Missing InstanceId header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
155  return;
156  }
158  {
159  //accessing an instance they don't have access to or one that's disabled
160  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
161  return;
162  }
163  }
164  }
165  catch (InvalidOperationException e)
166  {
167  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
168  return;
169  }
170 
171  if(ModelState?.IsValid == false)
172  {
173  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
174  //do some fuckery to remove RequiredAttribute errors
175  for (var I = 0; I < errorMessages.Count; ++I)
176  {
177  var message = errorMessages[I];
178  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
179  {
180  errorMessages.RemoveAt(I);
181  --I;
182  }
183  }
184  if (errorMessages.Count > 0)
185  {
186  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
187  return;
188  }
189  }
190 
191  Logger.LogInformation("Request made by User ID {0}. Api version: {1}. User-Agent: {2}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture) ?? "NULL", ApiHeaders.ApiVersion, ApiHeaders.UserAgent);
192  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
193  }
194  }
195 }
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 Microsoft.AspNetCore.Mvc;
4 using Microsoft.AspNetCore.Mvc.Filters;
5 using Microsoft.Extensions.DependencyInjection;
6 using Microsoft.Extensions.Logging;
7 using System;
8 using System.Collections.Generic;
9 using System.Globalization;
10 using System.IdentityModel.Tokens.Jwt;
11 using System.Linq;
12 using System.Security.Claims;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
19 
20 namespace Tgstation.Server.Host.Controllers
21 {
25  [Produces(ApiHeaders.ApplicationJson)]
26  [Consumes(ApiHeaders.ApplicationJson)]
27  public abstract class ApiController : Controller
28  {
32  protected ApiHeaders ApiHeaders { get; private set; }
33 
37  protected IDatabaseContext DatabaseContext { get; }
38 
43 
47  protected ILogger Logger { get; }
48 
52  protected Models.Instance Instance { get; }
53 
57  readonly bool requireInstance;
58 
64  public static async Task OnTokenValidated(TokenValidatedContext context)
65  {
66  var databaseContext = context.HttpContext.RequestServices.GetRequiredService<IDatabaseContext>();
67  var authenticationContextFactory = context.HttpContext.RequestServices.GetRequiredService<IAuthenticationContextFactory>();
68 
69  var userIdClaim = context.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
70 
71  if (userIdClaim == default(Claim))
72  throw new InvalidOperationException("Missing required claim!");
73 
74  long userId;
75  try
76  {
77  userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
78  }
79  catch (Exception e)
80  {
81  throw new InvalidOperationException("Failed to parse user ID!", e);
82  }
83 
84  ApiHeaders apiHeaders;
85  try
86  {
87  apiHeaders = new ApiHeaders(context.HttpContext.Request.GetTypedHeaders());
88  }
89  catch
90  {
91  //let OnActionExecutionAsync handle the reponse
92  return;
93  }
94 
95  await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.SecurityToken.ValidFrom, context.HttpContext.RequestAborted).ConfigureAwait(false);
96 
97  var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
98 
99  var enumerator = Enum.GetValues(typeof(RightsType));
100  var claims = new List<Claim>();
101  foreach (RightsType I in enumerator)
102  {
103  //if there's no instance user, do a weird thing and add all the instance roles
104  //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
105  //if user is null that means they got the token with an expired password
106  var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0 : authenticationContext.GetRight(I);
107  var rightEnum = RightsHelper.RightToType(I);
108  var right = (Enum)Enum.ToObject(rightEnum, rightInt);
109  foreach (Enum J in Enum.GetValues(rightEnum))
110  if (right.HasFlag(J))
111  claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
112  }
113 
114  context.Principal.AddIdentity(new ClaimsIdentity(claims));
115  }
116 
124  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
125  {
126  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
127  if (authenticationContextFactory == null)
128  throw new ArgumentNullException(nameof(authenticationContextFactory));
129  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
130  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
132  this.requireInstance = requireInstance;
133  }
134 
136  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
137  {
138  if (AuthenticationContext != null && AuthenticationContext.User == null)
139  {
140  //valid token, expired password
141  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
142  return;
143  }
144 
145  //validate the headers
146  try
147  {
148  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
149 
150  if (requireInstance)
151  {
152  if(!ApiHeaders.InstanceId.HasValue)
153  {
154  await BadRequest(new ErrorMessage { Message = "Missing InstanceId header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
155  return;
156  }
158  {
159  //accessing an instance they don't have access to or one that's disabled
160  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
161  return;
162  }
163  }
164  }
165  catch (InvalidOperationException e)
166  {
167  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
168  return;
169  }
170 
171  if(ModelState?.IsValid == false)
172  {
173  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
174  //do some fuckery to remove RequiredAttribute errors
175  for (var I = 0; I < errorMessages.Count; ++I)
176  {
177  var message = errorMessages[I];
178  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
179  {
180  errorMessages.RemoveAt(I);
181  --I;
182  }
183  }
184  if (errorMessages.Count > 0)
185  {
186  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
187  return;
188  }
189  }
190 
191  Logger.LogTrace("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path);
192  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
193  }
194  }
195 }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
@@ -113,7 +113,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
static async Task OnTokenValidated(TokenValidatedContext context)
Runs after a Api.Models.Token has been validated. Creates the IAuthenticationContext for the Controll...
diff --git a/_app_8g_8cs.html b/_app_8g_8cs.html index 0795e7f203..77b4241f6e 100644 --- a/_app_8g_8cs.html +++ b/_app_8g_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.ControlPanel/obj/Release/App.g.cs File Reference +tgstation-server: src/Tgstation.Server.ControlPanel/obj/Debug/App.g.cs File Reference @@ -83,7 +83,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_app_8g_8cs_source.html b/_app_8g_8cs_source.html index 108646dc4e..726f33edfe 100644 --- a/_app_8g_8cs_source.html +++ b/_app_8g_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.ControlPanel/obj/Release/App.g.cs Source File +tgstation-server: src/Tgstation.Server.ControlPanel/obj/Debug/App.g.cs Source File @@ -83,7 +83,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_application_8cs_source.html b/_application_8cs_source.html index a43443a081..8d8cfd3760 100644 --- a/_application_8cs_source.html +++ b/_application_8cs_source.html @@ -91,7 +91,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Application.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Cyberboss.AspNetCore.AsyncInitializer;
3 using Microsoft.AspNetCore.Authentication.JwtBearer;
4 using Microsoft.AspNetCore.Builder;
5 using Microsoft.AspNetCore.Hosting;
6 using Microsoft.AspNetCore.Hosting.Server.Features;
7 using Microsoft.AspNetCore.Identity;
8 using Microsoft.EntityFrameworkCore;
9 using Microsoft.Extensions.Configuration;
10 using Microsoft.Extensions.DependencyInjection;
11 using Microsoft.Extensions.Hosting;
12 using Microsoft.Extensions.Logging;
13 using Microsoft.IdentityModel.Tokens;
14 using Newtonsoft.Json;
15 using System;
16 using System.Globalization;
17 using System.IdentityModel.Tokens.Jwt;
18 using System.Linq;
19 using System.Reflection;
20 using System.Runtime.InteropServices;
21 using System.Threading.Tasks;
29 using Tgstation.Server.Host.IO;
32 
33 namespace Tgstation.Server.Host.Core
34 {
36  sealed class Application : IApplication
37  {
41  public string VersionPrefix => "tgstation-server";
42 
44  public string HostingPath => serverAddresses.Addresses.First();
45 
47  public Version Version { get; }
48 
50  public string VersionString { get; }
51 
55  readonly Microsoft.Extensions.Configuration.IConfiguration configuration;
56 
60  readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment;
61 
62  readonly TaskCompletionSource<object> startupTcs;
63 
67  IServerAddressesFeature serverAddresses;
68 
74  public Application(Microsoft.Extensions.Configuration.IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
75  {
76  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
77  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
78 
79  startupTcs = new TaskCompletionSource<object>();
80 
81  Version = Assembly.GetExecutingAssembly().GetName().Version;
82  VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
83  }
84 
89 #pragma warning disable CA1822 // Mark members as static
90  public void ConfigureServices(IServiceCollection services)
91 #pragma warning restore CA1822 // Mark members as static
92  {
93  if (services == null)
94  throw new ArgumentNullException(nameof(services));
95 
96  services.Configure<UpdatesConfiguration>(configuration.GetSection(UpdatesConfiguration.Section));
97  var databaseConfigurationSection = configuration.GetSection(DatabaseConfiguration.Section);
98  services.Configure<DatabaseConfiguration>(databaseConfigurationSection);
99  var generalConfigurationSection = configuration.GetSection(GeneralConfiguration.Section);
100  services.Configure<GeneralConfiguration>(generalConfigurationSection);
101 
102  //remember, anything you .Get manually can be null if the config is missing
103  var generalConfiguration = generalConfigurationSection.Get<GeneralConfiguration>();
104  var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
105  var ioManager = new DefaultIOManager();
106 
107  if (generalConfiguration?.DisableFileLogging != true)
108  {
109  var logPath = !String.IsNullOrEmpty(generalConfiguration?.LogFileDirectory) ? generalConfiguration.LogFileDirectory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
110  services.AddLogging(builder => builder.AddFile(ioManager.ConcatPath(logPath, "tgs-{Date}.log")));
111  }
112 
113  services.AddOptions();
114 
115  const string scheme = "JwtBearer";
116  services.AddAuthentication((options) =>
117  {
118  options.DefaultAuthenticateScheme = scheme;
119  options.DefaultChallengeScheme = scheme;
120  }).AddJwtBearer(scheme, jwtBearerOptions =>
121  {
122  jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
123  {
124  ValidateIssuerSigningKey = true,
125  IssuerSigningKey = new SymmetricSecurityKey(TokenFactory.TokenSigningKey),
126 
127  ValidateIssuer = true,
128  ValidIssuer = TokenFactory.TokenIssuer,
129 
130  ValidateLifetime = true,
131  ValidateAudience = true,
132  ValidAudience = TokenFactory.TokenAudience,
133 
134  ClockSkew = TimeSpan.FromMinutes(5),
135 
136  RequireSignedTokens = true,
137 
138  RequireExpirationTime = true
139  };
140  jwtBearerOptions.Events = new JwtBearerEvents
141  {
142  OnTokenValidated = ApiController.OnTokenValidated
143  };
144  });
145  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs
146 
147  services.AddMvc().AddJsonOptions(options =>
148  {
149  options.AllowInputFormatterExceptionMessages = true;
150  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
151  options.SerializerSettings.CheckAdditionalContent = true;
152  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
153  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
154  });
155 
156  var databaseConfiguration = databaseConfigurationSection.Get<DatabaseConfiguration>();
157 
158  void ConfigureDatabase(DbContextOptionsBuilder builder)
159  {
160  if (hostingEnvironment.IsDevelopment())
161  builder.EnableSensitiveDataLogging();
162  };
163 
164  switch (databaseConfiguration?.DatabaseType ?? DatabaseType.Sqlite)
165  {
166  case DatabaseType.MySql:
167  services.AddDbContext<MySqlDatabaseContext>(ConfigureDatabase);
168  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<MySqlDatabaseContext>());
169  break;
170  case DatabaseType.Sqlite:
171  services.AddDbContext<SqliteDatabaseContext>(ConfigureDatabase);
172  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<SqliteDatabaseContext>());
173  break;
174  case DatabaseType.SqlServer:
175  services.AddDbContext<SqlServerDatabaseContext>(ConfigureDatabase);
176  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<SqlServerDatabaseContext>());
177  break;
178  default:
179  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}!", nameof(DatabaseType)));
180  }
181 
183  services.AddSingleton<IIdentityCache, IdentityCache>();
184 
185  services.AddSingleton<ICryptographySuite, CryptographySuite>();
186  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
187  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
188  services.AddSingleton<ITokenFactory, TokenFactory>();
189  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
190 
191  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
192  services.AddSingleton(x => x.GetRequiredService<IGitHubClientFactory>().CreateClient());
193 
194  if (isWindows)
195  {
196  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
197  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
198  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
199  }
200  else
201  {
202  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
203  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
204  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
205  }
206 
207  services.AddSingleton<IExecutor, Executor>();
208  services.AddSingleton<IScriptExecutor, ScriptExecutor>();
209  services.AddSingleton<IProviderFactory, ProviderFactory>();
210  services.AddSingleton<IByondTopicSender>(new ByondTopicSender
211  {
212  ReceiveTimeout = 5000,
213  SendTimeout = 5000
214  });
215 
216  services.AddSingleton<InstanceFactory>();
217  services.AddSingleton<IInstanceFactory>(x => x.GetRequiredService<InstanceFactory>());
218  services.AddSingleton<InstanceManager>();
219  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
220  services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
221 
222  services.AddSingleton<IJobManager, JobManager>();
223 
224  services.AddSingleton<IIOManager>(ioManager);
225 
226  services.AddSingleton<DatabaseContextFactory>();
227  services.AddSingleton<IDatabaseContextFactory>(x => x.GetRequiredService<DatabaseContextFactory>());
228 
229  services.AddSingleton<IApplication>(this);
230  }
231 
237  public void Configure(IApplicationBuilder applicationBuilder, ILogger<Application> logger)
238  {
239  if (applicationBuilder == null)
240  throw new ArgumentNullException(nameof(applicationBuilder));
241  if (logger == null)
242  throw new ArgumentNullException(nameof(logger));
243 
244  logger.LogInformation(VersionString);
245 
246  serverAddresses = applicationBuilder.ServerFeatures.Get<IServerAddressesFeature>();
247 
248  applicationBuilder.UseDeveloperExceptionPage(); //it is not worth it to limit this, you should only ever get it if you're an authorized user
249 
250  applicationBuilder.UseAsyncInitialization(async cancellationToken =>
251  {
252  using (cancellationToken.Register(() => startupTcs.SetCanceled()))
253  await startupTcs.Task.ConfigureAwait(false);
254  });
255 
256  applicationBuilder.UseAuthentication();
257  applicationBuilder.UseMvc();
258  }
259 
261  public void Ready(Exception initializationError)
262  {
263  if (startupTcs.Task.IsCompleted)
264  throw new InvalidOperationException("Ready has already been called!");
265  if (initializationError == null)
266  startupTcs.SetResult(null);
267  else
268  startupTcs.SetException(initializationError);
269  }
270  }
271 }
+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.Hosting.Server.Features;
7 using Microsoft.AspNetCore.Identity;
8 using Microsoft.EntityFrameworkCore;
9 using Microsoft.Extensions.Configuration;
10 using Microsoft.Extensions.DependencyInjection;
11 using Microsoft.Extensions.Hosting;
12 using Microsoft.Extensions.Logging;
13 using Microsoft.IdentityModel.Tokens;
14 using Newtonsoft.Json;
15 using System;
16 using System.Globalization;
17 using System.IdentityModel.Tokens.Jwt;
18 using System.Linq;
19 using System.Reflection;
20 using System.Runtime.InteropServices;
21 using System.Threading.Tasks;
29 using Tgstation.Server.Host.IO;
32 
33 namespace Tgstation.Server.Host.Core
34 {
36  sealed class Application : IApplication
37  {
41  public string VersionPrefix => "tgstation-server";
42 
44  public string HostingPath => serverAddresses.Addresses.First();
45 
47  public Version Version { get; }
48 
50  public string VersionString { get; }
51 
55  readonly Microsoft.Extensions.Configuration.IConfiguration configuration;
56 
60  readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment;
61 
62  readonly TaskCompletionSource<object> startupTcs;
63 
67  IServerAddressesFeature serverAddresses;
68 
74  public Application(Microsoft.Extensions.Configuration.IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
75  {
76  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
77  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
78 
79  startupTcs = new TaskCompletionSource<object>();
80 
81  Version = Assembly.GetExecutingAssembly().GetName().Version;
82  VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
83  }
84 
89 #pragma warning disable CA1822 // Mark members as static
90  public void ConfigureServices(IServiceCollection services)
91 #pragma warning restore CA1822 // Mark members as static
92  {
93  if (services == null)
94  throw new ArgumentNullException(nameof(services));
95 
96  services.Configure<UpdatesConfiguration>(configuration.GetSection(UpdatesConfiguration.Section));
97  var databaseConfigurationSection = configuration.GetSection(DatabaseConfiguration.Section);
98  services.Configure<DatabaseConfiguration>(databaseConfigurationSection);
99  var generalConfigurationSection = configuration.GetSection(GeneralConfiguration.Section);
100  services.Configure<GeneralConfiguration>(generalConfigurationSection);
101 
102  //remember, anything you .Get manually can be null if the config is missing
103  var generalConfiguration = generalConfigurationSection.Get<GeneralConfiguration>();
104  var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
105  var ioManager = new DefaultIOManager();
106 
107  if (generalConfiguration?.DisableFileLogging != true)
108  {
109  var logPath = !String.IsNullOrEmpty(generalConfiguration?.LogFileDirectory) ? generalConfiguration.LogFileDirectory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
110  services.AddLogging(builder => builder.AddFile(ioManager.ConcatPath(logPath, "tgs-{Date}.log")));
111  }
112 
113  services.AddOptions();
114 
115  const string scheme = "JwtBearer";
116  services.AddAuthentication((options) =>
117  {
118  options.DefaultAuthenticateScheme = scheme;
119  options.DefaultChallengeScheme = scheme;
120  }).AddJwtBearer(scheme, jwtBearerOptions =>
121  {
122  jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
123  {
124  ValidateIssuerSigningKey = true,
125  IssuerSigningKey = new SymmetricSecurityKey(TokenFactory.TokenSigningKey),
126 
127  ValidateIssuer = true,
128  ValidIssuer = TokenFactory.TokenIssuer,
129 
130  ValidateLifetime = true,
131  ValidateAudience = true,
132  ValidAudience = TokenFactory.TokenAudience,
133 
134  ClockSkew = TimeSpan.FromMinutes(5),
135 
136  RequireSignedTokens = true,
137 
138  RequireExpirationTime = true
139  };
140  jwtBearerOptions.Events = new JwtBearerEvents
141  {
142  OnTokenValidated = ApiController.OnTokenValidated
143  };
144  });
145  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs
146 
147  services.AddMvc().AddJsonOptions(options =>
148  {
149  options.AllowInputFormatterExceptionMessages = true;
150  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
151  options.SerializerSettings.CheckAdditionalContent = true;
152  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
153  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
154  });
155 
156  var databaseConfiguration = databaseConfigurationSection.Get<DatabaseConfiguration>();
157 
158  void ConfigureDatabase(DbContextOptionsBuilder builder)
159  {
160  if (hostingEnvironment.IsDevelopment())
161  builder.EnableSensitiveDataLogging();
162  };
163 
164  switch (databaseConfiguration?.DatabaseType ?? DatabaseType.Sqlite)
165  {
166  case DatabaseType.MySql:
167  services.AddDbContext<MySqlDatabaseContext>(ConfigureDatabase);
168  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<MySqlDatabaseContext>());
169  break;
170  case DatabaseType.Sqlite:
171  services.AddDbContext<SqliteDatabaseContext>(ConfigureDatabase);
172  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<SqliteDatabaseContext>());
173  break;
174  case DatabaseType.SqlServer:
175  services.AddDbContext<SqlServerDatabaseContext>(ConfigureDatabase);
176  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<SqlServerDatabaseContext>());
177  break;
178  default:
179  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}!", nameof(DatabaseType)));
180  }
181 
183  services.AddSingleton<IIdentityCache, IdentityCache>();
184 
185  services.AddSingleton<ICryptographySuite, CryptographySuite>();
186  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
187  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
188  services.AddSingleton<ITokenFactory, TokenFactory>();
189  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
190 
191  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
192  services.AddSingleton(x => x.GetRequiredService<IGitHubClientFactory>().CreateClient());
193 
194  if (isWindows)
195  {
196  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
197  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
198  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
199  }
200  else
201  {
202  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
203  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
204  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
205  }
206 
207  services.AddSingleton<IExecutor, Executor>();
208  services.AddSingleton<IScriptExecutor, ScriptExecutor>();
209  services.AddSingleton<IProviderFactory, ProviderFactory>();
210  services.AddSingleton<IByondTopicSender>(new ByondTopicSender
211  {
212  ReceiveTimeout = 5000,
213  SendTimeout = 5000
214  });
215 
216  services.AddSingleton<InstanceFactory>();
217  services.AddSingleton<IInstanceFactory>(x => x.GetRequiredService<InstanceFactory>());
218  services.AddSingleton<InstanceManager>();
219  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
220  services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
221 
222  services.AddSingleton<IJobManager, JobManager>();
223 
224  services.AddSingleton<IIOManager>(ioManager);
225 
226  services.AddSingleton<DatabaseContextFactory>();
227  services.AddSingleton<IDatabaseContextFactory>(x => x.GetRequiredService<DatabaseContextFactory>());
228 
229  services.AddSingleton<IApplication>(this);
230  }
231 
237  public void Configure(IApplicationBuilder applicationBuilder, ILogger<Application> logger)
238  {
239  if (applicationBuilder == null)
240  throw new ArgumentNullException(nameof(applicationBuilder));
241  if (logger == null)
242  throw new ArgumentNullException(nameof(logger));
243 
244  logger.LogInformation(VersionString);
245 
246  logger.LogTrace("Configuring middleware...");
247 
248  serverAddresses = applicationBuilder.ServerFeatures.Get<IServerAddressesFeature>();
249 
250  applicationBuilder.UseDeveloperExceptionPage(); //it is not worth it to limit this, you should only ever get it if you're an authorized user
251 
252  applicationBuilder.UseAsyncInitialization(async cancellationToken =>
253  {
254  using (cancellationToken.Register(() => startupTcs.SetCanceled()))
255  await startupTcs.Task.ConfigureAwait(false);
256  });
257 
258  applicationBuilder.UseAuthentication();
259  applicationBuilder.UseMvc();
260  }
261 
263  public void Ready(Exception initializationError)
264  {
265  if (startupTcs.Task.IsCompleted)
266  throw new InvalidOperationException("Ready has already been called!");
267  if (initializationError == null)
268  startupTcs.SetResult(null);
269  else
270  startupTcs.SetException(initializationError);
271  }
272  }
273 }
Manages the runtime of Jobs
Definition: IJobManager.cs:13
@@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment
The Microsoft.AspNetCore.Hosting.IHostingEnvironment for the Application
Definition: Application.cs:60
void Configure(IApplicationBuilder applicationBuilder, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:237
- +
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
@@ -126,11 +126,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Configuration for the automatic update system
-
DatabaseContext<TParentContext> for Sqlserver
+
DatabaseContext<TParentContext> for Sqlserver
For downloading and installing BYOND extractions for a given system
For accessing the disk in a synchronous manner
-
void Ready(Exception initializationError)
Mark the IApplication as ready to run
Definition: Application.cs:261
+
void Ready(Exception initializationError)
Mark the IApplication as ready to run
Definition: Application.cs:263
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the UpdatesConfiguration res...
@@ -142,14 +142,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IGitHubClient CreateClient()
Create a client with anonymous authentication. Low rate limit
- +
void ConfigureServices(IServiceCollection services)
Configure dependency injected services
Definition: Application.cs:90
Configuration options for the Models.DatabaseContext<TParentContext>
- +
IIOManager that resolves paths to Environment.CurrentDirectory
@@ -166,7 +166,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IServerAddressesFeature serverAddresses
The IServerAddressesFeature for the Application
Definition: Application.cs:67
-
DatabaseContext<TParentContext> for MySQL
+
DatabaseContext<TParentContext> for MySQL
diff --git a/_byond_manager_8cs_source.html b/_byond_manager_8cs_source.html index 6e58ad5ed3..4e96f4ce00 100644 --- a/_byond_manager_8cs_source.html +++ b/_byond_manager_8cs_source.html @@ -91,7 +91,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.Linq;
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 {
14  sealed class ByondManager : IByondManager
15  {
16  const string VersionFileName = "Version.txt";
17  const string ActiveVersionFileName = "ActiveVersion.txt";
18 
19  const string BinPath = "byond/bin";
20 
22  public Version ActiveVersion { get; private set; }
23 
25  public IReadOnlyList<Version> InstalledVersions
26  {
27  get
28  {
29  lock (installedVersions)
30  return installedVersions.Select(x => Version.Parse(x.Key)).ToList();
31  }
32  }
33 
38 
43 
47  readonly ILogger<ByondManager> logger;
48 
52  readonly Dictionary<string, Task> installedVersions;
53 
57  readonly SemaphoreSlim semaphore;
58 
59  static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
60 
67  public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, ILogger<ByondManager> logger)
68  {
69  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
70  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
71  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
72 
73  installedVersions = new Dictionary<string, Task>();
74  semaphore = new SemaphoreSlim(1);
75  }
76 
78  public void Dispose() => semaphore.Dispose();
79 
86  async Task InstallVersion(Version version, CancellationToken cancellationToken)
87  {
88  var ourTcs = new TaskCompletionSource<object>();
89  Task inProgressTask;
90 
91  var versionKey = VersionKey(version);
92  bool installed;
93  lock (installedVersions)
94  {
95  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
96  if (!installed)
97  installedVersions.Add(versionKey, ourTcs.Task);
98  }
99  if(installed)
100  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
101  {
102  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
103  cancellationToken.ThrowIfCancellationRequested();
104  return;
105  }
106  try
107  {
108  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
109 
110  //okay up to us to install it then
111  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
112  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
113 
114  //byond can just decide to corrupt the zip fnr
115  //(or maybe our downloader is a shite)
116  //either way try a few times
117  for (var I = 0; I < 3; ++I)
118  {
119  var download = await downloadTask.ConfigureAwait(false);
120  try
121  {
122  await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
123  break;
124  }
125  catch (OperationCanceledException)
126  {
127  throw;
128  }
129  catch
130  {
131  if (I == 2)
132  throw;
133  downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
134  }
135  }
136  await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
137 
138  //make sure to do this last because this is what tells us we have a valid version in the future
139  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
140  }
141  catch
142  {
143  lock (installedVersions)
144  installedVersions.Remove(versionKey);
145  throw;
146  }
147  }
148 
150  public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
151  {
152  await InstallVersion(version, cancellationToken).ConfigureAwait(false);
153  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
154  {
155  await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
156  ActiveVersion = version;
157  }
158  }
159 
161  public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
162  {
163  var versionToUse = requiredVersion ?? ActiveVersion;
164  if (versionToUse == null)
165  throw new InvalidOperationException("No BYOND versions installed!");
166  await InstallVersion(requiredVersion, cancellationToken).ConfigureAwait(false);
167 
168  var versionKey = VersionKey(versionToUse);
169 
170  return new ByondExecutableLock
171  {
172  DreamDaemonPath = ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)),
173  DreamMakerPath = ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)),
174  Version = versionToUse
175  };
176  }
177 
179  public async Task StartAsync(CancellationToken cancellationToken)
180  {
181  var cacheCleanTask = byondInstaller.CleanCache(cancellationToken);
182 
183  async Task<byte[]> GetActiveVersion()
184  {
185  if (!await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false))
186  return null;
187  return await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
188  }
189 
190  var activeVersionBytesTask = GetActiveVersion();
191 
192  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
193  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
194 
195  async Task ReadVersion(string path)
196  {
197  var versionFile = ioManager.ConcatPath(path, VersionFileName);
198  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
199  {
200  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
201  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); //cleanup
202  return;
203  }
204  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
205  var text = Encoding.UTF8.GetString(bytes);
206  if (Version.TryParse(text, out var version))
207  {
208  var key = VersionKey(version);
209  lock (installedVersions)
210  if (!installedVersions.ContainsKey(key))
211  {
212  installedVersions.Add(key, Task.CompletedTask);
213  return;
214  }
215  }
216  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
217  };
218 
219  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
220 
221  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
222  if (activeVersionBytes != null)
223  {
224  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
225  if (Version.TryParse(activeVersionString, out var activeVersion))
226  ActiveVersion = activeVersion;
227  else
228  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
229 
230  await cacheCleanTask.ConfigureAwait(false);
231  }
232  }
233 
235  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
236  }
237 }
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:37
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
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 {
14  sealed class ByondManager : IByondManager
15  {
16  const string VersionFileName = "Version.txt";
17  const string ActiveVersionFileName = "ActiveVersion.txt";
18 
19  const string BinPath = "byond/bin";
20 
22  public Version ActiveVersion { get; private set; }
23 
25  public IReadOnlyList<Version> InstalledVersions
26  {
27  get
28  {
29  lock (installedVersions)
30  return installedVersions.Select(x => Version.Parse(x.Key)).ToList();
31  }
32  }
33 
38 
43 
47  readonly ILogger<ByondManager> logger;
48 
52  readonly Dictionary<string, Task> installedVersions;
53 
57  readonly SemaphoreSlim semaphore;
58 
59  static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
60 
67  public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, ILogger<ByondManager> logger)
68  {
69  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
70  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
71  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
72 
73  installedVersions = new Dictionary<string, Task>();
74  semaphore = new SemaphoreSlim(1);
75  }
76 
78  public void Dispose() => semaphore.Dispose();
79 
86  async Task InstallVersion(Version version, CancellationToken cancellationToken)
87  {
88  var ourTcs = new TaskCompletionSource<object>();
89  Task inProgressTask;
90 
91  var versionKey = VersionKey(version);
92  bool installed;
93  lock (installedVersions)
94  {
95  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
96  if (!installed)
97  installedVersions.Add(versionKey, ourTcs.Task);
98  }
99  if(installed)
100  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
101  {
102  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
103  cancellationToken.ThrowIfCancellationRequested();
104  return;
105  }
106  try
107  {
108  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
109 
110  //okay up to us to install it then
111  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
112  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
113 
114  //byond can just decide to corrupt the zip fnr
115  //(or maybe our downloader is a shite)
116  //either way try a few times
117  for (var I = 0; I < 3; ++I)
118  {
119  var download = await downloadTask.ConfigureAwait(false);
120  try
121  {
122  await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
123  break;
124  }
125  catch (OperationCanceledException)
126  {
127  throw;
128  }
129  catch
130  {
131  if (I == 2)
132  throw;
133  downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
134  }
135  }
136  await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
137 
138  //make sure to do this last because this is what tells us we have a valid version in the future
139  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
140  }
141  catch
142  {
143  lock (installedVersions)
144  installedVersions.Remove(versionKey);
145  throw;
146  }
147  }
148 
150  public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
151  {
152  await InstallVersion(version, cancellationToken).ConfigureAwait(false);
153  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
154  {
155  await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
156  ActiveVersion = version;
157  }
158  }
159 
161  public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
162  {
163  var versionToUse = requiredVersion ?? ActiveVersion;
164  if (versionToUse == null)
165  throw new InvalidOperationException("No BYOND versions installed!");
166  await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false);
167 
168  var versionKey = VersionKey(versionToUse);
169 
170  return new ByondExecutableLock
171  {
172  DreamDaemonPath = ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)),
173  DreamMakerPath = ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)),
174  Version = versionToUse
175  };
176  }
177 
179  public async Task StartAsync(CancellationToken cancellationToken)
180  {
181  var cacheCleanTask = byondInstaller.CleanCache(cancellationToken);
182 
183  async Task<byte[]> GetActiveVersion()
184  {
185  if (!await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false))
186  return null;
187  return await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
188  }
189 
190  var activeVersionBytesTask = GetActiveVersion();
191 
192  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
193  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
194 
195  async Task ReadVersion(string path)
196  {
197  var versionFile = ioManager.ConcatPath(path, VersionFileName);
198  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
199  {
200  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
201  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); //cleanup
202  return;
203  }
204  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
205  var text = Encoding.UTF8.GetString(bytes);
206  if (Version.TryParse(text, out var version))
207  {
208  var key = VersionKey(version);
209  lock (installedVersions)
210  if (!installedVersions.ContainsKey(key))
211  {
212  installedVersions.Add(key, Task.CompletedTask);
213  return;
214  }
215  }
216  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
217  };
218 
219  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
220 
221  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
222  if (activeVersionBytes != null)
223  {
224  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
225  if (Version.TryParse(activeVersionString, out var activeVersion))
226  ActiveVersion = activeVersion;
227  else
228  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
229 
230  await cacheCleanTask.ConfigureAwait(false);
231  }
232  }
233 
235  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
236  }
237 }
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:37
ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, ILogger< ByondManager > logger)
Construct a ByondManager
Definition: ByondManager.cs:67
diff --git a/_chat_8cs_source.html b/_chat_8cs_source.html index fec6ec18c4..3ddb94da99 100644 --- a/_chat_8cs_source.html +++ b/_chat_8cs_source.html @@ -110,7 +110,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For interacting with a chat service
Definition: IProvider.cs:11
- +
bool Connected(long connectionId)
If a given set of ChatSettings is connected
Definition: Chat.cs:507
For managing connected chat services
Definition: IChat.cs:13
diff --git a/_chat_controller_8cs_source.html b/_chat_controller_8cs_source.html index 72750f3e76..3914b112db 100644 --- a/_chat_controller_8cs_source.html +++ b/_chat_controller_8cs_source.html @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Linq.Expressions;
5 using System.Net;
6 using System.Reflection;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Microsoft.AspNetCore.Mvc;
10 using Microsoft.EntityFrameworkCore;
11 using Microsoft.Extensions.Logging;
17 using Z.EntityFramework.Plus;
18 
19 namespace Tgstation.Server.Host.Controllers
20 {
24  [Route("/" + nameof(Components.Chat.Chat))]
25  public sealed class ChatController : ModelController<Api.Models.ChatSettings>
26  {
31 
39  public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
40  {
41  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
42  }
43 
49  static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api) => new Models.ChatChannel
50  {
51  DiscordChannelId = api.DiscordChannelId,
52  IrcChannel = api.IrcChannel,
53  IsAdminChannel = api.IsAdminChannel,
54  IsWatchdogChannel = api.IsWatchdogChannel
55  };
56 
58  [TgsAuthorize(ChatSettingsRights.Create)]
59  public override async Task<IActionResult> Create([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken)
60  {
61  if (model == null)
62  throw new ArgumentNullException(nameof(model));
63 
64  if (String.IsNullOrWhiteSpace(model.Name))
65  return BadRequest(new ErrorMessage { Message = "name cannot be null or whitespace!" });
66 
67  if (String.IsNullOrWhiteSpace(model.ConnectionString))
68  return BadRequest(new ErrorMessage { Message = "connection_string cannot be null or whitespace!" });
69 
70  if (!model.Provider.HasValue)
71  return BadRequest(new ErrorMessage { Message = "provider cannot be null!" });
72 
73  if (!model.Enabled.HasValue)
74  return BadRequest(new ErrorMessage { Message = "enabled cannot be null!" });
75 
76  //try to update das db first
77  var dbModel = new Models.ChatSettings
78  {
79  Name = model.Name,
80  ConnectionString = model.ConnectionString,
81  Enabled = model.Enabled,
82  Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).ToList() ?? new List<Models.ChatChannel>(), //important that this isn't null
83  InstanceId = Instance.Id,
84  Provider = model.Provider,
85  };
86  DatabaseContext.ChatSettings.Add(dbModel);
87 
88  try
89  {
90  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
91  }
92  catch (DbUpdateConcurrencyException)
93  {
94  return Conflict();
95  }
96 
97  try
98  {
99  try
100  {
101  //try to create it
102  var instance = instanceManager.GetInstance(Instance);
103  await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false);
104 
105  if (dbModel.Channels.Count > 0)
106  await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false);
107  }
108  catch
109  {
110  //undo the add
111  DatabaseContext.ChatSettings.Remove(dbModel);
112  await DatabaseContext.Save(default).ConfigureAwait(false);
113  throw;
114  }
115  }
116  catch (InvalidOperationException e)
117  {
118  return BadRequest(new ErrorMessage { Message = e.Message });
119  }
120  return Json(dbModel.ToApi());
121  }
122 
124  [TgsAuthorize(ChatSettingsRights.Delete)]
125  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
126  {
127  var instance = instanceManager.GetInstance(Instance);
128  await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatSettings.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false);
129 
130  return Ok();
131  }
132 
134  [TgsAuthorize(ChatSettingsRights.Read)]
135  public override async Task<IActionResult> List(CancellationToken cancellationToken)
136  {
137  var query = DatabaseContext.ChatSettings.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels);
138 
139  var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
140 
141  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatSettings) & (int)ChatSettingsRights.ReadConnectionString) != 0;
142 
143  if (!connectionStrings)
144  foreach (var I in results)
145  I.ConnectionString = null;
146 
147  return Json(results.Select(x => x.ToApi()));
148  }
149 
151  [TgsAuthorize(ChatSettingsRights.Read)]
152  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
153  {
154  var query = DatabaseContext.ChatSettings.Where(x => x.Id == id).Include(x => x.Channels);
155 
156  var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
157  if (results == default)
158  return NotFound();
159 
160  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatSettings) & (int)ChatSettingsRights.ReadConnectionString) != 0;
161 
162  if (!connectionStrings)
163  results.ConnectionString = null;
164 
165  return Json(results.ToApi());
166  }
167 
169  [TgsAuthorize(ChatSettingsRights.WriteChannels | ChatSettingsRights.WriteConnectionString | ChatSettingsRights.WriteEnabled | ChatSettingsRights.WriteName | ChatSettingsRights.WriteProvider)]
170  public override async Task<IActionResult> Update([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken)
171  {
172  if (model == null)
173  throw new ArgumentNullException(nameof(model));
174 
175  var query = DatabaseContext.ChatSettings.Where(x => x.InstanceId == Instance.Id && x.Id == model.Id).Include(x => x.Channels);
176 
177  var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
178 
179  if (current == default)
180  return StatusCode((int)HttpStatusCode.Gone);
181 
182  var userRights = (ChatSettingsRights)AuthenticationContext.GetRight(RightsType.ChatSettings);
183 
184  bool anySettingsModified = false;
185 
186  bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatSettings, T>> expression, ChatSettingsRights requiredRight)
187  {
188  var memberSelectorExpression = (MemberExpression)expression.Body;
189  var property = (PropertyInfo)memberSelectorExpression.Member;
190 
191  var newVal = property.GetValue(model);
192  if (newVal == null)
193  return false;
194  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
195  return true;
196 
197  property.SetValue(current, newVal);
198  anySettingsModified = true;
199  return false;
200  };
201 
202  if (CheckModified(x => x.ConnectionString, ChatSettingsRights.WriteConnectionString)
203  || CheckModified(x => x.Enabled, ChatSettingsRights.WriteEnabled)
204  || CheckModified(x => x.Name, ChatSettingsRights.WriteName)
205  || CheckModified(x => x.Provider, ChatSettingsRights.WriteProvider)
206  || (model.Channels != null && !userRights.HasFlag(ChatSettingsRights.WriteChannels)))
207  return Forbid();
208 
209  if (model.Channels != null)
210  {
211  DatabaseContext.ChatChannels.RemoveRange(current.Channels);
212  var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList();
213  DatabaseContext.ChatChannels.AddRange(dbChannels);
214  current.Channels = dbChannels;
215  }
216 
217  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
218 
219  var chat = instanceManager.GetInstance(Instance).Chat;
220 
221  if (anySettingsModified)
222  //have to rebuild the thing first
223  await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false);
224 
225  if (model.Channels != null || anySettingsModified)
226  await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false);
227 
228  if (userRights.HasFlag(ChatSettingsRights.Read))
229  {
230  if (!userRights.HasFlag(ChatSettingsRights.ReadConnectionString))
231  current.ConnectionString = null;
232  return Json(current.ToApi());
233  }
234  return Ok();
235  }
236  }
237 }
- +
override async Task< IActionResult > Create([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken)
@@ -108,7 +108,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
string Message
A human readable description of the error
Definition: ErrorMessage.cs:11
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
- +
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:17
int GetRight(RightsType rightsType)
Get the value of a given rightsType
An ApiController representing a TModel
@@ -126,7 +126,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
override async Task< IActionResult > Update([FromBody] Api.Models.ChatSettings model, CancellationToken cancellationToken)
- +
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
7 
8 namespace Tgstation.Server.Host.Components.Chat.Commands
9 {
12  {
17 
22 
27 
32 
36  readonly Models.Instance instance;
37 
42 
51  public CommandFactory(IApplication application, IByondManager byondManager, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
52  {
53  this.application = application ?? throw new ArgumentNullException(nameof(application));
54  this.byondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
55  this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
56  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
57  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
58  }
59 
64  public void SetWatchdog(IWatchdog watchdog)
65  {
66  if (this.watchdog != null)
67  throw new InvalidOperationException("SetWatchdog has already been called!");
68  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
69  }
70 
72  public IReadOnlyList<ICommand> GenerateCommands()
73  {
74  if (watchdog == null)
75  throw new InvalidOperationException("SetWatchdog has not been called!");
76  return new List<ICommand>
77  {
78  new VersionCommand(application),
79  new ByondCommand(byondManager),
80  new PullRequestsCommand(watchdog, repositoryManager, databaseContextFactory, instance),
81  new KekCommand()
82  };
83  }
84  }
85 }
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
7 
8 namespace Tgstation.Server.Host.Components.Chat.Commands
9 {
12  {
17 
22 
27 
32 
36  readonly Models.Instance instance;
37 
42 
51  public CommandFactory(IApplication application, IByondManager byondManager, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
52  {
53  this.application = application ?? throw new ArgumentNullException(nameof(application));
54  this.byondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
55  this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
56  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
57  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
58  }
59 
64  public void SetWatchdog(IWatchdog watchdog)
65  {
66  if (this.watchdog != null)
67  throw new InvalidOperationException("SetWatchdog has already been called!");
68  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
69  }
70 
72  public IReadOnlyList<ICommand> GenerateCommands()
73  {
74  if (watchdog == null)
75  throw new InvalidOperationException("SetWatchdog has not been called!");
76  return new List<ICommand>
77  {
78  new VersionCommand(application),
79  new ByondCommand(byondManager),
80  new PullRequestsCommand(watchdog, repositoryManager, databaseContextFactory, instance),
81  new KekCommand()
82  };
83  }
84  }
85 }
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
readonly IByondManager byondManager
The IByondManager for the CommandFactory
diff --git a/_compiler_status_8cs.html b/_compiler_status_8cs.html index d2787fdc3c..8d8d92cfa1 100644 --- a/_compiler_status_8cs.html +++ b/_compiler_status_8cs.html @@ -107,12 +107,15 @@ Enumerations enum  Tgstation.Server.Api.Models.CompilerStatus {
  Tgstation.Server.Api.Models.CompilerStatus.Idle, Tgstation.Server.Api.Models.CompilerStatus.Copying, +Tgstation.Server.Api.Models.CompilerStatus.PreCompile, Tgstation.Server.Api.Models.CompilerStatus.Modifying, -Tgstation.Server.Api.Models.CompilerStatus.Compiling,
-  Tgstation.Server.Api.Models.CompilerStatus.Verifying, +  Tgstation.Server.Api.Models.CompilerStatus.Compiling, +Tgstation.Server.Api.Models.CompilerStatus.Verifying, +Tgstation.Server.Api.Models.CompilerStatus.PostCompile, Tgstation.Server.Api.Models.CompilerStatus.Duplicating, -Tgstation.Server.Api.Models.CompilerStatus.Symlinking, +
+  Tgstation.Server.Api.Models.CompilerStatus.Symlinking, Tgstation.Server.Api.Models.CompilerStatus.Cleanup
} Status of the DreamMaker for an Instance More...
diff --git a/_compiler_status_8cs_source.html b/_compiler_status_8cs_source.html index a4ff919a43..96359fbe3a 100644 --- a/_compiler_status_8cs_source.html +++ b/_compiler_status_8cs_source.html @@ -91,10 +91,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
CompilerStatus.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6 #pragma warning disable CA1717 // Only FlagsAttribute enums should have plural names
7  public enum CompilerStatus
8 #pragma warning restore CA1717 // Only FlagsAttribute enums should have plural names
9  {
13  Idle,
17  Copying,
21  Modifying,
25  Compiling,
29  Verifying,
37  Symlinking,
41  Cleanup
42  }
43 }
The .dme is having it&#39;s server side modifications applied
+Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6 #pragma warning disable CA1717 // Only FlagsAttribute enums should have plural names
7  public enum CompilerStatus
8 #pragma warning restore CA1717 // Only FlagsAttribute enums should have plural names
9  {
13  Idle,
17  Copying,
21  PreCompile,
25  Modifying,
29  Compiling,
33  Verifying,
45  Symlinking,
49  Cleanup
50  }
51 }
The .dme is having it&#39;s server side modifications applied
+ +
The compile results are being duplicated
CompilerStatus
Status of the DreamMaker for an Instance
A failed compile job is being erased
diff --git a/_components_2_watchdog_2_i_watchdog_factory_8cs_source.html b/_components_2_watchdog_2_i_watchdog_factory_8cs_source.html index ad9a588ea3..f9d37b3690 100644 --- a/_components_2_watchdog_2_i_watchdog_factory_8cs_source.html +++ b/_components_2_watchdog_2_i_watchdog_factory_8cs_source.html @@ -91,7 +91,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IWatchdogFactory.cs
-Go to the documentation of this file.
3 
4 namespace Tgstation.Server.Host.Components.Watchdog
5 {
9  interface IWatchdogFactory
10  {
17  IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings);
18  }
19 }
+Go to the documentation of this file.
3 
4 namespace Tgstation.Server.Host.Components.Watchdog
5 {
9  interface IWatchdogFactory
10  {
17  IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings);
18  }
19 }
diff --git a/_components_2_watchdog_2_reattach_information_8cs_source.html b/_components_2_watchdog_2_reattach_information_8cs_source.html index cf94b45bf9..66a4cbaf0f 100644 --- a/_components_2_watchdog_2_reattach_information_8cs_source.html +++ b/_components_2_watchdog_2_reattach_information_8cs_source.html @@ -91,14 +91,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ReattachInformation.cs
-Go to the documentation of this file.
3 
4 namespace Tgstation.Server.Host.Components.Watchdog
5 {
10  {
14  public IDmbProvider Dmb { get; set; }
15 
19  public ReattachInformation() { }
20 
26  public ReattachInformation(Models.ReattachInformation copy, IDmbFactory dmbFactory) : base(copy)
27  {
28  Dmb = dmbFactory.FromCompileJob(copy.CompileJob);
29  }
30  }
31 }
ReattachInformation(Models.ReattachInformation copy, IDmbFactory dmbFactory)
Construct a ReattachInformation from a given copy and dmbFactory
- - - +Go to the documentation of this file.
1 using System;
3 
4 namespace Tgstation.Server.Host.Components.Watchdog
5 {
10  {
14  public IDmbProvider Dmb { get; set; }
15 
19  public ReattachInformation() { }
20 
26  public ReattachInformation(Models.ReattachInformation copy, IDmbProvider dmb) : base(copy)
27  {
28  Dmb = dmb ?? throw new ArgumentNullException(nameof(dmb));
29  }
30  }
31 }
+ -
IDmbProvider FromCompileJob(CompileJob compileJob)
Gets a IDmbProvider for a given CompileJob
- +
ReattachInformation(Models.ReattachInformation copy, IDmbProvider dmb)
Construct a ReattachInformation from a given copy and dmb
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
diff --git a/_components_2_watchdog_2_watchdog_8cs_source.html b/_components_2_watchdog_2_watchdog_8cs_source.html index 7b1c299820..23b3ea5ef3 100644 --- a/_components_2_watchdog_2_watchdog_8cs_source.html +++ b/_components_2_watchdog_2_watchdog_8cs_source.html @@ -91,7 +91,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.Extensions.Logging;
3 using Newtonsoft.Json;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Components.Watchdog
17 {
20  {
24  const int AlphaBravoStartupSeperationInterval = 3000;
25 
27  public bool Running { get; private set; }
28 
30  public bool AlphaIsActive { get; private set; }
31 
33  public Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
34 
36  public LaunchResult LastLaunchResult { get; private set; }
37 
39  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; private set; }
40 
42  public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; }
43 
45  public RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
46 
50  readonly IChat chat;
51 
56 
61 
65  readonly ILogger<Watchdog> logger;
66 
71 
76 
80  readonly IByondTopicSender byondTopicSender;
81 
86 
90  readonly SemaphoreSlim semaphore;
91 
95  readonly Api.Models.Instance instance;
96 
100  readonly bool autoStart;
101 
105  CancellationTokenSource monitorCts;
106 
111 
115  TaskCompletionSource<object> activeParametersUpdated;
116 
125 
130 
146  public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerControl serverUpdater, ILogger<Watchdog> logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
147  {
148  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
149  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
150  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
151  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
152  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
153  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
154  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
155  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
156  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
157  this.autoStart = autoStart;
158 
159  if (serverUpdater == null)
160  throw new ArgumentNullException(nameof(serverUpdater));
161 
162  serverUpdater.RegisterForRestart(() => releaseServers = true);
163 
164  chat.RegisterCommandHandler(this);
165 
166  AlphaIsActive = true;
167  ActiveLaunchParameters = initialLaunchParameters;
168  releaseServers = false;
169  semaphore = new SemaphoreSlim(1);
170  activeParametersUpdated = new TaskCompletionSource<object>();
171  }
172 
174  public void Dispose()
175  {
176  DisposeAndNullControllers();
177  semaphore.Dispose();
178  }
179 
184  {
185  logger.LogTrace("DisposeAndNullControllers");
186  alphaServer?.Dispose();
187  alphaServer = null;
188  bravoServer?.Dispose();
189  bravoServer = null;
190  }
191 
195  async Task<WatchdogLaunchResult> RestartNoLock(bool graceful, CancellationToken cancellationToken)
196  {
197  var running = Running;
198  if (!graceful || !running)
199  {
200  Task chatTask;
201  if (running)
202  {
203  chatTask = chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken);
204  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
205  }
206  else
207  chatTask = Task.CompletedTask;
208  var result = await LaunchNoLock(true, !running, false, cancellationToken).ConfigureAwait(false);
209  await chatTask.ConfigureAwait(false);
210  return result;
211  }
212  var toReboot = AlphaIsActive ? alphaServer : bravoServer;
213  var other = AlphaIsActive ? bravoServer : alphaServer;
214  if (toReboot != null)
215  //todo, log the result
216  await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false);
217  return null;
218  }
219 
226  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
227  {
228  if (!Running)
229  return;
230  if (!graceful)
231  {
232  var chatTask = announce ? chat.SendWatchdogMessage("Terminating...", cancellationToken) : Task.CompletedTask;
233  await StopMonitor().ConfigureAwait(false);
234  DisposeAndNullControllers();
235  await chatTask.ConfigureAwait(false);
236  return;
237  }
238  var toKill = AlphaIsActive ? alphaServer : bravoServer;
239  var other = AlphaIsActive ? bravoServer : alphaServer;
240  if (toKill != null)
241  await toKill.SetRebootState(Components.Watchdog.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
242  }
243 
251  async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
252  {
253  logger.LogInformation("Monitor activation. Reason: {0}", activationReason);
254 
255  //returns true if the inactive server can't be used immediately
256  bool FullRestartDeadInactive()
257  {
258  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
259  {
260  logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
261  monitorState.NextAction = MonitorAction.Restart; //will dispose server
262  return true;
263  }
264  return false;
265  };
266 
267  //trys to set inactive server's port to the private port
268  async Task<bool> MakeInactiveActive()
269  {
270  logger.LogInformation("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
271  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
272 
273  if (!result)
274  {
275  logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
276  monitorState.NextAction = MonitorAction.Restart; //will dispose server
277  return false;
278  }
279 
280  // should always be set for InactiveServer
281  monitorState.InactiveServer.ClosePortOnReboot = false;
282  monitorState.ActiveServer.ClosePortOnReboot = true;
283 
284  //inactive server should always be using active launch parameters
285  LastLaunchParameters = ActiveLaunchParameters;
286 
287  var tmp = monitorState.ActiveServer;
288  monitorState.ActiveServer = monitorState.InactiveServer;
289  monitorState.InactiveServer = tmp;
290  AlphaIsActive = !AlphaIsActive;
291  return true;
292  }
293 
294  // Tries to load inactive server with latest dmb, falling back to current dmb on failure. Requires a lock on <see cref="semaphore"/>
295  async Task<bool> RestartInactiveServer()
296  {
297  logger.LogInformation("Rebooting inactive server...");
298  var newDmb = dmbFactory.LockNextDmb(cancellationToken);
299  bool usedMostRecentDmb;
300  try
301  {
302  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, await newDmb.ConfigureAwait(false), null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
303  usedMostRecentDmb = true;
304  }
305  catch (OperationCanceledException)
306  {
307  throw;
308  }
309  catch (Exception e)
310  {
311  logger.LogError("Exception occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
312  //ahh jeez, what do we do here?
313  //this is our fault, so it should never happen but
314  //idk maybe a database error while handling the newest dmb?
315  //either way try to start it using the active server's dmb as a backup
316  try
317  {
318  var dmbBackup = dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob);
319  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
320  usedMostRecentDmb = false;
321  await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
322  }
323  catch (OperationCanceledException)
324  {
325  throw;
326  }
327  catch (Exception e2)
328  {
329  //fuuuuucckkk
330  logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! This Exception: {0}", e2.ToString());
331  monitorState.InactiveServerCritFail = true;
332  await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
333  return true; //we didn't use the old dmb
334  }
335  }
336 
337  logger.LogInformation("Successfully relaunched inactive server!");
338  monitorState.RebootingInactiveServer = true;
339  // should always be set for InactiveServer
340  monitorState.InactiveServer.ClosePortOnReboot = false;
341  return usedMostRecentDmb;
342  }
343 
344  async Task UpdateAndRestartInactiveServer(bool breakAfter)
345  {
346  //replace the notification tcs here so that the next loop will read a fresh one
347  activeParametersUpdated = new TaskCompletionSource<object>();
348  monitorState.InactiveServer.Dispose(); //kill or recycle it
349  monitorState.NextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
350 
351  var usedLatestDmb = await RestartInactiveServer().ConfigureAwait(false);
352 
353  if (monitorState.NextAction == (breakAfter ? MonitorAction.Break : MonitorAction.Continue))
354  {
355  monitorState.ActiveServer.ClosePortOnReboot = false;
356  if (monitorState.InactiveServerHasStagedDmb && !usedLatestDmb)
357  monitorState.InactiveServerHasStagedDmb = false; //don't try to load it again though
358  }
359  };
360 
361  //reason handling
362  switch (activationReason)
363  {
364  case MonitorActivationReason.ActiveServerCrashed:
365  if(monitorState.ActiveServer.RebootState == Components.Watchdog.RebootState.Shutdown)
366  {
367  await chat.SendWatchdogMessage("Active server crashed or exited! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
368  monitorState.NextAction = MonitorAction.Exit;
369  break;
370  }
371 
372  if (FullRestartDeadInactive())
373  {
374  await chat.SendWatchdogMessage("Active server crashed or exited! Inactive server unable to online!", cancellationToken).ConfigureAwait(false);
375  break;
376  }
377 
378  await chat.SendWatchdogMessage("Active server crashed or exited! Onlining inactive server...", cancellationToken).ConfigureAwait(false);
379  if (!await MakeInactiveActive().ConfigureAwait(false))
380  break;
381 
382  monitorState.ActiveServer.ClosePortOnReboot = false;
383  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
384  break;
385  case MonitorActivationReason.InactiveServerCrashed:
386  await chat.SendWatchdogMessage("Inactive server crashed or exited! Rebooting...", cancellationToken).ConfigureAwait(false);
387  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
388  break;
389  case MonitorActivationReason.ActiveServerRebooted:
390  if (FullRestartDeadInactive())
391  break;
392 
393  //what matters here is the RebootState
394  bool restartOnceSwapped = false;
395  var rebootState = monitorState.ActiveServer.RebootState;
396  monitorState.ActiveServer.ResetRebootState(); //the DMAPI has already done this internally
397 
398  switch (rebootState)
399  {
400  case Components.Watchdog.RebootState.Normal:
401  break;
402  case Components.Watchdog.RebootState.Restart:
403  restartOnceSwapped = true;
404  break;
405  case Components.Watchdog.RebootState.Shutdown:
406  await chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
407  DisposeAndNullControllers();
408  Running = false;
409  monitorState.NextAction = MonitorAction.Exit;
410  return;
411  }
412 
413  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
414  if (sameCompileJob && monitorState.InactiveServerHasStagedDmb)
415  //both servers up to date
416  monitorState.InactiveServerHasStagedDmb = false;
417  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
418  //need a new launch in ActiveServer
419  restartOnceSwapped = true;
420 
421  if (!await MakeInactiveActive().ConfigureAwait(false))
422  break;
423 
424  if(!restartOnceSwapped)
425  //try to reopen inactive server on the private port so it's not pinging all the time
426  //failing that, just reboot it
427  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
428 
429  if (restartOnceSwapped) //for one reason or another,
430  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); //break because worse case, active server is still booting
431  else
432  monitorState.NextAction = MonitorAction.Break;
433  break;
434  case MonitorActivationReason.InactiveServerRebooted:
435  //should never happen but okay
436  logger.LogWarning("Inactive server rebooted, this is a bug in DM code!");
437  monitorState.RebootingInactiveServer = true;
438  monitorState.InactiveServer.ResetRebootState(); //the DMAPI has already done this internally
439  monitorState.ActiveServer.ClosePortOnReboot = false;
440  monitorState.NextAction = MonitorAction.Continue;
441  break;
442  case MonitorActivationReason.InactiveServerStartupComplete:
443  //eziest case of my life
444  monitorState.RebootingInactiveServer = false;
445  monitorState.ActiveServer.ClosePortOnReboot = true;
446  monitorState.NextAction = MonitorAction.Continue;
447  break;
448  case MonitorActivationReason.NewDmbAvailable:
449  monitorState.InactiveServerHasStagedDmb = true;
450  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); //next case does same thing
451  break;
452  case MonitorActivationReason.ActiveLaunchParametersUpdated:
453  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
454  break;
455  }
456  }
457 
463  async Task MonitorLifetimes(CancellationToken cancellationToken)
464  {
465  logger.LogDebug("Entered MonitorLifetimes");
466  var iteration = 1;
467  for(var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
468  {
469  logger.LogDebug("New iteration of monitor loop");
470  try
471  {
472  if(AlphaIsActive)
473  logger.LogDebug("Alpha is the active server");
474  else
475  logger.LogDebug("Bravo is the active server");
476 
477  if(monitorState.InactiveServerHasStagedDmb)
478  logger.LogDebug("Inactive server has staged .dmb");
479  if (monitorState.RebootingInactiveServer)
480  logger.LogDebug("Inactive server is rebooting");
481 
482  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
483  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
484 
485  var activeServerLifetime = monitorState.ActiveServer.Lifetime;
486  var inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
487  var activeServerReboot = monitorState.ActiveServer.OnReboot;
488  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
489  var inactiveServerStartup = monitorState.InactiveServer.LaunchResult;
490  var activeLaunchParametersChanged = activeParametersUpdated.Task;
491  var newDmbAvailable = dmbFactory.OnNewerDmb;
492 
493  var cancelTcs = new TaskCompletionSource<object>();
494  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
495  {
496  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
497  if (monitorState.RebootingInactiveServer)
498  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
499  await toWaitOn.ConfigureAwait(false);
500  cancellationToken.ThrowIfCancellationRequested();
501  }
502 
503  var chatTask = Task.CompletedTask;
504  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
505  {
506  MonitorActivationReason activationReason = default;
507  //multiple things may have happened, handle them one at a time
508  for (var moreActivationsToProcess = true; moreActivationsToProcess && monitorState.NextAction == MonitorAction.Continue; )
509  {
510  if (activeServerLifetime?.IsCompleted == true)
511  {
512  activationReason = MonitorActivationReason.ActiveServerCrashed;
513  activeServerLifetime = null;
514  }
515  else if (inactiveServerLifetime?.IsCompleted == true)
516  {
517  activationReason = MonitorActivationReason.InactiveServerCrashed;
518  inactiveServerLifetime = null;
519  }
520  else if (activeServerReboot?.IsCompleted == true)
521  {
522  activationReason = MonitorActivationReason.ActiveServerRebooted;
523  activeServerReboot = null;
524  }
525  else if (inactiveServerReboot?.IsCompleted == true)
526  {
527  activationReason = MonitorActivationReason.InactiveServerRebooted;
528  inactiveServerReboot = null;
529  }
530  else if (inactiveServerStartup?.IsCompleted == true)
531  {
532  activationReason = MonitorActivationReason.InactiveServerStartupComplete;
533  inactiveServerStartup = null;
534  }
535  else if (newDmbAvailable?.IsCompleted == true)
536  {
537  activationReason = MonitorActivationReason.NewDmbAvailable;
538  newDmbAvailable = null;
539  }
540  else if(activeLaunchParametersChanged?.IsCompleted == true)
541  {
542  activationReason = MonitorActivationReason.ActiveLaunchParametersUpdated;
543  activeLaunchParametersChanged = null;
544  }
545  else
546  moreActivationsToProcess = false;
547  }
548 
549  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
550  //writeback alphaServer and bravoServer
551  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
552  bravoServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
553  }
554 
555  //full reboot required
556  if (monitorState.NextAction == MonitorAction.Restart)
557  {
558  logger.LogDebug("Next state action is to restart");
559  DisposeAndNullControllers();
560  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
561  }
562 
563  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
564  {
565  WatchdogLaunchResult result;
566  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
567  {
568  result = await LaunchNoLock(false, false, false, cancellationToken).ConfigureAwait(false);
569  if (Running)
570  monitorState = new MonitorState(); //clean the slate
571  }
572 
573  await chatTask.ConfigureAwait(false);
574  if(!Running)
575  {
576  logger.LogWarning("Failed to automatically restart the watchdog! Alpha: {0}; Bravo: {1}", result.Alpha.ToString(), result.Bravo.ToString());
577  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); //max of one hour
578  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
579  await Task.WhenAll(Task.Delay((int)retryDelay, cancellationToken), chatTask).ConfigureAwait(false);
580  }
581  }
582  }
583  catch (OperationCanceledException)
584  {
585  logger.LogDebug("Monitor cancelled");
586  break;
587  }
588  catch (Exception e)
589  {
590  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}", iteration, JsonConvert.SerializeObject(monitorState));
591  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);
592  }
593  }
594  }
595 
596  async Task<bool> StopMonitor()
597  {
598  logger.LogTrace("StopMonitor");
599  if (monitorTask == null)
600  return false;
601  monitorCts.Cancel();
602  await monitorTask.ConfigureAwait(false);
603  monitorCts.Dispose();
604  monitorTask = null;
605  return true;
606  }
607 
608 
610  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
611  {
612  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
613  {
614  ActiveLaunchParameters = launchParameters;
615  if (Running)
616  //queue an update
617  activeParametersUpdated.TrySetResult(null);
618  }
619  }
620 
621  async Task<WatchdogLaunchResult> LaunchNoLock(bool startMonitor, bool announce, bool doReattach, CancellationToken cancellationToken)
622  {
623  using (var alphaStartCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
624  {
625  logger.LogTrace("Begin LaunchNoLock");
626  if (Running)
627  {
628  logger.LogTrace("Aborted due to already running!");
629  return null;
630  }
631 
632  Task chatTask;
633  //this is necessary, the monitor could be in it's sleep loop trying to restart
634  if (startMonitor && await StopMonitor().ConfigureAwait(false))
635  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
636  else if (announce)
637  chatTask = chat.SendWatchdogMessage("Starting...", cancellationToken);
638  else
639  chatTask = Task.CompletedTask;
640  //start both servers
641  LastLaunchParameters = ActiveLaunchParameters;
642  try
643  {
644  //good ole sanity
645  if (alphaServer != null || bravoServer != null)
646  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
647 
648  var reattachInfo = doReattach ? await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false) : null;
649  var doesntNeedNewDmb = doReattach && reattachInfo.Alpha != null && reattachInfo.Bravo != null;
650  var dmbToUse = doesntNeedNewDmb ? null : await dmbFactory.LockNextDmb(cancellationToken).ConfigureAwait(false);
651 
652  Task<ISessionController> alphaServerTask = null;
653  try
654  {
655  if (!doReattach || reattachInfo.Alpha == null)
656  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, alphaStartCts.Token);
657  else
658  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
659  //do a few seconds of delay so that any backends the servers use know that alpha came first
660  await Task.Delay(AlphaBravoStartupSeperationInterval, cancellationToken).ConfigureAwait(false);
661  Task<ISessionController> bravoServerTask;
662  if (!doReattach || reattachInfo.Bravo == null)
663  bravoServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken);
664  else
665  bravoServerTask = sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken);
666 
667  await Task.WhenAll(alphaServerTask, bravoServerTask).ConfigureAwait(false);
668 
669  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
670  {
671  var launch = await controller.LaunchResult.ConfigureAwait(false);
672  if (launch.ExitCode.HasValue)
673  //you killed us ray...
674  throw new Exception(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
675  return launch;
676  }
677 
678  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
679  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
680  //now we have two booting servers, get them up and running
681  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
682 
683  //don't forget about the cancelationToken
684  var cancelTcs = new TaskCompletionSource<object>();
685  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
686  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
687  cancellationToken.ThrowIfCancellationRequested();
688 
689  //update the live and staged jobs in the db
690  await databaseContextFactory.UseContext(async db =>
691  {
692  var settings = new Models.DreamDaemonSettings
693  {
694  InstanceId = instance.Id
695  };
696  var cj = (AlphaIsActive ? alphaServer : bravoServer).Dmb.CompileJob;
697  db.CompileJobs.Attach(cj);
698  db.DreamDaemonSettings.Attach(settings);
699  settings.StagedCompileJob = null;
700  settings.ActiveCompileJob = cj;
701  await db.Save(cancellationToken).ConfigureAwait(false);
702  }).ConfigureAwait(false);
703 
704  //both servers are now running, alpha is the active server, huzzah
705  AlphaIsActive = doReattach ? reattachInfo.AlphaIsActive : true;
706  LastLaunchResult = alphaLrt.Result;
707  Running = true;
708 
709  if (startMonitor)
710  {
711  await StopMonitor().ConfigureAwait(false);
712  monitorCts = new CancellationTokenSource();
713  monitorTask = MonitorLifetimes(monitorCts.Token);
714  }
715  return new WatchdogLaunchResult
716  {
717  Alpha = alphaLrt.Result,
718  Bravo = bravoLrt.Result
719  };
720  }
721  catch
722  {
723  if (alphaServer == null && bravoServer == null)
724  dmbToUse.Dispose(); //guaranteed to not be null here
725  DisposeAndNullControllers();
726  throw;
727  }
728  }
729  catch (Exception e)
730  {
731  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
732  throw;
733  }
734  finally
735  {
736  try
737  {
738  await chatTask.ConfigureAwait(false);
739  }
740  catch (OperationCanceledException) { }
741  }
742  }
743  }
744 
746  public async Task<WatchdogLaunchResult> Launch(CancellationToken cancellationToken)
747  {
748  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
749  return await LaunchNoLock(true, true, false, cancellationToken).ConfigureAwait(false);
750  }
751 
753  public async Task<WatchdogLaunchResult> Restart(bool graceful, CancellationToken cancellationToken)
754  {
755  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
756  return await RestartNoLock(graceful, cancellationToken).ConfigureAwait(false);
757  }
758 
760  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
761  {
762  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
763  await TerminateNoLock(graceful, true, cancellationToken).ConfigureAwait(false);
764  }
765 
767  public async Task StartAsync(CancellationToken cancellationToken)
768  {
769  if (autoStart)
770  await LaunchNoLock(true, true, true, cancellationToken).ConfigureAwait(false);
771  }
772 
774  public async Task StopAsync(CancellationToken cancellationToken)
775  {
776  if (releaseServers && Running)
777  {
778  var reattachInformation = new WatchdogReattachInformation
779  {
780  AlphaIsActive = AlphaIsActive
781  };
782  reattachInformation.Alpha = alphaServer?.Release();
783  reattachInformation.Bravo = bravoServer?.Release();
784  await reattachInfoHandler.Save(reattachInformation, cancellationToken).ConfigureAwait(false);
785  }
786  await Terminate(false, cancellationToken).ConfigureAwait(false);
787  }
788 
790  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
791  {
792  string results;
793  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
794  {
795  if (!Running)
796  return true;
797 
798  var builder = new StringBuilder(InteropConstants.DMTopicEvent);
799  foreach (var I in parameters)
800  {
801  builder.Append("&");
802  builder.Append(byondTopicSender.SanitizeString(I));
803  }
804 
805  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
806  results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
807  }
808 
809  if (results == null)
810  return true;
811 
812  List<Response> responses;
813  try
814  {
815  responses = JsonConvert.DeserializeObject<List<Response>>(results);
816  }
817  catch
818  {
819  logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
820  return true;
821  }
822 
823  await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
824 
825  return true;
826  }
827 
829  public async Task<string> HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
830  {
831  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
832  {
833  if (!Running)
834  return "ERROR: Server offline!";
835 
836  var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(InteropConstants.DMTopicChatCommand), byondTopicSender.SanitizeString(InteropConstants.DMParameterData), byondTopicSender.SanitizeString(JsonConvert.SerializeObject(arguments)));
837 
838  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
839  return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
840  }
841  }
842  }
843 }
async Task< WatchdogLaunchResult > Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
Definition: Watchdog.cs:753
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Components.Watchdog
17 {
20  {
24  const int AlphaBravoStartupSeperationInterval = 3000;
25 
27  public bool Running { get; private set; }
28 
30  public bool AlphaIsActive { get; private set; }
31 
33  public Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
34 
36  public LaunchResult LastLaunchResult { get; private set; }
37 
39  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; private set; }
40 
42  public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; }
43 
45  public RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
46 
50  readonly IChat chat;
51 
56 
61 
65  readonly ILogger<Watchdog> logger;
66 
71 
76 
80  readonly IByondTopicSender byondTopicSender;
81 
86 
90  readonly SemaphoreSlim semaphore;
91 
95  readonly Api.Models.Instance instance;
96 
100  readonly bool autoStart;
101 
105  CancellationTokenSource monitorCts;
106 
111 
115  TaskCompletionSource<object> activeParametersUpdated;
116 
125 
130 
146  public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerControl serverUpdater, ILogger<Watchdog> logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
147  {
148  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
149  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
150  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
151  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
152  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
153  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
154  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
155  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
156  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
157  this.autoStart = autoStart;
158 
159  if (serverUpdater == null)
160  throw new ArgumentNullException(nameof(serverUpdater));
161 
162  serverUpdater.RegisterForRestart(() => releaseServers = true);
163 
164  chat.RegisterCommandHandler(this);
165 
166  AlphaIsActive = true;
167  ActiveLaunchParameters = initialLaunchParameters;
168  releaseServers = false;
169  semaphore = new SemaphoreSlim(1);
170  activeParametersUpdated = new TaskCompletionSource<object>();
171  }
172 
174  public void Dispose()
175  {
176  DisposeAndNullControllers();
177  semaphore.Dispose();
178  }
179 
184  {
185  alphaServer?.Dispose();
186  alphaServer = null;
187  bravoServer?.Dispose();
188  bravoServer = null;
189  }
190 
194  async Task<WatchdogLaunchResult> RestartNoLock(bool graceful, CancellationToken cancellationToken)
195  {
196  var running = Running;
197  if (!graceful || !running)
198  {
199  Task chatTask;
200  if (running)
201  {
202  chatTask = chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken);
203  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
204  }
205  else
206  chatTask = Task.CompletedTask;
207  var result = await LaunchNoLock(true, !running, false, cancellationToken).ConfigureAwait(false);
208  await chatTask.ConfigureAwait(false);
209  return result;
210  }
211  var toReboot = AlphaIsActive ? alphaServer : bravoServer;
212  var other = AlphaIsActive ? bravoServer : alphaServer;
213  if (toReboot != null)
214  //todo, log the result
215  await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false);
216  return null;
217  }
218 
225  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
226  {
227  if (!Running)
228  return;
229  if (!graceful)
230  {
231  var chatTask = announce ? chat.SendWatchdogMessage("Terminating...", cancellationToken) : Task.CompletedTask;
232  await StopMonitor().ConfigureAwait(false);
233  DisposeAndNullControllers();
234  await chatTask.ConfigureAwait(false);
235  return;
236  }
237  var toKill = AlphaIsActive ? alphaServer : bravoServer;
238  var other = AlphaIsActive ? bravoServer : alphaServer;
239  if (toKill != null)
240  await toKill.SetRebootState(Components.Watchdog.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
241  }
242 
250  async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
251  {
252  logger.LogInformation("Monitor activation. Reason: {0}", activationReason);
253 
254  //returns true if the inactive server can't be used immediately
255  bool FullRestartDeadInactive()
256  {
257  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
258  {
259  logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
260  monitorState.NextAction = MonitorAction.Restart; //will dispose server
261  return true;
262  }
263  return false;
264  };
265 
266  //trys to set inactive server's port to the private port
267  async Task<bool> MakeInactiveActive()
268  {
269  logger.LogInformation("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
270  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
271 
272  if (!result)
273  {
274  logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
275  monitorState.NextAction = MonitorAction.Restart; //will dispose server
276  return false;
277  }
278 
279  // should always be set for InactiveServer
280  monitorState.InactiveServer.ClosePortOnReboot = false;
281  monitorState.ActiveServer.ClosePortOnReboot = true;
282 
283  //inactive server should always be using active launch parameters
284  LastLaunchParameters = ActiveLaunchParameters;
285 
286  var tmp = monitorState.ActiveServer;
287  monitorState.ActiveServer = monitorState.InactiveServer;
288  monitorState.InactiveServer = tmp;
289  AlphaIsActive = !AlphaIsActive;
290  return true;
291  }
292 
293  // Tries to load inactive server with latest dmb, falling back to current dmb on failure. Requires a lock on <see cref="semaphore"/>
294  async Task<bool> RestartInactiveServer()
295  {
296  logger.LogInformation("Rebooting inactive server...");
297  var newDmb = dmbFactory.LockNextDmb(cancellationToken);
298  bool usedMostRecentDmb;
299  try
300  {
301  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, await newDmb.ConfigureAwait(false), null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
302  usedMostRecentDmb = true;
303  }
304  catch (OperationCanceledException)
305  {
306  throw;
307  }
308  catch (Exception e)
309  {
310  logger.LogError("Exception occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
311  //ahh jeez, what do we do here?
312  //this is our fault, so it should never happen but
313  //idk maybe a database error while handling the newest dmb?
314  //either way try to start it using the active server's dmb as a backup
315  try
316  {
317  var dmbBackup = await dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
318 
319  if (dmbBackup == null) //NANI!?
320  //just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
321  throw new Exception("Creating backup DMB provider failed!");
322 
323  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
324  usedMostRecentDmb = false;
325  await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
326  }
327  catch (OperationCanceledException)
328  {
329  throw;
330  }
331  catch (Exception e2)
332  {
333  //fuuuuucckkk
334  logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
335  monitorState.InactiveServerCritFail = true;
336  await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
337  return true; //we didn't use the old dmb
338  }
339  }
340 
341  logger.LogInformation("Successfully relaunched inactive server!");
342  monitorState.RebootingInactiveServer = true;
343  // should always be set for InactiveServer
344  monitorState.InactiveServer.ClosePortOnReboot = false;
345  return usedMostRecentDmb;
346  }
347 
348  async Task UpdateAndRestartInactiveServer(bool breakAfter)
349  {
350  //replace the notification tcs here so that the next loop will read a fresh one
351  activeParametersUpdated = new TaskCompletionSource<object>();
352  monitorState.InactiveServer.Dispose(); //kill or recycle it
353  monitorState.NextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
354 
355  var usedLatestDmb = await RestartInactiveServer().ConfigureAwait(false);
356 
357  if (monitorState.NextAction == (breakAfter ? MonitorAction.Break : MonitorAction.Continue))
358  {
359  monitorState.ActiveServer.ClosePortOnReboot = false;
360  if (monitorState.InactiveServerHasStagedDmb && !usedLatestDmb)
361  monitorState.InactiveServerHasStagedDmb = false; //don't try to load it again though
362  }
363  };
364 
365  //reason handling
366  switch (activationReason)
367  {
368  case MonitorActivationReason.ActiveServerCrashed:
369  if(monitorState.ActiveServer.RebootState == Components.Watchdog.RebootState.Shutdown)
370  {
371  await chat.SendWatchdogMessage("Active server crashed or exited! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
372  monitorState.NextAction = MonitorAction.Exit;
373  break;
374  }
375 
376  if (FullRestartDeadInactive())
377  {
378  await chat.SendWatchdogMessage("Active server crashed or exited! Inactive server unable to online!", cancellationToken).ConfigureAwait(false);
379  break;
380  }
381 
382  await chat.SendWatchdogMessage("Active server crashed or exited! Onlining inactive server...", cancellationToken).ConfigureAwait(false);
383  if (!await MakeInactiveActive().ConfigureAwait(false))
384  break;
385 
386  monitorState.ActiveServer.ClosePortOnReboot = false;
387  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
388  break;
389  case MonitorActivationReason.InactiveServerCrashed:
390  await chat.SendWatchdogMessage("Inactive server crashed or exited! Rebooting...", cancellationToken).ConfigureAwait(false);
391  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
392  break;
393  case MonitorActivationReason.ActiveServerRebooted:
394  if (FullRestartDeadInactive())
395  break;
396 
397  //what matters here is the RebootState
398  bool restartOnceSwapped = false;
399  var rebootState = monitorState.ActiveServer.RebootState;
400  monitorState.ActiveServer.ResetRebootState(); //the DMAPI has already done this internally
401 
402  switch (rebootState)
403  {
404  case Components.Watchdog.RebootState.Normal:
405  break;
406  case Components.Watchdog.RebootState.Restart:
407  restartOnceSwapped = true;
408  break;
409  case Components.Watchdog.RebootState.Shutdown:
410  await chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
411  DisposeAndNullControllers();
412  Running = false;
413  monitorState.NextAction = MonitorAction.Exit;
414  return;
415  }
416 
417  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
418  if (sameCompileJob && monitorState.InactiveServerHasStagedDmb)
419  //both servers up to date
420  monitorState.InactiveServerHasStagedDmb = false;
421  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
422  //need a new launch in ActiveServer
423  restartOnceSwapped = true;
424 
425  if (!await MakeInactiveActive().ConfigureAwait(false))
426  break;
427 
428  if(!restartOnceSwapped)
429  //try to reopen inactive server on the private port so it's not pinging all the time
430  //failing that, just reboot it
431  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
432 
433  if (restartOnceSwapped) //for one reason or another,
434  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); //break because worse case, active server is still booting
435  else
436  monitorState.NextAction = MonitorAction.Break;
437  break;
438  case MonitorActivationReason.InactiveServerRebooted:
439  //should never happen but okay
440  logger.LogWarning("Inactive server rebooted, this is a bug in DM code!");
441  monitorState.RebootingInactiveServer = true;
442  monitorState.InactiveServer.ResetRebootState(); //the DMAPI has already done this internally
443  monitorState.ActiveServer.ClosePortOnReboot = false;
444  monitorState.NextAction = MonitorAction.Continue;
445  break;
446  case MonitorActivationReason.InactiveServerStartupComplete:
447  //eziest case of my life
448  monitorState.RebootingInactiveServer = false;
449  monitorState.ActiveServer.ClosePortOnReboot = true;
450  monitorState.NextAction = MonitorAction.Continue;
451  break;
452  case MonitorActivationReason.NewDmbAvailable:
453  monitorState.InactiveServerHasStagedDmb = true;
454  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); //next case does same thing
455  break;
456  case MonitorActivationReason.ActiveLaunchParametersUpdated:
457  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
458  break;
459  }
460  }
461 
467  async Task MonitorLifetimes(CancellationToken cancellationToken)
468  {
469  logger.LogDebug("Entered MonitorLifetimes");
470  var iteration = 1;
471  for(var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
472  {
473  logger.LogDebug("New iteration of monitor loop");
474  try
475  {
476  if(AlphaIsActive)
477  logger.LogDebug("Alpha is the active server");
478  else
479  logger.LogDebug("Bravo is the active server");
480 
481  if(monitorState.InactiveServerHasStagedDmb)
482  logger.LogDebug("Inactive server has staged .dmb");
483  if (monitorState.RebootingInactiveServer)
484  logger.LogDebug("Inactive server is rebooting");
485 
486  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
487  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
488 
489  var activeServerLifetime = monitorState.ActiveServer.Lifetime;
490  var inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
491  var activeServerReboot = monitorState.ActiveServer.OnReboot;
492  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
493  var inactiveServerStartup = monitorState.InactiveServer.LaunchResult;
494  var activeLaunchParametersChanged = activeParametersUpdated.Task;
495  var newDmbAvailable = dmbFactory.OnNewerDmb;
496 
497  var cancelTcs = new TaskCompletionSource<object>();
498  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
499  {
500  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
501  if (monitorState.RebootingInactiveServer)
502  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
503  await toWaitOn.ConfigureAwait(false);
504  cancellationToken.ThrowIfCancellationRequested();
505  }
506 
507  var chatTask = Task.CompletedTask;
508  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
509  {
510  MonitorActivationReason activationReason = default;
511  //multiple things may have happened, handle them one at a time
512  for (var moreActivationsToProcess = true; moreActivationsToProcess && monitorState.NextAction == MonitorAction.Continue; )
513  {
514  if (activeServerLifetime?.IsCompleted == true)
515  {
516  activationReason = MonitorActivationReason.ActiveServerCrashed;
517  activeServerLifetime = null;
518  }
519  else if (inactiveServerLifetime?.IsCompleted == true)
520  {
521  activationReason = MonitorActivationReason.InactiveServerCrashed;
522  inactiveServerLifetime = null;
523  }
524  else if (activeServerReboot?.IsCompleted == true)
525  {
526  activationReason = MonitorActivationReason.ActiveServerRebooted;
527  activeServerReboot = null;
528  }
529  else if (inactiveServerReboot?.IsCompleted == true)
530  {
531  activationReason = MonitorActivationReason.InactiveServerRebooted;
532  inactiveServerReboot = null;
533  }
534  else if (inactiveServerStartup?.IsCompleted == true)
535  {
536  activationReason = MonitorActivationReason.InactiveServerStartupComplete;
537  inactiveServerStartup = null;
538  }
539  else if (newDmbAvailable?.IsCompleted == true)
540  {
541  activationReason = MonitorActivationReason.NewDmbAvailable;
542  newDmbAvailable = null;
543  }
544  else if(activeLaunchParametersChanged?.IsCompleted == true)
545  {
546  activationReason = MonitorActivationReason.ActiveLaunchParametersUpdated;
547  activeLaunchParametersChanged = null;
548  }
549  else
550  moreActivationsToProcess = false;
551  }
552 
553  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
554  //writeback alphaServer and bravoServer
555  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
556  bravoServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
557  }
558 
559  //full reboot required
560  if (monitorState.NextAction == MonitorAction.Restart)
561  {
562  logger.LogDebug("Next state action is to restart");
563  DisposeAndNullControllers();
564  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
565  }
566 
567  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
568  {
569  WatchdogLaunchResult result;
570  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
571  {
572  result = await LaunchNoLock(false, false, false, cancellationToken).ConfigureAwait(false);
573  if (Running)
574  monitorState = new MonitorState(); //clean the slate
575  }
576 
577  await chatTask.ConfigureAwait(false);
578  if(!Running)
579  {
580  logger.LogWarning("Failed to automatically restart the watchdog! Alpha: {0}; Bravo: {1}", result.Alpha.ToString(), result.Bravo.ToString());
581  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); //max of one hour
582  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
583  await Task.WhenAll(Task.Delay((int)retryDelay, cancellationToken), chatTask).ConfigureAwait(false);
584  }
585  }
586  }
587  catch (OperationCanceledException)
588  {
589  logger.LogDebug("Monitor cancelled");
590  break;
591  }
592  catch (Exception e)
593  {
594  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}", iteration, JsonConvert.SerializeObject(monitorState));
595  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);
596  }
597  }
598  }
599 
600  async Task<bool> StopMonitor()
601  {
602  logger.LogTrace("StopMonitor");
603  if (monitorTask == null)
604  return false;
605  monitorCts.Cancel();
606  await monitorTask.ConfigureAwait(false);
607  monitorCts.Dispose();
608  monitorTask = null;
609  return true;
610  }
611 
612 
614  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
615  {
616  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
617  {
618  ActiveLaunchParameters = launchParameters;
619  if (Running)
620  //queue an update
621  activeParametersUpdated.TrySetResult(null);
622  }
623  }
624 
625  async Task<WatchdogLaunchResult> LaunchNoLock(bool startMonitor, bool announce, bool doReattach, CancellationToken cancellationToken)
626  {
627  using (var alphaStartCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
628  {
629  logger.LogTrace("Begin LaunchNoLock");
630  if (Running)
631  {
632  logger.LogTrace("Aborted due to already running!");
633  return null;
634  }
635 
636  Task chatTask;
637  //this is necessary, the monitor could be in it's sleep loop trying to restart
638  if (startMonitor && await StopMonitor().ConfigureAwait(false))
639  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
640  else if (announce)
641  chatTask = chat.SendWatchdogMessage("Starting...", cancellationToken);
642  else
643  chatTask = Task.CompletedTask;
644  //start both servers
645  LastLaunchParameters = ActiveLaunchParameters;
646  try
647  {
648  //good ole sanity
649  if (alphaServer != null || bravoServer != null)
650  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
651 
652  var reattachInfo = doReattach ? await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false) : null;
653  var doesntNeedNewDmb = doReattach && reattachInfo.Alpha != null && reattachInfo.Bravo != null;
654  var dmbToUse = doesntNeedNewDmb ? null : await dmbFactory.LockNextDmb(cancellationToken).ConfigureAwait(false);
655 
656  Task<ISessionController> alphaServerTask = null;
657  try
658  {
659  if (!doReattach || reattachInfo.Alpha == null)
660  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, alphaStartCts.Token);
661  else
662  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
663  //do a few seconds of delay so that any backends the servers use know that alpha came first
664  await Task.Delay(AlphaBravoStartupSeperationInterval, cancellationToken).ConfigureAwait(false);
665  Task<ISessionController> bravoServerTask;
666  if (!doReattach || reattachInfo.Bravo == null)
667  bravoServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken);
668  else
669  bravoServerTask = sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken);
670 
671  await Task.WhenAll(alphaServerTask, bravoServerTask).ConfigureAwait(false);
672 
673  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
674  {
675  var launch = await controller.LaunchResult.ConfigureAwait(false);
676  if (launch.ExitCode.HasValue)
677  //you killed us ray...
678  throw new Exception(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
679  return launch;
680  }
681 
682  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
683  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
684  //now we have two booting servers, get them up and running
685  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
686 
687  //don't forget about the cancelationToken
688  var cancelTcs = new TaskCompletionSource<object>();
689  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
690  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
691  cancellationToken.ThrowIfCancellationRequested();
692 
693  //update the live and staged jobs in the db
694  await databaseContextFactory.UseContext(async db =>
695  {
696  var settings = new Models.DreamDaemonSettings
697  {
698  InstanceId = instance.Id
699  };
700  var cj = (AlphaIsActive ? alphaServer : bravoServer).Dmb.CompileJob;
701  db.CompileJobs.Attach(cj);
702  db.DreamDaemonSettings.Attach(settings);
703  settings.StagedCompileJob = null;
704  settings.ActiveCompileJob = cj;
705  await db.Save(cancellationToken).ConfigureAwait(false);
706  }).ConfigureAwait(false);
707 
708  //both servers are now running, alpha is the active server, huzzah
709  AlphaIsActive = doReattach ? reattachInfo.AlphaIsActive : true;
710  LastLaunchResult = alphaLrt.Result;
711  Running = true;
712 
713  if (startMonitor)
714  {
715  await StopMonitor().ConfigureAwait(false);
716  monitorCts = new CancellationTokenSource();
717  monitorTask = MonitorLifetimes(monitorCts.Token);
718  }
719  return new WatchdogLaunchResult
720  {
721  Alpha = alphaLrt.Result,
722  Bravo = bravoLrt.Result
723  };
724  }
725  catch
726  {
727  if (alphaServer == null && bravoServer == null)
728  dmbToUse.Dispose(); //guaranteed to not be null here
729  DisposeAndNullControllers();
730  throw;
731  }
732  }
733  catch (Exception e)
734  {
735  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
736  throw;
737  }
738  finally
739  {
740  try
741  {
742  await chatTask.ConfigureAwait(false);
743  }
744  catch (OperationCanceledException) { }
745  }
746  }
747  }
748 
750  public async Task<WatchdogLaunchResult> Launch(CancellationToken cancellationToken)
751  {
752  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
753  return await LaunchNoLock(true, true, false, cancellationToken).ConfigureAwait(false);
754  }
755 
757  public async Task<WatchdogLaunchResult> Restart(bool graceful, CancellationToken cancellationToken)
758  {
759  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
760  return await RestartNoLock(graceful, cancellationToken).ConfigureAwait(false);
761  }
762 
764  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
765  {
766  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
767  await TerminateNoLock(graceful, true, cancellationToken).ConfigureAwait(false);
768  }
769 
771  public async Task StartAsync(CancellationToken cancellationToken)
772  {
773  if (autoStart)
774  await LaunchNoLock(true, true, true, cancellationToken).ConfigureAwait(false);
775  }
776 
778  public async Task StopAsync(CancellationToken cancellationToken)
779  {
780  if (releaseServers && Running)
781  {
782  var reattachInformation = new WatchdogReattachInformation
783  {
784  AlphaIsActive = AlphaIsActive
785  };
786  reattachInformation.Alpha = alphaServer?.Release();
787  reattachInformation.Bravo = bravoServer?.Release();
788  await reattachInfoHandler.Save(reattachInformation, cancellationToken).ConfigureAwait(false);
789  }
790  await Terminate(false, cancellationToken).ConfigureAwait(false);
791  }
792 
794  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
795  {
796  string results;
797  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
798  {
799  if (!Running)
800  return true;
801 
802  var builder = new StringBuilder(InteropConstants.DMTopicEvent);
803  foreach (var I in parameters)
804  {
805  builder.Append("&");
806  builder.Append(byondTopicSender.SanitizeString(I));
807  }
808 
809  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
810  results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
811  }
812 
813  if (results == null)
814  return true;
815 
816  List<Response> responses;
817  try
818  {
819  responses = JsonConvert.DeserializeObject<List<Response>>(results);
820  }
821  catch
822  {
823  logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
824  return true;
825  }
826 
827  await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
828 
829  return true;
830  }
831 
833  public async Task<string> HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
834  {
835  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
836  {
837  if (!Running)
838  return "ERROR: Server offline!";
839 
840  var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(InteropConstants.DMTopicChatCommand), byondTopicSender.SanitizeString(InteropConstants.DMParameterData), byondTopicSender.SanitizeString(JsonConvert.SerializeObject(arguments)));
841 
842  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
843  return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
844  }
845  }
846  }
847 }
async Task< WatchdogLaunchResult > Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
Definition: Watchdog.cs:757
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Watchdog
Definition: Watchdog.cs:75
@@ -100,10 +100,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
- +
Represents the result of trying to start a DD process
Definition: LaunchResult.cs:9
-
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Definition: Watchdog.cs:760
+
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Definition: Watchdog.cs:764
bool releaseServers
If the servers should be released instead of shutdown
Definition: Watchdog.cs:129
Consumes EventTypes and takes the appropriate actions
@@ -121,7 +121,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Task monitorTask
The Task running the monitor loop
Definition: Watchdog.cs:110
-
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently Running triggers a graceful restart ...
Definition: Watchdog.cs:610
+
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently Running triggers a graceful restart ...
Definition: Watchdog.cs:614
For managing connected chat services
Definition: IChat.cs:13
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:90
@@ -129,47 +129,47 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
bool IsPrimary
If the IDmbProvider.PrimaryDirectory of Dmb is being used
readonly IEventConsumer eventConsumer
The IEventConsumer for the Watchdog
Definition: Watchdog.cs:85
-
async Task< string > HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
Definition: Watchdog.cs:829
+
async Task< string > HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
Definition: Watchdog.cs:833
void DisposeAndNullControllers()
Call IDisposable.Dispose on alphaServer and bravoServer and set them to
Definition: Watchdog.cs:183
- +
Task< LaunchResult > LaunchResult
A Task that completes when DreamDaemon starts pumping the windows message queue after loading a ...
Definition: ISessionBase.cs:11
-
async Task< WatchdogLaunchResult > RestartNoLock(bool graceful, CancellationToken cancellationToken)
Implementation of Restart(bool, CancellationToken). Does not lock semaphore
Definition: Watchdog.cs:195
+
async Task< WatchdogLaunchResult > RestartNoLock(bool graceful, CancellationToken cancellationToken)
Implementation of Restart(bool, CancellationToken). Does not lock semaphore
Definition: Watchdog.cs:194
readonly Api.Models.Instance instance
The Api.Models.Instance for the Watchdog
Definition: Watchdog.cs:95
- + -
ReattachInformation Alpha
ReattachInformation for the Alpha session
-
async Task MonitorLifetimes(CancellationToken cancellationToken)
The loop that watches the watchdog
Definition: Watchdog.cs:463
+
ReattachInformation Alpha
ReattachInformation for the Alpha session
+
async Task MonitorLifetimes(CancellationToken cancellationToken)
The loop that watches the watchdog
Definition: Watchdog.cs:467
Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerControl serverUpdater, ILogger< Watchdog > logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Construct a IWatchdog
Definition: Watchdog.cs:146
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for the Watchdog
Definition: Watchdog.cs:55
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:774
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:778
Task< bool > SetPort(ushort newPort, CancellationToken cancellatonToken)
Causes the world to start listening on a newPort
ReattachInformation Release()
Releases the ISession without terminating it. Also calls System.IDisposable.Dispose ...
readonly IChat chat
The IChat for the Watchdog
Definition: Watchdog.cs:50
- + - + -
async Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
Definition: Watchdog.cs:790
+
async Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
Definition: Watchdog.cs:794
-
async Task< WatchdogLaunchResult > Launch(CancellationToken cancellationToken)
Start the IWatchdog
Definition: Watchdog.cs:746
+
async Task< WatchdogLaunchResult > Launch(CancellationToken cancellationToken)
Start the IWatchdog
Definition: Watchdog.cs:750
readonly bool autoStart
If the Watchdog should LaunchNoLock(bool, bool, bool, CancellationToken) in StartAsync(CancellationTo...
Definition: Watchdog.cs:100
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:767
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:771
ISessionController alphaServer
Server designation alpha
Definition: Watchdog.cs:120
Handles Commands.ICommands that map to those defined in a IJsonTrackingContext
-
async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
Handles the actions to take when the monitor has to "wake up"
Definition: Watchdog.cs:251
+
async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
Handles the actions to take when the monitor has to "wake up"
Definition: Watchdog.cs:250
readonly IReattachInfoHandler reattachInfoHandler
The IReattachInfoHandler for the Watchdog
Definition: Watchdog.cs:70
Task< string > SendCommand(string command, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
-
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock semaphore
Definition: Watchdog.cs:226
+
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock semaphore
Definition: Watchdog.cs:225
readonly IDmbFactory dmbFactory
The IDmbFactory for the Watchdog
Definition: Watchdog.cs:60
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
Definition: Watchdog.cs:105
void RegisterForRestart(Action action)
Register a given action to run before stopping the server for a restart
@@ -183,13 +183,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
void ResetRebootState()
Changes RebootState to Components.Watchdog.RebootState.Normal without telling the DMAPI ...
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the Watchdog
Definition: Watchdog.cs:80
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
-
async Task< WatchdogLaunchResult > LaunchNoLock(bool startMonitor, bool announce, bool doReattach, CancellationToken cancellationToken)
Definition: Watchdog.cs:621
+
async Task< WatchdogLaunchResult > LaunchNoLock(bool startMonitor, bool announce, bool doReattach, CancellationToken cancellationToken)
Definition: Watchdog.cs:625
- +
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:12
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
- +
+ +
+
+ + + + + + + + + + + + + + + + + + +
async Task Tgstation.Server.Host.Components.Compiler.DmbFactory.CleanUnusedCompileJobs (CompileJob exceptThisOne,
CancellationToken cancellationToken 
)
+
+ +

Deletes all compile jobs that are inactive in the Game folder exceptThisOne

+ +

Implements Tgstation.Server.Host.Components.Compiler.IDmbFactory.

+ +

Definition at line 212 of file DmbFactory.cs.

+ +

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

+
213  {
214  List<long> jobIdsToSkip;
215  //don't clean locked directories
216  lock (this)
217  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
218 
219  List<string> jobUidsToNotErase = null;
220 
221  //find the uids of locked directories
222  await databaseContextFactory.UseContext(async db =>
223  {
224  jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id) && x.DirectoryName.HasValue).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
225  }).ConfigureAwait(false);
226 
227  //add the other exemption
228  if (exceptThisOne != null)
229  jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
230 
231  //cleanup
232  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
233  int deleting = 0;
234  var tasks = directories.Select(async x =>
235  {
236  var nameOnly = ioManager.GetFileName(x);
237  if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
238  return;
239  try
240  {
241  ++deleting;
242  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
243  }
244  catch (Exception e)
245  {
246  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
247  }
248  }).ToList();
249  if (deleting > 0)
250  {
251  logger.LogDebug("Cleaning {0} unused game folders...", deleting);
252  await Task.WhenAll().ConfigureAwait(false);
253  }
254  }
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:36
+
string GetFileName(string path)
Gets the file name portion of a path
+
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory
Definition: DmbFactory.cs:40
+
readonly Api.Models.Instance instance
The Api.Models.Instance for the DmbFactory
Definition: DmbFactory.cs:45
+
Guid DirectoryName
The Game folder the results were compiled into
Definition: CompileJob.cs:33
+
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the DmbFactory
Definition: DmbFactory.cs:32
+
+
+
@@ -294,40 +351,53 @@ Private Attributes
- +
- + - + + + + + + + + + + +
IDmbProvider Tgstation.Server.Host.Components.Compiler.DmbFactory.FromCompileJob async Task<IDmbProvider> Tgstation.Server.Host.Components.Compiler.DmbFactory.FromCompileJob ( CompileJob compileJob)compileJob,
CancellationToken cancellationToken 
)

Gets a IDmbProvider for a given CompileJob

-

Implements Tgstation.Server.Host.Components.Compiler.IDmbFactory.

+

Implements Tgstation.Server.Host.Components.Compiler.IDmbFactory.

-

Definition at line 175 of file DmbFactory.cs.

+

Definition at line 173 of file DmbFactory.cs.

-

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

-
176  {
177  lock (this)
178  {
179  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
180  jobLockCounts.Add(compileJob.Id, 1);
181  else
182  jobLockCounts[compileJob.Id] = ++value;
183  return new DmbProvider(compileJob, ioManager, () => CleanJob(compileJob));
184  }
185  }
- -
readonly IIOManager ioManager
The IIOManager for the DmbFactory
Definition: DmbFactory.cs:35
-
void CleanJob(CompileJob job)
Delete the Api.Models.Internal.CompileJob.DirectoryName of job
Definition: DmbFactory.cs:85
+

References Tgstation.Server.Host.Components.Compiler.DmbProvider.Dispose(), and Tgstation.Server.Api.Models.Internal.CompileJob.Id.

+
174  {
175  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
176  var providerSubmitted = false;
177  var newProvider = new DmbProvider(compileJob, ioManager, () =>
178  {
179  if (providerSubmitted)
180  CleanJob(compileJob);
181  });
182 
183  try
184  {
185  var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
186  var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
187 
188  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
189  {
190  logger.LogWarning("Error loading compile job, .dmb missing!");
191  return null; //omae wa mou shinderu
192  }
193 
194  lock (this)
195  {
196  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
197  jobLockCounts.Add(compileJob.Id, 1);
198  else
199  jobLockCounts[compileJob.Id] = ++value;
200  providerSubmitted = true;
201  return newProvider;
202  }
203  }
204  finally
205  {
206  if (!providerSubmitted)
207  newProvider.Dispose();
208  }
209  }
+ +
readonly IIOManager ioManager
The IIOManager for the DmbFactory
Definition: DmbFactory.cs:36
+
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists
+
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory
Definition: DmbFactory.cs:40
+
void CleanJob(CompileJob job)
Delete the Api.Models.Internal.CompileJob.DirectoryName of job
Definition: DmbFactory.cs:93
+
string ConcatPath(params string[] paths)
Combines an array of strings into a path
- +
- + @@ -350,60 +420,15 @@ Private Attributes

Implements Tgstation.Server.Host.Components.Compiler.ICompileJobConsumer.

- - - -
-
-
Task Tgstation.Server.Host.Components.Compiler.DmbFactory.LoadCompileJob async Task Tgstation.Server.Host.Components.Compiler.DmbFactory.LoadCompileJob ( CompileJob  job,
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - -
async Task Tgstation.Server.Host.Components.Compiler.DmbFactory.LoadCompileJob (CompileJob job,
bool setAsStagedInDb,
CancellationToken cancellationToken 
)
-
-private
-
+

Definition at line 115 of file DmbFactory.cs.

-

Definition at line 106 of file DmbFactory.cs.

- -

References Tgstation.Server.Api.Models.Internal.Job.Cancelled, Tgstation.Server.Api.Models.Internal.CompileJob.DMApiValidated, Tgstation.Server.Api.Models.Internal.Job.ExceptionDetails, Tgstation.Server.Host.Models.DreamDaemonSettings.Id, Tgstation.Server.Host.Models.CompileJob.Job, and Tgstation.Server.Api.Models.Internal.Job.StoppedAt.

-
107  {
108  if (job == null)
109  throw new ArgumentNullException(nameof(job));
110  if (job.DMApiValidated != true || job.Job.Cancelled.Value || job.Job.ExceptionDetails != null || job.Job.StoppedAt == null)
111  throw new InvalidOperationException("Cannot load incomplete compile job!");
112  if (setAsStagedInDb)
113  await databaseContextFactory.UseContext(async db =>
114  {
115  var ddsettings = new DreamDaemonSettings
116  {
117  InstanceId = instance.Id
118  };
119  db.DreamDaemonSettings.Attach(ddsettings);
120  ddsettings.StagedCompileJob = job;
121  await db.Save(cancellationToken).ConfigureAwait(false);
122  }).ConfigureAwait(false);
123  lock (this)
124  {
125  var oldDmbProvider = nextDmbProvider;
126  if (oldDmbProvider != null && oldDmbProvider.CompileJob.Job.StoppedAt < oldDmbProvider.CompileJob.Job.StoppedAt)
127  throw new InvalidOperationException("Loaded compile job older than current job!");
129  newerDmbTcs.SetResult(nextDmbProvider);
130  newerDmbTcs = new TaskCompletionSource<IDmbProvider>();
131  }
132  }
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:57
- -
TaskCompletionSource< IDmbProvider > newerDmbTcs
TaskCompletionSource<TResult> resulting in the latest DmbProvider yet to exist
Definition: DmbFactory.cs:53
+

References Tgstation.Server.Api.Models.Internal.Job.Cancelled, Tgstation.Server.Api.Models.Internal.CompileJob.DMApiValidated, Tgstation.Server.Api.Models.Internal.Job.ExceptionDetails, and Tgstation.Server.Host.Models.CompileJob.Job.

+
116  {
117  if (job == null)
118  throw new ArgumentNullException(nameof(job));
119  if (job.DMApiValidated != true || job.Job.Cancelled == true || job.Job.ExceptionDetails != null)
120  throw new InvalidOperationException("Cannot load incomplete compile job!");
121  var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
122  if (newProvider == null)
123  return;
124  lock (this)
125  {
126  nextDmbProvider?.Dispose();
127  nextDmbProvider = newProvider;
128  newerDmbTcs.SetResult(nextDmbProvider);
129  newerDmbTcs = new TaskCompletionSource<object>();
130  }
131  }
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:62
bool DMApiValidated
If the DMAPI version detected is compatible
Definition: CompileJob.cs:18
-
DateTimeOffset StoppedAt
When the Job stopped
Definition: Job.cs:43
string ExceptionDetails
Details of any exceptions caught during the Job
Definition: Job.cs:30
Job Job
See Api.Models.CompileJob.Job
Definition: CompileJob.cs:12
-
IDmbProvider FromCompileJob(CompileJob compileJob)
Gets a IDmbProvider for a given CompileJob
Definition: DmbFactory.cs:175
-
readonly Api.Models.Instance instance
The Api.Models.Instance for the DmbFactory
Definition: DmbFactory.cs:44
-
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the DmbFactory
Definition: DmbFactory.cs:31
-
Task UseContext(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext
+
async Task< IDmbProvider > FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
Gets a IDmbProvider for a given CompileJob
Definition: DmbFactory.cs:173
+
TaskCompletionSource< object > newerDmbTcs
TaskCompletionSource<TResult> resulting in the latest DmbProvider yet to exist
Definition: DmbFactory.cs:58
bool Cancelled
If the Job was cancelled
Definition: Job.cs:50
@@ -426,9 +451,14 @@ Private Attributes

Implements Tgstation.Server.Host.Components.Compiler.IDmbFactory.

-

Definition at line 135 of file DmbFactory.cs.

-
136  {
137  Task<IDmbProvider> task;
138  lock (this)
139  if (nextDmbProvider != null)
140  return nextDmbProvider;
141  else
142  task = newerDmbTcs.Task;
143 
144  var result = await task.ConfigureAwait(false);
145  //so there's currently a race condition in DreamMakerController where the setting of CompileJob.RevisionInformation and thus IDmbProvider.RevisionInformation can be delayed to after this if someone tries to start the server instantly after compiling
146  //This is a terrible terrible hack to get around that
147  //I'm sorry future me, I can't think of any other way to fix this other than giving DreamMaker an IDatabaseContext or having the controller load the CompileJob
148  await Task.Delay(new TimeSpan(0, 0, 10), cancellationToken).ConfigureAwait(false);
149  return result;
150  }
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:57
-
TaskCompletionSource< IDmbProvider > newerDmbTcs
TaskCompletionSource<TResult> resulting in the latest DmbProvider yet to exist
Definition: DmbFactory.cs:53
+

Definition at line 134 of file DmbFactory.cs.

+ +

References Tgstation.Server.Host.Components.IDmbProvider.CompileJob, and Tgstation.Server.Api.Models.Internal.CompileJob.Id.

+
135  {
136  if (nextDmbProvider == null)
137  {
138  Task task;
139  lock (this)
140  task = newerDmbTcs.Task;
141  await task.ConfigureAwait(false);
142  }
143  lock (this)
144  {
146  return nextDmbProvider;
147  }
148  }
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:62
+
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
+ + +
TaskCompletionSource< object > newerDmbTcs
TaskCompletionSource<TResult> resulting in the latest DmbProvider yet to exist
Definition: DmbFactory.cs:58
@@ -466,9 +496,9 @@ Private Attributes

-

Definition at line 168 of file DmbFactory.cs.

-
169  {
170  using (cancellationToken.Register(() => cleanupCts.Cancel()))
171  await cleanupTask.ConfigureAwait(false);
172  }
Task cleanupTask
Task representing calls to CleanJob(CompileJob)
Definition: DmbFactory.cs:49
-
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask
Definition: DmbFactory.cs:39
+

Definition at line 166 of file DmbFactory.cs.

+
167  {
168  using (cancellationToken.Register(() => cleanupCts.Cancel()))
169  await cleanupTask.ConfigureAwait(false);
170  }
Task cleanupTask
Task representing calls to CleanJob(CompileJob)
Definition: DmbFactory.cs:54
+
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask
Definition: DmbFactory.cs:49
@@ -493,7 +523,7 @@ Private Attributes

The CancellationTokenSource for cleanupTask

-

Definition at line 39 of file DmbFactory.cs.

+

Definition at line 49 of file DmbFactory.cs.

@@ -517,7 +547,7 @@ Private Attributes

Task representing calls to CleanJob(CompileJob)

-

Definition at line 49 of file DmbFactory.cs.

+

Definition at line 54 of file DmbFactory.cs.

@@ -541,7 +571,7 @@ Private Attributes

The IDatabaseContextFactory for the DmbFactory

-

Definition at line 31 of file DmbFactory.cs.

+

Definition at line 32 of file DmbFactory.cs.

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

The Api.Models.Instance for the DmbFactory

-

Definition at line 44 of file DmbFactory.cs.

+

Definition at line 45 of file DmbFactory.cs.

@@ -589,7 +619,7 @@ Private Attributes

The IIOManager for the DmbFactory

-

Definition at line 35 of file DmbFactory.cs.

+

Definition at line 36 of file DmbFactory.cs.

@@ -611,11 +641,11 @@ Private Attributes
-

Definition at line 59 of file DmbFactory.cs.

+

Definition at line 64 of file DmbFactory.cs.

- +
@@ -623,7 +653,31 @@ Private Attributes + + +
- + + +
TaskCompletionSource<IDmbProvider> Tgstation.Server.Host.Components.Compiler.DmbFactory.newerDmbTcsreadonly ILogger<DmbFactory> Tgstation.Server.Host.Components.Compiler.DmbFactory.logger
+
+private
+
+ +

The ILogger for the DmbFactory

+ +

Definition at line 40 of file DmbFactory.cs.

+ +
+
+ +
+
+ + + @@ -635,7 +689,7 @@ Private Attributes

TaskCompletionSource<TResult> resulting in the latest DmbProvider yet to exist

-

Definition at line 53 of file DmbFactory.cs.

+

Definition at line 58 of file DmbFactory.cs.

@@ -659,7 +713,7 @@ Private Attributes

The latest DmbProvider

-

Definition at line 57 of file DmbFactory.cs.

+

Definition at line 62 of file DmbFactory.cs.

@@ -684,7 +738,7 @@ Private Attributes

-

Definition at line 20 of file DmbFactory.cs.

+

Definition at line 21 of file DmbFactory.cs.

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dmb_provider-members.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dmb_provider-members.html index f6685c26c7..2efa52ec5c 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dmb_provider-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dmb_provider-members.html @@ -107,7 +107,6 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
+ + +
TaskCompletionSource<object> Tgstation.Server.Host.Components.Compiler.DmbFactory.newerDmbTcs
PrimaryDirectoryTgstation.Server.Host.Components.Compiler.DmbProvider
RevisionInformationTgstation.Server.Host.Components.Compiler.DmbProvider
SecondaryDirectoryTgstation.Server.Host.Components.Compiler.DmbProvider
~DmbProvider()Tgstation.Server.Host.Components.Compiler.DmbProviderprivate
@@ -123,7 +122,7 @@ Public Member Functions void Dispose ()   void KeepAlive () - Disposing the IDmbProvider won't cause a cleanup of the working directory More...
+ Disposing the IDmbProvider won't cause a cleanup of the working directory More...
 

@@ -156,11 +155,6 @@ Properties

 The CompileJob of the .dmb More...
 
- - - -

-Private Member Functions

 ~DmbProvider ()
 
@@ -218,29 +212,6 @@ Private Attributes
43  {
44  CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob));
45  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
46  this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
47  }
Action onDispose
The Action to run when Dispose is called
Definition: DmbProvider.cs:34
- - - -
-
-

Private Attributes

readonly IIOManager ioManager
- - - - -
- - - - - - - -
Tgstation.Server.Host.Components.Compiler.DmbProvider.~DmbProvider ()
-
-private
-
-

Member Function Documentation

@@ -259,9 +230,8 @@ Private Attributes

-

Definition at line 52 of file DmbProvider.cs.

-
53  {
54  onDispose?.Invoke();
55  GC.SuppressFinalize(this);
56  }
Action onDispose
The Action to run when Dispose is called
Definition: DmbProvider.cs:34
-
+

Referenced by Tgstation.Server.Host.Components.Compiler.DmbFactory.FromCompileJob().

+ @@ -277,7 +247,7 @@ Private Attributes
-

Disposing the IDmbProvider won't cause a cleanup of the working directory

+

Disposing the IDmbProvider won't cause a cleanup of the working directory

Implements Tgstation.Server.Host.Components.IDmbProvider.

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dream_maker-members.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dream_maker-members.html index 5b8b93b4b1..0a6697e942 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dream_maker-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_compiler_1_1_dream_maker-members.html @@ -101,7 +101,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); applicationTgstation.Server.Host.Components.Compiler.DreamMakerprivate BDirectoryNameTgstation.Server.Host.Components.Compiler.DreamMaker byondTgstation.Server.Host.Components.Compiler.DreamMakerprivate - Compile(string projectName, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Compiler.DreamMaker + Compile(string projectName, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Compiler.DreamMaker compileJobConsumerTgstation.Server.Host.Components.Compiler.DreamMakerprivate configurationTgstation.Server.Host.Components.Compiler.DreamMakerprivate DmbExtensionTgstation.Server.Host.Components.Compiler.DreamMaker @@ -114,7 +114,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Compiler.DreamMakerprivate sessionControllerFactoryTgstation.Server.Host.Components.Compiler.DreamMakerprivate StatusTgstation.Server.Host.Components.Compiler.DreamMaker - VerifyApi(uint timeout, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Compiler.DreamMakerprivate + VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Compiler.DreamMakerprivate