From 55e056d63ef85b8883ea01efec4ba58af8b2314e Mon Sep 17 00:00:00 2001 From: tgstation-server Date: Mon, 8 Jun 2020 18:52:36 +0000 Subject: [PATCH] Deploy code docs to GitHub Pages for Travis build 3461 Commit: e5d80e232bb12d030f966c22c1eebd7ed679acf6 --- _administration_controller_8cs_source.html | 2 +- _api_controller_8cs_source.html | 15 +- _application_8cs_source.html | 10 +- _basic_watchdog_8cs_source.html | 6 +- _bridge_controller_8cs_source.html | 12 +- _byond_controller_8cs_source.html | 6 +- _chat_controller_8cs_source.html | 8 +- _chat_manager_8cs_source.html | 78 ++-- _chat_manager_factory_8cs_source.html | 2 +- _claims_injector_8cs_source.html | 2 +- ...ts_2_watchdog_2_i_watchdog_8cs_source.html | 3 +- ...atchdog_2_watchdog_factory_8cs_source.html | 2 +- _configuration_controller_8cs_source.html | 4 +- _dead_session_controller_8cs_source.html | 3 +- _dream_daemon_controller_8cs_source.html | 6 +- _dream_maker_client_8cs_source.html | 2 +- _dream_maker_controller_8cs_source.html | 7 +- _entity_id_8cs.html | 2 +- _entity_id_8cs_source.html | 2 +- _experimental_watchdog_8cs_source.html | 7 +- _home_controller_8cs_source.html | 2 +- _i_bridge_handler_8cs_source.html | 5 +- _i_bridge_registrar_8cs_source.html | 2 +- _i_dream_maker_client_8cs_source.html | 2 +- _i_instance_8cs_source.html | 3 +- _i_jobs_client_8cs_source.html | 2 +- _i_rename_notifyee_8cs.html | 122 +++++ _i_rename_notifyee_8cs_source.html | 109 +++++ _i_session_controller_8cs_source.html | 3 +- _instance_controller_8cs_source.html | 4 +- _instance_factory_8cs_source.html | 2 +- _instance_manager_8cs_source.html | 60 +-- _instance_manager_client_8cs_source.html | 4 +- _instance_user_controller_8cs_source.html | 6 +- _job_controller_8cs_source.html | 6 +- _job_manager_8cs_source.html | 32 +- _jobs_client_8cs_source.html | 2 +- _repository_controller_8cs_source.html | 6 +- ...vice_collection_extensions_8cs_source.html | 2 +- _session_controller_8cs_source.html | 66 +-- _session_controller_factory_8cs_source.html | 10 +- ..._8_api_2_models_2_instance_8cs_source.html | 3 +- ...s_2_internal_2_compile_job_8cs_source.html | 2 +- ..._2_models_2_internal_2_job_8cs_source.html | 2 +- ...2_deployment_2_dream_maker_8cs_source.html | 4 +- ...st_2_components_2_instance_8cs_source.html | 39 +- _user_controller_8cs_source.html | 2 +- _watchdog_base_8cs_source.html | 76 ++-- _windows_watchdog_factory_8cs_source.html | 2 +- annotated.html | 3 +- ...on_1_1_server_1_1_api_1_1_api_headers.html | 24 +- ...er_1_1_api_1_1_models_1_1_compile_job.html | 8 +- ..._1_models_1_1_compile_job__coll__graph.map | 2 +- ..._1_models_1_1_compile_job__coll__graph.md5 | 2 +- ...models_1_1_compile_job__inherit__graph.map | 2 +- ...models_1_1_compile_job__inherit__graph.md5 | 2 +- ...rver_1_1_api_1_1_models_1_1_entity_id.html | 20 +- ...1_models_1_1_entity_id__inherit__graph.map | 14 +- ...1_models_1_1_entity_id__inherit__graph.md5 | 2 +- ...1_models_1_1_entity_id__inherit__graph.png | Bin 22353 -> 31811 bytes ...1_api_1_1_models_1_1_instance-members.html | 2 +- ...erver_1_1_api_1_1_models_1_1_instance.html | 58 +-- ...i_1_1_models_1_1_instance__coll__graph.map | 3 + ...i_1_1_models_1_1_instance__coll__graph.md5 | 1 + ...i_1_1_models_1_1_instance__coll__graph.png | Bin 0 -> 4082 bytes ..._1_models_1_1_instance__inherit__graph.map | 3 +- ..._1_models_1_1_instance__inherit__graph.md5 | 2 +- ..._1_models_1_1_instance__inherit__graph.png | Bin 5181 -> 6366 bytes ..._1_1_models_1_1_internal_1_1_chat_bot.html | 4 +- ...1_models_1_1_internal_1_1_compile_job.html | 4 +- ..._internal_1_1_compile_job__coll__graph.map | 2 +- ..._internal_1_1_compile_job__coll__graph.md5 | 2 +- ...ternal_1_1_compile_job__inherit__graph.map | 2 +- ...ternal_1_1_compile_job__inherit__graph.md5 | 2 +- ...al_1_1_dream_daemon_launch_parameters.html | 2 +- ...1_api_1_1_models_1_1_internal_1_1_job.html | 14 +- ...dels_1_1_internal_1_1_job__coll__graph.map | 2 +- ...dels_1_1_internal_1_1_job__coll__graph.md5 | 2 +- ...s_1_1_internal_1_1_job__inherit__graph.map | 2 +- ...s_1_1_internal_1_1_job__inherit__graph.md5 | 2 +- ...1_1_internal_1_1_revision_information.html | 4 +- ..._api_1_1_models_1_1_internal_1_1_user.html | 2 +- ...1_1_server_1_1_api_1_1_models_1_1_job.html | 4 +- ..._1_api_1_1_models_1_1_job__coll__graph.map | 2 +- ..._1_api_1_1_models_1_1_job__coll__graph.md5 | 2 +- ...api_1_1_models_1_1_job__inherit__graph.map | 2 +- ...api_1_1_models_1_1_job__inherit__graph.md5 | 2 +- ..._server_1_1_api_1_1_models_1_1_limits.html | 2 +- ...i_1_1_models_1_1_revision_information.html | 2 +- ..._1_1_server_1_1_client_1_1_api_client.html | 2 +- ...lient_1_1_components_1_1_byond_client.html | 5 +- ...mponents_1_1_byond_client__coll__graph.map | 5 +- ...mponents_1_1_byond_client__coll__graph.md5 | 2 +- ...mponents_1_1_byond_client__coll__graph.png | Bin 15096 -> 15996 bytes ...t_1_1_components_1_1_chat_bots_client.html | 5 +- ...ents_1_1_chat_bots_client__coll__graph.map | 5 +- ...ents_1_1_chat_bots_client__coll__graph.md5 | 2 +- ...ents_1_1_chat_bots_client__coll__graph.png | Bin 15308 -> 16233 bytes ...1_components_1_1_configuration_client.html | 9 +- ..._1_1_configuration_client__coll__graph.map | 5 +- ..._1_1_configuration_client__coll__graph.md5 | 2 +- ..._1_1_configuration_client__coll__graph.png | Bin 16123 -> 17030 bytes ..._1_components_1_1_dream_daemon_client.html | 5 +- ...s_1_1_dream_daemon_client__coll__graph.map | 5 +- ...s_1_1_dream_daemon_client__coll__graph.md5 | 2 +- ...s_1_1_dream_daemon_client__coll__graph.png | Bin 15988 -> 16922 bytes ...1_1_components_1_1_dream_maker_client.html | 5 +- ...ts_1_1_dream_maker_client__coll__graph.map | 5 +- ...ts_1_1_dream_maker_client__coll__graph.md5 | 2 +- ...ts_1_1_dream_maker_client__coll__graph.png | Bin 16080 -> 16998 bytes ...1_components_1_1_instance_user_client.html | 5 +- ..._1_1_instance_user_client__coll__graph.map | 5 +- ..._1_1_instance_user_client__coll__graph.md5 | 2 +- ..._1_1_instance_user_client__coll__graph.png | Bin 15980 -> 16894 bytes ...client_1_1_components_1_1_jobs_client.html | 5 +- ...omponents_1_1_jobs_client__coll__graph.map | 5 +- ...omponents_1_1_jobs_client__coll__graph.md5 | 2 +- ...omponents_1_1_jobs_client__coll__graph.png | Bin 14738 -> 15625 bytes ..._1_1_components_1_1_repository_client.html | 5 +- ...nts_1_1_repository_client__coll__graph.map | 5 +- ...nts_1_1_repository_client__coll__graph.md5 | 2 +- ...nts_1_1_repository_client__coll__graph.png | Bin 16044 -> 16984 bytes ..._1_client_1_1_instance_manager_client.html | 8 +- ...omponents_1_1_byond_1_1_byond_manager.html | 4 +- ...mponents_1_1_chat_1_1_channel_mapping.html | 2 +- ...s_1_1_chat_1_1_channel_representation.html | 12 +- ...nts_1_1_chat_1_1_chat_manager-members.html | 31 +- ..._components_1_1_chat_1_1_chat_manager.html | 252 ++++++----- ...1_1_chat_1_1_chat_manager__coll__graph.md5 | 2 +- ...1_1_chat_1_1_chat_manager__coll__graph.png | Bin 187109 -> 171774 bytes ...1_1_components_1_1_chat_1_1_chat_user.html | 4 +- ...1_chat_1_1_commands_1_1_byond_command.html | 13 +- ...ommands_1_1_byond_command__coll__graph.map | 13 +- ...ommands_1_1_byond_command__coll__graph.md5 | 2 +- ...ommands_1_1_byond_command__coll__graph.png | Bin 35557 -> 39072 bytes ...chat_1_1_commands_1_1_command_factory.html | 19 +- ...mands_1_1_command_factory__coll__graph.map | 19 +- ...mands_1_1_command_factory__coll__graph.md5 | 2 +- ...mands_1_1_command_factory__coll__graph.png | Bin 59982 -> 61953 bytes ..._1_commands_1_1_pull_requests_command.html | 15 +- ...1_1_pull_requests_command__coll__graph.map | 15 +- ...1_1_pull_requests_command__coll__graph.md5 | 2 +- ...1_1_pull_requests_command__coll__graph.png | Bin 45287 -> 48024 bytes ...hat_1_1_commands_1_1_revision_command.html | 13 +- ...ands_1_1_revision_command__coll__graph.map | 13 +- ...ands_1_1_revision_command__coll__graph.md5 | 2 +- ...ands_1_1_revision_command__coll__graph.png | Bin 34511 -> 37967 bytes ...ts_1_1_chat_1_1_providers_1_1_message.html | 8 +- ...onents_1_1_deployment_1_1_dmb_factory.html | 4 +- ...onents_1_1_deployment_1_1_dream_maker.html | 4 +- ...nt_1_1_windows_swappable_dmb_provider.html | 2 +- ...ws_swappable_dmb_provider__coll__graph.map | 2 +- ...ws_swappable_dmb_provider__coll__graph.md5 | 2 +- ...ponents_1_1_events_1_1_event_consumer.html | 11 +- ...events_1_1_event_consumer__coll__graph.map | 11 +- ...events_1_1_event_consumer__coll__graph.md5 | 2 +- ...events_1_1_event_consumer__coll__graph.png | Bin 28754 -> 27550 bytes ...t_1_1_components_1_1_instance-members.html | 10 +- ..._1_1_host_1_1_components_1_1_instance.html | 201 +++++---- ...1_components_1_1_instance__coll__graph.map | 17 +- ...1_components_1_1_instance__coll__graph.md5 | 2 +- ...1_components_1_1_instance__coll__graph.png | Bin 83627 -> 82176 bytes ...omponents_1_1_instance__inherit__graph.map | 5 +- ...omponents_1_1_instance__inherit__graph.md5 | 2 +- ...omponents_1_1_instance__inherit__graph.png | Bin 9688 -> 14484 bytes ...t_1_1_components_1_1_instance_manager.html | 136 +++--- ...erop_1_1_bridge_1_1_bridge_parameters.html | 12 +- ...mponents_1_1_interop_1_1_chat_message.html | 4 +- ...nts_1_1_interop_1_1_d_m_api_constants.html | 4 +- ...ts_1_1_interop_1_1_d_m_api_parameters.html | 2 +- ...nterop_1_1_topic_1_1_topic_parameters.html | 2 +- ...ponents_1_1_repository_1_1_repository.html | 2 +- ...1_1_repository_1_1_repository_manager.html | 4 +- ...n_1_1_dead_session_controller-members.html | 35 +- ...1_session_1_1_dead_session_controller.html | 49 +- ...1_dead_session_controller__coll__graph.map | 7 +- ...1_dead_session_controller__coll__graph.md5 | 2 +- ...1_dead_session_controller__coll__graph.png | Bin 30441 -> 33318 bytes ...ead_session_controller__inherit__graph.map | 7 +- ...ead_session_controller__inherit__graph.md5 | 2 +- ...ead_session_controller__inherit__graph.png | Bin 7443 -> 9438 bytes ..._1_1_session_1_1_reattach_information.html | 4 +- ...ession_1_1_session_controller-members.html | 57 +-- ...ts_1_1_session_1_1_session_controller.html | 346 +++++++------- ...on_1_1_session_controller__coll__graph.map | 33 +- ...on_1_1_session_controller__coll__graph.md5 | 2 +- ...on_1_1_session_controller__coll__graph.png | Bin 187304 -> 201251 bytes ...1_1_session_controller__inherit__graph.map | 13 +- ...1_1_session_controller__inherit__graph.md5 | 2 +- ...1_1_session_controller__inherit__graph.png | Bin 13418 -> 14450 bytes ...797bf868ec451ab1cb9ad75eb9b55a9_cgraph.map | 4 + ...797bf868ec451ab1cb9ad75eb9b55a9_cgraph.md5 | 1 + ...797bf868ec451ab1cb9ad75eb9b55a9_cgraph.png | Bin 0 -> 10299 bytes ...ession_1_1_session_controller_factory.html | 24 +- ...1_watchdog_1_1_basic_watchdog-members.html | 37 +- ...nents_1_1_watchdog_1_1_basic_watchdog.html | 113 +++-- ...tchdog_1_1_basic_watchdog__coll__graph.map | 27 +- ...tchdog_1_1_basic_watchdog__coll__graph.md5 | 2 +- ...tchdog_1_1_basic_watchdog__coll__graph.png | Bin 135896 -> 147482 bytes ...dog_1_1_basic_watchdog__inherit__graph.map | 11 +- ...dog_1_1_basic_watchdog__inherit__graph.md5 | 2 +- ...dog_1_1_basic_watchdog__inherit__graph.png | Bin 23372 -> 22381 bytes ...dog_1_1_experimental_watchdog-members.html | 31 +- ..._1_watchdog_1_1_experimental_watchdog.html | 130 ++++-- ...1_1_experimental_watchdog__coll__graph.map | 31 +- ...1_1_experimental_watchdog__coll__graph.md5 | 2 +- ...1_1_experimental_watchdog__coll__graph.png | Bin 151483 -> 181142 bytes ..._experimental_watchdog__inherit__graph.map | 9 +- ..._experimental_watchdog__inherit__graph.md5 | 2 +- ..._experimental_watchdog__inherit__graph.png | Bin 19137 -> 18791 bytes ...a11de6335f98af3c29f4eb33ddbaa90_cgraph.map | 15 +- ...a11de6335f98af3c29f4eb33ddbaa90_cgraph.md5 | 2 +- ...a11de6335f98af3c29f4eb33ddbaa90_cgraph.png | Bin 66964 -> 67135 bytes ...onents_1_1_watchdog_1_1_monitor_state.html | 4 +- ..._1_watchdog_1_1_watchdog_base-members.html | 53 +-- ...onents_1_1_watchdog_1_1_watchdog_base.html | 385 +++++++++------- ...atchdog_1_1_watchdog_base__coll__graph.map | 25 +- ...atchdog_1_1_watchdog_base__coll__graph.md5 | 2 +- ...atchdog_1_1_watchdog_base__coll__graph.png | Bin 109516 -> 119573 bytes ...hdog_1_1_watchdog_base__inherit__graph.map | 19 +- ...hdog_1_1_watchdog_base__inherit__graph.md5 | 2 +- ...hdog_1_1_watchdog_base__inherit__graph.png | Bin 25039 -> 32899 bytes ...nts_1_1_watchdog_1_1_watchdog_factory.html | 4 +- ...watchdog_1_1_windows_watchdog-members.html | 45 +- ...nts_1_1_watchdog_1_1_windows_watchdog.html | 71 +-- ...hdog_1_1_windows_watchdog__coll__graph.map | 45 +- ...hdog_1_1_windows_watchdog__coll__graph.md5 | 2 +- ...hdog_1_1_windows_watchdog__coll__graph.png | Bin 233479 -> 217117 bytes ...g_1_1_windows_watchdog__inherit__graph.map | 11 +- ...g_1_1_windows_watchdog__inherit__graph.md5 | 2 +- ...g_1_1_windows_watchdog__inherit__graph.png | Bin 21172 -> 23840 bytes ...nfiguration_1_1_general_configuration.html | 2 +- ...rollers_1_1_administration_controller.html | 8 +- ...st_1_1_controllers_1_1_api_controller.html | 40 +- ...rollers_1_1_bridge_controller-members.html | 3 +- ...1_1_controllers_1_1_bridge_controller.html | 66 ++- ...ers_1_1_bridge_controller__coll__graph.md5 | 2 +- ...ers_1_1_bridge_controller__coll__graph.png | Bin 16320 -> 23498 bytes ..._1_1_controllers_1_1_byond_controller.html | 8 +- ...t_1_1_controllers_1_1_chat_controller.html | 20 +- ...trollers_1_1_configuration_controller.html | 10 +- ...ntrollers_1_1_dream_daemon_controller.html | 14 +- ...ontrollers_1_1_dream_maker_controller.html | 25 +- ...t_1_1_controllers_1_1_home_controller.html | 6 +- ...1_controllers_1_1_instance_controller.html | 12 +- ...trollers_1_1_instance_user_controller.html | 30 +- ...st_1_1_controllers_1_1_job_controller.html | 30 +- ...controllers_1_1_repository_controller.html | 32 +- ...ver_1_1_host_1_1_core_1_1_application.html | 14 +- ...r_1_1_host_1_1_core_1_1_async_delayer.html | 2 +- ...t_1_1_core_1_1_semaphore_slim_context.html | 2 +- ...service_collection_extensions-members.html | 5 +- ...ons_1_1_service_collection_extensions.html | 37 +- ...ice_collection_extensions__coll__graph.map | 2 + ...ice_collection_extensions__coll__graph.md5 | 1 + ...ice_collection_extensions__coll__graph.png | Bin 0 -> 5782 bytes ...r_1_1_host_1_1_jobs_1_1_job_exception.html | 2 +- ...ver_1_1_host_1_1_jobs_1_1_job_handler.html | 4 +- ...ver_1_1_host_1_1_jobs_1_1_job_manager.html | 80 ++-- ...rver_1_1_host_1_1_models_1_1_chat_bot.html | 4 +- ...r_1_1_host_1_1_models_1_1_compile_job.html | 4 +- ..._1_models_1_1_compile_job__coll__graph.map | 2 +- ..._1_models_1_1_compile_job__coll__graph.md5 | 2 +- ...models_1_1_compile_job__inherit__graph.map | 2 +- ...models_1_1_compile_job__inherit__graph.md5 | 2 +- ..._1_1_models_1_1_dream_daemon_settings.html | 4 +- ...t_1_1_models_1_1_dream_maker_settings.html | 4 +- ..._models_1_1_dual_reattach_information.html | 4 +- ..._host_1_1_models_1_1_instance-members.html | 2 +- ...rver_1_1_host_1_1_models_1_1_instance.html | 13 +- ...t_1_1_models_1_1_instance__coll__graph.map | 3 +- ...t_1_1_models_1_1_instance__coll__graph.md5 | 2 +- ...t_1_1_models_1_1_instance__coll__graph.png | Bin 8180 -> 8794 bytes ..._1_models_1_1_instance__inherit__graph.map | 3 +- ..._1_models_1_1_instance__inherit__graph.md5 | 2 +- ..._1_models_1_1_instance__inherit__graph.png | Bin 4627 -> 5366 bytes ...1_1_host_1_1_models_1_1_instance_user.html | 4 +- ..._1_server_1_1_host_1_1_models_1_1_job.html | 10 +- ...1_host_1_1_models_1_1_job__coll__graph.map | 2 +- ...1_host_1_1_models_1_1_job__coll__graph.md5 | 2 +- ...ost_1_1_models_1_1_job__inherit__graph.map | 2 +- ...ost_1_1_models_1_1_job__inherit__graph.md5 | 2 +- ..._models_1_1_reattach_information_base.html | 2 +- ...st_1_1_models_1_1_repository_settings.html | 4 +- ...t_1_1_models_1_1_revision_information.html | 8 +- ...1_server_1_1_host_1_1_models_1_1_user.html | 2 +- ...1_security_1_1_authentication_context.html | 4 +- ...host_1_1_security_1_1_claims_injector.html | 2 +- classes.html | 109 ++--- dir_7a3fd032a1362a6005a1fc9227a8dd86.html | 2 + files.html | 1 + functions_b.html | 2 +- functions_func_b.html | 2 +- functions_func_i.html | 11 +- functions_func_r.html | 10 +- functions_func_s.html | 2 +- functions_i.html | 10 +- functions_m.html | 4 + functions_n.html | 4 +- functions_prop_i.html | 1 - functions_r.html | 15 +- functions_s.html | 5 +- functions_t.html | 4 +- functions_vars_m.html | 4 + functions_vars_r.html | 3 + functions_vars_s.html | 3 + hierarchy.html | 74 +-- inherit_graph_100.map | 9 +- inherit_graph_100.md5 | 2 +- inherit_graph_100.png | Bin 3193 -> 29363 bytes inherit_graph_101.map | 9 +- inherit_graph_101.md5 | 2 +- inherit_graph_101.png | Bin 29363 -> 3363 bytes inherit_graph_102.map | 4 +- inherit_graph_102.md5 | 2 +- inherit_graph_102.png | Bin 3363 -> 2901 bytes inherit_graph_103.map | 3 +- inherit_graph_103.md5 | 2 +- inherit_graph_103.png | Bin 2901 -> 2691 bytes inherit_graph_104.map | 2 +- inherit_graph_104.md5 | 2 +- inherit_graph_104.png | Bin 2691 -> 2162 bytes inherit_graph_105.map | 2 +- inherit_graph_105.md5 | 2 +- inherit_graph_105.png | Bin 2162 -> 2496 bytes inherit_graph_106.map | 2 +- inherit_graph_106.md5 | 2 +- inherit_graph_106.png | Bin 2496 -> 2334 bytes inherit_graph_107.map | 2 +- inherit_graph_107.md5 | 2 +- inherit_graph_107.png | Bin 2334 -> 2398 bytes inherit_graph_108.map | 2 +- inherit_graph_108.md5 | 2 +- inherit_graph_108.png | Bin 2398 -> 2169 bytes inherit_graph_109.map | 2 +- inherit_graph_109.md5 | 2 +- inherit_graph_109.png | Bin 2169 -> 2680 bytes inherit_graph_110.map | 4 +- inherit_graph_110.md5 | 2 +- inherit_graph_110.png | Bin 2680 -> 4677 bytes inherit_graph_111.map | 6 +- inherit_graph_111.md5 | 2 +- inherit_graph_111.png | Bin 4677 -> 8392 bytes inherit_graph_112.map | 6 +- inherit_graph_112.md5 | 2 +- inherit_graph_112.png | Bin 8392 -> 10113 bytes inherit_graph_113.map | 5 +- inherit_graph_113.md5 | 2 +- inherit_graph_113.png | Bin 10113 -> 3519 bytes inherit_graph_114.map | 4 +- inherit_graph_114.md5 | 2 +- inherit_graph_114.png | Bin 3519 -> 3129 bytes inherit_graph_115.map | 5 +- inherit_graph_115.md5 | 2 +- inherit_graph_115.png | Bin 3129 -> 10657 bytes inherit_graph_116.map | 4 +- inherit_graph_116.md5 | 2 +- inherit_graph_116.png | Bin 10657 -> 2391 bytes inherit_graph_117.map | 2 +- inherit_graph_117.md5 | 2 +- inherit_graph_117.png | Bin 2391 -> 2213 bytes inherit_graph_118.map | 2 +- inherit_graph_118.md5 | 2 +- inherit_graph_118.png | Bin 2213 -> 1747 bytes inherit_graph_119.map | 3 +- inherit_graph_119.md5 | 2 +- inherit_graph_119.png | Bin 1747 -> 3370 bytes inherit_graph_120.map | 4 +- inherit_graph_120.md5 | 2 +- inherit_graph_120.png | Bin 3370 -> 3538 bytes inherit_graph_121.map | 5 +- inherit_graph_121.md5 | 2 +- inherit_graph_121.png | Bin 3538 -> 8918 bytes inherit_graph_122.map | 5 +- inherit_graph_122.md5 | 2 +- inherit_graph_122.png | Bin 8918 -> 3220 bytes inherit_graph_123.map | 3 +- inherit_graph_123.md5 | 2 +- inherit_graph_123.png | Bin 3220 -> 2202 bytes inherit_graph_124.map | 3 +- inherit_graph_124.md5 | 2 +- inherit_graph_124.png | Bin 2202 -> 2766 bytes inherit_graph_125.map | 4 +- inherit_graph_125.md5 | 2 +- inherit_graph_125.png | Bin 2766 -> 3189 bytes inherit_graph_126.map | 4 +- inherit_graph_126.md5 | 2 +- inherit_graph_126.png | Bin 3189 -> 3556 bytes inherit_graph_127.map | 4 +- inherit_graph_127.md5 | 2 +- inherit_graph_127.png | Bin 3556 -> 3337 bytes inherit_graph_128.map | 4 +- inherit_graph_128.md5 | 2 +- inherit_graph_128.png | Bin 3337 -> 3216 bytes inherit_graph_129.map | 5 +- inherit_graph_129.md5 | 2 +- inherit_graph_129.png | Bin 3216 -> 8455 bytes inherit_graph_130.map | 5 +- inherit_graph_130.md5 | 2 +- inherit_graph_130.png | Bin 8455 -> 3078 bytes inherit_graph_131.map | 4 +- inherit_graph_131.md5 | 2 +- inherit_graph_131.png | Bin 3078 -> 3539 bytes inherit_graph_17.map | 16 +- inherit_graph_17.md5 | 2 +- inherit_graph_17.png | Bin 22057 -> 29598 bytes inherit_graph_20.map | 4 +- inherit_graph_20.md5 | 2 +- inherit_graph_20.png | Bin 3478 -> 3613 bytes inherit_graph_21.map | 5 +- inherit_graph_21.md5 | 2 +- inherit_graph_21.png | Bin 3613 -> 9633 bytes inherit_graph_22.map | 6 +- inherit_graph_22.md5 | 2 +- inherit_graph_22.png | Bin 9633 -> 9087 bytes inherit_graph_23.map | 7 +- inherit_graph_23.md5 | 2 +- inherit_graph_23.png | Bin 9087 -> 11600 bytes inherit_graph_24.map | 7 +- inherit_graph_24.md5 | 2 +- inherit_graph_24.png | Bin 11600 -> 10057 bytes inherit_graph_25.map | 6 +- inherit_graph_25.md5 | 2 +- inherit_graph_25.png | Bin 10057 -> 9960 bytes inherit_graph_26.map | 6 +- inherit_graph_26.md5 | 2 +- inherit_graph_26.png | Bin 9960 -> 9844 bytes inherit_graph_27.map | 7 +- inherit_graph_27.md5 | 2 +- inherit_graph_27.png | Bin 9844 -> 11181 bytes inherit_graph_28.map | 5 +- inherit_graph_28.md5 | 2 +- inherit_graph_28.png | Bin 11181 -> 2300 bytes inherit_graph_29.map | 7 +- inherit_graph_29.md5 | 2 +- inherit_graph_29.png | Bin 2300 -> 16777 bytes inherit_graph_30.map | 7 +- inherit_graph_30.md5 | 2 +- inherit_graph_30.png | Bin 16777 -> 2380 bytes inherit_graph_31.map | 2 +- inherit_graph_31.md5 | 2 +- inherit_graph_31.png | Bin 2380 -> 2260 bytes inherit_graph_32.map | 2 +- inherit_graph_32.md5 | 2 +- inherit_graph_32.png | Bin 2260 -> 1899 bytes inherit_graph_33.map | 2 +- inherit_graph_33.md5 | 2 +- inherit_graph_33.png | Bin 1899 -> 2302 bytes inherit_graph_34.map | 15 +- inherit_graph_34.md5 | 2 +- inherit_graph_34.png | Bin 2302 -> 61653 bytes inherit_graph_35.map | 16 +- inherit_graph_35.md5 | 2 +- inherit_graph_35.png | Bin 61653 -> 3200 bytes inherit_graph_36.map | 4 +- inherit_graph_36.md5 | 2 +- inherit_graph_36.png | Bin 3200 -> 3032 bytes inherit_graph_37.map | 4 +- inherit_graph_37.md5 | 2 +- inherit_graph_37.png | Bin 3032 -> 3275 bytes inherit_graph_38.map | 4 +- inherit_graph_38.md5 | 2 +- inherit_graph_38.png | Bin 3275 -> 3294 bytes inherit_graph_39.map | 4 +- inherit_graph_39.md5 | 2 +- inherit_graph_39.png | Bin 3294 -> 3441 bytes inherit_graph_40.map | 4 +- inherit_graph_40.md5 | 2 +- inherit_graph_40.png | Bin 3441 -> 3424 bytes inherit_graph_41.map | 4 +- inherit_graph_41.md5 | 2 +- inherit_graph_41.png | Bin 3424 -> 3121 bytes inherit_graph_42.map | 4 +- inherit_graph_42.md5 | 2 +- inherit_graph_42.png | Bin 3121 -> 3282 bytes inherit_graph_43.map | 4 +- inherit_graph_43.md5 | 2 +- inherit_graph_43.png | Bin 3282 -> 3703 bytes inherit_graph_44.map | 4 +- inherit_graph_44.md5 | 2 +- inherit_graph_44.png | Bin 3703 -> 2998 bytes inherit_graph_45.map | 4 +- inherit_graph_45.md5 | 2 +- inherit_graph_45.png | Bin 2998 -> 3198 bytes inherit_graph_46.map | 4 +- inherit_graph_46.md5 | 2 +- inherit_graph_46.png | Bin 3198 -> 2901 bytes inherit_graph_47.map | 3 +- inherit_graph_47.md5 | 2 +- inherit_graph_47.png | Bin 2901 -> 2275 bytes inherit_graph_48.map | 3 +- inherit_graph_48.md5 | 2 +- inherit_graph_48.png | Bin 2275 -> 2904 bytes inherit_graph_49.map | 4 +- inherit_graph_49.md5 | 2 +- inherit_graph_49.png | Bin 2904 -> 2762 bytes inherit_graph_50.map | 3 +- inherit_graph_50.md5 | 2 +- inherit_graph_50.png | Bin 2762 -> 2535 bytes inherit_graph_51.map | 2 +- inherit_graph_51.md5 | 2 +- inherit_graph_51.png | Bin 2535 -> 2414 bytes inherit_graph_52.map | 2 +- inherit_graph_52.md5 | 2 +- inherit_graph_52.png | Bin 2414 -> 2455 bytes inherit_graph_53.map | 8 +- inherit_graph_53.md5 | 2 +- inherit_graph_53.png | Bin 2455 -> 36927 bytes inherit_graph_54.map | 9 +- inherit_graph_54.md5 | 2 +- inherit_graph_54.png | Bin 36927 -> 3482 bytes inherit_graph_55.map | 4 +- inherit_graph_55.md5 | 2 +- inherit_graph_55.png | Bin 3482 -> 3545 bytes inherit_graph_56.map | 4 +- inherit_graph_56.md5 | 2 +- inherit_graph_56.png | Bin 3545 -> 3499 bytes inherit_graph_57.map | 3 +- inherit_graph_57.md5 | 2 +- inherit_graph_57.png | Bin 3499 -> 2611 bytes inherit_graph_58.map | 3 +- inherit_graph_58.md5 | 2 +- inherit_graph_58.png | Bin 2611 -> 3621 bytes inherit_graph_59.map | 3 +- inherit_graph_59.md5 | 2 +- inherit_graph_59.png | Bin 3621 -> 2443 bytes inherit_graph_60.map | 3 +- inherit_graph_60.md5 | 2 +- inherit_graph_60.png | Bin 2443 -> 4082 bytes inherit_graph_61.map | 3 +- inherit_graph_61.md5 | 2 +- inherit_graph_61.png | Bin 4082 -> 2743 bytes inherit_graph_62.map | 7 +- inherit_graph_62.md5 | 2 +- inherit_graph_62.png | Bin 2743 -> 23979 bytes inherit_graph_63.map | 9 +- inherit_graph_63.md5 | 2 +- inherit_graph_63.png | Bin 23979 -> 8944 bytes inherit_graph_64.map | 4 +- inherit_graph_64.md5 | 2 +- inherit_graph_64.png | Bin 8944 -> 2583 bytes inherit_graph_65.map | 2 +- inherit_graph_65.md5 | 2 +- inherit_graph_65.png | Bin 2583 -> 2864 bytes inherit_graph_66.map | 4 +- inherit_graph_66.md5 | 2 +- inherit_graph_66.png | Bin 2864 -> 6130 bytes inherit_graph_67.map | 5 +- inherit_graph_67.md5 | 2 +- inherit_graph_67.png | Bin 6130 -> 3742 bytes inherit_graph_68.map | 3 +- inherit_graph_68.md5 | 2 +- inherit_graph_68.png | Bin 3742 -> 2589 bytes inherit_graph_69.map | 3 +- inherit_graph_69.md5 | 2 +- inherit_graph_69.png | Bin 2589 -> 3346 bytes inherit_graph_70.map | 4 +- inherit_graph_70.md5 | 2 +- inherit_graph_70.png | Bin 3346 -> 3364 bytes inherit_graph_71.map | 3 +- inherit_graph_71.md5 | 2 +- inherit_graph_71.png | Bin 3364 -> 2580 bytes inherit_graph_72.map | 2 +- inherit_graph_72.md5 | 2 +- inherit_graph_72.png | Bin 2580 -> 2830 bytes inherit_graph_73.map | 4 +- inherit_graph_73.md5 | 2 +- inherit_graph_73.png | Bin 2830 -> 5765 bytes inherit_graph_74.map | 4 +- inherit_graph_74.md5 | 2 +- inherit_graph_74.png | Bin 5765 -> 3009 bytes inherit_graph_75.map | 2 +- inherit_graph_75.md5 | 2 +- inherit_graph_75.png | Bin 3009 -> 2543 bytes inherit_graph_76.map | 2 +- inherit_graph_76.md5 | 2 +- inherit_graph_76.png | Bin 2543 -> 2381 bytes inherit_graph_77.map | 2 +- inherit_graph_77.md5 | 2 +- inherit_graph_77.png | Bin 2381 -> 2478 bytes inherit_graph_78.map | 2 +- inherit_graph_78.md5 | 2 +- inherit_graph_78.png | Bin 2478 -> 2545 bytes inherit_graph_79.map | 2 +- inherit_graph_79.md5 | 2 +- inherit_graph_79.png | Bin 2545 -> 2279 bytes inherit_graph_80.map | 3 +- inherit_graph_80.md5 | 2 +- inherit_graph_80.png | Bin 2279 -> 3310 bytes inherit_graph_81.map | 4 +- inherit_graph_81.md5 | 2 +- inherit_graph_81.png | Bin 3310 -> 3351 bytes inherit_graph_82.map | 91 +++- inherit_graph_82.md5 | 2 +- inherit_graph_82.png | Bin 3351 -> 574900 bytes inherit_graph_83.map | 91 +--- inherit_graph_83.md5 | 2 +- inherit_graph_83.png | Bin 540561 -> 6993 bytes inherit_graph_84.map | 5 +- inherit_graph_84.md5 | 2 +- inherit_graph_84.png | Bin 6993 -> 2797 bytes inherit_graph_85.map | 3 +- inherit_graph_85.md5 | 2 +- inherit_graph_85.png | Bin 2797 -> 3442 bytes inherit_graph_86.map | 2 +- inherit_graph_86.md5 | 2 +- inherit_graph_86.png | Bin 3442 -> 2761 bytes inherit_graph_87.map | 2 +- inherit_graph_87.md5 | 2 +- inherit_graph_87.png | Bin 2761 -> 2730 bytes inherit_graph_88.map | 2 +- inherit_graph_88.md5 | 2 +- inherit_graph_88.png | Bin 2730 -> 2817 bytes inherit_graph_89.map | 2 +- inherit_graph_89.md5 | 2 +- inherit_graph_89.png | Bin 2817 -> 3009 bytes inherit_graph_90.map | 2 +- inherit_graph_90.md5 | 2 +- inherit_graph_90.png | Bin 3009 -> 3166 bytes inherit_graph_91.map | 2 +- inherit_graph_91.md5 | 2 +- inherit_graph_91.png | Bin 3166 -> 3142 bytes inherit_graph_92.map | 2 +- inherit_graph_92.md5 | 2 +- inherit_graph_92.png | Bin 3142 -> 2661 bytes inherit_graph_93.map | 2 +- inherit_graph_93.md5 | 2 +- inherit_graph_93.png | Bin 2661 -> 2813 bytes inherit_graph_94.map | 2 +- inherit_graph_94.md5 | 2 +- inherit_graph_94.png | Bin 2813 -> 2664 bytes inherit_graph_95.map | 2 +- inherit_graph_95.md5 | 2 +- inherit_graph_95.png | Bin 2664 -> 3238 bytes inherit_graph_96.map | 2 +- inherit_graph_96.md5 | 2 +- inherit_graph_96.png | Bin 3238 -> 3128 bytes inherit_graph_97.map | 2 +- inherit_graph_97.md5 | 2 +- inherit_graph_97.png | Bin 3128 -> 3001 bytes inherit_graph_98.map | 2 +- inherit_graph_98.md5 | 2 +- inherit_graph_98.png | Bin 3001 -> 2698 bytes inherit_graph_99.map | 3 +- inherit_graph_99.md5 | 2 +- inherit_graph_99.png | Bin 2698 -> 3193 bytes inherits.html | 421 +++++++++--------- ..._1_server_1_1_client_1_1_i_api_client.html | 16 +- ..._client_1_1_i_instance_manager_client.html | 2 +- ...omponents_1_1_chat_1_1_i_chat_manager.html | 2 +- ..._1_1_chat_1_1_i_chat_tracking_context.html | 8 +- ...d4bf60a56058a56804994741cceff5_icgraph.map | 2 +- ...d4bf60a56058a56804994741cceff5_icgraph.md5 | 2 +- ...1_1_chat_1_1_providers_1_1_i_provider.html | 12 +- ...nts_1_1_deployment_1_1_i_dmb_provider.html | 2 +- ...1_1_components_1_1_i_instance-members.html | 4 +- ..._1_host_1_1_components_1_1_i_instance.html | 41 +- ...components_1_1_i_instance__coll__graph.map | 3 +- ...components_1_1_i_instance__coll__graph.md5 | 2 +- ...components_1_1_i_instance__coll__graph.png | Bin 8494 -> 12359 bytes ...ponents_1_1_i_instance__inherit__graph.map | 5 +- ...ponents_1_1_i_instance__inherit__graph.md5 | 2 +- ...ponents_1_1_i_instance__inherit__graph.png | Bin 10666 -> 16090 bytes ...1_1_components_1_1_i_instance_manager.html | 2 +- ...ponents_1_1_i_rename_notifyee-members.html | 112 +++++ ..._1_1_components_1_1_i_rename_notifyee.html | 196 ++++++++ ..._1_1_i_rename_notifyee__inherit__graph.map | 12 + ..._1_1_i_rename_notifyee__inherit__graph.md5 | 1 + ..._1_1_i_rename_notifyee__inherit__graph.png | Bin 0 -> 40760 bytes ...fed1472e47f53392237645d5d65d7a_icgraph.map | 3 + ...fed1472e47f53392237645d5d65d7a_icgraph.md5 | 1 + ...fed1472e47f53392237645d5d65d7a_icgraph.png | Bin 0 -> 6739 bytes ...op_1_1_bridge_1_1_i_bridge_dispatcher.html | 2 +- ...1_bridge_1_1_i_bridge_handler-members.html | 3 +- ...terop_1_1_bridge_1_1_i_bridge_handler.html | 65 +-- ...rop_1_1_bridge_1_1_i_bridge_registrar.html | 4 +- ...6d6072e7a96ece183b77544c9bfe4b_icgraph.map | 2 +- ...6d6072e7a96ece183b77544c9bfe4b_icgraph.md5 | 2 +- ...sion_1_1_i_session_controller-members.html | 35 +- ..._1_1_session_1_1_i_session_controller.html | 24 +- ..._1_1_i_session_controller__coll__graph.map | 5 +- ..._1_1_i_session_controller__coll__graph.md5 | 2 +- ..._1_1_i_session_controller__coll__graph.png | Bin 5605 -> 8092 bytes ...1_i_session_controller__inherit__graph.map | 9 +- ...1_i_session_controller__inherit__graph.md5 | 2 +- ...1_i_session_controller__inherit__graph.png | Bin 12910 -> 14381 bytes ...s_1_1_watchdog_1_1_i_watchdog-members.html | 15 +- ...omponents_1_1_watchdog_1_1_i_watchdog.html | 20 +- ...1_watchdog_1_1_i_watchdog__coll__graph.map | 1 + ...1_watchdog_1_1_i_watchdog__coll__graph.md5 | 2 +- ...1_watchdog_1_1_i_watchdog__coll__graph.png | Bin 9054 -> 11745 bytes ...atchdog_1_1_i_watchdog__inherit__graph.map | 15 +- ...atchdog_1_1_i_watchdog__inherit__graph.md5 | 2 +- ...atchdog_1_1_i_watchdog__inherit__graph.png | Bin 25126 -> 25239 bytes ..._1_host_1_1_core_1_1_i_server_control.html | 8 +- ..._1_1_i_authentication_context_factory.html | 4 +- ...m_1_1_i_assembly_information_provider.html | 2 +- ..._1_host_1_1_system_1_1_i_process_base.html | 2 +- ...station_1_1_server_1_1_api_1_1_models.html | 2 +- ...on_1_1_server_1_1_host_1_1_components.html | 3 + search/all_10.js | 12 +- search/all_12.js | 17 +- search/all_13.js | 9 +- search/all_14.js | 10 +- search/all_15.js | 2 +- search/all_17.js | 4 +- search/all_2.js | 4 +- search/all_3.js | 2 +- search/all_9.js | 18 +- search/all_a.js | 4 +- search/all_d.js | 5 +- search/all_e.js | 4 +- search/classes_10.js | 4 +- search/classes_12.js | 2 +- search/classes_13.js | 2 +- search/classes_8.js | 3 +- search/classes_9.js | 2 +- search/classes_f.js | 2 +- search/files_9.js | 5 +- search/functions_1.js | 2 +- search/functions_10.js | 1 - search/functions_11.js | 2 +- search/functions_8.js | 2 +- search/properties_8.js | 2 +- search/variables_11.js | 1 + search/variables_12.js | 1 + search/variables_13.js | 2 +- search/variables_c.js | 3 +- search/variables_f.js | 2 +- 729 files changed, 3747 insertions(+), 2784 deletions(-) create mode 100644 _i_rename_notifyee_8cs.html create mode 100644 _i_rename_notifyee_8cs_source.html create mode 100644 class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.map create mode 100644 class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_components_1_1_session_1_1_session_controller_a8797bf868ec451ab1cb9ad75eb9b55a9_cgraph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_components_1_1_session_1_1_session_controller_a8797bf868ec451ab1cb9ad75eb9b55a9_cgraph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_components_1_1_session_1_1_session_controller_a8797bf868ec451ab1cb9ad75eb9b55a9_cgraph.png create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_extensions_1_1_service_collection_extensions__coll__graph.map create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_extensions_1_1_service_collection_extensions__coll__graph.md5 create mode 100644 class_tgstation_1_1_server_1_1_host_1_1_extensions_1_1_service_collection_extensions__coll__graph.png create mode 100644 interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_i_rename_notifyee-members.html create mode 100644 interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_i_rename_notifyee.html create mode 100644 interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_i_rename_notifyee__inherit__graph.map create mode 100644 interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_i_rename_notifyee__inherit__graph.md5 create mode 100644 interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_i_rename_notifyee__inherit__graph.png create mode 100644 interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_i_rename_notifyee_a69fed1472e47f53392237645d5d65d7a_icgraph.map create mode 100644 interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_i_rename_notifyee_a69fed1472e47f53392237645d5d65d7a_icgraph.md5 create mode 100644 interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_i_rename_notifyee_a69fed1472e47f53392237645d5d65d7a_icgraph.png diff --git a/_administration_controller_8cs_source.html b/_administration_controller_8cs_source.html index 215a95372b..3d9ce213ed 100644 --- a/_administration_controller_8cs_source.html +++ b/_administration_controller_8cs_source.html @@ -121,7 +121,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task< IActionResult > Update([FromBody] Administration model, CancellationToken cancellationToken)
Attempt to perform a server upgrade.
async Task< IActionResult > CheckReleasesAndApplyUpdate(Version newVersion, CancellationToken cancellationToken)
Try to download and apply an update with a given newVersion .
-
A Controller for API functions
+
A Controller for API functions
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the AdministrationController
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the AdministrationController
diff --git a/_api_controller_8cs_source.html b/_api_controller_8cs_source.html index 42bd8bd80d..e5a49c2439 100644 --- a/_api_controller_8cs_source.html +++ b/_api_controller_8cs_source.html @@ -94,15 +94,16 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.AspNetCore.Mvc.Filters;
4 using Microsoft.Extensions.Logging;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Controllers
16 {
20  [Produces(ApiHeaders.ApplicationJson)]
21  [ApiController]
22  public abstract class ApiController : Controller
23  {
27  protected ApiHeaders ApiHeaders { get; private set; }
28 
32  protected IDatabaseContext DatabaseContext { get; }
33 
38 
42  protected ILogger Logger { get; }
43 
47  protected Models.Instance Instance { get; }
48 
52  readonly bool requireInstance;
53 
57  readonly bool requireHeaders;
58 
67  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
68  {
69  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
70  if (authenticationContextFactory == null)
71  throw new ArgumentNullException(nameof(authenticationContextFactory));
72  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
75  this.requireInstance = requireInstance;
76  this.requireHeaders = requireHeaders;
77  }
78 
80  #pragma warning disable CA1506 // TODO: Decomplexify
81  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
82  {
83  // ALL valid token and login requests that match a route go through this function
84  // 404 is returned before
85  if (AuthenticationContext != null && AuthenticationContext.User == null)
86  {
87  // valid token, expired password
88  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
89  return;
90  }
91 
92  // validate the headers
93  try
94  {
95  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
96 
97  if (!ApiHeaders.Compatible())
98  {
99  await StatusCode(
100  (int)HttpStatusCode.UpgradeRequired,
101  new ErrorMessage(ErrorCode.ApiMismatch))
102  .ExecuteResultAsync(context)
103  .ConfigureAwait(false);
104  return;
105  }
106 
107  if (requireInstance)
108  {
109  if (!ApiHeaders.InstanceId.HasValue)
110  {
111  await BadRequest(new ErrorMessage(ErrorCode.InstanceHeaderRequired)).ExecuteResultAsync(context).ConfigureAwait(false);
112  return;
113  }
114 
116  {
117  // accessing an instance they don't have access to or one that's disabled
118  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
119  return;
120  }
121  }
122  }
123  catch (InvalidOperationException e)
124  {
125  if (requireHeaders)
126  {
127  await BadRequest(
128  new ErrorMessage(ErrorCode.BadHeaders)
129  {
130  AdditionalData = e.Message
131  })
132  .ExecuteResultAsync(context)
133  .ConfigureAwait(false);
134  return;
135  }
136  }
137 
138  if (ModelState?.IsValid == false)
139  {
140  var errorMessages = ModelState
141  .SelectMany(x => x.Value.Errors)
142  .Select(x => x.ErrorMessage)
143 
144  // We use RequiredAttributes purely for preventing properties from becoming nullable in the databases
145  // We validate missing required fields in controllers
146  // Unfortunately, we can't remove the whole validator for that as it checks other things like StringLength
147  // This is the best way to deal with it unfortunately
148  .Where(x => !x.EndsWith(" field is required.", StringComparison.Ordinal));
149 
150  if (errorMessages.Any())
151  {
152  await BadRequest(
153  new ErrorMessage(ErrorCode.ModelValidationFailure)
154  {
155  AdditionalData = String.Join(Environment.NewLine, errorMessages)
156  })
157  .ExecuteResultAsync(context).ConfigureAwait(false);
158  return;
159  }
160 
161  ModelState.Clear();
162  }
163 
164  if (ApiHeaders != null)
165  Logger.LogDebug(
166  "Request details: User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}",
167  AuthenticationContext?.User.Id.Value.ToString(CultureInfo.InvariantCulture),
168  ApiHeaders.ApiVersion.Semver(),
170  Request.Method,
171  Request.Path,
172  Request.QueryString,
174 
175  try
176  {
177  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
178  }
179  catch (OperationCanceledException e)
180  {
181  Logger.LogDebug("Request cancelled! Exception: {0}", e);
182  throw;
183  }
184  }
185  #pragma warning restore CA1506
186  }
187 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.AspNetCore.Mvc.Filters;
4 using Microsoft.Extensions.Logging;
5 using Serilog.Context;
6 using System;
7 using System.Globalization;
8 using System.Linq;
9 using System.Net;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Produces(ApiHeaders.ApplicationJson)]
22  [ApiController]
23  public abstract class ApiController : Controller
24  {
28  protected ApiHeaders ApiHeaders { get; private set; }
29 
33  protected IDatabaseContext DatabaseContext { get; }
34 
39 
43  protected ILogger Logger { get; }
44 
48  protected Models.Instance Instance { get; }
49 
53  readonly bool requireInstance;
54 
58  readonly bool requireHeaders;
59 
68  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
69  {
70  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
71  if (authenticationContextFactory == null)
72  throw new ArgumentNullException(nameof(authenticationContextFactory));
73  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
74  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
76  this.requireInstance = requireInstance;
77  this.requireHeaders = requireHeaders;
78  }
79 
81  #pragma warning disable CA1506 // TODO: Decomplexify
82  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
83  {
84  // ALL valid token and login requests that match a route go through this function
85  // 404 is returned before
86  if (AuthenticationContext != null && AuthenticationContext.User == null)
87  {
88  // valid token, expired password
89  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
90  return;
91  }
92 
93  // validate the headers
94  try
95  {
96  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
97 
98  if (!ApiHeaders.Compatible())
99  {
100  await StatusCode(
101  (int)HttpStatusCode.UpgradeRequired,
102  new ErrorMessage(ErrorCode.ApiMismatch))
103  .ExecuteResultAsync(context)
104  .ConfigureAwait(false);
105  return;
106  }
107 
108  if (requireInstance)
109  {
110  if (!ApiHeaders.InstanceId.HasValue)
111  {
112  await BadRequest(new ErrorMessage(ErrorCode.InstanceHeaderRequired)).ExecuteResultAsync(context).ConfigureAwait(false);
113  return;
114  }
115 
117  {
118  // accessing an instance they don't have access to or one that's disabled
119  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
120  return;
121  }
122  }
123  }
124  catch (InvalidOperationException e)
125  {
126  if (requireHeaders)
127  {
128  await BadRequest(
129  new ErrorMessage(ErrorCode.BadHeaders)
130  {
131  AdditionalData = e.Message
132  })
133  .ExecuteResultAsync(context)
134  .ConfigureAwait(false);
135  return;
136  }
137  }
138 
139  if (ModelState?.IsValid == false)
140  {
141  var errorMessages = ModelState
142  .SelectMany(x => x.Value.Errors)
143  .Select(x => x.ErrorMessage)
144 
145  // We use RequiredAttributes purely for preventing properties from becoming nullable in the databases
146  // We validate missing required fields in controllers
147  // Unfortunately, we can't remove the whole validator for that as it checks other things like StringLength
148  // This is the best way to deal with it unfortunately
149  .Where(x => !x.EndsWith(" field is required.", StringComparison.Ordinal));
150 
151  if (errorMessages.Any())
152  {
153  await BadRequest(
154  new ErrorMessage(ErrorCode.ModelValidationFailure)
155  {
156  AdditionalData = String.Join(Environment.NewLine, errorMessages)
157  })
158  .ExecuteResultAsync(context).ConfigureAwait(false);
159  return;
160  }
161 
162  ModelState.Clear();
163  }
164 
165  using (ApiHeaders?.InstanceId != null
166  ? LogContext.PushProperty("Instance", ApiHeaders.InstanceId)
167  : null)
168  using (AuthenticationContext != null
169  ? LogContext.PushProperty("User", AuthenticationContext.User.Id)
170  : null)
171  using (LogContext.PushProperty("Request", $"{Request.Method} {Request.Path}"))
172  {
173  if (ApiHeaders != null)
174  Logger.LogDebug(
175  "Starting API Request: Version: {1}. User-Agent: {2}",
176  AuthenticationContext?.User.Id.Value.ToString(CultureInfo.InvariantCulture),
177  ApiHeaders.ApiVersion.Semver(),
179  Request.Method,
180  Request.Path,
181  Request.QueryString,
183  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
184  }
185  }
186  #pragma warning restore CA1506
187  }
188 }
-
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
Construct an ApiController
+
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
Construct an ApiController
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
-
override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+
override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
const string ApplicationJson
TODO: Remove this when we upgrade to .NET Standard 2.1
Definition: ApiHeaders.cs:22
+
Version ApiVersion
The client&#39;s API version
Definition: ApiHeaders.cs:82
bool Compatible()
Checks if the ApiVersion is compatible with Version
Represents the currently authenticated Api.Models.User
@@ -112,17 +113,17 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
string RawUserAgent
The client&#39;s raw user agent
Definition: ApiHeaders.cs:77
-
readonly bool requireHeaders
If ApiHeaders are required
+
readonly bool requireHeaders
If ApiHeaders are required
long Id
The ID of the User
Definition: User.cs:16
-
readonly bool requireInstance
If IAuthenticationContext.InstanceUser permissions are required to access the ApiController ...
+
readonly bool requireInstance
If IAuthenticationContext.InstanceUser permissions are required to access the ApiController ...
Metadata about a server instance
Definition: Instance.cs:9
IAuthenticationContext CurrentAuthenticationContext
The IAuthenticationContext the IAuthenticationContextFactory created
-
long InstanceId
The Models.Instance.Id being accessed
Definition: ApiHeaders.cs:67
+
long InstanceId
The instance Models.EntityId.Id being accessed
Definition: ApiHeaders.cs:67
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
diff --git a/_application_8cs_source.html b/_application_8cs_source.html index b324e0d22e..ae9860909c 100644 --- a/_application_8cs_source.html +++ b/_application_8cs_source.html @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Application.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Cyberboss.AspNetCore.AsyncInitializer;
3 using Microsoft.AspNetCore.Authentication.JwtBearer;
4 using Microsoft.AspNetCore.Builder;
5 using Microsoft.AspNetCore.Cors.Infrastructure;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.AspNetCore.Identity;
8 using Microsoft.Extensions.Configuration;
9 using Microsoft.Extensions.DependencyInjection;
10 using Microsoft.Extensions.Hosting;
11 using Microsoft.Extensions.Logging;
12 using Microsoft.Extensions.Options;
13 using Microsoft.Extensions.Primitives;
14 using Newtonsoft.Json;
15 using Serilog;
16 using Serilog.Events;
17 using Serilog.Formatting.Display;
18 using System;
19 using System.Globalization;
20 using System.IdentityModel.Tokens.Jwt;
21 using System.Threading.Tasks;
22 using Tgstation.Server.Api;
35 using Tgstation.Server.Host.IO;
40 
41 namespace Tgstation.Server.Host.Core
42 {
46 #pragma warning disable CA1506
48  {
52  readonly IWebHostEnvironment hostingEnvironment;
53 
58 
64  public Application(
65  IConfiguration configuration,
66  IWebHostEnvironment hostingEnvironment)
67  : base(configuration)
68  {
69  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
70  }
71 
76  public static IServerFactory CreateDefaultServerFactory()
77  => new ServerFactory(
79  IOManager);
80 
86  public void ConfigureServices(IServiceCollection services, IPostSetupServices postSetupServices)
87  {
88  ConfigureServices(services);
89 
90  if (postSetupServices == null)
91  throw new ArgumentNullException(nameof(postSetupServices));
92 
93  // configure configuration
94  services.UseStandardConfig<UpdatesConfiguration>(Configuration);
95  services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
96 
97  // enable options which give us config reloading
98  services.AddOptions();
99 
100  static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) =>
101  logLevel switch
102  {
103  LogLevel.Critical => LogEventLevel.Fatal,
104  LogLevel.Debug => LogEventLevel.Debug,
105  LogLevel.Error => LogEventLevel.Error,
106  LogLevel.Information => LogEventLevel.Information,
107  LogLevel.Trace => LogEventLevel.Verbose,
108  LogLevel.Warning => LogEventLevel.Warning,
109  LogLevel.None => null,
110  _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel)),
111  };
112 
113  var microsoftEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.MicrosoftLogLevel);
114  services.SetupLogging(
115  config =>
116  {
117  if (microsoftEventLevel.HasValue)
118  config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
119  },
120  sinkConfig =>
121  {
122  if (postSetupServices.FileLoggingConfiguration.Disable)
123  return;
124 
125  // common app data is C:/ProgramData on windows, else /usr/share
126  var logPath = !String.IsNullOrEmpty(postSetupServices.FileLoggingConfiguration.Directory)
127  ? postSetupServices.FileLoggingConfiguration.Directory
128  : IOManager.ConcatPath(
129  Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
131  "Logs");
132 
133  var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
134 
135  var formatter = new MessageTemplateTextFormatter(
136  "{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
137  null);
138 
139  logPath = IOManager.ConcatPath(logPath, "tgs-{Date}.log");
140  var rollingFileConfig = sinkConfig.File(
141  formatter,
142  logPath,
143  logEventLevel ?? LogEventLevel.Verbose,
144  50 * 1024 * 1024, // 50MB max size
145  flushToDiskInterval: TimeSpan.FromSeconds(2),
146  rollingInterval: RollingInterval.Day,
147  rollOnFileSizeLimit: true);
148  });
149 
150  // configure bearer token validation
151  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
152  {
153  // this line isn't actually run until the first request is made
154  // at that point tokenFactory will be populated
155  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
156  jwtBearerOptions.Events = new JwtBearerEvents
157  {
158  // Application is our composition root so this monstrosity of a line is okay
159  // At least, that's what I tell myself to sleep at night
160  OnTokenValidated = ctx => ctx
161  .HttpContext
162  .RequestServices
163  .GetRequiredService<IClaimsInjector>()
164  .InjectClaimsIntoContext(
165  ctx,
166  ctx.HttpContext.RequestAborted)
167  };
168  });
169 
170  // fucking prevents converting 'sub' to M$ bs
171  // can't be done in the above lambda, that's too late
172  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
173 
174  // add mvc, configure the json serializer settings
175  services
176  .AddMvc(options =>
177  {
178  options.EnableEndpointRouting = false;
179  })
180  .AddNewtonsoftJson(options =>
181  {
182  options.AllowInputFormatterExceptionMessages = true;
183  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
184  options.SerializerSettings.CheckAdditionalContent = true;
185  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
186  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
187  options.SerializerSettings.Converters = new[] { new VersionConverter() };
188  });
189 
190  if (hostingEnvironment.IsDevelopment())
191  {
192  static string GetDocumentationFilePath(string assemblyLocation) => IOManager.ConcatPath(IOManager.GetDirectoryName(assemblyLocation), String.Concat(IOManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
193  var assemblyDocumentationPath = GetDocumentationFilePath(typeof(Application).Assembly.Location);
194  var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location);
195  services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath));
196  services.AddSwaggerGenNewtonsoftSupport();
197  }
198 
199  // enable browser detection
200  services.AddDetectionCore().AddBrowser();
201 
202  // CORS conditionally enabled later
203  services.AddCors();
204 
205  void AddTypedContext<TContext>() where TContext : DatabaseContext
206  {
207  services.AddDbContext<TContext>(builder =>
208  {
209  if (hostingEnvironment.IsDevelopment())
210  builder.EnableSensitiveDataLogging();
211  });
212  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
213  }
214 
215  // add the correct database context type
216  var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
217  switch (dbType)
218  {
219  case DatabaseType.MySql:
220  case DatabaseType.MariaDB:
221  AddTypedContext<MySqlDatabaseContext>();
222  break;
223  case DatabaseType.SqlServer:
224  AddTypedContext<SqlServerDatabaseContext>();
225  break;
226  case DatabaseType.Sqlite:
227  AddTypedContext<SqliteDatabaseContext>();
228  break;
229  case DatabaseType.PostgresSql:
230  AddTypedContext<PostgresSqlDatabaseContext>();
231  break;
232  default:
233  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
234  }
235 
236  // configure other database services
237  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
238  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
239 
240  // configure security services
242  services.AddScoped<IClaimsInjector, ClaimsInjector>();
243  services.AddSingleton<IIdentityCache, IdentityCache>();
244  services.AddSingleton<ICryptographySuite, CryptographySuite>();
245  services.AddSingleton<ITokenFactory, TokenFactory>();
246  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
247 
248  // configure platform specific services
249  if (postSetupServices.PlatformIdentifier.IsWindows)
250  {
251  if (postSetupServices.GeneralConfiguration.UseBasicWatchdogOnWindows)
252  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
253  else
254  services.AddSingleton<IWatchdogFactory, WindowsWatchdogFactory>();
255 
256  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
257  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
258  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
259  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
260  services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
261 
262  services.AddSingleton<WindowsNetworkPromptReaper>();
263  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
264  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
265  }
266  else
267  {
268  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
269  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
270  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
271  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
272  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
273  services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
274  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
275  }
276 
277  // configure misc services
278  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
279  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
280  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
281  services.AddSingleton<IServerPortProvider, ServerPortProivder>();
282  services.AddSingleton<ITopicClient, TopicClient>();
283  services.AddSingleton(new SocketParameters
284  {
285  ReceiveTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
286  SendTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
287  ConnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
288  DisconnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout)
289  });
290 
291  // configure component services
292  services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
293  services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
294  services.AddSingleton<IProviderFactory, ProviderFactory>();
295  services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
296  services.AddSingleton<IInstanceFactory, InstanceFactory>();
297 
298  // configure root services
299  services.AddSingleton<IJobManager, JobManager>();
300 
301  services.AddSingleton<InstanceManager>();
302  services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
303  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
304  }
305 
307  protected override void ConfigureHostedService(IServiceCollection services)
308  => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
309 
320  public void Configure(
321  IApplicationBuilder applicationBuilder,
322  IServerControl serverControl,
323  ITokenFactory tokenFactory,
324  IInstanceManager instanceManager,
325  IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
326  IOptions<GeneralConfiguration> generalConfigurationOptions,
327  ILogger<Application> logger)
328  {
329  if (applicationBuilder == null)
330  throw new ArgumentNullException(nameof(applicationBuilder));
331  if (serverControl == null)
332  throw new ArgumentNullException(nameof(serverControl));
333 
334  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
335 
336  var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
337  var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
338 
339  if (logger == null)
340  throw new ArgumentNullException(nameof(logger));
341 
342  logger.LogDebug("Content Root: {0}", hostingEnvironment.ContentRootPath);
343  logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
344 
345  if (generalConfiguration.MinimumPasswordLength > Limits.MaximumStringLength)
346  {
347  logger.LogCritical("Configured minimum password length ({0}) is greater than the maximum database string length ({1})!");
348  serverControl.Die(new InvalidOperationException("Minimum password length greater than database limit!"));
349  return;
350  }
351 
352  // attempt to restart the server if the configuration changes
353  if (serverControl.WatchdogPresent)
354  ChangeToken.OnChange(Configuration.GetReloadToken, () => serverControl.Restart());
355 
356  // setup the HTTP request pipeline
357  // Final point where we wrap exceptions in a 500 (ErrorMessage) response
358  applicationBuilder.UseServerErrorHandling();
359 
360  // 503 requests made while the application is starting
361  applicationBuilder.UseAsyncInitialization(async (cancellationToken) =>
362  {
363  var tcs = new TaskCompletionSource<object>();
364  using (cancellationToken.Register(() => tcs.SetCanceled()))
365  await Task.WhenAny(tcs.Task, instanceManager.Ready).ConfigureAwait(false);
366  });
367 
368  // suppress OperationCancelledExceptions, they are just aborted HTTP requests
369  applicationBuilder.UseCancelledRequestSuppression();
370 
371  if (hostingEnvironment.IsDevelopment())
372  {
373  applicationBuilder.UseSwagger();
374  applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4"));
375  logger.LogTrace("Swagger API generation enabled");
376  }
377 
378  // Set up CORS based on configuration if necessary
379  Action<CorsPolicyBuilder> corsBuilder = null;
380  if (controlPanelConfiguration.AllowAnyOrigin)
381  {
382  logger.LogTrace("Access-Control-Allow-Origin: *");
383  corsBuilder = builder => builder.AllowAnyOrigin();
384  }
385  else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
386  {
387  logger.LogTrace("Access-Control-Allow-Origin: ", String.Join(',', controlPanelConfiguration.AllowedOrigins));
388  corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
389  }
390 
391  var originalBuilder = corsBuilder;
392  corsBuilder = builder =>
393  {
394  builder.AllowAnyHeader().AllowAnyMethod();
395  originalBuilder?.Invoke(builder);
396  };
397  applicationBuilder.UseCors(corsBuilder);
398 
399  // spa loading if necessary
400  if (controlPanelConfiguration.Enable)
401  {
402  logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
403  applicationBuilder.UseStaticFiles();
404  }
405  else
406  logger.LogDebug("Web control panel disabled!");
407 
408  // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
409  applicationBuilder.UseAuthentication();
410 
411  // suppress and log database exceptions
412  applicationBuilder.UseDbConflictHandling();
413 
414  // majority of handling is done in the controllers
415  applicationBuilder.UseMvc();
416 
417  // 404 anything that gets this far
418  }
419  }
420 }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Cyberboss.AspNetCore.AsyncInitializer;
3 using Microsoft.AspNetCore.Authentication.JwtBearer;
4 using Microsoft.AspNetCore.Builder;
5 using Microsoft.AspNetCore.Cors.Infrastructure;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.AspNetCore.Identity;
8 using Microsoft.Extensions.Configuration;
9 using Microsoft.Extensions.DependencyInjection;
10 using Microsoft.Extensions.Hosting;
11 using Microsoft.Extensions.Logging;
12 using Microsoft.Extensions.Options;
13 using Microsoft.Extensions.Primitives;
14 using Newtonsoft.Json;
15 using Serilog;
16 using Serilog.Events;
17 using Serilog.Formatting.Display;
18 using System;
19 using System.Globalization;
20 using System.IdentityModel.Tokens.Jwt;
21 using System.Threading.Tasks;
22 using Tgstation.Server.Api;
35 using Tgstation.Server.Host.IO;
40 
41 namespace Tgstation.Server.Host.Core
42 {
46 #pragma warning disable CA1506
48  {
52  readonly IWebHostEnvironment hostingEnvironment;
53 
58 
64  public Application(
65  IConfiguration configuration,
66  IWebHostEnvironment hostingEnvironment)
67  : base(configuration)
68  {
69  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
70  }
71 
76  public static IServerFactory CreateDefaultServerFactory()
77  => new ServerFactory(
79  IOManager);
80 
86  public void ConfigureServices(IServiceCollection services, IPostSetupServices postSetupServices)
87  {
88  ConfigureServices(services);
89 
90  if (postSetupServices == null)
91  throw new ArgumentNullException(nameof(postSetupServices));
92 
93  // configure configuration
94  services.UseStandardConfig<UpdatesConfiguration>(Configuration);
95  services.UseStandardConfig<ControlPanelConfiguration>(Configuration);
96 
97  // enable options which give us config reloading
98  services.AddOptions();
99 
100  static LogEventLevel? ConvertSeriLogLevel(LogLevel logLevel) =>
101  logLevel switch
102  {
103  LogLevel.Critical => LogEventLevel.Fatal,
104  LogLevel.Debug => LogEventLevel.Debug,
105  LogLevel.Error => LogEventLevel.Error,
106  LogLevel.Information => LogEventLevel.Information,
107  LogLevel.Trace => LogEventLevel.Verbose,
108  LogLevel.Warning => LogEventLevel.Warning,
109  LogLevel.None => null,
110  _ => throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel)),
111  };
112 
113  var microsoftEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.MicrosoftLogLevel);
114  services.SetupLogging(
115  config =>
116  {
117  if (microsoftEventLevel.HasValue)
118  config.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
119  },
120  sinkConfig =>
121  {
122  if (postSetupServices.FileLoggingConfiguration.Disable)
123  return;
124 
125  // common app data is C:/ProgramData on windows, else /usr/share
126  var logPath = !String.IsNullOrEmpty(postSetupServices.FileLoggingConfiguration.Directory)
127  ? postSetupServices.FileLoggingConfiguration.Directory
128  : IOManager.ConcatPath(
129  Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
131  "Logs");
132 
133  var logEventLevel = ConvertSeriLogLevel(postSetupServices.FileLoggingConfiguration.LogLevel);
134 
135  var formatter = new MessageTemplateTextFormatter(
136  "{Timestamp:o} "
138  + ": [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}",
139  null);
140 
141  logPath = IOManager.ConcatPath(logPath, "tgs-.log");
142  var rollingFileConfig = sinkConfig.File(
143  formatter,
144  logPath,
145  logEventLevel ?? LogEventLevel.Verbose,
146  50 * 1024 * 1024, // 50MB max size
147  flushToDiskInterval: TimeSpan.FromSeconds(2),
148  rollingInterval: RollingInterval.Day,
149  rollOnFileSizeLimit: true);
150  });
151 
152  // configure bearer token validation
153  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
154  {
155  // this line isn't actually run until the first request is made
156  // at that point tokenFactory will be populated
157  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
158  jwtBearerOptions.Events = new JwtBearerEvents
159  {
160  // Application is our composition root so this monstrosity of a line is okay
161  // At least, that's what I tell myself to sleep at night
162  OnTokenValidated = ctx => ctx
163  .HttpContext
164  .RequestServices
165  .GetRequiredService<IClaimsInjector>()
166  .InjectClaimsIntoContext(
167  ctx,
168  ctx.HttpContext.RequestAborted)
169  };
170  });
171 
172  // WARNING: STATIC CODE
173  // fucking prevents converting 'sub' to M$ bs
174  // can't be done in the above lambda, that's too late
175  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
176 
177  // add mvc, configure the json serializer settings
178  services
179  .AddMvc(options =>
180  {
181  options.EnableEndpointRouting = false;
182  })
183  .AddNewtonsoftJson(options =>
184  {
185  options.AllowInputFormatterExceptionMessages = true;
186  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
187  options.SerializerSettings.CheckAdditionalContent = true;
188  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
189  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
190  options.SerializerSettings.Converters = new[] { new VersionConverter() };
191  });
192 
193  if (hostingEnvironment.IsDevelopment())
194  {
195  static string GetDocumentationFilePath(string assemblyLocation) => IOManager.ConcatPath(IOManager.GetDirectoryName(assemblyLocation), String.Concat(IOManager.GetFileNameWithoutExtension(assemblyLocation), ".xml"));
196  var assemblyDocumentationPath = GetDocumentationFilePath(typeof(Application).Assembly.Location);
197  var apiDocumentationPath = GetDocumentationFilePath(typeof(ApiHeaders).Assembly.Location);
198  services.AddSwaggerGen(genOptions => SwaggerConfiguration.Configure(genOptions, assemblyDocumentationPath, apiDocumentationPath));
199  services.AddSwaggerGenNewtonsoftSupport();
200  }
201 
202  // enable browser detection
203  services.AddDetectionCore().AddBrowser();
204 
205  // CORS conditionally enabled later
206  services.AddCors();
207 
208  void AddTypedContext<TContext>() where TContext : DatabaseContext
209  {
210  services.AddDbContext<TContext>(builder =>
211  {
212  if (hostingEnvironment.IsDevelopment())
213  builder.EnableSensitiveDataLogging();
214  });
215  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
216  }
217 
218  // add the correct database context type
219  var dbType = postSetupServices.DatabaseConfiguration.DatabaseType;
220  switch (dbType)
221  {
222  case DatabaseType.MySql:
223  case DatabaseType.MariaDB:
224  AddTypedContext<MySqlDatabaseContext>();
225  break;
226  case DatabaseType.SqlServer:
227  AddTypedContext<SqlServerDatabaseContext>();
228  break;
229  case DatabaseType.Sqlite:
230  AddTypedContext<SqliteDatabaseContext>();
231  break;
232  case DatabaseType.PostgresSql:
233  AddTypedContext<PostgresSqlDatabaseContext>();
234  break;
235  default:
236  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
237  }
238 
239  // configure other database services
240  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
241  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
242 
243  // configure security services
245  services.AddScoped<IClaimsInjector, ClaimsInjector>();
246  services.AddSingleton<IIdentityCache, IdentityCache>();
247  services.AddSingleton<ICryptographySuite, CryptographySuite>();
248  services.AddSingleton<ITokenFactory, TokenFactory>();
249  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
250 
251  // configure platform specific services
252  if (postSetupServices.PlatformIdentifier.IsWindows)
253  {
254  if (postSetupServices.GeneralConfiguration.UseBasicWatchdogOnWindows)
255  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
256  else
257  services.AddSingleton<IWatchdogFactory, WindowsWatchdogFactory>();
258 
259  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
260  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
261  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
262  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
263  services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
264 
265  services.AddSingleton<WindowsNetworkPromptReaper>();
266  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
267  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
268  }
269  else
270  {
271  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
272  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
273  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
274  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
275  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
276  services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
277  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
278  }
279 
280  // configure misc services
281  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
282  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
283  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
284  services.AddSingleton<IServerPortProvider, ServerPortProivder>();
285  services.AddSingleton<ITopicClient, TopicClient>();
286  services.AddSingleton(new SocketParameters
287  {
288  ReceiveTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
289  SendTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
290  ConnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout),
291  DisconnectTimeout = TimeSpan.FromMilliseconds(postSetupServices.GeneralConfiguration.ByondTopicTimeout)
292  });
293 
294  // configure component services
295  services.AddSingleton<ILibGit2RepositoryFactory, LibGit2RepositoryFactory>();
296  services.AddSingleton<ILibGit2Commands, LibGit2Commands>();
297  services.AddSingleton<IProviderFactory, ProviderFactory>();
298  services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
299  services.AddSingleton<IInstanceFactory, InstanceFactory>();
300 
301  // configure root services
302  services.AddSingleton<IJobManager, JobManager>();
303 
304  services.AddSingleton<InstanceManager>();
305  services.AddSingleton<IBridgeDispatcher>(x => x.GetRequiredService<InstanceManager>());
306  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
307  }
308 
310  protected override void ConfigureHostedService(IServiceCollection services)
311  => services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
312 
323  public void Configure(
324  IApplicationBuilder applicationBuilder,
325  IServerControl serverControl,
326  ITokenFactory tokenFactory,
327  IInstanceManager instanceManager,
328  IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions,
329  IOptions<GeneralConfiguration> generalConfigurationOptions,
330  ILogger<Application> logger)
331  {
332  if (applicationBuilder == null)
333  throw new ArgumentNullException(nameof(applicationBuilder));
334  if (serverControl == null)
335  throw new ArgumentNullException(nameof(serverControl));
336 
337  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
338 
339  var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
340  var generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
341 
342  if (logger == null)
343  throw new ArgumentNullException(nameof(logger));
344 
345  logger.LogDebug("Content Root: {0}", hostingEnvironment.ContentRootPath);
346  logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
347 
348  if (generalConfiguration.MinimumPasswordLength > Limits.MaximumStringLength)
349  {
350  logger.LogCritical("Configured minimum password length ({0}) is greater than the maximum database string length ({1})!");
351  serverControl.Die(new InvalidOperationException("Minimum password length greater than database limit!"));
352  return;
353  }
354 
355  // attempt to restart the server if the configuration changes
356  if (serverControl.WatchdogPresent)
357  ChangeToken.OnChange(Configuration.GetReloadToken, () => serverControl.Restart());
358 
359  // setup the HTTP request pipeline
360  // Final point where we wrap exceptions in a 500 (ErrorMessage) response
361  applicationBuilder.UseServerErrorHandling();
362 
363  // 503 requests made while the application is starting
364  applicationBuilder.UseAsyncInitialization(async (cancellationToken) =>
365  {
366  var tcs = new TaskCompletionSource<object>();
367  using (cancellationToken.Register(() => tcs.SetCanceled()))
368  await Task.WhenAny(tcs.Task, instanceManager.Ready).ConfigureAwait(false);
369  });
370 
371  // suppress OperationCancelledExceptions, they are just aborted HTTP requests
372  applicationBuilder.UseCancelledRequestSuppression();
373 
374  if (hostingEnvironment.IsDevelopment())
375  {
376  applicationBuilder.UseSwagger();
377  applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4"));
378  logger.LogTrace("Swagger API generation enabled");
379  }
380 
381  // Set up CORS based on configuration if necessary
382  Action<CorsPolicyBuilder> corsBuilder = null;
383  if (controlPanelConfiguration.AllowAnyOrigin)
384  {
385  logger.LogTrace("Access-Control-Allow-Origin: *");
386  corsBuilder = builder => builder.AllowAnyOrigin();
387  }
388  else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
389  {
390  logger.LogTrace("Access-Control-Allow-Origin: ", String.Join(',', controlPanelConfiguration.AllowedOrigins));
391  corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
392  }
393 
394  var originalBuilder = corsBuilder;
395  corsBuilder = builder =>
396  {
397  builder.AllowAnyHeader().AllowAnyMethod();
398  originalBuilder?.Invoke(builder);
399  };
400  applicationBuilder.UseCors(corsBuilder);
401 
402  // spa loading if necessary
403  if (controlPanelConfiguration.Enable)
404  {
405  logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
406  applicationBuilder.UseStaticFiles();
407  }
408  else
409  logger.LogDebug("Web control panel disabled!");
410 
411  // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
412  applicationBuilder.UseAuthentication();
413 
414  // suppress and log database exceptions
415  applicationBuilder.UseDbConflictHandling();
416 
417  // majority of handling is done in the controllers
418  applicationBuilder.UseMvc();
419 
420  // 404 anything that gets this far
421  }
422  }
423 }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
@@ -116,12 +116,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
LogLevel LogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs
+
const string SerilogContextTemplate
Common template used for adding our custom log context to serilog.
For initially seeding a database
ITokenFactory tokenFactory
The ITokenFactory for the Application
Definition: Application.cs:57
- +
IPlatformIdentifier PlatformIdentifier
The IPlatformIdentifier.
Sanity limits to prevent users from overloading
Definition: Limits.cs:6
@@ -143,6 +144,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Application(IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
Construct an Application
Definition: Application.cs:64
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:17
+
readonly IWebHostEnvironment hostingEnvironment
The IWebHostEnvironment for the Application.
Definition: Application.cs:52
@@ -150,7 +152,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Configuration for the automatic update system
For downloading and installing BYOND extractions for a given system
-
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IInstanceManager instanceManager, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:320
+
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IInstanceManager instanceManager, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:323
For accessing the disk in a synchronous manner
DatabaseType DatabaseType
The Configuration.DatabaseType to create
@@ -176,7 +178,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JsonConverter for serializing Versions for BYOND.
static void Configure(SwaggerGenOptions swaggerGenOptions, string assemblyDocumentationPath, string apiDocumentationPath)
Configure the swagger settings.
- + diff --git a/_basic_watchdog_8cs_source.html b/_basic_watchdog_8cs_source.html index 50c2411388..0b26c9c54e 100644 --- a/_basic_watchdog_8cs_source.html +++ b/_basic_watchdog_8cs_source.html @@ -94,12 +94,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
BasicWatchdog.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Threading;
6 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Components.Watchdog
16 {
21  {
23  public sealed override bool AlphaIsActive => true;
24 
26  public sealed override Models.CompileJob ActiveCompileJob => Server?.Dmb.CompileJob;
27 
29  public sealed override RebootState? RebootState => Server?.RebootState;
30 
34  protected ISessionController Server { get; private set; }
35 
40 
56  public BasicWatchdog(
57  IChatManager chat,
58  ISessionControllerFactory sessionControllerFactory,
59  IDmbFactory dmbFactory,
60  IReattachInfoHandler reattachInfoHandler,
61  IDatabaseContextFactory databaseContextFactory,
62  IJobManager jobManager,
63  IServerControl serverControl,
64  IAsyncDelayer asyncDelayer,
65  ILogger<BasicWatchdog> logger,
66  DreamDaemonLaunchParameters initialLaunchParameters,
67  Api.Models.Instance instance,
68  bool autoStart)
69  : base(
70  chat,
71  sessionControllerFactory,
72  dmbFactory,
73  reattachInfoHandler,
74  databaseContextFactory,
75  jobManager,
76  serverControl,
77  asyncDelayer,
78  logger,
79  initialLaunchParameters,
80  instance,
81  autoStart)
82  { }
83 
85  protected override IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState)
86  {
87  if (Server == null)
88  return null;
89 
90  monitorState.ActiveServer = Server;
91 
92  return new Dictionary<MonitorActivationReason, Task>
93  {
94  { MonitorActivationReason.ActiveServerCrashed, Server.Lifetime },
95  { MonitorActivationReason.ActiveServerRebooted, Server.OnReboot },
96  { MonitorActivationReason.InactiveServerCrashed, Extensions.TaskExtensions.InfiniteTask() },
97  { MonitorActivationReason.InactiveServerRebooted, Extensions.TaskExtensions.InfiniteTask() },
98  { MonitorActivationReason.InactiveServerStartupComplete, Extensions.TaskExtensions.InfiniteTask() }
99  };
100  }
101 
103  protected override async Task HandleMonitorWakeup(MonitorActivationReason reason, MonitorState monitorState, CancellationToken cancellationToken)
104  {
105  switch (reason)
106  {
107  case MonitorActivationReason.ActiveServerCrashed:
108  string exitWord = Server.TerminationWasRequested ? "exited" : "crashed";
109  if (Server.RebootState == Session.RebootState.Shutdown)
110  {
111  // the time for graceful shutdown is now
112  await Chat.SendWatchdogMessage(
113  String.Format(
114  CultureInfo.InvariantCulture,
115  "Server {0}! Shutting down due to graceful termination request...",
116  exitWord),
117  false,
118  cancellationToken)
119  .ConfigureAwait(false);
120  monitorState.NextAction = MonitorAction.Exit;
121  }
122  else
123  {
124  await Chat.SendWatchdogMessage(
125  String.Format(
126  CultureInfo.InvariantCulture,
127  "Server {0}! Rebooting...",
128  exitWord),
129  false,
130  cancellationToken)
131  .ConfigureAwait(false);
132  monitorState.NextAction = MonitorAction.Restart;
133  }
134 
135  break;
136  case MonitorActivationReason.ActiveServerRebooted:
137  var rebootState = Server.RebootState;
138  if (gracefulRebootRequired && rebootState == Session.RebootState.Normal)
139  {
140  Logger.LogError("Watchdog reached normal reboot state with gracefulRebootRequired set!");
141  rebootState = Session.RebootState.Restart;
142  }
143 
144  gracefulRebootRequired = false;
145  Server.ResetRebootState();
146 
147  switch (rebootState)
148  {
149  case Session.RebootState.Normal:
150  monitorState.NextAction = HandleNormalReboot();
151  break;
152  case Session.RebootState.Restart:
153  monitorState.NextAction = MonitorAction.Restart;
154  break;
155  case Session.RebootState.Shutdown:
156  // graceful shutdown time
157  await Chat.SendWatchdogMessage(
158  "Active server rebooted! Shutting down due to graceful termination request...",
159  false,
160  cancellationToken)
161  .ConfigureAwait(false);
162  monitorState.NextAction = MonitorAction.Exit;
163  break;
164  default:
165  throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
166  }
167 
168  break;
169  case MonitorActivationReason.ActiveLaunchParametersUpdated:
170  await Server.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false);
171  gracefulRebootRequired = true;
172  break;
173  case MonitorActivationReason.NewDmbAvailable:
174  await HandleNewDmbAvailable(cancellationToken).ConfigureAwait(false);
175  break;
176  case MonitorActivationReason.InactiveServerCrashed:
177  case MonitorActivationReason.InactiveServerRebooted:
178  case MonitorActivationReason.InactiveServerStartupComplete:
179  throw new NotSupportedException($"Unsupported activation reason: {reason}");
180  case MonitorActivationReason.Heartbeat:
181  default:
182  throw new InvalidOperationException($"Invalid activation reason: {reason}");
183  }
184  }
185 
187  protected sealed override DualReattachInformation CreateReattachInformation()
189  {
190  AlphaIsActive = true,
191  Alpha = Server?.Release()
192  };
193 
195  protected override void DisposeAndNullControllersImpl()
196  {
197  Server?.Dispose();
198  Server = null;
199  Running = false;
200  gracefulRebootRequired = false;
201  }
202 
204  protected sealed override ISessionController GetActiveController() => Server;
205 
207  protected sealed override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
208  {
209  var serverToReattach = reattachInfo?.Alpha ?? reattachInfo?.Bravo;
210  var serverToKill = reattachInfo?.Bravo ?? reattachInfo?.Alpha;
211 
212  // vice versa
213  if (serverToKill == serverToReattach)
214  serverToKill = null;
215 
216  if (reattachInfo?.AlphaIsActive == false)
217  {
218  var temp = serverToReattach;
219  serverToReattach = serverToKill;
220  serverToKill = temp;
221  }
222 
223  // don't need a new dmb if reattaching
224  var doesntNeedNewDmb = serverToReattach != null;
225  var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(1);
226 
227  // if this try catches something, both servers are killed
228  bool inactiveServerWasKilled = false;
229  try
230  {
231  // start the alpha server task, either by launch a new process or attaching to an existing one
232  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
233  // The tasks pertaining to server startup times are in the ISessionControllers
234  Task<ISessionController> serverLaunchTask, inactiveReattachTask;
235  if (!doesntNeedNewDmb)
236  {
237  dmbToUse = await PrepServerForLaunch(dmbToUse, cancellationToken).ConfigureAwait(false);
238  serverLaunchTask = SessionControllerFactory.LaunchNew(
239  dmbToUse,
240  null,
241  ActiveLaunchParameters,
242  true,
243  true,
244  false,
245  cancellationToken);
246  }
247  else
248  serverLaunchTask = SessionControllerFactory.Reattach(serverToReattach, cancellationToken);
249 
250  bool thereIsAnInactiveServerToKill = serverToKill != null;
251  if (thereIsAnInactiveServerToKill)
252  inactiveReattachTask = SessionControllerFactory.Reattach(serverToKill, cancellationToken);
253  else
254  inactiveReattachTask = Task.FromResult<ISessionController>(null);
255 
256  // retrieve the session controller
257  Server = await serverLaunchTask.ConfigureAwait(false);
258 
259  // failed reattaches will return null
260  Server?.SetHighPriority();
261 
262  var inactiveServerController = await inactiveReattachTask.ConfigureAwait(false);
263  inactiveServerController?.Dispose();
264  inactiveServerWasKilled = inactiveServerController != null;
265 
266  // possiblity of null servers due to failed reattaches
267  if (Server == null)
268  {
269  callBeforeRecurse();
270  await NotifyOfFailedReattach(thereIsAnInactiveServerToKill && !inactiveServerWasKilled, cancellationToken).ConfigureAwait(false);
271  return;
272  }
273 
274  await CheckLaunchResult(Server, "Server", cancellationToken).ConfigureAwait(false);
275 
276  Server.EnableCustomChatCommands();
277  }
278  catch
279  {
280  // kill the controllers
281  bool serverWasActive = Server != null;
282  DisposeAndNullControllers();
283 
284  // server didn't get control of this dmb
285  if (dmbToUse != null && !serverWasActive)
286  dmbToUse.Dispose();
287 
288  if (serverToKill != null && !inactiveServerWasKilled)
289  serverToKill.Dmb.Dispose();
290  throw;
291  }
292  }
293 
298  protected virtual MonitorAction HandleNormalReboot()
299  {
300  bool dmbUpdatePending = ActiveLaunchParameters != LastLaunchParameters;
301  return dmbUpdatePending ? MonitorAction.Restart : MonitorAction.Continue;
302  }
303 
309  protected virtual Task HandleNewDmbAvailable(CancellationToken cancellationToken)
310  {
311  gracefulRebootRequired = true;
312  return Server.SetRebootState(Session.RebootState.Restart, cancellationToken);
313  }
314 
321  protected virtual Task<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken) => Task.FromResult(dmbToUse);
322 
324  public override Task ResetRebootState(CancellationToken cancellationToken)
325  {
326  if (!gracefulRebootRequired)
327  return base.ResetRebootState(cancellationToken);
328 
329  return Restart(true, cancellationToken);
330  }
331 
338  async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken)
339  {
340  // we lost the server, just restart entirely
341  DisposeAndNullControllers();
342  const string FailReattachMessage = "Unable to properly reattach to server! Restarting...";
343  Logger.LogWarning(FailReattachMessage);
344  Logger.LogDebug(inactiveReattachSuccess ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
345  Task chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken);
346  await LaunchNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
347  await chatTask.ConfigureAwait(false);
348  }
349  }
350 }
BasicWatchdog(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger< BasicWatchdog > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the BasicWatchdog .
-
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Threading;
6 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Components.Watchdog
16 {
21  {
23  public sealed override bool AlphaIsActive => true;
24 
26  public sealed override Models.CompileJob ActiveCompileJob => Server?.Dmb.CompileJob;
27 
29  public sealed override RebootState? RebootState => Server?.RebootState;
30 
34  protected ISessionController Server { get; private set; }
35 
40 
56  public BasicWatchdog(
57  IChatManager chat,
58  ISessionControllerFactory sessionControllerFactory,
59  IDmbFactory dmbFactory,
60  IReattachInfoHandler reattachInfoHandler,
61  IDatabaseContextFactory databaseContextFactory,
62  IJobManager jobManager,
63  IServerControl serverControl,
64  IAsyncDelayer asyncDelayer,
65  ILogger<BasicWatchdog> logger,
66  DreamDaemonLaunchParameters initialLaunchParameters,
67  Api.Models.Instance instance,
68  bool autoStart)
69  : base(
70  chat,
71  sessionControllerFactory,
72  dmbFactory,
73  reattachInfoHandler,
74  databaseContextFactory,
75  jobManager,
76  serverControl,
77  asyncDelayer,
78  logger,
79  initialLaunchParameters,
80  instance,
81  autoStart)
82  { }
83 
85  protected override IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState)
86  {
87  if (Server == null)
88  return null;
89 
90  monitorState.ActiveServer = Server;
91 
92  return new Dictionary<MonitorActivationReason, Task>
93  {
94  { MonitorActivationReason.ActiveServerCrashed, Server.Lifetime },
95  { MonitorActivationReason.ActiveServerRebooted, Server.OnReboot },
96  { MonitorActivationReason.InactiveServerCrashed, Extensions.TaskExtensions.InfiniteTask() },
97  { MonitorActivationReason.InactiveServerRebooted, Extensions.TaskExtensions.InfiniteTask() },
98  { MonitorActivationReason.InactiveServerStartupComplete, Extensions.TaskExtensions.InfiniteTask() }
99  };
100  }
101 
103  protected override async Task HandleMonitorWakeup(MonitorActivationReason reason, MonitorState monitorState, CancellationToken cancellationToken)
104  {
105  switch (reason)
106  {
107  case MonitorActivationReason.ActiveServerCrashed:
108  string exitWord = Server.TerminationWasRequested ? "exited" : "crashed";
109  if (Server.RebootState == Session.RebootState.Shutdown)
110  {
111  // the time for graceful shutdown is now
112  await Chat.SendWatchdogMessage(
113  String.Format(
114  CultureInfo.InvariantCulture,
115  "Server {0}! Shutting down due to graceful termination request...",
116  exitWord),
117  false,
118  cancellationToken)
119  .ConfigureAwait(false);
120  monitorState.NextAction = MonitorAction.Exit;
121  }
122  else
123  {
124  await Chat.SendWatchdogMessage(
125  String.Format(
126  CultureInfo.InvariantCulture,
127  "Server {0}! Rebooting...",
128  exitWord),
129  false,
130  cancellationToken)
131  .ConfigureAwait(false);
132  monitorState.NextAction = MonitorAction.Restart;
133  }
134 
135  break;
136  case MonitorActivationReason.ActiveServerRebooted:
137  var rebootState = Server.RebootState;
138  if (gracefulRebootRequired && rebootState == Session.RebootState.Normal)
139  {
140  Logger.LogError("Watchdog reached normal reboot state with gracefulRebootRequired set!");
141  rebootState = Session.RebootState.Restart;
142  }
143 
144  gracefulRebootRequired = false;
145  Server.ResetRebootState();
146 
147  switch (rebootState)
148  {
149  case Session.RebootState.Normal:
150  monitorState.NextAction = HandleNormalReboot();
151  break;
152  case Session.RebootState.Restart:
153  monitorState.NextAction = MonitorAction.Restart;
154  break;
155  case Session.RebootState.Shutdown:
156  // graceful shutdown time
157  await Chat.SendWatchdogMessage(
158  "Active server rebooted! Shutting down due to graceful termination request...",
159  false,
160  cancellationToken)
161  .ConfigureAwait(false);
162  monitorState.NextAction = MonitorAction.Exit;
163  break;
164  default:
165  throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
166  }
167 
168  break;
169  case MonitorActivationReason.ActiveLaunchParametersUpdated:
170  await Server.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false);
171  gracefulRebootRequired = true;
172  break;
173  case MonitorActivationReason.NewDmbAvailable:
174  await HandleNewDmbAvailable(cancellationToken).ConfigureAwait(false);
175  break;
176  case MonitorActivationReason.InactiveServerCrashed:
177  case MonitorActivationReason.InactiveServerRebooted:
178  case MonitorActivationReason.InactiveServerStartupComplete:
179  throw new NotSupportedException($"Unsupported activation reason: {reason}");
180  case MonitorActivationReason.Heartbeat:
181  default:
182  throw new InvalidOperationException($"Invalid activation reason: {reason}");
183  }
184  }
185 
187  protected sealed override DualReattachInformation CreateReattachInformation()
189  {
190  AlphaIsActive = true,
191  Alpha = Server?.Release()
192  };
193 
195  protected override void DisposeAndNullControllersImpl()
196  {
197  Server?.Dispose();
198  Server = null;
199  Running = false;
200  gracefulRebootRequired = false;
201  }
202 
204  protected sealed override ISessionController GetActiveController() => Server;
205 
207  protected sealed override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
208  {
209  var serverToReattach = reattachInfo?.Alpha ?? reattachInfo?.Bravo;
210  var serverToKill = reattachInfo?.Bravo ?? reattachInfo?.Alpha;
211 
212  // vice versa
213  if (serverToKill == serverToReattach)
214  serverToKill = null;
215 
216  if (reattachInfo?.AlphaIsActive == false)
217  {
218  var temp = serverToReattach;
219  serverToReattach = serverToKill;
220  serverToKill = temp;
221  }
222 
223  // don't need a new dmb if reattaching
224  var doesntNeedNewDmb = serverToReattach != null;
225  var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(1);
226 
227  // if this try catches something, both servers are killed
228  bool inactiveServerWasKilled = false;
229  try
230  {
231  // start the alpha server task, either by launch a new process or attaching to an existing one
232  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
233  // The tasks pertaining to server startup times are in the ISessionControllers
234  Task<ISessionController> serverLaunchTask, inactiveReattachTask;
235  if (!doesntNeedNewDmb)
236  {
237  dmbToUse = await PrepServerForLaunch(dmbToUse, cancellationToken).ConfigureAwait(false);
238  serverLaunchTask = SessionControllerFactory.LaunchNew(
239  dmbToUse,
240  null,
241  ActiveLaunchParameters,
242  true,
243  true,
244  false,
245  cancellationToken);
246  }
247  else
248  serverLaunchTask = SessionControllerFactory.Reattach(serverToReattach, cancellationToken);
249 
250  bool thereIsAnInactiveServerToKill = serverToKill != null;
251  if (thereIsAnInactiveServerToKill)
252  inactiveReattachTask = SessionControllerFactory.Reattach(serverToKill, cancellationToken);
253  else
254  inactiveReattachTask = Task.FromResult<ISessionController>(null);
255 
256  // retrieve the session controller
257  Server = await serverLaunchTask.ConfigureAwait(false);
258 
259  // failed reattaches will return null
260  Server?.SetHighPriority();
261 
262  var inactiveServerController = await inactiveReattachTask.ConfigureAwait(false);
263  inactiveServerController?.Dispose();
264  inactiveServerWasKilled = inactiveServerController != null;
265 
266  // possiblity of null servers due to failed reattaches
267  if (Server == null)
268  {
269  callBeforeRecurse();
270  await NotifyOfFailedReattach(thereIsAnInactiveServerToKill && !inactiveServerWasKilled, cancellationToken).ConfigureAwait(false);
271  return;
272  }
273 
274  await CheckLaunchResult(Server, "Server", cancellationToken).ConfigureAwait(false);
275 
276  Server.EnableCustomChatCommands();
277  }
278  catch
279  {
280  // kill the controllers
281  bool serverWasActive = Server != null;
282  DisposeAndNullControllers();
283 
284  // server didn't get control of this dmb
285  if (dmbToUse != null && !serverWasActive)
286  dmbToUse.Dispose();
287 
288  if (serverToKill != null && !inactiveServerWasKilled)
289  serverToKill.Dmb.Dispose();
290  throw;
291  }
292  }
293 
298  protected virtual MonitorAction HandleNormalReboot()
299  {
300  bool dmbUpdatePending = ActiveLaunchParameters != LastLaunchParameters;
301  return dmbUpdatePending ? MonitorAction.Restart : MonitorAction.Continue;
302  }
303 
309  protected virtual Task HandleNewDmbAvailable(CancellationToken cancellationToken)
310  {
311  gracefulRebootRequired = true;
312  return Server.SetRebootState(Session.RebootState.Restart, cancellationToken);
313  }
314 
321  protected virtual Task<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken) => Task.FromResult(dmbToUse);
322 
324  public override Task ResetRebootState(CancellationToken cancellationToken)
325  {
326  if (!gracefulRebootRequired)
327  return base.ResetRebootState(cancellationToken);
328 
329  return Restart(true, cancellationToken);
330  }
331 
338  async Task NotifyOfFailedReattach(bool inactiveReattachSuccess, CancellationToken cancellationToken)
339  {
340  // we lost the server, just restart entirely
341  DisposeAndNullControllers();
342  const string FailReattachMessage = "Unable to properly reattach to server! Restarting...";
343  Logger.LogWarning(FailReattachMessage);
344  Logger.LogDebug(inactiveReattachSuccess ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
345  Task chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken);
346  await LaunchNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
347  await chatTask.ConfigureAwait(false);
348  }
349 
351  public sealed override Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
352  => Server?.InstanceRenamed(newInstanceName, cancellationToken) ?? Task.CompletedTask;
353  }
354 }
BasicWatchdog(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger< BasicWatchdog > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the BasicWatchdog .
+
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
void EnableCustomChatCommands()
Enables the reading of custom chat commands from the ISessionController
bool gracefulRebootRequired
If the server is set to gracefully reboot due to a pending dmb or settings change.
- +
MonitorAction NextAction
The next MonitorAction to take in WatchdogBase.MonitorLifetimes(global::System.Threading.CancellationToken)
Definition: MonitorState.cs:24
async Task< ISessionController > LaunchNew(IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, DreamDaemonLaunchParameters launchParameters, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
Create a ISessionController from a freshly launch DreamDaemon instance
diff --git a/_bridge_controller_8cs_source.html b/_bridge_controller_8cs_source.html index 0a3134ceac..2bb71f6033 100644 --- a/_bridge_controller_8cs_source.html +++ b/_bridge_controller_8cs_source.html @@ -94,27 +94,29 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
BridgeController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using System;
5 using System.Net;
6 using System.Threading;
7 using System.Threading.Tasks;
8 using Tgstation.Server.Api;
11 
12 namespace Tgstation.Server.Host.Controllers
13 {
17  [Route("Bridge")]
18  [Produces(ApiHeaders.ApplicationJson)]
20  {
25 
29  readonly ILogger<BridgeController> logger;
30 
36  public BridgeController(IBridgeDispatcher bridgeDispatcher, ILogger<BridgeController> logger)
37  {
38  this.bridgeDispatcher = bridgeDispatcher ?? throw new ArgumentNullException(nameof(bridgeDispatcher));
39  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
40  }
41 
48  [HttpGet]
49  public async Task<IActionResult> Process([FromQuery]string data, CancellationToken cancellationToken)
50  {
51  // Nothing to see here
52  if (!IPAddress.IsLoopback(Request.HttpContext.Connection.RemoteIpAddress))
53  return NotFound();
54 
55  BridgeParameters request;
56  try
57  {
58  request = JsonConvert.DeserializeObject<BridgeParameters>(data, DMApiConstants.SerializerSettings);
59  }
60  catch
61  {
62  logger.LogWarning("Error deserializing bridge request: {0}", data);
63  return BadRequest();
64  }
65 
66  logger.LogTrace("Bridge Request: {0}", data);
67 
68  var response = await bridgeDispatcher.ProcessBridgeRequest(request, cancellationToken).ConfigureAwait(false);
69  if (response == null)
70  Forbid();
71 
72  var responseJson = JsonConvert.SerializeObject(response, DMApiConstants.SerializerSettings);
73  logger.LogTrace("Bridge Response: {0}", responseJson);
74  return Content(responseJson, ApiHeaders.ApplicationJson);
75  }
76  }
77 }
async Task< IActionResult > Process([FromQuery]string data, CancellationToken cancellationToken)
Processes a bridge request.
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Newtonsoft.Json;
5 using Serilog.Context;
6 using System;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Controllers
15 {
19  [Route("Bridge")]
20  [Produces(ApiHeaders.ApplicationJson)]
22  {
26  static long requestsProcessed;
27 
32 
36  readonly ILogger<BridgeController> logger;
37 
44  public BridgeController(IBridgeDispatcher bridgeDispatcher, IHostApplicationLifetime applicationLifetime, ILogger<BridgeController> logger)
45  {
46  this.bridgeDispatcher = bridgeDispatcher ?? throw new ArgumentNullException(nameof(bridgeDispatcher));
47  if (applicationLifetime == null)
48  throw new ArgumentNullException(nameof(applicationLifetime));
49 
50  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
51 
52  applicationLifetime.ApplicationStopped.Register(() => requestsProcessed = 0);
53  }
54 
61  [HttpGet]
62  public async Task<IActionResult> Process([FromQuery]string data, CancellationToken cancellationToken)
63  {
64  // Nothing to see here
65  if (!IPAddress.IsLoopback(Request.HttpContext.Connection.RemoteIpAddress))
66  return NotFound();
67 
68  using (LogContext.PushProperty("Bridge", Interlocked.Increment(ref requestsProcessed)))
69  {
70  BridgeParameters request;
71  try
72  {
73  request = JsonConvert.DeserializeObject<BridgeParameters>(data, DMApiConstants.SerializerSettings);
74  }
75  catch
76  {
77  logger.LogWarning("Error deserializing bridge request: {0}", data);
78  return BadRequest();
79  }
80 
81  logger.LogTrace("Bridge Request: {0}", data);
82 
83  var response = await bridgeDispatcher.ProcessBridgeRequest(request, cancellationToken).ConfigureAwait(false);
84  if (response == null)
85  Forbid();
86 
87  var responseJson = JsonConvert.SerializeObject(response, DMApiConstants.SerializerSettings);
88  logger.LogTrace("Bridge Response: {0}", responseJson);
89  return Content(responseJson, ApiHeaders.ApplicationJson);
90  }
91  }
92  }
93 }
async Task< IActionResult > Process([FromQuery]string data, CancellationToken cancellationToken)
Processes a bridge request.
+
BridgeController(IBridgeDispatcher bridgeDispatcher, IHostApplicationLifetime applicationLifetime, ILogger< BridgeController > logger)
Initializes a new instance of the BridgeController .
const string ApplicationJson
TODO: Remove this when we upgrade to .NET Standard 2.1
Definition: ApiHeaders.cs:22
+ +
static long requestsProcessed
Static counter for the number of requests processed.
Constants used for communication with the DMAPI
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:17
-
readonly IBridgeDispatcher bridgeDispatcher
The IBridgeDispatcher for the BridgeController
+
readonly IBridgeDispatcher bridgeDispatcher
The IBridgeDispatcher for the BridgeController
static readonly JsonSerializerSettings SerializerSettings
JsonSerializerSettings for use when communicating with the DMAPI.
-
Controller for recieving DMAPI requests from DreamDaemon.
-
readonly ILogger< BridgeController > logger
The ILogger for the BridgeController
+
Controller for recieving DMAPI requests from DreamDaemon.
+
readonly ILogger< BridgeController > logger
The ILogger for the BridgeController
-
BridgeController(IBridgeDispatcher bridgeDispatcher, ILogger< BridgeController > logger)
Initializes a new instance of the BridgeController .
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
8 using Tgstation.Server.Api;
16 
17 namespace Tgstation.Server.Host.Controllers
18 {
22  [Route(Routes.Byond)]
23  public sealed class ByondController : ApiController
24  {
29 
34 
43  public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger<ByondController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
44  {
45  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
46  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
47  }
48 
54  [HttpGet]
55  [TgsAuthorize(ByondRights.ReadActive)]
56  [ProducesResponseType(typeof(Api.Models.Byond), 200)]
57  public Task<IActionResult> Read() => Task.FromResult<IActionResult>(
58  Json(new Api.Models.Byond
59  {
60  Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion
61  }));
62 
68  [HttpGet(Routes.List)]
69  [TgsAuthorize(ByondRights.ListInstalled)]
70  [ProducesResponseType(typeof(IEnumerable<Api.Models.Byond>), 200)]
71  public Task<IActionResult> List() => Task.FromResult<IActionResult>(
72  Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond
73  {
74  Version = x
75  })));
76 
85  [HttpPost]
86  [TgsAuthorize(ByondRights.ChangeVersion)]
87  [ProducesResponseType(typeof(Api.Models.Byond), 200)]
88  [ProducesResponseType(typeof(Api.Models.Byond), 202)]
89  public async Task<IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
90  {
91  if (model == null)
92  throw new ArgumentNullException(nameof(model));
93 
94  var byondManager = instanceManager.GetInstance(Instance).ByondManager;
95 
96  // remove cruff fields
97  var installingVersion = new Version(model.Version.Major, model.Version.Minor);
98 
99  var result = new Api.Models.Byond();
100 
101  if (byondManager.InstalledVersions.Any(x => x == model.Version))
102  {
103  Logger.LogInformation("User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
104  await byondManager.ChangeVersion(model.Version, cancellationToken).ConfigureAwait(false);
105  }
106  else
107  {
108  Logger.LogInformation(
109  "User ID {0} installing BYOND version to {1} on instance ID {2}",
111  installingVersion,
112  Instance.Id);
113 
114  // run the install through the job manager
115  var job = new Models.Job
116  {
117  Description = $"Install BYOND version {installingVersion}",
118  StartedBy = AuthenticationContext.User,
119  CancelRightsType = RightsType.Byond,
120  CancelRight = (ulong)ByondRights.CancelInstall,
122  };
123  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false);
124  result.InstallJob = job.ToApi();
125  }
126 
127  if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0)
128  result.Version = byondManager.ActiveVersion;
129  return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result);
130  }
131  }
132 }
const string Byond
The Models.Byond controller
Definition: Routes.cs:34
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
8 using Tgstation.Server.Api;
16 
17 namespace Tgstation.Server.Host.Controllers
18 {
22  [Route(Routes.Byond)]
23  public sealed class ByondController : ApiController
24  {
29 
34 
43  public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger<ByondController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
44  {
45  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
46  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
47  }
48 
54  [HttpGet]
55  [TgsAuthorize(ByondRights.ReadActive)]
56  [ProducesResponseType(typeof(Api.Models.Byond), 200)]
57  public Task<IActionResult> Read() => Task.FromResult<IActionResult>(
58  Json(new Api.Models.Byond
59  {
60  Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion
61  }));
62 
68  [HttpGet(Routes.List)]
69  [TgsAuthorize(ByondRights.ListInstalled)]
70  [ProducesResponseType(typeof(IEnumerable<Api.Models.Byond>), 200)]
71  public Task<IActionResult> List() => Task.FromResult<IActionResult>(
72  Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond
73  {
74  Version = x
75  })));
76 
85  [HttpPost]
86  [TgsAuthorize(ByondRights.ChangeVersion)]
87  [ProducesResponseType(typeof(Api.Models.Byond), 200)]
88  [ProducesResponseType(typeof(Api.Models.Byond), 202)]
89  public async Task<IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
90  {
91  if (model == null)
92  throw new ArgumentNullException(nameof(model));
93 
94  var byondManager = instanceManager.GetInstance(Instance).ByondManager;
95 
96  // remove cruff fields
97  var installingVersion = new Version(model.Version.Major, model.Version.Minor);
98 
99  var result = new Api.Models.Byond();
100 
101  if (byondManager.InstalledVersions.Any(x => x == model.Version))
102  {
103  Logger.LogInformation("User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
104  await byondManager.ChangeVersion(model.Version, cancellationToken).ConfigureAwait(false);
105  }
106  else
107  {
108  Logger.LogInformation(
109  "User ID {0} installing BYOND version to {1} on instance ID {2}",
111  installingVersion,
112  Instance.Id);
113 
114  // run the install through the job manager
115  var job = new Models.Job
116  {
117  Description = $"Install BYOND version {installingVersion}",
118  StartedBy = AuthenticationContext.User,
119  CancelRightsType = RightsType.Byond,
120  CancelRight = (ulong)ByondRights.CancelInstall,
122  };
123  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false);
124  result.InstallJob = job.ToApi();
125  }
126 
127  if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0)
128  result.Version = byondManager.ActiveVersion;
129  return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result);
130  }
131  }
132 }
const string Byond
The Models.Byond controller
Definition: Routes.cs:34
+
long Id
The ID of the entity.
Definition: EntityId.cs:11
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
@@ -110,8 +111,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IJobManager jobManager
The IJobManager for the ByondController
Routes to a server actions
Definition: Routes.cs:9
- -
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
+
ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger< ByondController > logger)
Construct a ByondController
readonly IInstanceManager instanceManager
The IInstanceManager for the ByondController
diff --git a/_chat_controller_8cs_source.html b/_chat_controller_8cs_source.html index 235ef85639..a51e94b413 100644 --- a/_chat_controller_8cs_source.html +++ b/_chat_controller_8cs_source.html @@ -94,9 +94,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ChatController.cs
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Linq.Expressions;
5 using System.Net;
6 using System.Reflection;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Microsoft.AspNetCore.Mvc;
10 using Microsoft.EntityFrameworkCore;
11 using Microsoft.Extensions.Logging;
12 using Tgstation.Server.Api;
19 using Z.EntityFramework.Plus;
20 
21 namespace Tgstation.Server.Host.Controllers
22 {
26  [Route(Routes.Chat)]
27  #pragma warning disable CA1506 // TODO: Decomplexify
28  public sealed class ChatController : ApiController
29  {
34 
42  public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
43  {
44  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
45  }
46 
52  static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api) => new Models.ChatChannel
53  {
54  DiscordChannelId = api.DiscordChannelId,
55  IrcChannel = api.IrcChannel,
56  IsAdminChannel = api.IsAdminChannel ?? false,
57  IsWatchdogChannel = api.IsWatchdogChannel ?? false,
58  IsUpdatesChannel = api.IsUpdatesChannel ?? false,
59  Tag = api.Tag
60  };
61 
69  [HttpPut]
70  [TgsAuthorize(ChatBotRights.Create)]
71  [ProducesResponseType(typeof(Api.Models.ChatBot), 201)]
72  public async Task<IActionResult> Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
73  {
74  if (model == null)
75  throw new ArgumentNullException(nameof(model));
76 
77  var earlyOut = StandardModelChecks(model, true);
78  if (earlyOut != null)
79  return earlyOut;
80 
81  var countOfExistingBotsInInstance = await DatabaseContext
82  .ChatBots
83  .AsQueryable()
84  .Where(x => x.InstanceId == Instance.Id)
85  .CountAsync(cancellationToken)
86  .ConfigureAwait(false);
87 
88  if (countOfExistingBotsInInstance >= Instance.ChatBotLimit.Value)
89  return Conflict(new ErrorMessage(ErrorCode.ChatBotMax));
90 
91  model.Enabled ??= false;
92  model.ReconnectionInterval ??= 1;
93 
94  // try to update das db first
95  var dbModel = new Models.ChatBot
96  {
97  Name = model.Name,
98  ConnectionString = model.ConnectionString,
99  Enabled = model.Enabled,
100  Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).ToList() ?? new List<Models.ChatChannel>(), // important that this isn't null
101  InstanceId = Instance.Id,
102  Provider = model.Provider,
103  ReconnectionInterval = model.ReconnectionInterval,
104  ChannelLimit = model.ChannelLimit
105  };
106 
107  DatabaseContext.ChatBots.Add(dbModel);
108 
109  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
110  var instance = instanceManager.GetInstance(Instance);
111  try
112  {
113  // try to create it
114  await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false);
115 
116  if (dbModel.Channels.Count > 0)
117  await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false);
118  }
119  catch
120  {
121  // undo the add
122  DatabaseContext.ChatBots.Remove(dbModel);
123  await DatabaseContext.Save(default).ConfigureAwait(false);
124  await instance.Chat.DeleteConnection(dbModel.Id, cancellationToken).ConfigureAwait(false);
125  throw;
126  }
127 
128  return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
129  }
130 
138  [HttpDelete("{id}")]
139  [TgsAuthorize(ChatBotRights.Delete)]
140  [ProducesResponseType(204)]
141  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
142  {
143  var instance = instanceManager.GetInstance(Instance);
144  await Task.WhenAll(
145  instance.Chat.DeleteConnection(id, cancellationToken),
147  .ChatBots
148  .AsQueryable()
149  .Where(x => x.Id == id)
150  .DeleteAsync(cancellationToken))
151  .ConfigureAwait(false);
152 
153  return Ok();
154  }
155 
162  [HttpGet(Routes.List)]
163  [TgsAuthorize(ChatBotRights.Read)]
164  [ProducesResponseType(typeof(IEnumerable<Api.Models.ChatBot>), 200)]
165  public async Task<IActionResult> List(CancellationToken cancellationToken)
166  {
167  var query = DatabaseContext
168  .ChatBots
169  .AsQueryable()
170  .Where(x => x.InstanceId == Instance.Id)
171  .Include(x => x.Channels);
172 
173  var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
174 
175  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
176 
177  if (!connectionStrings)
178  foreach (var I in results)
179  I.ConnectionString = null;
180 
181  return Json(results.Select(x => x.ToApi()));
182  }
183 
192  [HttpGet("{id}")]
193  [TgsAuthorize(ChatBotRights.Read)]
194  [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
195  [ProducesResponseType(410)]
196  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
197  {
198  var query = DatabaseContext.ChatBots
199  .AsQueryable()
200  .Where(x => x.Id == id)
201  .Include(x => x.Channels);
202 
203  var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
204  if (results == default)
205  return StatusCode((int)HttpStatusCode.Gone);
206 
207  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
208 
209  if (!connectionStrings)
210  results.ConnectionString = null;
211 
212  return Json(results.ToApi());
213  }
214 
222  [HttpPost]
223  [TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)]
224  [ProducesResponseType(200)]
225  [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
226 #pragma warning disable CA1502 // TODO: Decomplexify
227 #pragma warning disable CA1506
228  public async Task<IActionResult> Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
229  #pragma warning restore CA1502
230  #pragma warning restore CA1506
231  {
232  if (model == null)
233  throw new ArgumentNullException(nameof(model));
234 
235  var earlyOut = StandardModelChecks(model, false);
236  if (earlyOut != null)
237  return earlyOut;
238 
239  var query = DatabaseContext
240  .ChatBots
241  .AsQueryable()
242  .Where(x => x.InstanceId == Instance.Id && x.Id == model.Id)
243  .Include(x => x.Channels);
244 
245  var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
246 
247  if (current == default)
248  return StatusCode((int)HttpStatusCode.Gone);
249 
250  if ((model.Channels?.Count ?? current.Channels.Count) > (model.ChannelLimit ?? current.ChannelLimit.Value))
251  {
252  // 400 or 409 depends on if the client sent both
253  var errorMessage = new ErrorMessage(ErrorCode.ChatBotMaxChannels);
254  if (model.Channels != null && model.ChannelLimit.HasValue)
255  return BadRequest(errorMessage);
256  return Conflict(errorMessage);
257  }
258 
259  var userRights = (ChatBotRights)AuthenticationContext.GetRight(RightsType.ChatBots);
260 
261  bool anySettingsModified = false;
262 
263  bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatBot, T>> expression, ChatBotRights requiredRight)
264  {
265  var memberSelectorExpression = (MemberExpression)expression.Body;
266  var property = (PropertyInfo)memberSelectorExpression.Member;
267 
268  var newVal = property.GetValue(model);
269  if (newVal == null)
270  return false;
271  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
272  return true;
273 
274  property.SetValue(current, newVal);
275  anySettingsModified = true;
276  return false;
277  }
278 
279  var oldProvider = current.Provider;
280 
281  if (CheckModified(x => x.ConnectionString, ChatBotRights.WriteConnectionString)
282  || CheckModified(x => x.Enabled, ChatBotRights.WriteEnabled)
283  || CheckModified(x => x.Name, ChatBotRights.WriteName)
284  || CheckModified(x => x.Provider, ChatBotRights.WriteProvider)
285  || CheckModified(x => x.ReconnectionInterval, ChatBotRights.WriteReconnectionInterval)
286  || CheckModified(x => x.ChannelLimit, ChatBotRights.WriteChannelLimit)
287  || (model.Channels != null && !userRights.HasFlag(ChatBotRights.WriteChannels)))
288  return Forbid();
289 
290  var hasChannels = model.Channels != null;
291  if (hasChannels || (model.Provider.HasValue && model.Provider != oldProvider))
292  {
293  DatabaseContext.ChatChannels.RemoveRange(current.Channels);
294  if (hasChannels)
295  {
296  var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList();
297  DatabaseContext.ChatChannels.AddRange(dbChannels);
298  current.Channels = dbChannels;
299  }
300  else
301  current.Channels.Clear();
302  }
303 
304  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
305 
306  var chat = instanceManager.GetInstance(Instance).Chat;
307 
308  if (anySettingsModified)
309  await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false); // have to rebuild the thing first
310 
311  if (model.Channels != null || anySettingsModified)
312  await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false);
313 
314  if (userRights.HasFlag(ChatBotRights.Read))
315  {
316  if (!userRights.HasFlag(ChatBotRights.ReadConnectionString))
317  current.ConnectionString = null;
318  return Json(current.ToApi());
319  }
320 
321  return Ok();
322  }
323 
330  private IActionResult StandardModelChecks(Api.Models.ChatBot model, bool forCreation)
331  {
332  if (model.ReconnectionInterval == 0)
333  throw new InvalidOperationException("RecconnectionInterval cannot be zero!");
334 
335  if (forCreation && !model.Provider.HasValue)
336  return BadRequest(new ErrorMessage(ErrorCode.ChatBotProviderMissing));
337 
338  if (model.Name != null && String.IsNullOrWhiteSpace(model.Name))
339  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWhitespaceName));
340 
341  if (model.ConnectionString != null && String.IsNullOrWhiteSpace(model.ConnectionString))
342  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWhitespaceConnectionString));
343 
344  if (!model.ValidateProviderChannelTypes())
345  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWrongChannelType));
346 
347  var defaultMaxChannels = (ulong)Math.Max(Models.ChatBot.DefaultChannelLimit, model.Channels?.Count ?? 0);
348  if (defaultMaxChannels > UInt16.MaxValue)
349  return BadRequest(new ErrorMessage(ErrorCode.ChatBotMaxChannels));
350 
351  if (forCreation)
352  model.ChannelLimit ??= (ushort)defaultMaxChannels;
353 
354  return null;
355  }
356  }
357  #pragma warning restore CA1506
358 }
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext.
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Linq.Expressions;
5 using System.Net;
6 using System.Reflection;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Microsoft.AspNetCore.Mvc;
10 using Microsoft.EntityFrameworkCore;
11 using Microsoft.Extensions.Logging;
12 using Tgstation.Server.Api;
19 using Z.EntityFramework.Plus;
20 
21 namespace Tgstation.Server.Host.Controllers
22 {
26  [Route(Routes.Chat)]
27  #pragma warning disable CA1506 // TODO: Decomplexify
28  public sealed class ChatController : ApiController
29  {
34 
42  public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
43  {
44  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
45  }
46 
52  static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api) => new Models.ChatChannel
53  {
54  DiscordChannelId = api.DiscordChannelId,
55  IrcChannel = api.IrcChannel,
56  IsAdminChannel = api.IsAdminChannel ?? false,
57  IsWatchdogChannel = api.IsWatchdogChannel ?? false,
58  IsUpdatesChannel = api.IsUpdatesChannel ?? false,
59  Tag = api.Tag
60  };
61 
69  [HttpPut]
70  [TgsAuthorize(ChatBotRights.Create)]
71  [ProducesResponseType(typeof(Api.Models.ChatBot), 201)]
72  public async Task<IActionResult> Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
73  {
74  if (model == null)
75  throw new ArgumentNullException(nameof(model));
76 
77  var earlyOut = StandardModelChecks(model, true);
78  if (earlyOut != null)
79  return earlyOut;
80 
81  var countOfExistingBotsInInstance = await DatabaseContext
82  .ChatBots
83  .AsQueryable()
84  .Where(x => x.InstanceId == Instance.Id)
85  .CountAsync(cancellationToken)
86  .ConfigureAwait(false);
87 
88  if (countOfExistingBotsInInstance >= Instance.ChatBotLimit.Value)
89  return Conflict(new ErrorMessage(ErrorCode.ChatBotMax));
90 
91  model.Enabled ??= false;
92  model.ReconnectionInterval ??= 1;
93 
94  // try to update das db first
95  var dbModel = new Models.ChatBot
96  {
97  Name = model.Name,
98  ConnectionString = model.ConnectionString,
99  Enabled = model.Enabled,
100  Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).ToList() ?? new List<Models.ChatChannel>(), // important that this isn't null
101  InstanceId = Instance.Id,
102  Provider = model.Provider,
103  ReconnectionInterval = model.ReconnectionInterval,
104  ChannelLimit = model.ChannelLimit
105  };
106 
107  DatabaseContext.ChatBots.Add(dbModel);
108 
109  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
110  var instance = instanceManager.GetInstance(Instance);
111  try
112  {
113  // try to create it
114  await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false);
115 
116  if (dbModel.Channels.Count > 0)
117  await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false);
118  }
119  catch
120  {
121  // undo the add
122  DatabaseContext.ChatBots.Remove(dbModel);
123  await DatabaseContext.Save(default).ConfigureAwait(false);
124  await instance.Chat.DeleteConnection(dbModel.Id, cancellationToken).ConfigureAwait(false);
125  throw;
126  }
127 
128  return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
129  }
130 
138  [HttpDelete("{id}")]
139  [TgsAuthorize(ChatBotRights.Delete)]
140  [ProducesResponseType(204)]
141  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
142  {
143  var instance = instanceManager.GetInstance(Instance);
144  await Task.WhenAll(
145  instance.Chat.DeleteConnection(id, cancellationToken),
147  .ChatBots
148  .AsQueryable()
149  .Where(x => x.Id == id)
150  .DeleteAsync(cancellationToken))
151  .ConfigureAwait(false);
152 
153  return Ok();
154  }
155 
162  [HttpGet(Routes.List)]
163  [TgsAuthorize(ChatBotRights.Read)]
164  [ProducesResponseType(typeof(IEnumerable<Api.Models.ChatBot>), 200)]
165  public async Task<IActionResult> List(CancellationToken cancellationToken)
166  {
167  var query = DatabaseContext
168  .ChatBots
169  .AsQueryable()
170  .Where(x => x.InstanceId == Instance.Id)
171  .Include(x => x.Channels);
172 
173  var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
174 
175  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
176 
177  if (!connectionStrings)
178  foreach (var I in results)
179  I.ConnectionString = null;
180 
181  return Json(results.Select(x => x.ToApi()));
182  }
183 
192  [HttpGet("{id}")]
193  [TgsAuthorize(ChatBotRights.Read)]
194  [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
195  [ProducesResponseType(410)]
196  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
197  {
198  var query = DatabaseContext.ChatBots
199  .AsQueryable()
200  .Where(x => x.Id == id)
201  .Include(x => x.Channels);
202 
203  var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
204  if (results == default)
205  return StatusCode((int)HttpStatusCode.Gone);
206 
207  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
208 
209  if (!connectionStrings)
210  results.ConnectionString = null;
211 
212  return Json(results.ToApi());
213  }
214 
222  [HttpPost]
223  [TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)]
224  [ProducesResponseType(200)]
225  [ProducesResponseType(typeof(Api.Models.ChatBot), 200)]
226 #pragma warning disable CA1502 // TODO: Decomplexify
227 #pragma warning disable CA1506
228  public async Task<IActionResult> Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
229  #pragma warning restore CA1502
230  #pragma warning restore CA1506
231  {
232  if (model == null)
233  throw new ArgumentNullException(nameof(model));
234 
235  var earlyOut = StandardModelChecks(model, false);
236  if (earlyOut != null)
237  return earlyOut;
238 
239  var query = DatabaseContext
240  .ChatBots
241  .AsQueryable()
242  .Where(x => x.InstanceId == Instance.Id && x.Id == model.Id)
243  .Include(x => x.Channels);
244 
245  var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
246 
247  if (current == default)
248  return StatusCode((int)HttpStatusCode.Gone);
249 
250  if ((model.Channels?.Count ?? current.Channels.Count) > (model.ChannelLimit ?? current.ChannelLimit.Value))
251  {
252  // 400 or 409 depends on if the client sent both
253  var errorMessage = new ErrorMessage(ErrorCode.ChatBotMaxChannels);
254  if (model.Channels != null && model.ChannelLimit.HasValue)
255  return BadRequest(errorMessage);
256  return Conflict(errorMessage);
257  }
258 
259  var userRights = (ChatBotRights)AuthenticationContext.GetRight(RightsType.ChatBots);
260 
261  bool anySettingsModified = false;
262 
263  bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatBot, T>> expression, ChatBotRights requiredRight)
264  {
265  var memberSelectorExpression = (MemberExpression)expression.Body;
266  var property = (PropertyInfo)memberSelectorExpression.Member;
267 
268  var newVal = property.GetValue(model);
269  if (newVal == null)
270  return false;
271  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
272  return true;
273 
274  property.SetValue(current, newVal);
275  anySettingsModified = true;
276  return false;
277  }
278 
279  var oldProvider = current.Provider;
280 
281  if (CheckModified(x => x.ConnectionString, ChatBotRights.WriteConnectionString)
282  || CheckModified(x => x.Enabled, ChatBotRights.WriteEnabled)
283  || CheckModified(x => x.Name, ChatBotRights.WriteName)
284  || CheckModified(x => x.Provider, ChatBotRights.WriteProvider)
285  || CheckModified(x => x.ReconnectionInterval, ChatBotRights.WriteReconnectionInterval)
286  || CheckModified(x => x.ChannelLimit, ChatBotRights.WriteChannelLimit)
287  || (model.Channels != null && !userRights.HasFlag(ChatBotRights.WriteChannels)))
288  return Forbid();
289 
290  var hasChannels = model.Channels != null;
291  if (hasChannels || (model.Provider.HasValue && model.Provider != oldProvider))
292  {
293  DatabaseContext.ChatChannels.RemoveRange(current.Channels);
294  if (hasChannels)
295  {
296  var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList();
297  DatabaseContext.ChatChannels.AddRange(dbChannels);
298  current.Channels = dbChannels;
299  }
300  else
301  current.Channels.Clear();
302  }
303 
304  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
305 
306  var chat = instanceManager.GetInstance(Instance).Chat;
307 
308  if (anySettingsModified)
309  await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false); // have to rebuild the thing first
310 
311  if (model.Channels != null || anySettingsModified)
312  await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false);
313 
314  if (userRights.HasFlag(ChatBotRights.Read))
315  {
316  if (!userRights.HasFlag(ChatBotRights.ReadConnectionString))
317  current.ConnectionString = null;
318  return Json(current.ToApi());
319  }
320 
321  return Ok();
322  }
323 
330  private IActionResult StandardModelChecks(Api.Models.ChatBot model, bool forCreation)
331  {
332  if (model.ReconnectionInterval == 0)
333  throw new InvalidOperationException("RecconnectionInterval cannot be zero!");
334 
335  if (forCreation && !model.Provider.HasValue)
336  return BadRequest(new ErrorMessage(ErrorCode.ChatBotProviderMissing));
337 
338  if (model.Name != null && String.IsNullOrWhiteSpace(model.Name))
339  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWhitespaceName));
340 
341  if (model.ConnectionString != null && String.IsNullOrWhiteSpace(model.ConnectionString))
342  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWhitespaceConnectionString));
343 
344  if (!model.ValidateProviderChannelTypes())
345  return BadRequest(new ErrorMessage(ErrorCode.ChatBotWrongChannelType));
346 
347  var defaultMaxChannels = (ulong)Math.Max(Models.ChatBot.DefaultChannelLimit, model.Channels?.Count ?? 0);
348  if (defaultMaxChannels > UInt16.MaxValue)
349  return BadRequest(new ErrorMessage(ErrorCode.ChatBotMaxChannels));
350 
351  if (forCreation)
352  model.ChannelLimit ??= (ushort)defaultMaxChannels;
353 
354  return null;
355  }
356  }
357  #pragma warning restore CA1506
358 }
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext.
+
long Id
The ID of the entity.
Definition: EntityId.cs:11
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
@@ -110,7 +111,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
Backend abstract implementation of IDatabaseContext
-
ushort ChatBotLimit
The maximum number of ChatBots the Instance may contain.
Definition: Instance.cs:52
+
ushort ChatBotLimit
The maximum number of ChatBots the Instance may contain.
Definition: Instance.cs:47
Routes to a server actions
Definition: Routes.cs:9
async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Delete a Api.Models.ChatBot.
ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger< ChatController > logger)
Construct a ChatController
@@ -118,8 +119,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task< IActionResult > List(CancellationToken cancellationToken)
List Api.Models.ChatBots.
async Task< IActionResult > Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
Updates a chat bot model .
const string Chat
The Models.ChatBot controller
Definition: Routes.cs:69
- -
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
+ diff --git a/_chat_manager_8cs_source.html b/_chat_manager_8cs_source.html index cf97d65e1c..e745c41e0b 100644 --- a/_chat_manager_8cs_source.html +++ b/_chat_manager_8cs_source.html @@ -94,88 +94,90 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ChatManager.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host.Components.Chat
16 {
18  // TODO: Decomplexify
19  #pragma warning disable CA1506
21  {
22  const string CommonMention = "!tgs";
23 
28 
33 
38 
43 
48 
52  readonly ILoggerFactory loggerFactory;
53 
57  readonly ILogger<ChatManager> logger;
58 
62  readonly IDictionary<string, ICommand> builtinCommands;
63 
67  readonly IDictionary<long, IProvider> providers;
68 
72  readonly IDictionary<ulong, ChannelMapping> mappedChannels;
73 
77  readonly IList<IChatTrackingContext> trackingContexts;
78 
82  readonly CancellationTokenSource handlerCts;
83 
87  readonly List<Models.ChatBot> activeChatBots;
88 
92  readonly object synchronizationLock;
93 
98 
103 
107  TaskCompletionSource<object> connectionsUpdated;
108 
113 
117  bool started;
118 
130  public ChatManager(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<ChatManager> logger, IEnumerable<Models.ChatBot> initialChatBots)
131  {
132  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
133  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
134  this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
135  if (serverControl == null)
136  throw new ArgumentNullException(nameof(serverControl));
137  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
138  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
139  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
140  activeChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
141 
142  restartRegistration = serverControl.RegisterForRestart(this);
143 
144  synchronizationLock = new object();
145 
146  builtinCommands = new Dictionary<string, ICommand>();
147  providers = new Dictionary<long, IProvider>();
148  mappedChannels = new Dictionary<ulong, ChannelMapping>();
149  trackingContexts = new List<IChatTrackingContext>();
150  handlerCts = new CancellationTokenSource();
151  connectionsUpdated = new TaskCompletionSource<object>();
152  channelIdCounter = 1;
153  }
154 
156  public void Dispose()
157  {
158  restartRegistration.Dispose();
159  handlerCts.Dispose();
160  foreach (var I in providers)
161  I.Value.Dispose();
162  }
163 
171  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
172  {
173  IProvider provider;
174  lock (providers)
175  if (!providers.TryGetValue(connectionId, out provider))
176  return null;
177 
178  Task trackingContextsUpdateTask;
179  lock (mappedChannels)
180  {
181  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
182  mappedChannels.Remove(I);
183 
184  var newMappedChannels = mappedChannels.Select(y => y.Value.Channel).ToList();
185 
186  if (updateTrackings)
187  lock (trackingContexts)
188  trackingContextsUpdateTask = Task.WhenAll(trackingContexts.Select(x => x.UpdateChannels(newMappedChannels, cancellationToken)));
189  else
190  trackingContextsUpdateTask = Task.CompletedTask;
191  }
192 
193  await trackingContextsUpdateTask.ConfigureAwait(false);
194 
195  return provider;
196  }
197 
205  #pragma warning disable CA1502
206  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
207  #pragma warning restore CA1502
208  {
209  // provider reconnected, remap channels.
210  if (message == null)
211  {
212  IEnumerable<Api.Models.ChatChannel> channelsToMap;
213  lock (activeChatBots)
214  channelsToMap = activeChatBots.FirstOrDefault()?.Channels;
215 
216  if (channelsToMap?.Any() ?? false)
217  {
218  long providerId;
219  lock (providers)
220  providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
221  await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false);
222  }
223 
224  return;
225  }
226 
227  // map the channel if it's private and we haven't seen it
228  lock (providers)
229  {
230  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
231  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
232  if (message.User.Channel.IsPrivateChannel)
233  lock (mappedChannels)
234  {
235  if (!provider.Connected)
236  return;
237  if (!enumerable.Any())
238  {
239  ulong newId;
240  lock (synchronizationLock)
241  newId = channelIdCounter++;
242  logger.LogTrace(
243  "Mapping private channel {0}:{1} as {2}",
244  message.User.Channel.ConnectionName,
245  message.User.FriendlyName,
246  newId);
247  mappedChannels.Add(newId, new ChannelMapping
248  {
249  IsWatchdogChannel = false,
250  ProviderChannelId = message.User.Channel.RealId,
251  ProviderId = providerId,
252  Channel = message.User.Channel
253  });
254  message.User.Channel.RealId = newId;
255  }
256  else
257  message.User.Channel.RealId = enumerable.First().Key;
258  }
259  else
260  {
261  // need to add tag and isAdminChannel
262  var mapping = enumerable.First().Value;
263  message.User.Channel.Id = mapping.Channel.Id;
264  message.User.Channel.Tag = mapping.Channel.Tag;
265  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
266  }
267  }
268 
269  var splits = new List<string>(message.Content.Trim().Split(' '));
270  var address = splits[0];
271  if (address.Length > 1 && (address.Last() == ':' || address.Last() == ','))
272  address = address[0..^1];
273 
274  address = address.ToUpperInvariant();
275 
276  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
277 
278  // no mention
279  if (!addressed && !message.User.Channel.IsPrivateChannel)
280  return;
281 
282  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
283 
284  if (addressed)
285  splits.RemoveAt(0);
286 
287  if (splits.Count == 0)
288  {
289  // just a mention
290  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
291  return;
292  }
293 
294  var command = splits[0].ToUpperInvariant();
295  splits.RemoveAt(0);
296  var arguments = String.Join(" ", splits);
297 
298  try
299  {
300  ICommand GetCommand(string commandName)
301  {
302  if (!builtinCommands.TryGetValue(commandName, out var handler))
303  {
304  handler = trackingContexts
305  .Where(x => x.CustomCommands != null)
306  .SelectMany(x => x.CustomCommands)
307  .Where(x => x.Name.ToUpperInvariant() == commandName)
308  .FirstOrDefault();
309  }
310 
311  return handler;
312  }
313 
314  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
315 
316  if (command == "HELP" || command == "?")
317  {
318  string helpText;
319  if (splits.Count == 0)
320  {
321  var allCommands = builtinCommands.Select(x => x.Value).ToList();
322  allCommands.AddRange(
323  trackingContexts
324  .Where(x => x.CustomCommands != null)
325  .SelectMany(
326  x => x.CustomCommands));
327  helpText = String.Format(CultureInfo.InvariantCulture, "Available commands (Type '?' or 'help' and then a command name for more details): {0}", String.Join(", ", allCommands.Select(x => x.Name)));
328  }
329  else
330  {
331  var helpHandler = GetCommand(splits[0].ToUpperInvariant());
332  if (helpHandler != default)
333  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
334  else
335  helpText = UnknownCommandMessage;
336  }
337 
338  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
339  return;
340  }
341 
342  var commandHandler = GetCommand(command);
343 
344  if (commandHandler == default)
345  {
346  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
347  return;
348  }
349 
350  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
351  {
352  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
353  return;
354  }
355 
356  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
357  if (result != null)
358  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
359  }
360  catch (OperationCanceledException)
361  {
362  throw;
363  }
364  catch (Exception e)
365  {
366  // error bc custom commands should reply about why it failed
367  logger.LogError("Error processing chat command: {0}", e);
368  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
369  }
370  }
371 
377  async Task MonitorMessages(CancellationToken cancellationToken)
378  {
379  var messageTasks = new Dictionary<IProvider, Task<Message>>();
380  try
381  {
382  while (!cancellationToken.IsCancellationRequested)
383  {
384  // prune disconnected providers
385  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
386  messageTasks.Remove(I.Key);
387 
388  // add new ones
389  Task updatedTask;
390  lock (synchronizationLock)
391  updatedTask = connectionsUpdated.Task;
392  lock (providers)
393  foreach (var I in providers)
394  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
395  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
396 
397  if (messageTasks.Count == 0)
398  {
399  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
400  continue;
401  }
402 
403  // wait for a message
404  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
405 
406  // process completed ones
407  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
408  {
409  var message = await I.Value.ConfigureAwait(false);
410  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
411  messageTasks.Remove(I.Key);
412  }
413  }
414  }
415  catch (OperationCanceledException)
416  {
417  logger.LogTrace("Message processing loop cancelled!");
418  }
419  catch (Exception e)
420  {
421  logger.LogError("Message monitor crashed! Exception: {0}", e);
422  }
423  }
424 
426  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
427  {
428  if (newChannels == null)
429  throw new ArgumentNullException(nameof(newChannels));
430  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
431  if (provider == null)
432  return;
433  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
434  lock (activeChatBots)
435  {
436  var botToUpdate = activeChatBots.FirstOrDefault(bot => bot.Id == connectionId);
437  if (botToUpdate != null)
438  botToUpdate.Channels = newChannels
439  .Select(apiModel => new Models.ChatChannel
440  {
441  DiscordChannelId = apiModel.DiscordChannelId,
442  IrcChannel = apiModel.IrcChannel,
443  IsAdminChannel = apiModel.IsAdminChannel,
444  IsUpdatesChannel = apiModel.IsUpdatesChannel,
445  IsWatchdogChannel = apiModel.IsWatchdogChannel,
446  Tag = apiModel.Tag
447  })
448  .ToList();
449  }
450 
451  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
452  {
453  IsWatchdogChannel = x.IsWatchdogChannel == true,
454  IsUpdatesChannel = x.IsUpdatesChannel == true,
455  IsAdminChannel = x.IsAdminChannel == true,
456  ProviderChannelId = y.RealId,
457  ProviderId = connectionId,
458  Channel = y
459  });
460 
461  ulong baseId;
462  lock (synchronizationLock)
463  {
464  baseId = channelIdCounter;
465  channelIdCounter += (ulong)results.Count;
466  }
467 
468  Task trackingContextUpdateTask;
469  lock (mappedChannels)
470  {
471  lock (providers)
472  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) // aborted again
473  return;
474  foreach (var I in mappings)
475  {
476  var newId = baseId++;
477  logger.LogTrace("Mapping channel {0}:{1} as {2}", I.Channel.ConnectionName, I.Channel.FriendlyName, newId);
478  mappedChannels.Add(newId, I);
479  I.Channel.RealId = newId;
480  }
481 
482  lock (trackingContexts)
483  trackingContextUpdateTask = Task.WhenAll(
484  trackingContexts.Select(
485  x => x.UpdateChannels(
486  mappedChannels.Select(y => y.Value.Channel).ToList(),
487  cancellationToken)));
488  }
489 
490  await trackingContextUpdateTask.ConfigureAwait(false);
491  }
492 
494  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
495  {
496  if (newSettings == null)
497  throw new ArgumentNullException(nameof(newSettings));
498  IProvider provider;
499 
500  async Task DisconnectProvider(IProvider p)
501  {
502  try
503  {
504  await p.Disconnect(cancellationToken).ConfigureAwait(false);
505  }
506  finally
507  {
508  p.Dispose();
509  }
510  }
511 
512  Task disconnectTask;
513  lock (providers)
514  {
515  // raw settings changes forces a rebuild of the provider
516  if (providers.TryGetValue(newSettings.Id, out provider))
517  {
518  providers.Remove(newSettings.Id);
519  disconnectTask = DisconnectProvider(provider);
520  }
521  else
522  disconnectTask = Task.CompletedTask;
523  if (newSettings.Enabled.Value)
524  {
525  provider = providerFactory.CreateProvider(newSettings);
526  providers.Add(newSettings.Id, provider);
527  }
528  }
529 
530  lock (mappedChannels)
531  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
532  mappedChannels.Remove(I);
533 
534  await disconnectTask.ConfigureAwait(false);
535 
536  if (started)
537  {
538  if (newSettings.Enabled.Value)
539  await provider.Connect(cancellationToken).ConfigureAwait(false);
540  lock (synchronizationLock)
541  {
542  // same thread shennanigans
543  var oldOne = connectionsUpdated;
544  connectionsUpdated = new TaskCompletionSource<object>();
545  oldOne.SetResult(null);
546  }
547  }
548 
549  Task reconnectionUpdateTask = Task.CompletedTask;
550  lock (activeChatBots)
551  {
552  var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
553  if (originalChatBot != null)
554  {
555  if (originalChatBot.ReconnectionInterval != newSettings.ReconnectionInterval)
556  reconnectionUpdateTask = provider.SetReconnectInterval(newSettings.ReconnectionInterval.Value);
557 
558  activeChatBots.Remove(originalChatBot);
559  }
560 
561  activeChatBots.Add(new Models.ChatBot
562  {
563  Id = newSettings.Id,
564  ConnectionString = newSettings.ConnectionString,
565  Enabled = newSettings.Enabled,
566  Name = newSettings.Name,
567  ReconnectionInterval = newSettings.ReconnectionInterval,
568  Provider = newSettings.Provider
569  });
570  }
571 
572  await reconnectionUpdateTask.ConfigureAwait(false);
573  }
574 
576  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
577  {
578  if (message == null)
579  throw new ArgumentNullException(nameof(message));
580  if (channelIds == null)
581  throw new ArgumentNullException(nameof(channelIds));
582 
583  logger.LogTrace("Chat send \"{0}\" to channels: {1}", message, String.Join(", ", channelIds));
584 
585  return Task.WhenAll(channelIds.Select(x =>
586  {
587  ChannelMapping channelMapping;
588  lock (mappedChannels)
589  if (!mappedChannels.TryGetValue(x, out channelMapping))
590  return Task.CompletedTask;
591  IProvider provider;
592  lock (providers)
593  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
594  return Task.CompletedTask;
595  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
596  }));
597  }
598 
600  public Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
601  {
602  List<ulong> wdChannels = null;
603  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
604 
605  // so it doesn't change while we're using it
606  lock (mappedChannels)
607  {
608  if (adminOnly)
609  {
610  wdChannels = mappedChannels.Where(x => x.Value.IsAdminChannel).Select(x => x.Key).ToList();
611  if (wdChannels.Count == 0)
612  adminOnly = false;
613  }
614 
615  if (!adminOnly)
616  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
617  }
618 
619  return SendMessage(message, wdChannels, cancellationToken);
620  }
621 
623  public async Task<Func<string, string, Task>> SendDeploymentMessage(
624  Models.RevisionInformation revisionInformation,
625  Version byondVersion,
626  DateTimeOffset? estimatedCompletionTime,
627  string gitHubOwner,
628  string gitHubRepo,
629  bool localCommitPushed,
630  CancellationToken cancellationToken)
631  {
632  List<ulong> wdChannels;
633  lock (mappedChannels) // so it doesn't change while we're using it
634  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
635 
636  logger.LogTrace("Sending deployment message for RevisionInformation: {0}", revisionInformation.Id);
637 
638  var callbacks = new List<Func<string, string, Task>>();
639 
640  await Task.WhenAll(
641  wdChannels.Select(
642  async x =>
643  {
644  ChannelMapping channelMapping;
645  lock (mappedChannels)
646  if (!mappedChannels.TryGetValue(x, out channelMapping))
647  return;
648  IProvider provider;
649  lock (providers)
650  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
651  return;
652  try
653  {
654  var callback = await provider.SendUpdateMessage(
655  revisionInformation,
656  byondVersion,
657  estimatedCompletionTime,
658  gitHubOwner,
659  gitHubRepo,
660  channelMapping.ProviderChannelId,
661  localCommitPushed,
662  cancellationToken)
663  .ConfigureAwait(false);
664 
665  callbacks.Add(callback);
666  }
667  catch (Exception ex)
668  {
669  logger.LogWarning(
670  "Error sending deploy message to provider {0}! Exception: {1}",
671  channelMapping.ProviderId,
672  ex);
673  }
674  }))
675  .ConfigureAwait(false);
676 
677  return (errorMessage, dreamMakerOutput) => Task.WhenAll(callbacks.Select(x => x(errorMessage, dreamMakerOutput)));
678  }
679 
681  public async Task StartAsync(CancellationToken cancellationToken)
682  {
683  foreach (var I in commandFactory.GenerateCommands())
684  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
685  var initialChatBots = activeChatBots.ToList();
686  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
687  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
688  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
689  chatHandler = MonitorMessages(handlerCts.Token);
690  started = true;
691  }
692 
694  public async Task StopAsync(CancellationToken cancellationToken)
695  {
696  handlerCts.Cancel();
697  if (chatHandler != null)
698  await chatHandler.ConfigureAwait(false);
699  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
700  }
701 
704  {
705  if (customCommandHandler == null)
706  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
707 
708  IChatTrackingContext context = null;
709  lock (mappedChannels)
710  context = new ChatTrackingContext(
711  customCommandHandler,
712  mappedChannels.Select(y => y.Value.Channel),
713  loggerFactory.CreateLogger<ChatTrackingContext>(),
714  () =>
715  {
716  lock (trackingContexts)
717  trackingContexts.Remove(context);
718  });
719 
720  lock (trackingContexts)
721  trackingContexts.Add(context);
722 
723  return context;
724  }
725 
727  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
728  {
729  if (this.customCommandHandler != null)
730  throw new InvalidOperationException("RegisterCommandHandler() already called!");
731  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
732  }
733 
735  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
736  {
737  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
738  if (provider != null)
739  try
740  {
741  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
742  }
743  finally
744  {
745  provider.Dispose();
746  }
747  }
748 
750  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
751  {
752  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
753  List<ulong> wdChannels;
754  lock (mappedChannels) // so it doesn't change while we're using it
755  wdChannels = mappedChannels.Select(x => x.Key).ToList();
756  return SendMessage(message, wdChannels, cancellationToken);
757  }
758  }
759 }
bool IsWatchdogChannel
If Channel is a watchdog channel
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using Serilog.Context;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
14 using Tgstation.Server.Host.IO;
15 
16 namespace Tgstation.Server.Host.Components.Chat
17 {
19  // TODO: Decomplexify
20  #pragma warning disable CA1506
22  {
23  const string CommonMention = "!tgs";
24 
29 
34 
39 
44 
49 
53  readonly ILoggerFactory loggerFactory;
54 
58  readonly ILogger<ChatManager> logger;
59 
63  readonly IDictionary<string, ICommand> builtinCommands;
64 
68  readonly IDictionary<long, IProvider> providers;
69 
73  readonly IDictionary<ulong, ChannelMapping> mappedChannels;
74 
78  readonly IList<IChatTrackingContext> trackingContexts;
79 
83  readonly CancellationTokenSource handlerCts;
84 
88  readonly List<Models.ChatBot> activeChatBots;
89 
93  readonly object synchronizationLock;
94 
99 
104 
108  TaskCompletionSource<object> connectionsUpdated;
109 
114 
119 
123  bool started;
124 
136  public ChatManager(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<ChatManager> logger, IEnumerable<Models.ChatBot> initialChatBots)
137  {
138  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
139  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
140  this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
141  if (serverControl == null)
142  throw new ArgumentNullException(nameof(serverControl));
143  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
144  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
145  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
146  activeChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
147 
148  restartRegistration = serverControl.RegisterForRestart(this);
149 
150  synchronizationLock = new object();
151 
152  builtinCommands = new Dictionary<string, ICommand>();
153  providers = new Dictionary<long, IProvider>();
154  mappedChannels = new Dictionary<ulong, ChannelMapping>();
155  trackingContexts = new List<IChatTrackingContext>();
156  handlerCts = new CancellationTokenSource();
157  connectionsUpdated = new TaskCompletionSource<object>();
158  channelIdCounter = 1;
159  }
160 
162  public void Dispose()
163  {
164  restartRegistration.Dispose();
165  handlerCts.Dispose();
166  foreach (var I in providers)
167  I.Value.Dispose();
168  }
169 
177  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
178  {
179  IProvider provider;
180  lock (providers)
181  if (!providers.TryGetValue(connectionId, out provider))
182  return null;
183 
184  Task trackingContextsUpdateTask;
185  lock (mappedChannels)
186  {
187  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
188  mappedChannels.Remove(I);
189 
190  var newMappedChannels = mappedChannels.Select(y => y.Value.Channel).ToList();
191 
192  if (updateTrackings)
193  lock (trackingContexts)
194  trackingContextsUpdateTask = Task.WhenAll(trackingContexts.Select(x => x.UpdateChannels(newMappedChannels, cancellationToken)));
195  else
196  trackingContextsUpdateTask = Task.CompletedTask;
197  }
198 
199  await trackingContextsUpdateTask.ConfigureAwait(false);
200 
201  return provider;
202  }
203 
211  #pragma warning disable CA1502
212  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
213  #pragma warning restore CA1502
214  {
215  // provider reconnected, remap channels.
216  if (message == null)
217  {
218  IEnumerable<Api.Models.ChatChannel> channelsToMap;
219  lock (activeChatBots)
220  channelsToMap = activeChatBots.FirstOrDefault()?.Channels;
221 
222  if (channelsToMap?.Any() ?? false)
223  {
224  long providerId;
225  lock (providers)
226  providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
227  await ChangeChannels(providerId, channelsToMap, cancellationToken).ConfigureAwait(false);
228  }
229 
230  return;
231  }
232 
233  // map the channel if it's private and we haven't seen it
234  lock (providers)
235  {
236  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
237  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
238  if (message.User.Channel.IsPrivateChannel)
239  lock (mappedChannels)
240  {
241  if (!provider.Connected)
242  return;
243  if (!enumerable.Any())
244  {
245  ulong newId;
246  lock (synchronizationLock)
247  newId = channelIdCounter++;
248  logger.LogTrace(
249  "Mapping private channel {0}:{1} as {2}",
250  message.User.Channel.ConnectionName,
251  message.User.FriendlyName,
252  newId);
253  mappedChannels.Add(newId, new ChannelMapping
254  {
255  IsWatchdogChannel = false,
256  ProviderChannelId = message.User.Channel.RealId,
257  ProviderId = providerId,
258  Channel = message.User.Channel
259  });
260  message.User.Channel.RealId = newId;
261  }
262  else
263  message.User.Channel.RealId = enumerable.First().Key;
264  }
265  else
266  {
267  // need to add tag and isAdminChannel
268  var mapping = enumerable.First().Value;
269  message.User.Channel.Id = mapping.Channel.Id;
270  message.User.Channel.Tag = mapping.Channel.Tag;
271  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
272  }
273  }
274 
275  var splits = new List<string>(message.Content.Trim().Split(' '));
276  var address = splits[0];
277  if (address.Length > 1 && (address.Last() == ':' || address.Last() == ','))
278  address = address[0..^1];
279 
280  address = address.ToUpperInvariant();
281 
282  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
283 
284  // no mention
285  if (!addressed && !message.User.Channel.IsPrivateChannel)
286  return;
287 
288  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
289 
290  if (addressed)
291  splits.RemoveAt(0);
292 
293  if (splits.Count == 0)
294  {
295  // just a mention
296  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
297  return;
298  }
299 
300  var command = splits[0].ToUpperInvariant();
301  splits.RemoveAt(0);
302  var arguments = String.Join(" ", splits);
303 
304  try
305  {
306  ICommand GetCommand(string commandName)
307  {
308  if (!builtinCommands.TryGetValue(commandName, out var handler))
309  {
310  handler = trackingContexts
311  .Where(x => x.CustomCommands != null)
312  .SelectMany(x => x.CustomCommands)
313  .Where(x => x.Name.ToUpperInvariant() == commandName)
314  .FirstOrDefault();
315  }
316 
317  return handler;
318  }
319 
320  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
321 
322  if (command == "HELP" || command == "?")
323  {
324  string helpText;
325  if (splits.Count == 0)
326  {
327  var allCommands = builtinCommands.Select(x => x.Value).ToList();
328  allCommands.AddRange(
329  trackingContexts
330  .Where(x => x.CustomCommands != null)
331  .SelectMany(
332  x => x.CustomCommands));
333  helpText = String.Format(CultureInfo.InvariantCulture, "Available commands (Type '?' or 'help' and then a command name for more details): {0}", String.Join(", ", allCommands.Select(x => x.Name)));
334  }
335  else
336  {
337  var helpHandler = GetCommand(splits[0].ToUpperInvariant());
338  if (helpHandler != default)
339  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
340  else
341  helpText = UnknownCommandMessage;
342  }
343 
344  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
345  return;
346  }
347 
348  var commandHandler = GetCommand(command);
349 
350  if (commandHandler == default)
351  {
352  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
353  return;
354  }
355 
356  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
357  {
358  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
359  return;
360  }
361 
362  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
363  if (result != null)
364  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
365  }
366  catch (OperationCanceledException)
367  {
368  throw;
369  }
370  catch (Exception e)
371  {
372  // error bc custom commands should reply about why it failed
373  logger.LogError("Error processing chat command: {0}", e);
374  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
375  }
376  }
377 
383  async Task MonitorMessages(CancellationToken cancellationToken)
384  {
385  var messageTasks = new Dictionary<IProvider, Task<Message>>();
386  try
387  {
388  while (!cancellationToken.IsCancellationRequested)
389  {
390  // prune disconnected providers
391  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
392  messageTasks.Remove(I.Key);
393 
394  // add new ones
395  Task updatedTask;
396  lock (synchronizationLock)
397  updatedTask = connectionsUpdated.Task;
398  lock (providers)
399  foreach (var I in providers)
400  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
401  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
402 
403  if (messageTasks.Count == 0)
404  {
405  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
406  continue;
407  }
408 
409  // wait for a message
410  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
411 
412  // process completed ones
413  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
414  {
415  var message = await I.Value.ConfigureAwait(false);
416  var messageNumber = Interlocked.Increment(ref messagesProcessed);
417  using (LogContext.PushProperty("ChatMessage", messageNumber))
418  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
419  messageTasks.Remove(I.Key);
420  }
421  }
422  }
423  catch (OperationCanceledException)
424  {
425  logger.LogTrace("Message processing loop cancelled!");
426  }
427  catch (Exception e)
428  {
429  logger.LogError("Message monitor crashed! Exception: {0}", e);
430  }
431  }
432 
434  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
435  {
436  if (newChannels == null)
437  throw new ArgumentNullException(nameof(newChannels));
438  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
439  if (provider == null)
440  return;
441  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
442  lock (activeChatBots)
443  {
444  var botToUpdate = activeChatBots.FirstOrDefault(bot => bot.Id == connectionId);
445  if (botToUpdate != null)
446  botToUpdate.Channels = newChannels
447  .Select(apiModel => new Models.ChatChannel
448  {
449  DiscordChannelId = apiModel.DiscordChannelId,
450  IrcChannel = apiModel.IrcChannel,
451  IsAdminChannel = apiModel.IsAdminChannel,
452  IsUpdatesChannel = apiModel.IsUpdatesChannel,
453  IsWatchdogChannel = apiModel.IsWatchdogChannel,
454  Tag = apiModel.Tag
455  })
456  .ToList();
457  }
458 
459  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
460  {
461  IsWatchdogChannel = x.IsWatchdogChannel == true,
462  IsUpdatesChannel = x.IsUpdatesChannel == true,
463  IsAdminChannel = x.IsAdminChannel == true,
464  ProviderChannelId = y.RealId,
465  ProviderId = connectionId,
466  Channel = y
467  });
468 
469  ulong baseId;
470  lock (synchronizationLock)
471  {
472  baseId = channelIdCounter;
473  channelIdCounter += (ulong)results.Count;
474  }
475 
476  Task trackingContextUpdateTask;
477  lock (mappedChannels)
478  {
479  lock (providers)
480  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) // aborted again
481  return;
482  foreach (var I in mappings)
483  {
484  var newId = baseId++;
485  logger.LogTrace("Mapping channel {0}:{1} as {2}", I.Channel.ConnectionName, I.Channel.FriendlyName, newId);
486  mappedChannels.Add(newId, I);
487  I.Channel.RealId = newId;
488  }
489 
490  lock (trackingContexts)
491  trackingContextUpdateTask = Task.WhenAll(
492  trackingContexts.Select(
493  x => x.UpdateChannels(
494  mappedChannels.Select(y => y.Value.Channel).ToList(),
495  cancellationToken)));
496  }
497 
498  await trackingContextUpdateTask.ConfigureAwait(false);
499  }
500 
502  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
503  {
504  if (newSettings == null)
505  throw new ArgumentNullException(nameof(newSettings));
506  IProvider provider;
507 
508  async Task DisconnectProvider(IProvider p)
509  {
510  try
511  {
512  await p.Disconnect(cancellationToken).ConfigureAwait(false);
513  }
514  finally
515  {
516  p.Dispose();
517  }
518  }
519 
520  Task disconnectTask;
521  lock (providers)
522  {
523  // raw settings changes forces a rebuild of the provider
524  if (providers.TryGetValue(newSettings.Id, out provider))
525  {
526  providers.Remove(newSettings.Id);
527  disconnectTask = DisconnectProvider(provider);
528  }
529  else
530  disconnectTask = Task.CompletedTask;
531  if (newSettings.Enabled.Value)
532  {
533  provider = providerFactory.CreateProvider(newSettings);
534  providers.Add(newSettings.Id, provider);
535  }
536  }
537 
538  lock (mappedChannels)
539  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
540  mappedChannels.Remove(I);
541 
542  await disconnectTask.ConfigureAwait(false);
543 
544  if (started)
545  {
546  if (newSettings.Enabled.Value)
547  await provider.Connect(cancellationToken).ConfigureAwait(false);
548  lock (synchronizationLock)
549  {
550  // same thread shennanigans
551  var oldOne = connectionsUpdated;
552  connectionsUpdated = new TaskCompletionSource<object>();
553  oldOne.SetResult(null);
554  }
555  }
556 
557  Task reconnectionUpdateTask = Task.CompletedTask;
558  lock (activeChatBots)
559  {
560  var originalChatBot = activeChatBots.FirstOrDefault(bot => bot.Id == newSettings.Id);
561  if (originalChatBot != null)
562  {
563  if (originalChatBot.ReconnectionInterval != newSettings.ReconnectionInterval)
564  reconnectionUpdateTask = provider.SetReconnectInterval(newSettings.ReconnectionInterval.Value);
565 
566  activeChatBots.Remove(originalChatBot);
567  }
568 
569  activeChatBots.Add(new Models.ChatBot
570  {
571  Id = newSettings.Id,
572  ConnectionString = newSettings.ConnectionString,
573  Enabled = newSettings.Enabled,
574  Name = newSettings.Name,
575  ReconnectionInterval = newSettings.ReconnectionInterval,
576  Provider = newSettings.Provider
577  });
578  }
579 
580  await reconnectionUpdateTask.ConfigureAwait(false);
581  }
582 
584  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
585  {
586  if (message == null)
587  throw new ArgumentNullException(nameof(message));
588  if (channelIds == null)
589  throw new ArgumentNullException(nameof(channelIds));
590 
591  logger.LogTrace("Chat send \"{0}\" to channels: {1}", message, String.Join(", ", channelIds));
592 
593  return Task.WhenAll(channelIds.Select(x =>
594  {
595  ChannelMapping channelMapping;
596  lock (mappedChannels)
597  if (!mappedChannels.TryGetValue(x, out channelMapping))
598  return Task.CompletedTask;
599  IProvider provider;
600  lock (providers)
601  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
602  return Task.CompletedTask;
603  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
604  }));
605  }
606 
608  public Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
609  {
610  List<ulong> wdChannels = null;
611  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
612 
613  // so it doesn't change while we're using it
614  lock (mappedChannels)
615  {
616  if (adminOnly)
617  {
618  wdChannels = mappedChannels.Where(x => x.Value.IsAdminChannel).Select(x => x.Key).ToList();
619  if (wdChannels.Count == 0)
620  adminOnly = false;
621  }
622 
623  if (!adminOnly)
624  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
625  }
626 
627  return SendMessage(message, wdChannels, cancellationToken);
628  }
629 
631  public async Task<Func<string, string, Task>> SendDeploymentMessage(
632  Models.RevisionInformation revisionInformation,
633  Version byondVersion,
634  DateTimeOffset? estimatedCompletionTime,
635  string gitHubOwner,
636  string gitHubRepo,
637  bool localCommitPushed,
638  CancellationToken cancellationToken)
639  {
640  List<ulong> wdChannels;
641  lock (mappedChannels) // so it doesn't change while we're using it
642  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
643 
644  logger.LogTrace("Sending deployment message for RevisionInformation: {0}", revisionInformation.Id);
645 
646  var callbacks = new List<Func<string, string, Task>>();
647 
648  await Task.WhenAll(
649  wdChannels.Select(
650  async x =>
651  {
652  ChannelMapping channelMapping;
653  lock (mappedChannels)
654  if (!mappedChannels.TryGetValue(x, out channelMapping))
655  return;
656  IProvider provider;
657  lock (providers)
658  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
659  return;
660  try
661  {
662  var callback = await provider.SendUpdateMessage(
663  revisionInformation,
664  byondVersion,
665  estimatedCompletionTime,
666  gitHubOwner,
667  gitHubRepo,
668  channelMapping.ProviderChannelId,
669  localCommitPushed,
670  cancellationToken)
671  .ConfigureAwait(false);
672 
673  callbacks.Add(callback);
674  }
675  catch (Exception ex)
676  {
677  logger.LogWarning(
678  "Error sending deploy message to provider {0}! Exception: {1}",
679  channelMapping.ProviderId,
680  ex);
681  }
682  }))
683  .ConfigureAwait(false);
684 
685  return (errorMessage, dreamMakerOutput) => Task.WhenAll(callbacks.Select(x => x(errorMessage, dreamMakerOutput)));
686  }
687 
689  public async Task StartAsync(CancellationToken cancellationToken)
690  {
691  foreach (var I in commandFactory.GenerateCommands())
692  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
693  var initialChatBots = activeChatBots.ToList();
694  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
695  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
696  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
697  chatHandler = MonitorMessages(handlerCts.Token);
698  started = true;
699  }
700 
702  public async Task StopAsync(CancellationToken cancellationToken)
703  {
704  handlerCts.Cancel();
705  if (chatHandler != null)
706  await chatHandler.ConfigureAwait(false);
707  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
708  }
709 
712  {
713  if (customCommandHandler == null)
714  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
715 
716  IChatTrackingContext context = null;
717  lock (mappedChannels)
718  context = new ChatTrackingContext(
719  customCommandHandler,
720  mappedChannels.Select(y => y.Value.Channel),
721  loggerFactory.CreateLogger<ChatTrackingContext>(),
722  () =>
723  {
724  lock (trackingContexts)
725  trackingContexts.Remove(context);
726  });
727 
728  lock (trackingContexts)
729  trackingContexts.Add(context);
730 
731  return context;
732  }
733 
735  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
736  {
737  if (this.customCommandHandler != null)
738  throw new InvalidOperationException("RegisterCommandHandler() already called!");
739  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
740  }
741 
743  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
744  {
745  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
746  if (provider != null)
747  try
748  {
749  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
750  }
751  finally
752  {
753  provider.Dispose();
754  }
755  }
756 
758  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
759  {
760  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
761  List<ulong> wdChannels;
762  lock (mappedChannels) // so it doesn't change while we're using it
763  wdChannels = mappedChannels.Select(x => x.Key).ToList();
764  return SendMessage(message, wdChannels, cancellationToken);
765  }
766  }
767 }
bool IsWatchdogChannel
If Channel is a watchdog channel
Represents a mapping of a ChannelRepresentation.RealId
-
ulong channelIdCounter
Used for remapping ChannelRepresentation.RealIds
Definition: ChatManager.cs:112
-
Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
Definition: ChatManager.cs:600
-
async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
Change chat settings. If the ChatBot.Id is not currently in use, a new connection will be made instea...
Definition: ChatManager.cs:494
-
readonly ICommandFactory commandFactory
The ICommandFactory for the ChatManager
Definition: ChatManager.cs:37
+
ulong channelIdCounter
Used for remapping ChannelRepresentation.RealIds
Definition: ChatManager.cs:113
+
Task SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
Definition: ChatManager.cs:608
+
async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
Change chat settings. If the ChatBot.Id is not currently in use, a new connection will be made instea...
Definition: ChatManager.cs:502
+
readonly ICommandFactory commandFactory
The ICommandFactory for the ChatManager
Definition: ChatManager.cs:38
-
ChatUser User
The Components.Chat.ChatUser who sent the Message
Definition: Message.cs:16
+
ChatUser User
The ChatUser who sent the Message
Definition: Message.cs:16
-
readonly IDictionary< string, ICommand > builtinCommands
Unchanging ICommands in the ChatManager mapped by ICommand.Name
Definition: ChatManager.cs:62
- -
ChatManager(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger< ChatManager > logger, IEnumerable< Models.ChatBot > initialChatBots)
Construct a ChatManager
Definition: ChatManager.cs:130
+
readonly IDictionary< string, ICommand > builtinCommands
Unchanging ICommands in the ChatManager mapped by ICommand.Name
Definition: ChatManager.cs:63
+ +
ChatManager(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger< ChatManager > logger, IEnumerable< Models.ChatBot > initialChatBots)
Construct a ChatManager
Definition: ChatManager.cs:136
Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
-
readonly IDictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBot.Id
Definition: ChatManager.cs:67
+
readonly IDictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBot.Id
Definition: ChatManager.cs:68
- + +
bool IsAdminChannel
If this is considered a channel for admin commands
-
async Task< IProvider > RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
Remove a IProvider from providers and mappedChannels optionally updating the trackingContexts as well...
Definition: ChatManager.cs:171
-
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: ChatManager.cs:750
+
async Task< IProvider > RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
Remove a IProvider from providers and mappedChannels optionally updating the trackingContexts as well...
Definition: ChatManager.cs:177
+
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: ChatManager.cs:758
Represents a message recieved by a IProvider
Definition: Message.cs:6
-
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection
Definition: ChatManager.cs:735
+
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection
Definition: ChatManager.cs:743
For interacting with a chat service
Definition: IProvider.cs:12
-
Task chatHandler
The Task that monitors incoming chat messages
Definition: ChatManager.cs:102
-
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the ChatManager
Definition: ChatManager.cs:42
+
Task chatHandler
The Task that monitors incoming chat messages
Definition: ChatManager.cs:103
+
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the ChatManager
Definition: ChatManager.cs:43
Manage the server chat bots
Definition: ChatBot.cs:9
-
readonly IList< IChatTrackingContext > trackingContexts
The active IChatTrackingContexts for the ChatManager
Definition: ChatManager.cs:77
+
readonly IList< IChatTrackingContext > trackingContexts
The active IChatTrackingContexts for the ChatManager
Definition: ChatManager.cs:78
For managing connected chat services
Definition: IChatManager.cs:13
string Id
Backing field for RealId. Represented as a string to avoid BYOND percision loss
-
async Task< Func< string, string, Task > > SendDeploymentMessage(Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, bool localCommitPushed, CancellationToken cancellationToken)
Send the message for a deployment to configured deployment channels.
Definition: ChatManager.cs:623
+
async Task< Func< string, string, Task > > SendDeploymentMessage(Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, bool localCommitPushed, CancellationToken cancellationToken)
Send the message for a deployment to configured deployment channels.
Definition: ChatManager.cs:631
string FriendlyName
The friendly name of the user
Definition: ChatUser.cs:30
-
IChatTrackingContext CreateTrackingContext()
Start tracking Commands.CustomCommands and ChannelRepresentations.
Definition: ChatManager.cs:703
+
IChatTrackingContext CreateTrackingContext()
Start tracking Commands.CustomCommands and ChannelRepresentations.
Definition: ChatManager.cs:711
string ConnectionName
The name of the connection the ChannelRepresentation belongs to
-
readonly CancellationTokenSource handlerCts
The CancellationTokenSource for chatHandler
Definition: ChatManager.cs:82
+
readonly CancellationTokenSource handlerCts
The CancellationTokenSource for chatHandler
Definition: ChatManager.cs:83
-
ICustomCommandHandler customCommandHandler
The ICustomCommandHandler for the ChangeChannels(long, IEnumerable<Api.Models.ChatChannel>, CancellationToken)
Definition: ChatManager.cs:97
+
ICustomCommandHandler customCommandHandler
The ICustomCommandHandler for the ChangeChannels(long, IEnumerable<Api.Models.ChatChannel>, CancellationToken)
Definition: ChatManager.cs:98
Task< bool > Connect(CancellationToken cancellationToken)
Attempt to connect the IProvider
-
readonly object synchronizationLock
Used for various lock statements throughout this .
Definition: ChatManager.cs:92
+
readonly object synchronizationLock
Used for various lock statements throughout this .
Definition: ChatManager.cs:93
-
bool started
If StartAsync(CancellationToken) has been called
Definition: ChatManager.cs:117
-
readonly IIOManager ioManager
The IIOManager for the ChatManager
Definition: ChatManager.cs:32
+
bool started
If StartAsync(CancellationToken) has been called
Definition: ChatManager.cs:123
+
readonly IIOManager ioManager
The IIOManager for the ChatManager
Definition: ChatManager.cs:33
-
async Task ChangeChannels(long connectionId, IEnumerable< Api.Models.ChatChannel > newChannels, CancellationToken cancellationToken)
Change chat channels
Definition: ChatManager.cs:426
+
async Task ChangeChannels(long connectionId, IEnumerable< Api.Models.ChatChannel > newChannels, CancellationToken cancellationToken)
Change chat channels
Definition: ChatManager.cs:434
string BotMention
The string that indicates the IProvider was mentioned
Definition: IProvider.cs:22
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:694
-
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: ChatManager.cs:727
-
TaskCompletionSource< object > connectionsUpdated
The TaskCompletionSource<TResult> that completes when ChatBots change
Definition: ChatManager.cs:107
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:702
+
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: ChatManager.cs:735
+
TaskCompletionSource< object > connectionsUpdated
The TaskCompletionSource<TResult> that completes when ChatBots change
Definition: ChatManager.cs:108
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:681
-
readonly List< Models.ChatBot > activeChatBots
The active Models.ChatBot for the ChatManager
Definition: ChatManager.cs:87
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: ChatManager.cs:689
+
readonly List< Models.ChatBot > activeChatBots
The active Models.ChatBot for the ChatManager
Definition: ChatManager.cs:88
Handles Commands.ICommands that map to those defined in a IChatTrackingContext
-
Task SendMessage(string message, IEnumerable< ulong > channelIds, CancellationToken cancellationToken)
Send a chat message to a given set of channelIds
Definition: ChatManager.cs:576
+
Task SendMessage(string message, IEnumerable< ulong > channelIds, CancellationToken cancellationToken)
Send a chat message to a given set of channelIds
Definition: ChatManager.cs:584
Represents a tracking of dynamic chat json files
-
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ChatManager
Definition: ChatManager.cs:52
-
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
Processes a message
Definition: ChatManager.cs:206
+
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ChatManager
Definition: ChatManager.cs:53
+
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
Processes a message
Definition: ChatManager.cs:212
Interface for using filesystems
Definition: IIOManager.cs:11
-
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the ChatManager
Definition: ChatManager.cs:47
+
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the ChatManager
Definition: ChatManager.cs:48
ChannelRepresentation Channel
The ChannelRepresentation the user spoke from
Definition: ChatUser.cs:40
-
readonly IDictionary< ulong, ChannelMapping > mappedChannels
Map of ChannelRepresentation.RealIds to ChannelMappings
Definition: ChatManager.cs:72
+
readonly IDictionary< ulong, ChannelMapping > mappedChannels
Map of ChannelRepresentation.RealIds to ChannelMappings
Definition: ChatManager.cs:73
Represents the lifetime of a IRestartHandler registration
-
readonly IProviderFactory providerFactory
The IProviderFactory for the ChatManager
Definition: ChatManager.cs:27
+
readonly IProviderFactory providerFactory
The IProviderFactory for the ChatManager
Definition: ChatManager.cs:28
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
-
readonly ILogger< ChatManager > logger
The ILogger for the ChatManager
Definition: ChatManager.cs:57
+
readonly ILogger< ChatManager > logger
The ILogger for the ChatManager
Definition: ChatManager.cs:58
bool Enabled
If the connection is enabled
Definition: ChatBot.cs:26
-
async Task MonitorMessages(CancellationToken cancellationToken)
Monitors active providers for new Messages
Definition: ChatManager.cs:377
+
async Task MonitorMessages(CancellationToken cancellationToken)
Monitors active providers for new Messages
Definition: ChatManager.cs:383
Represents a command that can be invoked by talking to chat bots
Definition: ICommand.cs:9
Task< IReadOnlyCollection< ChannelRepresentation > > MapChannels(IEnumerable< Api.Models.ChatChannel > channels, CancellationToken cancellationToken)
Get the ChannelRepresentations for given channels
+
long messagesProcessed
The number of Messages processed.
Definition: ChatManager.cs:118
Task SetReconnectInterval(uint reconnectInterval)
Set the interval at which the provider tries to reconnect.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_chat_manager_factory_8cs_source.html b/_chat_manager_factory_8cs_source.html index a6f8c348cd..2820382947 100644 --- a/_chat_manager_factory_8cs_source.html +++ b/_chat_manager_factory_8cs_source.html @@ -96,7 +96,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
7 using Tgstation.Server.Host.IO;
8 
9 namespace Tgstation.Server.Host.Components.Chat
10 {
13  {
17  readonly ILoggerFactory loggerFactory;
18 
23 
28 
33 
41  public ChatManagerFactory(IProviderFactory providerFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory)
42  {
43  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
44  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
45  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
46  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
47  }
48 
50  public IChatManager CreateChatManager(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable<Models.ChatBot> initialChatBots) => new ChatManager(providerFactory, ioManager, commandFactory, serverControl, asyncDelayer, loggerFactory, loggerFactory.CreateLogger<ChatManager>(), initialChatBots);
51  }
52 }
- +
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the ChatManagerFactory
diff --git a/_claims_injector_8cs_source.html b/_claims_injector_8cs_source.html index 693e0bc6b0..4d2a45ef24 100644 --- a/_claims_injector_8cs_source.html +++ b/_claims_injector_8cs_source.html @@ -109,7 +109,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
static Type RightToType(RightsType rightsType)
Map a given rightsType to its respective Enum Type
For injecting global::System.Security.Claims.Claims that Controllers.TgsAuthorizeAttribute can look f...
-
long InstanceId
The Models.Instance.Id being accessed
Definition: ApiHeaders.cs:67
+
long InstanceId
The instance Models.EntityId.Id being accessed
Definition: ApiHeaders.cs:67
static string RoleName(RightsType rightsType, Enum right)
Gets the role claim name used for a given rightsType and right
Definition: RightsHelper.cs:61
diff --git a/_components_2_watchdog_2_i_watchdog_8cs_source.html b/_components_2_watchdog_2_i_watchdog_8cs_source.html index 6f929aafcf..e23055fae4 100644 --- a/_components_2_watchdog_2_i_watchdog_8cs_source.html +++ b/_components_2_watchdog_2_i_watchdog_8cs_source.html @@ -94,12 +94,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IWatchdog.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Threading;
4 using System.Threading.Tasks;
8 
9 namespace Tgstation.Server.Host.Components.Watchdog
10 {
15  {
19  bool Running { get; }
20 
24  bool AlphaIsActive { get; }
25 
29  Models.CompileJob ActiveCompileJob { get; }
30 
34  DreamDaemonLaunchParameters ActiveLaunchParameters { get; }
35 
39  DreamDaemonLaunchParameters LastLaunchParameters { get; }
40 
45 
51  Task Launch(CancellationToken cancellationToken);
52 
59  Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken);
60 
67  Task Restart(bool graceful, CancellationToken cancellationToken);
68 
75  Task Terminate(bool graceful, CancellationToken cancellationToken);
76 
82  Task ResetRebootState(CancellationToken cancellationToken);
83  }
84 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Threading;
4 using System.Threading.Tasks;
8 
9 namespace Tgstation.Server.Host.Components.Watchdog
10 {
15  {
19  bool Running { get; }
20 
24  bool AlphaIsActive { get; }
25 
29  Models.CompileJob ActiveCompileJob { get; }
30 
34  DreamDaemonLaunchParameters ActiveLaunchParameters { get; }
35 
39  DreamDaemonLaunchParameters LastLaunchParameters { get; }
40 
45 
51  Task Launch(CancellationToken cancellationToken);
52 
59  Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken);
60 
67  Task Restart(bool graceful, CancellationToken cancellationToken);
68 
75  Task Terminate(bool graceful, CancellationToken cancellationToken);
76 
82  Task ResetRebootState(CancellationToken cancellationToken);
83  }
84 }
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
+
Handler for an instance being renamed.
Consumes EventTypes and takes the appropriate actions
diff --git a/_components_2_watchdog_2_watchdog_factory_8cs_source.html b/_components_2_watchdog_2_watchdog_factory_8cs_source.html index dcaa66f3cc..59b0029cd7 100644 --- a/_components_2_watchdog_2_watchdog_factory_8cs_source.html +++ b/_components_2_watchdog_2_watchdog_factory_8cs_source.html @@ -101,7 +101,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
- +
bool AutoStart
If DreamDaemon starts when it&#39;s Instance starts
diff --git a/_configuration_controller_8cs_source.html b/_configuration_controller_8cs_source.html index 90adfaba3e..dc6748e4f8 100644 --- a/_configuration_controller_8cs_source.html +++ b/_configuration_controller_8cs_source.html @@ -117,7 +117,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Routes to a server actions
Definition: Routes.cs:9
readonly IIOManager ioManager
The IIOManager for the ConfigurationController
- +
Represents a game configuration file. Create and delete actions uncerimonuously overwrite/delete file...
@@ -136,7 +136,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
ConfigurationType ConfigurationType
If ConfigurationFile can be used on the Instance
Definition: Instance.cs:40
+
ConfigurationType ConfigurationType
If ConfigurationFile can be used on the Instance
Definition: Instance.cs:35
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Components.Session
8 {
13  {
15  public Task<LaunchResult> LaunchResult { get; }
16 
18  public bool IsPrimary => false;
19 
21  public bool TerminationWasRequested => false;
22 
24  public ApiValidationStatus ApiValidationStatus => throw new NotSupportedException();
25 
27  public IDmbProvider Dmb { get; }
28 
30  public ushort? Port => null;
31 
33  public bool ClosePortOnReboot
34  {
35  get => false;
36  set => throw new NotSupportedException();
37  }
38 
40  public RebootState RebootState => throw new NotSupportedException();
41 
43  public Task OnReboot { get; }
44 
46  public Task OnPrime { get; }
47 
49  public Task<int> Lifetime { get; }
50 
52  public Version DMApiVersion => throw new NotSupportedException();
53 
57  readonly object disposeLock;
58 
62  bool disposed;
63 
68  public DeadSessionController(IDmbProvider dmbProvider)
69  {
70  Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
71  LaunchResult = Task.FromResult(new LaunchResult
72  {
73  StartupTime = TimeSpan.FromSeconds(0)
74  });
75  Lifetime = Task.FromResult(-1);
76  OnReboot = Extensions.TaskExtensions.InfiniteTask();
77  OnPrime = Extensions.TaskExtensions.InfiniteTask();
78  disposeLock = new object();
79  }
80 
82  public void Dispose()
83  {
84  lock (disposeLock)
85  {
86  if (disposed)
87  return;
88  disposed = true;
89  }
90 
91  Dmb.Dispose();
92  }
93 
95  public void EnableCustomChatCommands() => throw new NotSupportedException();
96 
98  public ReattachInformation Release() => throw new NotSupportedException();
99 
101  public void ResetRebootState() => throw new NotSupportedException();
102 
104  public Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken) => throw new NotSupportedException();
105 
107  public void SetHighPriority() => throw new NotSupportedException();
108 
110  public Task<bool> SetPort(ushort newPort, CancellationToken cancellatonToken) => throw new NotSupportedException();
111 
113  public Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken) => throw new NotSupportedException();
114 
116  public void ReplaceDmbProvider(IDmbProvider newProvider) => throw new NotSupportedException();
117 
119  public void Suspend() => throw new NotSupportedException();
120 
122  public void Resume() => throw new NotSupportedException();
123  }
124 }
ApiValidationStatus
Status of DMAPI validation
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Components.Session
8 {
13  {
15  public Task<LaunchResult> LaunchResult { get; }
16 
18  public bool IsPrimary => false;
19 
21  public bool TerminationWasRequested => false;
22 
24  public ApiValidationStatus ApiValidationStatus => throw new NotSupportedException();
25 
27  public IDmbProvider Dmb { get; }
28 
30  public ushort? Port => null;
31 
33  public bool ClosePortOnReboot
34  {
35  get => false;
36  set => throw new NotSupportedException();
37  }
38 
40  public RebootState RebootState => throw new NotSupportedException();
41 
43  public Task OnReboot { get; }
44 
46  public Task OnPrime { get; }
47 
49  public Task<int> Lifetime { get; }
50 
52  public Version DMApiVersion => throw new NotSupportedException();
53 
57  readonly object disposeLock;
58 
62  bool disposed;
63 
68  public DeadSessionController(IDmbProvider dmbProvider)
69  {
70  Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
71  LaunchResult = Task.FromResult(new LaunchResult
72  {
73  StartupTime = TimeSpan.FromSeconds(0)
74  });
75  Lifetime = Task.FromResult(-1);
76  OnReboot = Extensions.TaskExtensions.InfiniteTask();
77  OnPrime = Extensions.TaskExtensions.InfiniteTask();
78  disposeLock = new object();
79  }
80 
82  public void Dispose()
83  {
84  lock (disposeLock)
85  {
86  if (disposed)
87  return;
88  disposed = true;
89  }
90 
91  Dmb.Dispose();
92  }
93 
95  public void EnableCustomChatCommands() => throw new NotSupportedException();
96 
98  public ReattachInformation Release() => throw new NotSupportedException();
99 
101  public void ResetRebootState() => throw new NotSupportedException();
102 
104  public Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken) => throw new NotSupportedException();
105 
107  public void SetHighPriority() => throw new NotSupportedException();
108 
110  public Task<bool> SetPort(ushort newPort, CancellationToken cancellatonToken) => throw new NotSupportedException();
111 
113  public Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken) => throw new NotSupportedException();
114 
116  public void ReplaceDmbProvider(IDmbProvider newProvider) => throw new NotSupportedException();
117 
119  public void Suspend() => throw new NotSupportedException();
120 
122  public void Resume() => throw new NotSupportedException();
123 
125  public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) => Task.CompletedTask;
126  }
127 }
ApiValidationStatus
Status of DMAPI validation
@@ -102,6 +102,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Parameters necessary for duplicating a ISessionController session
Represents the result of trying to start a DD process
Definition: LaunchResult.cs:9
+
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
bool disposed
If the DeadSessionController was Disposed
diff --git a/_dream_daemon_controller_8cs_source.html b/_dream_daemon_controller_8cs_source.html index 2dc2887329..f3fb55b0e5 100644 --- a/_dream_daemon_controller_8cs_source.html +++ b/_dream_daemon_controller_8cs_source.html @@ -94,10 +94,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Linq.Expressions;
7 using System.Net;
8 using System.Reflection;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
20 
21 namespace Tgstation.Server.Host.Controllers
22 {
26  [Route(Routes.DreamDaemon)]
27  public sealed class DreamDaemonController : ApiController
28  {
33 
38 
47  public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamDaemonController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
48  {
49  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
50  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
51  }
52 
60  [HttpPut]
61  [TgsAuthorize(DreamDaemonRights.Start)]
62  [ProducesResponseType(typeof(Api.Models.Job), 202)]
63  [ProducesResponseType(410)]
64  public async Task<IActionResult> Create(CancellationToken cancellationToken)
65  {
66  // alias for launching DD
67  var instance = instanceManager.GetInstance(Instance);
68 
69  if (instance.Watchdog.Running)
70  return StatusCode((int)HttpStatusCode.Gone);
71 
72  var job = new Models.Job
73  {
74  Description = "Launch DreamDaemon",
75  CancelRight = (ulong)DreamDaemonRights.Shutdown,
76  CancelRightsType = RightsType.DreamDaemon,
78  StartedBy = AuthenticationContext.User
79  };
80  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false);
81  return Accepted(job.ToApi());
82  }
83 
90  [HttpGet]
91  [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)]
92  [ProducesResponseType(typeof(DreamDaemon), 200)]
93  [ProducesResponseType(410)]
94  public Task<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
95 
102  async Task<IActionResult> ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
103  {
104  var instance = instanceManager.GetInstance(Instance);
105  var dd = instance.Watchdog;
106 
107  var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0;
108  var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0;
109 
110  if (settings == null)
111  {
112  settings = await DatabaseContext
113  .Instances
114  .AsQueryable()
115  .Where(x => x.Id == Instance.Id)
116  .Select(x => x.DreamDaemonSettings)
117  .FirstOrDefaultAsync(cancellationToken)
118  .ConfigureAwait(false);
119  if (settings == default)
120  return StatusCode((int)HttpStatusCode.Gone);
121  }
122 
123  var result = new DreamDaemon();
124  if (metadata)
125  {
126  var alphaActive = dd.AlphaIsActive;
127  var llp = dd.LastLaunchParameters;
128  var rstate = dd.RebootState;
129  result.AutoStart = settings.AutoStart.Value;
130  result.CurrentPort = alphaActive ? llp?.PrimaryPort.Value : llp?.SecondaryPort.Value;
131  result.CurrentSecurity = llp?.SecurityLevel.Value;
132  result.CurrentAllowWebclient = llp?.AllowWebClient.Value;
133  result.PrimaryPort = settings.PrimaryPort.Value;
134  result.AllowWebClient = settings.AllowWebClient.Value;
135  result.Running = dd.Running;
136  result.SecondaryPort = settings.SecondaryPort.Value;
137  result.SecurityLevel = settings.SecurityLevel.Value;
138  result.SoftRestart = rstate == RebootState.Restart;
139  result.SoftShutdown = rstate == RebootState.Shutdown;
140  result.StartupTimeout = settings.StartupTimeout.Value;
141  result.HeartbeatSeconds = settings.HeartbeatSeconds.Value;
142  }
143 
144  if (revision)
145  {
146  var latestCompileJob = instance.LatestCompileJob();
147  result.ActiveCompileJob = ((dd.Running ? dd.ActiveCompileJob : latestCompileJob) ?? latestCompileJob)?.ToApi();
148  if (latestCompileJob?.Id != result.ActiveCompileJob?.Id)
149  result.StagedCompileJob = latestCompileJob?.ToApi();
150  }
151 
152  return Json(result);
153  }
154 
161  [HttpDelete]
162  [TgsAuthorize(DreamDaemonRights.Shutdown)]
163  [ProducesResponseType(204)]
164  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
165  {
166  var instance = instanceManager.GetInstance(Instance);
167  await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false);
168  return NoContent();
169  }
170 
179  [HttpPost]
180  [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout | DreamDaemonRights.SetHeartbeatInterval)]
181  [ProducesResponseType(typeof(DreamDaemon), 200)]
182  [ProducesResponseType(410)]
183  #pragma warning disable CA1502 // TODO: Decomplexify
184  #pragma warning disable CA1506
185  public async Task<IActionResult> Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
186  {
187  if (model == null)
188  throw new ArgumentNullException(nameof(model));
189 
190  if (model.PrimaryPort == 0)
191  throw new InvalidOperationException("Primary port cannot be 0!");
192 
193  if (model.SecondaryPort == 0)
194  throw new InvalidOperationException("Secondary port cannot be 0!");
195 
196  if (model.SoftShutdown == true && model.SoftRestart == true)
197  return BadRequest(new ErrorMessage(ErrorCode.DreamDaemonDoubleSoft));
198 
199  // alias for changing DD settings
200  var current = await DatabaseContext
201  .Instances
202  .AsQueryable()
203  .Where(x => x.Id == Instance.Id)
204  .Select(x => x.DreamDaemonSettings)
205  .FirstOrDefaultAsync(cancellationToken)
206  .ConfigureAwait(false);
207 
208  if (current == default)
209  return StatusCode((int)HttpStatusCode.Gone);
210 
211  var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon);
212 
213  bool CheckModified<T>(Expression<Func<Api.Models.Internal.DreamDaemonSettings, T>> expression, DreamDaemonRights requiredRight)
214  {
215  var memberSelectorExpression = (MemberExpression)expression.Body;
216  var property = (PropertyInfo)memberSelectorExpression.Member;
217 
218  var newVal = property.GetValue(model);
219  if (newVal == null)
220  return false;
221  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
222  return true;
223 
224  property.SetValue(current, newVal);
225  return false;
226  }
227 
228  var instance = instanceManager.GetInstance(Instance);
229  var dd = instance.Watchdog;
230  var rebootState = dd.RebootState;
231  var oldSoftRestart = rebootState == RebootState.Restart;
232  var oldSoftShutdown = rebootState == RebootState.Shutdown;
233 
234  if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient)
235  || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart)
236  || CheckModified(x => x.PrimaryPort, DreamDaemonRights.SetPorts)
237  || CheckModified(x => x.SecondaryPort, DreamDaemonRights.SetPorts)
238  || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity)
239  || (model.SoftRestart.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart))
240  || (model.SoftShutdown.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown))
241  || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout)
242  || CheckModified(x => x.HeartbeatSeconds, DreamDaemonRights.SetHeartbeatInterval))
243  return Forbid();
244 
245  if (current.PrimaryPort == current.SecondaryPort)
246  return BadRequest(new ErrorMessage(ErrorCode.DreamDaemonDuplicatePorts));
247 
248  var wd = instanceManager.GetInstance(Instance).Watchdog;
249 
250  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
251 
252  // run this second because current may be modified by it
253  await wd.ChangeSettings(current, cancellationToken).ConfigureAwait(false);
254 
255  if (!oldSoftRestart && model.SoftRestart == true)
256  await wd.Restart(true, cancellationToken).ConfigureAwait(false);
257  else if (!oldSoftShutdown && model.SoftShutdown == true)
258  await wd.Terminate(true, cancellationToken).ConfigureAwait(false);
259  else if ((oldSoftRestart && model.SoftRestart == false) || (oldSoftShutdown && model.SoftShutdown == false))
260  await wd.ResetRebootState(cancellationToken).ConfigureAwait(false);
261 
262  return await ReadImpl(current, cancellationToken).ConfigureAwait(false);
263  }
264  #pragma warning restore CA1506
265  #pragma warning restore CA1502
266 
273  [HttpPatch]
274  [TgsAuthorize(DreamDaemonRights.Restart)]
275  [ProducesResponseType(typeof(Api.Models.Job), 202)]
276  public async Task<IActionResult> Restart(CancellationToken cancellationToken)
277  {
278  var job = new Models.Job
279  {
280  Instance = Instance,
281  CancelRightsType = RightsType.DreamDaemon,
282  CancelRight = (ulong)DreamDaemonRights.Shutdown,
283  StartedBy = AuthenticationContext.User,
284  Description = "Restart Watchdog"
285  };
286 
287  var watchdog = instanceManager.GetInstance(Instance).Watchdog;
288 
289  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false);
290  return Accepted(job.ToApi());
291  }
292  }
293 }
async Task< IActionResult > Delete(CancellationToken cancellationToken)
Stops the Watchdog if it&#39;s running.
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Linq.Expressions;
7 using System.Net;
8 using System.Reflection;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
20 
21 namespace Tgstation.Server.Host.Controllers
22 {
26  [Route(Routes.DreamDaemon)]
27  public sealed class DreamDaemonController : ApiController
28  {
33 
38 
47  public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamDaemonController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
48  {
49  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
50  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
51  }
52 
60  [HttpPut]
61  [TgsAuthorize(DreamDaemonRights.Start)]
62  [ProducesResponseType(typeof(Api.Models.Job), 202)]
63  [ProducesResponseType(410)]
64  public async Task<IActionResult> Create(CancellationToken cancellationToken)
65  {
66  // alias for launching DD
67  var instance = instanceManager.GetInstance(Instance);
68 
69  if (instance.Watchdog.Running)
70  return StatusCode((int)HttpStatusCode.Gone);
71 
72  var job = new Models.Job
73  {
74  Description = "Launch DreamDaemon",
75  CancelRight = (ulong)DreamDaemonRights.Shutdown,
76  CancelRightsType = RightsType.DreamDaemon,
78  StartedBy = AuthenticationContext.User
79  };
80  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false);
81  return Accepted(job.ToApi());
82  }
83 
90  [HttpGet]
91  [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)]
92  [ProducesResponseType(typeof(DreamDaemon), 200)]
93  [ProducesResponseType(410)]
94  public Task<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
95 
102  async Task<IActionResult> ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
103  {
104  var instance = instanceManager.GetInstance(Instance);
105  var dd = instance.Watchdog;
106 
107  var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0;
108  var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0;
109 
110  if (settings == null)
111  {
112  settings = await DatabaseContext
113  .Instances
114  .AsQueryable()
115  .Where(x => x.Id == Instance.Id)
116  .Select(x => x.DreamDaemonSettings)
117  .FirstOrDefaultAsync(cancellationToken)
118  .ConfigureAwait(false);
119  if (settings == default)
120  return StatusCode((int)HttpStatusCode.Gone);
121  }
122 
123  var result = new DreamDaemon();
124  if (metadata)
125  {
126  var alphaActive = dd.AlphaIsActive;
127  var llp = dd.LastLaunchParameters;
128  var rstate = dd.RebootState;
129  result.AutoStart = settings.AutoStart.Value;
130  result.CurrentPort = alphaActive ? llp?.PrimaryPort.Value : llp?.SecondaryPort.Value;
131  result.CurrentSecurity = llp?.SecurityLevel.Value;
132  result.CurrentAllowWebclient = llp?.AllowWebClient.Value;
133  result.PrimaryPort = settings.PrimaryPort.Value;
134  result.AllowWebClient = settings.AllowWebClient.Value;
135  result.Running = dd.Running;
136  result.SecondaryPort = settings.SecondaryPort.Value;
137  result.SecurityLevel = settings.SecurityLevel.Value;
138  result.SoftRestart = rstate == RebootState.Restart;
139  result.SoftShutdown = rstate == RebootState.Shutdown;
140  result.StartupTimeout = settings.StartupTimeout.Value;
141  result.HeartbeatSeconds = settings.HeartbeatSeconds.Value;
142  }
143 
144  if (revision)
145  {
146  var latestCompileJob = instance.LatestCompileJob();
147  result.ActiveCompileJob = ((dd.Running ? dd.ActiveCompileJob : latestCompileJob) ?? latestCompileJob)?.ToApi();
148  if (latestCompileJob?.Id != result.ActiveCompileJob?.Id)
149  result.StagedCompileJob = latestCompileJob?.ToApi();
150  }
151 
152  return Json(result);
153  }
154 
161  [HttpDelete]
162  [TgsAuthorize(DreamDaemonRights.Shutdown)]
163  [ProducesResponseType(204)]
164  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
165  {
166  var instance = instanceManager.GetInstance(Instance);
167  await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false);
168  return NoContent();
169  }
170 
179  [HttpPost]
180  [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout | DreamDaemonRights.SetHeartbeatInterval)]
181  [ProducesResponseType(typeof(DreamDaemon), 200)]
182  [ProducesResponseType(410)]
183  #pragma warning disable CA1502 // TODO: Decomplexify
184  #pragma warning disable CA1506
185  public async Task<IActionResult> Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
186  {
187  if (model == null)
188  throw new ArgumentNullException(nameof(model));
189 
190  if (model.PrimaryPort == 0)
191  throw new InvalidOperationException("Primary port cannot be 0!");
192 
193  if (model.SecondaryPort == 0)
194  throw new InvalidOperationException("Secondary port cannot be 0!");
195 
196  if (model.SoftShutdown == true && model.SoftRestart == true)
197  return BadRequest(new ErrorMessage(ErrorCode.DreamDaemonDoubleSoft));
198 
199  // alias for changing DD settings
200  var current = await DatabaseContext
201  .Instances
202  .AsQueryable()
203  .Where(x => x.Id == Instance.Id)
204  .Select(x => x.DreamDaemonSettings)
205  .FirstOrDefaultAsync(cancellationToken)
206  .ConfigureAwait(false);
207 
208  if (current == default)
209  return StatusCode((int)HttpStatusCode.Gone);
210 
211  var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon);
212 
213  bool CheckModified<T>(Expression<Func<Api.Models.Internal.DreamDaemonSettings, T>> expression, DreamDaemonRights requiredRight)
214  {
215  var memberSelectorExpression = (MemberExpression)expression.Body;
216  var property = (PropertyInfo)memberSelectorExpression.Member;
217 
218  var newVal = property.GetValue(model);
219  if (newVal == null)
220  return false;
221  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
222  return true;
223 
224  property.SetValue(current, newVal);
225  return false;
226  }
227 
228  var instance = instanceManager.GetInstance(Instance);
229  var dd = instance.Watchdog;
230  var rebootState = dd.RebootState;
231  var oldSoftRestart = rebootState == RebootState.Restart;
232  var oldSoftShutdown = rebootState == RebootState.Shutdown;
233 
234  if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient)
235  || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart)
236  || CheckModified(x => x.PrimaryPort, DreamDaemonRights.SetPorts)
237  || CheckModified(x => x.SecondaryPort, DreamDaemonRights.SetPorts)
238  || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity)
239  || (model.SoftRestart.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftRestart))
240  || (model.SoftShutdown.HasValue && !AuthenticationContext.InstanceUser.DreamDaemonRights.Value.HasFlag(DreamDaemonRights.SoftShutdown))
241  || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout)
242  || CheckModified(x => x.HeartbeatSeconds, DreamDaemonRights.SetHeartbeatInterval))
243  return Forbid();
244 
245  if (current.PrimaryPort == current.SecondaryPort)
246  return BadRequest(new ErrorMessage(ErrorCode.DreamDaemonDuplicatePorts));
247 
248  var wd = instanceManager.GetInstance(Instance).Watchdog;
249 
250  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
251 
252  // run this second because current may be modified by it
253  await wd.ChangeSettings(current, cancellationToken).ConfigureAwait(false);
254 
255  if (!oldSoftRestart && model.SoftRestart == true)
256  await wd.Restart(true, cancellationToken).ConfigureAwait(false);
257  else if (!oldSoftShutdown && model.SoftShutdown == true)
258  await wd.Terminate(true, cancellationToken).ConfigureAwait(false);
259  else if ((oldSoftRestart && model.SoftRestart == false) || (oldSoftShutdown && model.SoftShutdown == false))
260  await wd.ResetRebootState(cancellationToken).ConfigureAwait(false);
261 
262  return await ReadImpl(current, cancellationToken).ConfigureAwait(false);
263  }
264  #pragma warning restore CA1506
265  #pragma warning restore CA1502
266 
273  [HttpPatch]
274  [TgsAuthorize(DreamDaemonRights.Restart)]
275  [ProducesResponseType(typeof(Api.Models.Job), 202)]
276  public async Task<IActionResult> Restart(CancellationToken cancellationToken)
277  {
278  var job = new Models.Job
279  {
280  Instance = Instance,
281  CancelRightsType = RightsType.DreamDaemon,
282  CancelRight = (ulong)DreamDaemonRights.Shutdown,
283  StartedBy = AuthenticationContext.User,
284  Description = "Restart Watchdog"
285  };
286 
287  var watchdog = instanceManager.GetInstance(Instance).Watchdog;
288 
289  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false);
290  return Accepted(job.ToApi());
291  }
292  }
293 }
async Task< IActionResult > Delete(CancellationToken cancellationToken)
Stops the Watchdog if it&#39;s running.
+
long Id
The ID of the entity.
Definition: EntityId.cs:11
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
@@ -122,9 +123,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonRights DreamDaemonRights
The Rights.DreamDaemonRights of the InstanceUser
Definition: InstanceUser.cs:33
async Task< IActionResult > Create(CancellationToken cancellationToken)
Launches the watchdog.
DbSet< Instance > Instances
The Instances in the DatabaseContext.
- +
DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger< DreamDaemonController > logger)
Construct a DreamDaemonController
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
ApiController for managing the DreamDaemon
const string DreamDaemon
The Models.DreamDaemon controller
Definition: Routes.cs:44
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
diff --git a/_dream_maker_client_8cs_source.html b/_dream_maker_client_8cs_source.html index 3566b6fbed..ef14b443b5 100644 --- a/_dream_maker_client_8cs_source.html +++ b/_dream_maker_client_8cs_source.html @@ -108,7 +108,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Routes to a server actions
Definition: Routes.cs:9
readonly Instance instance
The Instance for the DreamMakerClient
const string DreamMaker
The Models.DreamMaker controller
Definition: Routes.cs:74
-
Common base of CompileJobs and Jobs.
Definition: EntityId.cs:6
+
Common base of Instances, CompileJobs, and Jobs.
Definition: EntityId.cs:6
Metadata about a server instance
Definition: Instance.cs:9
static string SetID(string route, long id)
Apply an id postfix to a route
diff --git a/_dream_maker_controller_8cs_source.html b/_dream_maker_controller_8cs_source.html index fdf99c5c97..1d7f7bcaf1 100644 --- a/_dream_maker_controller_8cs_source.html +++ b/_dream_maker_controller_8cs_source.html @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamMakerController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
18 
19 namespace Tgstation.Server.Host.Controllers
20 {
24  [Route(Routes.DreamMaker)]
25  #pragma warning disable CA1506 // TODO: Decomplexify
26  public sealed class DreamMakerController : ApiController
27  {
32 
37 
46  public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamMakerController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
47  {
48  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
49  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
50  }
51 
58  [HttpGet]
59  [TgsAuthorize(DreamMakerRights.Read)]
60  [ProducesResponseType(typeof(DreamMaker), 200)]
61  public async Task<IActionResult> Read(CancellationToken cancellationToken)
62  {
63  var instance = instanceManager.GetInstance(Instance);
64  var dreamMakerSettings = await DatabaseContext
66  .AsQueryable()
67  .Where(x => x.InstanceId == Instance.Id)
68  .FirstOrDefaultAsync(cancellationToken)
69  .ConfigureAwait(false);
70  return Json(dreamMakerSettings.ToApi());
71  }
72 
81  [HttpGet("{id}")]
82  [TgsAuthorize(DreamMakerRights.CompileJobs)]
83  [ProducesResponseType(typeof(Api.Models.CompileJob), 200)]
84  [ProducesResponseType(404)]
85  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
86  {
87  var compileJob = await DatabaseContext
89  .AsQueryable()
90  .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
91  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
92  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
93  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
94  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
95  if (compileJob == default)
96  return NotFound();
97  return Json(compileJob.ToApi());
98  }
99 
106  [HttpGet(Routes.List)]
107  [TgsAuthorize(DreamMakerRights.CompileJobs)]
108  [ProducesResponseType(typeof(List<EntityId>), 200)]
109  public async Task<IActionResult> List(CancellationToken cancellationToken)
110  {
111  var compileJobs = await DatabaseContext
112  .CompileJobs
113  .AsQueryable()
114  .Where(x => x.Job.Instance.Id == Instance.Id)
115  .OrderByDescending(x => x.Job.StoppedAt)
116  .Select(x => new EntityId
117  {
118  Id = x.Id
119  })
120  .ToListAsync(cancellationToken)
121  .ConfigureAwait(false);
122  return Json(compileJobs);
123  }
124 
131  [HttpPut]
132  [TgsAuthorize(DreamMakerRights.Compile)]
133  [ProducesResponseType(typeof(Api.Models.Job), 202)]
134  public async Task<IActionResult> Create(CancellationToken cancellationToken)
135  {
136  var job = new Models.Job
137  {
138  Description = "Compile active repository code",
139  StartedBy = AuthenticationContext.User,
140  CancelRightsType = RightsType.DreamMaker,
141  CancelRight = (ulong)DreamMakerRights.CancelCompile,
143  };
144 
145  IInstance instance = instanceManager.GetInstance(Instance);
146  await jobManager.RegisterOperation(
147  job,
148  instance.DreamMaker.DeploymentProcess,
149  cancellationToken)
150  .ConfigureAwait(false);
151  return Accepted(job.ToApi());
152  }
153 
163  [HttpPost]
164  [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)]
165  [ProducesResponseType(typeof(DreamMaker), 200)]
166  [ProducesResponseType(204)]
167  [ProducesResponseType(410)]
168  public async Task<IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
169  {
170  if (model == null)
171  throw new ArgumentNullException(nameof(model));
172 
173  if (model.ApiValidationPort == 0)
174  throw new InvalidOperationException("ApiValidationPort cannot be 0!");
175 
176  var hostModel = await DatabaseContext
178  .AsQueryable()
179  .Where(x => x.InstanceId == Instance.Id)
180  .FirstOrDefaultAsync(cancellationToken)
181  .ConfigureAwait(false);
182  if (hostModel == null)
183  return StatusCode((int)HttpStatusCode.Gone);
184 
185  if (model.ProjectName != null)
186  {
188  return Forbid();
189  if (model.ProjectName.Length == 0)
190  hostModel.ProjectName = null;
191  else
192  hostModel.ProjectName = model.ProjectName;
193  }
194 
195  if (model.ApiValidationPort.HasValue)
196  {
197  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort))
198  return Forbid();
199  hostModel.ApiValidationPort = model.ApiValidationPort;
200  }
201 
202  if (model.ApiValidationSecurityLevel.HasValue)
203  {
204  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel))
205  return Forbid();
206  hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
207  }
208 
209  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
210 
211  if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0)
212  return NoContent();
213 
214  return await Read(cancellationToken).ConfigureAwait(false);
215  }
216  }
217 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
18 
19 namespace Tgstation.Server.Host.Controllers
20 {
24  [Route(Routes.DreamMaker)]
25  #pragma warning disable CA1506 // TODO: Decomplexify
26  public sealed class DreamMakerController : ApiController
27  {
32 
37 
46  public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamMakerController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
47  {
48  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
49  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
50  }
51 
58  [HttpGet]
59  [TgsAuthorize(DreamMakerRights.Read)]
60  [ProducesResponseType(typeof(DreamMaker), 200)]
61  public async Task<IActionResult> Read(CancellationToken cancellationToken)
62  {
63  var instance = instanceManager.GetInstance(Instance);
64  var dreamMakerSettings = await DatabaseContext
66  .AsQueryable()
67  .Where(x => x.InstanceId == Instance.Id)
68  .FirstOrDefaultAsync(cancellationToken)
69  .ConfigureAwait(false);
70  return Json(dreamMakerSettings.ToApi());
71  }
72 
81  [HttpGet("{id}")]
82  [TgsAuthorize(DreamMakerRights.CompileJobs)]
83  [ProducesResponseType(typeof(Api.Models.CompileJob), 200)]
84  [ProducesResponseType(404)]
85  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
86  {
87  var compileJob = await DatabaseContext
89  .AsQueryable()
90  .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
91  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
92  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
93  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
94  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
95  if (compileJob == default)
96  return NotFound();
97  return Json(compileJob.ToApi());
98  }
99 
106  [HttpGet(Routes.List)]
107  [TgsAuthorize(DreamMakerRights.CompileJobs)]
108  [ProducesResponseType(typeof(List<EntityId>), 200)]
109  public async Task<IActionResult> List(CancellationToken cancellationToken)
110  {
111  var compileJobs = await DatabaseContext
112  .CompileJobs
113  .AsQueryable()
114  .Where(x => x.Job.Instance.Id == Instance.Id)
115  .OrderByDescending(x => x.Job.StoppedAt)
116  .Select(x => new EntityId
117  {
118  Id = x.Id
119  })
120  .ToListAsync(cancellationToken)
121  .ConfigureAwait(false);
122  return Json(compileJobs);
123  }
124 
131  [HttpPut]
132  [TgsAuthorize(DreamMakerRights.Compile)]
133  [ProducesResponseType(typeof(Api.Models.Job), 202)]
134  public async Task<IActionResult> Create(CancellationToken cancellationToken)
135  {
136  var job = new Models.Job
137  {
138  Description = "Compile active repository code",
139  StartedBy = AuthenticationContext.User,
140  CancelRightsType = RightsType.DreamMaker,
141  CancelRight = (ulong)DreamMakerRights.CancelCompile,
143  };
144 
145  IInstance instance = instanceManager.GetInstance(Instance);
146  await jobManager.RegisterOperation(
147  job,
148  instance.DreamMaker.DeploymentProcess,
149  cancellationToken)
150  .ConfigureAwait(false);
151  return Accepted(job.ToApi());
152  }
153 
163  [HttpPost]
164  [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)]
165  [ProducesResponseType(typeof(DreamMaker), 200)]
166  [ProducesResponseType(204)]
167  [ProducesResponseType(410)]
168  public async Task<IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
169  {
170  if (model == null)
171  throw new ArgumentNullException(nameof(model));
172 
173  if (model.ApiValidationPort == 0)
174  throw new InvalidOperationException("ApiValidationPort cannot be 0!");
175 
176  var hostModel = await DatabaseContext
178  .AsQueryable()
179  .Where(x => x.InstanceId == Instance.Id)
180  .FirstOrDefaultAsync(cancellationToken)
181  .ConfigureAwait(false);
182  if (hostModel == null)
183  return StatusCode((int)HttpStatusCode.Gone);
184 
185  if (model.ProjectName != null)
186  {
188  return Forbid();
189  if (model.ProjectName.Length == 0)
190  hostModel.ProjectName = null;
191  else
192  hostModel.ProjectName = model.ProjectName;
193  }
194 
195  if (model.ApiValidationPort.HasValue)
196  {
197  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort))
198  return Forbid();
199  hostModel.ApiValidationPort = model.ApiValidationPort;
200  }
201 
202  if (model.ApiValidationSecurityLevel.HasValue)
203  {
204  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel))
205  return Forbid();
206  hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
207  }
208 
209  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
210 
211  if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0)
212  return NoContent();
213 
214  return await Read(cancellationToken).ConfigureAwait(false);
215  }
216  }
217 }
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
Represents the state of the DreamMaker compiler. Create action starts a new compile. Delete action cancels the current compile
Definition: DreamMaker.cs:8
@@ -121,13 +121,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task< IActionResult > Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
Update deployment settings.
IDreamMaker DreamMaker
The IDreamMaker for the IInstance.
Definition: IInstance.cs:31
const string DreamMaker
The Models.DreamMaker controller
Definition: Routes.cs:74
- +
For interacting with the instance services
Definition: IInstance.cs:16
DreamMakerRights
Rights for Models.DreamMaker
DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger< DreamMakerController > logger)
Construct a DreamMakerController
readonly IJobManager jobManager
The IJobManager for the DreamMakerController
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
-
Common base of CompileJobs and Jobs.
Definition: EntityId.cs:6
+
Common base of Instances, CompileJobs, and Jobs.
Definition: EntityId.cs:6
DreamMakerRights DreamMakerRights
The Rights.DreamMakerRights of the InstanceUser
Definition: InstanceUser.cs:39
diff --git a/_entity_id_8cs.html b/_entity_id_8cs.html index e1bc6f138f..e77441d1f0 100644 --- a/_entity_id_8cs.html +++ b/_entity_id_8cs.html @@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

Classes

class  Tgstation.Server.Api.Models.EntityId - Common base of CompileJobs and Jobs. More...
+ Common base of Instances, CompileJobs, and Jobs. More...
  - + @@ -314,6 +314,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_api_headers.html b/class_tgstation_1_1_server_1_1_api_1_1_api_headers.html index 34d98d6059..5abc3686ee 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_api_headers.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_api_headers.html @@ -149,7 +149,7 @@ Public Attributes - + @@ -173,7 +173,7 @@ Static Public Attributes - + @@ -333,7 +333,7 @@ Static Private Attributes
const string ApiVersionHeader
The ApiVersion header key
Definition: ApiHeaders.cs:27
const string InstanceIdHeader
The InstanceId header key
Definition: ApiHeaders.cs:32
string Token
The client&#39;s JWT
Definition: ApiHeaders.cs:87
-
long InstanceId
The Models.Instance.Id being accessed
Definition: ApiHeaders.cs:67
+
long InstanceId
The instance Models.EntityId.Id being accessed
Definition: ApiHeaders.cs:67
const string UsernameHeader
The Username header key
Definition: ApiHeaders.cs:47
const string BasicAuthenticationScheme
The JWT authentication header scheme
Definition: ApiHeaders.cs:42
@@ -454,7 +454,7 @@ Static Private Attributes

Checks if the ApiVersion is compatible with Version

Returns
if the API is compatible, otherwise
-

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

+

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

Here is the caller graph for this function:
@@ -496,7 +496,7 @@ Here is the caller graph for this function:
Parameters

diff --git a/_entity_id_8cs_source.html b/_entity_id_8cs_source.html index b729b6d042..b64e03b492 100644 --- a/_entity_id_8cs_source.html +++ b/_entity_id_8cs_source.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public class EntityId
7  {
11  public long Id { get; set; }
12  }
13 }
-
Common base of CompileJobs and Jobs.
Definition: EntityId.cs:6
+
Common base of Instances, CompileJobs, and Jobs.
Definition: EntityId.cs:6
diff --git a/_experimental_watchdog_8cs_source.html b/_experimental_watchdog_8cs_source.html index ea3c10431e..922d17e35c 100644 --- a/_experimental_watchdog_8cs_source.html +++ b/_experimental_watchdog_8cs_source.html @@ -94,15 +94,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ExperimentalWatchdog.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.Globalization;
6 using System.Threading;
7 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Components.Watchdog
17 {
22  {
26  const int AlphaBravoStartupSeperationInterval = 10; // TODO: Make this configurable
27 
29  public override bool AlphaIsActive => alphaIsActive;
30 
32  public override Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
33 
35  public override RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
36 
41 
46 
51 
68  IChatManager chat,
69  ISessionControllerFactory sessionControllerFactory,
70  IDmbFactory dmbFactory,
71  IReattachInfoHandler reattachInfoHandler,
72  IDatabaseContextFactory databaseContextFactory,
73  IJobManager jobManager,
74  IServerControl serverControl,
75  IAsyncDelayer asyncDelayer,
76  ILogger<ExperimentalWatchdog> logger,
77  DreamDaemonLaunchParameters initialLaunchParameters,
78  Api.Models.Instance instance, bool autoStart)
79  : base(
80  chat,
81  sessionControllerFactory,
82  dmbFactory,
83  reattachInfoHandler,
84  databaseContextFactory,
85  jobManager,
86  serverControl,
87  asyncDelayer,
88  logger,
89  initialLaunchParameters,
90  instance,
91  autoStart)
92  {
93  alphaIsActive = true;
94  }
95 
97  #pragma warning disable CA1502 // TODO: Decomplexify
98  protected override async Task HandleMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
99  {
100  Logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
101 
102  // this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
103  // I'll do my best to walk you through it
104 
105  // returns true if the inactive server can't be used immediately
106  // also sets monitor to restart if the above holds
107  bool FullRestartDeadInactive()
108  {
109  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
110  {
111  Logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
112  monitorState.NextAction = MonitorAction.Restart; // will dispose server
113  return true;
114  }
115 
116  return false;
117  }
118 
119  // trys to set inactive server's port to the public game port
120  // doesn't handle closing active server's port
121  // returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
122  // on failure, sets monitor to restart
123  async Task<bool> MakeInactiveActive()
124  {
125  Logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
126  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
127 
128  if (!result)
129  {
130  Logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
131  monitorState.NextAction = MonitorAction.Restart; // will dispose server
132  return false;
133  }
134 
135  // inactive server should always be using active launch parameters
136  LastLaunchParameters = ActiveLaunchParameters;
137 
138  var tmp = monitorState.ActiveServer;
139  monitorState.ActiveServer = monitorState.InactiveServer;
140  monitorState.InactiveServer = tmp;
141  alphaIsActive = !AlphaIsActive;
142  monitorState.ActiveServer.EnableCustomChatCommands();
143  return true;
144  }
145 
146  // Kills and tries to launch inactive server with the latest dmb
147  // falls back to current dmb on failure
148  // Sets critfail on inactive server failing that
149  // returns false if the backup dmb was used successfully, true otherwise
150  async Task UpdateAndRestartInactiveServer(bool breakAfter)
151  {
152  ActiveParametersUpdated = new TaskCompletionSource<object>();
153  monitorState.InactiveServer.Dispose(); // kill or recycle it
154  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
155  monitorState.NextAction = desiredNextAction;
156 
157  Logger.LogInformation("Rebooting inactive server...");
158  var newDmb = DmbFactory.LockNextDmb(1);
159  try
160  {
162  newDmb,
163  null,
164  ActiveLaunchParameters,
165  false,
166  !monitorState.ActiveServer.IsPrimary,
167  false,
168  cancellationToken)
169  .ConfigureAwait(false);
170  monitorState.InactiveServer.SetHighPriority();
171  }
172  catch (OperationCanceledException)
173  {
174  throw;
175  }
176  catch (Exception e)
177  {
178  Logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
179 
180  // ahh jeez, what do we do here?
181  // this is our fault, so it should never happen but
182  // idk maybe a database error while handling the newest dmb?
183  // either way try to start it using the active server's dmb as a backup
184  try
185  {
186  var dmbBackup = await DmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
187 
188  if (dmbBackup == null) // NANI!?
189  throw new InvalidOperationException("Watchdog double crit-fail!"); // just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
190 
192  dmbBackup,
193  null,
194  ActiveLaunchParameters,
195  false,
196  !monitorState.ActiveServer.IsPrimary,
197  false,
198  cancellationToken)
199  .ConfigureAwait(false);
200  monitorState.InactiveServer.SetHighPriority();
201  await Chat.SendWatchdogMessage(
202  "Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...",
203  false,
204  cancellationToken).ConfigureAwait(false);
205  }
206  catch (OperationCanceledException)
207  {
208  throw;
209  }
210  catch (Exception e2)
211  {
212  // fuuuuucckkk
213  Logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
214  monitorState.InactiveServerCritFail = true;
215  await Chat.SendWatchdogMessage(
216  "Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits",
217  false,
218  cancellationToken).ConfigureAwait(false);
219  return;
220  }
221  }
222 
223  Logger.LogInformation("Successfully relaunched inactive server!");
224  monitorState.RebootingInactiveServer = true;
225  }
226 
227  static string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
228 
229  // reason handling
230  switch (activationReason)
231  {
232  case MonitorActivationReason.ActiveServerCrashed:
233  if (monitorState.ActiveServer.RebootState == Session.RebootState.Shutdown)
234  {
235  // the time for graceful shutdown is now
236  await Chat.SendWatchdogMessage(
237  String.Format(
238  CultureInfo.InvariantCulture,
239  "Active server {0}! Shutting down due to graceful termination request...",
240  ExitWord(monitorState.ActiveServer)),
241  false,
242  cancellationToken).ConfigureAwait(false);
243  monitorState.NextAction = MonitorAction.Exit;
244  break;
245  }
246 
247  if (FullRestartDeadInactive())
248  {
249  // tell chat about it and go ahead
250  await Chat.SendWatchdogMessage(
251  String.Format(
252  CultureInfo.InvariantCulture,
253  "Active server {0}! Inactive server unable to online!",
254  ExitWord(monitorState.ActiveServer)),
255  false,
256  cancellationToken).ConfigureAwait(false);
257 
258  // we've already been set to restart
259  break;
260  }
261 
262  // tell chat about it
263  await Chat.SendWatchdogMessage(
264  String.Format(
265  CultureInfo.InvariantCulture,
266  "Active server {0}! Onlining inactive server...",
267  ExitWord(monitorState.ActiveServer)),
268  false,
269  cancellationToken)
270  .ConfigureAwait(false);
271 
272  // try to activate the inactive server
273  if (!await MakeInactiveActive().ConfigureAwait(false))
274  break; // failing that, we've already been set to restart
275 
276  // bring up another inactive server
277  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
278  break;
279  case MonitorActivationReason.InactiveServerCrashed:
280  // just announce and try to bring it back
281  await Chat.SendWatchdogMessage(
282  String.Format(
283  CultureInfo.InvariantCulture,
284  "Inactive server {0}! Rebooting...",
285  ExitWord(monitorState.InactiveServer)),
286  false,
287  cancellationToken)
288  .ConfigureAwait(false);
289  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
290  break;
291  case MonitorActivationReason.ActiveServerRebooted:
292  // ideal goal: active server just closed its port
293  // tell inactive server to open it's port and that's now the active server
294  var rebootState = monitorState.ActiveServer.RebootState;
295  monitorState.ActiveServer.ResetRebootState(); // the DMAPI has already done this internally
296 
297  if (FullRestartDeadInactive() && rebootState != Session.RebootState.Shutdown)
298  break; // full restart if the inactive server is being fucky
299 
300  // what matters here is the RebootState
301  var restartOnceSwapped = false;
302 
303  switch (rebootState)
304  {
305  case Session.RebootState.Normal:
306  // life as normal
307  break;
308  case Session.RebootState.Restart:
309  // reboot the current active server once the inactive one activates
310  restartOnceSwapped = true;
311  break;
312  case Session.RebootState.Shutdown:
313  // graceful shutdown time
314  await Chat.SendWatchdogMessage(
315  "Active server rebooted! Shutting down due to graceful termination request...",
316  false,
317  cancellationToken)
318  .ConfigureAwait(false);
319  monitorState.NextAction = MonitorAction.Exit;
320  return;
321  default:
322  throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
323  }
324 
325  // are both servers now running the same CompileJob?
326  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
327 
328  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
329  restartOnceSwapped = true; // need a new launch to update either settings or compile job
330 
331  if (restartOnceSwapped)
332  /*
333  * we need to manually restart active server
334  * just kill it here, easier that way
335  */
336  monitorState.ActiveServer.Dispose();
337 
338  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
339 
340  if (activeServerStillHasPortOpen)
341  /* we didn't want active server to swap for some reason and it still has it's port open
342  * just continue as normal
343  */
344  break;
345 
346  if (!await MakeInactiveActive().ConfigureAwait(false))
347  break; // monitor will restart
348 
349  // servers now swapped
350  // enable this now if inactive server is not still valid
351  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
352 
353  if (!restartOnceSwapped)
354  /*
355  * now try to reopen it on the private port
356  * failing that, just reboot it
357  */
358  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
359 
360  // break either way because any issues past this point would be solved by the reboot
361  if (restartOnceSwapped) // for one reason or another
362  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); // update and reboot
363  else
364  monitorState.NextAction = MonitorAction.Skip; // only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
365  break;
366  case MonitorActivationReason.InactiveServerRebooted:
367  // just don't let the active server close it's port if the inactive server isn't ready
368  monitorState.RebootingInactiveServer = true;
369  monitorState.InactiveServer.ResetRebootState();
370  monitorState.ActiveServer.ClosePortOnReboot = false;
371  monitorState.NextAction = MonitorAction.Continue;
372  break;
373  case MonitorActivationReason.InactiveServerStartupComplete:
374  // opposite of above case
375  monitorState.RebootingInactiveServer = false;
376  monitorState.ActiveServer.ClosePortOnReboot = true;
377  monitorState.NextAction = MonitorAction.Continue;
378  break;
379  case MonitorActivationReason.NewDmbAvailable:
380  case MonitorActivationReason.ActiveLaunchParametersUpdated:
381  // just reload the inactive server and wait for a swap to apply the changes
382  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
383  break;
384  case MonitorActivationReason.Heartbeat:
385  default:
386  throw new InvalidOperationException(
387  String.Format(
388  CultureInfo.InvariantCulture,
389  "Invalid monitor activation reason: {0}!",
390  activationReason));
391  }
392  }
393 #pragma warning restore CA1502
394 
396  protected override void DisposeAndNullControllersImpl()
397  {
398  alphaServer?.Dispose();
399  alphaServer = null;
400  bravoServer?.Dispose();
401  bravoServer = null;
402  Running = false;
403  }
404 
406  protected override IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState)
407  {
408  if (AlphaIsActive)
409  Logger.LogDebug("Alpha is the active server");
410  else
411  Logger.LogDebug("Bravo is the active server");
412 
413  if (monitorState.RebootingInactiveServer)
414  Logger.LogDebug("Inactive server is rebooting");
415 
416  // update the monitor state with the inactive/active servers
417  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
418  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
419 
420  if (monitorState.ActiveServer.ClosePortOnReboot)
421  Logger.LogDebug("Active server will close port on reboot");
422  if (monitorState.InactiveServer.ClosePortOnReboot)
423  Logger.LogDebug("Inactive server will close port on reboot");
424 
425  Logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
426  Logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
427 
428  return new Dictionary<MonitorActivationReason, Task>
429  {
430  { MonitorActivationReason.ActiveServerCrashed, monitorState.ActiveServer.Lifetime },
431  { MonitorActivationReason.ActiveServerRebooted, monitorState.ActiveServer.OnReboot },
432  { MonitorActivationReason.InactiveServerCrashed, monitorState.InactiveServer.Lifetime },
433  { MonitorActivationReason.InactiveServerRebooted, monitorState.InactiveServer.OnReboot },
434  { MonitorActivationReason.InactiveServerStartupComplete, monitorState.InactiveServer.OnPrime }
435  };
436  }
437 
439  #pragma warning disable CA1502 // TODO: Decomplexify
440  protected override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
441  {
442  Debug.Assert(alphaServer == null && bravoServer == null, "Entered LaunchNoLock with one or more of the servers not being null!");
443 
444  // don't need a new dmb if reattaching
445  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
446  var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(2);
447 
448  // if this try catches something, both servers are killed
449  try
450  {
451  // start the alpha server task, either by launch a new process or attaching to an existing one
452  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
453  // The tasks pertaining to server startup times are in the ISessionControllers
454  Task<ISessionController> alphaServerTask;
455  if (!doesntNeedNewDmb)
456  alphaServerTask = SessionControllerFactory.LaunchNew(
457  dmbToUse,
458  null,
459  ActiveLaunchParameters,
460  true,
461  true,
462  false,
463  cancellationToken);
464  else
465  alphaServerTask = SessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
466 
467  // retrieve the session controller
468  var startTime = DateTimeOffset.Now;
469  alphaServer = await alphaServerTask.ConfigureAwait(false);
470 
471  // failed reattaches will return null
472  alphaServer?.SetHighPriority();
473 
474  // extra delay for total ordering
475  var now = DateTimeOffset.Now;
476  var delay = now - startTime;
477 
478  // definitely not if reattaching though
479  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
480  await AsyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
481 
482  // now bring bravo up
483  if (!doesntNeedNewDmb)
484  bravoServer = await SessionControllerFactory.LaunchNew(
485  dmbToUse,
486  null,
487  ActiveLaunchParameters,
488  false,
489  false,
490  false,
491  cancellationToken)
492  .ConfigureAwait(false);
493  else
494  bravoServer = await SessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
495 
496  // failed reattaches will return null
497  bravoServer?.SetHighPriority();
498 
499  // possiblity of null servers due to failed reattaches
500  if (alphaServer == null || bravoServer == null)
501  {
502  await chatTask.ConfigureAwait(false);
503  var bothServersDead = alphaServer == null && bravoServer == null;
504  if (bothServersDead
505  || (alphaServer == null && reattachInfo.AlphaIsActive)
506  || (bravoServer == null && !reattachInfo.AlphaIsActive))
507  {
508  // we lost the active server, just restart entirely
509  DisposeAndNullControllers();
510  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
511  Logger.LogWarning(FailReattachMessage);
512  Logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
513  chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken);
514  callBeforeRecurse();
515  await LaunchNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
516  await chatTask.ConfigureAwait(false);
517  return;
518  }
519 
520  // we still have the active server but the other one is dead to us, hand it off to the monitor to restart
521  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
522  chatTask = Chat.SendWatchdogMessage(InactiveReattachFailureMessage, false, cancellationToken);
523  Logger.LogWarning(InactiveReattachFailureMessage);
524 
525  if (reattachInfo.AlphaIsActive)
526  bravoServer = SessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
527  else
528  alphaServer = SessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
529  }
530 
531  var alphaLrt = CheckLaunchResult(alphaServer, "Alpha", cancellationToken);
532  var bravoLrt = CheckLaunchResult(bravoServer, "Bravo", cancellationToken);
533 
534  // this task completes when both serers have finished booting
535  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
536 
537  await allTask.ConfigureAwait(false);
538 
539  // both servers are now running, alpha is the active server(unless reattach), huzzah
540  alphaIsActive = reattachInfo?.AlphaIsActive ?? true;
541 
542  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
543  activeServer.EnableCustomChatCommands();
544  activeServer.ClosePortOnReboot = true;
545  }
546  catch
547  {
548  if (dmbToUse != null)
549  {
550  // we locked 2 dmbs
551  if (bravoServer == null)
552  {
553  // bravo didn't get control of his
554  dmbToUse.Dispose();
555  if (alphaServer == null)
556  dmbToUse.Dispose(); // alpha didn't get control of his
557  }
558  }
559  else if (doesntNeedNewDmb) // we have reattachInfo
560  if (bravoServer == null)
561  {
562  // bravo didn't get control of his
563  reattachInfo.Bravo?.Dmb.Dispose();
564  if (alphaServer == null)
565  reattachInfo.Alpha?.Dmb.Dispose(); // alpha didn't get control of his
566  }
567 
568  // kill the controllers
569  DisposeAndNullControllers();
570  throw;
571  }
572  }
573  #pragma warning restore CA1502
574 
576  protected override ISessionController GetActiveController() => AlphaIsActive ? alphaServer : bravoServer;
577 
579  protected override DualReattachInformation CreateReattachInformation()
581  {
582  AlphaIsActive = AlphaIsActive,
583  Alpha = alphaServer?.Release(),
584  Bravo = bravoServer?.Release()
585  };
586  }
587 }
bool InactiveServerCritFail
If the inactive server is in an unrecoverable state
Definition: MonitorState.cs:19
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.Globalization;
6 using System.Threading;
7 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Components.Watchdog
17 {
22  {
26  const int AlphaBravoStartupSeperationInterval = 10; // TODO: Make this configurable
27 
29  public override bool AlphaIsActive => alphaIsActive;
30 
32  public override Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
33 
35  public override RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
36 
41 
46 
51 
68  IChatManager chat,
69  ISessionControllerFactory sessionControllerFactory,
70  IDmbFactory dmbFactory,
71  IReattachInfoHandler reattachInfoHandler,
72  IDatabaseContextFactory databaseContextFactory,
73  IJobManager jobManager,
74  IServerControl serverControl,
75  IAsyncDelayer asyncDelayer,
76  ILogger<ExperimentalWatchdog> logger,
77  DreamDaemonLaunchParameters initialLaunchParameters,
78  Api.Models.Instance instance, bool autoStart)
79  : base(
80  chat,
81  sessionControllerFactory,
82  dmbFactory,
83  reattachInfoHandler,
84  databaseContextFactory,
85  jobManager,
86  serverControl,
87  asyncDelayer,
88  logger,
89  initialLaunchParameters,
90  instance,
91  autoStart)
92  {
93  alphaIsActive = true;
94  }
95 
97  #pragma warning disable CA1502 // TODO: Decomplexify
98  protected override async Task HandleMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
99  {
100  Logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
101 
102  // this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
103  // I'll do my best to walk you through it
104 
105  // returns true if the inactive server can't be used immediately
106  // also sets monitor to restart if the above holds
107  bool FullRestartDeadInactive()
108  {
109  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
110  {
111  Logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
112  monitorState.NextAction = MonitorAction.Restart; // will dispose server
113  return true;
114  }
115 
116  return false;
117  }
118 
119  // trys to set inactive server's port to the public game port
120  // doesn't handle closing active server's port
121  // returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
122  // on failure, sets monitor to restart
123  async Task<bool> MakeInactiveActive()
124  {
125  Logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
126  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
127 
128  if (!result)
129  {
130  Logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
131  monitorState.NextAction = MonitorAction.Restart; // will dispose server
132  return false;
133  }
134 
135  // inactive server should always be using active launch parameters
136  LastLaunchParameters = ActiveLaunchParameters;
137 
138  var tmp = monitorState.ActiveServer;
139  monitorState.ActiveServer = monitorState.InactiveServer;
140  monitorState.InactiveServer = tmp;
141  alphaIsActive = !AlphaIsActive;
142  monitorState.ActiveServer.EnableCustomChatCommands();
143  return true;
144  }
145 
146  // Kills and tries to launch inactive server with the latest dmb
147  // falls back to current dmb on failure
148  // Sets critfail on inactive server failing that
149  // returns false if the backup dmb was used successfully, true otherwise
150  async Task UpdateAndRestartInactiveServer(bool breakAfter)
151  {
152  ActiveParametersUpdated = new TaskCompletionSource<object>();
153  monitorState.InactiveServer.Dispose(); // kill or recycle it
154  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
155  monitorState.NextAction = desiredNextAction;
156 
157  Logger.LogInformation("Rebooting inactive server...");
158  var newDmb = DmbFactory.LockNextDmb(1);
159  try
160  {
162  newDmb,
163  null,
164  ActiveLaunchParameters,
165  false,
166  !monitorState.ActiveServer.IsPrimary,
167  false,
168  cancellationToken)
169  .ConfigureAwait(false);
170  monitorState.InactiveServer.SetHighPriority();
171  }
172  catch (OperationCanceledException)
173  {
174  throw;
175  }
176  catch (Exception e)
177  {
178  Logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
179 
180  // ahh jeez, what do we do here?
181  // this is our fault, so it should never happen but
182  // idk maybe a database error while handling the newest dmb?
183  // either way try to start it using the active server's dmb as a backup
184  try
185  {
186  var dmbBackup = await DmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
187 
188  if (dmbBackup == null) // NANI!?
189  throw new InvalidOperationException("Watchdog double crit-fail!"); // just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
190 
192  dmbBackup,
193  null,
194  ActiveLaunchParameters,
195  false,
196  !monitorState.ActiveServer.IsPrimary,
197  false,
198  cancellationToken)
199  .ConfigureAwait(false);
200  monitorState.InactiveServer.SetHighPriority();
201  await Chat.SendWatchdogMessage(
202  "Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...",
203  false,
204  cancellationToken).ConfigureAwait(false);
205  }
206  catch (OperationCanceledException)
207  {
208  throw;
209  }
210  catch (Exception e2)
211  {
212  // fuuuuucckkk
213  Logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
214  monitorState.InactiveServerCritFail = true;
215  await Chat.SendWatchdogMessage(
216  "Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits",
217  false,
218  cancellationToken).ConfigureAwait(false);
219  return;
220  }
221  }
222 
223  Logger.LogInformation("Successfully relaunched inactive server!");
224  monitorState.RebootingInactiveServer = true;
225  }
226 
227  static string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
228 
229  // reason handling
230  switch (activationReason)
231  {
232  case MonitorActivationReason.ActiveServerCrashed:
233  if (monitorState.ActiveServer.RebootState == Session.RebootState.Shutdown)
234  {
235  // the time for graceful shutdown is now
236  await Chat.SendWatchdogMessage(
237  String.Format(
238  CultureInfo.InvariantCulture,
239  "Active server {0}! Shutting down due to graceful termination request...",
240  ExitWord(monitorState.ActiveServer)),
241  false,
242  cancellationToken).ConfigureAwait(false);
243  monitorState.NextAction = MonitorAction.Exit;
244  break;
245  }
246 
247  if (FullRestartDeadInactive())
248  {
249  // tell chat about it and go ahead
250  await Chat.SendWatchdogMessage(
251  String.Format(
252  CultureInfo.InvariantCulture,
253  "Active server {0}! Inactive server unable to online!",
254  ExitWord(monitorState.ActiveServer)),
255  false,
256  cancellationToken).ConfigureAwait(false);
257 
258  // we've already been set to restart
259  break;
260  }
261 
262  // tell chat about it
263  await Chat.SendWatchdogMessage(
264  String.Format(
265  CultureInfo.InvariantCulture,
266  "Active server {0}! Onlining inactive server...",
267  ExitWord(monitorState.ActiveServer)),
268  false,
269  cancellationToken)
270  .ConfigureAwait(false);
271 
272  // try to activate the inactive server
273  if (!await MakeInactiveActive().ConfigureAwait(false))
274  break; // failing that, we've already been set to restart
275 
276  // bring up another inactive server
277  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
278  break;
279  case MonitorActivationReason.InactiveServerCrashed:
280  // just announce and try to bring it back
281  await Chat.SendWatchdogMessage(
282  String.Format(
283  CultureInfo.InvariantCulture,
284  "Inactive server {0}! Rebooting...",
285  ExitWord(monitorState.InactiveServer)),
286  false,
287  cancellationToken)
288  .ConfigureAwait(false);
289  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
290  break;
291  case MonitorActivationReason.ActiveServerRebooted:
292  // ideal goal: active server just closed its port
293  // tell inactive server to open it's port and that's now the active server
294  var rebootState = monitorState.ActiveServer.RebootState;
295  monitorState.ActiveServer.ResetRebootState(); // the DMAPI has already done this internally
296 
297  if (FullRestartDeadInactive() && rebootState != Session.RebootState.Shutdown)
298  break; // full restart if the inactive server is being fucky
299 
300  // what matters here is the RebootState
301  var restartOnceSwapped = false;
302 
303  switch (rebootState)
304  {
305  case Session.RebootState.Normal:
306  // life as normal
307  break;
308  case Session.RebootState.Restart:
309  // reboot the current active server once the inactive one activates
310  restartOnceSwapped = true;
311  break;
312  case Session.RebootState.Shutdown:
313  // graceful shutdown time
314  await Chat.SendWatchdogMessage(
315  "Active server rebooted! Shutting down due to graceful termination request...",
316  false,
317  cancellationToken)
318  .ConfigureAwait(false);
319  monitorState.NextAction = MonitorAction.Exit;
320  return;
321  default:
322  throw new InvalidOperationException($"Invalid reboot state: {rebootState}");
323  }
324 
325  // are both servers now running the same CompileJob?
326  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
327 
328  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
329  restartOnceSwapped = true; // need a new launch to update either settings or compile job
330 
331  if (restartOnceSwapped)
332  /*
333  * we need to manually restart active server
334  * just kill it here, easier that way
335  */
336  monitorState.ActiveServer.Dispose();
337 
338  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
339 
340  if (activeServerStillHasPortOpen)
341  /* we didn't want active server to swap for some reason and it still has it's port open
342  * just continue as normal
343  */
344  break;
345 
346  if (!await MakeInactiveActive().ConfigureAwait(false))
347  break; // monitor will restart
348 
349  // servers now swapped
350  // enable this now if inactive server is not still valid
351  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
352 
353  if (!restartOnceSwapped)
354  /*
355  * now try to reopen it on the private port
356  * failing that, just reboot it
357  */
358  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
359 
360  // break either way because any issues past this point would be solved by the reboot
361  if (restartOnceSwapped) // for one reason or another
362  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); // update and reboot
363  else
364  monitorState.NextAction = MonitorAction.Skip; // only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
365  break;
366  case MonitorActivationReason.InactiveServerRebooted:
367  // just don't let the active server close it's port if the inactive server isn't ready
368  monitorState.RebootingInactiveServer = true;
369  monitorState.InactiveServer.ResetRebootState();
370  monitorState.ActiveServer.ClosePortOnReboot = false;
371  monitorState.NextAction = MonitorAction.Continue;
372  break;
373  case MonitorActivationReason.InactiveServerStartupComplete:
374  // opposite of above case
375  monitorState.RebootingInactiveServer = false;
376  monitorState.ActiveServer.ClosePortOnReboot = true;
377  monitorState.NextAction = MonitorAction.Continue;
378  break;
379  case MonitorActivationReason.NewDmbAvailable:
380  case MonitorActivationReason.ActiveLaunchParametersUpdated:
381  // just reload the inactive server and wait for a swap to apply the changes
382  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
383  break;
384  case MonitorActivationReason.Heartbeat:
385  default:
386  throw new InvalidOperationException(
387  String.Format(
388  CultureInfo.InvariantCulture,
389  "Invalid monitor activation reason: {0}!",
390  activationReason));
391  }
392  }
393 #pragma warning restore CA1502
394 
396  protected override void DisposeAndNullControllersImpl()
397  {
398  alphaServer?.Dispose();
399  alphaServer = null;
400  bravoServer?.Dispose();
401  bravoServer = null;
402  Running = false;
403  }
404 
406  protected override IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState)
407  {
408  if (AlphaIsActive)
409  Logger.LogDebug("Alpha is the active server");
410  else
411  Logger.LogDebug("Bravo is the active server");
412 
413  if (monitorState.RebootingInactiveServer)
414  Logger.LogDebug("Inactive server is rebooting");
415 
416  // update the monitor state with the inactive/active servers
417  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
418  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
419 
420  if (monitorState.ActiveServer.ClosePortOnReboot)
421  Logger.LogDebug("Active server will close port on reboot");
422  if (monitorState.InactiveServer.ClosePortOnReboot)
423  Logger.LogDebug("Inactive server will close port on reboot");
424 
425  Logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
426  Logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
427 
428  return new Dictionary<MonitorActivationReason, Task>
429  {
430  { MonitorActivationReason.ActiveServerCrashed, monitorState.ActiveServer.Lifetime },
431  { MonitorActivationReason.ActiveServerRebooted, monitorState.ActiveServer.OnReboot },
432  { MonitorActivationReason.InactiveServerCrashed, monitorState.InactiveServer.Lifetime },
433  { MonitorActivationReason.InactiveServerRebooted, monitorState.InactiveServer.OnReboot },
434  { MonitorActivationReason.InactiveServerStartupComplete, monitorState.InactiveServer.OnPrime }
435  };
436  }
437 
439  #pragma warning disable CA1502 // TODO: Decomplexify
440  protected override async Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
441  {
442  Debug.Assert(alphaServer == null && bravoServer == null, "Entered LaunchNoLock with one or more of the servers not being null!");
443 
444  // don't need a new dmb if reattaching
445  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
446  var dmbToUse = doesntNeedNewDmb ? null : DmbFactory.LockNextDmb(2);
447 
448  // if this try catches something, both servers are killed
449  try
450  {
451  // start the alpha server task, either by launch a new process or attaching to an existing one
452  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
453  // The tasks pertaining to server startup times are in the ISessionControllers
454  Task<ISessionController> alphaServerTask;
455  if (!doesntNeedNewDmb)
456  alphaServerTask = SessionControllerFactory.LaunchNew(
457  dmbToUse,
458  null,
459  ActiveLaunchParameters,
460  true,
461  true,
462  false,
463  cancellationToken);
464  else
465  alphaServerTask = SessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
466 
467  // retrieve the session controller
468  var startTime = DateTimeOffset.Now;
469  alphaServer = await alphaServerTask.ConfigureAwait(false);
470 
471  // failed reattaches will return null
472  alphaServer?.SetHighPriority();
473 
474  // extra delay for total ordering
475  var now = DateTimeOffset.Now;
476  var delay = now - startTime;
477 
478  // definitely not if reattaching though
479  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
480  await AsyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
481 
482  // now bring bravo up
483  if (!doesntNeedNewDmb)
484  bravoServer = await SessionControllerFactory.LaunchNew(
485  dmbToUse,
486  null,
487  ActiveLaunchParameters,
488  false,
489  false,
490  false,
491  cancellationToken)
492  .ConfigureAwait(false);
493  else
494  bravoServer = await SessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
495 
496  // failed reattaches will return null
497  bravoServer?.SetHighPriority();
498 
499  // possiblity of null servers due to failed reattaches
500  if (alphaServer == null || bravoServer == null)
501  {
502  await chatTask.ConfigureAwait(false);
503  var bothServersDead = alphaServer == null && bravoServer == null;
504  if (bothServersDead
505  || (alphaServer == null && reattachInfo.AlphaIsActive)
506  || (bravoServer == null && !reattachInfo.AlphaIsActive))
507  {
508  // we lost the active server, just restart entirely
509  DisposeAndNullControllers();
510  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
511  Logger.LogWarning(FailReattachMessage);
512  Logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
513  chatTask = Chat.SendWatchdogMessage(FailReattachMessage, false, cancellationToken);
514  callBeforeRecurse();
515  await LaunchNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
516  await chatTask.ConfigureAwait(false);
517  return;
518  }
519 
520  // we still have the active server but the other one is dead to us, hand it off to the monitor to restart
521  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
522  chatTask = Chat.SendWatchdogMessage(InactiveReattachFailureMessage, false, cancellationToken);
523  Logger.LogWarning(InactiveReattachFailureMessage);
524 
525  if (reattachInfo.AlphaIsActive)
526  bravoServer = SessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
527  else
528  alphaServer = SessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
529  }
530 
531  var alphaLrt = CheckLaunchResult(alphaServer, "Alpha", cancellationToken);
532  var bravoLrt = CheckLaunchResult(bravoServer, "Bravo", cancellationToken);
533 
534  // this task completes when both serers have finished booting
535  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
536 
537  await allTask.ConfigureAwait(false);
538 
539  // both servers are now running, alpha is the active server(unless reattach), huzzah
540  alphaIsActive = reattachInfo?.AlphaIsActive ?? true;
541 
542  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
543  activeServer.EnableCustomChatCommands();
544  activeServer.ClosePortOnReboot = true;
545  }
546  catch
547  {
548  if (dmbToUse != null)
549  {
550  // we locked 2 dmbs
551  if (bravoServer == null)
552  {
553  // bravo didn't get control of his
554  dmbToUse.Dispose();
555  if (alphaServer == null)
556  dmbToUse.Dispose(); // alpha didn't get control of his
557  }
558  }
559  else if (doesntNeedNewDmb) // we have reattachInfo
560  if (bravoServer == null)
561  {
562  // bravo didn't get control of his
563  reattachInfo.Bravo?.Dmb.Dispose();
564  if (alphaServer == null)
565  reattachInfo.Alpha?.Dmb.Dispose(); // alpha didn't get control of his
566  }
567 
568  // kill the controllers
569  DisposeAndNullControllers();
570  throw;
571  }
572  }
573  #pragma warning restore CA1502
574 
576  protected override ISessionController GetActiveController() => AlphaIsActive ? alphaServer : bravoServer;
577 
579  protected override DualReattachInformation CreateReattachInformation()
581  {
582  AlphaIsActive = AlphaIsActive,
583  Alpha = alphaServer?.Release(),
584  Bravo = bravoServer?.Release()
585  };
586 
588  public override Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
589  => Task.WhenAll(
590  alphaServer?.InstanceRenamed(newInstanceName, cancellationToken) ?? Task.CompletedTask,
591  bravoServer?.InstanceRenamed(newInstanceName, cancellationToken) ?? Task.CompletedTask);
592  }
593 }
bool InactiveServerCritFail
If the inactive server is in an unrecoverable state
Definition: MonitorState.cs:19
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
-
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
+
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
void EnableCustomChatCommands()
Enables the reading of custom chat commands from the ISessionController
ExperimentalWatchdog(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger< ExperimentalWatchdog > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the ExperimentalWatchdog .
long Id
The ID of the entity.
Definition: EntityId.cs:11
- +
MonitorAction NextAction
The next MonitorAction to take in WatchdogBase.MonitorLifetimes(global::System.Threading.CancellationToken)
Definition: MonitorState.cs:24
async Task< ISessionController > LaunchNew(IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, DreamDaemonLaunchParameters launchParameters, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
Create a ISessionController from a freshly launch DreamDaemon instance
@@ -129,6 +129,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
A IWatchdog that tries to manage 2 servers at once for maximum uptime.
bool RebootingInactiveServer
If the inactive server is being rebooted
Definition: MonitorState.cs:14
+
Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
Called when the owning Instance is renamed.
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
ISessionController CreateDeadSession(IDmbProvider dmbProvider)
Creates a ISessionController that appears to have started and died with exit code -1 ...
diff --git a/_home_controller_8cs_source.html b/_home_controller_8cs_source.html index 433667ab2b..30ff2dff05 100644 --- a/_home_controller_8cs_source.html +++ b/_home_controller_8cs_source.html @@ -125,7 +125,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController
- +
bool IsTokenAuthentication
If the header uses password or JWT authentication
Definition: ApiHeaders.cs:102
const string Root
The root controller
Definition: Routes.cs:14
diff --git a/_i_bridge_handler_8cs_source.html b/_i_bridge_handler_8cs_source.html index 52bf041260..0abfd15f2a 100644 --- a/_i_bridge_handler_8cs_source.html +++ b/_i_bridge_handler_8cs_source.html @@ -94,11 +94,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IBridgeHandler.cs
-Go to the documentation of this file.
1 using System.Threading;
2 using System.Threading.Tasks;
3 
4 namespace Tgstation.Server.Host.Components.Interop.Bridge
5 {
8  {
13 
20  Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken);
21  }
22 }
- +Go to the documentation of this file.
diff --git a/_i_bridge_registrar_8cs_source.html b/_i_bridge_registrar_8cs_source.html index 01c725acee..92e760f607 100644 --- a/_i_bridge_registrar_8cs_source.html +++ b/_i_bridge_registrar_8cs_source.html @@ -97,7 +97,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
diff --git a/_i_dream_maker_client_8cs_source.html b/_i_dream_maker_client_8cs_source.html index 4f4becfef5..2b369f15b0 100644 --- a/_i_dream_maker_client_8cs_source.html +++ b/_i_dream_maker_client_8cs_source.html @@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
Common base of CompileJobs and Jobs.
Definition: EntityId.cs:6
+
Common base of Instances, CompileJobs, and Jobs.
Definition: EntityId.cs:6
diff --git a/_i_instance_8cs_source.html b/_i_instance_8cs_source.html index 9477fef1b7..fc85db183f 100644 --- a/_i_instance_8cs_source.html +++ b/_i_instance_8cs_source.html @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IInstance.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Components
12 {
17  {
22 
27 
32 
36  IWatchdog Watchdog { get; }
37 
41  IChatManager Chat { get; }
42 
47 
52  void Rename(string newName);
53 
59  Task SetAutoUpdateInterval(uint newInterval);
60  }
61 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Components
12 {
17  {
22 
27 
32 
36  IWatchdog Watchdog { get; }
37 
41  IChatManager Chat { get; }
42 
47 
53  Task SetAutoUpdateInterval(uint newInterval);
54  }
55 }
@@ -106,6 +106,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For managing connected chat services
Definition: IChatManager.cs:13
+
Handler for an instance being renamed.
For interacting with the instance services
Definition: IInstance.cs:16
For managing the BYOND installation
For managing the Configuration directory
diff --git a/_i_jobs_client_8cs_source.html b/_i_jobs_client_8cs_source.html index 4d4bb40761..1e28cc9efb 100644 --- a/_i_jobs_client_8cs_source.html +++ b/_i_jobs_client_8cs_source.html @@ -99,7 +99,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
Common base of CompileJobs and Jobs.
Definition: EntityId.cs:6
+
Common base of Instances, CompileJobs, and Jobs.
Definition: EntityId.cs:6
Represents a long running job on the server. Model is read-only, updates attempt to cancel the job ...
Definition: Job.cs:6
diff --git a/_i_rename_notifyee_8cs.html b/_i_rename_notifyee_8cs.html new file mode 100644 index 0000000000..8a2058708e --- /dev/null +++ b/_i_rename_notifyee_8cs.html @@ -0,0 +1,122 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Components/IRenameNotifyee.cs File Reference + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.2 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+ +
+
IRenameNotifyee.cs File Reference
+
+
+ +

Go to the source code of this file.

+ + + + + +

+Classes

interface  Tgstation.Server.Host.Components.IRenameNotifyee
 Handler for an instance being renamed. More...
 
+ + + +

+Namespaces

namespace  Tgstation.Server.Host.Components
 
+
+ + + + diff --git a/_i_rename_notifyee_8cs_source.html b/_i_rename_notifyee_8cs_source.html new file mode 100644 index 0000000000..aa9bf36572 --- /dev/null +++ b/_i_rename_notifyee_8cs_source.html @@ -0,0 +1,109 @@ + + + + + + +tgstation-server: src/Tgstation.Server.Host/Components/IRenameNotifyee.cs Source File + + + + + + + + + + +
+
+ + + + + + + +
+
tgstation-server +  4.3.2 +
+
The /tg/station 13 server suite
+
+
+ + + + + + +
+
+ + +
+ +
+ + +
+
+
+
IRenameNotifyee.cs
+
+
+Go to the documentation of this file.
1 using System.Threading;
2 using System.Threading.Tasks;
3 
4 namespace Tgstation.Server.Host.Components
5 {
9  public interface IRenameNotifyee
10  {
17  Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken);
18  }
19 }
+ +
Handler for an instance being renamed.
+ +
+ + + + diff --git a/_i_session_controller_8cs_source.html b/_i_session_controller_8cs_source.html index 35c662810c..b616089a10 100644 --- a/_i_session_controller_8cs_source.html +++ b/_i_session_controller_8cs_source.html @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ISessionController.cs
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Components.Session
9 {
14  {
18  Task<LaunchResult> LaunchResult { get; }
19 
23  bool IsPrimary { get; }
24 
28  bool TerminationWasRequested { get; }
29 
34 
38  Version DMApiVersion { get; }
39 
43  IDmbProvider Dmb { get; }
44 
48  ushort? Port { get; }
49 
53  bool ClosePortOnReboot { get; set; }
54 
59 
63  Task OnReboot { get; }
64 
68  Task OnPrime { get; }
69 
74  ReattachInformation Release();
75 
82  Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken);
83 
90  Task<bool> SetPort(ushort newPort, CancellationToken cancellatonToken);
91 
98  Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken);
99 
103  void ResetRebootState();
104 
108  void EnableCustomChatCommands();
109 
114  void ReplaceDmbProvider(IDmbProvider newProvider);
115  }
116 }
ApiValidationStatus
Status of DMAPI validation
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Components.Session
9 {
14  {
18  Task<LaunchResult> LaunchResult { get; }
19 
23  bool IsPrimary { get; }
24 
28  bool TerminationWasRequested { get; }
29 
34 
38  Version DMApiVersion { get; }
39 
43  IDmbProvider Dmb { get; }
44 
48  ushort? Port { get; }
49 
53  bool ClosePortOnReboot { get; set; }
54 
59 
63  Task OnReboot { get; }
64 
68  Task OnPrime { get; }
69 
74  ReattachInformation Release();
75 
82  Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken);
83 
90  Task<bool> SetPort(ushort newPort, CancellationToken cancellatonToken);
91 
98  Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken);
99 
103  void ResetRebootState();
104 
108  void EnableCustomChatCommands();
109 
114  void ReplaceDmbProvider(IDmbProvider newProvider);
115  }
116 }
ApiValidationStatus
Status of DMAPI validation
@@ -103,6 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Parameters necessary for duplicating a ISessionController session
Represents the result of trying to start a DD process
Definition: LaunchResult.cs:9
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
+
Handler for an instance being renamed.
Handles communication with a DreamDaemon IProcess
diff --git a/_instance_controller_8cs_source.html b/_instance_controller_8cs_source.html index ea3015a524..ce4956958b 100644 --- a/_instance_controller_8cs_source.html +++ b/_instance_controller_8cs_source.html @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Linq.Expressions;
11 using System.Net;
12 using System.Reflection;
13 using System.Threading;
14 using System.Threading.Tasks;
15 using Tgstation.Server.Api;
21 using Tgstation.Server.Host.IO;
26 
27 namespace Tgstation.Server.Host.Controllers
28 {
32  [Route(Routes.InstanceManager)]
33  #pragma warning disable CA1506 // TODO: Decomplexify
34  public sealed class InstanceController : ApiController
35  {
39  const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
40 
41  const string MoveInstanceJobPrefix = "Move instance ID ";
42 
47 
52 
57 
62 
67 
72 
86  IDatabaseContext databaseContext,
87  IAuthenticationContextFactory authenticationContextFactory,
88  IJobManager jobManager,
89  IInstanceManager instanceManager,
90  IIOManager ioManager,
91  IAssemblyInformationProvider assemblyInformationProvider,
92  IPlatformIdentifier platformIdentifier,
93  IOptions<GeneralConfiguration> generalConfigurationOptions,
94  ILogger<InstanceController> logger)
95  : base(databaseContext, authenticationContextFactory, logger, false, true)
96  {
97  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
98  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
99  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
100  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
101  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
102  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
103  }
104 
105  string NormalizePath(string path)
106  {
107  if (path == null)
108  return null;
109 
110  path = ioManager.ResolvePath(path);
111  if (platformIdentifier.IsWindows)
112  path = path.ToUpperInvariant().Replace('\\', '/');
113 
114  return path;
115  }
116 
117  Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
118  {
119  ByondRights = (ByondRights)~0U,
126  UserId = AuthenticationContext.User.Id
127  };
128 
137  [HttpPut]
138  [TgsAuthorize(InstanceManagerRights.Create)]
139  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
140  [ProducesResponseType(typeof(Api.Models.Instance), 201)]
141  public async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
142  {
143  if (model == null)
144  throw new ArgumentNullException(nameof(model));
145 
146  if (String.IsNullOrWhiteSpace(model.Name))
147  return BadRequest(new ErrorMessage(ErrorCode.InstanceWhitespaceName));
148 
149  var targetInstancePath = NormalizePath(model.Path);
150  model.Path = targetInstancePath;
151 
152  var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory);
153 
154  bool InstanceIsChildOf(string otherPath)
155  {
156  if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
157  return false;
158 
159  bool sameLength = targetInstancePath.Length == otherPath.Length;
160  char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
161  return sameLength
162  || dirSeparatorChar == Path.DirectorySeparatorChar
163  || dirSeparatorChar == Path.AltDirectorySeparatorChar;
164  }
165 
166  if (InstanceIsChildOf(installationDirectoryPath))
167  return Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
168 
169  // Validate it's not a child of any other instance
170  IActionResult earlyOut = null;
171  ulong countOfOtherInstances = 0;
172  using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
173  {
174  var newCancellationToken = cts.Token;
175  try
176  {
177  await DatabaseContext
178  .Instances
179  .AsQueryable()
180  .Select(x => new Models.Instance
181  {
182  Path = x.Path
183  })
184  .ForEachAsync(
185  otherInstance =>
186  {
187  if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
188  earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached));
189  else if (InstanceIsChildOf(otherInstance.Path))
190  earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
191 
192  if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
193  cts.Cancel();
194  },
195  newCancellationToken)
196  .ConfigureAwait(false);
197  }
198  catch (OperationCanceledException)
199  {
200  cancellationToken.ThrowIfCancellationRequested();
201  }
202  }
203 
204  if (earlyOut != null)
205  return earlyOut;
206 
207  // Last test, ensure it's in the list of valid paths
208  if (!(generalConfiguration.ValidInstancePaths?
209  .Select(path => NormalizePath(path))
210  .Any(path => InstanceIsChildOf(path)) ?? true))
211  return BadRequest(new ErrorMessage(ErrorCode.InstanceNotAtWhitelistedPath));
212 
213  async Task<bool> DirExistsAndIsNotEmpty()
214  {
215  if (!await ioManager.DirectoryExists(model.Path, cancellationToken).ConfigureAwait(false))
216  return false;
217 
218  var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
219  var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken);
220 
221  var files = await filesTask.ConfigureAwait(false);
222  var dirs = await dirsTask.ConfigureAwait(false);
223 
224  return files.Concat(dirs).Any();
225  }
226 
227  var dirExistsTask = DirExistsAndIsNotEmpty();
228  bool attached = false;
229  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
230  if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
231  return Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath));
232  else
233  attached = true;
234 
235  var newInstance = new Models.Instance
236  {
237  ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
239  {
240  AllowWebClient = false,
241  AutoStart = false,
242  PrimaryPort = 1337,
243  SecondaryPort = 1338,
244  SecurityLevel = DreamDaemonSecurity.Safe,
245  StartupTimeout = 60,
246  HeartbeatSeconds = 60
247  },
249  {
250  ApiValidationPort = 1339,
251  ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
252  },
253  Name = model.Name,
254  Online = false,
255  Path = model.Path,
256  AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
257  ChatBotLimit = model.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
259  {
260  CommitterEmail = "tgstation-server@users.noreply.github.com",
261  CommitterName = assemblyInformationProvider.VersionPrefix,
262  PushTestMergeCommits = false,
263  ShowTestMergeCommitters = false,
264  AutoUpdatesKeepTestMerges = false,
265  AutoUpdatesSynchronize = false,
266  PostTestMergeComment = false
267  },
268  InstanceUsers = new List<Models.InstanceUser> // give this user full privileges on the instance
269  {
270  InstanceAdminUser()
271  }
272  };
273 
274  DatabaseContext.Instances.Add(newInstance);
275  try
276  {
277  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
278 
279  try
280  {
281  // actually reserve it now
282  await ioManager.CreateDirectory(targetInstancePath, cancellationToken).ConfigureAwait(false);
283  await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
284  }
285  catch
286  {
287  // oh shit delete the model
288  DatabaseContext.Instances.Remove(newInstance);
289 
290  await DatabaseContext.Save(default).ConfigureAwait(false);
291  throw;
292  }
293  }
294  catch (IOException e)
295  {
296  return Conflict(new ErrorMessage(ErrorCode.IOError)
297  {
298  AdditionalData = e.Message
299  });
300  }
301 
302  Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
303 
304  var api = newInstance.ToApi();
305  return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
306  }
307 
316  [HttpDelete("{id}")]
317  [TgsAuthorize(InstanceManagerRights.Delete)]
318  [ProducesResponseType(204)]
319  [ProducesResponseType(410)]
320  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
321  {
322  var originalModel = await DatabaseContext
323  .Instances
324  .AsQueryable()
325  .Where(x => x.Id == id)
326  .Include(x => x.WatchdogReattachInformation)
327  .Include(x => x.WatchdogReattachInformation.Alpha)
328  .Include(x => x.WatchdogReattachInformation.Bravo)
329  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
330  if (originalModel == default)
331  return StatusCode((int)HttpStatusCode.Gone);
332  if (originalModel.Online.Value)
333  return Conflict(new ErrorMessage(ErrorCode.InstanceDetachOnline));
334 
335  if (originalModel.WatchdogReattachInformation != null)
336  {
337  DatabaseContext.WatchdogReattachInformations.Remove(originalModel.WatchdogReattachInformation);
338  if (originalModel.WatchdogReattachInformation.Alpha != null)
339  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Alpha);
340  if (originalModel.WatchdogReattachInformation.Bravo != null)
341  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Bravo);
342  }
343 
344  DatabaseContext.Instances.Remove(originalModel);
345 
346  var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
347  await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), default).ConfigureAwait(false);
348  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // cascades everything
349  return NoContent();
350  }
351 
360  [HttpPost]
361  [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline | InstanceManagerRights.SetChatBotLimit)]
362  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
363  [ProducesResponseType(typeof(Api.Models.Instance), 202)]
364  [ProducesResponseType(410)]
365 #pragma warning disable CA1502 // TODO: Decomplexify
366  public async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
367  {
368  if (model == null)
369  throw new ArgumentNullException(nameof(model));
370 
371  IQueryable<Models.Instance> InstanceQuery() => DatabaseContext
372  .Instances
373  .AsQueryable()
374  .Where(x => x.Id == model.Id);
375 
376  var moveJob = await InstanceQuery()
377  .SelectMany(x => x.Jobs).
378 #pragma warning disable CA1307 // Specify StringComparison
379  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
380 #pragma warning restore CA1307 // Specify StringComparison
381  .Select(x => new Models.Job
382  {
383  Id = x.Id
384  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
385 
386  if (moveJob != default)
387  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false); // cancel it now
388 
389  var originalModel = await InstanceQuery()
390  .Include(x => x.RepositorySettings)
391  .Include(x => x.ChatSettings)
392  .ThenInclude(x => x.Channels)
393  .Include(x => x.DreamDaemonSettings) // need these for onlining
394  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
395  if (originalModel == default(Models.Instance))
396  return StatusCode((int)HttpStatusCode.Gone);
397 
398  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
399  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
400  {
401  var memberSelectorExpression = (MemberExpression)expression.Body;
402  var property = (PropertyInfo)memberSelectorExpression.Member;
403 
404  var newVal = property.GetValue(model);
405  if (newVal == null)
406  return false;
407  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
408  return true;
409 
410  property.SetValue(originalModel, newVal);
411  return false;
412  }
413 
414  string originalModelPath = null;
415  string rawPath = null;
416  if (model.Path != null)
417  {
418  rawPath = NormalizePath(model.Path);
419 
420  if (model.Path != originalModel.Path)
421  {
422  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
423  return Forbid();
424  if (originalModel.Online.Value && model.Online != true)
425  return Conflict(new ErrorMessage(ErrorCode.InstanceRelocateOnline));
426 
427  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
428  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
429  return Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath));
430 
431  originalModelPath = originalModel.Path;
432  originalModel.Path = model.Path;
433  }
434  }
435 
436  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
437  var originalOnline = originalModel.Online.Value;
438  var renamed = model.Name != null && originalModel.Name != model.Name;
439 
440  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
441  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
442  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
443  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline)
444  || CheckModified(x => x.ChatBotLimit, InstanceManagerRights.SetChatBotLimit))
445  return Forbid();
446 
447  if (model.ChatBotLimit.HasValue)
448  {
449  var countOfExistingChatBots = await DatabaseContext
450  .ChatBots
451  .AsQueryable()
452  .Where(x => x.InstanceId == originalModel.Id)
453  .CountAsync(cancellationToken)
454  .ConfigureAwait(false);
455 
456  if (countOfExistingChatBots > model.ChatBotLimit.Value)
457  return Conflict(new ErrorMessage(ErrorCode.ChatBotMax));
458  }
459 
460  // ensure the current user has write privilege on the instance
461  var usersInstanceUser = await InstanceQuery()
462  .SelectMany(x => x.InstanceUsers)
463  .Where(x => x.UserId == AuthenticationContext.User.Id)
464  .FirstOrDefaultAsync(cancellationToken)
465  .ConfigureAwait(false);
466  if (usersInstanceUser == default)
467  {
468  var instanceAdminUser = InstanceAdminUser();
469  instanceAdminUser.InstanceId = originalModel.Id;
470  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
471  }
472  else
473  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
474 
475  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
476 
477  if (renamed)
478  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
479 
480  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
481  try
482  {
483  if (originalOnline && model.Online == false)
484  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
485  else if (!originalOnline && model.Online == true)
486  {
487  // force autostart false here because we don't want any long running jobs right now
488  // remember to document this
489  originalModel.DreamDaemonSettings.AutoStart = false;
490  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
491  }
492  }
493  catch (Exception e)
494  {
495  if(!(e is OperationCanceledException))
496  Logger.LogError("Error changing instance online state! Exception: {0}", e);
497  originalModel.Online = originalOnline;
498  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
499  if (originalModelPath != null)
500  originalModel.Path = originalModelPath;
501  await DatabaseContext.Save(default).ConfigureAwait(false);
502  throw;
503  }
504 
505  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
506  {
507  Id = originalModel.Id
508  };
509 
510  var moving = originalModelPath != null;
511  if (moving)
512  {
513  var job = new Models.Job
514  {
515  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
516  Instance = originalModel,
517  CancelRightsType = RightsType.InstanceManager,
518  CancelRight = (ulong)InstanceManagerRights.Relocate,
519  StartedBy = AuthenticationContext.User
520  };
521 
522  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
523  api.MoveJob = job.ToApi();
524  }
525 
526  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
527  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
528 
529  return moving ? (IActionResult)Accepted(api) : Json(api);
530  }
531 #pragma warning restore CA1502
532 
539  [HttpGet(Routes.List)]
540  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
541  [ProducesResponseType(typeof(IEnumerable<Api.Models.Instance>), 200)]
542  public async Task<IActionResult> List(CancellationToken cancellationToken)
543  {
544  IQueryable<Models.Instance> GetBaseQuery()
545  {
546  IQueryable<Models.Instance> query = DatabaseContext.Instances;
548  query = query
549  .Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id))
550  .Where(x => x.InstanceUsers.Any(instanceUser =>
551  instanceUser.ByondRights != ByondRights.None ||
552  instanceUser.ChatBotRights != ChatBotRights.None ||
553  instanceUser.ConfigurationRights != ConfigurationRights.None ||
554  instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
555  instanceUser.DreamMakerRights != DreamMakerRights.None ||
556  instanceUser.InstanceUserRights != InstanceUserRights.None));
557 
558  // Hack for EF IAsyncEnumerable BS
559  return query.Select(x => x);
560  }
561 
562  var moveJobs = await GetBaseQuery()
563  .SelectMany(x => x.Jobs)
564 #pragma warning disable CA1307 // Specify StringComparison
565  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
566 #pragma warning restore CA1307 // Specify StringComparison
567  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
568  .Include(x => x.Instance)
569  .ToListAsync(cancellationToken)
570  .ConfigureAwait(false);
571 
572  var instances = await GetBaseQuery()
573  .ToListAsync(cancellationToken)
574  .ConfigureAwait(false);
575 
576  var apis = instances.Select(x => x.ToApi());
577  foreach(var I in moveJobs)
578  apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi(); // if this .First() fails i will personally murder kevinz000 because I just know he is somehow responsible
579  return Json(apis);
580  }
581 
590  [HttpGet("{id}")]
591  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
592  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
593  [ProducesResponseType(410)]
594  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
595  {
596  var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
597  IQueryable<Models.Instance> QueryForUser()
598  {
599  var query = DatabaseContext
600  .Instances
601  .AsQueryable()
602  .Where(x => x.Id == id);
603 
604  if (cantList)
605  query = query.Include(x => x.InstanceUsers);
606  return query;
607  }
608 
609  var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
610 
611  if (instance == null)
612  return StatusCode((int)HttpStatusCode.Gone);
613 
614  if (cantList && !instance.InstanceUsers.Any(instanceUser => instanceUser.UserId == AuthenticationContext.User.Id &&
615  (instanceUser.ByondRights != ByondRights.None ||
616  instanceUser.ChatBotRights != ChatBotRights.None ||
617  instanceUser.ConfigurationRights != ConfigurationRights.None ||
618  instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
619  instanceUser.DreamMakerRights != DreamMakerRights.None ||
620  instanceUser.InstanceUserRights != InstanceUserRights.None)))
621  return Forbid();
622 
623  var api = instance.ToApi();
624 
625  var moveJob = await QueryForUser()
626  .SelectMany(x => x.Jobs)
627 #pragma warning disable CA1307 // Specify StringComparison
628  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
629 #pragma warning restore CA1307 // Specify StringComparison
630  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
631  .FirstOrDefaultAsync(cancellationToken)
632  .ConfigureAwait(false);
633  api.MoveJob = moveJob?.ToApi();
634  return Json(api);
635  }
636  }
637 }
string Name
The name of the User
Definition: User.cs:41
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Linq.Expressions;
11 using System.Net;
12 using System.Reflection;
13 using System.Threading;
14 using System.Threading.Tasks;
15 using Tgstation.Server.Api;
21 using Tgstation.Server.Host.IO;
26 
27 namespace Tgstation.Server.Host.Controllers
28 {
32  [Route(Routes.InstanceManager)]
33  #pragma warning disable CA1506 // TODO: Decomplexify
34  public sealed class InstanceController : ApiController
35  {
39  const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
40 
41  const string MoveInstanceJobPrefix = "Move instance ID ";
42 
47 
52 
57 
62 
67 
72 
86  IDatabaseContext databaseContext,
87  IAuthenticationContextFactory authenticationContextFactory,
88  IJobManager jobManager,
89  IInstanceManager instanceManager,
90  IIOManager ioManager,
91  IAssemblyInformationProvider assemblyInformationProvider,
92  IPlatformIdentifier platformIdentifier,
93  IOptions<GeneralConfiguration> generalConfigurationOptions,
94  ILogger<InstanceController> logger)
95  : base(databaseContext, authenticationContextFactory, logger, false, true)
96  {
97  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
98  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
99  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
100  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
101  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
102  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
103  }
104 
105  string NormalizePath(string path)
106  {
107  if (path == null)
108  return null;
109 
110  path = ioManager.ResolvePath(path);
111  if (platformIdentifier.IsWindows)
112  path = path.ToUpperInvariant().Replace('\\', '/');
113 
114  return path;
115  }
116 
117  Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
118  {
119  ByondRights = (ByondRights)~0U,
126  UserId = AuthenticationContext.User.Id
127  };
128 
137  [HttpPut]
138  [TgsAuthorize(InstanceManagerRights.Create)]
139  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
140  [ProducesResponseType(typeof(Api.Models.Instance), 201)]
141  public async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
142  {
143  if (model == null)
144  throw new ArgumentNullException(nameof(model));
145 
146  if (String.IsNullOrWhiteSpace(model.Name))
147  return BadRequest(new ErrorMessage(ErrorCode.InstanceWhitespaceName));
148 
149  var targetInstancePath = NormalizePath(model.Path);
150  model.Path = targetInstancePath;
151 
152  var installationDirectoryPath = NormalizePath(DefaultIOManager.CurrentDirectory);
153 
154  bool InstanceIsChildOf(string otherPath)
155  {
156  if (!targetInstancePath.StartsWith(otherPath, StringComparison.Ordinal))
157  return false;
158 
159  bool sameLength = targetInstancePath.Length == otherPath.Length;
160  char dirSeparatorChar = targetInstancePath.ToCharArray()[Math.Min(otherPath.Length, targetInstancePath.Length - 1)];
161  return sameLength
162  || dirSeparatorChar == Path.DirectorySeparatorChar
163  || dirSeparatorChar == Path.AltDirectorySeparatorChar;
164  }
165 
166  if (InstanceIsChildOf(installationDirectoryPath))
167  return Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
168 
169  // Validate it's not a child of any other instance
170  IActionResult earlyOut = null;
171  ulong countOfOtherInstances = 0;
172  using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
173  {
174  var newCancellationToken = cts.Token;
175  try
176  {
177  await DatabaseContext
178  .Instances
179  .AsQueryable()
180  .Select(x => new Models.Instance
181  {
182  Path = x.Path
183  })
184  .ForEachAsync(
185  otherInstance =>
186  {
187  if (++countOfOtherInstances >= generalConfiguration.InstanceLimit)
188  earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceLimitReached));
189  else if (InstanceIsChildOf(otherInstance.Path))
190  earlyOut ??= Conflict(new ErrorMessage(ErrorCode.InstanceAtConflictingPath));
191 
192  if (earlyOut != null && !newCancellationToken.IsCancellationRequested)
193  cts.Cancel();
194  },
195  newCancellationToken)
196  .ConfigureAwait(false);
197  }
198  catch (OperationCanceledException)
199  {
200  cancellationToken.ThrowIfCancellationRequested();
201  }
202  }
203 
204  if (earlyOut != null)
205  return earlyOut;
206 
207  // Last test, ensure it's in the list of valid paths
208  if (!(generalConfiguration.ValidInstancePaths?
209  .Select(path => NormalizePath(path))
210  .Any(path => InstanceIsChildOf(path)) ?? true))
211  return BadRequest(new ErrorMessage(ErrorCode.InstanceNotAtWhitelistedPath));
212 
213  async Task<bool> DirExistsAndIsNotEmpty()
214  {
215  if (!await ioManager.DirectoryExists(model.Path, cancellationToken).ConfigureAwait(false))
216  return false;
217 
218  var filesTask = ioManager.GetFiles(model.Path, cancellationToken);
219  var dirsTask = ioManager.GetDirectories(model.Path, cancellationToken);
220 
221  var files = await filesTask.ConfigureAwait(false);
222  var dirs = await dirsTask.ConfigureAwait(false);
223 
224  return files.Concat(dirs).Any();
225  }
226 
227  var dirExistsTask = DirExistsAndIsNotEmpty();
228  bool attached = false;
229  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
230  if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
231  return Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath));
232  else
233  attached = true;
234 
235  var newInstance = new Models.Instance
236  {
237  ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
239  {
240  AllowWebClient = false,
241  AutoStart = false,
242  PrimaryPort = 1337,
243  SecondaryPort = 1338,
244  SecurityLevel = DreamDaemonSecurity.Safe,
245  StartupTimeout = 60,
246  HeartbeatSeconds = 60
247  },
249  {
250  ApiValidationPort = 1339,
251  ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
252  },
253  Name = model.Name,
254  Online = false,
255  Path = model.Path,
256  AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
257  ChatBotLimit = model.ChatBotLimit ?? Models.Instance.DefaultChatBotLimit,
259  {
260  CommitterEmail = "tgstation-server@users.noreply.github.com",
261  CommitterName = assemblyInformationProvider.VersionPrefix,
262  PushTestMergeCommits = false,
263  ShowTestMergeCommitters = false,
264  AutoUpdatesKeepTestMerges = false,
265  AutoUpdatesSynchronize = false,
266  PostTestMergeComment = false
267  },
268  InstanceUsers = new List<Models.InstanceUser> // give this user full privileges on the instance
269  {
270  InstanceAdminUser()
271  }
272  };
273 
274  DatabaseContext.Instances.Add(newInstance);
275  try
276  {
277  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
278 
279  try
280  {
281  // actually reserve it now
282  await ioManager.CreateDirectory(targetInstancePath, cancellationToken).ConfigureAwait(false);
283  await ioManager.DeleteFile(ioManager.ConcatPath(targetInstancePath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
284  }
285  catch
286  {
287  // oh shit delete the model
288  DatabaseContext.Instances.Remove(newInstance);
289 
290  await DatabaseContext.Save(default).ConfigureAwait(false);
291  throw;
292  }
293  }
294  catch (IOException e)
295  {
296  return Conflict(new ErrorMessage(ErrorCode.IOError)
297  {
298  AdditionalData = e.Message
299  });
300  }
301 
302  Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
303 
304  var api = newInstance.ToApi();
305  return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
306  }
307 
316  [HttpDelete("{id}")]
317  [TgsAuthorize(InstanceManagerRights.Delete)]
318  [ProducesResponseType(204)]
319  [ProducesResponseType(410)]
320  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
321  {
322  var originalModel = await DatabaseContext
323  .Instances
324  .AsQueryable()
325  .Where(x => x.Id == id)
326  .Include(x => x.WatchdogReattachInformation)
327  .Include(x => x.WatchdogReattachInformation.Alpha)
328  .Include(x => x.WatchdogReattachInformation.Bravo)
329  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
330  if (originalModel == default)
331  return StatusCode((int)HttpStatusCode.Gone);
332  if (originalModel.Online.Value)
333  return Conflict(new ErrorMessage(ErrorCode.InstanceDetachOnline));
334 
335  if (originalModel.WatchdogReattachInformation != null)
336  {
337  DatabaseContext.WatchdogReattachInformations.Remove(originalModel.WatchdogReattachInformation);
338  if (originalModel.WatchdogReattachInformation.Alpha != null)
339  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Alpha);
340  if (originalModel.WatchdogReattachInformation.Bravo != null)
341  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Bravo);
342  }
343 
344  DatabaseContext.Instances.Remove(originalModel);
345 
346  var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
347  await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), default).ConfigureAwait(false);
348  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // cascades everything
349  return NoContent();
350  }
351 
360  [HttpPost]
361  [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline | InstanceManagerRights.SetChatBotLimit)]
362  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
363  [ProducesResponseType(typeof(Api.Models.Instance), 202)]
364  [ProducesResponseType(410)]
365 #pragma warning disable CA1502 // TODO: Decomplexify
366  public async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
367  {
368  if (model == null)
369  throw new ArgumentNullException(nameof(model));
370 
371  IQueryable<Models.Instance> InstanceQuery() => DatabaseContext
372  .Instances
373  .AsQueryable()
374  .Where(x => x.Id == model.Id);
375 
376  var moveJob = await InstanceQuery()
377  .SelectMany(x => x.Jobs).
378 #pragma warning disable CA1307 // Specify StringComparison
379  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
380 #pragma warning restore CA1307 // Specify StringComparison
381  .Select(x => new Models.Job
382  {
383  Id = x.Id
384  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
385 
386  if (moveJob != default)
387  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false); // cancel it now
388 
389  var originalModel = await InstanceQuery()
390  .Include(x => x.RepositorySettings)
391  .Include(x => x.ChatSettings)
392  .ThenInclude(x => x.Channels)
393  .Include(x => x.DreamDaemonSettings) // need these for onlining
394  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
395  if (originalModel == default(Models.Instance))
396  return StatusCode((int)HttpStatusCode.Gone);
397 
398  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
399  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
400  {
401  var memberSelectorExpression = (MemberExpression)expression.Body;
402  var property = (PropertyInfo)memberSelectorExpression.Member;
403 
404  var newVal = property.GetValue(model);
405  if (newVal == null)
406  return false;
407  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
408  return true;
409 
410  property.SetValue(originalModel, newVal);
411  return false;
412  }
413 
414  string originalModelPath = null;
415  string rawPath = null;
416  if (model.Path != null)
417  {
418  rawPath = NormalizePath(model.Path);
419 
420  if (model.Path != originalModel.Path)
421  {
422  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
423  return Forbid();
424  if (originalModel.Online.Value && model.Online != true)
425  return Conflict(new ErrorMessage(ErrorCode.InstanceRelocateOnline));
426 
427  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
428  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
429  return Conflict(new ErrorMessage(ErrorCode.InstanceAtExistingPath));
430 
431  originalModelPath = originalModel.Path;
432  originalModel.Path = model.Path;
433  }
434  }
435 
436  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
437  var originalOnline = originalModel.Online.Value;
438  var renamed = model.Name != null && originalModel.Name != model.Name;
439 
440  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
441  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
442  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
443  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline)
444  || CheckModified(x => x.ChatBotLimit, InstanceManagerRights.SetChatBotLimit))
445  return Forbid();
446 
447  if (model.ChatBotLimit.HasValue)
448  {
449  var countOfExistingChatBots = await DatabaseContext
450  .ChatBots
451  .AsQueryable()
452  .Where(x => x.InstanceId == originalModel.Id)
453  .CountAsync(cancellationToken)
454  .ConfigureAwait(false);
455 
456  if (countOfExistingChatBots > model.ChatBotLimit.Value)
457  return Conflict(new ErrorMessage(ErrorCode.ChatBotMax));
458  }
459 
460  // ensure the current user has write privilege on the instance
461  var usersInstanceUser = await InstanceQuery()
462  .SelectMany(x => x.InstanceUsers)
463  .Where(x => x.UserId == AuthenticationContext.User.Id)
464  .FirstOrDefaultAsync(cancellationToken)
465  .ConfigureAwait(false);
466  if (usersInstanceUser == default)
467  {
468  var instanceAdminUser = InstanceAdminUser();
469  instanceAdminUser.InstanceId = originalModel.Id;
470  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
471  }
472  else
473  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
474 
475  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
476 
477  if (renamed)
478  await instanceManager.GetInstance(originalModel).InstanceRenamed(originalModel.Name, cancellationToken).ConfigureAwait(false);
479 
480  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
481  try
482  {
483  if (originalOnline && model.Online == false)
484  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
485  else if (!originalOnline && model.Online == true)
486  {
487  // force autostart false here because we don't want any long running jobs right now
488  // remember to document this
489  originalModel.DreamDaemonSettings.AutoStart = false;
490  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
491  }
492  }
493  catch (Exception e)
494  {
495  if(!(e is OperationCanceledException))
496  Logger.LogError("Error changing instance online state! Exception: {0}", e);
497  originalModel.Online = originalOnline;
498  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
499  if (originalModelPath != null)
500  originalModel.Path = originalModelPath;
501  await DatabaseContext.Save(default).ConfigureAwait(false);
502  throw;
503  }
504 
505  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
506  {
507  Id = originalModel.Id
508  };
509 
510  var moving = originalModelPath != null;
511  if (moving)
512  {
513  var job = new Models.Job
514  {
515  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
516  Instance = originalModel,
517  CancelRightsType = RightsType.InstanceManager,
518  CancelRight = (ulong)InstanceManagerRights.Relocate,
519  StartedBy = AuthenticationContext.User
520  };
521 
522  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
523  api.MoveJob = job.ToApi();
524  }
525 
526  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
527  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
528 
529  return moving ? (IActionResult)Accepted(api) : Json(api);
530  }
531 #pragma warning restore CA1502
532 
539  [HttpGet(Routes.List)]
540  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
541  [ProducesResponseType(typeof(IEnumerable<Api.Models.Instance>), 200)]
542  public async Task<IActionResult> List(CancellationToken cancellationToken)
543  {
544  IQueryable<Models.Instance> GetBaseQuery()
545  {
546  IQueryable<Models.Instance> query = DatabaseContext.Instances;
548  query = query
549  .Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id))
550  .Where(x => x.InstanceUsers.Any(instanceUser =>
551  instanceUser.ByondRights != ByondRights.None ||
552  instanceUser.ChatBotRights != ChatBotRights.None ||
553  instanceUser.ConfigurationRights != ConfigurationRights.None ||
554  instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
555  instanceUser.DreamMakerRights != DreamMakerRights.None ||
556  instanceUser.InstanceUserRights != InstanceUserRights.None));
557 
558  // Hack for EF IAsyncEnumerable BS
559  return query.Select(x => x);
560  }
561 
562  var moveJobs = await GetBaseQuery()
563  .SelectMany(x => x.Jobs)
564 #pragma warning disable CA1307 // Specify StringComparison
565  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
566 #pragma warning restore CA1307 // Specify StringComparison
567  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
568  .Include(x => x.Instance)
569  .ToListAsync(cancellationToken)
570  .ConfigureAwait(false);
571 
572  var instances = await GetBaseQuery()
573  .ToListAsync(cancellationToken)
574  .ConfigureAwait(false);
575 
576  var apis = instances.Select(x => x.ToApi());
577  foreach(var I in moveJobs)
578  apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi(); // if this .First() fails i will personally murder kevinz000 because I just know he is somehow responsible
579  return Json(apis);
580  }
581 
590  [HttpGet("{id}")]
591  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
592  [ProducesResponseType(typeof(Api.Models.Instance), 200)]
593  [ProducesResponseType(410)]
594  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
595  {
596  var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
597  IQueryable<Models.Instance> QueryForUser()
598  {
599  var query = DatabaseContext
600  .Instances
601  .AsQueryable()
602  .Where(x => x.Id == id);
603 
604  if (cantList)
605  query = query.Include(x => x.InstanceUsers);
606  return query;
607  }
608 
609  var instance = await QueryForUser().FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
610 
611  if (instance == null)
612  return StatusCode((int)HttpStatusCode.Gone);
613 
614  if (cantList && !instance.InstanceUsers.Any(instanceUser => instanceUser.UserId == AuthenticationContext.User.Id &&
615  (instanceUser.ByondRights != ByondRights.None ||
616  instanceUser.ChatBotRights != ChatBotRights.None ||
617  instanceUser.ConfigurationRights != ConfigurationRights.None ||
618  instanceUser.DreamDaemonRights != DreamDaemonRights.None ||
619  instanceUser.DreamMakerRights != DreamMakerRights.None ||
620  instanceUser.InstanceUserRights != InstanceUserRights.None)))
621  return Forbid();
622 
623  var api = instance.ToApi();
624 
625  var moveJob = await QueryForUser()
626  .SelectMany(x => x.Jobs)
627 #pragma warning disable CA1307 // Specify StringComparison
628  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
629 #pragma warning restore CA1307 // Specify StringComparison
630  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
631  .FirstOrDefaultAsync(cancellationToken)
632  .ConfigureAwait(false);
633  api.MoveJob = moveJob?.ToApi();
634  return Json(api);
635  }
636  }
637 }
string Name
The name of the User
Definition: User.cs:41
DbSet< ChatBot > ChatBots
The ChatBots in the DatabaseContext.
ConfigurationRights
Rights for Models.ConfigurationFile
@@ -137,7 +137,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DbSet< Instance > Instances
The Instances in the DatabaseContext.
async Task< IActionResult > Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
Modify an Api.Models.Instance&#39;s settings.
- +
DbSet< ReattachInformation > ReattachInformations
The ReattachInformations in the DatabaseContext.
DreamMakerRights
Rights for Models.DreamMaker
DbSet< InstanceUser > InstanceUsers
The InstanceUsers in the DatabaseContext.
diff --git a/_instance_factory_8cs_source.html b/_instance_factory_8cs_source.html index 746f37c7f8..fe984e7c00 100644 --- a/_instance_factory_8cs_source.html +++ b/_instance_factory_8cs_source.html @@ -167,7 +167,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the InstanceFactory
- +
For identifying the current platform
diff --git a/_instance_manager_8cs_source.html b/_instance_manager_8cs_source.html index 6eddda08ef..dc28c9969f 100644 --- a/_instance_manager_8cs_source.html +++ b/_instance_manager_8cs_source.html @@ -94,48 +94,50 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManager.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
20 
21 namespace Tgstation.Server.Host.Components
22 {
25  {
27  public Task Ready => readyTcs.Task;
28 
33 
38 
43 
48 
53 
58 
63 
68 
72  readonly ILogger<InstanceManager> logger;
73 
77  readonly IDictionary<long, IInstance> instances;
78 
82  readonly IDictionary<string, IBridgeHandler> bridgeHandlers;
83 
88 
92  readonly TaskCompletionSource<object> readyTcs;
93 
98 
102  bool disposed;
103 
118  IInstanceFactory instanceFactory,
119  IIOManager ioManager,
120  IDatabaseContextFactory databaseContextFactory,
121  IAssemblyInformationProvider assemblyInformationProvider,
122  IJobManager jobManager,
123  IServerControl serverControl,
124  ISystemIdentityFactory systemIdentityFactory,
125  IAsyncDelayer asyncDelayer,
126  IOptions<GeneralConfiguration> generalConfigurationOptions,
127  ILogger<InstanceManager> logger)
128  {
129  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
130  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
131  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
132  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
133  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
134  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
135  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
136  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
137  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
138  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
139 
140  serverControl.RegisterForRestart(this);
141 
142  instances = new Dictionary<long, IInstance>();
143  bridgeHandlers = new Dictionary<string, IBridgeHandler>();
144  readyTcs = new TaskCompletionSource<object>();
145  }
146 
148  public void Dispose()
149  {
150  lock (instances)
151  {
152  if (disposed)
153  return;
154  disposed = true;
155  }
156 
157  foreach (var I in instances)
158  I.Value.Dispose();
159  }
160 
162  public IInstance GetInstance(Models.Instance metadata)
163  {
164  if (metadata == null)
165  throw new ArgumentNullException(nameof(metadata));
166  lock (instances)
167  {
168  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
169  throw new InvalidOperationException("Instance not online!");
170  return instance;
171  }
172  }
173 
175  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
176  {
177  if (newPath == null)
178  throw new ArgumentNullException(nameof(newPath));
179  if (instance.Online.Value)
180  throw new InvalidOperationException("Cannot move an online instance!");
181  var oldPath = instance.Path;
182  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
183  await databaseContextFactory.UseContext(db =>
184  {
185  var targetInstance = new Models.Instance
186  {
187  Id = instance.Id
188  };
189  db.Instances.Attach(targetInstance);
190  targetInstance.Path = newPath;
191  return db.Save(cancellationToken);
192  }).ConfigureAwait(false);
193  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
194  }
195 
197  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
198  {
199  if (metadata == null)
200  throw new ArgumentNullException(nameof(metadata));
201  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
202  IInstance instance;
203  lock (instances)
204  {
205  if (!instances.TryGetValue(metadata.Id, out instance))
206  throw new InvalidOperationException("Instance not online!");
207  instances.Remove(metadata.Id);
208  }
209 
210  try
211  {
212  // we are the one responsible for cancelling his jobs
213  var tasks = new List<Task>();
214  await databaseContextFactory.UseContext(async db =>
215  {
216  var jobs = db
217  .Jobs
218  .AsQueryable()
219  .Where(x => x.Instance.Id == metadata.Id)
220  .Select(x => new Models.Job
221  {
222  Id = x.Id
223  });
224  await jobs.ForEachAsync(job =>
225  {
226  lock (tasks)
227  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
228  }, cancellationToken).ConfigureAwait(false);
229  }).ConfigureAwait(false);
230 
231  await Task.WhenAll(tasks).ConfigureAwait(false);
232 
233  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
234  }
235  finally
236  {
237  instance.Dispose();
238  }
239  }
240 
242  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
243  {
244  if (metadata == null)
245  throw new ArgumentNullException(nameof(metadata));
246  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
247  var instance = instanceFactory.CreateInstance(this, metadata);
248  try
249  {
250  lock (instances)
251  {
252  if (instances.ContainsKey(metadata.Id))
253  throw new InvalidOperationException("Instance already online!");
254  instances.Add(metadata.Id, instance);
255  }
256  }
257  catch
258  {
259  instance.Dispose();
260  throw;
261  }
262 
263  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
264  }
265 
267  #pragma warning disable CA1506 // TODO: Decomplexify
268  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
269  {
270  logger.LogInformation(assemblyInformationProvider.VersionString);
271 
272  try
273  {
274  CheckSystemCompatibility();
275  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
276  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
277  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
278  var dbInstances = databaseContext
279  .Instances
280  .AsQueryable()
281  .Where(x => x.Online.Value)
282  .Include(x => x.RepositorySettings)
283  .Include(x => x.ChatSettings)
284  .ThenInclude(x => x.Channels)
285  .Include(x => x.DreamDaemonSettings)
286  .ToAsyncEnumerable();
287  var tasks = new List<Task>();
288  await factoryStartup.ConfigureAwait(false);
289  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
290  await Task.WhenAll(tasks).ConfigureAwait(false);
291  logger.LogInformation("Server ready!");
292  readyTcs.SetResult(null);
293  }
294  catch (OperationCanceledException)
295  {
296  logger.LogInformation("Cancelled instance manager initialization!");
297  }
298  catch (Exception e)
299  {
300  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
301  try
302  {
303  await serverControl.Die(e).ConfigureAwait(false);
304  return;
305  }
306  catch (Exception e2)
307  {
308  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
309  }
310 
311  throw;
312  }
313  });
314  #pragma warning restore CA1506 // TODO: Decomplexify
315 
317  public async Task StopAsync(CancellationToken cancellationToken)
318  {
319  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
320  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
321  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
322 
323  // downgrade the db if necessary
324  if (downgradeVersion != null)
325  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
326  }
327 
329  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
330  {
331  downgradeVersion = updateVersion != null && updateVersion < assemblyInformationProvider.Version ? updateVersion : null;
332  return Task.CompletedTask;
333  }
334 
339  {
340  if (generalConfiguration.UseExperimentalWatchdog && !Debugger.IsAttached)
341  throw new InvalidOperationException(
342  "The experimental watchdog is currently non-functional! Please disable the '{nameof(generalConfiguration.UseExperimentalWatchdog)}' option in your configuration!");
343 
344  using var systemIdentity = systemIdentityFactory.GetCurrent();
345  if (!systemIdentity.CanCreateSymlinks)
346  throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!");
347  }
348 
350  public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
351  {
352  if (parameters == null)
353  throw new ArgumentNullException(nameof(parameters));
354 
355  IBridgeHandler bridgeHandler = null;
356  for (var i = 0; bridgeHandler == null && i < 30; ++i)
357  {
358  // There's a miniscule time period where we could potentially receive a bridge request and not have the registration ready when we launch DD
359  // This is a stopgap
360  Task delayTask = Task.CompletedTask;
361  lock (bridgeHandlers)
362  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
363  delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
364 
365  await delayTask.ConfigureAwait(false);
366  }
367 
368  if (bridgeHandler == null)
369  lock (bridgeHandlers)
370  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
371  {
372  logger.LogWarning("Recieved invalid bridge request with accees identifier: {0}", parameters.AccessIdentifier);
373  return null;
374  }
375 
376  return await bridgeHandler.ProcessBridgeRequest(parameters, cancellationToken).ConfigureAwait(false);
377  }
378 
381  {
382  if (bridgeHandler == null)
383  throw new ArgumentNullException(nameof(bridgeHandler));
384 
385  var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier;
386  lock (bridgeHandlers)
387  {
388  bridgeHandlers.Add(accessIdentifier, bridgeHandler);
389  logger.LogTrace("Registered bridge handler: {0}", accessIdentifier);
390  }
391 
392  return new BridgeRegistration(() =>
393  {
394  lock (bridgeHandlers)
395  {
396  bridgeHandlers.Remove(accessIdentifier);
397  logger.LogTrace("Unregistered bridge handler: {0}", accessIdentifier);
398  }
399  });
400  }
401  }
402 }
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the InstanceManager
-
readonly IServerControl serverControl
The IServerControl for the InstanceManager
-
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
17 using Tgstation.Server.Host.IO;
21 
22 namespace Tgstation.Server.Host.Components
23 {
26  {
28  public Task Ready => readyTcs.Task;
29 
34 
39 
44 
49 
54 
59 
64 
69 
73  readonly ILogger<InstanceManager> logger;
74 
78  readonly IDictionary<long, IInstance> instances;
79 
83  readonly IDictionary<string, IBridgeHandler> bridgeHandlers;
84 
89 
93  readonly TaskCompletionSource<object> readyTcs;
94 
99 
103  bool disposed;
104 
119  IInstanceFactory instanceFactory,
120  IIOManager ioManager,
121  IDatabaseContextFactory databaseContextFactory,
122  IAssemblyInformationProvider assemblyInformationProvider,
123  IJobManager jobManager,
124  IServerControl serverControl,
125  ISystemIdentityFactory systemIdentityFactory,
126  IAsyncDelayer asyncDelayer,
127  IOptions<GeneralConfiguration> generalConfigurationOptions,
128  ILogger<InstanceManager> logger)
129  {
130  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
131  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
132  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
133  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
134  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
135  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
136  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
137  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
138  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
139  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
140 
141  serverControl.RegisterForRestart(this);
142 
143  instances = new Dictionary<long, IInstance>();
144  bridgeHandlers = new Dictionary<string, IBridgeHandler>();
145  readyTcs = new TaskCompletionSource<object>();
146  }
147 
149  public void Dispose()
150  {
151  lock (instances)
152  {
153  if (disposed)
154  return;
155  disposed = true;
156  }
157 
158  foreach (var I in instances)
159  I.Value.Dispose();
160  }
161 
163  public IInstance GetInstance(Models.Instance metadata)
164  {
165  if (metadata == null)
166  throw new ArgumentNullException(nameof(metadata));
167  lock (instances)
168  {
169  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
170  throw new InvalidOperationException("Instance not online!");
171  return instance;
172  }
173  }
174 
176  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
177  {
178  if (newPath == null)
179  throw new ArgumentNullException(nameof(newPath));
180  if (instance.Online.Value)
181  throw new InvalidOperationException("Cannot move an online instance!");
182  var oldPath = instance.Path;
183  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
184  await databaseContextFactory.UseContext(db =>
185  {
186  var targetInstance = new Models.Instance
187  {
188  Id = instance.Id
189  };
190  db.Instances.Attach(targetInstance);
191  targetInstance.Path = newPath;
192  return db.Save(cancellationToken);
193  }).ConfigureAwait(false);
194  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
195  }
196 
198  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
199  {
200  if (metadata == null)
201  throw new ArgumentNullException(nameof(metadata));
202  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
203  IInstance instance;
204  lock (instances)
205  {
206  if (!instances.TryGetValue(metadata.Id, out instance))
207  throw new InvalidOperationException("Instance not online!");
208  instances.Remove(metadata.Id);
209  }
210 
211  try
212  {
213  // we are the one responsible for cancelling his jobs
214  var tasks = new List<Task>();
215  await databaseContextFactory.UseContext(async db =>
216  {
217  var jobs = db
218  .Jobs
219  .AsQueryable()
220  .Where(x => x.Instance.Id == metadata.Id)
221  .Select(x => new Models.Job
222  {
223  Id = x.Id
224  });
225  await jobs.ForEachAsync(job =>
226  {
227  lock (tasks)
228  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
229  }, cancellationToken).ConfigureAwait(false);
230  }).ConfigureAwait(false);
231 
232  await Task.WhenAll(tasks).ConfigureAwait(false);
233 
234  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
235  }
236  finally
237  {
238  instance.Dispose();
239  }
240  }
241 
243  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
244  {
245  if (metadata == null)
246  throw new ArgumentNullException(nameof(metadata));
247  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
248  var instance = instanceFactory.CreateInstance(this, metadata);
249  try
250  {
251  lock (instances)
252  {
253  if (instances.ContainsKey(metadata.Id))
254  throw new InvalidOperationException("Instance already online!");
255  instances.Add(metadata.Id, instance);
256  }
257  }
258  catch
259  {
260  instance.Dispose();
261  throw;
262  }
263 
264  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
265  }
266 
268  #pragma warning disable CA1506 // TODO: Decomplexify
269  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
270  {
271  logger.LogInformation(assemblyInformationProvider.VersionString);
272 
273  try
274  {
275  CheckSystemCompatibility();
276  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
277  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
278  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
279  var dbInstances = databaseContext
280  .Instances
281  .AsQueryable()
282  .Where(x => x.Online.Value)
283  .Include(x => x.RepositorySettings)
284  .Include(x => x.ChatSettings)
285  .ThenInclude(x => x.Channels)
286  .Include(x => x.DreamDaemonSettings)
287  .ToAsyncEnumerable();
288  var tasks = new List<Task>();
289  await factoryStartup.ConfigureAwait(false);
290  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
291  await Task.WhenAll(tasks).ConfigureAwait(false);
292  logger.LogInformation("Server ready!");
293  readyTcs.SetResult(null);
294  }
295  catch (OperationCanceledException)
296  {
297  logger.LogInformation("Cancelled instance manager initialization!");
298  }
299  catch (Exception e)
300  {
301  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
302  try
303  {
304  await serverControl.Die(e).ConfigureAwait(false);
305  return;
306  }
307  catch (Exception e2)
308  {
309  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
310  }
311 
312  throw;
313  }
314  });
315  #pragma warning restore CA1506 // TODO: Decomplexify
316 
318  public async Task StopAsync(CancellationToken cancellationToken)
319  {
320  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
321  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
322  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
323 
324  // downgrade the db if necessary
325  if (downgradeVersion != null)
326  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
327  }
328 
330  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
331  {
332  downgradeVersion = updateVersion != null && updateVersion < assemblyInformationProvider.Version ? updateVersion : null;
333  return Task.CompletedTask;
334  }
335 
340  {
341  if (generalConfiguration.UseExperimentalWatchdog && !Debugger.IsAttached)
342  throw new InvalidOperationException(
343  "The experimental watchdog is currently non-functional! Please disable the '{nameof(generalConfiguration.UseExperimentalWatchdog)}' option in your configuration!");
344 
345  using var systemIdentity = systemIdentityFactory.GetCurrent();
346  if (!systemIdentity.CanCreateSymlinks)
347  throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!");
348  }
349 
351  public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
352  {
353  if (parameters == null)
354  throw new ArgumentNullException(nameof(parameters));
355 
356  IBridgeHandler bridgeHandler = null;
357  for (var i = 0; bridgeHandler == null && i < 30; ++i)
358  {
359  // There's a miniscule time period where we could potentially receive a bridge request and not have the registration ready when we launch DD
360  // This is a stopgap
361  Task delayTask = Task.CompletedTask;
362  lock (bridgeHandlers)
363  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
364  delayTask = asyncDelayer.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
365 
366  await delayTask.ConfigureAwait(false);
367  }
368 
369  if (bridgeHandler == null)
370  lock (bridgeHandlers)
371  if (!bridgeHandlers.TryGetValue(parameters.AccessIdentifier, out bridgeHandler))
372  {
373  logger.LogWarning("Recieved invalid bridge request with accees identifier: {0}", parameters.AccessIdentifier);
374  return null;
375  }
376 
377  return await bridgeHandler.ProcessBridgeRequest(parameters, cancellationToken).ConfigureAwait(false);
378  }
379 
382  {
383  if (bridgeHandler == null)
384  throw new ArgumentNullException(nameof(bridgeHandler));
385 
386  var accessIdentifier = bridgeHandler.DMApiParameters.AccessIdentifier;
387  lock (bridgeHandlers)
388  {
389  bridgeHandlers.Add(accessIdentifier, bridgeHandler);
390  logger.LogTrace("Registered bridge handler: {0}", accessIdentifier);
391  }
392 
393  return new BridgeRegistration(() =>
394  {
395  lock (bridgeHandlers)
396  {
397  bridgeHandlers.Remove(accessIdentifier);
398  logger.LogTrace("Unregistered bridge handler: {0}", accessIdentifier);
399  }
400  });
401  }
402  }
403 }
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the InstanceManager
+
readonly IServerControl serverControl
The IServerControl for the InstanceManager
+
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
-
readonly IDictionary< string, IBridgeHandler > bridgeHandlers
Map of DMApiParameters.AccessIdentifiers to their respective IBridgeHandlers.
+ +
readonly IDictionary< string, IBridgeHandler > bridgeHandlers
Map of DMApiParameters.AccessIdentifiers to their respective IBridgeHandlers.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
+
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
-
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the InstanceManager
-
bool disposed
If the InstanceManager has been Disposed
-
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
-
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the InstanceManager
+
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the InstanceManager
+
bool disposed
If the InstanceManager has been Disposed
+
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
+
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the InstanceManager
-
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
-
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
+
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
+ +
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
-
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
-
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceManager.
+
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
+
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the InstanceManager.
-
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
-
readonly IJobManager jobManager
The IJobManager for the InstanceManager
+
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
+
readonly IJobManager jobManager
The IJobManager for the InstanceManager
-
readonly IDictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances. Also used as a object.
+
readonly IDictionary< long, IInstance > instances
Map of instance EntityId.Ids to respective IInstances. Also used as a object.
-
readonly TaskCompletionSource< object > readyTcs
The TaskCompletionSource<TResult> for Ready.
-
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
+
readonly TaskCompletionSource< object > readyTcs
The TaskCompletionSource<TResult> for Ready.
+
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
For interacting with the instance services
Definition: IInstance.cs:16
- -
async Task StopAsync(CancellationToken cancellationToken)
+ +
async Task StopAsync(CancellationToken cancellationToken)
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance
- -
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
+ +
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
-
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
+
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
Manages the runtime of Jobs
Definition: IJobManager.cs:13
@@ -144,19 +146,19 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
readonly IIOManager ioManager
The IIOManager for the InstanceManager
+
readonly IIOManager ioManager
The IIOManager for the InstanceManager
Interface for using filesystems
Definition: IIOManager.cs:11
-
void CheckSystemCompatibility()
Check we have a valid system identity.
-
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IAssemblyInformationProvider assemblyInformationProvider, IJobManager jobManager, IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< InstanceManager > logger)
Construct an InstanceManager
-
async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
Move an IInstance
+
void CheckSystemCompatibility()
Check we have a valid system identity.
+
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IAssemblyInformationProvider assemblyInformationProvider, IJobManager jobManager, IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, IOptions< GeneralConfiguration > generalConfigurationOptions, ILogger< InstanceManager > logger)
Construct an InstanceManager
+
async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
Move an IInstance
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
Represents a registration of an interop session.
- + -
DMApiParameters DMApiParameters
The DMApiParameters for the IBridgeHandler.
+
DMApiParameters DMApiParameters
The DMApiParameters for the IBridgeHandler.
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_instance_manager_client_8cs_source.html b/_instance_manager_client_8cs_source.html index 246c8643b0..9b9172cb0d 100644 --- a/_instance_manager_client_8cs_source.html +++ b/_instance_manager_client_8cs_source.html @@ -94,7 +94,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManagerClient.cs
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Threading;
4 using System.Threading.Tasks;
5 using Tgstation.Server.Api;
8 
9 namespace Tgstation.Server.Client
10 {
13  {
18 
22  readonly Dictionary<long, IInstanceClient> cachedClients;
23 
29  {
30  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
31 
32  cachedClients = new Dictionary<long, IInstanceClient>();
33  }
34 
36  public Task<Instance> CreateOrAttach(Instance instance, CancellationToken cancellationToken) => apiClient.Create<Instance, Instance>(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
37 
39  public Task Detach(Instance instance, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
40 
42  public Task<IReadOnlyList<Instance>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Instance>>(Routes.ListRoute(Routes.InstanceManager), cancellationToken);
43 
45  public Task<Instance> Update(Instance instance, CancellationToken cancellationToken) => apiClient.Update<Instance, Instance>(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
46 
48  public Task<Instance> GetId(Instance instance, CancellationToken cancellationToken) => apiClient.Read<Instance>(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
49 
52  {
53  if (!cachedClients.TryGetValue(instance?.Id ?? throw new ArgumentNullException(nameof(instance)), out var client))
54  {
55  client = new InstanceClient(apiClient, instance);
56  cachedClients.Add(instance.Id, client);
57  }
58 
59  return client;
60  }
61  }
62 }
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Threading;
4 using System.Threading.Tasks;
5 using Tgstation.Server.Api;
8 
9 namespace Tgstation.Server.Client
10 {
13  {
18 
22  readonly Dictionary<long, IInstanceClient> cachedClients;
23 
29  {
30  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
31 
32  cachedClients = new Dictionary<long, IInstanceClient>();
33  }
34 
36  public Task<Instance> CreateOrAttach(Instance instance, CancellationToken cancellationToken) => apiClient.Create<Instance, Instance>(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
37 
39  public Task Detach(Instance instance, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
40 
42  public Task<IReadOnlyList<Instance>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Instance>>(Routes.ListRoute(Routes.InstanceManager), cancellationToken);
43 
45  public Task<Instance> Update(Instance instance, CancellationToken cancellationToken) => apiClient.Update<Instance, Instance>(Routes.InstanceManager, instance ?? throw new ArgumentNullException(nameof(instance)), cancellationToken);
46 
48  public Task<Instance> GetId(Instance instance, CancellationToken cancellationToken) => apiClient.Read<Instance>(Routes.SetID(Routes.InstanceManager, instance?.Id ?? throw new ArgumentNullException(nameof(instance))), cancellationToken);
49 
52  {
53  if (!cachedClients.TryGetValue(instance?.Id ?? throw new ArgumentNullException(nameof(instance)), out var client))
54  {
55  client = new InstanceClient(apiClient, instance);
56  cachedClients.Add(instance.Id, client);
57  }
58 
59  return client;
60  }
61  }
62 }
+
long Id
The ID of the entity.
Definition: EntityId.cs:11
@@ -109,7 +110,6 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManagerClient(IApiClient apiClient)
Construct an InstanceManagerClient
readonly Dictionary< long, IInstanceClient > cachedClients
Map of already created IInstanceClients
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
const string InstanceManager
The Models.Instance controller
Definition: Routes.cs:29
Metadata about a server instance
Definition: Instance.cs:9
diff --git a/_instance_user_controller_8cs_source.html b/_instance_user_controller_8cs_source.html index ab23246951..34b0451361 100644 --- a/_instance_user_controller_8cs_source.html +++ b/_instance_user_controller_8cs_source.html @@ -94,11 +94,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceUserController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
16 using Z.EntityFramework.Plus;
17 
18 namespace Tgstation.Server.Host.Controllers
19 {
23  [Route(Routes.InstanceUser)]
24  public sealed class InstanceUserController : ApiController
25  {
32  public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
33  { }
34 
40  IActionResult StandardModelChecks(Api.Models.InstanceUser model)
41  {
42  if (model == null)
43  throw new ArgumentNullException(nameof(model));
44 
45  if (!model.UserId.HasValue)
46  return BadRequest(new ErrorMessage(ErrorCode.UserMissingId));
47 
48  return null;
49  }
50 
58  [HttpPut]
59  [TgsAuthorize(InstanceUserRights.CreateUsers)]
60  [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)]
61  public async Task<IActionResult> Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
62  {
63  // Don't check the result as how can a new user have an ID
64  StandardModelChecks(model);
65 
66  var dbUser = new Models.InstanceUser
67  {
68  ByondRights = RightsHelper.Clamp(model.ByondRights ?? ByondRights.None),
69  ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? ChatBotRights.None),
70  ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? ConfigurationRights.None),
71  DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? DreamDaemonRights.None),
72  DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? DreamMakerRights.None),
73  RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? RepositoryRights.None),
74  InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? InstanceUserRights.None),
75  UserId = model.UserId,
76  InstanceId = Instance.Id
77  };
78 
79  DatabaseContext.InstanceUsers.Add(dbUser);
80 
81  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
82  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
83  }
84 
93  [HttpPost]
94  [TgsAuthorize(InstanceUserRights.WriteUsers)]
95  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
96  [ProducesResponseType(410)]
97  #pragma warning disable CA1506 // TODO: Decomplexify
98  public async Task<IActionResult> Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
99  {
100  var earlyOut = StandardModelChecks(model);
101  if (earlyOut != null)
102  return earlyOut;
103 
104  var originalUser = await DatabaseContext
105  .Instances
106  .AsQueryable()
107  .Where(x => x.Id == Instance.Id)
108  .SelectMany(x => x.InstanceUsers)
109  .Where(x => x.UserId == model.UserId)
110  .FirstOrDefaultAsync(cancellationToken)
111  .ConfigureAwait(false);
112  if (originalUser == null)
113  return StatusCode((int)HttpStatusCode.Gone);
114 
115  originalUser.ByondRights = RightsHelper.Clamp(model.ByondRights ?? originalUser.ByondRights.Value);
116  originalUser.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalUser.RepositoryRights.Value);
117  originalUser.InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? originalUser.InstanceUserRights.Value);
118  originalUser.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalUser.ChatBotRights.Value);
119  originalUser.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalUser.ConfigurationRights.Value);
120  originalUser.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalUser.DreamDaemonRights.Value);
121  originalUser.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalUser.DreamMakerRights.Value);
122 
123  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
124  return Json(originalUser.UserId == AuthenticationContext.User.Id || (AuthenticationContext.GetRight(RightsType.InstanceUser) & (ulong)InstanceUserRights.ReadUsers) != 0 ? originalUser.ToApi() : new Api.Models.InstanceUser
125  {
126  UserId = originalUser.UserId
127  });
128  }
129 #pragma warning restore CA1506
130  [HttpGet]
136  [TgsAuthorize]
137  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
138  public IActionResult Read() => Json(AuthenticationContext.InstanceUser.ToApi());
139 
146  [HttpGet(Routes.List)]
147  [TgsAuthorize(InstanceUserRights.ReadUsers)]
148  [ProducesResponseType(typeof(IEnumerable<Api.Models.InstanceUser>), 200)]
149  public async Task<IActionResult> List(CancellationToken cancellationToken)
150  {
151  var users = await DatabaseContext
152  .Instances
153  .AsQueryable()
154  .Where(x => x.Id == Instance.Id)
155  .SelectMany(x => x.InstanceUsers)
156  .ToListAsync(cancellationToken)
157  .ConfigureAwait(false);
158  return Json(users.Select(x => x.ToApi()));
159  }
160 
169  [HttpGet("{id}")]
170  [TgsAuthorize(InstanceUserRights.ReadUsers)]
171  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
172  [ProducesResponseType(410)]
173  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
174  {
175  // this functions as userId
176  var user = await DatabaseContext
177  .Instances
178  .AsQueryable()
179  .Where(x => x.Id == Instance.Id)
180  .SelectMany(x => x.InstanceUsers)
181  .Where(x => x.UserId == id)
182  .FirstOrDefaultAsync(cancellationToken)
183  .ConfigureAwait(false);
184  if (user == default)
185  return StatusCode((int)HttpStatusCode.Gone);
186  return Json(user.ToApi());
187  }
188 
196  [HttpDelete("{id}")]
197  [TgsAuthorize(InstanceUserRights.WriteUsers)]
198  [ProducesResponseType(204)]
199  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
200  {
201  await DatabaseContext
202  .Instances
203  .AsQueryable()
204  .Where(x => x.Id == Instance.Id)
205  .SelectMany(x => x.InstanceUsers)
206  .Where(x => x.UserId == id)
207  .DeleteAsync(cancellationToken)
208  .ConfigureAwait(false);
209  return NoContent();
210  }
211  }
212 }
InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger< InstanceUserController > logger)
Construct a UserController
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
16 using Z.EntityFramework.Plus;
17 
18 namespace Tgstation.Server.Host.Controllers
19 {
23  [Route(Routes.InstanceUser)]
24  public sealed class InstanceUserController : ApiController
25  {
32  public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
33  { }
34 
40  IActionResult StandardModelChecks(Api.Models.InstanceUser model)
41  {
42  if (model == null)
43  throw new ArgumentNullException(nameof(model));
44 
45  if (!model.UserId.HasValue)
46  return BadRequest(new ErrorMessage(ErrorCode.UserMissingId));
47 
48  return null;
49  }
50 
58  [HttpPut]
59  [TgsAuthorize(InstanceUserRights.CreateUsers)]
60  [ProducesResponseType(typeof(Api.Models.InstanceUser), 201)]
61  public async Task<IActionResult> Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
62  {
63  // Don't check the result as how can a new user have an ID
64  StandardModelChecks(model);
65 
66  var dbUser = new Models.InstanceUser
67  {
68  ByondRights = RightsHelper.Clamp(model.ByondRights ?? ByondRights.None),
69  ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? ChatBotRights.None),
70  ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? ConfigurationRights.None),
71  DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? DreamDaemonRights.None),
72  DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? DreamMakerRights.None),
73  RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? RepositoryRights.None),
74  InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? InstanceUserRights.None),
75  UserId = model.UserId,
76  InstanceId = Instance.Id
77  };
78 
79  DatabaseContext.InstanceUsers.Add(dbUser);
80 
81  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
82  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
83  }
84 
93  [HttpPost]
94  [TgsAuthorize(InstanceUserRights.WriteUsers)]
95  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
96  [ProducesResponseType(410)]
97  #pragma warning disable CA1506 // TODO: Decomplexify
98  public async Task<IActionResult> Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
99  {
100  var earlyOut = StandardModelChecks(model);
101  if (earlyOut != null)
102  return earlyOut;
103 
104  var originalUser = await DatabaseContext
105  .Instances
106  .AsQueryable()
107  .Where(x => x.Id == Instance.Id)
108  .SelectMany(x => x.InstanceUsers)
109  .Where(x => x.UserId == model.UserId)
110  .FirstOrDefaultAsync(cancellationToken)
111  .ConfigureAwait(false);
112  if (originalUser == null)
113  return StatusCode((int)HttpStatusCode.Gone);
114 
115  originalUser.ByondRights = RightsHelper.Clamp(model.ByondRights ?? originalUser.ByondRights.Value);
116  originalUser.RepositoryRights = RightsHelper.Clamp(model.RepositoryRights ?? originalUser.RepositoryRights.Value);
117  originalUser.InstanceUserRights = RightsHelper.Clamp(model.InstanceUserRights ?? originalUser.InstanceUserRights.Value);
118  originalUser.ChatBotRights = RightsHelper.Clamp(model.ChatBotRights ?? originalUser.ChatBotRights.Value);
119  originalUser.ConfigurationRights = RightsHelper.Clamp(model.ConfigurationRights ?? originalUser.ConfigurationRights.Value);
120  originalUser.DreamDaemonRights = RightsHelper.Clamp(model.DreamDaemonRights ?? originalUser.DreamDaemonRights.Value);
121  originalUser.DreamMakerRights = RightsHelper.Clamp(model.DreamMakerRights ?? originalUser.DreamMakerRights.Value);
122 
123  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
124  return Json(originalUser.UserId == AuthenticationContext.User.Id || (AuthenticationContext.GetRight(RightsType.InstanceUser) & (ulong)InstanceUserRights.ReadUsers) != 0 ? originalUser.ToApi() : new Api.Models.InstanceUser
125  {
126  UserId = originalUser.UserId
127  });
128  }
129 #pragma warning restore CA1506
130  [HttpGet]
136  [TgsAuthorize]
137  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
138  public IActionResult Read() => Json(AuthenticationContext.InstanceUser.ToApi());
139 
146  [HttpGet(Routes.List)]
147  [TgsAuthorize(InstanceUserRights.ReadUsers)]
148  [ProducesResponseType(typeof(IEnumerable<Api.Models.InstanceUser>), 200)]
149  public async Task<IActionResult> List(CancellationToken cancellationToken)
150  {
151  var users = await DatabaseContext
152  .Instances
153  .AsQueryable()
154  .Where(x => x.Id == Instance.Id)
155  .SelectMany(x => x.InstanceUsers)
156  .ToListAsync(cancellationToken)
157  .ConfigureAwait(false);
158  return Json(users.Select(x => x.ToApi()));
159  }
160 
169  [HttpGet("{id}")]
170  [TgsAuthorize(InstanceUserRights.ReadUsers)]
171  [ProducesResponseType(typeof(Api.Models.InstanceUser), 200)]
172  [ProducesResponseType(410)]
173  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
174  {
175  // this functions as userId
176  var user = await DatabaseContext
177  .Instances
178  .AsQueryable()
179  .Where(x => x.Id == Instance.Id)
180  .SelectMany(x => x.InstanceUsers)
181  .Where(x => x.UserId == id)
182  .FirstOrDefaultAsync(cancellationToken)
183  .ConfigureAwait(false);
184  if (user == default)
185  return StatusCode((int)HttpStatusCode.Gone);
186  return Json(user.ToApi());
187  }
188 
196  [HttpDelete("{id}")]
197  [TgsAuthorize(InstanceUserRights.WriteUsers)]
198  [ProducesResponseType(204)]
199  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
200  {
201  await DatabaseContext
202  .Instances
203  .AsQueryable()
204  .Where(x => x.Id == Instance.Id)
205  .SelectMany(x => x.InstanceUsers)
206  .Where(x => x.UserId == id)
207  .DeleteAsync(cancellationToken)
208  .ConfigureAwait(false);
209  return NoContent();
210  }
211  }
212 }
InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger< InstanceUserController > logger)
Construct a UserController
ConfigurationRights
Rights for Models.ConfigurationFile
+
long Id
The ID of the entity.
Definition: EntityId.cs:11
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
@@ -120,11 +121,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DbSet< Instance > Instances
The Instances in the DatabaseContext.
- +
DreamMakerRights
Rights for Models.DreamMaker
DbSet< InstanceUser > InstanceUsers
The InstanceUsers in the DatabaseContext.
RepositoryRights
Rights for a Models.Repository
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
long Id
The ID of the User
Definition: User.cs:16
diff --git a/_job_controller_8cs_source.html b/_job_controller_8cs_source.html index 1dcf5be672..07c3194cc6 100644 --- a/_job_controller_8cs_source.html +++ b/_job_controller_8cs_source.html @@ -94,8 +94,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JobController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.Jobs)]
22  public sealed class JobController : ApiController
23  {
28 
36  public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger<JobController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
37  {
38  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
39  }
40 
47  [HttpGet]
48  [TgsAuthorize]
49  [ProducesResponseType(typeof(IEnumerable<Api.Models.Job>), 200)]
50  public async Task<IActionResult> Read(CancellationToken cancellationToken)
51  {
52  var result = await DatabaseContext
53  .Jobs
54  .AsQueryable()
55  .Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue)
56  .OrderByDescending(x => x.StartedAt)
57  .ToListAsync(cancellationToken)
58  .ConfigureAwait(false);
59  return Json(result.Select(x => x.ToApi()));
60  }
61 
68  [HttpGet(Routes.List)]
69  [TgsAuthorize]
70  [ProducesResponseType(typeof(List<Api.Models.EntityId>), 200)]
71  public async Task<IActionResult> List(CancellationToken cancellationToken)
72  {
73  // you KNOW this will need pagination eventually right?
74  var jobs = await DatabaseContext
75  .Jobs
76  .AsQueryable()
77  .Where(x => x.Instance.Id == Instance.Id)
78  .OrderByDescending(x => x.StartedAt)
79  .Select(x => new Api.Models.EntityId
80  {
81  Id = x.Id
82  })
83  .ToListAsync(cancellationToken)
84  .ConfigureAwait(false);
85  return Json(jobs);
86  }
87 
97  [HttpDelete("{id}")]
98  [TgsAuthorize]
99  [ProducesResponseType(typeof(Api.Models.Job), 202)]
100  [ProducesResponseType(404)]
101  [ProducesResponseType(410)]
102  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
103  {
104  // don't care if an instance post or not at this point
105  var job = await DatabaseContext
106  .Jobs
107  .AsQueryable()
108  .Where(x => x.Id == id && x.Instance.Id == Instance.Id)
109  .FirstOrDefaultAsync(cancellationToken)
110  .ConfigureAwait(false);
111  if (job == default(Job))
112  return NotFound();
113 
114  if (job.StoppedAt != null)
115  return StatusCode((int)HttpStatusCode.Gone);
116 
117  if (job.CancelRight.HasValue && job.CancelRightsType.HasValue && (AuthenticationContext.GetRight(job.CancelRightsType.Value) & job.CancelRight.Value) == 0)
118  return Forbid();
119 
120  var updatedJob = await jobManager.CancelJob(job, AuthenticationContext.User, false, cancellationToken).ConfigureAwait(false);
121  return updatedJob != null ? (IActionResult)Accepted(updatedJob.ToApi()) : StatusCode((int)HttpStatusCode.Gone);
122  }
123 
132  [HttpGet("{id}")]
133  [TgsAuthorize]
134  [ProducesResponseType(typeof(Api.Models.Job), 200)]
135  [ProducesResponseType(404)]
136  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
137  {
138  var job = await DatabaseContext
139  .Jobs
140  .AsQueryable()
141  .Where(x => x.Id == id && x.Instance.Id == Instance.Id)
142  .Include(x => x.StartedBy)
143  .FirstOrDefaultAsync(cancellationToken)
144  .ConfigureAwait(false);
145  if (job == default)
146  return NotFound();
147  var api = job.ToApi();
148  api.Progress = jobManager.JobProgress(job);
149  return Json(api);
150  }
151  }
152 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.Jobs)]
22  public sealed class JobController : ApiController
23  {
28 
36  public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger<JobController> logger) : base(databaseContext, authenticationContextFactory, logger, true, true)
37  {
38  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
39  }
40 
47  [HttpGet]
48  [TgsAuthorize]
49  [ProducesResponseType(typeof(IEnumerable<Api.Models.Job>), 200)]
50  public async Task<IActionResult> Read(CancellationToken cancellationToken)
51  {
52  var result = await DatabaseContext
53  .Jobs
54  .AsQueryable()
55  .Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue)
56  .OrderByDescending(x => x.StartedAt)
57  .ToListAsync(cancellationToken)
58  .ConfigureAwait(false);
59  return Json(result.Select(x => x.ToApi()));
60  }
61 
68  [HttpGet(Routes.List)]
69  [TgsAuthorize]
70  [ProducesResponseType(typeof(List<Api.Models.EntityId>), 200)]
71  public async Task<IActionResult> List(CancellationToken cancellationToken)
72  {
73  // you KNOW this will need pagination eventually right?
74  var jobs = await DatabaseContext
75  .Jobs
76  .AsQueryable()
77  .Where(x => x.Instance.Id == Instance.Id)
78  .OrderByDescending(x => x.StartedAt)
79  .Select(x => new Api.Models.EntityId
80  {
81  Id = x.Id
82  })
83  .ToListAsync(cancellationToken)
84  .ConfigureAwait(false);
85  return Json(jobs);
86  }
87 
97  [HttpDelete("{id}")]
98  [TgsAuthorize]
99  [ProducesResponseType(typeof(Api.Models.Job), 202)]
100  [ProducesResponseType(404)]
101  [ProducesResponseType(410)]
102  public async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
103  {
104  // don't care if an instance post or not at this point
105  var job = await DatabaseContext
106  .Jobs
107  .AsQueryable()
108  .Where(x => x.Id == id && x.Instance.Id == Instance.Id)
109  .FirstOrDefaultAsync(cancellationToken)
110  .ConfigureAwait(false);
111  if (job == default(Job))
112  return NotFound();
113 
114  if (job.StoppedAt != null)
115  return StatusCode((int)HttpStatusCode.Gone);
116 
117  if (job.CancelRight.HasValue && job.CancelRightsType.HasValue && (AuthenticationContext.GetRight(job.CancelRightsType.Value) & job.CancelRight.Value) == 0)
118  return Forbid();
119 
120  var updatedJob = await jobManager.CancelJob(job, AuthenticationContext.User, false, cancellationToken).ConfigureAwait(false);
121  return updatedJob != null ? (IActionResult)Accepted(updatedJob.ToApi()) : StatusCode((int)HttpStatusCode.Gone);
122  }
123 
132  [HttpGet("{id}")]
133  [TgsAuthorize]
134  [ProducesResponseType(typeof(Api.Models.Job), 200)]
135  [ProducesResponseType(404)]
136  public async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
137  {
138  var job = await DatabaseContext
139  .Jobs
140  .AsQueryable()
141  .Where(x => x.Id == id && x.Instance.Id == Instance.Id)
142  .Include(x => x.StartedBy)
143  .FirstOrDefaultAsync(cancellationToken)
144  .ConfigureAwait(false);
145  if (job == default)
146  return NotFound();
147  var api = job.ToApi();
148  api.Progress = jobManager.JobProgress(job);
149  return Json(api);
150  }
151  }
152 }
+
long Id
The ID of the entity.
Definition: EntityId.cs:11
@@ -107,10 +108,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Routes to a server actions
Definition: Routes.cs:9
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
- +
async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Get a specific Api.Models.Job.
const string Jobs
The Models.Job controller
Definition: Routes.cs:79
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
async Task< IActionResult > Read(CancellationToken cancellationToken)
Get active Api.Models.Jobs for the instance.
diff --git a/_job_manager_8cs_source.html b/_job_manager_8cs_source.html index 0f69c75863..0907cd90a4 100644 --- a/_job_manager_8cs_source.html +++ b/_job_manager_8cs_source.html @@ -94,47 +94,47 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JobManager.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Jobs
12 {
15  {
20 
24  readonly ILogger<JobManager> logger;
25 
29  readonly Dictionary<long, JobHandler> jobs;
30 
34  readonly object synchronizationLock;
35 
41  public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger<JobManager> logger)
42  {
43  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
44  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
45  jobs = new Dictionary<long, JobHandler>();
46  synchronizationLock = new object();
47  }
48 
50  public void Dispose()
51  {
52  foreach (var job in jobs)
53  job.Value.Dispose();
54  }
55 
62  {
63  lock (synchronizationLock)
64  {
65  if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler))
66  throw new InvalidOperationException("Job not running!");
67  return jobHandler;
68  }
69  }
70 
78  async Task RunJob(Job job, Func<Job, IDatabaseContextFactory, CancellationToken, Task> operation, CancellationToken cancellationToken)
79  {
80  try
81  {
82  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
83  try
84  {
85  var oldJob = job;
86  job = new Job { Id = oldJob.Id };
87 
88  await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false);
89 
90  logger.LogDebug("Job {0} completed!", job.Id);
91  }
92  catch (OperationCanceledException)
93  {
94  logger.LogDebug("Job {0} cancelled!", job.Id);
95  job.Cancelled = true;
96  }
97  catch (JobException e)
98  {
99  job.ErrorCode = e.ErrorCode;
100  job.ExceptionDetails = e.Message;
101  LogRegularException();
102  if (e.InnerException != null)
103  logger.LogDebug(
104  "Inner exception for job {0}: {1}",
105  job.Id,
106  e.InnerException is JobException
107  ? e.InnerException.Message
108  : e.InnerException.ToString());
109  }
110  catch (Exception e)
111  {
112  job.ExceptionDetails = e.ToString();
113  LogRegularException();
114  }
115 
116  await databaseContextFactory.UseContext(async databaseContext =>
117  {
118  var attachedJob = new Job
119  {
120  Id = job.Id
121  };
122 
123  databaseContext.Jobs.Attach(attachedJob);
124  attachedJob.StoppedAt = DateTimeOffset.Now;
125  attachedJob.ExceptionDetails = job.ExceptionDetails;
126  attachedJob.ErrorCode = job.ErrorCode;
127  attachedJob.Cancelled = job.Cancelled;
128 
129  await databaseContext.Save(default).ConfigureAwait(false);
130  }).ConfigureAwait(false);
131  }
132  finally
133  {
134  lock (synchronizationLock)
135  {
136  var handler = jobs[job.Id];
137  jobs.Remove(job.Id);
138  handler.Dispose();
139  }
140  }
141  }
142 
144  public Task RegisterOperation(Job job, Func<Job, IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
145  {
146  if (job == null)
147  throw new ArgumentNullException(nameof(job));
148  if (operation == null)
149  throw new ArgumentNullException(nameof(operation));
150 
151  job.StartedAt = DateTimeOffset.Now;
152  job.Cancelled = false;
153 
154  job.Instance = new Instance
155  {
156  Id = job.Instance.Id
157  };
158  databaseContext.Instances.Attach(job.Instance);
159 
160  job.StartedBy = new User
161  {
162  Id = job.StartedBy.Id
163  };
164  databaseContext.Users.Attach(job.StartedBy);
165 
166  databaseContext.Jobs.Add(job);
167 
168  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
169  logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
170  var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
171  operation(jobParam, serviceProvider, y =>
172  {
173  lock (synchronizationLock)
174  if (jobs.TryGetValue(job.Id, out var handler))
175  handler.Progress = y;
176  }, ct),
177  x));
178  lock (synchronizationLock)
179  jobs.Add(job.Id, jobHandler);
180  });
181 
183  public async Task StartAsync(CancellationToken cancellationToken)
184  {
185  logger.LogTrace("Starting job manager...");
186  await databaseContextFactory.UseContext(async databaseContext =>
187  {
188  // mark all jobs as cancelled
189  var badJobs = await databaseContext
190  .Jobs
191  .AsQueryable()
192  .Where(y => !y.StoppedAt.HasValue)
193  .Select(y => y.Id)
194  .ToListAsync(cancellationToken)
195  .ConfigureAwait(false);
196  if (badJobs.Count > 0)
197  {
198  logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
199  foreach (var I in badJobs)
200  {
201  var job = new Job { Id = I };
202  databaseContext.Jobs.Attach(job);
203  job.Cancelled = true;
204  job.StoppedAt = DateTimeOffset.Now;
205  }
206 
207  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
208  }
209  }).ConfigureAwait(false);
210  logger.LogDebug("Job manager started!");
211  }
212 
214  public async Task StopAsync(CancellationToken cancellationToken)
215  {
216  var joinTasks = jobs.Select(x =>
217  {
218  x.Value.Cancel();
219  return x.Value.Wait(cancellationToken);
220  });
221  await Task.WhenAll(joinTasks).ConfigureAwait(false);
222  }
223 
225  public async Task<Job> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
226  {
227  if (job == null)
228  throw new ArgumentNullException(nameof(job));
229  if (user == null)
230  throw new ArgumentNullException(nameof(user));
231  JobHandler handler;
232  try
233  {
234  handler = CheckGetJob(job);
235  }
236  catch (InvalidOperationException)
237  {
238  // this is fine
239  return null;
240  }
241 
242  handler.Cancel(); // this will ensure the db update is only done once
243  await databaseContextFactory.UseContext(async databaseContext =>
244  {
245  var updatedJob = new Job { Id = job.Id };
246  databaseContext.Jobs.Attach(job);
247  var attachedUser = new User { Id = user.Id };
248  databaseContext.Users.Attach(user);
249  updatedJob.CancelledBy = attachedUser;
250 
251  // let either startup or cancellation set job.cancelled
252  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
253  job.CancelledBy = user;
254  }).ConfigureAwait(false);
255  if (blocking)
256  await handler.Wait(cancellationToken).ConfigureAwait(false);
257  return job;
258  }
259 
261  public int? JobProgress(Job job)
262  {
263  if (job == null)
264  throw new ArgumentNullException(nameof(job));
265  lock (synchronizationLock)
266  {
267  if (!jobs.TryGetValue(job.Id, out var handler))
268  return null;
269  return handler.Progress;
270  }
271  }
272 
274  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
275  {
276  if (job == null)
277  throw new ArgumentNullException(nameof(job));
278  if (canceller == null)
279  throw new ArgumentNullException(nameof(canceller));
280  JobHandler handler;
281  lock (synchronizationLock)
282  {
283  if (!jobs.TryGetValue(job.Id, out handler))
284  return;
285  }
286 
287  Task cancelTask = null;
288  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
289  await handler.Wait(cancellationToken).ConfigureAwait(false);
290 
291  if (cancelTask != null)
292  await cancelTask.ConfigureAwait(false);
293  }
294  }
295 }
long Id
The ID of the entity.
Definition: EntityId.cs:11
-
readonly IDatabaseContextFactory databaseContextFactory
The IServiceProvider for the JobManager
Definition: JobManager.cs:19
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Serilog.Context;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Jobs
13 {
16  {
21 
25  readonly ILogger<JobManager> logger;
26 
30  readonly Dictionary<long, JobHandler> jobs;
31 
35  readonly object synchronizationLock;
36 
42  public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger<JobManager> logger)
43  {
44  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
45  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
46  jobs = new Dictionary<long, JobHandler>();
47  synchronizationLock = new object();
48  }
49 
51  public void Dispose()
52  {
53  foreach (var job in jobs)
54  job.Value.Dispose();
55  }
56 
63  {
64  lock (synchronizationLock)
65  {
66  if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler))
67  throw new InvalidOperationException("Job not running!");
68  return jobHandler;
69  }
70  }
71 
79  async Task RunJob(Job job, Func<Job, IDatabaseContextFactory, CancellationToken, Task> operation, CancellationToken cancellationToken)
80  {
81  using (LogContext.PushProperty("Job", job.Id))
82  try
83  {
84  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
85  try
86  {
87  var oldJob = job;
88  job = new Job { Id = oldJob.Id };
89 
90  await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false);
91 
92  logger.LogDebug("Job {0} completed!", job.Id);
93  }
94  catch (OperationCanceledException)
95  {
96  logger.LogDebug("Job {0} cancelled!", job.Id);
97  job.Cancelled = true;
98  }
99  catch (JobException e)
100  {
101  job.ErrorCode = e.ErrorCode;
102  job.ExceptionDetails = e.Message;
103  LogRegularException();
104  if (e.InnerException != null)
105  logger.LogDebug(
106  "Inner exception for job {0}: {1}",
107  job.Id,
108  e.InnerException is JobException
109  ? e.InnerException.Message
110  : e.InnerException.ToString());
111  }
112  catch (Exception e)
113  {
114  job.ExceptionDetails = e.ToString();
115  LogRegularException();
116  }
117 
118  await databaseContextFactory.UseContext(async databaseContext =>
119  {
120  var attachedJob = new Job
121  {
122  Id = job.Id
123  };
124 
125  databaseContext.Jobs.Attach(attachedJob);
126  attachedJob.StoppedAt = DateTimeOffset.Now;
127  attachedJob.ExceptionDetails = job.ExceptionDetails;
128  attachedJob.ErrorCode = job.ErrorCode;
129  attachedJob.Cancelled = job.Cancelled;
130 
131  await databaseContext.Save(default).ConfigureAwait(false);
132  }).ConfigureAwait(false);
133  }
134  finally
135  {
136  lock (synchronizationLock)
137  {
138  var handler = jobs[job.Id];
139  jobs.Remove(job.Id);
140  handler.Dispose();
141  }
142  }
143  }
144 
146  public Task RegisterOperation(Job job, Func<Job, IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
147  {
148  if (job == null)
149  throw new ArgumentNullException(nameof(job));
150  if (operation == null)
151  throw new ArgumentNullException(nameof(operation));
152 
153  job.StartedAt = DateTimeOffset.Now;
154  job.Cancelled = false;
155 
156  job.Instance = new Instance
157  {
158  Id = job.Instance.Id
159  };
160  databaseContext.Instances.Attach(job.Instance);
161 
162  job.StartedBy = new User
163  {
164  Id = job.StartedBy.Id
165  };
166  databaseContext.Users.Attach(job.StartedBy);
167 
168  databaseContext.Jobs.Add(job);
169 
170  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
171  logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
172  var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
173  operation(jobParam, serviceProvider, y =>
174  {
175  lock (synchronizationLock)
176  if (jobs.TryGetValue(job.Id, out var handler))
177  handler.Progress = y;
178  }, ct),
179  x));
180  lock (synchronizationLock)
181  jobs.Add(job.Id, jobHandler);
182  });
183 
185  public async Task StartAsync(CancellationToken cancellationToken)
186  {
187  logger.LogTrace("Starting job manager...");
188  await databaseContextFactory.UseContext(async databaseContext =>
189  {
190  // mark all jobs as cancelled
191  var badJobs = await databaseContext
192  .Jobs
193  .AsQueryable()
194  .Where(y => !y.StoppedAt.HasValue)
195  .Select(y => y.Id)
196  .ToListAsync(cancellationToken)
197  .ConfigureAwait(false);
198  if (badJobs.Count > 0)
199  {
200  logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
201  foreach (var I in badJobs)
202  {
203  var job = new Job { Id = I };
204  databaseContext.Jobs.Attach(job);
205  job.Cancelled = true;
206  job.StoppedAt = DateTimeOffset.Now;
207  }
208 
209  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
210  }
211  }).ConfigureAwait(false);
212  logger.LogDebug("Job manager started!");
213  }
214 
216  public async Task StopAsync(CancellationToken cancellationToken)
217  {
218  var joinTasks = jobs.Select(x =>
219  {
220  x.Value.Cancel();
221  return x.Value.Wait(cancellationToken);
222  });
223  await Task.WhenAll(joinTasks).ConfigureAwait(false);
224  }
225 
227  public async Task<Job> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
228  {
229  if (job == null)
230  throw new ArgumentNullException(nameof(job));
231  if (user == null)
232  throw new ArgumentNullException(nameof(user));
233  JobHandler handler;
234  try
235  {
236  handler = CheckGetJob(job);
237  }
238  catch (InvalidOperationException)
239  {
240  // this is fine
241  return null;
242  }
243 
244  handler.Cancel(); // this will ensure the db update is only done once
245  await databaseContextFactory.UseContext(async databaseContext =>
246  {
247  var updatedJob = new Job { Id = job.Id };
248  databaseContext.Jobs.Attach(job);
249  var attachedUser = new User { Id = user.Id };
250  databaseContext.Users.Attach(user);
251  updatedJob.CancelledBy = attachedUser;
252 
253  // let either startup or cancellation set job.cancelled
254  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
255  job.CancelledBy = user;
256  }).ConfigureAwait(false);
257  if (blocking)
258  await handler.Wait(cancellationToken).ConfigureAwait(false);
259  return job;
260  }
261 
263  public int? JobProgress(Job job)
264  {
265  if (job == null)
266  throw new ArgumentNullException(nameof(job));
267  lock (synchronizationLock)
268  {
269  if (!jobs.TryGetValue(job.Id, out var handler))
270  return null;
271  return handler.Progress;
272  }
273  }
274 
276  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
277  {
278  if (job == null)
279  throw new ArgumentNullException(nameof(job));
280  if (canceller == null)
281  throw new ArgumentNullException(nameof(canceller));
282  JobHandler handler;
283  lock (synchronizationLock)
284  {
285  if (!jobs.TryGetValue(job.Id, out handler))
286  return;
287  }
288 
289  Task cancelTask = null;
290  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
291  await handler.Wait(cancellationToken).ConfigureAwait(false);
292 
293  if (cancelTask != null)
294  await cancelTask.ConfigureAwait(false);
295  }
296  }
297 }
long Id
The ID of the entity.
Definition: EntityId.cs:11
+
readonly IDatabaseContextFactory databaseContextFactory
The IServiceProvider for the JobManager
Definition: JobManager.cs:20
User StartedBy
See Api.Models.Job.StartedBy
Definition: Job.cs:12
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:183
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:185
-
readonly Dictionary< long, JobHandler > jobs
Dictionary<TKey, TValue> of Job Api.Models.EntityId.Ids to running JobHandlers
Definition: JobManager.cs:29
- + +
readonly Dictionary< long, JobHandler > jobs
Dictionary<TKey, TValue> of Job Api.Models.EntityId.Ids to running JobHandlers
Definition: JobManager.cs:30
+
DateTimeOffset StartedAt
When the Job was started
Definition: Job.cs:32
string Description
English description of the Job
Definition: Job.cs:16
-
int JobProgress(Job job)
Get the Api.Models.Job.Progress for a job
Definition: JobManager.cs:261
+
int JobProgress(Job job)
Get the Api.Models.Job.Progress for a job
Definition: JobManager.cs:263
string ExceptionDetails
Details of any exceptions caught during the Job
Definition: Job.cs:26
Class for pairing Tasks with CancellationTokenSources
Definition: JobHandler.cs:10
User CancelledBy
See Api.Models.Job.CancelledBy
Definition: Job.cs:17
Instance Instance
The Models.Instance the job belongs to if any
Definition: Job.cs:23
-
async Task< Job > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job
Definition: JobManager.cs:225
+
async Task< Job > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job
Definition: JobManager.cs:227
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
-
JobHandler CheckGetJob(Job job)
Gets the JobHandler for a given job if it exists
Definition: JobManager.cs:61
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
+
JobHandler CheckGetJob(Job job)
Gets the JobHandler for a given job if it exists
Definition: JobManager.cs:62
ErrorCode ErrorCode
The Models.ErrorCode associated with the Job if any.
Definition: Job.cs:21
-
async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete
Definition: JobManager.cs:274
+
async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete
Definition: JobManager.cs:276
long Id
The ID of the User
Definition: User.cs:16
-
JobManager(IDatabaseContextFactory databaseContextFactory, ILogger< JobManager > logger)
Construct a JobManager
Definition: JobManager.cs:41
+
JobManager(IDatabaseContextFactory databaseContextFactory, ILogger< JobManager > logger)
Construct a JobManager
Definition: JobManager.cs:42
Manages the runtime of Jobs
Definition: IJobManager.cs:13
-
readonly object synchronizationLock
object for various operations.
Definition: JobManager.cs:34
+
readonly object synchronizationLock
object for various operations.
Definition: JobManager.cs:35
-
async Task RunJob(Job job, Func< Job, IDatabaseContextFactory, CancellationToken, Task > operation, CancellationToken cancellationToken)
Runner for JobHandlers
Definition: JobManager.cs:78
+
async Task RunJob(Job job, Func< Job, IDatabaseContextFactory, CancellationToken, Task > operation, CancellationToken cancellationToken)
Runner for JobHandlers
Definition: JobManager.cs:79
bool Cancelled
If the Job was cancelled
Definition: Job.cs:43
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:214
- +
async Task StopAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:216
+
async Task Wait(CancellationToken cancellationToken)
Wait for task to complete
Definition: JobHandler.cs:47
ErrorCode ErrorCode
The Api.Models.ErrorCode associated with the JobException.
Definition: JobException.cs:14
-
readonly ILogger< JobManager > logger
The ILogger for the JobManager
Definition: JobManager.cs:24
+
readonly ILogger< JobManager > logger
The ILogger for the JobManager
Definition: JobManager.cs:25
diff --git a/_jobs_client_8cs_source.html b/_jobs_client_8cs_source.html index be1c76a3b9..a01c84fad4 100644 --- a/_jobs_client_8cs_source.html +++ b/_jobs_client_8cs_source.html @@ -104,7 +104,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Routes to a server actions
Definition: Routes.cs:9
const string Jobs
The Models.Job controller
Definition: Routes.cs:79
-
Common base of CompileJobs and Jobs.
Definition: EntityId.cs:6
+
Common base of Instances, CompileJobs, and Jobs.
Definition: EntityId.cs:6
readonly IApiClient apiClient
The IApiClient for the JobsClient
Definition: JobsClient.cs:16
Metadata about a server instance
Definition: Instance.cs:9
diff --git a/_repository_controller_8cs_source.html b/_repository_controller_8cs_source.html index 3441f036f9..7844b34592 100644 --- a/_repository_controller_8cs_source.html +++ b/_repository_controller_8cs_source.html @@ -94,11 +94,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RepositoryController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Linq.Expressions;
10 using System.Net;
11 using System.Reflection;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
24 
25 namespace Tgstation.Server.Host.Controllers
26 {
30  [Route(Routes.Repository)]
31  #pragma warning disable CA1506 // TODO: Decomplexify
32  public sealed class RepositoryController : ApiController
33  {
38 
43 
48 
53 
64  public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger<RepositoryController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true, true)
65  {
66  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
67  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
68  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
69  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
70  }
71 
72  async Task<bool> LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action<Models.RevisionInformation> revInfoSink, CancellationToken cancellationToken)
73  {
74  var repoSha = repository.Head;
75 
76  IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> query) => query
77  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
78  .Include(x => x.CompileJobs)
79  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy);
80 
81  var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
82 
83  // If the DB doesn't have it, check the local set
84  if (revisionInfo == default)
85  revisionInfo = databaseContext
87  .Local
88  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
89  .FirstOrDefault();
90 
91  var needsDbUpdate = revisionInfo == default;
92  if (needsDbUpdate)
93  {
94  // needs insertion
95  revisionInfo = new Models.RevisionInformation
96  {
97  Instance = instance,
98  CommitSha = repoSha,
99  CompileJobs = new List<Models.CompileJob>(),
100  ActiveTestMerges = new List<RevInfoTestMerge>() // non null vals for api returns
101  };
102 
103  lock (databaseContext) // cleaner this way
104  databaseContext.RevisionInformations.Add(revisionInfo);
105  }
106 
107  revisionInfo.OriginCommitSha ??= lastOriginCommitSha;
108  if (revisionInfo.OriginCommitSha == null)
109  {
110  revisionInfo.OriginCommitSha = repoSha;
111  Logger.LogWarning(Components.Repository.Repository.OriginTrackingErrorTemplate, repoSha);
112  }
113 
114  revInfoSink?.Invoke(revisionInfo);
115  return needsDbUpdate;
116  }
117 
118  async Task<bool> PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
119  {
120  if (repository.IsGitHubRepository)
121  {
122  model.GitHubOwner = repository.GitHubOwner;
123  model.GitHubName = repository.GitHubRepoName;
124  }
125 
126  model.Origin = repository.Origin;
127  model.Reference = repository.Reference;
128 
129  // rev info stuff
130  Models.RevisionInformation revisionInfo = null;
131  var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, null, x => revisionInfo = x, cancellationToken).ConfigureAwait(false);
132  model.RevisionInformation = revisionInfo.ToApi();
133  return needsDbUpdate;
134  }
135 
144  [HttpPut]
145  [TgsAuthorize(RepositoryRights.SetOrigin)]
146  [ProducesResponseType(typeof(Repository), 201)]
147  [ProducesResponseType(410)]
148  public async Task<IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken)
149  {
150  if (model == null)
151  throw new ArgumentNullException(nameof(model));
152 
153  if (model.Origin == null)
154  return BadRequest(ErrorCode.RepoMissingOrigin);
155 
156  if (model.AccessUser == null ^ model.AccessToken == null)
157  return BadRequest(ErrorCode.RepoMismatchUserAndAccessToken);
158 
159  var currentModel = await DatabaseContext
161  .AsQueryable()
162  .Where(x => x.InstanceId == Instance.Id)
163  .FirstOrDefaultAsync(cancellationToken)
164  .ConfigureAwait(false);
165 
166  if (currentModel == default)
167  return StatusCode((int)HttpStatusCode.Gone);
168 
169  // normalize github urls
170  const string BadGitHubUrl = "://www.github.com/";
171  var uiOrigin = model.Origin.ToUpperInvariant();
172  var uiBad = BadGitHubUrl.ToUpperInvariant();
173  var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant();
174  if (uiOrigin.Contains(uiBad, StringComparison.Ordinal))
175  model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal);
176 
177  currentModel.AccessToken = model.AccessToken;
178  currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm
179  var cloneBranch = model.Reference;
180  var origin = model.Origin;
181 
182  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
183 
184  if (repoManager.CloneInProgress)
185  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
186 
187  if (repoManager.InUse)
188  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
189 
190  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
191 
192  // clone conflict
193  if (repo != null)
194  return Conflict(new ErrorMessage(ErrorCode.RepoExists));
195 
196  var job = new Models.Job
197  {
198  Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"),
199  StartedBy = AuthenticationContext.User,
200  CancelRightsType = RightsType.Repository,
201  CancelRight = (ulong)RepositoryRights.CancelClone,
203  };
204  var api = currentModel.ToApi();
205  await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
206  {
207  using var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false);
208  if (repos == null)
209  throw new JobException(ErrorCode.RepoExists);
210  var instance = new Models.Instance
211  {
212  Id = Instance.Id
213  };
214  await databaseContextFactory.UseContext(
215  async databaseContext =>
216  {
217  databaseContext.Instances.Attach(instance);
218  if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false))
219  await databaseContext.Save(ct).ConfigureAwait(false);
220  })
221  .ConfigureAwait(false);
222  }, cancellationToken).ConfigureAwait(false);
223 
224  api.Origin = model.Origin;
225  api.Reference = model.Reference;
226  api.ActiveJob = job.ToApi();
227 
228  return StatusCode((int)HttpStatusCode.Created, api);
229  }
230 
238  [HttpDelete]
239  [TgsAuthorize(RepositoryRights.Delete)]
240  [ProducesResponseType(typeof(Repository), 202)]
241  [ProducesResponseType(410)]
242  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
243  {
244  var currentModel = await DatabaseContext
246  .AsQueryable()
247  .Where(x => x.InstanceId == Instance.Id)
248  .FirstOrDefaultAsync(cancellationToken)
249  .ConfigureAwait(false);
250 
251  if (currentModel == default)
252  return StatusCode((int)HttpStatusCode.Gone);
253 
254  currentModel.AccessToken = null;
255  currentModel.AccessUser = null;
256 
257  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
258 
259  Logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id);
260 
261  var job = new Models.Job
262  {
263  Description = "Delete repository",
264  StartedBy = AuthenticationContext.User,
266  };
267  var api = currentModel.ToApi();
268  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
269  api.ActiveJob = job.ToApi();
270  return Accepted(api);
271  }
272 
281  [HttpGet]
282  [TgsAuthorize(RepositoryRights.Read)]
283  [ProducesResponseType(typeof(Repository), 200)]
284  [ProducesResponseType(typeof(Repository), 201)]
285  [ProducesResponseType(410)]
286  public async Task<IActionResult> Read(CancellationToken cancellationToken)
287  {
288  var currentModel = await DatabaseContext
290  .AsQueryable()
291  .Where(x => x.InstanceId == Instance.Id)
292  .FirstOrDefaultAsync(cancellationToken)
293  .ConfigureAwait(false);
294 
295  if (currentModel == default)
296  return StatusCode((int)HttpStatusCode.Gone, new ErrorMessage(ErrorCode.RepoMissing));
297 
298  var api = currentModel.ToApi();
299  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
300 
301  if (repoManager.CloneInProgress)
302  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
303 
304  if (repoManager.InUse)
305  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
306 
307  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
308  if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false))
309  {
310  // user may have fucked with the repo manually, do what we can
311  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
312  return StatusCode((int)HttpStatusCode.Created, api);
313  }
314 
315  return Json(api);
316  }
317 
327  [HttpPost]
328  [TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)]
329  [ProducesResponseType(typeof(Repository), 200)]
330  [ProducesResponseType(typeof(Repository), 202)]
331  [ProducesResponseType(410)]
332  #pragma warning disable CA1502, CA1505 // TODO: Decomplexify
333  public async Task<IActionResult> Update([FromBody]Repository model, CancellationToken cancellationToken)
334  {
335  if (model == null)
336  throw new ArgumentNullException(nameof(model));
337 
338  if (model.AccessUser == null ^ model.AccessToken == null)
339  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchUserAndAccessToken));
340 
341  if (model.CheckoutSha != null && model.Reference != null)
342  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndReference));
343 
344  if (model.CheckoutSha != null && model.UpdateFromOrigin == true)
345  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndUpdate));
346 
347  if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true)
348  return BadRequest(new ErrorMessage(ErrorCode.RepoDuplicateTestMerge));
349 
350  if (model.CommitterName?.Length == 0)
351  return BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterName));
352 
353  if (model.CommitterEmail?.Length == 0)
354  return BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterEmail));
355 
356  var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0;
357  var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository);
358  if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest))
359  return Forbid();
360 
361  var currentModel = await DatabaseContext
363  .AsQueryable()
364  .Where(x => x.InstanceId == Instance.Id)
365  .FirstOrDefaultAsync(cancellationToken)
366  .ConfigureAwait(false);
367 
368  if (currentModel == default)
369  return StatusCode((int)HttpStatusCode.Gone);
370 
371  bool CheckModified<T>(Expression<Func<Api.Models.Internal.RepositorySettings, T>> expression, RepositoryRights requiredRight)
372  {
373  var memberSelectorExpression = (MemberExpression)expression.Body;
374  var property = (PropertyInfo)memberSelectorExpression.Member;
375 
376  var newVal = property.GetValue(model);
377  if (newVal == null)
378  return false;
379  if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal)
380  return true;
381 
382  property.SetValue(currentModel, newVal);
383  return false;
384  }
385 
386  if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials)
387  || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials)
388  || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings)
389  || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings)
390  || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter)
391  || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter)
392  || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits)
393  || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits)
394  || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits)
395  || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch)))
396  return Forbid();
397 
398  if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0)
399  {
400  // setting an empty string clears everything
401  currentModel.AccessUser = null;
402  currentModel.AccessToken = null;
403  }
404 
405  var canRead = userRights.HasFlag(RepositoryRights.Read);
406 
407  var api = canRead ? currentModel.ToApi() : new Repository();
408  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
409 
410  if (canRead)
411  {
412  if (repoManager.CloneInProgress)
413  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
414 
415  if (repoManager.InUse)
416  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
417 
418  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
419  if (repo == null)
420  return Conflict(new ErrorMessage(ErrorCode.RepoMissing));
421  await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false);
422 
423  if (model.Origin != null && model.Origin != repo.Origin)
424  return BadRequest(new ErrorMessage(ErrorCode.RepoCantChangeOrigin));
425  }
426 
427  // this is just db stuf so stow it away
428  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
429 
430  // format the job description
431  string description = null;
432  if (model.UpdateFromOrigin == true)
433  if (model.Reference != null)
434  description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference);
435  else if (model.CheckoutSha != null)
436  description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha);
437  else
438  description = "Pull current repository reference";
439  else if (model.Reference != null || model.CheckoutSha != null)
440  description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha);
441 
442  if (newTestMerges)
443  description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}",
444  description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T",
445  String.Join(", ", model.NewTestMerges.Select(x =>
446  String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number,
447  x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))),
448  description != null ? String.Empty : " in repository");
449 
450  if (description == null)
451  return Json(api); // no git changes
452 
453  var job = new Models.Job
454  {
455  Description = description,
456  StartedBy = AuthenticationContext.User,
457  Instance = Instance,
458  CancelRightsType = RightsType.Repository,
459  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
460  };
461 
462  // Time to access git, do it in a job
463  await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
464  {
465  using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false);
466  if (repo == null)
467  throw new JobException(ErrorCode.RepoMissing);
468 
469  var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null;
470 
471  var startReference = repo.Reference;
472  var startSha = repo.Head;
473  string postUpdateSha = null;
474 
475  if (newTestMerges && !repo.IsGitHubRepository)
476  throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote);
477 
478  var committerName = currentModel.ShowTestMergeCommitters.Value
480  : currentModel.CommitterName;
481 
482  var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null;
483 
484  var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
485  var doneSteps = 0;
486 
487  Action<int> NextProgressReporter()
488  {
489  var tmpDoneSteps = doneSteps;
490  ++doneSteps;
491  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps);
492  }
493 
494  progressReporter(0);
495 
496  // get a base line for where we are
497  Models.RevisionInformation lastRevisionInfo = null;
498 
499  var attachedInstance = new Models.Instance
500  {
501  Id = Instance.Id
502  };
503 
504  Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory
505  .UseContext(
506  async databaseContext =>
507  {
508  databaseContext.Instances.Attach(attachedInstance);
509  var previousRevInfo = lastRevisionInfo;
510  var needsUpdate = await LoadRevisionInformation(
511  repo,
512  databaseContext,
513  attachedInstance,
514  lastOriginCommitSha,
515  x => lastRevisionInfo = x,
516  ct)
517  .ConfigureAwait(false);
518 
519  if (testMergeToAdd != null)
520  {
521  // rev info may have already loaded the user
522  var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id);
523  if (mergedBy == default)
524  {
525  mergedBy = new Models.User
526  {
528  };
529 
530  databaseContext.Users.Attach(mergedBy);
531  }
532 
533  testMergeToAdd.MergedBy = mergedBy;
534 
535  lastRevisionInfo.ActiveTestMerges.AddRange(previousRevInfo.ActiveTestMerges);
536  lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
537  {
538  TestMerge = testMergeToAdd
539  });
540  lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
541 
542  needsUpdate = true;
543  }
544 
545  if (needsUpdate)
546  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
547  });
548 
549  await CallLoadRevInfo().ConfigureAwait(false);
550 
551  // apply new rev info, tracking applied test merges
552  Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha);
553 
554  try
555  {
556  // fetch/pull
557  if (model.UpdateFromOrigin == true)
558  {
559  if (!repo.Tracking)
560  throw new JobException(ErrorCode.RepoReferenceRequired);
561  await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false);
562  doneSteps = 1;
563  if (!modelHasShaOrReference)
564  {
565  var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false);
566  if (!fastForward.HasValue)
567  throw new JobException(ErrorCode.RepoMergeConflict);
568  await UpdateRevInfo().ConfigureAwait(false);
569  if (fastForward.Value)
570  {
571  lastRevisionInfo.OriginCommitSha = repo.Head;
572  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
573  postUpdateSha = repo.Head;
574  }
575  else
576  NextProgressReporter()(100);
577  }
578  }
579 
580  // checkout/hard reset
581  if (modelHasShaOrReference)
582  {
583  var validCheckoutSha =
584  model.CheckoutSha != null
585  && !repo.Head.StartsWith(model.CheckoutSha, StringComparison.OrdinalIgnoreCase);
586  var validCheckoutReference =
587  model.Reference != null
588  && !repo.Reference.Equals(model.Reference, StringComparison.OrdinalIgnoreCase);
589  if (validCheckoutSha || validCheckoutReference)
590  {
591  var committish = model.CheckoutSha ?? model.Reference;
592  var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false);
593 
594  if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
595  throw new JobException(ErrorCode.RepoSwappedShaOrReference);
596 
597  await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false);
598  await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin
599  }
600  else
601  NextProgressReporter()(100);
602 
603  if (hardResettingToOriginReference)
604  {
605  if (!repo.Tracking)
606  throw new JobException(ErrorCode.RepoReferenceNotTracking);
607  await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false);
608  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
609  await CallLoadRevInfo().ConfigureAwait(false);
610 
611  // repo head is on origin so force this
612  // will update the db if necessary
613  lastRevisionInfo.OriginCommitSha = repo.Head;
614  }
615  }
616 
617  // test merging
618  Dictionary<int, Octokit.PullRequest> prMap = null;
619  if (newTestMerges)
620  {
621  // bit of sanitization
622  foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision)))
623  I.PullRequestRevision = null;
624 
625  var gitHubClient = currentModel.AccessToken != null
626  ? gitHubClientFactory.CreateClient(currentModel.AccessToken)
627  : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken)
628  ? gitHubClientFactory.CreateClient()
629  : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken));
630 
631  var repoOwner = repo.GitHubOwner;
632  var repoName = repo.GitHubRepoName;
633 
634  // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out
635  Models.RevisionInformation revInfoWereLookingFor = null;
636  bool needToApplyRemainingPrs = true;
637  if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
638  {
639  // In order for this to work though we need the shas of all the commits
640  if (model.NewTestMerges.Any(x => x.PullRequestRevision == null))
641  prMap = new Dictionary<int, Octokit.PullRequest>();
642 
643  bool cantSearch = false;
644  foreach (var I in model.NewTestMerges)
645  {
646  if (I.PullRequestRevision != null)
647 #pragma warning disable CA1308 // Normalize strings to uppercase
648  I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant(); // ala libgit2
649 #pragma warning restore CA1308 // Normalize strings to uppercase
650  else
651  try
652  {
653  // retrieve the latest sha
654  var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
655  prMap.Add(I.Number, pr);
656  I.PullRequestRevision = pr.Head.Sha;
657  }
658  catch
659  {
660  cantSearch = true;
661  break;
662  }
663  }
664 
665  if (!cantSearch)
666  {
667  List<Models.RevisionInformation> dbPull = null;
668 
669  await databaseContextFactory.UseContext(
670  async databaseContext =>
671  dbPull = await databaseContext.RevisionInformations
672  .AsQueryable()
673  .Where(x => x.Instance.Id == Instance.Id
674  && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
675  && x.ActiveTestMerges.Count <= model.NewTestMerges.Count
676  && x.ActiveTestMerges.Count > 0)
677  .Include(x => x.ActiveTestMerges)
678  .ThenInclude(x => x.TestMerge)
679  .ToListAsync(cancellationToken)
680  .ConfigureAwait(false))
681  .ConfigureAwait(false);
682 
683  // split here cause this bit has to be done locally
684  revInfoWereLookingFor = dbPull
685  .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count
686  && x.ActiveTestMerges.Select(y => y.TestMerge)
687  .All(y => model.NewTestMerges.Any(z =>
688  y.Number == z.Number
689  && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
690  && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
691  .FirstOrDefault();
692 
693  if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1)
694  {
695  // okay try to add at least SOME prs we've seen before
696  var search = model.NewTestMerges.ToList();
697 
698  var appliedTestMergeIds = new List<long>();
699 
700  Models.RevisionInformation lastGoodRevInfo = null;
701  do
702  {
703  foreach (var I in search)
704  {
705  revInfoWereLookingFor = dbPull
706  .Where(x => model.NewTestMerges.Any(z =>
707  x.PrimaryTestMerge.Number == z.Number
708  && x.PrimaryTestMerge.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
709  && (x.PrimaryTestMerge.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))
710  && x.ActiveTestMerges.Select(y => y.TestMerge).All(y => appliedTestMergeIds.Contains(y.Id)))
711  .FirstOrDefault();
712 
713  if (revInfoWereLookingFor != null)
714  {
715  lastGoodRevInfo = revInfoWereLookingFor;
716  appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id);
717  search.Remove(I);
718  break;
719  }
720  }
721  }
722  while (revInfoWereLookingFor != null && search.Count > 0);
723 
724  revInfoWereLookingFor = lastGoodRevInfo;
725  needToApplyRemainingPrs = search.Count != 0;
726  if (needToApplyRemainingPrs)
727  model.NewTestMerges = search;
728  }
729  else if (revInfoWereLookingFor != null)
730  needToApplyRemainingPrs = false;
731  }
732  }
733 
734  if (revInfoWereLookingFor != null)
735  {
736  // goteem
737  Logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha);
738  await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false);
739  lastRevisionInfo = revInfoWereLookingFor;
740  }
741 
742  if (needToApplyRemainingPrs)
743  {
744  foreach (var I in model.NewTestMerges)
745  {
746  Octokit.PullRequest pr = null;
747  string errorMessage = null;
748 
749  if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number))
750  throw new JobException(ErrorCode.RepoDuplicateTestMerge);
751 
752  Exception exception = null;
753  try
754  {
755  // load from cache if possible
756  if (prMap == null || !prMap.TryGetValue(I.Number, out pr))
757  pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
758  }
759  catch (Octokit.RateLimitExceededException ex)
760  {
761  // you look at your anonymous access and sigh
762  errorMessage = "REMOTE API ERROR: RATE LIMITED";
763  exception = ex;
764  }
765  catch (Octokit.AuthorizationException ex)
766  {
767  errorMessage = "REMOTE API ERROR: BAD CREDENTIALS";
768  exception = ex;
769  }
770  catch (Octokit.NotFoundException ex)
771  {
772  // you look at your shithub and sigh
773  errorMessage = "REMOTE API ERROR: PULL REQUEST NOT FOUND";
774  exception = ex;
775  }
776 
777  if (exception != null)
778  Logger.LogWarning("Error retrieving pull request metadata: {0}", exception);
779 
780  // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it
781  if (I.PullRequestRevision == null && pr != null)
782  I.PullRequestRevision = pr.Head.Sha;
783 
784  var mergeResult = await repo.AddTestMerge(
785  I,
786  committerName,
787  currentModel.CommitterEmail,
788  currentModel.AccessUser,
789  currentModel.AccessToken,
790  NextProgressReporter(),
791  ct).ConfigureAwait(false);
792 
793  if (!mergeResult.HasValue)
794  throw new JobException(
795  ErrorCode.RepoTestMergeConflict,
796  new JobException(
797  $"Merge of PR #{I.Number} at {I.PullRequestRevision.Substring(0, 7)} conflicted!"));
798 
799  ++doneSteps;
800 
801  // MergedBy will be set later
802  var tm = new Models.TestMerge
803  {
804  Author = pr?.User.Login ?? errorMessage,
805  BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty,
806  MergedAt = DateTimeOffset.Now,
807  TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty,
808  Comment = I.Comment,
809  Number = I.Number,
810  PullRequestRevision = I.PullRequestRevision,
811  Url = pr?.HtmlUrl ?? errorMessage
812  };
813 
814  await UpdateRevInfo(tm).ConfigureAwait(false);
815  }
816  }
817  }
818 
819  var currentHead = repo.Head;
820  if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))
821  {
822  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false);
823  await UpdateRevInfo().ConfigureAwait(false);
824  }
825  }
826  catch
827  {
828  doneSteps = 0;
829  numSteps = 2;
830 
831  // Forget what we've done and abort
832  await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false);
833  if (startReference != null && repo.Head != startSha)
834  await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false);
835  else
836  progressReporter(100);
837  throw;
838  }
839  }, cancellationToken).ConfigureAwait(false);
840 
841  api.ActiveJob = job.ToApi();
842  return Accepted(api);
843  }
844  #pragma warning restore CA1502, CA1505
845  }
846 }
string Name
The name of the User
Definition: User.cs:41
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Linq.Expressions;
10 using System.Net;
11 using System.Reflection;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
24 
25 namespace Tgstation.Server.Host.Controllers
26 {
30  [Route(Routes.Repository)]
31  #pragma warning disable CA1506 // TODO: Decomplexify
32  public sealed class RepositoryController : ApiController
33  {
38 
43 
48 
53 
64  public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger<RepositoryController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true, true)
65  {
66  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
67  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
68  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
69  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
70  }
71 
72  async Task<bool> LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action<Models.RevisionInformation> revInfoSink, CancellationToken cancellationToken)
73  {
74  var repoSha = repository.Head;
75 
76  IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> query) => query
77  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
78  .Include(x => x.CompileJobs)
79  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy);
80 
81  var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
82 
83  // If the DB doesn't have it, check the local set
84  if (revisionInfo == default)
85  revisionInfo = databaseContext
87  .Local
88  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
89  .FirstOrDefault();
90 
91  var needsDbUpdate = revisionInfo == default;
92  if (needsDbUpdate)
93  {
94  // needs insertion
95  revisionInfo = new Models.RevisionInformation
96  {
97  Instance = instance,
98  CommitSha = repoSha,
99  CompileJobs = new List<Models.CompileJob>(),
100  ActiveTestMerges = new List<RevInfoTestMerge>() // non null vals for api returns
101  };
102 
103  lock (databaseContext) // cleaner this way
104  databaseContext.RevisionInformations.Add(revisionInfo);
105  }
106 
107  revisionInfo.OriginCommitSha ??= lastOriginCommitSha;
108  if (revisionInfo.OriginCommitSha == null)
109  {
110  revisionInfo.OriginCommitSha = repoSha;
111  Logger.LogWarning(Components.Repository.Repository.OriginTrackingErrorTemplate, repoSha);
112  }
113 
114  revInfoSink?.Invoke(revisionInfo);
115  return needsDbUpdate;
116  }
117 
118  async Task<bool> PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
119  {
120  if (repository.IsGitHubRepository)
121  {
122  model.GitHubOwner = repository.GitHubOwner;
123  model.GitHubName = repository.GitHubRepoName;
124  }
125 
126  model.Origin = repository.Origin;
127  model.Reference = repository.Reference;
128 
129  // rev info stuff
130  Models.RevisionInformation revisionInfo = null;
131  var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, null, x => revisionInfo = x, cancellationToken).ConfigureAwait(false);
132  model.RevisionInformation = revisionInfo.ToApi();
133  return needsDbUpdate;
134  }
135 
144  [HttpPut]
145  [TgsAuthorize(RepositoryRights.SetOrigin)]
146  [ProducesResponseType(typeof(Repository), 201)]
147  [ProducesResponseType(410)]
148  public async Task<IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken)
149  {
150  if (model == null)
151  throw new ArgumentNullException(nameof(model));
152 
153  if (model.Origin == null)
154  return BadRequest(ErrorCode.RepoMissingOrigin);
155 
156  if (model.AccessUser == null ^ model.AccessToken == null)
157  return BadRequest(ErrorCode.RepoMismatchUserAndAccessToken);
158 
159  var currentModel = await DatabaseContext
161  .AsQueryable()
162  .Where(x => x.InstanceId == Instance.Id)
163  .FirstOrDefaultAsync(cancellationToken)
164  .ConfigureAwait(false);
165 
166  if (currentModel == default)
167  return StatusCode((int)HttpStatusCode.Gone);
168 
169  // normalize github urls
170  const string BadGitHubUrl = "://www.github.com/";
171  var uiOrigin = model.Origin.ToUpperInvariant();
172  var uiBad = BadGitHubUrl.ToUpperInvariant();
173  var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant();
174  if (uiOrigin.Contains(uiBad, StringComparison.Ordinal))
175  model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal);
176 
177  currentModel.AccessToken = model.AccessToken;
178  currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm
179  var cloneBranch = model.Reference;
180  var origin = model.Origin;
181 
182  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
183 
184  if (repoManager.CloneInProgress)
185  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
186 
187  if (repoManager.InUse)
188  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
189 
190  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
191 
192  // clone conflict
193  if (repo != null)
194  return Conflict(new ErrorMessage(ErrorCode.RepoExists));
195 
196  var job = new Models.Job
197  {
198  Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"),
199  StartedBy = AuthenticationContext.User,
200  CancelRightsType = RightsType.Repository,
201  CancelRight = (ulong)RepositoryRights.CancelClone,
203  };
204  var api = currentModel.ToApi();
205  await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
206  {
207  using var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false);
208  if (repos == null)
209  throw new JobException(ErrorCode.RepoExists);
210  var instance = new Models.Instance
211  {
212  Id = Instance.Id
213  };
214  await databaseContextFactory.UseContext(
215  async databaseContext =>
216  {
217  databaseContext.Instances.Attach(instance);
218  if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false))
219  await databaseContext.Save(ct).ConfigureAwait(false);
220  })
221  .ConfigureAwait(false);
222  }, cancellationToken).ConfigureAwait(false);
223 
224  api.Origin = model.Origin;
225  api.Reference = model.Reference;
226  api.ActiveJob = job.ToApi();
227 
228  return StatusCode((int)HttpStatusCode.Created, api);
229  }
230 
238  [HttpDelete]
239  [TgsAuthorize(RepositoryRights.Delete)]
240  [ProducesResponseType(typeof(Repository), 202)]
241  [ProducesResponseType(410)]
242  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
243  {
244  var currentModel = await DatabaseContext
246  .AsQueryable()
247  .Where(x => x.InstanceId == Instance.Id)
248  .FirstOrDefaultAsync(cancellationToken)
249  .ConfigureAwait(false);
250 
251  if (currentModel == default)
252  return StatusCode((int)HttpStatusCode.Gone);
253 
254  currentModel.AccessToken = null;
255  currentModel.AccessUser = null;
256 
257  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
258 
259  Logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id);
260 
261  var job = new Models.Job
262  {
263  Description = "Delete repository",
264  StartedBy = AuthenticationContext.User,
266  };
267  var api = currentModel.ToApi();
268  await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
269  api.ActiveJob = job.ToApi();
270  return Accepted(api);
271  }
272 
281  [HttpGet]
282  [TgsAuthorize(RepositoryRights.Read)]
283  [ProducesResponseType(typeof(Repository), 200)]
284  [ProducesResponseType(typeof(Repository), 201)]
285  [ProducesResponseType(410)]
286  public async Task<IActionResult> Read(CancellationToken cancellationToken)
287  {
288  var currentModel = await DatabaseContext
290  .AsQueryable()
291  .Where(x => x.InstanceId == Instance.Id)
292  .FirstOrDefaultAsync(cancellationToken)
293  .ConfigureAwait(false);
294 
295  if (currentModel == default)
296  return StatusCode((int)HttpStatusCode.Gone, new ErrorMessage(ErrorCode.RepoMissing));
297 
298  var api = currentModel.ToApi();
299  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
300 
301  if (repoManager.CloneInProgress)
302  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
303 
304  if (repoManager.InUse)
305  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
306 
307  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
308  if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false))
309  {
310  // user may have fucked with the repo manually, do what we can
311  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
312  return StatusCode((int)HttpStatusCode.Created, api);
313  }
314 
315  return Json(api);
316  }
317 
327  [HttpPost]
328  [TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)]
329  [ProducesResponseType(typeof(Repository), 200)]
330  [ProducesResponseType(typeof(Repository), 202)]
331  [ProducesResponseType(410)]
332  #pragma warning disable CA1502, CA1505 // TODO: Decomplexify
333  public async Task<IActionResult> Update([FromBody]Repository model, CancellationToken cancellationToken)
334  {
335  if (model == null)
336  throw new ArgumentNullException(nameof(model));
337 
338  if (model.AccessUser == null ^ model.AccessToken == null)
339  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchUserAndAccessToken));
340 
341  if (model.CheckoutSha != null && model.Reference != null)
342  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndReference));
343 
344  if (model.CheckoutSha != null && model.UpdateFromOrigin == true)
345  return BadRequest(new ErrorMessage(ErrorCode.RepoMismatchShaAndUpdate));
346 
347  if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true)
348  return BadRequest(new ErrorMessage(ErrorCode.RepoDuplicateTestMerge));
349 
350  if (model.CommitterName?.Length == 0)
351  return BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterName));
352 
353  if (model.CommitterEmail?.Length == 0)
354  return BadRequest(new ErrorMessage(ErrorCode.RepoWhitespaceCommitterEmail));
355 
356  var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0;
357  var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository);
358  if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest))
359  return Forbid();
360 
361  var currentModel = await DatabaseContext
363  .AsQueryable()
364  .Where(x => x.InstanceId == Instance.Id)
365  .FirstOrDefaultAsync(cancellationToken)
366  .ConfigureAwait(false);
367 
368  if (currentModel == default)
369  return StatusCode((int)HttpStatusCode.Gone);
370 
371  bool CheckModified<T>(Expression<Func<Api.Models.Internal.RepositorySettings, T>> expression, RepositoryRights requiredRight)
372  {
373  var memberSelectorExpression = (MemberExpression)expression.Body;
374  var property = (PropertyInfo)memberSelectorExpression.Member;
375 
376  var newVal = property.GetValue(model);
377  if (newVal == null)
378  return false;
379  if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal)
380  return true;
381 
382  property.SetValue(currentModel, newVal);
383  return false;
384  }
385 
386  if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials)
387  || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials)
388  || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings)
389  || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings)
390  || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter)
391  || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter)
392  || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits)
393  || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits)
394  || CheckModified(x => x.PostTestMergeComment, RepositoryRights.ChangeTestMergeCommits)
395  || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch)))
396  return Forbid();
397 
398  if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0)
399  {
400  // setting an empty string clears everything
401  currentModel.AccessUser = null;
402  currentModel.AccessToken = null;
403  }
404 
405  var canRead = userRights.HasFlag(RepositoryRights.Read);
406 
407  var api = canRead ? currentModel.ToApi() : new Repository();
408  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
409 
410  if (canRead)
411  {
412  if (repoManager.CloneInProgress)
413  return Conflict(new ErrorMessage(ErrorCode.RepoCloning));
414 
415  if (repoManager.InUse)
416  return Conflict(new ErrorMessage(ErrorCode.RepoBusy));
417 
418  using var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false);
419  if (repo == null)
420  return Conflict(new ErrorMessage(ErrorCode.RepoMissing));
421  await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false);
422 
423  if (model.Origin != null && model.Origin != repo.Origin)
424  return BadRequest(new ErrorMessage(ErrorCode.RepoCantChangeOrigin));
425  }
426 
427  // this is just db stuf so stow it away
428  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
429 
430  // format the job description
431  string description = null;
432  if (model.UpdateFromOrigin == true)
433  if (model.Reference != null)
434  description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference);
435  else if (model.CheckoutSha != null)
436  description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha);
437  else
438  description = "Pull current repository reference";
439  else if (model.Reference != null || model.CheckoutSha != null)
440  description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha);
441 
442  if (newTestMerges)
443  description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}",
444  description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T",
445  String.Join(", ", model.NewTestMerges.Select(x =>
446  String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number,
447  x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))),
448  description != null ? String.Empty : " in repository");
449 
450  if (description == null)
451  return Json(api); // no git changes
452 
453  var job = new Models.Job
454  {
455  Description = description,
456  StartedBy = AuthenticationContext.User,
457  Instance = Instance,
458  CancelRightsType = RightsType.Repository,
459  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
460  };
461 
462  // Time to access git, do it in a job
463  await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
464  {
465  using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false);
466  if (repo == null)
467  throw new JobException(ErrorCode.RepoMissing);
468 
469  var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null;
470 
471  var startReference = repo.Reference;
472  var startSha = repo.Head;
473  string postUpdateSha = null;
474 
475  if (newTestMerges && !repo.IsGitHubRepository)
476  throw new JobException(ErrorCode.RepoUnsupportedTestMergeRemote);
477 
478  var committerName = currentModel.ShowTestMergeCommitters.Value
480  : currentModel.CommitterName;
481 
482  var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null;
483 
484  var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
485  var doneSteps = 0;
486 
487  Action<int> NextProgressReporter()
488  {
489  var tmpDoneSteps = doneSteps;
490  ++doneSteps;
491  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps);
492  }
493 
494  progressReporter(0);
495 
496  // get a base line for where we are
497  Models.RevisionInformation lastRevisionInfo = null;
498 
499  var attachedInstance = new Models.Instance
500  {
501  Id = Instance.Id
502  };
503 
504  Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory
505  .UseContext(
506  async databaseContext =>
507  {
508  databaseContext.Instances.Attach(attachedInstance);
509  var previousRevInfo = lastRevisionInfo;
510  var needsUpdate = await LoadRevisionInformation(
511  repo,
512  databaseContext,
513  attachedInstance,
514  lastOriginCommitSha,
515  x => lastRevisionInfo = x,
516  ct)
517  .ConfigureAwait(false);
518 
519  if (testMergeToAdd != null)
520  {
521  // rev info may have already loaded the user
522  var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id);
523  if (mergedBy == default)
524  {
525  mergedBy = new Models.User
526  {
528  };
529 
530  databaseContext.Users.Attach(mergedBy);
531  }
532 
533  testMergeToAdd.MergedBy = mergedBy;
534 
535  lastRevisionInfo.ActiveTestMerges.AddRange(previousRevInfo.ActiveTestMerges);
536  lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
537  {
538  TestMerge = testMergeToAdd
539  });
540  lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
541 
542  needsUpdate = true;
543  }
544 
545  if (needsUpdate)
546  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
547  });
548 
549  await CallLoadRevInfo().ConfigureAwait(false);
550 
551  // apply new rev info, tracking applied test merges
552  Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha);
553 
554  try
555  {
556  // fetch/pull
557  if (model.UpdateFromOrigin == true)
558  {
559  if (!repo.Tracking)
560  throw new JobException(ErrorCode.RepoReferenceRequired);
561  await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false);
562  doneSteps = 1;
563  if (!modelHasShaOrReference)
564  {
565  var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false);
566  if (!fastForward.HasValue)
567  throw new JobException(ErrorCode.RepoMergeConflict);
568  await UpdateRevInfo().ConfigureAwait(false);
569  if (fastForward.Value)
570  {
571  lastRevisionInfo.OriginCommitSha = repo.Head;
572  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
573  postUpdateSha = repo.Head;
574  }
575  else
576  NextProgressReporter()(100);
577  }
578  }
579 
580  // checkout/hard reset
581  if (modelHasShaOrReference)
582  {
583  var validCheckoutSha =
584  model.CheckoutSha != null
585  && !repo.Head.StartsWith(model.CheckoutSha, StringComparison.OrdinalIgnoreCase);
586  var validCheckoutReference =
587  model.Reference != null
588  && !repo.Reference.Equals(model.Reference, StringComparison.OrdinalIgnoreCase);
589  if (validCheckoutSha || validCheckoutReference)
590  {
591  var committish = model.CheckoutSha ?? model.Reference;
592  var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false);
593 
594  if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
595  throw new JobException(ErrorCode.RepoSwappedShaOrReference);
596 
597  await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false);
598  await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin
599  }
600  else
601  NextProgressReporter()(100);
602 
603  if (hardResettingToOriginReference)
604  {
605  if (!repo.Tracking)
606  throw new JobException(ErrorCode.RepoReferenceNotTracking);
607  await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false);
608  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
609  await CallLoadRevInfo().ConfigureAwait(false);
610 
611  // repo head is on origin so force this
612  // will update the db if necessary
613  lastRevisionInfo.OriginCommitSha = repo.Head;
614  }
615  }
616 
617  // test merging
618  Dictionary<int, Octokit.PullRequest> prMap = null;
619  if (newTestMerges)
620  {
621  // bit of sanitization
622  foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision)))
623  I.PullRequestRevision = null;
624 
625  var gitHubClient = currentModel.AccessToken != null
626  ? gitHubClientFactory.CreateClient(currentModel.AccessToken)
627  : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken)
628  ? gitHubClientFactory.CreateClient()
629  : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken));
630 
631  var repoOwner = repo.GitHubOwner;
632  var repoName = repo.GitHubRepoName;
633 
634  // optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out
635  Models.RevisionInformation revInfoWereLookingFor = null;
636  bool needToApplyRemainingPrs = true;
637  if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
638  {
639  // In order for this to work though we need the shas of all the commits
640  if (model.NewTestMerges.Any(x => x.PullRequestRevision == null))
641  prMap = new Dictionary<int, Octokit.PullRequest>();
642 
643  bool cantSearch = false;
644  foreach (var I in model.NewTestMerges)
645  {
646  if (I.PullRequestRevision != null)
647 #pragma warning disable CA1308 // Normalize strings to uppercase
648  I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant(); // ala libgit2
649 #pragma warning restore CA1308 // Normalize strings to uppercase
650  else
651  try
652  {
653  // retrieve the latest sha
654  var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
655  prMap.Add(I.Number, pr);
656  I.PullRequestRevision = pr.Head.Sha;
657  }
658  catch
659  {
660  cantSearch = true;
661  break;
662  }
663  }
664 
665  if (!cantSearch)
666  {
667  List<Models.RevisionInformation> dbPull = null;
668 
669  await databaseContextFactory.UseContext(
670  async databaseContext =>
671  dbPull = await databaseContext.RevisionInformations
672  .AsQueryable()
673  .Where(x => x.Instance.Id == Instance.Id
674  && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
675  && x.ActiveTestMerges.Count <= model.NewTestMerges.Count
676  && x.ActiveTestMerges.Count > 0)
677  .Include(x => x.ActiveTestMerges)
678  .ThenInclude(x => x.TestMerge)
679  .ToListAsync(cancellationToken)
680  .ConfigureAwait(false))
681  .ConfigureAwait(false);
682 
683  // split here cause this bit has to be done locally
684  revInfoWereLookingFor = dbPull
685  .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count
686  && x.ActiveTestMerges.Select(y => y.TestMerge)
687  .All(y => model.NewTestMerges.Any(z =>
688  y.Number == z.Number
689  && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
690  && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
691  .FirstOrDefault();
692 
693  if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1)
694  {
695  // okay try to add at least SOME prs we've seen before
696  var search = model.NewTestMerges.ToList();
697 
698  var appliedTestMergeIds = new List<long>();
699 
700  Models.RevisionInformation lastGoodRevInfo = null;
701  do
702  {
703  foreach (var I in search)
704  {
705  revInfoWereLookingFor = dbPull
706  .Where(x => model.NewTestMerges.Any(z =>
707  x.PrimaryTestMerge.Number == z.Number
708  && x.PrimaryTestMerge.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
709  && (x.PrimaryTestMerge.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))
710  && x.ActiveTestMerges.Select(y => y.TestMerge).All(y => appliedTestMergeIds.Contains(y.Id)))
711  .FirstOrDefault();
712 
713  if (revInfoWereLookingFor != null)
714  {
715  lastGoodRevInfo = revInfoWereLookingFor;
716  appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id);
717  search.Remove(I);
718  break;
719  }
720  }
721  }
722  while (revInfoWereLookingFor != null && search.Count > 0);
723 
724  revInfoWereLookingFor = lastGoodRevInfo;
725  needToApplyRemainingPrs = search.Count != 0;
726  if (needToApplyRemainingPrs)
727  model.NewTestMerges = search;
728  }
729  else if (revInfoWereLookingFor != null)
730  needToApplyRemainingPrs = false;
731  }
732  }
733 
734  if (revInfoWereLookingFor != null)
735  {
736  // goteem
737  Logger.LogDebug("Reusing existing SHA {0}...", revInfoWereLookingFor.CommitSha);
738  await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false);
739  lastRevisionInfo = revInfoWereLookingFor;
740  }
741 
742  if (needToApplyRemainingPrs)
743  {
744  foreach (var I in model.NewTestMerges)
745  {
746  Octokit.PullRequest pr = null;
747  string errorMessage = null;
748 
749  if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number))
750  throw new JobException(ErrorCode.RepoDuplicateTestMerge);
751 
752  Exception exception = null;
753  try
754  {
755  // load from cache if possible
756  if (prMap == null || !prMap.TryGetValue(I.Number, out pr))
757  pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
758  }
759  catch (Octokit.RateLimitExceededException ex)
760  {
761  // you look at your anonymous access and sigh
762  errorMessage = "REMOTE API ERROR: RATE LIMITED";
763  exception = ex;
764  }
765  catch (Octokit.AuthorizationException ex)
766  {
767  errorMessage = "REMOTE API ERROR: BAD CREDENTIALS";
768  exception = ex;
769  }
770  catch (Octokit.NotFoundException ex)
771  {
772  // you look at your shithub and sigh
773  errorMessage = "REMOTE API ERROR: PULL REQUEST NOT FOUND";
774  exception = ex;
775  }
776 
777  if (exception != null)
778  Logger.LogWarning("Error retrieving pull request metadata: {0}", exception);
779 
780  // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it
781  if (I.PullRequestRevision == null && pr != null)
782  I.PullRequestRevision = pr.Head.Sha;
783 
784  var mergeResult = await repo.AddTestMerge(
785  I,
786  committerName,
787  currentModel.CommitterEmail,
788  currentModel.AccessUser,
789  currentModel.AccessToken,
790  NextProgressReporter(),
791  ct).ConfigureAwait(false);
792 
793  if (!mergeResult.HasValue)
794  throw new JobException(
795  ErrorCode.RepoTestMergeConflict,
796  new JobException(
797  $"Merge of PR #{I.Number} at {I.PullRequestRevision.Substring(0, 7)} conflicted!"));
798 
799  ++doneSteps;
800 
801  // MergedBy will be set later
802  var tm = new Models.TestMerge
803  {
804  Author = pr?.User.Login ?? errorMessage,
805  BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty,
806  MergedAt = DateTimeOffset.Now,
807  TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty,
808  Comment = I.Comment,
809  Number = I.Number,
810  PullRequestRevision = I.PullRequestRevision,
811  Url = pr?.HtmlUrl ?? errorMessage
812  };
813 
814  await UpdateRevInfo(tm).ConfigureAwait(false);
815  }
816  }
817  }
818 
819  var currentHead = repo.Head;
820  if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))
821  {
822  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false);
823  await UpdateRevInfo().ConfigureAwait(false);
824  }
825  }
826  catch
827  {
828  doneSteps = 0;
829  numSteps = 2;
830 
831  // Forget what we've done and abort
832  await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false);
833  if (startReference != null && repo.Head != startSha)
834  await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false);
835  else
836  progressReporter(100);
837  throw;
838  }
839  }, cancellationToken).ConfigureAwait(false);
840 
841  api.ActiveJob = job.ToApi();
842  return Accepted(api);
843  }
844  #pragma warning restore CA1502, CA1505
845  }
846 }
string Name
The name of the User
Definition: User.cs:41
IDatabaseCollection< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
+
long Id
The ID of the entity.
Definition: EntityId.cs:11
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
@@ -132,9 +133,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents a git repository
Definition: Repository.cs:8
const string Repository
The Models.Repository controller
Definition: Routes.cs:39
RevisionInformation RevisionInformation
The current Models.RevisionInformation for the Repository
Definition: Repository.cs:23
- +
RepositoryRights
Rights for a Models.Repository
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for the RepositoryController
string Reference
The branch or tag HEAD points to
Definition: Repository.cs:48
IEnumerable< TModel > Local
An IEnumerable<T> of TModel s prioritizing in the working set.
diff --git a/_service_collection_extensions_8cs_source.html b/_service_collection_extensions_8cs_source.html index baa2106275..74efb30fd6 100644 --- a/_service_collection_extensions_8cs_source.html +++ b/_service_collection_extensions_8cs_source.html @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServiceCollectionExtensions.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Configuration;
2 using Microsoft.Extensions.DependencyInjection;
3 using Microsoft.Extensions.Logging;
4 using Serilog;
5 using Serilog.Configuration;
6 using System;
7 using System.Diagnostics;
8 using System.Globalization;
10 
11 namespace Tgstation.Server.Host.Extensions
12 {
17  {
25  public static IServiceCollection UseStandardConfig<TConfig>(this IServiceCollection serviceCollection, IConfiguration configuration) where TConfig : class
26  {
27  if (serviceCollection == null)
28  throw new ArgumentNullException(nameof(serviceCollection));
29  if (configuration == null)
30  throw new ArgumentNullException(nameof(configuration));
31 
32  const string SectionFieldName = nameof(GeneralConfiguration.Section);
33 
34  var configType = typeof(TConfig);
35  var sectionField = configType.GetField(SectionFieldName);
36  if (sectionField == null)
37  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has no {1} field!", configType, SectionFieldName));
38 
39  var stringType = typeof(string);
40  if (sectionField.FieldType != stringType)
41  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has invalid {1} field type, must be {2}!", configType, SectionFieldName, stringType));
42 
43  var sectionName = (string)sectionField.GetValue(null);
44 
45  return serviceCollection.Configure<TConfig>(configuration.GetSection(sectionName));
46  }
47 
55  public static IServiceCollection SetupLogging(
56  this IServiceCollection serviceCollection,
57  Action<LoggerConfiguration> configurationAction,
58  Action<LoggerSinkConfiguration> sinkConfigurationAction = null)
59  => serviceCollection.AddLogging(builder =>
60  {
61  builder.ClearProviders();
62 
63  var configuration = new LoggerConfiguration()
64  .MinimumLevel
65  .Verbose();
66 
67  configurationAction?.Invoke(configuration);
68 
69  configuration
70  .WriteTo
71  .Async(sinkConfiguration =>
72  {
73  sinkConfiguration.Console(
74  outputTemplate: "[{Timestamp:HH:mm:ss}] {Level:w3}: {SourceContext:l}{NewLine} {Message:lj}{NewLine}{Exception}");
75  sinkConfigurationAction?.Invoke(sinkConfiguration);
76  });
77 
78  builder.AddSerilog(configuration.CreateLogger(), true);
79 
80  if (Debugger.IsAttached)
81  builder.AddDebug();
82  });
83  }
84 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Configuration;
2 using Microsoft.Extensions.DependencyInjection;
3 using Microsoft.Extensions.Logging;
4 using Serilog;
5 using Serilog.Configuration;
6 using System;
7 using System.Diagnostics;
8 using System.Globalization;
10 
11 namespace Tgstation.Server.Host.Extensions
12 {
17  {
21  public const string SerilogContextTemplate = "(Instance:{Instance}|Job:{Job}|Request:{Request}|User:{User}|Monitor:{Monitor}|Bridge:{Bridge}|Chat:{ChatMessage})";
22 
30  public static IServiceCollection UseStandardConfig<TConfig>(this IServiceCollection serviceCollection, IConfiguration configuration) where TConfig : class
31  {
32  if (serviceCollection == null)
33  throw new ArgumentNullException(nameof(serviceCollection));
34  if (configuration == null)
35  throw new ArgumentNullException(nameof(configuration));
36 
37  const string SectionFieldName = nameof(GeneralConfiguration.Section);
38 
39  var configType = typeof(TConfig);
40  var sectionField = configType.GetField(SectionFieldName);
41  if (sectionField == null)
42  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has no {1} field!", configType, SectionFieldName));
43 
44  var stringType = typeof(string);
45  if (sectionField.FieldType != stringType)
46  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has invalid {1} field type, must be {2}!", configType, SectionFieldName, stringType));
47 
48  var sectionName = (string)sectionField.GetValue(null);
49 
50  return serviceCollection.Configure<TConfig>(configuration.GetSection(sectionName));
51  }
52 
60  public static IServiceCollection SetupLogging(
61  this IServiceCollection serviceCollection,
62  Action<LoggerConfiguration> configurationAction,
63  Action<LoggerSinkConfiguration> sinkConfigurationAction = null)
64  => serviceCollection.AddLogging(builder =>
65  {
66  builder.ClearProviders();
67 
68  var configuration = new LoggerConfiguration()
69  .MinimumLevel
70  .Verbose();
71 
72  configurationAction?.Invoke(configuration);
73 
74  configuration
75  .Enrich.FromLogContext()
76  .WriteTo
77  .Async(sinkConfiguration =>
78  {
79  sinkConfiguration.Console(
80  outputTemplate: "[{Timestamp:HH:mm:ss}] {Level:w3}: {SourceContext:l} "
81  + SerilogContextTemplate
82  + "{NewLine} {Message:lj}{NewLine}{Exception}");
83  sinkConfigurationAction?.Invoke(sinkConfiguration);
84  });
85 
86  builder.AddSerilog(configuration.CreateLogger(), true);
87 
88  if (Debugger.IsAttached)
89  builder.AddDebug();
90  });
91  }
92 }
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
diff --git a/_session_controller_8cs_source.html b/_session_controller_8cs_source.html index 1e8451d0d6..c0f8edb600 100644 --- a/_session_controller_8cs_source.html +++ b/_session_controller_8cs_source.html @@ -94,96 +94,98 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SessionController.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
20 
21 namespace Tgstation.Server.Host.Components.Session
22 {
25  {
27  public DMApiParameters DMApiParameters => reattachInformation;
28 
30  public bool IsPrimary
31  {
32  get
33  {
34  CheckDisposed();
35  return reattachInformation.IsPrimary;
36  }
37  }
38 
41  {
42  get
43  {
44  if (!Lifetime.IsCompleted)
45  throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
46  return apiValidationStatus;
47  }
48  }
49 
51  public IDmbProvider Dmb
52  {
53  get
54  {
55  CheckDisposed();
56  return reattachInformation.Dmb;
57  }
58  }
59 
61  public ushort? Port
62  {
63  get
64  {
65  CheckDisposed();
66  if (portClosedForReboot)
67  return null;
68  return reattachInformation.Port;
69  }
70  }
71 
74  {
75  get
76  {
77  CheckDisposed();
78  return reattachInformation.RebootState;
79  }
80  }
81 
83  public Version DMApiVersion { get; private set; }
84 
86  public bool ClosePortOnReboot { get; set; }
87 
89  public bool TerminationWasRequested { get; private set; }
90 
92  public Task<LaunchResult> LaunchResult { get; }
93 
95  public Task<int> Lifetime => process.Lifetime;
96 
98  public Task OnReboot => rebootTcs.Task;
99 
101  public Task OnPrime => primeTcs.Task;
102 
107 
111  readonly CancellationTokenSource reattachTopicCts;
112 
116  readonly ITopicClient byondTopicSender;
117 
122 
126  readonly IProcess process;
127 
132 
137 
141  readonly IChatManager chat;
142 
146  readonly ILogger<SessionController> logger;
147 
151  readonly object synchronizationLock;
152 
156  TaskCompletionSource<bool> portAssignmentTcs;
157 
161  ushort? nextPort;
162 
166  TaskCompletionSource<object> rebootTcs;
167 
171  TaskCompletionSource<object> primeTcs;
172 
177 
181  bool disposed;
182 
187 
191  bool released;
192 
208  ReattachInformation reattachInformation,
209  IProcess process,
210  IByondExecutableLock byondLock,
211  ITopicClient byondTopicSender,
212  IChatTrackingContext chatTrackingContext,
213  IBridgeRegistrar bridgeRegistrar,
214  IChatManager chat,
215  IAssemblyInformationProvider assemblyInformationProvider,
216  ILogger<SessionController> logger,
217  uint? startupTimeout,
218  bool reattached)
219  {
220  this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
221  this.process = process ?? throw new ArgumentNullException(nameof(process));
222  this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
223  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
224  this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext));
225  bridgeRegistration = bridgeRegistrar?.RegisterHandler(this) ?? throw new ArgumentNullException(nameof(bridgeRegistrar));
226  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
227  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
228 
229  this.chatTrackingContext.SetChannelSink(this);
230 
231  portClosedForReboot = false;
232  disposed = false;
233  apiValidationStatus = ApiValidationStatus.NeverValidated;
234  released = false;
235 
236  rebootTcs = new TaskCompletionSource<object>();
237  primeTcs = new TaskCompletionSource<object>();
238  reattachTopicCts = new CancellationTokenSource();
239  synchronizationLock = new object();
240 
241  _ = process.Lifetime.ContinueWith(
242  x =>
243  {
244  lock (synchronizationLock)
245  if (!disposed)
246  reattachTopicCts.Cancel();
247  chatTrackingContext.Active = false;
248  },
249  TaskScheduler.Current);
250 
251  LaunchResult = GetLaunchResult(
252  assemblyInformationProvider,
253  startupTimeout,
254  reattached);
255 
256  logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port);
257  }
258 
263 #pragma warning disable CA1821 // Remove empty Finalizers TODO: remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
264  ~SessionController() => Dispose(false);
265 #pragma warning restore CA1821 // Remove empty Finalizers
266 
268  public void Dispose()
269  {
270  Dispose(true);
271  GC.SuppressFinalize(this);
272  }
273 
278  void Dispose(bool disposing)
279  {
280  lock (synchronizationLock)
281  {
282  if (disposed)
283  return;
284  disposed = true;
285  logger.LogTrace("Disposing...");
286  if (disposing)
287  {
288  if (!released)
289  {
290  process.Terminate();
291  byondLock.Dispose();
292  }
293 
294  process.Dispose();
295  bridgeRegistration.Dispose();
296  reattachInformation.Dmb?.Dispose(); // will be null when released
297  chatTrackingContext.Dispose();
298  reattachTopicCts.Dispose();
299  }
300  else
301  {
302  if (logger != null)
303  logger.LogError("Being disposed via finalizer!");
304  if (!released)
305  if (process != null)
306  process.Terminate();
307  else if (logger != null)
308  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
309  }
310  }
311  }
312 
320  async Task<LaunchResult> GetLaunchResult(
321  IAssemblyInformationProvider assemblyInformationProvider,
322  uint? startupTimeout,
323  bool reattached)
324  {
325  var startTime = DateTimeOffset.Now;
326  Task toAwait = process.Startup;
327 
328  if (startupTimeout.HasValue)
329  toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
330 
331  await toAwait.ConfigureAwait(false);
332 
333  var result = new LaunchResult
334  {
335  ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
336  StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
337  };
338 
339  logger.LogTrace("Launch result: {0}", result);
340 
341  if (!result.ExitCode.HasValue && reattached && !disposed)
342  {
343  var reattachResponse = await SendCommand(
344  new TopicParameters(
345  assemblyInformationProvider.Version,
346  reattachInformation.RuntimeInformation.ServerPort),
347  reattachTopicCts.Token)
348  .ConfigureAwait(false);
349 
350  if (reattachResponse.InteropResponse?.CustomCommands != null)
351  chatTrackingContext.CustomCommands = reattachResponse.InteropResponse.CustomCommands;
352  else if (reattachResponse.InteropResponse != null)
353  logger.LogWarning(
354  "DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
355  Dmb.CompileJob.DMApiVersion.Semver());
356  }
357 
358  return result;
359  }
360 
362  public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
363  {
364  if (parameters == null)
365  throw new ArgumentNullException(nameof(parameters));
366 
367  var response = new BridgeResponse();
368  switch (parameters.CommandType)
369  {
370  case BridgeCommandType.ChatSend:
371  if (parameters.ChatMessage == null)
372  return new BridgeResponse
373  {
374  ErrorMessage = "Missing chatMessage field!"
375  };
376 
377  if (parameters.ChatMessage.ChannelIds == null)
378  return new BridgeResponse
379  {
380  ErrorMessage = "Missing channelIds field in chatMessage!"
381  };
382 
383  if(parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _)))
384  return new BridgeResponse
385  {
386  ErrorMessage = "Invalid channelIds in chatMessage!"
387  };
388 
389  if (parameters.ChatMessage.Text == null)
390  return new BridgeResponse
391  {
392  ErrorMessage = "Missing message field in chatMessage!"
393  };
394 
395  await chat.SendMessage(
396  parameters.ChatMessage.Text,
397  parameters.ChatMessage.ChannelIds.Select(UInt64.Parse),
398  cancellationToken).ConfigureAwait(false);
399  break;
400  case BridgeCommandType.Prime:
401  var oldPrimeTcs = primeTcs;
402  primeTcs = new TaskCompletionSource<object>();
403  oldPrimeTcs.SetResult(null);
404  break;
405  case BridgeCommandType.Kill:
406  logger.LogInformation("Bridge requested process termination!");
407  TerminationWasRequested = true;
408  process.Terminate();
409  break;
410  case BridgeCommandType.PortUpdate:
411  lock (synchronizationLock)
412  {
413  if (!parameters.CurrentPort.HasValue)
414  {
416  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
417  return new BridgeResponse
418  {
419  ErrorMessage = "Missing stringified port as data parameter!"
420  };
421  }
422 
423  var currentPort = parameters.CurrentPort.Value;
424  if (!nextPort.HasValue)
425  reattachInformation.Port = parameters.CurrentPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
426  else
427  {
428  // nextPort is ready, tell DD to switch to that
429  // if it fails it'll kill itself
430  response.NewPort = nextPort.Value;
431  reattachInformation.Port = nextPort.Value;
432  nextPort = null;
433 
434  // we'll also get here from SetPort so complete that task
435  var tmpTcs = portAssignmentTcs;
436  portAssignmentTcs = null;
437  tmpTcs.SetResult(true);
438  }
439 
440  portClosedForReboot = false;
441  }
442 
443  break;
444  case BridgeCommandType.Startup:
445  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
446  if (parameters.Version == null)
447  return new BridgeResponse
448  {
449  ErrorMessage = "Missing dmApiVersion field!"
450  };
451 
452  DMApiVersion = parameters.Version;
453  switch (parameters.MinimumSecurityLevel)
454  {
455  case DreamDaemonSecurity.Ultrasafe:
456  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
457  break;
458  case DreamDaemonSecurity.Safe:
459  apiValidationStatus = ApiValidationStatus.RequiresSafe;
460  break;
461  case DreamDaemonSecurity.Trusted:
462  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
463  break;
464  case null:
465  return new BridgeResponse
466  {
467  ErrorMessage = "Missing minimumSecurityLevel field!"
468  };
469  default:
470  return new BridgeResponse
471  {
472  ErrorMessage = "Invalid minimumSecurityLevel!"
473  };
474  }
475 
476  response.RuntimeInformation = reattachInformation.RuntimeInformation;
477 
478  // Load custom commands
479  chatTrackingContext.CustomCommands = parameters.CustomCommands;
480  break;
481  case BridgeCommandType.Reboot:
482  if (ClosePortOnReboot)
483  {
484  chatTrackingContext.Active = false;
485  response.NewPort = 0;
486  portClosedForReboot = true;
487  }
488 
489  var oldRebootTcs = rebootTcs;
490  rebootTcs = new TaskCompletionSource<object>();
491  oldRebootTcs.SetResult(null);
492  break;
493  case null:
494  response.ErrorMessage = "Missing commandType!";
495  break;
496  default:
497  response.ErrorMessage = "Requested commandType not supported!";
498  break;
499  }
500 
501  return response;
502  }
503 
508  {
509  if (disposed)
510  throw new ObjectDisposedException(nameof(SessionController));
511  }
512 
514  public void EnableCustomChatCommands() => chatTrackingContext.Active = true;
515 
518  {
519  CheckDisposed();
520 
521  // we still don't want to dispose the dmb yet, even though we're keeping it alive
522  var tmpProvider = reattachInformation.Dmb;
523  reattachInformation.Dmb = null;
524  released = true;
525  Dispose();
526  byondLock.DoNotDeleteThisSession();
527  tmpProvider.KeepAlive();
528  reattachInformation.Dmb = tmpProvider;
529  return reattachInformation;
530  }
531 
533  public async Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
534  {
535  if (Lifetime.IsCompleted)
536  {
537  logger.LogWarning(
538  "Attempted to send a command to an inactive SessionController: {0}",
539  parameters.CommandType);
540  return null;
541  }
542 
543  parameters.AccessIdentifier = reattachInformation.AccessIdentifier;
544 
545  var json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings);
546  logger.LogTrace("Topic request: {0}", json);
547  try
548  {
549  var commandString = String.Format(CultureInfo.InvariantCulture,
550  "?{0}={1}",
551  byondTopicSender.SanitizeString(DMApiConstants.TopicData),
552  byondTopicSender.SanitizeString(json));
553 
554  var targetPort = reattachInformation.Port;
555 
556  var topicResponse = await byondTopicSender.SendTopic(
557  new IPEndPoint(IPAddress.Loopback, targetPort),
558  commandString,
559  cancellationToken).ConfigureAwait(false);
560 
561  var topicReturn = topicResponse.StringData;
562 
563  Interop.Topic.TopicResponse interopResponse = null;
564  if (topicReturn != null)
565  try
566  {
567  interopResponse = JsonConvert.DeserializeObject<Interop.Topic.TopicResponse>(topicReturn, DMApiConstants.SerializerSettings);
568  if (interopResponse.ErrorMessage != null)
569  {
570  logger.LogWarning("Errored topic response for command {0}: {1}", parameters.CommandType, interopResponse.ErrorMessage);
571  }
572 
573  logger.LogTrace("Interop response: {0}", topicReturn);
574  }
575  catch
576  {
577  logger.LogWarning("Invalid interop response: {0}", topicReturn);
578  }
579 
580  return new CombinedTopicResponse(topicResponse, interopResponse);
581  }
582  catch (OperationCanceledException)
583  {
584  logger.LogTrace(
585  "Topic request {0}!",
586  cancellationToken.IsCancellationRequested
587  ? "aborted"
588  : "timed out");
589  cancellationToken.ThrowIfCancellationRequested();
590  }
591  catch (Exception e)
592  {
593  logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, e);
594  }
595 
596  return null;
597  }
598 
600  public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
601  {
602  CheckDisposed();
603 
604  if (port == 0)
605  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
606 
607  async Task<bool> ImmediateTopicPortChange()
608  {
609  var commandResult = await SendCommand(
610  new TopicParameters(port),
611  cancellationToken)
612  .ConfigureAwait(false);
613 
614  if (commandResult.InteropResponse?.ErrorMessage != null)
615  return false;
616 
617  reattachInformation.Port = port;
618  return true;
619  }
620 
621  lock (synchronizationLock)
622  if (portClosedForReboot)
623  {
624  if (portAssignmentTcs != null)
625  throw new InvalidOperationException("A port change operation is already in progress!");
626  nextPort = port;
627  portAssignmentTcs = new TaskCompletionSource<bool>();
628  return portAssignmentTcs.Task;
629  }
630  else
631  return ImmediateTopicPortChange();
632  }
633 
635  public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
636  {
637  if (RebootState == newRebootState)
638  return true;
639 
640  logger.LogTrace("Changing reboot state to {0}", newRebootState);
641 
642  reattachInformation.RebootState = newRebootState;
643  var result = await SendCommand(
644  new TopicParameters(newRebootState),
645  cancellationToken)
646  .ConfigureAwait(false);
647 
648  return result?.InteropResponse != null && result.InteropResponse?.ErrorMessage == null;
649  }
650 
652  public void ResetRebootState()
653  {
654  CheckDisposed();
655  logger.LogTrace("Resetting reboot state...");
656  reattachInformation.RebootState = RebootState.Normal;
657  }
658 
660  public void SetHighPriority() => process.SetHighPriority();
661 
663  public void Suspend() => process.Suspend();
664 
666  public void Resume() => process.Resume();
667 
669  public void ReplaceDmbProvider(IDmbProvider dmbProvider)
670  {
671  var oldDmb = reattachInformation.Dmb;
672  reattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
673  oldDmb.Dispose();
674  }
675 
677  public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
678  => SendCommand(new TopicParameters(newInstanceName), cancellationToken);
679 
681  public Task UpdateChannels(IEnumerable<ChannelRepresentation> newChannels, CancellationToken cancellationToken)
682  => SendCommand(
683  new TopicParameters(
684  new ChatUpdate(newChannels)),
685  cancellationToken);
686  }
687 }
readonly CancellationTokenSource reattachTopicCts
A CancellationTokenSource used for the topic send operation made on reattaching.
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Serilog.Context;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Net;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Tgstation.Server.Api;
21 
22 namespace Tgstation.Server.Host.Components.Session
23 {
26  {
28  public DMApiParameters DMApiParameters => reattachInformation;
29 
31  public bool IsPrimary
32  {
33  get
34  {
35  CheckDisposed();
36  return reattachInformation.IsPrimary;
37  }
38  }
39 
42  {
43  get
44  {
45  if (!Lifetime.IsCompleted)
46  throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
47  return apiValidationStatus;
48  }
49  }
50 
52  public IDmbProvider Dmb
53  {
54  get
55  {
56  CheckDisposed();
57  return reattachInformation.Dmb;
58  }
59  }
60 
62  public ushort? Port
63  {
64  get
65  {
66  CheckDisposed();
67  if (portClosedForReboot)
68  return null;
69  return reattachInformation.Port;
70  }
71  }
72 
75  {
76  get
77  {
78  CheckDisposed();
79  return reattachInformation.RebootState;
80  }
81  }
82 
84  public Version DMApiVersion { get; private set; }
85 
87  public bool ClosePortOnReboot { get; set; }
88 
90  public bool TerminationWasRequested { get; private set; }
91 
93  public Task<LaunchResult> LaunchResult { get; }
94 
96  public Task<int> Lifetime => process.Lifetime;
97 
99  public Task OnReboot => rebootTcs.Task;
100 
102  public Task OnPrime => primeTcs.Task;
103 
108 
112  readonly Api.Models.Instance metadata;
113 
117  readonly CancellationTokenSource reattachTopicCts;
118 
122  readonly ITopicClient byondTopicSender;
123 
128 
132  readonly IProcess process;
133 
138 
143 
147  readonly IChatManager chat;
148 
152  readonly ILogger<SessionController> logger;
153 
157  readonly object synchronizationLock;
158 
162  TaskCompletionSource<bool> portAssignmentTcs;
163 
167  ushort? nextPort;
168 
172  TaskCompletionSource<object> rebootTcs;
173 
177  TaskCompletionSource<object> primeTcs;
178 
183 
187  bool disposed;
188 
193 
197  bool released;
198 
215  ReattachInformation reattachInformation,
216  Api.Models.Instance metadata,
217  IProcess process,
218  IByondExecutableLock byondLock,
219  ITopicClient byondTopicSender,
220  IChatTrackingContext chatTrackingContext,
221  IBridgeRegistrar bridgeRegistrar,
222  IChatManager chat,
223  IAssemblyInformationProvider assemblyInformationProvider,
224  ILogger<SessionController> logger,
225  uint? startupTimeout,
226  bool reattached)
227  {
228  this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
229  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
230  this.process = process ?? throw new ArgumentNullException(nameof(process));
231  this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
232  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
233  this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext));
234  bridgeRegistration = bridgeRegistrar?.RegisterHandler(this) ?? throw new ArgumentNullException(nameof(bridgeRegistrar));
235  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
236  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
237 
238  this.chatTrackingContext.SetChannelSink(this);
239 
240  portClosedForReboot = false;
241  disposed = false;
242  apiValidationStatus = ApiValidationStatus.NeverValidated;
243  released = false;
244 
245  rebootTcs = new TaskCompletionSource<object>();
246  primeTcs = new TaskCompletionSource<object>();
247  reattachTopicCts = new CancellationTokenSource();
248  synchronizationLock = new object();
249 
250  _ = process.Lifetime.ContinueWith(
251  x =>
252  {
253  lock (synchronizationLock)
254  if (!disposed)
255  reattachTopicCts.Cancel();
256  chatTrackingContext.Active = false;
257  },
258  TaskScheduler.Current);
259 
260  LaunchResult = GetLaunchResult(
261  assemblyInformationProvider,
262  startupTimeout,
263  reattached);
264 
265  logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port);
266  }
267 
272 #pragma warning disable CA1821 // Remove empty Finalizers TODO: remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
273  ~SessionController() => Dispose(false);
274 #pragma warning restore CA1821 // Remove empty Finalizers
275 
277  public void Dispose()
278  {
279  Dispose(true);
280  GC.SuppressFinalize(this);
281  }
282 
287  void Dispose(bool disposing)
288  {
289  lock (synchronizationLock)
290  {
291  if (disposed)
292  return;
293  disposed = true;
294  logger.LogTrace("Disposing...");
295  if (disposing)
296  {
297  if (!released)
298  {
299  process.Terminate();
300  byondLock.Dispose();
301  }
302 
303  process.Dispose();
304  bridgeRegistration.Dispose();
305  reattachInformation.Dmb?.Dispose(); // will be null when released
306  chatTrackingContext.Dispose();
307  reattachTopicCts.Dispose();
308  }
309  else
310  {
311  if (logger != null)
312  logger.LogError("Being disposed via finalizer!");
313  if (!released)
314  if (process != null)
315  process.Terminate();
316  else if (logger != null)
317  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
318  }
319  }
320  }
321 
329  async Task<LaunchResult> GetLaunchResult(
330  IAssemblyInformationProvider assemblyInformationProvider,
331  uint? startupTimeout,
332  bool reattached)
333  {
334  var startTime = DateTimeOffset.Now;
335  Task toAwait = process.Startup;
336 
337  if (startupTimeout.HasValue)
338  toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
339 
340  await toAwait.ConfigureAwait(false);
341 
342  var result = new LaunchResult
343  {
344  ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
345  StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
346  };
347 
348  logger.LogTrace("Launch result: {0}", result);
349 
350  if (!result.ExitCode.HasValue && reattached && !disposed)
351  {
352  var reattachResponse = await SendCommand(
353  new TopicParameters(
354  assemblyInformationProvider.Version,
355  reattachInformation.RuntimeInformation.ServerPort),
356  reattachTopicCts.Token)
357  .ConfigureAwait(false);
358 
359  if (reattachResponse.InteropResponse?.CustomCommands != null)
360  chatTrackingContext.CustomCommands = reattachResponse.InteropResponse.CustomCommands;
361  else if (reattachResponse.InteropResponse != null)
362  logger.LogWarning(
363  "DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
364  Dmb.CompileJob.DMApiVersion.Semver());
365  }
366 
367  return result;
368  }
369 
371  public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
372  {
373  if (parameters == null)
374  throw new ArgumentNullException(nameof(parameters));
375 
376  using (LogContext.PushProperty("Instance", metadata.Id))
377  {
378  logger.LogTrace("Handling bridge request...");
379  var response = new BridgeResponse();
380  switch (parameters.CommandType)
381  {
382  case BridgeCommandType.ChatSend:
383  if (parameters.ChatMessage == null)
384  return new BridgeResponse
385  {
386  ErrorMessage = "Missing chatMessage field!"
387  };
388 
389  if (parameters.ChatMessage.ChannelIds == null)
390  return new BridgeResponse
391  {
392  ErrorMessage = "Missing channelIds field in chatMessage!"
393  };
394 
395  if (parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _)))
396  return new BridgeResponse
397  {
398  ErrorMessage = "Invalid channelIds in chatMessage!"
399  };
400 
401  if (parameters.ChatMessage.Text == null)
402  return new BridgeResponse
403  {
404  ErrorMessage = "Missing message field in chatMessage!"
405  };
406 
407  await chat.SendMessage(
408  parameters.ChatMessage.Text,
409  parameters.ChatMessage.ChannelIds.Select(UInt64.Parse),
410  cancellationToken).ConfigureAwait(false);
411  break;
412  case BridgeCommandType.Prime:
413  var oldPrimeTcs = primeTcs;
414  primeTcs = new TaskCompletionSource<object>();
415  oldPrimeTcs.SetResult(null);
416  break;
417  case BridgeCommandType.Kill:
418  logger.LogInformation("Bridge requested process termination!");
419  TerminationWasRequested = true;
420  process.Terminate();
421  break;
422  case BridgeCommandType.PortUpdate:
423  lock (synchronizationLock)
424  {
425  if (!parameters.CurrentPort.HasValue)
426  {
428  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
429  return new BridgeResponse
430  {
431  ErrorMessage = "Missing stringified port as data parameter!"
432  };
433  }
434 
435  var currentPort = parameters.CurrentPort.Value;
436  if (!nextPort.HasValue)
437  reattachInformation.Port = parameters.CurrentPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
438  else
439  {
440  // nextPort is ready, tell DD to switch to that
441  // if it fails it'll kill itself
442  response.NewPort = nextPort.Value;
443  reattachInformation.Port = nextPort.Value;
444  nextPort = null;
445 
446  // we'll also get here from SetPort so complete that task
447  var tmpTcs = portAssignmentTcs;
448  portAssignmentTcs = null;
449  tmpTcs.SetResult(true);
450  }
451 
452  portClosedForReboot = false;
453  }
454 
455  break;
456  case BridgeCommandType.Startup:
457  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
458  if (parameters.Version == null)
459  return new BridgeResponse
460  {
461  ErrorMessage = "Missing dmApiVersion field!"
462  };
463 
464  DMApiVersion = parameters.Version;
465  switch (parameters.MinimumSecurityLevel)
466  {
467  case DreamDaemonSecurity.Ultrasafe:
468  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
469  break;
470  case DreamDaemonSecurity.Safe:
471  apiValidationStatus = ApiValidationStatus.RequiresSafe;
472  break;
473  case DreamDaemonSecurity.Trusted:
474  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
475  break;
476  case null:
477  return new BridgeResponse
478  {
479  ErrorMessage = "Missing minimumSecurityLevel field!"
480  };
481  default:
482  return new BridgeResponse
483  {
484  ErrorMessage = "Invalid minimumSecurityLevel!"
485  };
486  }
487 
488  response.RuntimeInformation = reattachInformation.RuntimeInformation;
489 
490  // Load custom commands
491  chatTrackingContext.CustomCommands = parameters.CustomCommands;
492  break;
493  case BridgeCommandType.Reboot:
494  if (ClosePortOnReboot)
495  {
496  chatTrackingContext.Active = false;
497  response.NewPort = 0;
498  portClosedForReboot = true;
499  }
500 
501  var oldRebootTcs = rebootTcs;
502  rebootTcs = new TaskCompletionSource<object>();
503  oldRebootTcs.SetResult(null);
504  break;
505  case null:
506  response.ErrorMessage = "Missing commandType!";
507  break;
508  default:
509  response.ErrorMessage = "Requested commandType not supported!";
510  break;
511  }
512 
513  return response;
514  }
515  }
516 
521  {
522  if (disposed)
523  throw new ObjectDisposedException(nameof(SessionController));
524  }
525 
527  public void EnableCustomChatCommands() => chatTrackingContext.Active = true;
528 
531  {
532  CheckDisposed();
533 
534  // we still don't want to dispose the dmb yet, even though we're keeping it alive
535  var tmpProvider = reattachInformation.Dmb;
536  reattachInformation.Dmb = null;
537  released = true;
538  Dispose();
539  byondLock.DoNotDeleteThisSession();
540  tmpProvider.KeepAlive();
541  reattachInformation.Dmb = tmpProvider;
542  return reattachInformation;
543  }
544 
546  public async Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
547  {
548  if (Lifetime.IsCompleted)
549  {
550  logger.LogWarning(
551  "Attempted to send a command to an inactive SessionController: {0}",
552  parameters.CommandType);
553  return null;
554  }
555 
556  parameters.AccessIdentifier = reattachInformation.AccessIdentifier;
557 
558  var json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings);
559  logger.LogTrace("Topic request: {0}", json);
560  try
561  {
562  var commandString = String.Format(CultureInfo.InvariantCulture,
563  "?{0}={1}",
564  byondTopicSender.SanitizeString(DMApiConstants.TopicData),
565  byondTopicSender.SanitizeString(json));
566 
567  var targetPort = reattachInformation.Port;
568 
569  var topicResponse = await byondTopicSender.SendTopic(
570  new IPEndPoint(IPAddress.Loopback, targetPort),
571  commandString,
572  cancellationToken).ConfigureAwait(false);
573 
574  var topicReturn = topicResponse.StringData;
575 
576  Interop.Topic.TopicResponse interopResponse = null;
577  if (topicReturn != null)
578  try
579  {
580  interopResponse = JsonConvert.DeserializeObject<Interop.Topic.TopicResponse>(topicReturn, DMApiConstants.SerializerSettings);
581  if (interopResponse.ErrorMessage != null)
582  {
583  logger.LogWarning("Errored topic response for command {0}: {1}", parameters.CommandType, interopResponse.ErrorMessage);
584  }
585 
586  logger.LogTrace("Interop response: {0}", topicReturn);
587  }
588  catch
589  {
590  logger.LogWarning("Invalid interop response: {0}", topicReturn);
591  }
592 
593  return new CombinedTopicResponse(topicResponse, interopResponse);
594  }
595  catch (OperationCanceledException)
596  {
597  logger.LogTrace(
598  "Topic request {0}!",
599  cancellationToken.IsCancellationRequested
600  ? "aborted"
601  : "timed out");
602  cancellationToken.ThrowIfCancellationRequested();
603  }
604  catch (Exception e)
605  {
606  logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, e);
607  }
608 
609  return null;
610  }
611 
613  public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
614  {
615  CheckDisposed();
616 
617  if (port == 0)
618  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
619 
620  async Task<bool> ImmediateTopicPortChange()
621  {
622  var commandResult = await SendCommand(
623  new TopicParameters(port),
624  cancellationToken)
625  .ConfigureAwait(false);
626 
627  if (commandResult.InteropResponse?.ErrorMessage != null)
628  return false;
629 
630  reattachInformation.Port = port;
631  return true;
632  }
633 
634  lock (synchronizationLock)
635  if (portClosedForReboot)
636  {
637  if (portAssignmentTcs != null)
638  throw new InvalidOperationException("A port change operation is already in progress!");
639  nextPort = port;
640  portAssignmentTcs = new TaskCompletionSource<bool>();
641  return portAssignmentTcs.Task;
642  }
643  else
644  return ImmediateTopicPortChange();
645  }
646 
648  public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
649  {
650  if (RebootState == newRebootState)
651  return true;
652 
653  logger.LogTrace("Changing reboot state to {0}", newRebootState);
654 
655  reattachInformation.RebootState = newRebootState;
656  var result = await SendCommand(
657  new TopicParameters(newRebootState),
658  cancellationToken)
659  .ConfigureAwait(false);
660 
661  return result?.InteropResponse != null && result.InteropResponse?.ErrorMessage == null;
662  }
663 
665  public void ResetRebootState()
666  {
667  CheckDisposed();
668  logger.LogTrace("Resetting reboot state...");
669  reattachInformation.RebootState = RebootState.Normal;
670  }
671 
673  public void SetHighPriority() => process.SetHighPriority();
674 
676  public void Suspend() => process.Suspend();
677 
679  public void Resume() => process.Resume();
680 
682  public void ReplaceDmbProvider(IDmbProvider dmbProvider)
683  {
684  var oldDmb = reattachInformation.Dmb;
685  reattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
686  oldDmb.Dispose();
687  }
688 
690  public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
691  => SendCommand(new TopicParameters(newInstanceName), cancellationToken);
692 
694  public Task UpdateChannels(IEnumerable<ChannelRepresentation> newChannels, CancellationToken cancellationToken)
695  => SendCommand(
696  new TopicParameters(
697  new ChatUpdate(newChannels)),
698  cancellationToken);
699  }
700 }
readonly CancellationTokenSource reattachTopicCts
A CancellationTokenSource used for the topic send operation made on reattaching.
ApiValidationStatus
Status of DMAPI validation
-
readonly IChatManager chat
The IChatManager for the SessionController
+
readonly IChatManager chat
The IChatManager for the SessionController
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
-
readonly object synchronizationLock
object for port updates and disposed.
-
ushort nextPort
The port to assign DreamDaemon when it queries for it
-
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController
+
readonly object synchronizationLock
object for port updates and disposed.
+
ushort nextPort
The port to assign DreamDaemon when it queries for it
+
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController
Version Version
The DMAPI global::System.Version for BridgeCommandType.Startup requests.
-
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
+
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
-
readonly IProcess process
The IProcess for the SessionController
+
readonly IProcess process
The IProcess for the SessionController
+ +
readonly Api.Models.Instance metadata
The Instance metadata.
ICollection< CustomCommand > CustomCommands
The DMAPI CustomCommands for BridgeCommandType.Startup requests.
-
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
- +
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
+ -
TaskCompletionSource< bool > portAssignmentTcs
The TaskCompletionSource<TResult> SetPort(ushort, CancellationToken) waits on when DreamDaemon curren...
- +
TaskCompletionSource< bool > portAssignmentTcs
The TaskCompletionSource<TResult> SetPort(ushort, CancellationToken) waits on when DreamDaemon curren...
+
ChatMessage ChatMessage
The Interop.ChatMessage for BridgeCommandType.ChatSend requests.
-
void ReplaceDmbProvider(IDmbProvider dmbProvider)
Replace Dmb with a given newProvider , disposing the old one.
+
void ReplaceDmbProvider(IDmbProvider dmbProvider)
Replace Dmb with a given newProvider , disposing the old one.
Parameters necessary for duplicating a ISessionController session
-
async Task< CombinedTopicResponse > SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
+
async Task< CombinedTopicResponse > SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
-
bool portClosedForReboot
If we know DreamDaemon currently has it&#39;s port closed
+
bool portClosedForReboot
If we know DreamDaemon currently has it&#39;s port closed
const string TopicData
Parameter json is encoded in for topic requests.
DreamDaemonSecurity MinimumSecurityLevel
The minimum required DreamDaemonSecurity level for BridgeCommandType.Startup requests.
Notifyee of when ChannelRepresentations in a IChatTrackingContext are updated.
Definition: IChannelSink.cs:10
Constants used for communication with the DMAPI
Represents the result of trying to start a DD process
Definition: LaunchResult.cs:9
-
readonly IBridgeRegistration bridgeRegistration
The IBridgeRegistration for the SessionController
-
bool released
If process should be kept alive instead
+
readonly IBridgeRegistration bridgeRegistration
The IBridgeRegistration for the SessionController
+
bool released
If process should be kept alive instead
For managing connected chat services
Definition: IChatManager.cs:13
ushort CurrentPort
The current port for BridgeCommandType.PortUpdate requests.
-
Task< bool > SetPort(ushort port, CancellationToken cancellationToken)
Causes the world to start listening on a newPort
-
void Dispose(bool disposing)
Implements the IDisposable pattern
+
Task< bool > SetPort(ushort port, CancellationToken cancellationToken)
Causes the world to start listening on a newPort
+
void Dispose(bool disposing)
Implements the IDisposable pattern
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
-
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
-
SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, ILogger< SessionController > logger, uint?startupTimeout, bool reattached)
Construct a SessionController
-
readonly IByondExecutableLock byondLock
The IByondExecutableLock for the SessionController
+
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
+
readonly IByondExecutableLock byondLock
The IByondExecutableLock for the SessionController
static readonly JsonSerializerSettings SerializerSettings
JsonSerializerSettings for use when communicating with the DMAPI.
bool Active
If the CustomCommands should be used.
-
ReattachInformation Release()
Releases the IProcess without terminating it. Also calls IDisposable.Dispose
+
ReattachInformation Release()
Releases the IProcess without terminating it. Also calls IDisposable.Dispose
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance
Abstraction over a global::System.Diagnostics.Process
Definition: IProcess.cs:9
-
readonly ILogger< SessionController > logger
The ILogger for the SessionController
+
readonly ILogger< SessionController > logger
The ILogger for the SessionController
Represents usage of the two primary BYOND server executables
Handles communication with a DreamDaemon IProcess
ICollection< string > ChannelIds
The ICollection<T> of Chat.ChannelRepresentation.Ids to sent the Text to. Must be safe to parse as ul...
Definition: ChatMessage.cs:18
-
async Task< bool > SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
Attempts to change the current RebootState to newRebootState
+
async Task< bool > SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
Attempts to change the current RebootState to newRebootState
BridgeCommandType
Represents the BridgeParameters.CommandType.
Represents a BYOND installation
Definition: Byond.cs:8
-
readonly ITopicClient byondTopicSender
The ITopicClient for the SessionController
+
readonly ITopicClient byondTopicSender
The ITopicClient for the SessionController
Represents a tracking of dynamic chat json files
DreamDaemonSecurity
DreamDaemon&#39;s security level
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
-
readonly IChatTrackingContext chatTrackingContext
The IChatTrackingContext for the SessionController
+
readonly IChatTrackingContext chatTrackingContext
The IChatTrackingContext for the SessionController
void SetChannelSink(IChannelSink channelSink)
Sets the channelSink for the IChatTrackingContext.
+
SessionController(ReattachInformation reattachInformation, Api.Models.Instance metadata, IProcess process, IByondExecutableLock byondLock, ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, ILogger< SessionController > logger, uint?startupTimeout, bool reattached)
Construct a SessionController
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
Represents an update of ChannelRepresentations.
Definition: ChatUpdate.cs:11
-
async Task< LaunchResult > GetLaunchResult(IAssemblyInformationProvider assemblyInformationProvider, uint?startupTimeout, bool reattached)
The Task<TResult> for LaunchResult.
+
async Task< LaunchResult > GetLaunchResult(IAssemblyInformationProvider assemblyInformationProvider, uint?startupTimeout, bool reattached)
The Task<TResult> for LaunchResult.
Represents a registration of an interop session.
-
bool disposed
If the SessionController has been disposed
- -
TaskCompletionSource< object > primeTcs
The TaskCompletionSource<TResult> that completes when DD tells us it&#39;s primed.
+
bool disposed
If the SessionController has been disposed
+ +
TaskCompletionSource< object > primeTcs
The TaskCompletionSource<TResult> that completes when DD tells us it&#39;s primed.
Combines a global::Byond.TopicSender.TopicResponse with a TopicResponse.
-
TaskCompletionSource< object > rebootTcs
The TaskCompletionSource<TResult> that completes when DD tells us about a reboot
-
void ResetRebootState()
Changes RebootState to RebootState.Normal without telling the DMAPI
+
TaskCompletionSource< object > rebootTcs
The TaskCompletionSource<TResult> that completes when DD tells us about a reboot
+
void ResetRebootState()
Changes RebootState to RebootState.Normal without telling the DMAPI
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Globalization;
5 using System.Linq;
6 using System.Net;
7 using System.Net.Sockets;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
20 using Tgstation.Server.Host.IO;
24 
25 namespace Tgstation.Server.Host.Components.Session
26 {
29  {
34 
38  readonly IByondManager byond;
39 
43  readonly ITopicClient byondTopicSender;
44 
49 
54 
59 
63  readonly IChatManager chat;
64 
69 
74 
79 
84 
88  readonly ILoggerFactory loggerFactory;
89 
93  readonly ILogger<SessionControllerFactory> logger;
94 
98  readonly Api.Models.Instance instance;
99 
105  static string SecurityWord(DreamDaemonSecurity securityLevel)
106  {
107  return securityLevel switch
108  {
109  DreamDaemonSecurity.Safe => "safe",
110  DreamDaemonSecurity.Trusted => "trusted",
111  DreamDaemonSecurity.Ultrasafe => "ultrasafe",
112  _ => throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)),
113  };
114  }
115 
120  static void PortBindTest(ushort port)
121  {
122  using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
123 
124  try
125  {
126  socket.Bind(new IPEndPoint(IPAddress.Loopback, port));
127  }
128  catch (Exception ex)
129  {
130  throw new JobException(ErrorCode.DreamDaemonPortInUse, ex);
131  }
132  }
133 
152  IProcessExecutor processExecutor,
153  IByondManager byond,
154  ITopicClient byondTopicSender,
155  ICryptographySuite cryptographySuite,
156  IAssemblyInformationProvider assemblyInformationProvider,
157  IIOManager ioManager,
158  IChatManager chat,
159  INetworkPromptReaper networkPromptReaper,
160  IPlatformIdentifier platformIdentifier,
161  IBridgeRegistrar bridgeRegistrar,
162  IServerPortProvider serverPortProvider,
163  ILoggerFactory loggerFactory,
164  ILogger<SessionControllerFactory> logger,
165  Api.Models.Instance instance)
166  {
167  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
168  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
169  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
170  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
171  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
172  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
173  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
174  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
175  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
176  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
177  this.bridgeRegistrar = bridgeRegistrar ?? throw new ArgumentNullException(nameof(bridgeRegistrar));
178  this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
179  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
180  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
181  }
182 
184  #pragma warning disable CA1506 // TODO: Decomplexify
185  public async Task<ISessionController> LaunchNew(
186  IDmbProvider dmbProvider,
187  IByondExecutableLock currentByondLock,
188  DreamDaemonLaunchParameters launchParameters,
189  bool primaryPort,
190  bool primaryDirectory,
191  bool apiValidate,
192  CancellationToken cancellationToken)
193  {
194  var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort;
195  if (!portToUse.HasValue)
196  throw new InvalidOperationException("Given port is null!");
197 
198  var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory;
199 
200  switch (dmbProvider.CompileJob.MinimumSecurityLevel)
201  {
202  case DreamDaemonSecurity.Ultrasafe:
203  break;
204  case DreamDaemonSecurity.Safe:
205  if (launchParameters.SecurityLevel == DreamDaemonSecurity.Ultrasafe)
206  launchParameters.SecurityLevel = DreamDaemonSecurity.Safe;
207  break;
208  case DreamDaemonSecurity.Trusted:
209  launchParameters.SecurityLevel = DreamDaemonSecurity.Trusted;
210  break;
211  default:
212  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel));
213  }
214 
215  var chatTrackingContext = chat.CreateTrackingContext();
216  try
217  {
218  // get the byond lock
219  var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
220  try
221  {
222  if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted)
223  await byondLock.TrustDmbPath(ioManager.ConcatPath(basePath, dmbProvider.DmbName), cancellationToken).ConfigureAwait(false);
224 
225  PortBindTest(portToUse.Value);
226  await CheckPagerIsNotRunning(cancellationToken).ConfigureAwait(false);
227 
228  var accessIdentifier = cryptographySuite.GetSecureString();
229 
230  // set command line options
231  // more sanitization here cause it uses the same scheme
232  var parameters = $"{DMApiConstants.ParamApiVersion}={byondTopicSender.SanitizeString(DMApiConstants.Version.Semver().ToString())}&{byondTopicSender.SanitizeString(DMApiConstants.ParamServerPort)}={serverPortProvider.HttpApiPort}&{byondTopicSender.SanitizeString(DMApiConstants.ParamAccessIdentifier)}={byondTopicSender.SanitizeString(accessIdentifier)}";
233 
234  var visibility = apiValidate ? "invisible" : "public";
235 
236  // important to run on all ports to allow port changing
237  Guid? logFileGuid = null;
238  var arguments = String.Format(
239  CultureInfo.InvariantCulture,
240  "{0} -port {1} -ports 1-65535 {2}-close -{3} -{4}{5} -public -params \"{6}\"",
241  dmbProvider.DmbName,
242  portToUse,
243  launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
244  SecurityWord(launchParameters.SecurityLevel.Value),
245  visibility,
246  platformIdentifier.IsWindows
247  ? $" -log {logFileGuid = Guid.NewGuid()}"
248  : String.Empty, // Just use stdout on linux
249  parameters);
250 
251  // See https://github.com/tgstation/tgstation-server/issues/719
252  var noShellExecute = !platformIdentifier.IsWindows;
253 
254  // launch dd
255  var process = processExecutor.LaunchProcess(
256  byondLock.DreamDaemonPath,
257  basePath,
258  arguments,
259  noShellExecute,
260  noShellExecute,
261  noShellExecute: noShellExecute);
262 
263  async Task<string> GetDDOutput()
264  {
265  if (!platformIdentifier.IsWindows)
266  return process.GetCombinedOutput();
267 
268  var logFilePath = ioManager.ConcatPath(basePath, logFileGuid.ToString());
269  try
270  {
271  var dreamDaemonLogBytes = await ioManager.ReadAllBytes(
272  logFilePath,
273  default)
274  .ConfigureAwait(false);
275 
276  return Encoding.UTF8.GetString(dreamDaemonLogBytes);
277  }
278  finally
279  {
280  try
281  {
282  await ioManager.DeleteFile(logFilePath, default).ConfigureAwait(false);
283  }
284  catch (Exception ex)
285  {
286  logger.LogWarning("Failed to delete DreamDaemon log file {0}: {1}", logFilePath, ex);
287  }
288  }
289  }
290 
291  // Log DD output
292  _ = process.Lifetime.ContinueWith(
293  async x =>
294  {
295  try
296  {
297  var ddOutput = await GetDDOutput().ConfigureAwait(false);
298  logger.LogTrace(
299  "DreamDaemon Output:{0}{1}",
300  Environment.NewLine, ddOutput);
301  }
302  catch (Exception ex)
303  {
304  logger.LogWarning("Error reading DreamDaemon output: {0}", ex);
305  }
306  },
307  TaskScheduler.Current);
308 
309  try
310  {
311  networkPromptReaper.RegisterProcess(process);
312 
313  var runtimeInformation = CreateRuntimeInformation(
314  dmbProvider,
315  chatTrackingContext,
316  launchParameters.SecurityLevel.Value,
317  apiValidate);
318 
319  var reattachInformation = new ReattachInformation(
320  dmbProvider,
321  process,
322  runtimeInformation,
323  accessIdentifier,
324  portToUse.Value,
325  primaryDirectory);
326 
327  var sessionController = new SessionController(
328  reattachInformation,
329  process,
330  byondLock,
331  byondTopicSender,
332  chatTrackingContext,
333  bridgeRegistrar,
334  chat,
335  assemblyInformationProvider,
336  loggerFactory.CreateLogger<SessionController>(),
337  launchParameters.StartupTimeout,
338  false);
339 
340  return sessionController;
341  }
342  catch
343  {
344  process.Terminate();
345  process.Dispose();
346  throw;
347  }
348  }
349  catch
350  {
351  if (currentByondLock == null)
352  byondLock.Dispose();
353  throw;
354  }
355  }
356  catch
357  {
358  chatTrackingContext.Dispose();
359  throw;
360  }
361  }
362  #pragma warning restore CA1506
363 
365  public async Task<ISessionController> Reattach(
366  ReattachInformation reattachInformation,
367  CancellationToken cancellationToken)
368  {
369  if (reattachInformation == null)
370  throw new ArgumentNullException(nameof(reattachInformation));
371 
372  var chatTrackingContext = chat.CreateTrackingContext();
373  try
374  {
375  var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
376  try
377  {
378  var process = processExecutor.GetProcess(reattachInformation.ProcessId);
379  if (process == null)
380  return null;
381 
382  try
383  {
384  networkPromptReaper.RegisterProcess(process);
385  var runtimeInformation = CreateRuntimeInformation(
386  reattachInformation.Dmb,
387  chatTrackingContext,
388  null,
389  false);
390  reattachInformation.SetRuntimeInformation(runtimeInformation);
391 
392  var controller = new SessionController(
393  reattachInformation,
394  process,
395  byondLock,
396  byondTopicSender,
397  chatTrackingContext,
398  bridgeRegistrar,
399  chat,
400  assemblyInformationProvider,
401  loggerFactory.CreateLogger<SessionController>(),
402  null,
403  true);
404 
405  process = null;
406  byondLock = null;
407  chatTrackingContext = null;
408 
409  return controller;
410  }
411  finally
412  {
413  process?.Dispose();
414  }
415  }
416  finally
417  {
418  byondLock?.Dispose();
419  }
420  }
421  finally
422  {
423  chatTrackingContext?.Dispose();
424  }
425  }
426 
428  public ISessionController CreateDeadSession(IDmbProvider dmbProvider) => new DeadSessionController(dmbProvider);
429 
439  IDmbProvider dmbProvider,
440  IChatTrackingContext chatTrackingContext,
441  DreamDaemonSecurity? securityLevel,
442  bool apiValidateOnly)
443  {
444  var revisionInfo = new Api.Models.Internal.RevisionInformation
445  {
446  CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha,
447  OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha
448  };
449 
450  var testMerges = dmbProvider
451  .CompileJob
454  .Select(x => x.TestMerge)
455  .Select(x => new TestMergeInformation(x, revisionInfo))
456  ?? Enumerable.Empty<TestMergeInformation>();
457 
458  return new RuntimeInformation(
459  assemblyInformationProvider,
460  serverPortProvider,
461  testMerges,
462  chatTrackingContext.Channels,
463  instance,
464  revisionInfo,
465  securityLevel,
466  apiValidateOnly);
467  }
468 
474  async Task CheckPagerIsNotRunning(CancellationToken cancellationToken)
475  {
476  if (!platformIdentifier.IsWindows)
477  return;
478 
479  using var otherProcess = processExecutor.GetProcessByName("byond");
480  if (otherProcess == null)
481  return;
482 
483  var otherUsernameTask = otherProcess.GetExecutingUsername(cancellationToken);
484  using var ourProcess = processExecutor.GetCurrentProcess();
485  var ourUserName = await ourProcess.GetExecutingUsername(cancellationToken).ConfigureAwait(false);
486  var otherUserName = await otherUsernameTask.ConfigureAwait(false);
487 
488  if(otherUserName.Equals(ourUserName, StringComparison.Ordinal))
489  throw new JobException(ErrorCode.DeploymentPagerRunning);
490  }
491  }
492 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
-
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Globalization;
5 using System.Linq;
6 using System.Net;
7 using System.Net.Sockets;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
20 using Tgstation.Server.Host.IO;
24 
25 namespace Tgstation.Server.Host.Components.Session
26 {
29  {
34 
38  readonly IByondManager byond;
39 
43  readonly ITopicClient byondTopicSender;
44 
49 
54 
59 
63  readonly IChatManager chat;
64 
69 
74 
79 
84 
88  readonly ILoggerFactory loggerFactory;
89 
93  readonly ILogger<SessionControllerFactory> logger;
94 
98  readonly Api.Models.Instance instance;
99 
105  static string SecurityWord(DreamDaemonSecurity securityLevel)
106  {
107  return securityLevel switch
108  {
109  DreamDaemonSecurity.Safe => "safe",
110  DreamDaemonSecurity.Trusted => "trusted",
111  DreamDaemonSecurity.Ultrasafe => "ultrasafe",
112  _ => throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)),
113  };
114  }
115 
120  static void PortBindTest(ushort port)
121  {
122  using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
123 
124  try
125  {
126  socket.Bind(new IPEndPoint(IPAddress.Any, port));
127  }
128  catch (Exception ex)
129  {
130  throw new JobException(ErrorCode.DreamDaemonPortInUse, ex);
131  }
132  }
133 
152  IProcessExecutor processExecutor,
153  IByondManager byond,
154  ITopicClient byondTopicSender,
155  ICryptographySuite cryptographySuite,
156  IAssemblyInformationProvider assemblyInformationProvider,
157  IIOManager ioManager,
158  IChatManager chat,
159  INetworkPromptReaper networkPromptReaper,
160  IPlatformIdentifier platformIdentifier,
161  IBridgeRegistrar bridgeRegistrar,
162  IServerPortProvider serverPortProvider,
163  ILoggerFactory loggerFactory,
164  ILogger<SessionControllerFactory> logger,
165  Api.Models.Instance instance)
166  {
167  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
168  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
169  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
170  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
171  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
172  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
173  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
174  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
175  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
176  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
177  this.bridgeRegistrar = bridgeRegistrar ?? throw new ArgumentNullException(nameof(bridgeRegistrar));
178  this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
179  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
180  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
181  }
182 
184  #pragma warning disable CA1506 // TODO: Decomplexify
185  public async Task<ISessionController> LaunchNew(
186  IDmbProvider dmbProvider,
187  IByondExecutableLock currentByondLock,
188  DreamDaemonLaunchParameters launchParameters,
189  bool primaryPort,
190  bool primaryDirectory,
191  bool apiValidate,
192  CancellationToken cancellationToken)
193  {
194  var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort;
195  if (!portToUse.HasValue)
196  throw new InvalidOperationException("Given port is null!");
197 
198  var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory;
199 
200  switch (dmbProvider.CompileJob.MinimumSecurityLevel)
201  {
202  case DreamDaemonSecurity.Ultrasafe:
203  break;
204  case DreamDaemonSecurity.Safe:
205  if (launchParameters.SecurityLevel == DreamDaemonSecurity.Ultrasafe)
206  launchParameters.SecurityLevel = DreamDaemonSecurity.Safe;
207  break;
208  case DreamDaemonSecurity.Trusted:
209  launchParameters.SecurityLevel = DreamDaemonSecurity.Trusted;
210  break;
211  default:
212  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel));
213  }
214 
215  var chatTrackingContext = chat.CreateTrackingContext();
216  try
217  {
218  // get the byond lock
219  var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
220  try
221  {
222  if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted)
223  await byondLock.TrustDmbPath(ioManager.ConcatPath(basePath, dmbProvider.DmbName), cancellationToken).ConfigureAwait(false);
224 
225  PortBindTest(portToUse.Value);
226  await CheckPagerIsNotRunning(cancellationToken).ConfigureAwait(false);
227 
228  var accessIdentifier = cryptographySuite.GetSecureString();
229 
230  // set command line options
231  // more sanitization here cause it uses the same scheme
232  var parameters = $"{DMApiConstants.ParamApiVersion}={byondTopicSender.SanitizeString(DMApiConstants.Version.Semver().ToString())}&{byondTopicSender.SanitizeString(DMApiConstants.ParamServerPort)}={serverPortProvider.HttpApiPort}&{byondTopicSender.SanitizeString(DMApiConstants.ParamAccessIdentifier)}={byondTopicSender.SanitizeString(accessIdentifier)}";
233 
234  var visibility = apiValidate ? "invisible" : "public";
235 
236  // important to run on all ports to allow port changing
237  Guid? logFileGuid = null;
238  var arguments = String.Format(
239  CultureInfo.InvariantCulture,
240  "{0} -port {1} -ports 1-65535 {2}-close -{3} -{4}{5} -public -params \"{6}\"",
241  dmbProvider.DmbName,
242  portToUse,
243  launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
244  SecurityWord(launchParameters.SecurityLevel.Value),
245  visibility,
246  platformIdentifier.IsWindows
247  ? $" -log {logFileGuid = Guid.NewGuid()}"
248  : String.Empty, // Just use stdout on linux
249  parameters);
250 
251  // See https://github.com/tgstation/tgstation-server/issues/719
252  var noShellExecute = !platformIdentifier.IsWindows;
253 
254  // launch dd
255  var process = processExecutor.LaunchProcess(
256  byondLock.DreamDaemonPath,
257  basePath,
258  arguments,
259  noShellExecute,
260  noShellExecute,
261  noShellExecute: noShellExecute);
262 
263  async Task<string> GetDDOutput()
264  {
265  if (!platformIdentifier.IsWindows)
266  return process.GetCombinedOutput();
267 
268  var logFilePath = ioManager.ConcatPath(basePath, logFileGuid.ToString());
269  try
270  {
271  var dreamDaemonLogBytes = await ioManager.ReadAllBytes(
272  logFilePath,
273  default)
274  .ConfigureAwait(false);
275 
276  return Encoding.UTF8.GetString(dreamDaemonLogBytes);
277  }
278  finally
279  {
280  try
281  {
282  await ioManager.DeleteFile(logFilePath, default).ConfigureAwait(false);
283  }
284  catch (Exception ex)
285  {
286  logger.LogWarning("Failed to delete DreamDaemon log file {0}: {1}", logFilePath, ex);
287  }
288  }
289  }
290 
291  // Log DD output
292  _ = process.Lifetime.ContinueWith(
293  async x =>
294  {
295  try
296  {
297  var ddOutput = await GetDDOutput().ConfigureAwait(false);
298  logger.LogTrace(
299  "DreamDaemon Output:{0}{1}",
300  Environment.NewLine, ddOutput);
301  }
302  catch (Exception ex)
303  {
304  logger.LogWarning("Error reading DreamDaemon output: {0}", ex);
305  }
306  },
307  TaskScheduler.Current);
308 
309  try
310  {
311  networkPromptReaper.RegisterProcess(process);
312 
313  var runtimeInformation = CreateRuntimeInformation(
314  dmbProvider,
315  chatTrackingContext,
316  launchParameters.SecurityLevel.Value,
317  apiValidate);
318 
319  var reattachInformation = new ReattachInformation(
320  dmbProvider,
321  process,
322  runtimeInformation,
323  accessIdentifier,
324  portToUse.Value,
325  primaryDirectory);
326 
327  var sessionController = new SessionController(
328  reattachInformation,
329  instance,
330  process,
331  byondLock,
332  byondTopicSender,
333  chatTrackingContext,
334  bridgeRegistrar,
335  chat,
336  assemblyInformationProvider,
337  loggerFactory.CreateLogger<SessionController>(),
338  launchParameters.StartupTimeout,
339  false);
340 
341  return sessionController;
342  }
343  catch
344  {
345  process.Terminate();
346  process.Dispose();
347  throw;
348  }
349  }
350  catch
351  {
352  if (currentByondLock == null)
353  byondLock.Dispose();
354  throw;
355  }
356  }
357  catch
358  {
359  chatTrackingContext.Dispose();
360  throw;
361  }
362  }
363  #pragma warning restore CA1506
364 
366  public async Task<ISessionController> Reattach(
367  ReattachInformation reattachInformation,
368  CancellationToken cancellationToken)
369  {
370  if (reattachInformation == null)
371  throw new ArgumentNullException(nameof(reattachInformation));
372 
373  var chatTrackingContext = chat.CreateTrackingContext();
374  try
375  {
376  var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
377  try
378  {
379  var process = processExecutor.GetProcess(reattachInformation.ProcessId);
380  if (process == null)
381  return null;
382 
383  try
384  {
385  networkPromptReaper.RegisterProcess(process);
386  var runtimeInformation = CreateRuntimeInformation(
387  reattachInformation.Dmb,
388  chatTrackingContext,
389  null,
390  false);
391  reattachInformation.SetRuntimeInformation(runtimeInformation);
392 
393  var controller = new SessionController(
394  reattachInformation,
395  instance,
396  process,
397  byondLock,
398  byondTopicSender,
399  chatTrackingContext,
400  bridgeRegistrar,
401  chat,
402  assemblyInformationProvider,
403  loggerFactory.CreateLogger<SessionController>(),
404  null,
405  true);
406 
407  process = null;
408  byondLock = null;
409  chatTrackingContext = null;
410 
411  return controller;
412  }
413  finally
414  {
415  process?.Dispose();
416  }
417  }
418  finally
419  {
420  byondLock?.Dispose();
421  }
422  }
423  finally
424  {
425  chatTrackingContext?.Dispose();
426  }
427  }
428 
430  public ISessionController CreateDeadSession(IDmbProvider dmbProvider) => new DeadSessionController(dmbProvider);
431 
441  IDmbProvider dmbProvider,
442  IChatTrackingContext chatTrackingContext,
443  DreamDaemonSecurity? securityLevel,
444  bool apiValidateOnly)
445  {
446  var revisionInfo = new Api.Models.Internal.RevisionInformation
447  {
448  CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha,
449  OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha
450  };
451 
452  var testMerges = dmbProvider
453  .CompileJob
456  .Select(x => x.TestMerge)
457  .Select(x => new TestMergeInformation(x, revisionInfo))
458  ?? Enumerable.Empty<TestMergeInformation>();
459 
460  return new RuntimeInformation(
461  assemblyInformationProvider,
462  serverPortProvider,
463  testMerges,
464  chatTrackingContext.Channels,
465  instance,
466  revisionInfo,
467  securityLevel,
468  apiValidateOnly);
469  }
470 
476  async Task CheckPagerIsNotRunning(CancellationToken cancellationToken)
477  {
478  if (!platformIdentifier.IsWindows)
479  return;
480 
481  using var otherProcess = processExecutor.GetProcessByName("byond");
482  if (otherProcess == null)
483  return;
484 
485  var otherUsernameTask = otherProcess.GetExecutingUsername(cancellationToken);
486  using var ourProcess = processExecutor.GetCurrentProcess();
487  var ourUserName = await ourProcess.GetExecutingUsername(cancellationToken).ConfigureAwait(false);
488  var otherUserName = await otherUsernameTask.ConfigureAwait(false);
489 
490  if(otherUserName.Equals(ourUserName, StringComparison.Ordinal))
491  throw new JobException(ErrorCode.DeploymentPagerRunning);
492  }
493  }
494 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
+
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
readonly IIOManager ioManager
The IIOManager for the SessionControllerFactory
@@ -114,12 +114,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
static string SecurityWord(DreamDaemonSecurity securityLevel)
Change a given securityLevel into the appropriate DreamDaemon command line word
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SessionControllerFactory
- +
DreamDaemonSecurity SecurityLevel
The DreamDaemonSecurity level of DreamDaemon
Parameters necessary for duplicating a ISessionController session
readonly ILogger< SessionControllerFactory > logger
The ILogger for the SessionControllerFactory
-
RuntimeInformation CreateRuntimeInformation(IDmbProvider dmbProvider, IChatTrackingContext chatTrackingContext, DreamDaemonSecurity?securityLevel, bool apiValidateOnly)
Create RuntimeInformation.
+
RuntimeInformation CreateRuntimeInformation(IDmbProvider dmbProvider, IChatTrackingContext chatTrackingContext, DreamDaemonSecurity?securityLevel, bool apiValidateOnly)
Create RuntimeInformation.
Contains various cryptographic functions
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the SessionControllerFactory
@@ -139,7 +139,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IReadOnlyCollection< ChannelRepresentation > Channels
IReadOnlyCollection<T> of ChannelRepresentations in the IChatTrackingContext.
-
async Task CheckPagerIsNotRunning(CancellationToken cancellationToken)
Make sure the BYOND pager is not running.
+
async Task CheckPagerIsNotRunning(CancellationToken cancellationToken)
Make sure the BYOND pager is not running.
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon
For managing the BYOND installation
diff --git a/_tgstation_8_server_8_api_2_models_2_instance_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_instance_8cs_source.html index d968bd5709..bf5f3b1cf1 100644 --- a/_tgstation_8_server_8_api_2_models_2_instance_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_instance_8cs_source.html @@ -94,11 +94,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Instance.cs
-Go to the documentation of this file.
1 using System.ComponentModel.DataAnnotations;
2 using System.ComponentModel.DataAnnotations.Schema;
3 
4 namespace Tgstation.Server.Api.Models
5 {
9  public class Instance
10  {
14  public long Id { get; set; }
15 
19  [Required]
20  [StringLength(Limits.MaximumStringLength)]
21  public string? Name { get; set; }
22 
26  [Required]
27  public string? Path { get; set; }
28 
32  [Required]
33  public bool? Online { get; set; }
34 
38  [Required]
39  [EnumDataType(typeof(ConfigurationType))]
40  public ConfigurationType? ConfigurationType { get; set; }
41 
45  [Required]
46  public uint? AutoUpdateInterval { get; set; }
47 
51  [Required]
52  public ushort? ChatBotLimit { get; set; }
53 
58  [NotMapped]
59  public Job? MoveJob { get; set; }
60 
65  public Instance CloneMetadata() => new Instance
66  {
67  Id = Id,
68  Name = Name,
69  Path = Path,
70  Online = Online,
72  AutoUpdateInterval = AutoUpdateInterval
73  };
74  }
75 }
const int MaximumStringLength
Length limit for strings in fields.
Definition: Limits.cs:11
+Go to the documentation of this file.
1 using System.ComponentModel.DataAnnotations;
2 using System.ComponentModel.DataAnnotations.Schema;
3 
4 namespace Tgstation.Server.Api.Models
5 {
9  public class Instance : EntityId
10  {
14  [Required]
15  [StringLength(Limits.MaximumStringLength)]
16  public string? Name { get; set; }
17 
21  [Required]
22  public string? Path { get; set; }
23 
27  [Required]
28  public bool? Online { get; set; }
29 
33  [Required]
34  [EnumDataType(typeof(ConfigurationType))]
35  public ConfigurationType? ConfigurationType { get; set; }
36 
40  [Required]
41  public uint? AutoUpdateInterval { get; set; }
42 
46  [Required]
47  public ushort? ChatBotLimit { get; set; }
48 
53  [NotMapped]
54  public Job? MoveJob { get; set; }
55 
60  public Instance CloneMetadata() => new Instance
61  {
62  Id = Id,
63  Name = Name,
64  Path = Path,
65  Online = Online,
67  AutoUpdateInterval = AutoUpdateInterval
68  };
69  }
70 }
const int MaximumStringLength
Length limit for strings in fields.
Definition: Limits.cs:11
Sanity limits to prevent users from overloading
Definition: Limits.cs:6
ConfigurationType
The type of configuration allowed on an Instance
+
Common base of Instances, CompileJobs, and Jobs.
Definition: EntityId.cs:6
Metadata about a server instance
Definition: Instance.cs:9
Represents a long running job on the server. Model is read-only, updates attempt to cancel the job ...
Definition: Job.cs:6
diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs_source.html index 420ce79856..31cc45290d 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_compile_job_8cs_source.html @@ -97,7 +97,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
1 using System;
2 using System.ComponentModel.DataAnnotations;
3 using System.ComponentModel.DataAnnotations.Schema;
4 
5 namespace Tgstation.Server.Api.Models.Internal
6 {
10  public class CompileJob : EntityId
11  {
15  [Required]
16  public string? DmeName { get; set; }
17 
21  [Required]
22  public string? Output { get; set; }
23 
27  [Required]
28  public Guid? DirectoryName { get; set; }
29 
33  [Required]
34  public DreamDaemonSecurity? MinimumSecurityLevel { get; set; }
35 
39  [NotMapped]
40  public virtual Version? DMApiVersion { get; set; }
41  }
42 }
Represents a run of DreamMaker
Definition: CompileJob.cs:10
-
Common base of CompileJobs and Jobs.
Definition: EntityId.cs:6
+
Common base of Instances, CompileJobs, and Jobs.
Definition: EntityId.cs:6
DreamDaemonSecurity
DreamDaemon&#39;s security level
diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs_source.html index 44a7f03df9..5485bdc895 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_job_8cs_source.html @@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents a long running job
Definition: Job.cs:10
-
Common base of CompileJobs and Jobs.
Definition: EntityId.cs:6
+
Common base of Instances, CompileJobs, and Jobs.
Definition: EntityId.cs:6
diff --git a/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs_source.html b/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs_source.html index fb2c1d24aa..2926fbb382 100644 --- a/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_deployment_2_dream_maker_8cs_source.html @@ -113,7 +113,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, IEventConsumer eventConsumer, IChatManager chatManager, IProcessExecutor processExecutor, IGitHubClientFactory gitHubClientFactory, ICompileJobSink compileJobConsumer, IRepositoryManager repositoryManager, ILogger< DreamMaker > logger, Api.Models.Instance metadata)
Construct DreamMaker
Definition: DreamMaker.cs:139
-
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:103
+
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:104
async Task DeploymentProcess(Models.Job job, IDatabaseContextFactory databaseContextFactory, Action< int > progressReporter, CancellationToken cancellationToken)
Definition: DreamMaker.cs:468
readonly Api.Models.Instance metadata
The Instance DreamMaker belongs to.
Definition: DreamMaker.cs:108
@@ -169,7 +169,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents information about a current git revison
- +
readonly IChatManager chatManager
The IChatManager for DreamMaker
Definition: DreamMaker.cs:78
diff --git a/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html b/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html index 7ca9acc18f..552dd237d3 100644 --- a/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html @@ -94,8 +94,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Instance.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
18 
19 namespace Tgstation.Server.Host.Components
20 {
22  #pragma warning disable CA1506 // TODO: Decomplexify
23  sealed class Instance : IInstance
24  {
27 
29  public IByondManager ByondManager { get; }
30 
32  public IWatchdog Watchdog { get; }
33 
35  public IChatManager Chat { get; }
36 
38  public StaticFiles.IConfiguration Configuration { get; }
39 
41  public IDreamMaker DreamMaker { get; }
42 
47 
52 
57 
62 
66  readonly ILogger<Instance> logger;
67 
71  readonly Api.Models.Instance metadata;
72 
76  readonly object timerLock;
77 
81  Task timerTask;
82 
86  CancellationTokenSource timerCts;
87 
103  public Instance(
104  Api.Models.Instance metadata,
105  IRepositoryManager repositoryManager,
106  IByondManager byondManager,
107  IDreamMaker dreamMaker,
108  IWatchdog watchdog,
109  IChatManager chat,
110  StaticFiles.IConfiguration
111  configuration,
112  IDatabaseContextFactory databaseContextFactory,
113  IDmbFactory dmbFactory,
114  IJobManager jobManager,
115  IEventConsumer eventConsumer,
116  ILogger<Instance> logger)
117  {
118  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
119  RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
120  ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
121  DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
122  Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
123  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
124  Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
125  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
126  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
127  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
128  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
129  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
130 
131  timerLock = new object();
132  }
133 
135  public void Dispose()
136  {
137  timerCts?.Dispose();
138  Configuration.Dispose();
139  Chat.Dispose();
140  Watchdog.Dispose();
141  dmbFactory.Dispose();
143  }
144 
151  #pragma warning disable CA1502 // TODO: Decomplexify
152  async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
153  {
154  logger.LogTrace("Entering auto-update loop");
155  while (true)
156  try
157  {
158  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
159  logger.LogInformation("Beginning auto update...");
160  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
161  try
162  {
163  User user = null;
164  await databaseContextFactory.UseContext(
165  async (db) => user = await db
166  .Users
167  .AsQueryable()
168  .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName))
169  .FirstAsync(cancellationToken)
170  .ConfigureAwait(false))
171  .ConfigureAwait(false);
172  var repositoryUpdateJob = new Job
173  {
174  Instance = new Models.Instance
175  {
176  Id = metadata.Id
177  },
178  Description = "Scheduled repository update",
179  CancelRightsType = RightsType.Repository,
180  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
181  StartedBy = user
182  };
183 
184  string deploySha = null;
185  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContextFactory, progressReporter, jobCancellationToken) =>
186  {
187  // assume 5 steps with synchronize
188  const int ProgressSections = 7;
189  const int ProgressStep = 100 / ProgressSections;
190  string repoHead = null;
191 
192  await databaseContextFactory.UseContext(
193  async databaseContext =>
194  {
195  var repositorySettingsTask = databaseContext
196  .RepositorySettings
197  .AsQueryable()
198  .Where(x => x.InstanceId == metadata.Id)
199  .FirstAsync(jobCancellationToken);
200 
201  const int NumSteps = 3;
202  var doneSteps = 0;
203 
204  Action<int> NextProgressReporter()
205  {
206  var tmpDoneSteps = doneSteps;
207  ++doneSteps;
208  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps);
209  }
210 
211  using var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false);
212  if (repo == null)
213  {
214  logger.LogTrace("Aborting repo update, no repository!");
215  return;
216  }
217 
218  var startSha = repo.Head;
219  if (!repo.Tracking)
220  {
221  logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
222  deploySha = startSha;
223  return;
224  }
225 
226  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
227 
228  // the main point of auto update is to pull the remote
229  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
230 
231  RevisionInformation currentRevInfo = null;
232  bool hasDbChanges = false;
233 
234  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
235  .AsQueryable()
236  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
237  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
238  .FirstOrDefaultAsync(jobCancellationToken);
239 
240  async Task UpdateRevInfo(string currentHead, bool onOrigin)
241  {
242  if (currentRevInfo == null)
243  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
244 
245  if (currentRevInfo == default)
246  {
247  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
248  onOrigin = true;
249  }
250 
251  var attachedInstance = new Models.Instance
252  {
253  Id = metadata.Id
254  };
255  var oldRevInfo = currentRevInfo;
256  currentRevInfo = new RevisionInformation
257  {
258  CommitSha = currentHead,
259  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
260  Instance = attachedInstance
261  };
262  if (!onOrigin)
263  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
264 
265  databaseContext.Instances.Attach(attachedInstance);
266  databaseContext.RevisionInformations.Add(currentRevInfo);
267  hasDbChanges = true;
268  }
269 
270  // take appropriate auto update actions
271  bool shouldSyncTracked;
272  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
273  {
274  logger.LogTrace("Preserving test merges...");
275 
276  var currentRevInfoTask = LoadRevInfo();
277 
278  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
279 
280  if (!result.HasValue)
281  throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict);
282 
283  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
284 
285  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
286  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
287 
288  var currentHead = repo.Head;
289  if (currentHead != startSha)
290  {
291  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
292  shouldSyncTracked = stillOnOrigin;
293  }
294  else
295  shouldSyncTracked = false;
296  }
297  else
298  {
299  logger.LogTrace("Not preserving test merges...");
300  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
301 
302  var currentHead = repo.Head;
303 
304  currentRevInfo = await databaseContext.RevisionInformations
305  .AsQueryable()
306  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
307  .FirstOrDefaultAsync(jobCancellationToken)
308  .ConfigureAwait(false);
309 
310  if (currentHead != startSha && currentRevInfo == default)
311  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
312 
313  shouldSyncTracked = true;
314  }
315 
316  // synch if necessary
317  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
318  {
319  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
320  var currentHead = repo.Head;
321  if (currentHead != currentRevInfo.CommitSha)
322  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
323  }
324 
325  repoHead = repo.Head;
326 
327  if (hasDbChanges)
328  try
329  {
330  await databaseContext.Save(jobCancellationToken).ConfigureAwait(false);
331  }
332  catch
333  {
334  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
335  throw;
336  }
337  })
338  .ConfigureAwait(false);
339 
340  progressReporter(5 * ProgressStep);
341  deploySha = repoHead;
342  }, cancellationToken).ConfigureAwait(false);
343 
344  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
345 
346  if (deploySha == null)
347  {
348  logger.LogTrace("Aborting auto update, repository error!");
349  continue;
350  }
351 
352  if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
353  {
354  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
355  continue;
356  }
357 
358  // finally set up the job
359  var compileProcessJob = new Job
360  {
361  StartedBy = user,
362  Instance = repositoryUpdateJob.Instance,
363  Description = "Scheduled code deployment",
364  CancelRightsType = RightsType.DreamMaker,
365  CancelRight = (ulong)DreamMakerRights.CancelCompile
366  };
367 
368  await jobManager.RegisterOperation(
369  compileProcessJob,
371  cancellationToken).ConfigureAwait(false);
372 
373  await jobManager.WaitForJobCompletion(compileProcessJob, user, default, cancellationToken).ConfigureAwait(false);
374  }
375  catch (OperationCanceledException)
376  {
377  logger.LogDebug("Cancelled auto update job!");
378  throw;
379  }
380  catch (Exception e)
381  {
382  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
383  continue;
384  }
385  }
386  catch (OperationCanceledException)
387  {
388  break;
389  }
390 
391  logger.LogTrace("Leaving auto update loop...");
392  }
393  #pragma warning restore CA1502
394 
396  public void Rename(string newName)
397  {
398  if (String.IsNullOrWhiteSpace(newName))
399  throw new ArgumentNullException(nameof(newName));
400  metadata.Name = newName;
401  }
402 
404  public async Task StartAsync(CancellationToken cancellationToken)
405  {
406  await Task.WhenAll(
407  SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value),
408  Configuration.StartAsync(cancellationToken),
409  ByondManager.StartAsync(cancellationToken),
410  Chat.StartAsync(cancellationToken),
411  dmbFactory.StartAsync(cancellationToken))
412  .ConfigureAwait(false);
413 
414  // dependent on so many things, its just safer this way
415  await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
416 
417  await dmbFactory.CleanUnusedCompileJobs(cancellationToken).ConfigureAwait(false);
418  }
419 
421  public async Task StopAsync(CancellationToken cancellationToken)
422  {
423  await SetAutoUpdateInterval(0).ConfigureAwait(false);
424  await Watchdog.StopAsync(cancellationToken).ConfigureAwait(false);
425  await Task.WhenAll(
426  Configuration.StopAsync(cancellationToken),
427  ByondManager.StopAsync(cancellationToken),
428  Chat.StopAsync(cancellationToken),
429  dmbFactory.StopAsync(cancellationToken))
430  .ConfigureAwait(false);
431  }
432 
434  public async Task SetAutoUpdateInterval(uint newInterval)
435  {
436  Task toWait;
437  lock (timerLock)
438  {
439  if (timerTask != null)
440  {
441  timerCts.Cancel();
442  toWait = timerTask;
443  }
444  else
445  toWait = Task.CompletedTask;
446  }
447 
448  await toWait.ConfigureAwait(false);
449  if (newInterval == 0)
450  return;
451  lock (timerLock)
452  {
453  // race condition, just quit
454  if (timerTask != null)
455  return;
456  timerCts?.Dispose();
457  timerCts = new CancellationTokenSource();
458  timerTask = TimerLoop(newInterval, timerCts.Token);
459  }
460  }
461 
463  public CompileJob LatestCompileJob() => dmbFactory.LatestCompileJob();
464  }
465 }
-
async Task SetAutoUpdateInterval(uint newInterval)
Change the Api.Models.Instance.AutoUpdateInterval for the IInstance
Definition: Instance.cs:434
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Serilog.Context;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
19 
20 namespace Tgstation.Server.Host.Components
21 {
23  #pragma warning disable CA1506 // TODO: Decomplexify
24  sealed class Instance : IInstance
25  {
28 
30  public IByondManager ByondManager { get; }
31 
33  public IWatchdog Watchdog { get; }
34 
36  public IChatManager Chat { get; }
37 
39  public StaticFiles.IConfiguration Configuration { get; }
40 
42  public IDreamMaker DreamMaker { get; }
43 
48 
53 
58 
63 
67  readonly ILogger<Instance> logger;
68 
72  readonly Api.Models.Instance metadata;
73 
77  readonly object timerLock;
78 
82  Task timerTask;
83 
87  CancellationTokenSource timerCts;
88 
104  public Instance(
105  Api.Models.Instance metadata,
106  IRepositoryManager repositoryManager,
107  IByondManager byondManager,
108  IDreamMaker dreamMaker,
109  IWatchdog watchdog,
110  IChatManager chat,
111  StaticFiles.IConfiguration
112  configuration,
113  IDatabaseContextFactory databaseContextFactory,
114  IDmbFactory dmbFactory,
115  IJobManager jobManager,
116  IEventConsumer eventConsumer,
117  ILogger<Instance> logger)
118  {
119  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
120  RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
121  ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
122  DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
123  Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
124  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
125  Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
126  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
127  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
128  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
129  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
130  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
131 
132  timerLock = new object();
133  }
134 
136  public void Dispose()
137  {
138  using (LogContext.PushProperty("Instance", metadata.Id))
139  {
140  timerCts?.Dispose();
141  Configuration.Dispose();
142  Chat.Dispose();
143  Watchdog.Dispose();
144  dmbFactory.Dispose();
146  }
147  }
148 
155  #pragma warning disable CA1502 // TODO: Decomplexify
156  async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
157  {
158  logger.LogDebug("Entering auto-update loop");
159  while (true)
160  try
161  {
162  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
163  logger.LogInformation("Beginning auto update...");
164  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
165  try
166  {
167  User user = null;
168  await databaseContextFactory.UseContext(
169  async (db) => user = await db
170  .Users
171  .AsQueryable()
172  .Where(x => x.CanonicalName == User.CanonicalizeName(Api.Models.User.AdminName))
173  .FirstAsync(cancellationToken)
174  .ConfigureAwait(false))
175  .ConfigureAwait(false);
176  var repositoryUpdateJob = new Job
177  {
178  Instance = new Models.Instance
179  {
180  Id = metadata.Id
181  },
182  Description = "Scheduled repository update",
183  CancelRightsType = RightsType.Repository,
184  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
185  StartedBy = user
186  };
187 
188  string deploySha = null;
189  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContextFactory, progressReporter, jobCancellationToken) =>
190  {
191  // assume 5 steps with synchronize
192  const int ProgressSections = 7;
193  const int ProgressStep = 100 / ProgressSections;
194  string repoHead = null;
195 
196  await databaseContextFactory.UseContext(
197  async databaseContext =>
198  {
199  var repositorySettingsTask = databaseContext
200  .RepositorySettings
201  .AsQueryable()
202  .Where(x => x.InstanceId == metadata.Id)
203  .FirstAsync(jobCancellationToken);
204 
205  const int NumSteps = 3;
206  var doneSteps = 0;
207 
208  Action<int> NextProgressReporter()
209  {
210  var tmpDoneSteps = doneSteps;
211  ++doneSteps;
212  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps);
213  }
214 
215  using var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false);
216  if (repo == null)
217  {
218  logger.LogTrace("Aborting repo update, no repository!");
219  return;
220  }
221 
222  var startSha = repo.Head;
223  if (!repo.Tracking)
224  {
225  logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
226  deploySha = startSha;
227  return;
228  }
229 
230  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
231 
232  // the main point of auto update is to pull the remote
233  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
234 
235  RevisionInformation currentRevInfo = null;
236  bool hasDbChanges = false;
237 
238  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
239  .AsQueryable()
240  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
241  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
242  .FirstOrDefaultAsync(jobCancellationToken);
243 
244  async Task UpdateRevInfo(string currentHead, bool onOrigin)
245  {
246  if (currentRevInfo == null)
247  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
248 
249  if (currentRevInfo == default)
250  {
251  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
252  onOrigin = true;
253  }
254 
255  var attachedInstance = new Models.Instance
256  {
257  Id = metadata.Id
258  };
259  var oldRevInfo = currentRevInfo;
260  currentRevInfo = new RevisionInformation
261  {
262  CommitSha = currentHead,
263  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
264  Instance = attachedInstance
265  };
266  if (!onOrigin)
267  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
268 
269  databaseContext.Instances.Attach(attachedInstance);
270  databaseContext.RevisionInformations.Add(currentRevInfo);
271  hasDbChanges = true;
272  }
273 
274  // take appropriate auto update actions
275  bool shouldSyncTracked;
276  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
277  {
278  logger.LogTrace("Preserving test merges...");
279 
280  var currentRevInfoTask = LoadRevInfo();
281 
282  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
283 
284  if (!result.HasValue)
285  throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict);
286 
287  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
288 
289  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
290  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
291 
292  var currentHead = repo.Head;
293  if (currentHead != startSha)
294  {
295  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
296  shouldSyncTracked = stillOnOrigin;
297  }
298  else
299  shouldSyncTracked = false;
300  }
301  else
302  {
303  logger.LogTrace("Not preserving test merges...");
304  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
305 
306  var currentHead = repo.Head;
307 
308  currentRevInfo = await databaseContext.RevisionInformations
309  .AsQueryable()
310  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
311  .FirstOrDefaultAsync(jobCancellationToken)
312  .ConfigureAwait(false);
313 
314  if (currentHead != startSha && currentRevInfo == default)
315  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
316 
317  shouldSyncTracked = true;
318  }
319 
320  // synch if necessary
321  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
322  {
323  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
324  var currentHead = repo.Head;
325  if (currentHead != currentRevInfo.CommitSha)
326  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
327  }
328 
329  repoHead = repo.Head;
330 
331  if (hasDbChanges)
332  try
333  {
334  await databaseContext.Save(jobCancellationToken).ConfigureAwait(false);
335  }
336  catch
337  {
338  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
339  throw;
340  }
341  })
342  .ConfigureAwait(false);
343 
344  progressReporter(5 * ProgressStep);
345  deploySha = repoHead;
346  }, cancellationToken).ConfigureAwait(false);
347 
348  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
349 
350  if (deploySha == null)
351  {
352  logger.LogTrace("Aborting auto update, repository error!");
353  continue;
354  }
355 
356  if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
357  {
358  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
359  continue;
360  }
361 
362  // finally set up the job
363  var compileProcessJob = new Job
364  {
365  StartedBy = user,
366  Instance = repositoryUpdateJob.Instance,
367  Description = "Scheduled code deployment",
368  CancelRightsType = RightsType.DreamMaker,
369  CancelRight = (ulong)DreamMakerRights.CancelCompile
370  };
371 
372  await jobManager.RegisterOperation(
373  compileProcessJob,
375  cancellationToken).ConfigureAwait(false);
376 
377  await jobManager.WaitForJobCompletion(compileProcessJob, user, default, cancellationToken).ConfigureAwait(false);
378  }
379  catch (OperationCanceledException)
380  {
381  logger.LogDebug("Cancelled auto update job!");
382  throw;
383  }
384  catch (Exception e)
385  {
386  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
387  continue;
388  }
389  }
390  catch (OperationCanceledException)
391  {
392  break;
393  }
394 
395  logger.LogTrace("Leaving auto update loop...");
396  }
397  #pragma warning restore CA1502
398 
400  public Task InstanceRenamed(string newName, CancellationToken cancellationToken)
401  {
402  if (String.IsNullOrWhiteSpace(newName))
403  throw new ArgumentNullException(nameof(newName));
404  metadata.Name = newName;
405  return Watchdog.InstanceRenamed(newName, cancellationToken);
406  }
407 
409  public async Task StartAsync(CancellationToken cancellationToken)
410  {
411  using (LogContext.PushProperty("Instance", metadata.Id))
412  {
413  await Task.WhenAll(
414  SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value),
415  Configuration.StartAsync(cancellationToken),
416  ByondManager.StartAsync(cancellationToken),
417  Chat.StartAsync(cancellationToken),
418  dmbFactory.StartAsync(cancellationToken))
419  .ConfigureAwait(false);
420 
421  // dependent on so many things, its just safer this way
422  await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
423 
424  await dmbFactory.CleanUnusedCompileJobs(cancellationToken).ConfigureAwait(false);
425  }
426  }
427 
429  public async Task StopAsync(CancellationToken cancellationToken)
430  {
431  using (LogContext.PushProperty("Instance", metadata.Id))
432  {
433  await SetAutoUpdateInterval(0).ConfigureAwait(false);
434  await Watchdog.StopAsync(cancellationToken).ConfigureAwait(false);
435  await Task.WhenAll(
436  Configuration.StopAsync(cancellationToken),
437  ByondManager.StopAsync(cancellationToken),
438  Chat.StopAsync(cancellationToken),
439  dmbFactory.StopAsync(cancellationToken))
440  .ConfigureAwait(false);
441  }
442  }
443 
445  public async Task SetAutoUpdateInterval(uint newInterval)
446  {
447  Task toWait;
448  lock (timerLock)
449  {
450  if (timerTask != null)
451  {
452  logger.LogTrace("Cancelling auto-update task");
453  timerCts.Cancel();
454  timerCts.Dispose();
455  toWait = timerTask;
456  timerTask = null;
457  timerCts = null;
458  }
459  else
460  toWait = Task.CompletedTask;
461  }
462 
463  await toWait.ConfigureAwait(false);
464  if (newInterval == 0)
465  {
466  logger.LogTrace("New auto-update interval is 0. Not starting task.");
467  return;
468  }
469 
470  lock (timerLock)
471  {
472  // race condition, just quit
473  if (timerTask != null)
474  {
475  logger.LogDebug("Aborting auto update interval change due to race condition!");
476  return;
477  }
478 
479  timerCts = new CancellationTokenSource();
480  timerTask = TimerLoop(newInterval, timerCts.Token);
481  }
482  }
483 
485  public CompileJob LatestCompileJob() => dmbFactory.LatestCompileJob();
486  }
487 }
Task InstanceRenamed(string newName, CancellationToken cancellationToken)
Called when the owning Instance is renamed.
Definition: Instance.cs:400
+ +
async Task SetAutoUpdateInterval(uint newInterval)
Change the Api.Models.Instance.AutoUpdateInterval for the IInstance
Definition: Instance.cs:445
+
long Id
The ID of the entity.
Definition: EntityId.cs:11
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
@@ -103,11 +105,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
readonly object timerLock
object for timerCts and timerTask.
Definition: Instance.cs:76
+
readonly object timerLock
object for timerCts and timerTask.
Definition: Instance.cs:77
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: Instance.cs:421
-
void Rename(string newName)
Rename the IInstance
Definition: Instance.cs:396
-
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:103
+ +
async Task StopAsync(CancellationToken cancellationToken)
Definition: Instance.cs:429
+
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:104
async Task DeploymentProcess(Models.Job job, IDatabaseContextFactory databaseContextFactory, Action< int > progressReporter, CancellationToken cancellationToken)
Definition: DreamMaker.cs:468
@@ -116,25 +118,24 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For managing connected chat services
Definition: IChatManager.cs:13
-
readonly IEventConsumer eventConsumer
The IEventConsumer for the Instance
Definition: Instance.cs:61
-
Task timerTask
The auto update Task
Definition: Instance.cs:81
+
readonly IEventConsumer eventConsumer
The IEventConsumer for the Instance
Definition: Instance.cs:62
+
Task timerTask
The auto update Task
Definition: Instance.cs:82
-
readonly Api.Models.Instance metadata
The Api.Models.Instance for the Instance
Definition: Instance.cs:71
+
readonly Api.Models.Instance metadata
The Api.Models.Instance for the Instance
Definition: Instance.cs:72
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
-
CancellationTokenSource timerCts
CancellationTokenSource for timerTask
Definition: Instance.cs:86
+
CancellationTokenSource timerCts
CancellationTokenSource for timerTask
Definition: Instance.cs:87
static string CanonicalizeName(string name)
Change a Api.Models.Internal.User.Name into a CanonicalName.
-
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Instance
Definition: Instance.cs:46
+
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Instance
Definition: Instance.cs:47
For interacting with the instance services
Definition: IInstance.cs:16
DreamMakerRights
Rights for Models.DreamMaker
-
readonly ILogger< Instance > logger
The ILogger for the Instance
Definition: Instance.cs:66
+
readonly ILogger< Instance > logger
The ILogger for the Instance
Definition: Instance.cs:67
For managing the BYOND installation
RepositoryRights
Rights for a Models.Repository
Consumes EventTypes and takes the appropriate actions
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
-
async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
Pull the repository and compile for every set of given minutes
Definition: Instance.cs:152
+
async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
Pull the repository and compile for every set of given minutes
Definition: Instance.cs:156
Instance Instance
The Models.Instance the RevisionInformation belongs to
@@ -147,15 +148,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
readonly IDmbFactory dmbFactory
The IDmbFactory for the Instance
Definition: Instance.cs:51
+
readonly IDmbFactory dmbFactory
The IDmbFactory for the Instance
Definition: Instance.cs:52
- -
readonly IJobManager jobManager
The IJobManager for the Instance
Definition: Instance.cs:56
+ +
readonly IJobManager jobManager
The IJobManager for the Instance
Definition: Instance.cs:57
Repository(LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger< Repository > logger, Action onDispose)
Construct a Repository
Definition: Repository.cs:115
async Task< IRepository > LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Instance.cs:404
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Instance.cs:409
Task StopAsync(CancellationToken cancellationToken)
- +
List< RevInfoTestMerge > ActiveTestMerges
See Api.Models.RevisionInformation.ActiveTestMerges
string OriginCommitSha
The sha of the most recent remote commit
diff --git a/_user_controller_8cs_source.html b/_user_controller_8cs_source.html index 45a22131d7..dde13c1207 100644 --- a/_user_controller_8cs_source.html +++ b/_user_controller_8cs_source.html @@ -125,7 +125,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManagerRights
Rights for managing Models.Instances
- +
long Id
The ID of the User
Definition: User.cs:16
diff --git a/_watchdog_base_8cs_source.html b/_watchdog_base_8cs_source.html index 42c60e5593..770586157b 100644 --- a/_watchdog_base_8cs_source.html +++ b/_watchdog_base_8cs_source.html @@ -94,10 +94,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WatchdogBase.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
21 
22 namespace Tgstation.Server.Host.Components.Watchdog
23 {
27  #pragma warning disable CA1506 // TODO: Decomplexify
29  {
31  public bool Running
32  {
33  get => running;
34  set
35  {
36  running = value;
37  Logger.LogTrace("Running set to {0}", running);
38  }
39  }
40 
42  public abstract bool AlphaIsActive { get; }
43 
45  public abstract Models.CompileJob ActiveCompileJob { get; }
46 
48  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; }
49 
51  public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; }
52 
54  public abstract RebootState? RebootState { get; }
55 
59  protected TaskCompletionSource<object> ActiveParametersUpdated { get; set; }
60 
64  protected SemaphoreSlim Semaphore { get; }
65 
69  protected ILogger Logger { get; }
70 
74  protected IChatManager Chat { get; }
75 
80 
84  protected IDmbFactory DmbFactory { get; }
85 
89  protected IAsyncDelayer AsyncDelayer { get; }
90 
94  readonly Api.Models.Instance instance;
95 
100 
105 
110 
115 
119  readonly object controllerDisposeLock;
120 
124  readonly bool autoStart;
125 
130 
134  CancellationTokenSource monitorCts;
135 
140 
145 
150 
154  bool running;
155 
159  bool disposed;
160 
176  protected WatchdogBase(
177  IChatManager chat,
178  ISessionControllerFactory sessionControllerFactory,
179  IDmbFactory dmbFactory,
180  IReattachInfoHandler reattachInfoHandler,
181  IDatabaseContextFactory databaseContextFactory,
182  IJobManager jobManager,
183  IServerControl serverControl,
184  IAsyncDelayer asyncDelayer,
185  ILogger logger,
186  DreamDaemonLaunchParameters initialLaunchParameters,
187  Api.Models.Instance instance,
188  bool autoStart)
189  {
190  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
191  SessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
192  DmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
193  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
194  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
195  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
196  AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
197  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
198  ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
199  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
200  this.autoStart = autoStart;
201 
202  if (serverControl == null)
203  throw new ArgumentNullException(nameof(serverControl));
204 
205  chat.RegisterCommandHandler(this);
206 
207  ActiveLaunchParameters = initialLaunchParameters;
208  releaseServers = false;
209  ActiveParametersUpdated = new TaskCompletionSource<object>();
210  controllerDisposeLock = new object();
211 
212  restartRegistration = serverControl.RegisterForRestart(this);
213  try
214  {
215  Semaphore = new SemaphoreSlim(1);
216  }
217  catch
218  {
219  restartRegistration.Dispose();
220  throw;
221  }
222  }
223 
225  public void Dispose()
226  {
227  Logger.LogTrace("Disposing...");
228  Semaphore.Dispose();
229  restartRegistration.Dispose();
230  DisposeAndNullControllers();
231  monitorCts?.Dispose();
232  disposed = true;
233  }
234 
242  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
243  {
244  if (!Running)
245  return;
246  if (!graceful)
247  {
248  var eventTask = HandleEvent(releaseServers ? EventType.WatchdogDetach : EventType.WatchdogShutdown, null, cancellationToken);
249 
250  var chatTask = announce ? Chat.SendWatchdogMessage("Shutting down...", false, cancellationToken) : Task.CompletedTask;
251 
252  await eventTask.ConfigureAwait(false);
253 
254  await StopMonitor().ConfigureAwait(false);
255 
256  DisposeAndNullControllers();
257 
258  LastLaunchParameters = null;
259 
260  await chatTask.ConfigureAwait(false);
261  return;
262  }
263 
264  // merely set the reboot state
265  var toKill = GetActiveController();
266  if (toKill != null)
267  {
268  await toKill.SetRebootState(Session.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
269  Logger.LogTrace("Graceful termination requested");
270  }
271  else
272  Logger.LogTrace("Could not gracefully terminate as there is no active controller!");
273  }
274 
281  async Task<MonitorAction> HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
282  {
283  Logger.LogTrace("Sending heartbeat to active server...");
284  var response = await activeServer.SendCommand(new TopicParameters(), cancellationToken).ConfigureAwait(false);
285 
286  var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown;
287  if (response == null)
288  {
289  switch (++heartbeatsMissed)
290  {
291  case 1:
292  Logger.LogDebug("DEFCON 4: Watchdog missed first heartbeat!");
293  break;
294  case 2:
295  var message2 = "DEFCON 3: Watchdog has missed 2 heartbeats!";
296  Logger.LogInformation(message2);
297  await Chat.SendWatchdogMessage(message2, true, cancellationToken).ConfigureAwait(false);
298  break;
299  case 3:
300  var actionToTake = shouldShutdown
301  ? "shutdown"
302  : "be restarted";
303  var message3 = $"DEFCON 2: Watchdog has missed 3 heartbeats! If DreamDaemon does not respond to the next one, the watchdog will {actionToTake}!";
304  Logger.LogWarning(message3);
305  await Chat.SendWatchdogMessage(message3, false, cancellationToken).ConfigureAwait(false);
306  break;
307  case 4:
308  var actionTaken = shouldShutdown
309  ? "Shutting down due to graceful termination request"
310  : "Restarting";
311  var message4 = $"DEFCON 1: Four heartbeats have been missed! {actionTaken}...";
312  Logger.LogWarning(message4);
313  DisposeAndNullControllers();
314  await Chat.SendWatchdogMessage(message4, false, cancellationToken).ConfigureAwait(false);
315  return shouldShutdown ? MonitorAction.Exit : MonitorAction.Restart;
316  default:
317  Logger.LogError("Invalid heartbeats missed count: {0}", heartbeatsMissed);
318  break;
319  }
320  }
321  else
322  heartbeatsMissed = 0;
323 
324  return MonitorAction.Continue;
325  }
326 
335  protected async Task LaunchNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
336  {
337  Logger.LogTrace("Begin LaunchImplNoLock");
338 
339  if (Running)
340  throw new JobException(ErrorCode.WatchdogRunning);
341 
342  if (reattachInfo == null && !DmbFactory.DmbAvailable)
343  throw new JobException(ErrorCode.WatchdogCompileJobCorrupted);
344 
345  // this is necessary, the monitor could be in it's sleep loop trying to restart, if so cancel THAT monitor and start our own with blackjack and hookers
346  Task chatTask;
347  if (startMonitor && await StopMonitor().ConfigureAwait(false))
348  chatTask = Chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", false, cancellationToken);
349  else if (announce)
350  chatTask = Chat.SendWatchdogMessage(reattachInfo == null ? "Launching..." : "Reattaching...", false, cancellationToken); // simple announce
351  else
352  chatTask = Task.CompletedTask; // no announce
353 
354  // since neither server is running, this is safe to do
355  LastLaunchParameters = ActiveLaunchParameters;
356  heartbeatsMissed = 0;
357 
358  // for when we call ourself and want to not catch thrown exceptions
359  var recursiveCallToHappen = false;
360  try
361  {
362  await InitControllers(() => recursiveCallToHappen = true, chatTask, reattachInfo, cancellationToken).ConfigureAwait(false);
363  if (recursiveCallToHappen)
364  return;
365 
366  await chatTask.ConfigureAwait(false);
367 
368  Logger.LogInformation("Launched servers successfully");
369  Running = true;
370 
371  if (startMonitor)
372  {
373  monitorCts = new CancellationTokenSource();
374  monitorTask = MonitorLifetimes(monitorCts.Token);
375  }
376  }
377  catch (Exception e)
378  {
379  // don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
380  if (!recursiveCallToHappen && !cancellationToken.IsCancellationRequested)
381  {
382  var originalChatTask = chatTask;
383  async Task ChainChatTaskWithErrorMessage()
384  {
385  await originalChatTask.ConfigureAwait(false);
386  await Chat.SendWatchdogMessage("Startup failed!", false, cancellationToken).ConfigureAwait(false);
387  }
388 
389  chatTask = ChainChatTaskWithErrorMessage();
390  Logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
391  }
392 
393  throw;
394  }
395  finally
396  {
397  // finish the chat task that's in flight
398  try
399  {
400  await chatTask.ConfigureAwait(false);
401  }
402  catch (OperationCanceledException) { }
403  }
404  }
405 
410  protected async Task<bool> StopMonitor()
411  {
412  Logger.LogTrace("StopMonitor");
413  if (monitorTask == null)
414  return false;
415  var wasRunning = !monitorTask.IsCompleted;
416  monitorCts.Cancel();
417  await monitorTask.ConfigureAwait(false);
418  Logger.LogTrace("Stopped Monitor");
419  monitorCts.Dispose();
420  monitorTask = null;
421  monitorCts = null;
422  return wasRunning;
423  }
424 
432  protected async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
433  {
434  var launchResult = await controller.LaunchResult.WithToken(cancellationToken).ConfigureAwait(false);
435 
436  // Dead sessions won't trigger this
437  if (launchResult.ExitCode.HasValue) // you killed us ray...
438  throw new JobException(
439  ErrorCode.WatchdogStartupFailed,
440  new JobException($"{serverName} failed to start: {launchResult}"));
441  if (!launchResult.StartupTime.HasValue)
442  throw new JobException(
443  ErrorCode.WatchdogStartupTimeout,
444  new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s"));
445  }
446 
450  protected abstract void DisposeAndNullControllersImpl();
451 
455  protected void DisposeAndNullControllers()
456  {
457  Logger.LogTrace("DisposeAndNullControllers");
458  lock (controllerDisposeLock)
459  DisposeAndNullControllersImpl();
460  }
461 
466  protected abstract ISessionController GetActiveController();
467 
472  protected abstract DualReattachInformation CreateReattachInformation();
473 
480  protected abstract IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState);
481 
489  protected abstract Task HandleMonitorWakeup(
490  MonitorActivationReason activationReason,
491  MonitorState monitorState,
492  CancellationToken cancellationToken);
493 
499  private async Task<MonitorState> MonitorRestart(CancellationToken cancellationToken)
500  {
501  Logger.LogTrace("Monitor restart!");
502  DisposeAndNullControllers();
503 
504  var chatTask = Task.CompletedTask;
505  for (var retryAttempts = 1; ; ++retryAttempts)
506  {
507  Exception launchException = null;
508  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
509  try
510  {
511  // use LaunchImplNoLock without announcements or restarting the monitor
512  await LaunchNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
513  if (Running)
514  {
515  Logger.LogDebug("Relaunch successful, resetting monitor state...");
516  return new MonitorState();
517  }
518  }
519  catch (OperationCanceledException)
520  {
521  throw;
522  }
523  catch (Exception e)
524  {
525  launchException = e;
526  }
527 
528  await chatTask.ConfigureAwait(false);
529  if (!Running)
530  {
531  if (launchException == null)
532  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
533  else
534  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
535  var retryDelay = Math.Min(
536  Convert.ToInt32(
537  Math.Pow(2, retryAttempts)),
538  TimeSpan.FromHours(1).Seconds); // max of one hour, increasing by a power of 2 each time
539  chatTask = Chat.SendWatchdogMessage(
540  $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}",
541  false,
542  cancellationToken);
543  await Task.WhenAll(
545  TimeSpan.FromSeconds(retryDelay),
546  cancellationToken),
547  chatTask)
548  .ConfigureAwait(false);
549  }
550  }
551  }
552 
558  private async Task MonitorLifetimes(CancellationToken cancellationToken)
559  {
560  Logger.LogTrace("Entered MonitorLifetimes");
561  using var _ = cancellationToken.Register(() => Logger.LogTrace("Monitor cancellationToken triggered"));
562 
563  // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
564  var iteration = 1;
565  try
566  {
567  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
568  try
569  {
570  Logger.LogDebug("Iteration {0} of monitor loop", iteration);
571 
572  // load the activation tasks into local variables
573  var serverTasks = GetMonitoredServerTasks(monitorState);
574  if (serverTasks.Count != 5)
575  throw new InvalidOperationException("Expected 5 monitored server tasks!");
576 
577  var activeServerLifetime = serverTasks[MonitorActivationReason.ActiveServerCrashed];
578  var activeServerReboot = serverTasks[MonitorActivationReason.ActiveServerRebooted];
579  var inactiveServerLifetime = serverTasks[MonitorActivationReason.InactiveServerCrashed];
580  var inactiveServerReboot = serverTasks[MonitorActivationReason.InactiveServerRebooted];
581  var inactiveStartupComplete = serverTasks[MonitorActivationReason.InactiveServerStartupComplete];
582 
583  Task activeLaunchParametersChanged = ActiveParametersUpdated.Task;
584  var newDmbAvailable = DmbFactory.OnNewerDmb;
585 
586  var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value;
587  var heartbeat = heartbeatSeconds == 0
588  ? Extensions.TaskExtensions.InfiniteTask()
589  : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds));
590 
591  // cancel waiting if requested
592  var cancelTcs = new TaskCompletionSource<object>();
593  var toWaitOn = Task.WhenAny(
594  activeServerLifetime,
595  activeServerReboot,
596  inactiveServerLifetime,
597  inactiveServerReboot,
598  inactiveStartupComplete,
599  heartbeat,
600  newDmbAvailable,
601  cancelTcs.Task,
602  activeLaunchParametersChanged);
603 
604  // wait for something to happen
605  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
606  await toWaitOn.ConfigureAwait(false);
607 
608  cancellationToken.ThrowIfCancellationRequested();
609  Logger.LogTrace("Monitor activated");
610 
611  // always run HandleMonitorWakeup from the context of the semaphore lock
612  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
613  {
614  // multiple things may have happened, handle them one at a time
615  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
616  {
617  MonitorActivationReason activationReason = default; // this will always be assigned before being used
618 
619  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
620  {
621  var taskCompleted = task?.IsCompleted == true;
622  task = null;
623  if (monitorState.NextAction == MonitorAction.Skip)
624  monitorState.NextAction = MonitorAction.Continue;
625  else if (taskCompleted)
626  {
627  activationReason = testActivationReason;
628  return true;
629  }
630 
631  return false;
632  }
633 
634  // process the tasks in this order and call HandlerMonitorWakup for each depending on the new monitorState
635  var anyActivation = CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
636  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
637  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
638  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
639  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
640  || CheckActivationReason(ref inactiveStartupComplete, MonitorActivationReason.InactiveServerStartupComplete)
641  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated)
642  || CheckActivationReason(ref heartbeat, MonitorActivationReason.Heartbeat);
643 
644  if (!anyActivation)
645  moreActivationsToProcess = false;
646  else
647  {
648  Logger.LogTrace("Reason: {0}", activationReason);
649  if (activationReason == MonitorActivationReason.Heartbeat)
650  monitorState.NextAction = await HandleHeartbeat(
651  monitorState.ActiveServer,
652  cancellationToken)
653  .ConfigureAwait(false);
654  else
655  await HandleMonitorWakeup(
656  activationReason,
657  monitorState,
658  cancellationToken)
659  .ConfigureAwait(false);
660  }
661  }
662  }
663 
664  Logger.LogTrace("Next monitor action is to {0}", monitorState.NextAction);
665 
666  // Restart if requested
667  if (monitorState.NextAction == MonitorAction.Restart)
668  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
669  }
670  catch (OperationCanceledException)
671  {
672  // let this bubble, other exceptions caught below
673  throw;
674  }
675  catch (Exception e)
676  {
677  // really, this should NEVER happen
678  Logger.LogError(
679  "Monitor crashed! Iteration: {0}, Monitor State: {1}, Exception: {2}",
680  iteration,
681  JsonConvert.SerializeObject(monitorState),
682  e);
683 
684  var nextActionMessage = monitorState.NextAction != MonitorAction.Exit
685  ? "Restarting"
686  : "Shutting down";
687  var chatTask = Chat.SendWatchdogMessage(
688  $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}",
689  false,
690  cancellationToken);
691 
692  if (disposed)
693  monitorState.NextAction = MonitorAction.Exit;
694  else if (monitorState.NextAction != MonitorAction.Exit)
695  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
696 
697  await chatTask.ConfigureAwait(false);
698  }
699  }
700  catch (OperationCanceledException)
701  {
702  // stop signal
703  Logger.LogDebug("Monitor cancelled");
704 
705  if (releaseServers)
706  {
707  Logger.LogTrace("Detaching servers...");
708  releasedReattachInformation = CreateReattachInformation();
709  }
710  }
711 
712  DisposeAndNullControllers();
713 
714  Logger.LogTrace("Monitor exiting...");
715  }
716 
725  protected abstract Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken);
726 
728  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
729  {
730  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
731  {
732  bool match = launchParameters.CanApplyWithoutReboot(ActiveLaunchParameters);
733  ActiveLaunchParameters = launchParameters;
734  if (match || !Running)
735  return;
736 
737  ActiveParametersUpdated.TrySetResult(null); // queue an update
738  ActiveParametersUpdated = new TaskCompletionSource<object>();
739  }
740  }
741 
743  public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
744  {
745  var activeServer = GetActiveController();
746 
747  // Server may have ended
748  if (activeServer == null)
749  return;
750 
751  var notification = new EventNotification(eventType, parameters);
752  var result = await activeServer.SendCommand(
753  new TopicParameters(notification),
754  cancellationToken)
755  .ConfigureAwait(false);
756 
757  if (result?.InteropResponse?.ChatResponses != null)
758  await Task.WhenAll(
759  result.InteropResponse.ChatResponses.Select(
760  x => Chat.SendMessage(
761  x.Text,
762  x.ChannelIds
763  .Select(channelIdString =>
764  {
765  if (UInt64.TryParse(channelIdString, out var channelId))
766  return (ulong?)channelId;
767 
768  return null;
769  })
770  .Where(nullableChannelId => nullableChannelId.HasValue)
771  .Select(nullableChannelId => nullableChannelId.Value),
772  cancellationToken)))
773  .ConfigureAwait(false);
774  }
775 
777  public async Task<string> HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
778  {
779  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
780  {
781  if (!Running)
782  return "ERROR: Server offline!";
783 
784  var commandObject = new ChatCommand(sender, commandName, arguments);
785 
786  var command = new TopicParameters(commandObject);
787 
788  var activeServer = GetActiveController();
789  var commandResult = await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false);
790 
791  if (commandResult == null)
792  return "TGS: Bad topic exchange!";
793 
794  if (commandResult.InteropResponse == null)
795  return "TGS: Bad topic response!";
796 
797  return commandResult.InteropResponse.CommandResponseMessage ??
798  "TGS: Command processed but no DMAPI response returned!";
799  }
800  }
801 
803  public async Task Launch(CancellationToken cancellationToken)
804  {
805  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
806  await LaunchNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
807  }
808 
810  public virtual async Task ResetRebootState(CancellationToken cancellationToken)
811  {
812  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
813  {
814  if (!Running)
815  return;
816  var toClear = GetActiveController();
817  if (toClear != null)
818  await toClear.SetRebootState(Session.RebootState.Normal, cancellationToken).ConfigureAwait(false);
819  }
820  }
821 
823  public async Task Restart(bool graceful, CancellationToken cancellationToken)
824  {
825  if (!Running)
826  throw new JobException(ErrorCode.WatchdogRunning);
827 
828  Logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
829  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
830  {
831  if (!graceful)
832  {
833  Task chatTask;
834  if (Running)
835  {
836  chatTask = Chat.SendWatchdogMessage("Manual restart triggered...", false, cancellationToken);
837  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
838  }
839  else
840  chatTask = Task.CompletedTask;
841  await LaunchNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
842  await chatTask.ConfigureAwait(false);
843  }
844 
845  var toReboot = GetActiveController();
846  if (toReboot != null
847  && !await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false))
848  Logger.LogWarning("Unable to send reboot state change event!");
849  }
850  }
851 
853  public async Task StartAsync(CancellationToken cancellationToken)
854  {
855  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
856  if (!autoStart && reattachInfo == null)
857  return;
858 
859  long? adminUserId = null;
860 
861  await databaseContextFactory.UseContext(
862  async db => adminUserId = await db
863  .Users
864  .AsQueryable()
865  .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
866  .Select(x => x.Id)
867  .FirstAsync(cancellationToken)
868  .ConfigureAwait(false))
869  .ConfigureAwait(false);
870  var job = new Models.Job
871  {
872  StartedBy = new Models.User
873  {
874  Id = adminUserId.Value
875  },
876  Instance = new Models.Instance
877  {
878  Id = instance.Id
879  },
880  Description = $"Instance startup watchdog {(reattachInfo != null ? "reattach" : "launch")}",
881  CancelRight = (ulong)DreamDaemonRights.Shutdown,
882  CancelRightsType = RightsType.DreamDaemon
883  };
884  await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) =>
885  {
886  using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false))
887  await LaunchNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
888  }, cancellationToken).ConfigureAwait(false);
889  }
890 
892  public async Task StopAsync(CancellationToken cancellationToken)
893  {
894  await TerminateNoLock(false, !releaseServers, cancellationToken).ConfigureAwait(false);
895  if (releasedReattachInformation != null)
896  {
897  await reattachInfoHandler.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(false);
898  releasedReattachInformation = null;
899  releaseServers = false;
900  }
901  }
902 
904  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
905  {
906  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
907  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
908  }
909 
911  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
912  {
913  releaseServers = true;
914  if (Running)
915  await Chat.SendWatchdogMessage("Detaching...", false, cancellationToken).ConfigureAwait(false);
916  }
917  }
918 }
readonly bool autoStart
If the WatchdogBase should LaunchNoLock(bool, bool, DualReattachInformation, CancellationToken) in St...
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Serilog.Context;
5 using System;
6 using System.Collections.Generic;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
22 
23 namespace Tgstation.Server.Host.Components.Watchdog
24 {
28  #pragma warning disable CA1506 // TODO: Decomplexify
30  {
32  public bool Running
33  {
34  get => running;
35  set
36  {
37  running = value;
38  Logger.LogTrace("Running set to {0}", running);
39  }
40  }
41 
43  public abstract bool AlphaIsActive { get; }
44 
46  public abstract Models.CompileJob ActiveCompileJob { get; }
47 
49  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; protected set; }
50 
52  public DreamDaemonLaunchParameters LastLaunchParameters { get; protected set; }
53 
55  public abstract RebootState? RebootState { get; }
56 
60  protected TaskCompletionSource<object> ActiveParametersUpdated { get; set; }
61 
65  protected SemaphoreSlim Semaphore { get; }
66 
70  protected ILogger Logger { get; }
71 
75  protected IChatManager Chat { get; }
76 
81 
85  protected IDmbFactory DmbFactory { get; }
86 
90  protected IAsyncDelayer AsyncDelayer { get; }
91 
95  readonly Api.Models.Instance instance;
96 
101 
106 
111 
116 
120  readonly object controllerDisposeLock;
121 
125  readonly bool autoStart;
126 
131 
135  CancellationTokenSource monitorCts;
136 
141 
146 
151 
155  bool running;
156 
160  bool disposed;
161 
177  protected WatchdogBase(
178  IChatManager chat,
179  ISessionControllerFactory sessionControllerFactory,
180  IDmbFactory dmbFactory,
181  IReattachInfoHandler reattachInfoHandler,
182  IDatabaseContextFactory databaseContextFactory,
183  IJobManager jobManager,
184  IServerControl serverControl,
185  IAsyncDelayer asyncDelayer,
186  ILogger logger,
187  DreamDaemonLaunchParameters initialLaunchParameters,
188  Api.Models.Instance instance,
189  bool autoStart)
190  {
191  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
192  SessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
193  DmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
194  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
195  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
196  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
197  AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
198  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
199  ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
200  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
201  this.autoStart = autoStart;
202 
203  if (serverControl == null)
204  throw new ArgumentNullException(nameof(serverControl));
205 
206  chat.RegisterCommandHandler(this);
207 
208  ActiveLaunchParameters = initialLaunchParameters;
209  releaseServers = false;
210  ActiveParametersUpdated = new TaskCompletionSource<object>();
211  controllerDisposeLock = new object();
212 
213  restartRegistration = serverControl.RegisterForRestart(this);
214  try
215  {
216  Semaphore = new SemaphoreSlim(1);
217  }
218  catch
219  {
220  restartRegistration.Dispose();
221  throw;
222  }
223  }
224 
226  public void Dispose()
227  {
228  Logger.LogTrace("Disposing...");
229  Semaphore.Dispose();
230  restartRegistration.Dispose();
231  DisposeAndNullControllers();
232  monitorCts?.Dispose();
233  disposed = true;
234  }
235 
243  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
244  {
245  if (!Running)
246  return;
247  if (!graceful)
248  {
249  var eventTask = HandleEvent(releaseServers ? EventType.WatchdogDetach : EventType.WatchdogShutdown, null, cancellationToken);
250 
251  var chatTask = announce ? Chat.SendWatchdogMessage("Shutting down...", false, cancellationToken) : Task.CompletedTask;
252 
253  await eventTask.ConfigureAwait(false);
254 
255  await StopMonitor().ConfigureAwait(false);
256 
257  DisposeAndNullControllers();
258 
259  LastLaunchParameters = null;
260 
261  await chatTask.ConfigureAwait(false);
262  return;
263  }
264 
265  // merely set the reboot state
266  var toKill = GetActiveController();
267  if (toKill != null)
268  {
269  await toKill.SetRebootState(Session.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
270  Logger.LogTrace("Graceful termination requested");
271  }
272  else
273  Logger.LogTrace("Could not gracefully terminate as there is no active controller!");
274  }
275 
282  async Task<MonitorAction> HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
283  {
284  Logger.LogTrace("Sending heartbeat to active server...");
285  var response = await activeServer.SendCommand(new TopicParameters(), cancellationToken).ConfigureAwait(false);
286 
287  var shouldShutdown = activeServer.RebootState == Session.RebootState.Shutdown;
288  if (response == null)
289  {
290  switch (++heartbeatsMissed)
291  {
292  case 1:
293  Logger.LogDebug("DEFCON 4: DreamDaemon missed first heartbeat!");
294  break;
295  case 2:
296  var message2 = "DEFCON 3: DreamDaemon has missed 2 heartbeats!";
297  Logger.LogInformation(message2);
298  await Chat.SendWatchdogMessage(message2, true, cancellationToken).ConfigureAwait(false);
299  break;
300  case 3:
301  var actionToTake = shouldShutdown
302  ? "shutdown"
303  : "be restarted";
304  var message3 = $"DEFCON 2: DreamDaemon has missed 3 heartbeats! If it does not respond to the next one, the watchdog will {actionToTake}!";
305  Logger.LogWarning(message3);
306  await Chat.SendWatchdogMessage(message3, false, cancellationToken).ConfigureAwait(false);
307  break;
308  case 4:
309  var actionTaken = shouldShutdown
310  ? "Shutting down due to graceful termination request"
311  : "Restarting";
312  var message4 = $"DEFCON 1: Four heartbeats have been missed! {actionTaken}...";
313  Logger.LogWarning(message4);
314  DisposeAndNullControllers();
315  await Chat.SendWatchdogMessage(message4, false, cancellationToken).ConfigureAwait(false);
316  return shouldShutdown ? MonitorAction.Exit : MonitorAction.Restart;
317  default:
318  Logger.LogError("Invalid heartbeats missed count: {0}", heartbeatsMissed);
319  break;
320  }
321  }
322  else
323  heartbeatsMissed = 0;
324 
325  return MonitorAction.Continue;
326  }
327 
336  protected async Task LaunchNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
337  {
338  Logger.LogTrace("Begin LaunchImplNoLock");
339 
340  if (Running)
341  throw new JobException(ErrorCode.WatchdogRunning);
342 
343  if (reattachInfo == null && !DmbFactory.DmbAvailable)
344  throw new JobException(ErrorCode.WatchdogCompileJobCorrupted);
345 
346  // this is necessary, the monitor could be in it's sleep loop trying to restart, if so cancel THAT monitor and start our own with blackjack and hookers
347  Task chatTask;
348  if (startMonitor && await StopMonitor().ConfigureAwait(false))
349  chatTask = Chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", false, cancellationToken);
350  else if (announce)
351  chatTask = Chat.SendWatchdogMessage(reattachInfo == null ? "Launching..." : "Reattaching...", false, cancellationToken); // simple announce
352  else
353  chatTask = Task.CompletedTask; // no announce
354 
355  // since neither server is running, this is safe to do
356  LastLaunchParameters = ActiveLaunchParameters;
357  heartbeatsMissed = 0;
358 
359  // for when we call ourself and want to not catch thrown exceptions
360  var recursiveCallToHappen = false;
361  try
362  {
363  await InitControllers(() => recursiveCallToHappen = true, chatTask, reattachInfo, cancellationToken).ConfigureAwait(false);
364  if (recursiveCallToHappen)
365  return;
366 
367  await chatTask.ConfigureAwait(false);
368 
369  Logger.LogInformation("Launched servers successfully");
370  Running = true;
371 
372  if (startMonitor)
373  {
374  monitorCts = new CancellationTokenSource();
375  monitorTask = MonitorLifetimes(monitorCts.Token);
376  }
377  }
378  catch (Exception e)
379  {
380  // don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
381  if (!recursiveCallToHappen && !cancellationToken.IsCancellationRequested)
382  {
383  var originalChatTask = chatTask;
384  async Task ChainChatTaskWithErrorMessage()
385  {
386  await originalChatTask.ConfigureAwait(false);
387  await Chat.SendWatchdogMessage("Startup failed!", false, cancellationToken).ConfigureAwait(false);
388  }
389 
390  chatTask = ChainChatTaskWithErrorMessage();
391  Logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
392  }
393 
394  throw;
395  }
396  finally
397  {
398  // finish the chat task that's in flight
399  try
400  {
401  await chatTask.ConfigureAwait(false);
402  }
403  catch (OperationCanceledException) { }
404  }
405  }
406 
411  protected async Task<bool> StopMonitor()
412  {
413  Logger.LogTrace("StopMonitor");
414  if (monitorTask == null)
415  return false;
416  var wasRunning = !monitorTask.IsCompleted;
417  monitorCts.Cancel();
418  await monitorTask.ConfigureAwait(false);
419  Logger.LogTrace("Stopped Monitor");
420  monitorCts.Dispose();
421  monitorTask = null;
422  monitorCts = null;
423  return wasRunning;
424  }
425 
433  protected async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
434  {
435  var launchResult = await controller.LaunchResult.WithToken(cancellationToken).ConfigureAwait(false);
436 
437  // Dead sessions won't trigger this
438  if (launchResult.ExitCode.HasValue) // you killed us ray...
439  throw new JobException(
440  ErrorCode.WatchdogStartupFailed,
441  new JobException($"{serverName} failed to start: {launchResult}"));
442  if (!launchResult.StartupTime.HasValue)
443  throw new JobException(
444  ErrorCode.WatchdogStartupTimeout,
445  new JobException($"{serverName} timed out on startup: {ActiveLaunchParameters.StartupTimeout.Value}s"));
446  }
447 
451  protected abstract void DisposeAndNullControllersImpl();
452 
456  protected void DisposeAndNullControllers()
457  {
458  Logger.LogTrace("DisposeAndNullControllers");
459  lock (controllerDisposeLock)
460  DisposeAndNullControllersImpl();
461  }
462 
467  protected abstract ISessionController GetActiveController();
468 
473  protected abstract DualReattachInformation CreateReattachInformation();
474 
481  protected abstract IReadOnlyDictionary<MonitorActivationReason, Task> GetMonitoredServerTasks(MonitorState monitorState);
482 
490  protected abstract Task HandleMonitorWakeup(
491  MonitorActivationReason activationReason,
492  MonitorState monitorState,
493  CancellationToken cancellationToken);
494 
500  private async Task<MonitorState> MonitorRestart(CancellationToken cancellationToken)
501  {
502  Logger.LogTrace("Monitor restart!");
503  DisposeAndNullControllers();
504 
505  var chatTask = Task.CompletedTask;
506  for (var retryAttempts = 1; ; ++retryAttempts)
507  {
508  Exception launchException = null;
509  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
510  try
511  {
512  // use LaunchImplNoLock without announcements or restarting the monitor
513  await LaunchNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
514  if (Running)
515  {
516  Logger.LogDebug("Relaunch successful, resetting monitor state...");
517  return new MonitorState();
518  }
519  }
520  catch (OperationCanceledException)
521  {
522  throw;
523  }
524  catch (Exception e)
525  {
526  launchException = e;
527  }
528 
529  await chatTask.ConfigureAwait(false);
530  if (!Running)
531  {
532  if (launchException == null)
533  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
534  else
535  Logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
536  var retryDelay = Math.Min(
537  Convert.ToInt32(
538  Math.Pow(2, retryAttempts)),
539  TimeSpan.FromHours(1).Seconds); // max of one hour, increasing by a power of 2 each time
540  chatTask = Chat.SendWatchdogMessage(
541  $"Failed to restart (Attempt: {retryAttempts}), retrying in {retryDelay}",
542  false,
543  cancellationToken);
544  await Task.WhenAll(
546  TimeSpan.FromSeconds(retryDelay),
547  cancellationToken),
548  chatTask)
549  .ConfigureAwait(false);
550  }
551  }
552  }
553 
559  private async Task MonitorLifetimes(CancellationToken cancellationToken)
560  {
561  Logger.LogTrace("Entered MonitorLifetimes");
562  using var _ = cancellationToken.Register(() => Logger.LogTrace("Monitor cancellationToken triggered"));
563 
564  // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
565  var iteration = 1;
566  try
567  {
568  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
569  using (LogContext.PushProperty("Monitor", iteration))
570  try
571  {
572  Logger.LogTrace("Iteration {0} of monitor loop", iteration);
573 
574  // load the activation tasks into local variables
575  var serverTasks = GetMonitoredServerTasks(monitorState);
576  if (serverTasks.Count != 5)
577  throw new InvalidOperationException("Expected 5 monitored server tasks!");
578 
579  var activeServerLifetime = serverTasks[MonitorActivationReason.ActiveServerCrashed];
580  var activeServerReboot = serverTasks[MonitorActivationReason.ActiveServerRebooted];
581  var inactiveServerLifetime = serverTasks[MonitorActivationReason.InactiveServerCrashed];
582  var inactiveServerReboot = serverTasks[MonitorActivationReason.InactiveServerRebooted];
583  var inactiveStartupComplete = serverTasks[MonitorActivationReason.InactiveServerStartupComplete];
584 
585  Task activeLaunchParametersChanged = ActiveParametersUpdated.Task;
586  var newDmbAvailable = DmbFactory.OnNewerDmb;
587 
588  var heartbeatSeconds = ActiveLaunchParameters.HeartbeatSeconds.Value;
589  var heartbeat = heartbeatSeconds == 0
590  ? Extensions.TaskExtensions.InfiniteTask()
591  : Task.Delay(TimeSpan.FromSeconds(heartbeatSeconds));
592 
593  // cancel waiting if requested
594  var cancelTcs = new TaskCompletionSource<object>();
595  var toWaitOn = Task.WhenAny(
596  activeServerLifetime,
597  activeServerReboot,
598  inactiveServerLifetime,
599  inactiveServerReboot,
600  inactiveStartupComplete,
601  heartbeat,
602  newDmbAvailable,
603  cancelTcs.Task,
604  activeLaunchParametersChanged);
605 
606  // wait for something to happen
607  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
608  await toWaitOn.ConfigureAwait(false);
609 
610  cancellationToken.ThrowIfCancellationRequested();
611  Logger.LogTrace("Monitor activated");
612 
613  // always run HandleMonitorWakeup from the context of the semaphore lock
614  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
615  {
616  // multiple things may have happened, handle them one at a time
617  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
618  {
619  MonitorActivationReason activationReason = default; // this will always be assigned before being used
620 
621  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
622  {
623  var taskCompleted = task?.IsCompleted == true;
624  task = null;
625  if (monitorState.NextAction == MonitorAction.Skip)
626  monitorState.NextAction = MonitorAction.Continue;
627  else if (taskCompleted)
628  {
629  activationReason = testActivationReason;
630  return true;
631  }
632 
633  return false;
634  }
635 
636  // process the tasks in this order and call HandlerMonitorWakup for each depending on the new monitorState
637  var anyActivation = CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
638  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
639  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
640  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
641  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
642  || CheckActivationReason(ref inactiveStartupComplete, MonitorActivationReason.InactiveServerStartupComplete)
643  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated)
644  || CheckActivationReason(ref heartbeat, MonitorActivationReason.Heartbeat);
645 
646  if (!anyActivation)
647  moreActivationsToProcess = false;
648  else
649  {
650  Logger.LogTrace("Reason: {0}", activationReason);
651  if (activationReason == MonitorActivationReason.Heartbeat)
652  monitorState.NextAction = await HandleHeartbeat(
653  monitorState.ActiveServer,
654  cancellationToken)
655  .ConfigureAwait(false);
656  else
657  await HandleMonitorWakeup(
658  activationReason,
659  monitorState,
660  cancellationToken)
661  .ConfigureAwait(false);
662  }
663  }
664  }
665 
666  Logger.LogTrace("Next monitor action is to {0}", monitorState.NextAction);
667 
668  // Restart if requested
669  if (monitorState.NextAction == MonitorAction.Restart)
670  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
671  }
672  catch (OperationCanceledException)
673  {
674  // let this bubble, other exceptions caught below
675  throw;
676  }
677  catch (Exception e)
678  {
679  // really, this should NEVER happen
680  Logger.LogError(
681  "Monitor crashed! Iteration: {0}, Monitor State: {1}, Exception: {2}",
682  iteration,
683  JsonConvert.SerializeObject(monitorState),
684  e);
685 
686  var nextActionMessage = monitorState.NextAction != MonitorAction.Exit
687  ? "Restarting"
688  : "Shutting down";
689  var chatTask = Chat.SendWatchdogMessage(
690  $"Monitor crashed, this should NEVER happen! Please report this, full details in logs! {nextActionMessage}. Error: {e.Message}",
691  false,
692  cancellationToken);
693 
694  if (disposed)
695  monitorState.NextAction = MonitorAction.Exit;
696  else if (monitorState.NextAction != MonitorAction.Exit)
697  monitorState = await MonitorRestart(cancellationToken).ConfigureAwait(false);
698 
699  await chatTask.ConfigureAwait(false);
700  }
701  }
702  catch (OperationCanceledException)
703  {
704  // stop signal
705  Logger.LogDebug("Monitor cancelled");
706 
707  if (releaseServers)
708  {
709  Logger.LogTrace("Detaching servers...");
710  releasedReattachInformation = CreateReattachInformation();
711  }
712  }
713 
714  DisposeAndNullControllers();
715 
716  Logger.LogTrace("Monitor exiting...");
717  }
718 
727  protected abstract Task InitControllers(Action callBeforeRecurse, Task chatTask, DualReattachInformation reattachInfo, CancellationToken cancellationToken);
728 
730  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
731  {
732  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
733  {
734  bool match = launchParameters.CanApplyWithoutReboot(ActiveLaunchParameters);
735  ActiveLaunchParameters = launchParameters;
736  if (match || !Running)
737  return;
738 
739  ActiveParametersUpdated.TrySetResult(null); // queue an update
740  ActiveParametersUpdated = new TaskCompletionSource<object>();
741  }
742  }
743 
745  public async Task HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
746  {
747  var activeServer = GetActiveController();
748 
749  // Server may have ended
750  if (activeServer == null)
751  return;
752 
753  var notification = new EventNotification(eventType, parameters);
754  var result = await activeServer.SendCommand(
755  new TopicParameters(notification),
756  cancellationToken)
757  .ConfigureAwait(false);
758 
759  if (result?.InteropResponse?.ChatResponses != null)
760  await Task.WhenAll(
761  result.InteropResponse.ChatResponses.Select(
762  x => Chat.SendMessage(
763  x.Text,
764  x.ChannelIds
765  .Select(channelIdString =>
766  {
767  if (UInt64.TryParse(channelIdString, out var channelId))
768  return (ulong?)channelId;
769 
770  return null;
771  })
772  .Where(nullableChannelId => nullableChannelId.HasValue)
773  .Select(nullableChannelId => nullableChannelId.Value),
774  cancellationToken)))
775  .ConfigureAwait(false);
776  }
777 
779  public async Task<string> HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
780  {
781  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
782  {
783  if (!Running)
784  return "ERROR: Server offline!";
785 
786  var commandObject = new ChatCommand(sender, commandName, arguments);
787 
788  var command = new TopicParameters(commandObject);
789 
790  var activeServer = GetActiveController();
791  var commandResult = await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false);
792 
793  if (commandResult == null)
794  return "TGS: Bad topic exchange!";
795 
796  if (commandResult.InteropResponse == null)
797  return "TGS: Bad topic response!";
798 
799  return commandResult.InteropResponse.CommandResponseMessage ??
800  "TGS: Command processed but no DMAPI response returned!";
801  }
802  }
803 
805  public async Task Launch(CancellationToken cancellationToken)
806  {
807  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
808  await LaunchNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
809  }
810 
812  public virtual async Task ResetRebootState(CancellationToken cancellationToken)
813  {
814  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
815  {
816  if (!Running)
817  return;
818  var toClear = GetActiveController();
819  if (toClear != null)
820  await toClear.SetRebootState(Session.RebootState.Normal, cancellationToken).ConfigureAwait(false);
821  }
822  }
823 
825  public async Task Restart(bool graceful, CancellationToken cancellationToken)
826  {
827  if (!Running)
828  throw new JobException(ErrorCode.WatchdogRunning);
829 
830  Logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
831  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
832  {
833  if (!graceful)
834  {
835  Task chatTask;
836  if (Running)
837  {
838  chatTask = Chat.SendWatchdogMessage("Manual restart triggered...", false, cancellationToken);
839  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
840  }
841  else
842  chatTask = Task.CompletedTask;
843  await LaunchNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
844  await chatTask.ConfigureAwait(false);
845  }
846 
847  var toReboot = GetActiveController();
848  if (toReboot != null
849  && !await toReboot.SetRebootState(Session.RebootState.Restart, cancellationToken).ConfigureAwait(false))
850  Logger.LogWarning("Unable to send reboot state change event!");
851  }
852  }
853 
855  public async Task StartAsync(CancellationToken cancellationToken)
856  {
857  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
858  if (!autoStart && reattachInfo == null)
859  return;
860 
861  long? adminUserId = null;
862 
863  await databaseContextFactory.UseContext(
864  async db => adminUserId = await db
865  .Users
866  .AsQueryable()
867  .Where(x => x.CanonicalName == Models.User.CanonicalizeName(Api.Models.User.AdminName))
868  .Select(x => x.Id)
869  .FirstAsync(cancellationToken)
870  .ConfigureAwait(false))
871  .ConfigureAwait(false);
872  var job = new Models.Job
873  {
874  StartedBy = new Models.User
875  {
876  Id = adminUserId.Value
877  },
878  Instance = new Models.Instance
879  {
880  Id = instance.Id
881  },
882  Description = $"Instance startup watchdog {(reattachInfo != null ? "reattach" : "launch")}",
883  CancelRight = (ulong)DreamDaemonRights.Shutdown,
884  CancelRightsType = RightsType.DreamDaemon
885  };
886  await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) =>
887  {
888  using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false))
889  await LaunchNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
890  }, cancellationToken).ConfigureAwait(false);
891  }
892 
894  public async Task StopAsync(CancellationToken cancellationToken)
895  {
896  await TerminateNoLock(false, !releaseServers, cancellationToken).ConfigureAwait(false);
897  if (releasedReattachInformation != null)
898  {
899  await reattachInfoHandler.Save(releasedReattachInformation, cancellationToken).ConfigureAwait(false);
900  releasedReattachInformation = null;
901  releaseServers = false;
902  }
903  }
904 
906  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
907  {
908  using (await SemaphoreSlimContext.Lock(Semaphore, cancellationToken).ConfigureAwait(false))
909  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
910  }
911 
913  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
914  {
915  releaseServers = true;
916  if (Running)
917  await Chat.SendWatchdogMessage("Detaching...", false, cancellationToken).ConfigureAwait(false);
918  }
919 
921  public abstract Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken);
922  }
923 }
readonly bool autoStart
If the WatchdogBase should LaunchNoLock(bool, bool, DualReattachInformation, CancellationToken) in St...
-
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
- +
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
+
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
MonitorAction NextAction
The next MonitorAction to take in WatchdogBase.MonitorLifetimes(global::System.Threading.CancellationToken)
Definition: MonitorState.cs:24
@@ -109,82 +109,84 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
async Task MonitorLifetimes(CancellationToken cancellationToken)
The main loop of the watchdog. Ayschronously waits for events to occur and then responds to them...
+
async Task MonitorLifetimes(CancellationToken cancellationToken)
The main loop of the watchdog. Ayschronously waits for events to occur and then responds to them...
+
Represents a chat command to be handled by DD
Definition: ChatCommand.cs:9
-
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:103
-
WatchdogBase(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the WatchdogBase .
-
async Task< MonitorState > MonitorRestart(CancellationToken cancellationToken)
Attempt to restart the monitor from scratch.
-
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock Semaphore
-
async Task HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
+
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:104
+
WatchdogBase(IChatManager chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Initializes a new instance of the WatchdogBase .
+
async Task< MonitorState > MonitorRestart(CancellationToken cancellationToken)
Attempt to restart the monitor from scratch.
+
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock Semaphore
+
async Task HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
-
async Task Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
+
async Task Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
-
readonly IJobManager jobManager
The IJobManager for the WatchdogBase.
+
readonly IJobManager jobManager
The IJobManager for the WatchdogBase.
bool CanApplyWithoutReboot(DreamDaemonLaunchParameters otherParameters)
Check if we match a given set of otherParameters . StartupTimeout is excluded.
-
readonly IReattachInfoHandler reattachInfoHandler
The IReattachInfoHandler for the WatchdogBase
Definition: WatchdogBase.cs:99
+
readonly IReattachInfoHandler reattachInfoHandler
The IReattachInfoHandler for the WatchdogBase
- +
For managing connected chat services
Definition: IChatManager.cs:13
+ -
Task monitorTask
The Task running the monitor loop
-
async Task StopAsync(CancellationToken cancellationToken)
+
Task monitorTask
The Task running the monitor loop
+
async Task StopAsync(CancellationToken cancellationToken)
DreamDaemonRights
Rights for Models.DreamDaemon
-
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the WatchdogBase
-
int heartbeatsMissed
The number of hearbeats missed.
+
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the WatchdogBase
+
int heartbeatsMissed
The number of hearbeats missed.
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
-
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
+
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
-
async Task< MonitorAction > HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
Handles a watchdog heartbeat.
+
async Task< MonitorAction > HandleHeartbeat(ISessionController activeServer, CancellationToken cancellationToken)
Handles a watchdog heartbeat.
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
-
virtual async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
-
void DisposeAndNullControllers()
Wrapper for DisposeAndNullControllersImpl under a locked context.
+
virtual async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
+
void DisposeAndNullControllers()
Wrapper for DisposeAndNullControllersImpl under a locked context.
Task Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan
MonitorActivationReason
Reasons for the monitor to wake up
Data structure for TopicCommandType.EventNotification requests.
-
async Task< string > HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
Handle a chat command
+
async Task< string > HandleChatCommand(string commandName, string arguments, ChatUser sender, CancellationToken cancellationToken)
Handle a chat command
Task< CombinedTopicResponse > SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
- +
ISessionController ActiveServer
The active ISessionController
Definition: MonitorState.cs:30
Represents a tgs_chat_user datum
Definition: ChatUser.cs:10
-
async Task StartAsync(CancellationToken cancellationToken)
-
readonly object controllerDisposeLock
object used for DisposeAndNullControllers.
+
async Task StartAsync(CancellationToken cancellationToken)
+
readonly object controllerDisposeLock
object used for DisposeAndNullControllers.
-
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently Running triggers a graceful restart ...
+
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently Running triggers a graceful restart ...
-
async Task LaunchNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the watchdog.
-
bool releaseServers
If the servers should be released instead of shutdown
+
async Task LaunchNoLock(bool startMonitor, bool announce, DualReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the watchdog.
+
bool releaseServers
If the servers should be released instead of shutdown
Handles communication with a DreamDaemon IProcess
Manages the runtime of Jobs
Definition: IJobManager.cs:13
-
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
+
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Handles Commands.ICommands that map to those defined in a IChatTrackingContext
-
async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
-
async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
Check the LaunchResult of a given controller for errors and throw a JobException if any are detected...
+
async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
+
async Task CheckLaunchResult(ISessionController controller, string serverName, CancellationToken cancellationToken)
Check the LaunchResult of a given controller for errors and throw a JobException if any are detected...
Handles saving and loading DualReattachInformation
-
DualReattachInformation releasedReattachInformation
Used when detaching servers.
-
readonly Api.Models.Instance instance
The Api.Models.Instance for the WatchdogBase.
Definition: WatchdogBase.cs:94
-
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
+
DualReattachInformation releasedReattachInformation
Used when detaching servers.
+
readonly Api.Models.Instance instance
The Api.Models.Instance for the WatchdogBase.
Definition: WatchdogBase.cs:95
+
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
Represents the lifetime of a IRestartHandler registration
MonitorAction
The action for the monitor loop to take when control is returned to it
Definition: MonitorAction.cs:6
Task< LaunchResult > LaunchResult
A Task that completes when DreamDaemon starts pumping the windows message queue after loading a ...
- +
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
The (absolute) state of the ExperimentalWatchdog
Definition: MonitorState.cs:9
-
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the WatchdogBase.
+
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the WatchdogBase.
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
@@ -192,7 +194,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
bool disposed
If the WatchdogBase has been Disposed.
+
bool disposed
If the WatchdogBase has been Disposed.
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:14
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_windows_watchdog_factory_8cs_source.html b/_windows_watchdog_factory_8cs_source.html index 6c35732af8..c73b342e3d 100644 --- a/_windows_watchdog_factory_8cs_source.html +++ b/_windows_watchdog_factory_8cs_source.html @@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
- +
bool AutoStart
If DreamDaemon starts when it&#39;s Instance starts
diff --git a/annotated.html b/annotated.html index 67a50ee3c8..c8765a0715 100644 --- a/annotated.html +++ b/annotated.html @@ -121,7 +121,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

 NConfiguration
 The ApiVersion header key More...
 
const string InstanceIdHeader = "instance"
 The InstanceId header key More...
 The InstanceId header key More...
 
const string JwtAuthenticationScheme = "bearer"
 The JWT authentication header scheme More...

Properties

long InstanceId [get, set]
 The Models.Instance.Id being accessed More...
 The instance Models.EntityId.Id being accessed More...
 
string RawUserAgent [get]
 The client's raw user agent More...
- +
headersThe HttpRequestHeaders to set
instanceIdThe Models.Instance.Id for the request
instanceIdThe instance Models.EntityId.Id for the request
@@ -513,7 +513,7 @@ Here is the caller graph for this function:
const string ApiVersionHeader
The ApiVersion header key
Definition: ApiHeaders.cs:27
const string InstanceIdHeader
The InstanceId header key
Definition: ApiHeaders.cs:32
string Token
The client&#39;s JWT
Definition: ApiHeaders.cs:87
-
long InstanceId
The Models.Instance.Id being accessed
Definition: ApiHeaders.cs:67
+
long InstanceId
The instance Models.EntityId.Id being accessed
Definition: ApiHeaders.cs:67
const string BasicAuthenticationScheme
The JWT authentication header scheme
Definition: ApiHeaders.cs:42

@@ -559,7 +559,7 @@ Here is the caller graph for this function:

Definition at line 22 of file ApiHeaders.cs.

-

Referenced by Tgstation.Server.Host.Core.SwaggerConfiguration.AddDefaultResponses(), Tgstation.Server.Host.Controllers.BridgeController.Process(), and Tgstation.Server.Client.ApiClient.RunRequest< TResult >().

+

Referenced by Tgstation.Server.Host.Core.SwaggerConfiguration.AddDefaultResponses(), Tgstation.Server.Host.Controllers.BridgeController.Process(), and Tgstation.Server.Client.ApiClient.RunRequest< TResult >().

@@ -615,7 +615,7 @@ Here is the caller graph for this function:
-

The InstanceId header key

+

The InstanceId header key

Definition at line 32 of file ApiHeaders.cs.

@@ -772,7 +772,7 @@ Here is the caller graph for this function:

Definition at line 82 of file ApiHeaders.cs.

-

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

+

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

@@ -794,11 +794,11 @@ Here is the caller graph for this function:
@@ -850,7 +850,7 @@ Here is the caller graph for this function:

Definition at line 77 of file ApiHeaders.cs.

-

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

+

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

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job.html index 4ee1db9d2a..3d43bcc911 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job.html @@ -109,7 +109,7 @@ Inheritance diagram for Tgstation.Server.Api.Models.CompileJob:
Inheritance graph
- +
[legend]
@@ -118,7 +118,7 @@ Collaboration diagram for Tgstation.Server.Api.Models.CompileJob:
Collaboration graph
- +
[legend]
@@ -180,7 +180,7 @@ Properties

Definition at line 21 of file CompileJob.cs.

-

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Components.Session.SessionControllerFactory.Reattach().

+

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Components.Session.SessionControllerFactory.Reattach().

@@ -230,7 +230,7 @@ Properties

Definition at line 16 of file CompileJob.cs.

-

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.CreateRuntimeInformation().

+

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.CreateRuntimeInformation().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__coll__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__coll__graph.map index b0839b26f1..e3f0509700 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__coll__graph.map @@ -1,4 +1,4 @@ - + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__coll__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__coll__graph.md5 index fcceef0381..6a7c119e3f 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__coll__graph.md5 @@ -1 +1 @@ -1a27ffa5426f7477c316f2628fad6513 \ No newline at end of file +9073eb114db791beef600c04e19f2775 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__inherit__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__inherit__graph.map index b0839b26f1..e3f0509700 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__inherit__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__inherit__graph.map @@ -1,4 +1,4 @@ - + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__inherit__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__inherit__graph.md5 index fcceef0381..6a7c119e3f 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__inherit__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_compile_job__inherit__graph.md5 @@ -1 +1 @@ -1a27ffa5426f7477c316f2628fad6513 \ No newline at end of file +9073eb114db791beef600c04e19f2775 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id.html index b049e31b25..5421789a7d 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id.html @@ -101,19 +101,21 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-

Common base of CompileJobs and Jobs. +

Common base of Instances, CompileJobs, and Jobs. More...

Inheritance diagram for Tgstation.Server.Api.Models.EntityId:
Inheritance graph
- - - - - - + + + + + + + +
[legend]
@@ -124,7 +126,7 @@ Properties
 

Detailed Description

-

Common base of CompileJobs and Jobs.

+

Common base of Instances, CompileJobs, and Jobs.

Definition at line 6 of file EntityId.cs.

Property Documentation

@@ -150,7 +152,7 @@ Properties

Definition at line 11 of file EntityId.cs.

-

Referenced by Tgstation.Server.Host.Jobs.JobManager.CancelJob(), Tgstation.Server.Host.Jobs.JobManager.CheckGetJob(), Tgstation.Server.Host.Components.Deployment.DmbFactory.CleanJob(), Tgstation.Server.Client.Components.DreamMakerClient.DreamMakerClient(), Tgstation.Server.Host.Components.Deployment.DmbFactory.FromCompileJob(), Tgstation.Server.Host.Components.Watchdog.ExperimentalWatchdog.GetMonitoredServerTasks(), Tgstation.Server.Host.Components.Watchdog.ExperimentalWatchdog.HandleMonitorWakeup(), Tgstation.Server.Host.Components.Watchdog.WindowsWatchdog.HandleNewDmbAvailable(), Tgstation.Server.Host.Jobs.JobManager.JobProgress(), Tgstation.Server.Client.Components.JobsClient.JobsClient(), Tgstation.Server.Host.Controllers.DreamMakerController.List(), Tgstation.Server.Host.Components.Deployment.DmbFactory.LockNextDmb(), Tgstation.Server.Host.Jobs.JobManager.RunJob(), and Tgstation.Server.Host.Jobs.JobManager.WaitForJobCompletion().

+

Referenced by Tgstation.Server.Host.Jobs.JobManager.CancelJob(), Tgstation.Server.Host.Jobs.JobManager.CheckGetJob(), Tgstation.Server.Host.Components.Deployment.DmbFactory.CleanJob(), Tgstation.Server.Host.Controllers.InstanceUserController.Create(), Tgstation.Server.Host.Controllers.ChatController.Create(), Tgstation.Server.Host.Controllers.RepositoryController.Create(), Tgstation.Server.Client.InstanceManagerClient.CreateClient(), Tgstation.Server.Host.Controllers.JobController.Delete(), Tgstation.Server.Host.Controllers.InstanceUserController.Delete(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Client.Components.DreamMakerClient.DreamMakerClient(), Tgstation.Server.Host.Components.Deployment.DmbFactory.FromCompileJob(), Tgstation.Server.Host.Controllers.DreamMakerController.GetId(), Tgstation.Server.Host.Controllers.JobController.GetId(), Tgstation.Server.Host.Controllers.InstanceUserController.GetId(), Tgstation.Server.Host.Components.Watchdog.ExperimentalWatchdog.GetMonitoredServerTasks(), Tgstation.Server.Host.Components.Watchdog.ExperimentalWatchdog.HandleMonitorWakeup(), Tgstation.Server.Host.Components.Watchdog.WindowsWatchdog.HandleNewDmbAvailable(), Tgstation.Server.Client.InstanceManagerClient.InstanceManagerClient(), Tgstation.Server.Host.Jobs.JobManager.JobProgress(), Tgstation.Server.Client.Components.JobsClient.JobsClient(), Tgstation.Server.Host.Controllers.JobController.List(), Tgstation.Server.Host.Controllers.DreamMakerController.List(), Tgstation.Server.Host.Controllers.InstanceUserController.List(), Tgstation.Server.Host.Controllers.ChatController.List(), Tgstation.Server.Host.Components.Deployment.DmbFactory.LockNextDmb(), Tgstation.Server.Host.Controllers.JobController.Read(), Tgstation.Server.Host.Controllers.DreamMakerController.Read(), Tgstation.Server.Host.Controllers.RepositoryController.Read(), Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl(), Tgstation.Server.Host.Jobs.JobManager.RunJob(), Tgstation.Server.Host.Components.Instance.TimerLoop(), Tgstation.Server.Host.Controllers.ByondController.Update(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), Tgstation.Server.Host.Controllers.DreamMakerController.Update(), Tgstation.Server.Host.Controllers.DreamDaemonController.Update(), Tgstation.Server.Host.Controllers.ChatController.Update(), Tgstation.Server.Host.Controllers.RepositoryController.Update(), and Tgstation.Server.Host.Jobs.JobManager.WaitForJobCompletion().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.map index 76bbb5ec3e..4f7771b3d0 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.map @@ -1,8 +1,10 @@ - - - - - - + + + + + + + + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.md5 index 3b21efa3e2..e3e7c8f733 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.md5 @@ -1 +1 @@ -a6c8ea4d224ca64b2901a178353afe71 \ No newline at end of file +ace789e8b6d34d2c8b578428292a792f \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.png b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_entity_id__inherit__graph.png index f24d7df823f9e490a6ddcc61f130b74fcf857e9b..a85560b6768159fdcd34844bd9abf2c1a1d8c2be 100644 GIT binary patch literal 31811 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�&aabht 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

+qB9dF!cpUZP+g%H)=(QzHR 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 m8bf(MpChatBotLimitTgstation.Server.Api.Models.Instance CloneMetadata()Tgstation.Server.Api.Models.Instance ConfigurationTypeTgstation.Server.Api.Models.Instance - IdTgstation.Server.Api.Models.Instance + IdTgstation.Server.Api.Models.EntityId MoveJobTgstation.Server.Api.Models.Instance NameTgstation.Server.Api.Models.Instance OnlineTgstation.Server.Api.Models.Instance diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance.html index c3d4910467..63977cb286 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance.html @@ -109,7 +109,16 @@ Inheritance diagram for Tgstation.Server.Api.Models.Instance:

Inheritance graph
- + + + +
[legend]
+
+Collaboration diagram for Tgstation.Server.Api.Models.Instance:
+
+
Collaboration graph
+ +
[legend]
@@ -121,9 +130,6 @@ Public Member Functions
- - - @@ -145,6 +151,10 @@ Properties + + + +

Properties

long Id [get, set]
 The id of the Instance. Not modifiable More...
 
string Name [get, set]
 The name of the Instance More...
 
Job MoveJob [get, set]
 The Job representing a change of Path More...
 
- Properties inherited from Tgstation.Server.Api.Models.EntityId
long Id [get, set]
 The ID of the entity. More...
 

Detailed Description

Metadata about a server instance

@@ -190,7 +200,7 @@ Properties

The time interval in minutes the repository is automatically pulled and compiles. 0 disables

-

Definition at line 46 of file Instance.cs.

+

Definition at line 41 of file Instance.cs.

@@ -214,7 +224,7 @@ Properties

The maximum number of ChatBots the Instance may contain.

-

Definition at line 52 of file Instance.cs.

+

Definition at line 47 of file Instance.cs.

Referenced by Tgstation.Server.Host.Controllers.ChatController.Create().

@@ -240,36 +250,10 @@ Properties

If ConfigurationFile can be used on the Instance

-

Definition at line 40 of file Instance.cs.

+

Definition at line 35 of file Instance.cs.

Referenced by Tgstation.Server.Host.Controllers.ConfigurationController.ForbidDueToModeConflicts().

- - - -
-
- - - - - -
- - - - -
long Tgstation.Server.Api.Models.Instance.Id
-
-getset
-
- -

The id of the Instance. Not modifiable

- -

Definition at line 14 of file Instance.cs.

- -

Referenced by Tgstation.Server.Host.Controllers.InstanceUserController.Create(), Tgstation.Server.Host.Controllers.ChatController.Create(), Tgstation.Server.Host.Controllers.RepositoryController.Create(), Tgstation.Server.Client.InstanceManagerClient.CreateClient(), Tgstation.Server.Host.Controllers.JobController.Delete(), Tgstation.Server.Host.Controllers.InstanceUserController.Delete(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.DreamMakerController.GetId(), Tgstation.Server.Host.Controllers.JobController.GetId(), Tgstation.Server.Host.Controllers.InstanceUserController.GetId(), Tgstation.Server.Client.InstanceManagerClient.InstanceManagerClient(), Tgstation.Server.Host.Controllers.JobController.List(), Tgstation.Server.Host.Controllers.DreamMakerController.List(), Tgstation.Server.Host.Controllers.InstanceUserController.List(), Tgstation.Server.Host.Controllers.ChatController.List(), Tgstation.Server.Host.Controllers.JobController.Read(), Tgstation.Server.Host.Controllers.DreamMakerController.Read(), Tgstation.Server.Host.Controllers.RepositoryController.Read(), Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl(), Tgstation.Server.Host.Jobs.JobManager.RunJob(), Tgstation.Server.Host.Components.Instance.TimerLoop(), Tgstation.Server.Host.Controllers.ByondController.Update(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), Tgstation.Server.Host.Controllers.DreamMakerController.Update(), Tgstation.Server.Host.Controllers.DreamDaemonController.Update(), Tgstation.Server.Host.Controllers.ChatController.Update(), and Tgstation.Server.Host.Controllers.RepositoryController.Update().

-
@@ -293,7 +277,7 @@ Properties

The Job representing a change of Path

Due to how Jobs are children of Instances but moving one requires the Instance to be offline, interactions with this Job are performed in a non-standard fashion. The Job is read by querying the Instance again (either via list or ID lookup) and cancelled by making any sort of update to the Instance. Once the Instance comes back Online it can be queried like a normal job

-

Definition at line 59 of file Instance.cs.

+

Definition at line 54 of file Instance.cs.

@@ -317,7 +301,7 @@ Properties

The name of the Instance

-

Definition at line 21 of file Instance.cs.

+

Definition at line 16 of file Instance.cs.

@@ -341,7 +325,7 @@ Properties

If the Instance is online

-

Definition at line 33 of file Instance.cs.

+

Definition at line 28 of file Instance.cs.

@@ -365,7 +349,7 @@ Properties

The path to where the Instance is located. Can only be changed while the Instance is offline. Must not exist when the instance is created

-

Definition at line 27 of file Instance.cs.

+

Definition at line 22 of file Instance.cs.

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.map new file mode 100644 index 0000000000..1c5a699508 --- /dev/null +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.map @@ -0,0 +1,3 @@ + + + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.md5 new file mode 100644 index 0000000000..540191f32a --- /dev/null +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.md5 @@ -0,0 +1 @@ +f7cf4a23025b167cc72c42ba9600055b \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.png b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__coll__graph.png new file mode 100644 index 0000000000000000000000000000000000000000..632bf4b5edb672e48901ce7e67a45f66ba2ce4f4 GIT binary patch literal 4082 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 literal 0 HcmV?d00001 diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.map index f3285323e6..fd0092b9b9 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.map @@ -1,3 +1,4 @@ - + + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.md5 index 006efb7d2c..257fe18d7e 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.md5 @@ -1 +1 @@ -bbb6210850dd019f97bc46b375985b6b \ No newline at end of file +9aabe82c33efceb650ef232d19df76f8 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.png b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance__inherit__graph.png index 60d851ced0433b1fa854be83411f3121a2582e8a..5adbfe2632ee86bb056de0df39cd0892b1447497 100644 GIT binary patch literal 6366 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

}*b;D9s4P$HvCA${IAEu77EJA?EnZS^@Bd8Bo;# zyan-B2O1dYeZa;~OSG3r64@>P_LD3bY7Hd4Hg)Le=nA2q?8aE@j7zfIvq4A!SOx>U z0jrGtklK%UTcVauNkIXq`f16ia?(A}TXI6Au=JFJ^1&5g0PqiR)Br{Zh=@uY?R#HS zfZ7K&IC%5jO-&~;jpO?Vxr=zE16{?ls79xmV(ksnt9`0RKID{?X&D(RZSMd#GW~L( zXSE_}U=+ngf82D(-3F2L(GCu%=G{A(kx^Du6s{JiHu(S*vcfmEw_k#`DESVEf$5KS zmbYyNb20^O5Wo0F3rfPuNqAMI1bktG@mPGTivmLCh^-N4O(rlZzhijqL z49=m`^y&!#Bi712HUd3UI(}3&!5OJiI%DuchOwDPCJ8T^oc(-#*XM~dY+vE&L`5i ztEN+lmp2~?Z`fVv?6e-B1y#z67c)P9{yaYRK5{}@)}^MB0|~mJ>wuHHQMaNjKt$fV zcMl|hUz0UdalJ6HK>zSiVr>%>6GOwP`FWshj0_CV$dwcoAF{E5TTMFx&(`uolz7*U z;rWl4BS8m;j*M(1)}E7yWI8R$;{9Po;E0Ne z-MJ0YRL^6OSN zEs$W)Drd=~&&kQj%E~$v{pfZJt2{F)NmEr-6&y`R(8WZJsU~@OTO0`yQ9^ut;P0Q9 z{4nS;$v*=*Vh3Fj_?%IomPW#-e(KMX;^@$|e50lxWV}A@|MW>TOm=vD{4H>0a(giw z7YnV9g>FH1t7aZSyl4kt-=AZeGG1O@4fXY?Z{Hr?iq8Hf>{f7ps30Pk@ndOe3<}Me zj=`74N65v^O-Mj+U+pI)$$!Mhgr$Xt93i)GL*0m4!aI&8Ui?0yvW9EyI~g)rVG?E);2#zks!uc&tL4qY{|7ZuSsnlY literal 5181 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 diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_chat_bot.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_chat_bot.html index 1277586e60..3180eb2b89 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_chat_bot.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_chat_bot.html @@ -285,7 +285,7 @@ Here is the call graph for this function:

Definition at line 26 of file ChatBot.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ChangeSettings().

+

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ChangeSettings().

@@ -311,7 +311,7 @@ Here is the call graph for this function:

Definition at line 14 of file ChatBot.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ChangeSettings(), and Tgstation.Server.Client.Components.ChatBotsClient.ChatBotsClient().

+

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ChangeSettings(), and Tgstation.Server.Client.Components.ChatBotsClient.ChatBotsClient().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job.html index 288734eaac..1147927e55 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job.html @@ -110,7 +110,7 @@ Inheritance diagram for Tgstation.Server.Api.Models.Internal.CompileJob: - +
[legend]
@@ -118,7 +118,7 @@ Collaboration diagram for Tgstation.Server.Api.Models.Internal.CompileJob:
Collaboration graph
- +
[legend]
diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__coll__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__coll__graph.map index f8599e838c..ad5a814498 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__coll__graph.map @@ -1,3 +1,3 @@ - + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__coll__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__coll__graph.md5 index 4ab4dae221..3177db8847 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__coll__graph.md5 @@ -1 +1 @@ -6f99f652e1c161c00a448b037402abc4 \ No newline at end of file +3df440e94d167fd1c64e6f8038375b59 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__inherit__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__inherit__graph.map index 4050fd0a00..50676b33a0 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__inherit__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__inherit__graph.map @@ -1,5 +1,5 @@ - + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__inherit__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__inherit__graph.md5 index 21d8033a67..e3b8ea0c1f 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__inherit__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_compile_job__inherit__graph.md5 @@ -1 +1 @@ -61d909ca14f90c80acdaf67853c3deef \ No newline at end of file +ffa539d9d1cfaff7d7c71edd1ce5d388 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_dream_daemon_launch_parameters.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_dream_daemon_launch_parameters.html index c3e6fbab61..5c8ca009e7 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_dream_daemon_launch_parameters.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_dream_daemon_launch_parameters.html @@ -170,7 +170,7 @@ Properties
Returns
if they match, otherwise
-

Referenced by Tgstation.Server.Host.Components.Watchdog.WatchdogBase.ChangeSettings().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.WatchdogBase.ChangeSettings().

Here is the caller graph for this function:
diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job.html index e6c3f943eb..6dab747de8 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job.html @@ -110,7 +110,7 @@ Inheritance diagram for Tgstation.Server.Api.Models.Internal.Job: - +
[legend]
@@ -118,7 +118,7 @@ Collaboration diagram for Tgstation.Server.Api.Models.Internal.Job:
Collaboration graph
- +
[legend]
@@ -180,7 +180,7 @@ Properties

Definition at line 43 of file Job.cs.

-

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob(), and Tgstation.Server.Host.Jobs.JobManager.StartAsync().

+

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob(), and Tgstation.Server.Host.Jobs.JobManager.StartAsync().

@@ -254,7 +254,7 @@ Properties

Definition at line 16 of file Job.cs.

-

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob().

+

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob().

@@ -280,7 +280,7 @@ Properties

Definition at line 21 of file Job.cs.

-

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob().

+

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob().

@@ -306,7 +306,7 @@ Properties

Definition at line 26 of file Job.cs.

-

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob().

+

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob().

@@ -332,7 +332,7 @@ Properties

Definition at line 32 of file Job.cs.

-

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob().

+

Referenced by Tgstation.Server.Host.Jobs.JobManager.RunJob().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__coll__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__coll__graph.map index 66d67eb496..082529e12d 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__coll__graph.map @@ -1,3 +1,3 @@ - + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__coll__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__coll__graph.md5 index 7d3213489a..37eb763615 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__coll__graph.md5 @@ -1 +1 @@ -2bfa60fb0f912539172c3188718bf9cc \ No newline at end of file +343ed05c23b95b88de7dc939e7d9e8c6 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__inherit__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__inherit__graph.map index 4b2ea4e74e..6398de36c5 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__inherit__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__inherit__graph.map @@ -1,5 +1,5 @@ - + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__inherit__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__inherit__graph.md5 index 230b1a4ede..479c531f38 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__inherit__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_job__inherit__graph.md5 @@ -1 +1 @@ -3bf963aeb836e765f1e2d4aed3de0fc3 \ No newline at end of file +287b4cce2a474386cc13cd836dc3b3f7 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_revision_information.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_revision_information.html index 44ecabe78c..8b53a557d7 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_revision_information.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_revision_information.html @@ -149,7 +149,7 @@ Properties

Definition at line 15 of file RevisionInformation.cs.

-

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.CreateRuntimeInformation(), Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.SendUpdateMessage(), and Tgstation.Server.Host.Components.Instance.TimerLoop().

+

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.CreateRuntimeInformation(), Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.SendUpdateMessage(), and Tgstation.Server.Host.Components.Instance.TimerLoop().

@@ -175,7 +175,7 @@ Properties

Definition at line 22 of file RevisionInformation.cs.

-

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.CreateRuntimeInformation(), Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.SendUpdateMessage(), and Tgstation.Server.Host.Components.Instance.TimerLoop().

+

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.CreateRuntimeInformation(), Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.SendUpdateMessage(), and Tgstation.Server.Host.Components.Instance.TimerLoop().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html index 8a70e47d51..c8cb81d99d 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html @@ -239,7 +239,7 @@ Properties

Definition at line 16 of file User.cs.

-

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

+

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

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job.html index 25bded2c7c..0a23f804ad 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job.html @@ -109,7 +109,7 @@ Inheritance diagram for Tgstation.Server.Api.Models.Job:
Inheritance graph
- +
[legend]
@@ -118,7 +118,7 @@ Collaboration diagram for Tgstation.Server.Api.Models.Job:
Collaboration graph
- +
[legend]
diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__coll__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__coll__graph.map index 321d64e3c0..f6f25fed86 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__coll__graph.map @@ -1,4 +1,4 @@ - + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__coll__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__coll__graph.md5 index d625f7fbdc..60aaa1c2e6 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__coll__graph.md5 @@ -1 +1 @@ -a9deb221641e123488d45aa4e8b12c17 \ No newline at end of file +37c9158a12798f823e481ed8f8f29a81 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__inherit__graph.map b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__inherit__graph.map index 321d64e3c0..f6f25fed86 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__inherit__graph.map +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__inherit__graph.map @@ -1,4 +1,4 @@ - + diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__inherit__graph.md5 b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__inherit__graph.md5 index d625f7fbdc..60aaa1c2e6 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__inherit__graph.md5 +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_job__inherit__graph.md5 @@ -1 +1 @@ -a9deb221641e123488d45aa4e8b12c17 \ No newline at end of file +37c9158a12798f823e481ed8f8f29a81 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_limits.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_limits.html index df726f506c..90f90d3835 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_limits.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_limits.html @@ -153,7 +153,7 @@ Public Attributes

Definition at line 11 of file Limits.cs.

-

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

+

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

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_revision_information.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_revision_information.html index ba037295bf..735aebeea0 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_revision_information.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_revision_information.html @@ -165,7 +165,7 @@ Properties

Definition at line 16 of file RevisionInformation.cs.

-

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.CreateRuntimeInformation().

+

Referenced by Tgstation.Server.Host.Components.Session.SessionControllerFactory.CreateRuntimeInformation().

diff --git a/class_tgstation_1_1_server_1_1_client_1_1_api_client.html b/class_tgstation_1_1_server_1_1_client_1_1_api_client.html index c5f6c30ca6..3dacf1f706 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_api_client.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_api_client.html @@ -893,7 +893,7 @@ Private Attributes - +
routeThe route to run
bodyThe body of the request
methodThe method of the request
instanceIdThe optional Instance.Id for the request
instanceIdThe optional instance EntityId.Id for the request
tokenRefreshIf this is a token refresh operation.
cancellationTokenThe CancellationToken for the operation
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client.html b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client.html index fa86dedd58..4e1f7123c0 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client.html @@ -120,8 +120,9 @@ Collaboration diagram for Tgstation.Server.Client.Components.ByondClient: - - + + +
[legend]
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.map b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.map index 3210589b1d..8bdc15482c 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.map @@ -1,6 +1,7 @@ - - + + + diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.md5 b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.md5 index 5c861ebed8..2401897800 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.md5 @@ -1 +1 @@ -893073c918ae27c8fa3ff66b7d6faee3 \ No newline at end of file +faa1f089bd66c78e0d0c3e5863b3c5eb \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.png b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_byond_client__coll__graph.png index f6119cdac618062e5ea489d3208facc6a22fd252..1da801cadb4996d17f8a5c489d2eb79247a79b11 100644 GIT binary patch literal 15996 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 - - + + +
[legend]
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.map b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.map index 52f44a6e15..7b7c3c500e 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.map @@ -1,6 +1,7 @@ - - + + + diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.md5 b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.md5 index 0b18174c88..7c95d6d1f3 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.md5 @@ -1 +1 @@ -aeadeff51473612b26e34752836c5406 \ No newline at end of file +39249f1819a890f362981222fd70e9ee \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.png b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_chat_bots_client__coll__graph.png index aaea4a598d665c4116797a84ccf012340152e1a5..ff3690f958669d1f49fa1d07a35f011b8b81fa5e 100644 GIT binary patch literal 16233 zcmbWeby!tf+djGw1VK=`TLl#Fy8|WYN-%G}0nr08-K=F1is=DV3D&?rzRA z_xoPI>pSP$=lpTlT;H$fD3;H znykEmzi^FZAIhN4kpDi_WX7OS%&5mQQfe+AS4Ld*R5eE>))lp$75nD-KEkoXQr4yM z%q#7FvS4~dmvQ|HXEg%$j;ONu#h>1Q#YICx13?8pZhNDyg*K ztKeWY__cqAK=Cghy#47)ILq>*qbD*l@=ZQ5!8(L~iZC{REcR$hG@goPp+WONvB^_2 zGdw;%K0*dz@}u3AZe36QuHIv68k&Y5Kk!*tScLQ02jBWA#d}iVNX>07HI2sxXPp?; zKTc0u>Mt~z{vJ)uYmC#GED?FMw`S7)Sw1*4wA+2emsye+=X&gutL*Ge&CTcGI&QgL zQOHPE@L;42oZ4S-Qc_Y9#M}kaRc^GmxARyI6bYN1h@TzXmseCYG&Y`V3cCCdjm}AM zS{dXIqZ7R3Xy?>3h_srk!^6WxCasqe_b`Fzir(Jd;E0I8swxpn zD=S{t&1Y}lzRjA+FO2-UfX&K^7NNE!UAQhTZjf8@nq4tT#@Lt@KARWab6xI(fV|vA zLc(uhGXYL$EILimD*F225~usv9GV5+&W^jzg1fj4Yw6>; z4IikekiL2IChE-_UkVP5J6fyXLdZyHXhN!-*Cw|Yx`muqKXxAF{5afMicUxf_V>pb zF1N!b2sj5fqO%h%;0X)ODoAF$>aL?CX$TJdR^Oq5G--$op@*Ej{Kj|#cEX1bf(a7b z+?N#;6i_~Y{-_KL4W)hks%CNuQ`9sxq+31m8SR<#ywB9c#E;LcvwmoZ4mCb8@#@OM zh)>>UrzoGE9<}|Wqbpagq>aAJP`NSNnRwCL+q=B7GAF~ls*wMAj`;58W@CdN-tR&~ z36!Llmv|r%UEl{n8+7l(_k%f{`jOnQE2z5Q>3#3)yi{g0W_`y{p7}f~E-vmm2M5`i z!jmVy$;nLc&{?7%8Ch9{S65d*Xv@1h#eU%&0Rh2!10K_O(`5L}ISFakak2lN@m2uaeow_IPsmClYTe`Tpy^e2A{P=NkxUM!uoRu|o z*AJF@#PRWQk^A1NX)Fmnec0m|HkK<_u%Bd*-oJl8D<_Av8$(kzKR)i`x-~no{I_J% zex%Czp{eQTedh-caP{=`Xm9nmw&LMkqW96`dMGDH$flTd>odQZAv~8aU%vGCKI`?g zBy6=NCBZPkUU&HV><8($mKIhvHr(6q2q`Ei_ILUXd98=olMjDkewVQ2orz$w?F&8(Z$MKkTCQc6mqOm~ggQ4-}3H?#EC zuonuAzu>5g^!y+u_gA(m`_uVfDUG8w?n@npM-?Wm5%?#EJ7lnY&S$ILgwf|@6zW%L zAa5SDNA-BYGzI)ldw7wChQ>JQ%a_|MckZao_vL^1m9Hy^{2Qj!qG))n_DEsAzcAUz z+M0`yQB>>Eqem|`Yjea4;2|5En4DKsRBUK(m!lT4{bUPsn1A$0RTq=7v~i7<^<*-g zWid)9JKb{%{uiz9@;DKSX7KiFtpAtd#ujLeB8g^s8_yB-?nY}k-TCi7Z+?Tn9x0w- zPqh#=xUY$s&CimMU^rk-vPN=pa$53roxl0q<|{!hEF6^ErJ-IPU|;Y>g~<3SB!;D^ zX-E^}h#pxoNeg}m&Kh}I|5#=vA|pdEHLc5JciB)$U7ZNU!_SY4x=z06EJBKdMM3cd zQP8H;0(W5ry4Z9 zxJd9$&J=2RQbIz)_|ckoeRJr{K+(iQl#fa_&aZWv zQm%5^GG$3kiyYtIAg89I%TCGklP34W?WH;YTF<*x0z`Fm@^-5ogb1k$W;dxV+<6Q!hzp=RMFH<_?Y!_xuT-XnJm}31e}9OvH^FWl?Wfr5`*H&&fTnrLFzRP=cM1cKWrHpPLa3 zbbEE>f{6*2ot#WMGS7}s_}&`z=1dJ>mTrZGUsJlysBbFaC3;R#?3$Is5#HsW(Q!_6 zH*Vb4tJ=l;(|rbytxQQf+dn&-cX)*18_MC=Cq`i#)US z2*#S=urLq*pt|A7=W}yoWZ1vnlGAV*v`a}fV520o*s3Fw>gq1S0N2{ zo_DHTiFNS>Oc)8~9CEc48n{}j#_OqjAz3xhHD!8QZ1t{_VE7SSFo+d#@mgwM7~H+G zNV8pvOpgxFI83-CatDO3ki~BYZ(jJPxBm%7u91Pffi)j_ng2XoniWlnT%+Zmmutr1 z({mbNAi<1u@bY2;GLUE~c(?LVEAp}q1H6njL|#_@&;XxkMfT4{y5SSEc1&P=GAox_ z14uDkghJW(W8>qe_BW=WO5S!*4c4hikq-I?u5`@vyyxTEp6&Rszfv|vSdAx>pIltb zYv8`Z54|T>tji}xr>E|8hdYcxqz~{5nhXll#)$J6O(hM3jw8k=@7CS_uDa~(igJ&; zRQK;+#zLuNGvE^1HpFtAHT*3xL}dwB`h^VooE+_4VP=+Gxl4wP2^hSQp@NSZ-=j_q zJb(WD#uG^nqi0uN1qEFZ6{Y7jZ8zFDsIIOuUVr!geQ4u7zA*X&dP;5s%lbZ2j8|=K zt?{i-|5OxZ&dyH#iREDm?wV;L)CGb=#+MbZ=yW&XArq94X1O(-WFo7@s&d`7N!UuKxbA(d4-6Ro6=IIxeb}45=i)c|&SBR?9%fuJA3l zu=X_+YLkh^fJX`neY%O9EbQ!f034{r-0m2-EnEZGLL_mr9n(_H`PZ9)#lbOrKKx$H_F8{@_06TSkAGklu*5#GTOQ!uUj9o4y|^h` zHM7X_@9q7A1DZ1t&V${7k}^>uxHeca80j-?l`VrtdY z)R-Ori7B$eevTDd=18SuQ)9rzjZyct5Hka`SO0XK(+aKl z>AuNFVMhYM2VP!Y1Ep4U%PT8OBMaZgYUKOAd-pEExh;+p zs-I$Xa&ly5k{iKLnN6z?7WSRhv4!r;==gX%3kwUBkDnj5udgqF_p7tn@Tgu>a1`0k zDGXORUp_rO9e{R3?eX|3X`XJ`rKP@nKRGtPni{bJ{VH5KI=bR85$DzVwzi7^*8?yZ zOyA7R>}+afjo(#3imFk%8MV)Ux6P!;T+e)pebMrXYPu34{T)RaD zvxe!T%-;1jDf}CE%+&o^JVPZULpFXA7Fr{0%wpS#dmH5oFROJ#0`9$%{~$2Ylda0b z%PXa%L|ke;aufISj~_qwVVa^76N%tn(ysiwDO&uR>L5XQJ zJl&*fPZFc+>+9RGPNTiRK~7*fQb~c2kB{=9GH}6Kt2+}1h|*jQTXcU;;w)rx(s&zC zq5W+8tE#H1mJ`{>j~h-;j+#xnF&3I(^mj=C&ysIl@b$5&JE3fiWP14cF@BQguC~Yi zM9?Gy+QZXx18}C%+GtHw+YC^P7QrzOKQ8^s;OC+)>nQjof!fnU3(<{lWIFMhT3Xjv zEyB6$j!Chwu+VFD1_lQE(=m#7R@2aD<|W)aJBFUEnKcvC3h3U{v^2QiaM^e+mb-T= zIx}WN$kkMH}5asUSx*SIB&ChEtt?>~OL3JtwTc}wfp zxIj)Ew_!wEJol9wH}D@ne*6gT4AxA~=0{Oi(vXml>$h%w%Ti9`u^Rm9wr2wzEJ4`u zrq}6#+1uDy^Y#+26Su2uY^*FSSc2BWKKD{2)zseVBqe(t;@2H-Uc`~AFSna2vKkV= zY+DVLK5g{JhduQ>TU5S*4Rw67=H1%jPq=vAB>?=qT(#fETP~arSu%!kn9XW3E@ohl6%9%NUKa1cQ(L+6I3M6W(bX-~-sY5oeMPHs2 z8hsPK?#%uzvti=WEBA3+>U#>eqjWQNe=?N2XH&1*g+#a1vH=PpQh=uBf2W}r9!AhN zgmTvg!|It=GgvVqb0MD=-1+%Nz_GvIdyqbP_tSlAEA1^SBN$1YV6>4+{%_vO` z?p+>1!H2G{cY)7)9n4T|SO&?kHq5leH+xlj9NdOQp4O+h!|>t#S6FltpgRaT%o7`U z?x+Jupj7=V_j|zw+08+hnKWdXfFqRG2zaJ+SmfUzJgC(^A~cgO)b>1%kH-W5SXO-{uQyc_E;ZW();abeZ}{7C>ssu}KBNk`{3tPg3f z_^A%TG4icvW@b&@-KrnuW~QgFdb#vx9sD|n0s`ma?rs7L4#nuX#mH;yKZy5%Yo@pK zc^l>TnU`8xT9T~M^`P9mte}S-qTH4_sL063=y;gT3s0vJ3Thy5#7j4B+;BNsDO;L0 zUv`zS7H&W~qbgj=%%xmt5E95-+JmXwt2mo1u%|L#rP zn(H$8`SwPFkR2mMhBB^cBAtk&-X&^Ct|M`>j zth2pcO?Gc%qu~`c9&>yf31FfhG3=q{?<2HrZF$(_-}{G!5yCzqr=Z9l>8dQA8m@77 z#t1xn_N>0S`TpF=pLA2FT~L3T1=YrXf3Vchh;vy`A@&#E8!s4H1snuI85XMOX%jA# z=}hgJ-s{}lt(u0O>Sjm4u07zHzzbAcIlHPLVsxHBaSen5PK>lO&=kJez008$VdUcC zLN7pbP#QA$P@DDpmqosq#ll%}3*gq*W?2^9GcFjA+nms^Tvk3+D<+F;m0HqVViXOg z;WIVbS?o1_|Jlp1(%}ZOeU}F@7eQSU^4KpLj7n%39YU9}!%Cxf9?}DO%G%mG;jTTK zii*m&IR*v>gam}dzflD3V@}0+#VL|bP~rG?jrW-sQVq3dvNtz2e!FDr(+&2FL`4vr ze9iSKxk6tQ!FKg3PKjwpBdmTq=nBFdEdKuf<3D3Ll&+JbJ_)D}{zpeaAHW7Mdj341 zGg0JX`|5y2y!~ZF#1V%*>C+y)1wCo3|CN45aZws?3v~^qRdeyhXw~r zf*$hH-X2g(ic?={inubr_o>Hqa)R~`f{G??Caoq=e$XoyE??FhGKjBzF^}o{CH~cs z^F*RA!Fkjb4^-0c?d>vlcDx|}e*5~l+DVr~LP7#*)UeP=UcY|580KdH^Yf*oq}fMj zXXiP{X;Awx$1^iCMPI-AS5(}E#a(RLL5p#aAL_C0Y6+)fiEo4NV!NpaiLHD&Tq|UZ zj1k|y$&gb~NoT9ELI)WtwQ8z=h21QQl)m@;7Eu7;0Qsm{Wdgrg*4EjlI1T*tmp9VcTfu>k2Mmd-d>a8gLf^ z(o*k-_TW}@B}*_}c@U-~QVQt!8%*2p3|T7J|9#qv_qqizLsK&|0W8viM&F}Y)^~Rw zz~+kCC*%e=Yqj4 zdCtoyKtf@c#rGvm2BEh1ir+aoJ|3T%@`Ei75;HcF_i?d?<9urI6e8q;Uioyq{uSuW z4Q)@t?`aS5qWWHAy{5w5fE%!*$8X01LwT>1x-81$2|$<;$S z==F_`6hFLxOdb#cgfxjBOvj*i%m+U^xx4KyhwP1cGa)#7cw}S)8W6wh8B&h`gUP+W zgAd4D*t9E&$i&11b8_Lrg?bnhF$G0HzkxduVA1}Y-x&n0=>@EZwdUF$sJ7(-)4YsO zmUu4x#_1n18-o^wNJn#-2%w@JDXnl6bn+O)_LHYYuf7irw<@$a;M7kNh%x)}Wm?FV zndWHs1Qg@YU)eGEZ-3sDd%sN>$IX=MsGlEF%PyOdo?bsTR@a%~h}IXLe4kVKXV$Z1 z9eu;v!!T@Vj4CDX5x$8keon5>M2UO7$iRdhf9$X7$I^qaQ@$ajuFLyWm3xLvjLXXU zPZYqAxn%E4us5Ma8SSr6Jd&5E=tg_xdX*H^aD7A{+INYucT(m?$He$StDS;giSE+W z&_IBBQAvqkOpI#GYPw4D@v-|r@zfmyZ!z31@hZ}#{xcuAfm7W=(w3I|-6(1~AW*0K zQ4qcE!(=;|5fwEKs$D3b_`+Ne7_stdFxjUJ`GGj3@~KMUOd+YBGDb3c$0zW#8m+5E}jG8w`SY&i&@ zjBOm+nUL$|Rgi&~rca>F+U>7DwJWu-w9FfnG_!j5VXx&|$WU~Gi@HZa&J#&>^(H1} z&}ZrIzLc6f)bYDW^}aM+MjCDr#PGh!nK!5FFDof=@bwr88z;_c0)0a-v+%}AW>KQB zjEl6BQ@2%~ooeOrb`11f5KAuWOTa~`$DaG5*e`+mc1W<=QT!fhuto2yqaC7dIxG_vh0hjm*up*$quB_ROWT$&IW|SSP0nC0Vf?4vyvJ zJDPu|Q8^aG1P@-4eXi+3Q5)uQAT4?Bf===e@+hzJ3kyN~-W7ahyba&x@OrB-sp$k8 zTVEFC=XQx~6s@G)bsp95T0J+^-MO|Cct;>tPxTO(L@{h@#6$uSra9iwE8A}9J?03# zs-WV&B5swC@JLCi2s`M~NuZd!m$dPuI&XbJf%U#$bANwHxFW5N&Acfw#x3J>^P}Yo zlH}?t#)Y7Z(ecN`4_ZhssGrL@|LN3O2>(I5)nPh|Jsp|c!ANFy$LjhJ8OAjK?!ILS zmRZN44e(l>Ob`%c9z0+b5~3DyTF%mA0!><)SklHyzgIVW?ytWGSzeecPJ z_n9+|on77daD^P`(kVGP@8cOo%H!U5bZAdl#r!F!_dHGJJUo`reLKw&6{{4s)EcVn zl7Z9mnN;nDi`-D<$B*;~9s|ioQd^rwR8*9hlr(kgrSi&SS)BWatNn-8=XIo6qd&nW zfNsn4S^fhq9^OhVO5pjJoWJlTRureu2!JY=;7tfkSwdE;SO@_d-NMEf+xh~g%U{~ z{rLKo5{D|5&l=w?SVhaRNxQfRLHYGz5OD(ied@x+ix!(Bmh&XiyA~b#{-EI%dmcG~ z%VzTT%X7EfLa~}a=yW-w@8xzL^6PgC@FV&YNGon-zu#U$fvL+celSgjIqzg)V*&7t zQ@1p@O}#x)r0jkd<6RNwW^gZp*6Jeb6(u@=i+kDH6=Q+%&>L|+XM7FW5)mJKP=45DCpAzU(I7&G}*_0Yk!6!gD4Cx z3mNh2t5s{0e==l+>?U#86g~!P8n~Y4Gi{etP>2{5fX3`H6)uRtcm&`+$xz7@zRV;Z zHa>1t?9r=BcoAeA(73<^Lz=Zlp+UZr?oeQ9;ayoR`>qXI(3uj1$sMuZp-@buNEX2H zrh)4W1!#s-Ks7rSHBm&XpsO{3SilP(fwa!x*RM(AALSK>=YVFryu7>)b!lo4W1+KS zrMjy>5l-+*@89d4U{6mh;(pKKV1_BQ`F_gHf2P$OR_ z08CkCP;WqA1YyW{@ZYRpw}qI9PCH36!sKpjn-d=5!jqcIhj_yE2i%I(mD=e6)6>%r z)&#ad>X?L@hUgz4$h`)mUD&x897uX-P=26~jW<&nBJjy>ww)%=poTW+5rBMNiQ)YeQw15y2K@lp>3ME?_ox z7j#U9`t?%t@2YBm}8$b^L^Z@aM@r5{SP1> z7U;9Lz?cU<_^YxO(f8;Ct^I?7@DO2@jO@y1E6~=VLI0BxfZYQydU>p_F2L>8ooMRE zY;%H#N6L#W$N7ojD`m*AqX%z zpzt8@<`KoSv$J#39^_s1A+|er$ai;l5fKaC3bl35Tez2ogaiwOaj;ep8Kd0mq%y$? zo+b>k>F-|}{Yr-hz@*6H$}j8xHZVXP5D;*kiz_fK%_LEGGw07|LpqBlAVj<_>qfwX zQvhwc0G?c5Dy$RGG3}tKL&R0ko+yq|0|h*D+iESU3g6F93GqMtka#DZLm}%^>8F&8 zdW8NxZ;P9VQ51g#HABF1z(+alL9ea`#2JvT3B)|DeE;|R@RpXvsQ|hhMYfpUs;gQ~ zPNjooZm9g-Q2OW;FacAtvM#{=?#yN7ayOgVQRnR;Nb8Lpgg;QwV31LTZI^{ zJ2i$oAf>@_ZGyq_nzRtY2CN)i!7TK*0LdbUTf7OoAYgyrgks z5@?Z5_|?U=y^doaN(zxG1+HIKc$TY4E{#vYp;fgh4UhV@YuAdte7Of30gOlNuw0hWGhT^7M?1lrLXM0nFdCwYBBjjmSE2olA6HFZ4dS2+~G? z9#_!4l=|LW&0?1gW8O$eZvYdxP@#9JLN6kmPEf$LJt+qqfRv&lVpxFC0mwkpYNpJ! zZ%^RWDlsJjdYYnkGaBSVFc8o+HPE2orQRe^ivFRl+yxjvhTag4!EFnZpKWbV&CT)c z?d_-M=1e-?^M{ufmm>Khkl)tV*NKxY@@yPO_GXuJ#UoVh9TH?0WvoULcuk0ji4ob) z#nm;NXtPiocNyUP0vW za)UbeUVjfUGUVRfKCiB>j&Px#pO)5%&=jK{U4@o&&%l6DPhY>cp!|+0^hIO=Gm3j$ zH%RsRFROz)P6PM{JtNQA-Rcm-%jCID0`&9NC1qL(uj;}52l7@pJ=kL)H5#=Vu zE$A1Vb)TQz=h^z>9y^Ry{f|#EkPFLY-~ska5gi@D2luFtDU=#x}OM6CwhCt>@95 zzWhonM@N3}Wb%5i4`%7YpF_MT z1@jX+RsD9$RZZU7(@Ysxsew6TLY;JBwp%58Bw8m|Rl*!%2g&8D06K#+Lp^uj8 zcv(IFk3hUqF}U}|>E%mOCS4t!)Vw^oIX4{@DJd*cGP35naE=aU<@b{2+A3!cWN>`2 z*fBh2W@eCjv6a_g<)k)V_zK`o=KlSsFcw7F0S5;rGc%5zkBl=eBG9LOK79DlIdqQGJ9)0Cw{uUYy0e`9eTaoWZ>5YDD@ZQ<2zG zOzR+JYG3=eZ#d9!9Fqklb+Z_bmJH8sjxK!&)$gp69tLnco(c12+DwfmEx zXMzotU7UAKHf|gwem`i}=diI6F(#CX+ZY7U9R>OvUe7}ZWSc`@ftn=g<|YiLUBM2a zv%9-H6w`*bHrd?cjv0F^*25j5z>LHj%Qc)6^CL8uuy1Wd?5Pg)JwG)u$5z&DfnY`! z_FpJ22u*-{`}y&&YIwfSxqtAII zSElvL3Itj+Dpx@YA~;xPg@3&Iu<%~GhpvzN-5DR0=)^F71U92SrStYGp*y#1LW2DS zC9CJeHAjf5?A>CJ`@NJ=V7tCb*oqZbRK(8@0*Nqr(!^EPRDsOph;!$&GolQ+UWsDJ z^+!5*JrpiW3x)y(jXn*O13u`mFZ` z=Ghp@*a&BPD4jouRXMhsU?J#!qHLRuGvge)zr=b&^kju-M3faB@K16_qkw^{acyXG z@z!Itl7vcqLs{x~PY6fk{?E@hy}W%SI^%60L#+3oq|v1uwE`c}eDZ*^lj5&@&4yHu ziAHX*qJzqBLE;Bb*-JCzt7iah{MDB*J@FIzgd;Mn=uG6Z9Ldkw?ijJq*NB+>8j3(> z2#=c!hI-fv%~d8+3Zo*~?D@Qn8-IK3S_pxPtg;J+T=C(ykN?IM63lI|rexC1T`J9`lFw zd}s6YWJ`z$G)dx|^TS>FKI)fC*y3F$o9H6h_6I&RE@ZQ>$L}RRU3-onbQ;IA`c3tN zOph=G(pb??@9{_39bY;vIi!3`(d&akVtqJKnQ>@Bp#U^ivmoP0e(Rsw)3(*x-_}K6 z`DS36dOjn8DYR68rmUTt`K47oj{7Sln-ADX$@REiwdM-A>_5Clpu(h28IP>SeDa@P z(1gh&*w}1BeY#$ng7#u-SR~4AQ|T=O#88?_O<*WWzWA)0{z;^Yi>$ z%53h((>&|HjTZ$NXQQY$ti@z{YI(-fHD&?pX86l<0~Heqg(8;8Z<(`Bf4UN9yWyh5RRg9^O8=2KgG3`On1sF94O4yQh)j zja9sEY|9`1@}&qmBYpXr@G{`j55^cR(l zyIA3cF3+Cbg5CBO$=_YCj+2(@!MWVN8Qr@>FKSTyswVzgd!Ag6uRLh2VRGS%GM~=j zkf+Cq_Cdguh8Ec|{8H~0^q&o!_)~D026x?P%iUluBfV^`8Ctb6)nwT_B`(CD}7^Wsw;~_{s zVqBk|nm#|r$5FdNs@$qNm$(K*L#xOL3;a{U-$|Fad3d78Ob6fX(u#_4ElaSbmKJuV zr+~gf0ct|v3)vv&-DU0Nk*fY*9WPX`l0L4H;8b>!wM^?WS^(OKTnI4UMOj(dfx*GX zu`wpFvD|V?t|68;@S%XjLJ%hl1dJ2-z`qmbo!`7WjuM^=15%eKQ014v3NB{}U zj`hE0$%tqJMHMpBSn>B$o$?{W4vtC?4B+F(k40`fXiSOT|4Oi=LK|fe_3|{Ko2g!0TohgjiHv**UVP0_53$u^LE+igyW(*AgZ&;S;sV|g zm5-D%GWtY}>_Ss&HG2c^_oYj{x$#kPacrla2b<3^9t8yj_dpJZg9K^nt{Bh=6hRJt z=r@!l-k+<L0cavs`_6?1 zH9dbWgE{~<66C(AJ&$&q!DkvXa)sm)EPX#v3qWDZ%g={6T&AkK+R(2mXB%+-b7t)R ziyZsFn@8p4NpIi24Fwv4`RS$pS|)`{u%UA?$k_pa;KFP1Y_J7JM@JDb4$G5MoICbO zbZqQ3gC~?YDV<-;FhdOBsRC$6p%AI$uKnztTv2r-QwVN<%&zi>O1j2923FRl7tuZ? zyEym|_=NKo2;zr>4G3nNepxXnaIRH}U-x+iDPrWLMeZ_7oF%1v=)M`??{5Tz7SXh< z;edp8iD~cSy8oOFKxN6lEG{d1sH5{<=k)&$aUm|%`-k63wW_wLhzGU~eESCkEQls4 z@0?jdh-{$5>@qmTDN5I)AO~}>H8%yNiDFAGS)2j#IZ`@0v^P08>=yo9165T}s};P> zu>YRU#90eUT>PgEwkXzaU(R-)_%tWao7fkqC=pDJdR8MX2SdTuNNTgk@ySF zs5A@@>w$Yv6crW4{5k)>XB%`XS|b?9fTyv;dkJ7g1&4ruO^y81PcISG=Il8|^PkL|eR1ZYu2eK#)n zB3f$0_wSFu2}=i8`9|Tcy=H+P$*9W|Q3@#c_c9d{{UKsa086wt!EndUeyPvFgLcs` z&=$^kMelCY+UDqdeG!c$hs@jerXnQ>{O~SQBAXsqI}$wAY%4?e-fZ~&TN!FKg=u>N z3&dc97a#|4PfIIdAB&Nak`2TQ3w0+qw@MzTWvzz3J`IqFNRc5(am8EcJFi@B1d$IP z8ia3ncsQci6MzceQ+fG(%EaZOT+J>2`S~l2K3JhRW^=YuHxsW2i3Ej85DH=JHzw~l zjEv~p%&4{^3G_$qkyeCr?rSwcV2&%B=y=L@_dxEFDeB|Lu(xl?(=#(oW`4#R>zK5) z-%)dtmK}bfT6WVa&f<6P&eDNa)he4QW1dLC6Rpb~xyp@1W6 zt)LSXfrN#shk&i^^mmf&`QO+mVmdnHh!f&vgvWWpy=)Z2Sur4Vm!QJrFKJ+4?&w8b zls=NwfC5ZNCqM`@)>|8cQq!OXtk6ABcjl~Doq(;XYI2=ClI?!aG1~~7V?)l_K(zK7 zqD?U{Sk1G6;^ugrR7xqirsO{nK!eQ3VmW2xd274A;IXoF_a|a>_DLYmZ5O#)-5GCrDWqK{ha;UwWpBV10|)}#f$>z zJ4o^Qiy9+%@@<81o^Rof-)^iX8!LY=W8~|%s2790FO~=hYJF6GJXSjFNiyILRI%sK z8O2Qq&a#VKA)Jd~5Lt)BBU&z3`~3rA97w0p$FM7E=ng>}{|`UU<+`F;(k>_Ow({JR z_NEBfg_s;uxiV3A;_U>Ka z&w`q79SG0Qv?A)w&oQlt8yL`X0c`^=3BchYJjSNP^v3Uxd_vo#m;dtN-PY#4#PFo0 zK;O)6%QSj0!%_Zp&U<)cGV}yfkc+v=QH3k@;%rKl|GX;#o*=pF0~(O=>Trc{tY%dn z=u=4Q7vj1u`|B*YM6~`8K{wZP%ev+#k6Q@nAjE);s25yII7L-`v|5FrBPhB{Vn>3e z?bJ9pIQ4^rw4s!ojgVu_ja!|Y!-F_HA-xbWWa`x=cNE66Gw{ppTY;_I5);{=vpP4Tmmzs@+|Zv9JEF+`V>LHFa0(h<{I#p?kFElvM+3Ml~fpDkIx-_*4bmNzReFA2!k)ck02#AzPylHF?bwz&&L zSS{_oY4(!pLt3&H?ZW)EolNeW!X?N!JOz z-U;v>oD#(a*TC^7A3uLHRPArQ6tua9JH_$wREV$Qb#jP>g4WAxIlwbq?W)@T0S*p; z<%5cBF(3Np@a9(q?G=W9Fj)U$m#x#=%iN3QrIvrsf&LCcA)E$A(wAKECg3KZ58QSZ zuZi!1CS`41(6R}{;M+{ALo6h?z)*v?!ne@fyAyCI0PTwQm6zOlX#`ajgE<5XL5$-+ zX~KZKyjvS8Uzsl;i5f^EglulU#1nolMTkylWiMkKU`n zp-+qr9Bd*4`{@!0uel|@esHuFvSO)}LJwqQKt!ZM&Tw-5#{AnUNq(*nHQkO)0|2K; z%>S+`)15-^fO{!C zGP=Q8jMX0%;BYPtI}!YpB-#K`C*Z5*AI(Qb#_b=;?0+0cXbxlp$ z>J?Yn2^|0WzBp_i{5=X*lil&YotnBjP2~-la9Ux9>u`3NaD))flSLwD!+-OFIGmkFJ-)9XQ*h77_x}RC>(XHW literal 15308 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 diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client.html b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client.html index 974f2575b4..d53ff385aa 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client.html @@ -121,8 +121,9 @@ Collaboration diagram for Tgstation.Server.Client.Components.ConfigurationClient - - + + +
[legend]
@@ -437,12 +438,12 @@ Here is the call graph for this function:

Definition at line 58 of file ConfigurationClient.cs.

References Tgstation.Server.Api.Routes.Configuration, Tgstation.Server.Api.Routes.ConfigurationFile, and Tgstation.Server.Api.Models.ConfigurationFile.Path.

-
59  {
60  if (file == null)
61  throw new ArgumentNullException(nameof(file));
62  return apiClient.Read<ConfigurationFile>(
63  Routes.ConfigurationFile + SanitizeGetPath(file.Path ?? throw new ArgumentException("file.Path should not be null!", nameof(file))),
64  instance.Id,
65  cancellationToken);
66  }
readonly IApiClient apiClient
The IApiClient for the ConfigurationClient
+
59  {
60  if (file == null)
61  throw new ArgumentNullException(nameof(file));
62  return apiClient.Read<ConfigurationFile>(
63  Routes.ConfigurationFile + SanitizeGetPath(file.Path ?? throw new ArgumentException("file.Path should not be null!", nameof(file))),
64  instance.Id,
65  cancellationToken);
66  }
long Id
The ID of the entity.
Definition: EntityId.cs:11
+
readonly IApiClient apiClient
The IApiClient for the ConfigurationClient
string Path
The path to the ConfigurationFile file
Routes to a server actions
Definition: Routes.cs:9
readonly Instance instance
The Instance for the ConfigurationClient
static string SanitizeGetPath(string path)
Sanitize a ConfigurationFile path for use in a GET Uri
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Represents a game configuration file. Create and delete actions uncerimonuously overwrite/delete file...
const string ConfigurationFile
Full combination of Configuration and File
Definition: Routes.cs:59
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.map b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.map index c2b98fb570..d227cf615b 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.map @@ -1,6 +1,7 @@ - - + + + diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.md5 b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.md5 index 7605d48baa..544676acda 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.md5 @@ -1 +1 @@ -c86858680f19cd32500fc647a1261a87 \ No newline at end of file +805ea1cb7748629b712e42d7c00a7c07 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.png b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_configuration_client__coll__graph.png index 7dba5050b7342228366d59cae42e590f58f637d2..91e2c74405558af6383e17bdf85bbecc577e6e8c 100644 GIT binary patch literal 17030 zcmb_^by!u~+V2t&=`a9MKtusiO1h;{T0oEvDUp!w4v`iKrMsj{IuuY+knTnrq(thz zll`6h)p_n;_c@*&YvWvFj(Fc+4FeVAB=K>`aZo4}{$nYzrzjL!CH!*^7AE|ab@Ey{ z{0&oIT2c&khWszNE-Mm+x`lcyCamljzdqrlrquTL(w3##3nh9N)QH$sG4l-P@6N4zgAw|t32u&C3Qf`*iA{&(3dDgxQX%nqh7NA`%y~T1}0G-@!{d2TKUUs7>E zZY_O20CvoamFJ@pGf=SlFCW}rmoEKUUe??F)f=za zT3yZS>+9>;v*gpWM04j3hMk?=E8eiSFZFIm)AREf=;-LRyM5B1-Htfp26r3Qghon8 znKZF%Y;3j;U0;n=ghVn~-YIc%R#v{cw!WT{G4N;3b*Y;|CXN$-adDA_mDQ`Tkok$U z^g@NC_6G_c9m6B#-b1O3r(zNk%_Ae%jf{+9`5c&Xb8|-rhG4ADbT}4|3!}rsJsTSv zpY6OFEyG@3UY3%Rld!ga_*6-$*lEW|E)kcM>DIk_*cllavFxUoNgk-7rV9(lulwtr zeSaGq?EC1}BXxE1r%#`ThleNs=FzOS#U3iq@=<2E_~WzRTB>-^t*bbS3JNGbKE6-t z4E*e{N%>WuwfW1Js9cQ?HZ;COeYh;XySkELl!nT6 z!G=4lzkacBaB$==QnBcwA`=t41?8os4Ky`1xfiaHl9u-5<>r#X+*R#ycnIn?qG;LJ zBHa?<#~#NxSaki|+1b%B_YfDq__M*oBco&O*CE5tthe30U0osH+uQY%gdDd#U)$O$ zS8s&6ro*mgb$54P7fd7)k*Xw;q^V3-W#i(KR8&;Vb9(#sEekw<|FF>uT~ST#+xIU^ zJ0orW{{GoHIf}Wbsj0WQ`S?nE3|AGSeSFZDoM0Mj>+1B3j4&y~E?v2DY8es$p>Wxb~ug^~1GFFl8hV0dkm6cqflmeqKDK#`S<`)-BdTgw%b&ZXO z?(L0@j^1Ww4s&DDAVmFIT}`X5{^)i(-Q+`J$eyG_PDJE6Qff>?M|bIRAcmo#p^1r! zLadH<9IthK`)9UXHrbK&ML8YTtExklE%!u`ZqOj44DfdNM(g$Q=!XZI7{ zOxd{BMQl7gF)J$;UlD5fg?=%Hr5}pqmn6q?fGXb>cy_O zfX+^t{-GfnMn?P#7cM**ni#Ai7QyOuy*mC*yK7+J8nQpE7jXRXZ(tf27(CFfjlH6f z|GX4)V`GDi$NG*$=uK=wLc&klb=68MI;BSVa4=YHe<=i$v7#|I96z+Rvm4oUJKCQa zud*pBD)NMrKIu3Y!r8&^`iJFNfo7ol$={}q4&1r6u%+}u&8o`=ZJ{dM?IXSCW5mY6 zX`cTcIkUQo3t!$D&!d#lSe=jT(9}amJkiQ)k%{-2m1QJ$OMb&Y_P$5k)K1M(KpOx}Q8HNa!;ydN` z>C+R*Z{P0;3kzR&b<^<;3{0tx6UU+!J1?qzGP1G)8xvP%ChGz|yHsL0YzjUyGGfK> ze)y)vV?B29mx-6Ej-9H3G&Z#_yJQn%FV^<<_KF4Ca?0Dcu)GHtIXR<0`X2gaEjhn` zuk5WyW@5UXoR^0mDE8JL(=^E768>ynN(;P~x+s*+-dC?)C8wo*VQQ3rlly?tSX8p@ zswW4E(i4YMmcdX#l`g_1J|%^6Z7tD-*8C34DnIPVu>FIEGYb7;wTZ4<0+KQH)Fj4!Wkk^=*p|e_k%2`%kJ})O~(w7y+iAgX& zXX#uK`p!(v~#y!&{3u$i8 zvcEKIvCd+M=OguBN~*ksg1NcHq4I{$f$|pL(S@fG(7GSfx&*s*cu$u&j%L>8#%d`etV~CggQ>ler(c z{VgM?sv;cG<>J^AeE9IKf{Mx|R8a+I>(%MFZC!SI%+hR3<=Ibq%aLrmZ zY%w3`tyqfBL_{zLKOJ5$XvGs#R(_Nfbu|B&@gZB#@cB6Ue&Q->X%WH%?rvJPW7d2L z7&G{BOYr8+PLuaBuoI9CETY`I8*%smtz%Hq37^H0C5f$_&-Jsw?w(yutrkpWh$7PH z1s)q3GM1ElPOGi7Bu&lCq~zsQFDfO zht1WiQj({Mjo&-`u{sixbj~-0Z%xgCQtC@2tMdH0&!N~eGDil_ZP(fbQ185z70-O77gZl_Mr!Fw&%eZwie zklCR22>IP^A<@g;o(CJ>*jT!oesiaXudzKhsX7kktn2($#023;xExT~F$IwXHle?joBc5gQq&uPw6g#x>!M4+itmppaoanzR2RJBG(~u^x8mkAZ<_HXYd1 zC)Y?dxqOg`|Gy5oV& zr3>ColZ&vZ9F_4zedy<7On0Hbmoxq8yUUUsHZ1=w5-uwVr?}{QH{gUVF0!{vW6hjz zW2u9?mhTT*@h?1)PC^S8YF}M>iOTIK{rw}e|F*teV&}lLi`Mcz7hM*{(|bPkZhI`c zp4W`oa0m(c$gVHv)QtFs?*klz8oNmYKPFH|6h^6XAsMFJnFZ?1Qf_{Ji@eZ@ufIR0 z9hYCYg(Z|pq5t5`5Kvy%RHM*WXK)T6O0#wr49bk;ohU z)6>&kd`ZII%0kNE;FsH#4IlI-syRIBs~_c?-%hWTe)6OlI^=w2uRSx=UMO+@9XS8z zN6RXX{0lMGPwz5Qi|sf$Ipx$~i(vz_@598g;-%WlaaKIL;)zxsntHlg&wKYXJ}xeL zSX-J$*XhXEn1jT@%d)}0g>3B`!3NdV3juH6;sB<@@xjFRPD!DL=FVc+e)Z0sJGRdg z5*B{S3;DtO2%hXJMTME?%RBK?^7Hen&+Cz4iu&WFG=92bJIEos>8G!)t&MYLCoz)n zy0aySly96w9FT<7*0gZvM-E)e+HFi)`NUujv%y<;?`CwYtl?d}coB<`unqe1c9_R8 zIxfkB#tiA0`Nj3l)3irbHg@e#bpgm2v<3^Ssi_%X%_1()u6vuEO>f7QHWqnuw_|SG zdE&A@^y~ltY2AqexRR2R%F4>qvn@ey-n{XwtgNh#%L-R!5QhqonwnZN|Yb+S=M`_VBe(&R4@IGc&VSBPADI z1*znd1Y5e|xVHC(biVxjsWj0QDNTR%Hh_PCJ8HF#tk94nA|ri>&hlC2vBSoA6GDoDNSQ@$M>LL&-MB<>)6>t-ui4;*&jwJDPinD@S=Wbz-SI!6zr@p0~I6Ru|XQ^zq@2<4zaXh%G!R zU!!uiv`5fK?6$D3?k)SZVJ|^wguj%WqM3C<;Xhh?kHKH{Ti#xa@A*(M8TR+DGaTyM zoSY=p)zxYxFR-%`EB-!)y!@Gl@o`0#g z%&pa#NKkvwnke96wLL5T>({TwsVSG$uiu8ma^qIsb*Gc#j3%t$7kR~hYn z3z5l&9f!;nJnzSkAD*N-uEf&O54-@pB9m=VtQLH7bU?$(N{rfC$t#@oAr$m1C}4`? zVrRcP<#s^P-qFEoHT~#;R`u2H?r!TIW)6;PZDCZ#Q*ld)E(BL-Wg304uMG3!eVHD! zY&fr3xw#C%H@FC>+;)j~_x473g^ssGfnbZ?x!m;iE5?-T4*uW0)wbyE`R`Y1k2Y%l z{9P^Vt+dq1=(x|wXxLN!Jx0eJTSrHyd#SLdrlz@E+3-mIgNH{mRC7kA?zPr1&cj)M7czCYo!Tbjcs`~- zGiQK@#qXkDBG7FOz77mgC5PY1ixK5|dbos~EY$QDLZPpdbuJq7hlfi^5%1o4_xCHm zFg7;mh-Sn&N~y|G%9@?|67Xm0?DSId>*9@)l81l({E3SN6~7`r^pVzN{o{IP6070L7@V2{%_{G&U&Vl@iO^pyF-7*z1lN?S&%1Y* zkB_F#TAO?>ZNn+F>f^GU40f1wx@6oPFKlbe{_x>L)bX`HnzfD*S1@4r5-4TL2)nxS z&(6;30#gE}P~jZ>$+F>u1SWU5!9!?&b1H0nT+{Vvy)vG^;p~*peq%fk1Pw%57%nyl zXx?#|@gpsAJF*|Ice?@g!g>b;4m|kUub~utR`cJ8rS4pI-EJX`=dZMw5P{;xq+R<4 zR&%cN16%wqmB%sh6*@Vw2KN(XhC(M@T=rAp-bu^O_c5pTvuQ{3nS#gCDb0zAiHX}~|H1h9I0|+B`t|t3SRkBg6|b&m%6#b44=sK8 z^Cl=KGcz+@aGLJ(OYC!yl9C?(nF}L(_yP+xy}v#d`_K@_+S+=!wxy+Idgl=3xw7&S zN_cX0wYuKPSfyp#^3SY@gamw`#js5JT^~8tObTjh_~a7#17VucQLs^2oOey;mzGNH z7MGU3^!5^?!lR=6hIBkAkWB>b2wJM9me%X#zPsGq+}WDiHdR$sWNb!wI5;@Tg@s|G zqw4Va^k})+8a+W+o2auh@0dJ@B3s+rGrN(g2buTQVb3A&DVM$Icn zEQCpL^9^(ro4x@28?BUuGCK4&`Q-t;LR@iS;}!N@_ANbXW_wlD)w5p%uUI#%o2;I! zJb}JW6ey-vX_3(0)*ctl0s!0f_jfugD=QQVA3NcJyKxHT*a0j|fIbM?qF-7iyOIF~+`4-cocJMgN)ERv=H?Am7Q-T6*AsDIMARuHH z6tu#|puQZ)VzVg28TROwXI$J3mo$o`9%FPrTG@tM7eHvir=+Chc0c9@Jmptx&}Qu( znEO=Q2D%+gKMM;BDy+@N%j<&JZDM_WeeS~-N8ejoGQ@?-4foeZ{+t|4ZNvX*peVx> zSOoE#*HJ+mW$ZJ&_mfmsxo*(;-C)5;!Jq49`GP45!?SKLQ`?UQCZeLEnp#`^-I$KZ zOpXUvT0?Hca#>OuK^Zk0DkxXiwzubOZf<6A`u)Pu$!R9Pa#G~^^XD~V2FAt#P#NVC zGaKS|Tq*RG z?&kC~%GlUgv)=Xkqd6`ac=uP7DjjCs05UQJBQs1?1;5{{H=Y zcnOX|ysf;vd_4DSwA&F_AbBBjVI1wI04^3*R6?P!@f@=GpM%(FopqmH^h33VD67O$U-on~ z6G7J%GO{;CMJ!jZT?4r+yXEaoFE6ie-@bXOJ-|eDQyh_ykbLXxoS!eu&GjxWW(^Jw z25pGN`I@N`{sJ2N02x20(Xt)1p5Q=Y63) zij2$RW}NX%vmdg*9ga@(m9b-BV3eu1nHARg!zqQ;y96>=%Nt@EUS(~iIFPeg zp;U77@VxFzNJwC{o|n2Q;QZc=5fBMX1pR{tVkRc^#_ZMQf)VA`3s2zN04|DHIBRUS z1z%_5UI#QfbF=V?PMLjvxjlh^mho9|xgkJfEF2sW3kxPdG{ofO?V06Eqw57Af^I_t zPW$u;4`fNk#e7LIu}8OvikfQH_6+s)(Yzite%H~^z!eo0MO4l7bPTBWOX(eanNBYD zo0IqvQWz0Zz7sVLdHiR}_B?sv;Y0upQfg~&g3t+Ng4=2u1@xzN5wxl%Xe87`Shpgi z!sFtCV7V*Q+rCB6D5|LVL3;zC8&yzHK*1ni(PgvP*+eLG+&I}tOA-_j6_wGi{C>Zm zQM&g?RNA*ZY}9)!w7`6H&^aBHUBv_D%&|&bV&d22%(XJ~0E0^-@837~41)c}cgiL?y#WPSkw)>p$UpFe+| zo(mItmP?>gw&xC_DLFbOUSpBom+9r@z#(my5Sv;Cx8Bk6!2z;+=rfj4pUdPt3tO{N z=?}lN<<*pWw2qd%8hIE@!AAmmX+TgAuJKzL$%2UuBLL4uWo2KHB6Tq3K?Q}%FSJ_+ zm#psicZXVyJtKmY&#}j0f~DKie>Y=yVA+|`CUAOFxSY409hb}q1f~%Z3fy3 zphG=q`HZJUrj|~!e-AdpfR5Y(k=bUcyCu%DJ`lRs%z&zn`Oa|Y;KbmOsB&7xjdtGZ zOjC`vJau>Dn5@na6(^_L@!S#K25s(=?(WzB?)G|#^?}5$)0!!(6jhMNx4d#=8GMp| ziM|JYH-;WOQ;AE8!&7+o%+H0Cl6py#XNBR;kG>h<(ZUzd#J7ZT`P8t3Dcp5{&YEr+E`s(lf z!rJD^^5OL4WK=~(1r(O)T5UM+;z1-$md>9s! zS|Eak3V%XovN#_wX}URC=eYZe7;sjR!=`q(?Hv)RBfC*r0<9ENMlmqbT=z$e)rxd4 zoC5Oo){A{6=|7)Q2)isLC1va2z<4RxeEc(UQ&ZF0J89V$L#;2!BsR20bF)8vYVyOa zeEs_c4wFu|e>4dpK7I(7sMH;S9iunK-@kvyPLl``adQ)Jb#>)-7`vQ`o%>`ZBIXa7 zg9n48sl>IizgxHREsu}iy*0(qR01XDfm#ttznBaF)0c+RJ!U(uK%$W3!Rd;8r&8$n zYwuX4W0+d#l3e>Xs~`13NP!2(0**qgKE&|i<*gQ-b!celqKvm{ zbp7e0&X0CPj~_o@GsDNnBPUlsGrW6QpeamfQn{*rV|uY`pE0fPBT}$0U2<@)N?-l) z-nU|#jB&T{rdQ;qk??!P3GRjIo<|k;id5!j))`EezfpbRU>~~ZA*SQk_ADlFd%9K0 zZ`sy$wapT=)ryLq`mAKuU^U zkgIuqNDf}0uUFuAu{(EOc($P{A8L6V*@IwFm0_LW8=jb^Q+EwBM$R|8?DYSPbF*QcRqbnij`}tbmw2D=uyxyXy+Y&pOOp{YI&&YTBZi0fVSyKtKQ?;Y@$d6fx-m z-@A7u!@AlG<|i?+F)h) z&Y1%Z4bAG;4{AS+HIDp{kuql6opRQw7M9D$J4wvbNoTGXKx+NdJSz)zfhJ655KIr%+Wz6=!Q4x;_u#}>TNftmF`YDAAQGUzmEK0hGv5Tq`YhA^=!~)183Zk|BM+*s+*vZB6C^9J{iR zw7|@l=7)SJDJdz>tSmY_JUlSBePOnW+c-{`kJ`pT3t}-JWsi-E`vN$>zP>)bZgwnJ z{&vJIe*T+q)_}^l7$>>4WXL)!Rrbo8aY9S0O3E9kHA9>e@l6I}M!5R=^1%?&(!N_#Hu;L-=+?w(=4nKw=`I#%Sgm zeBLiCe-`5`i2(L}kEAmgt1yqxuhihHH;qJNW@hg8zj*K7Jw)R`Y7T(2X=rF#lZEXS z#ZDS6`m7)|4&#gK8yN5`|K`*;Nu;ErBJ}X^u=hbApxx@A-tYPEj;r~VM3j`>OiH=g zJs&G`Xf|^my5;_It)6xMm91TxovX~y;)Q-GJSnMXBuITiQ!Bst)ySRWQy1>;MEsUZ zRKaaRr!y%VM#LPZ>Zb)|Wmh9Bme%W;O@ z+tWiBM8fE;KrbdNd|_>E4J5 z^b|^E*48!*dyf5OyOoLbvwXSdfC7yjR|q{dsJdNRx%DUe8o=8P&Q4DdvBTQNMq|zW z0MstMqy6>U{73WI=!LUQ@%_iRDdxMaTdXf@D z`5FP>>E03)q%smJA?y)3}OiS~DU;!{YBG6OTzUAKr^~M`) zY&cfB`ug6oahwPzd@^(Z97-P`kJanfUZA5Q+o=pDMIU;$mA$<`d{kjM6$-e`;VCnb zQ<<$}k;e$eoS?nuYYzcCt^oR!l9J1iBFSGwv@+tWS23WUtEsEsrl-ehIR1@G*VC6F z4R!~vo0}UFI2jliK$KN5c!!5dN=mrfFfLwfYHmgYR)fGh(8tfRm5l*r{Bacog_0GT z)p(stj4YqcBA$qdh^~Qw4_NPdz*;~}Ea_|ONQ0>X-upf#299lROT|nHZ=Q~dxk03k z`{6|@_dnE~U0sN10c&;NF8)(;@~v*3n}V)nAR%}HTLk*Fb$YZp2eRjrn4y2{`+sJ= z%lC5ii2p7|KBI@j@A=0-3U$Bun~X5o5Ojdp5ztK%8&=)tk1kxc^c}7+k87SA7%BYS9_@yQa{IeGgJBs)DezKJVn=ZJ^J^N&Tj1a1vBtp0#^!eW z%bxx;r}@~t{PDnf!s2|1z^Y9Tz=E`igaoEbr01H>o~-oLcW{CJJ1>q}iDNS+01AXK zyWhX9LE|qm1+?}R^tbR29|A%4PtVApzI7`Z-B|L?;s^L)_dhe2{*lFnACMrP!|GtZ z!H%PgivgSzkbdCTaGi@o&5~gIXsIzFWTzC>)!z!Z?CXL`m+&kLlruOr1h5G}_W(##C} zlg_H^su3VEl97{(=;%=0y?Yl)&d_OTCvjLRNp>T_p!E9%0ii1#KQCh~F9YD6hU^J^ zy@P{8cNZE=i-7xaY=XDVpDjJ;)lJ|{aB_00Romt~+0jQdI>Z;C5~5U5Q9)=|90{Cb zMn*>9i;x>41fxy_w*OMX18VYQkF!&djV~e2O<}_+d3kv`8MoD~CBi186~M;(1M~~U zj`J?(QRwWf{_xnCP5tAS9Uu-AId1Fo zyZwD%7x)WmQY(n5icg=Sfy)MjQ+;N8Wo;<4#9?zXDB7>*TJbpr9z4K9 zWL_xFU=_WEq|x!F$C+7qCwNZ=P{!8EN}=1@EatbbmM)sFd^^u^*|zNcP~^|NLYwyY z=m_0pJmD?-%kdmD=fb$vF^!%$B9-1Hqn_E#O+qmD`=z3*?G7f=*kwVX0LtnEa-Q6T z?6S(cckh4%V>)e1xMqcnYu7nPofOd`tz=*VLN(yHd)2ud+-6}RLJ-8kMhg^*{)s!c z)0Dh5goGNp=_S(O_2)8Tw81+D^EKbZ%p^4s3Llb|1MdgkZbs+qZ%lQZ*b7^AE9nam zTQR8*#mKw*OYjE!-IP_?1m0^PB&#^Ey+JEeM}uTm1_z;BX-Nr--HI~6BvE;JLf}Bn zpd!5It)QvaKfERK#^@iby#~rEDPj@9Y9iC9{Op;2Polt^kPsDzpAc0cJCCXaLH_B| z&rDe#h(F56$iS}Z82SFDZB7qRN>N3HQ6))!Vc}9b*YgH<0XShrFTP&}A^Z{Yy^>+i zxUKKId;&-G#fv-f2?-Er^RKd5O3lrE8|g4r?*?+2H;AX&&dYbfdWcg3;XI94Q$z|E ziHu4y&n_(m06P_?znTi16|_PmHcHpiBXkxFj#_$V=50Q{8yXd_A~Yaof(9^pY#{XC zg#&ql)QXfIJGDW^^c~nSE(OecTYxDl@!>F7XZ=b--bIKg7yFy z0l~34Zt0>zj($QhBL@@a)g)|l2x6ih+1YVCcxT0h^th ztFecnpc-3SF?o4;%NALwpD46hdjXMEE7regG5*;LiWkbWqJopI$MXXEa2r@BAh^BP zh=#?1j3M`7)LkO1>BYrXCs})GQ`9S6T-1Mr4672m`yj;#CCvbO*h@2XYTu6TZn=%s z^XP2dH!vrN;XIxlHJteb24XWXFz8xXbYItsqNwWn_N@`b(1p-(z&BR_jhnOP3rD9$ zXmD|H#S3{*%E`%@Q_^j6K)I00=(FD!Y2)aZR*cp{SCrr&^Kbfrx?f)IN7PI#Z-ZoI zNOS&2bv`C>5)l=JROFlHW)Y~&`iq?(tUJ_UJEf+j5nsD@5l}3&M$HCy3fP8br4;|G zaNjErbGw3zi~GXJh#{eY1{5gxW{@CepNL&>kBtaqK6A&-T6{snlZlnOyJFl&Ao9lA z1UW3bw*v^NB-GT%z?9YlTmc^8EVv)>{F9TDTf4iy;2lCs6g>T7P-O`I3*tK<6dDA1 zh%JRIC3#Q+g|;lEfW+L7zCLfD2Ox*DnEbc}wUXYA^1f=pS2$bS@JOa47mc&B3v^Rm z-UVJHotpT1Y^hZKT}A$WHPneVgO|em1F@E$oezrWndPW>aKuB$c-2daM%Of_*wN*s zw@dV+4px8k$c3Rj7pWGiP6>u^3Ueds+Qo7rLuJwTiXnZSFW(!gifS_Y*;6rY>HNZ- zJ9Wk(4Om%v)qKF>6zZg;y{k+yj7dqoov8Wms`-PFe#I%D(6s3Dfo+D*w2jT0tH zy>~u{aLV;%ALn1cOogYEw;j8DZpf!hNa8N~;3a2BdqEQj&b*66qOHt#@s21}?mPg#yl>qt&|I$NmrW9Qfl_xinNZq6 zlwYWgJX#b_PjFaaLiUcY?UnN%RpiU@HMN_}3rrJ)De`V15>@+rJyl-E+|LP{y6#3- z738yi%LL}#P_WPPPf%C*_#9tUTC4W3Dz7>YUiQ$W8_6aWsA|!+mQ(l<89qx%O(xJq z!Oy4>uzvL%lF>Wi?+U!@YfJD?DymA*ty~dShv|&P2l+ArR^OD20wnv``-q*<3t}|w z>H3(44rU4;74cz7p0Js*90m#h=*?oR#iD*G4jq@;mwsvT(E93>qjz8M%l8xcUdo-x zVG*1U#_{#Is;S6VGLSdx4?Pc7J*hg_q3IW}@Z7_7W3~0a_8@?)d03l@|E)1L^#qoz z^ex|T?Y9wVQ3qz!?b-hHY2@g%f|6YJ#3IhT=kd)t@p7r)OI(R1O|PCm<5hzAj=)DeS7Jc=D5BFi#F7`$coE1G6o+R&Wr^Xg}_IRM_9dh=P#G& z1*_Jt!a2=DcOu7JqkaeZ#_0>8g|YOVbOjbD`8Y{fak0o=BqqB2qz@Bwx%l>-wyRBz zGNc!>1i%#NqQQQ`mN%SpG2{9`Hq$k9{>G7`s%JhLTB4&R(GYk&9}UyfMsv{|V)+NCP37uv7Y{$0rnm%0O_i`pCqlDS}!qfp}C<+rA~ zE;?E_P(IB$4pT`aNj{5bdPybSD0Q7IcDb>60t?yZtY3+uH~d-3`rV?lAPoA^--4cI zLi+VrzK81&BI@M^Z3I|;BFX|+=l&)s5jl?Xv*4gdVb*?a(kwQ7NKkvJI^B=TZ7+0_S8I!apTQeXSi*VC> zk%3DY*0$ISdUQ<~$<~nck3P&NZ08GGATkw1EF#6%Z>BnEFT($5AW#DRcNZcUCQK{beCREcuDM05tjAC z_-%_xY%B_O8bZOXv@6RPs&U?9fTlVy5lrdVHh1rM=(?dpE34M^SQTYu-GN*cGIkRp zJbZivl^_h9nmPqZiGi022|NZMjVW1K0g7o7Gf=~=ckGSM>{{(0M4j%U#Leh6c-?Iu zy2ikSFQp{sR&O@3;9Xg;+JR{!9zXaHV1jx9N4tIdHX3x36^leoK&~y|!WDrLA|@{0 zG&)L-L@ctB+~0!dkW=FXJKdREozcsr4BFiT&Cf(Ytsu<+H0g%%E)zBN1uzT2arTUi zyaIYA#&jWZ{bE`p64nA+5xO-2M63X>r0~*B-wd+bMq7b z%OZH@=pMtY(+b1D0poLb`RAqR=xET~B|(shlVpgx{qW&sh!!EN7my&hVJ$gqkoe!< z-=E%g1nWl3$%!W`5pG-{FSM|*zz~&{G4Pwl=;ZGnIC_ZLG7X-xfPg?SrGWa(xBORU z_mnDAPnC90p@H-w_k}`8u3yInKLMP_^z`(yKh^Glun{p9v8tiL&Q(siqi+qpVfn%m z->WMmB=kVN3=3jY5a$D5_Y26#>H-pjaPc5KF)?(0{sj^m0>?d$%W`qP%4!A;FuDS8 zv&WINdQILJae6HOKP85c@RfdP*?>~xJ=+z`IIzAqk!;Y zAp^DV1ghfP>}>OQ4WNO7VEXEVf}@`A0UlxJPLEIzIG=3b#_2{gX|?kM$?KW}0Qkbt zu(*ec-?5RKm)Clwv!};6$=Jx~A?(j!wFgFI{(5k4W^R7|D;9>gebi7qc!Y2XA&sRj z2hS-iDyOFWbhWdG^RE-M5sQ1F5*8Z0gQ_vR14aj`%E}lZFzg?~=(v~Z0fA zlX(gIU?Nu~@}vLXaST0bIzq@-P~9LWXSf-05e0TKP-9{^31R?_<&p%!v-Jf6UgCs- zb^#(m!-aE)u7Vp=4L$eTnwq@a))p5r;o8PcA@`doE70XY%8Twwd+F5mAucX0J6lT6 z*$je5QK!gF3#WdP@u#{{i5HRUFbfOCJz(+kEW;_{em#o;i49P?SR6JqIb|m~{?`?l zc*C91>H^T&dCWmb@dBs2ed#VCggpWRE+P3T*q5b+eB9i(;8x8EZ=zl-iS!AFrGu0d zmi_wZMbvP)87ZvCXv*&wPA)EUAk?h$N*bqUrJE_S<{6|c)_wdk)iHjtK(g^oE$RHke!+DbzP0B+5QDk>5K(ubQMgSQ1}XwZRI zqPV#pJy%n^1(%m18*+1T!1iT%_|W^&twxadKobr%)z$`w#+ZO-r{hIv2?NPHrMCt<}To||;D;U=VQFLRAa9NLrmzNys9i;f6 z$b5khqSWuD2-tNz-91(K0qK15^g}Vf{Z1!e@59%SYBdCtF*Z!_@0;TCazjAO$_ZR4 z&&>CHa!>k63>UW^-@)&M3l_0IeyABuH%0}A$kH1aTxmW&wsFpD+q;?|uvC$m>Hh)r za0AGNg5*kct4hrdGS;5XpL>LYAJE_L5EC0tL;{XLul7+0p-$pMM>4OSJU#Z`jV1CwbUCWcQtbTkhjVTzu{3-M;8}k>)pH={g9KBdQAudKsKv`jIARhRUPeg zb(hYS0!D$F8|_Gp7_56{>z`K{+xq8$BT0Z54PH0o?+^jI$kd!Mzh8YMpK&!^W}@W% z^HeDnht25e(w=UE&R8r*Cv)&29@F1J*-KAit;llp+*PpPKZ{s-7uf75mi~j!o z$SEue3dqaMKq?b5aJr;@mL-rI+RuywL7=&P={9>-&33 zAm8|NcGTOgk{k^~#v_i?+#Br2KD!QH$E{ls5d1#*p#h?i(7ni$-1a~GPQXa6*S--z zpw+rP=P?b}D1NQH6n$V(tCzX#kxEWSM;A=afwQ-_2f;t#7cbI5xdG%A&eUu`wvnI( z3V*DiTlEtz*Y6fCxhA;%-ri`Sd--djNpb4HJONs;0*nb!Ha)ltiCmkEE6mKq{QLJW z$`iytSfY}SBQi3wWU$(x(iy_n{Y!qXp@{@t;Lhd1r*<8QKDBoP00t;mM;`4B8PDvS zcOkeuc6XA4*Aj;)WTYG3-QxYoytj?DaafRY-49!4~k0R$AykTP7zzBk}IRW~BFnr_qc>K?b z!IkKR>1ofMrJgU}zdvqCJd|#5-?OsL^^E8x8n4%SBj{G9s8gtERcjK#<&VD_e>bCl zd1W9E0^~0tY~QvlRrJIQq%I=azKkYw=nEyKrHJlL4JJv$>EFC@S15*vObN+JEEMB@ z8uc$I1*( z5WCFlB43!8vKE!Py#4$+g9xYN^@=i1=*_CP$Hx!5cQKq!_qmK^;q?S;z?_UYqs3roAz^(;1uNy$E`q*qhA z>UsZ?OgoGG{QT`nmrY@i$+#@W6}WSk=jPD=GyC%g>J88GKPtpmBxN%@>m4R^8Y`;- zv5)bI2_*3oe3R!gS zh~P<@oVbS3qIMh97L4x#i{WEO=kwq`c*E*?$#u+|DG;1qoK#hYz1F|4nIrXODsq z6j^I;Z$$6VkTC>*zJNYoY+7>0PQiFl0*iX0uT%pk*ft |okR zeNX_GV5;QmFQ8)#vG z(%k9EQ9Qq_7vn7tY<}cb6RO>;q~Fkj(b%G@aT}bCU772GW3yGlVD_Y z8;E!G{R`8)4D<034XVg4dBzsjW~N#P6h_JzA>15qLqb{r^=N#$GrP7H48pv%cmIzc z3Rg);Qo-XKFkBui)CmKx6qpdA?-mpg3>E6c)ZnSv$V!hI8X2Y5*HfCBnqpyLDg1Q> z%@k~!99wr7XL>2zl6ynGK2bvo-d@Uh8Qcl5ws@dq|C|^2%N<(U+4iXWWSpve?pI@pXKfeqK^(>VYq0Gnw|J_HTGrX0)**>9ib9W!}X+|VIipmd! z;qu>Lw5O*LO8_Hi8}3FyBuaQfmO5P)bbas0$jI}Xz4Of5=h7KcGyf4<;alFco(ugj w6cfC2r3?A!#|5Io4GCljaCs8j{m3Iq%t<8 literal 16123 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 - - + + +
[legend]
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.map b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.map index cff1744c6c..e7ec302e87 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.map @@ -1,6 +1,7 @@ - - + + + diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.md5 b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.md5 index 38b1381700..a883b88a89 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.md5 @@ -1 +1 @@ -af29e2f47aab417059a0de98ec98f5f2 \ No newline at end of file +f0e6b804b8a357b234f6eb5c257c8baa \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.png b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_daemon_client__coll__graph.png index 632fc208127c59a5b3918faab2af644d834886b5..42255ac25172284eded132cb3c34d4a489514a2c 100644 GIT binary patch literal 16922 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 diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client.html b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client.html index 5bd977ae59..36cca0cb85 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client.html @@ -120,8 +120,9 @@ Collaboration diagram for Tgstation.Server.Client.Components.DreamMakerClient: - - + + +
[legend]
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.map b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.map index c08dcd2d0c..aaf0aa0067 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.map @@ -1,6 +1,7 @@ - - + + + diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.md5 b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.md5 index f27e231e7d..99db486e93 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.md5 @@ -1 +1 @@ -514ff5732f4ebc067074e10164a6f4bb \ No newline at end of file +fa51b128454260c0bca8b93af255de2e \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.png b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_dream_maker_client__coll__graph.png index b556c46f87d9261955fd8638e89f81a46eb669ec..508b777a6873863ea26c4a310a6595d68d61b1a3 100644 GIT binary patch literal 16998 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 Y$u9jU*cF0b9D%xj_knDov{AtS0-PT6X#fBK literal 16080 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 diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client.html b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client.html index 4c286da7c0..177fa5111c 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client.html @@ -120,8 +120,9 @@ Collaboration diagram for Tgstation.Server.Client.Components.InstanceUserClient: - - + + +
[legend]
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.map b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.map index b7d7ffdc6a..03071127e5 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.map @@ -1,6 +1,7 @@ - - + + + diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.md5 b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.md5 index c44f38f91f..81b4263541 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.md5 @@ -1 +1 @@ -6b64ab3bc5d362eb596d8e0138d22df5 \ No newline at end of file +c3537925ece54730a87cd931ee892be1 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.png b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_instance_user_client__coll__graph.png index 5da50a2d65a5fc3d7786716734a0b4d0f6861837..35dcd67db625517bbd1274d471900efd6e6b91fa 100644 GIT binary patch literal 16894 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 QfI=zAs>qbyxgYfZ0YRYb(f|Me literal 15980 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 diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client.html b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client.html index fec5e362a8..bd78bb1c87 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client.html @@ -119,8 +119,9 @@ Collaboration diagram for Tgstation.Server.Client.Components.JobsClient: - - + + +
[legend]
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.map b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.map index 22e66285ae..b7af527ecb 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.map @@ -1,6 +1,7 @@ - - + + + diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.md5 b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.md5 index 846410d3e7..373c33ddbc 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.md5 @@ -1 +1 @@ -ee32b050ec9822df645685ef6873ccc6 \ No newline at end of file +a333e9c71deac007bef25d7e577b6f91 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.png b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_jobs_client__coll__graph.png index 9071d804a29e84184a4b292591ce3e96c9f55839..0d249c90fc3334feba41d9939abe98ed6e0ccb13 100644 GIT binary patch literal 15625 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 HIN*N)S#l@E literal 14738 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 diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client.html b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client.html index d6f8ebfc5a..3de1f27b24 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client.html @@ -120,8 +120,9 @@ Collaboration diagram for Tgstation.Server.Client.Components.RepositoryClient: - - + + +
[legend]
diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.map b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.map index 8f11700db0..ea8107aa93 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.map +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.map @@ -1,6 +1,7 @@ - - + + + diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.md5 b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.md5 index 37d437aaa9..a70abf5a48 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.md5 +++ b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.md5 @@ -1 +1 @@ -71e756818857387cd8a4b089e1e3ead1 \ No newline at end of file +27372748cf1f8557805100475e62d8b4 \ No newline at end of file diff --git a/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.png b/class_tgstation_1_1_server_1_1_client_1_1_components_1_1_repository_client__coll__graph.png index bee843c9d234dd287fc26feea58b361922e71f1f..7515fc536d618faa1baebd0d4a375fac364f3745 100644 GIT binary patch literal 16984 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 diff --git a/class_tgstation_1_1_server_1_1_client_1_1_instance_manager_client.html b/class_tgstation_1_1_server_1_1_client_1_1_instance_manager_client.html index b141aa90f5..7e53a5e4af 100644 --- a/class_tgstation_1_1_server_1_1_client_1_1_instance_manager_client.html +++ b/class_tgstation_1_1_server_1_1_client_1_1_instance_manager_client.html @@ -192,7 +192,7 @@ Private Attributes

Definition at line 28 of file InstanceManagerClient.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Routes.InstanceManager, Tgstation.Server.Api.Routes.ListRoute(), and Tgstation.Server.Api.Routes.SetID().

+

References Tgstation.Server.Api.Models.EntityId.Id, Tgstation.Server.Api.Routes.InstanceManager, Tgstation.Server.Api.Routes.ListRoute(), and Tgstation.Server.Api.Routes.SetID().

29  {
30  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
31 
32  cachedClients = new Dictionary<long, IInstanceClient>();
33  }
readonly IApiClient apiClient
The IApiClient for the InstanceManagerClient
readonly Dictionary< long, IInstanceClient > cachedClients
Map of already created IInstanceClients
@@ -230,11 +230,11 @@ Here is the call graph for this function:

Definition at line 51 of file InstanceManagerClient.cs.

-

References Tgstation.Server.Api.Models.Instance.Id.

-
52  {
53  if (!cachedClients.TryGetValue(instance?.Id ?? throw new ArgumentNullException(nameof(instance)), out var client))
54  {
55  client = new InstanceClient(apiClient, instance);
56  cachedClients.Add(instance.Id, client);
57  }
58 
59  return client;
60  }
+

References Tgstation.Server.Api.Models.EntityId.Id.

+
52  {
53  if (!cachedClients.TryGetValue(instance?.Id ?? throw new ArgumentNullException(nameof(instance)), out var client))
54  {
55  client = new InstanceClient(apiClient, instance);
56  cachedClients.Add(instance.Id, client);
57  }
58 
59  return client;
60  }
long Id
The ID of the entity.
Definition: EntityId.cs:11
+
readonly IApiClient apiClient
The IApiClient for the InstanceManagerClient
readonly Dictionary< long, IInstanceClient > cachedClients
Map of already created IInstanceClients
-
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_byond_manager.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_byond_manager.html index f7026865bd..783ab4ee18 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_byond_manager.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_byond_1_1_byond_manager.html @@ -431,7 +431,7 @@ Here is the call graph for this function:

Definition at line 243 of file ByondManager.cs.

-

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

+

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

244  {
245  async Task<byte[]> GetActiveVersion()
246  {
247  var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
248  return !activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
249  }
250 
251  var activeVersionBytesTask = GetActiveVersion();
252 
253  // Create local cfg directory in case it doesn't exist
254  var localCfgDirectory = ioManager.ConcatPath(
258  localCfgDirectory,
259  cancellationToken).ConfigureAwait(false);
260 
261  // Delete trusted.txt so it doesn't grow too large
262  var trustedFilePath =
264  localCfgDirectory,
266  logger.LogTrace("Deleting trusted .dmbs file {0}", trustedFilePath);
267  await ioManager.DeleteFile(
268  trustedFilePath,
269  cancellationToken).ConfigureAwait(false);
270 
271  var byondDirectory = ioManager.ResolvePath();
272  await ioManager.CreateDirectory(byondDirectory, cancellationToken).ConfigureAwait(false);
273  var directories = await ioManager.GetDirectories(byondDirectory, cancellationToken).ConfigureAwait(false);
274 
275  async Task ReadVersion(string path)
276  {
277  var versionFile = ioManager.ConcatPath(path, VersionFileName);
278  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
279  {
280  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
281  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); // cleanup
282  return;
283  }
284 
285  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
286  var text = Encoding.UTF8.GetString(bytes);
287  if (Version.TryParse(text, out var version))
288  {
289  var key = VersionKey(version);
290  lock (installedVersions)
291  if (!installedVersions.ContainsKey(key))
292  {
293  logger.LogDebug("Adding detected BYOND version {0}...", key);
294  installedVersions.Add(key, Task.CompletedTask);
295  return;
296  }
297  }
298 
299  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
300  }
301 
302  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
303 
304  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
305  if (activeVersionBytes != null)
306  {
307  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
308  bool hasRequestedActiveVersion;
309  lock (installedVersions)
310  hasRequestedActiveVersion = installedVersions.ContainsKey(activeVersionString);
311  if (hasRequestedActiveVersion && Version.TryParse(activeVersionString, out var activeVersion))
312  ActiveVersion = activeVersion.Semver();
313  else
314  {
315  logger.LogWarning("Failed to load saved active version {0}!", activeVersionString);
316  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
317  }
318  }
319  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory, removes and does not enter any symlinks encounterd.
string PathToUserByondFolder
The path to the BYOND folder for the user.
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:62
@@ -480,7 +480,7 @@ Here is the caller graph for this function:

-

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

+

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

Here is the caller graph for this function:
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel_mapping.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel_mapping.html index 7ba35ec4bb..5d4e7adddd 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel_mapping.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel_mapping.html @@ -224,7 +224,7 @@ Properties

Definition at line 21 of file ChannelMapping.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ChangeChannels().

+

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ChangeChannels().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel_representation.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel_representation.html index 7d40f7e0a7..cc0a9ea4ea 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel_representation.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel_representation.html @@ -156,7 +156,7 @@ Properties

Definition at line 37 of file ChannelRepresentation.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

+

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

@@ -206,7 +206,7 @@ Properties

Definition at line 16 of file ChannelRepresentation.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.Client_MessageReceived(), Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.MapChannels(), and Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

+

Referenced by Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.Client_MessageReceived(), Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.MapChannels(), and Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

@@ -232,7 +232,7 @@ Properties

Definition at line 42 of file ChannelRepresentation.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.Providers.IrcProvider.Disconnect(), and Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

+

Referenced by Tgstation.Server.Host.Components.Chat.Providers.IrcProvider.Disconnect(), and Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

@@ -258,7 +258,7 @@ Properties

Definition at line 47 of file ChannelRepresentation.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

+

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

@@ -285,7 +285,7 @@ Properties

Definition at line 24 of file ChannelRepresentation.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

+

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

@@ -311,7 +311,7 @@ Properties

Definition at line 52 of file ChannelRepresentation.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

+

Referenced by Tgstation.Server.Host.Components.Chat.ChatManager.ProcessMessage().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat_manager-members.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat_manager-members.html index 2c375990bb..02d6c5e004 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat_manager-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat_manager-members.html @@ -121,21 +121,22 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
- - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
loggerTgstation.Server.Host.Components.Chat.ChatManagerprivate
loggerFactoryTgstation.Server.Host.Components.Chat.ChatManagerprivate
mappedChannelsTgstation.Server.Host.Components.Chat.ChatManagerprivate
MonitorMessages(CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManagerprivate
ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManagerprivate
providerFactoryTgstation.Server.Host.Components.Chat.ChatManagerprivate
providersTgstation.Server.Host.Components.Chat.ChatManagerprivate
RegisterCommandHandler(ICustomCommandHandler customCommandHandler)Tgstation.Server.Host.Components.Chat.ChatManager
RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManagerprivate
restartRegistrationTgstation.Server.Host.Components.Chat.ChatManagerprivate
SendDeploymentMessage(Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, bool localCommitPushed, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
SendMessage(string message, IEnumerable< ulong > channelIds, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
StartAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
startedTgstation.Server.Host.Components.Chat.ChatManagerprivate
StopAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
synchronizationLockTgstation.Server.Host.Components.Chat.ChatManagerprivate
trackingContextsTgstation.Server.Host.Components.Chat.ChatManagerprivate
messagesProcessedTgstation.Server.Host.Components.Chat.ChatManagerprivate
MonitorMessages(CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManagerprivate
ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManagerprivate
providerFactoryTgstation.Server.Host.Components.Chat.ChatManagerprivate
providersTgstation.Server.Host.Components.Chat.ChatManagerprivate
RegisterCommandHandler(ICustomCommandHandler customCommandHandler)Tgstation.Server.Host.Components.Chat.ChatManager
RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManagerprivate
restartRegistrationTgstation.Server.Host.Components.Chat.ChatManagerprivate
SendDeploymentMessage(Models.RevisionInformation revisionInformation, Version byondVersion, DateTimeOffset?estimatedCompletionTime, string gitHubOwner, string gitHubRepo, bool localCommitPushed, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
SendMessage(string message, IEnumerable< ulong > channelIds, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
SendWatchdogMessage(string message, bool adminOnly, CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
StartAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
startedTgstation.Server.Host.Components.Chat.ChatManagerprivate
StopAsync(CancellationToken cancellationToken)Tgstation.Server.Host.Components.Chat.ChatManager
synchronizationLockTgstation.Server.Host.Components.Chat.ChatManagerprivate
trackingContextsTgstation.Server.Host.Components.Chat.ChatManagerprivate