From 720360e367e13cf6011b20d75915967e8679a9ce Mon Sep 17 00:00:00 2001 From: tgstation-server Date: Thu, 29 Nov 2018 18:38:31 +0000 Subject: [PATCH] Deploy code docs to GitHub Pages for Appveyor build 1043 Commit: 0173da05d77ff7d5f7579bd5622cea39ca493902 --- ...35553___m_s_initial_create_8cs_source.html | 8 +- ...43029___m_y_initial_create_8cs_source.html | 8 +- _administration_8cs.html | 1 + _administration_8cs_source.html | 4 +- _administration_client_8cs_source.html | 2 +- _administration_controller_8cs_source.html | 28 +- _administration_rights_8cs_source.html | 2 +- _administration_test_8cs_source.html | 6 +- _api_client_8cs_source.html | 7 +- _api_controller_8cs_source.html | 6 +- _api_headers_8cs_source.html | 6 +- _api_validation_status_8cs_source.html | 4 +- _application_8cs_source.html | 32 +- _authentication_context_8cs_source.html | 2 +- ...entication_context_factory_8cs_source.html | 2 +- _bool_converter_8cs_source.html | 2 +- _byond_client_8cs_source.html | 6 +- _byond_controller_8cs_source.html | 2 +- _byond_executable_lock_8cs_source.html | 2 +- _byond_manager_8cs_source.html | 22 +- _byond_rights_8cs_source.html | 2 +- _channel_8cs_source.html | 2 +- _chat_8cs_source.html | 74 ++-- _chat_bot_rights_8cs_source.html | 2 +- _chat_bots_client_8cs_source.html | 6 +- _chat_command_8cs.html | 1 + _chat_command_8cs_source.html | 4 +- _chat_controller_8cs_source.html | 16 +- _chat_factory_8cs_source.html | 2 +- _chat_provider_8cs_source.html | 2 +- _claims_injector_8cs_source.html | 2 +- _comm_context_8cs_source.html | 2 +- ...ents_2_watchdog_2_watchdog_8cs_source.html | 98 ++--- ...atchdog_2_watchdog_factory_8cs_source.html | 2 +- _configuration_8cs_source.html | 40 +- _configuration_client_8cs_source.html | 10 +- _configuration_controller_8cs_source.html | 12 +- _configuration_file_8cs_source.html | 2 +- _configuration_rights_8cs_source.html | 2 +- _configuration_type_8cs_source.html | 2 +- _console_8cs_source.html | 2 +- _constants_8cs.html | 1 + _constants_8cs_source.html | 4 +- _cryptography_suite_8cs_source.html | 14 +- _database_context_8cs_source.html | 16 +- _database_type_8cs_source.html | 2 +- _dead_session_controller_8cs_source.html | 2 +- _default_i_o_manager_8cs_source.html | 21 +- ...db_context_factory_helpers_8cs_source.html | 2 +- _discord_provider_8cs_source.html | 10 +- _dmb_factory_8cs_source.html | 32 +- _dmb_provider_8cs_source.html | 14 +- _dream_daemon_client_8cs_source.html | 4 +- _dream_daemon_controller_8cs_source.html | 18 +- _dream_daemon_rights_8cs_source.html | 2 +- _dream_daemon_security_8cs_source.html | 2 +- _dream_daemon_status_8cs_source.html | 2 +- _dream_maker_controller_8cs_source.html | 26 +- _dream_maker_rights_8cs_source.html | 2 +- _event_consumer_8cs_source.html | 6 +- _event_type_8cs_source.html | 2 +- _file_logging_configuration_8cs_source.html | 2 +- _global_suppressions_8cs_source.html | 2 +- _home_controller_8cs_source.html | 24 +- _i_administration_client_8cs_source.html | 2 +- _i_api_client_8cs_source.html | 2 +- _i_byond_manager_8cs_source.html | 2 +- _i_database_context_8cs_source.html | 2 +- _i_dmb_factory_8cs_source.html | 2 +- _i_instance_8cs_source.html | 10 +- _i_job_manager_8cs_source.html | 2 +- _i_post_write_handler_8cs.html | 1 + _i_post_write_handler_8cs_source.html | 2 +- _i_server_client_8cs_source.html | 4 +- _identity_cache_8cs_source.html | 4 +- _instance_controller_8cs_source.html | 38 +- _instance_factory_8cs_source.html | 12 +- _instance_manager_8cs_source.html | 14 +- _instance_manager_client_8cs_source.html | 2 +- _instance_manager_rights_8cs_source.html | 2 +- _instance_manager_test_8cs_source.html | 4 +- _instance_user_client_8cs_source.html | 6 +- _instance_user_controller_8cs_source.html | 16 +- _instance_user_rights_8cs_source.html | 2 +- _integration_test_8cs_source.html | 2 +- ..._connection_string_builder_8cs_source.html | 4 +- _irc_password_type_8cs_source.html | 2 +- _irc_provider_8cs_source.html | 26 +- _job_controller_8cs_source.html | 6 +- _job_handler_8cs_source.html | 8 +- _job_manager_8cs_source.html | 12 +- _jobs_client_8cs_source.html | 6 +- _json_tracking_context_8cs_source.html | 10 +- _message_8cs_source.html | 2 +- _monitor_action_8cs_source.html | 2 +- _monitor_activation_reason_8cs_source.html | 2 +- _my_sql_database_context_8cs_source.html | 2 +- ...gn_time_db_context_factory_8cs_source.html | 4 +- _native_methods_8cs_source.html | 6 +- _posix_byond_installer_8cs_source.html | 19 +- _posix_network_prompt_reaper_8cs_source.html | 6 +- _posix_post_write_handler_8cs.html | 2 +- _posix_post_write_handler_8cs_source.html | 4 +- _process_8cs_source.html | 14 +- _process_executor_8cs_source.html | 8 +- _pull_requests_command_8cs_source.html | 4 +- _reboot_state_8cs_source.html | 2 +- _release_notes_8_assembly_info_8cs.html | 4 +- ...ease_notes_8_assembly_info_8cs_source.html | 6 +- _repository_client_8cs_source.html | 6 +- _repository_controller_8cs_source.html | 30 +- _repository_manager_8cs_source.html | 6 +- _repository_rights_8cs_source.html | 2 +- _resolving_i_o_manager_8cs_source.html | 6 +- _rights_helper_8cs_source.html | 2 +- _rights_type_8cs_source.html | 2 +- _semaphore_slim_context_8cs_source.html | 5 +- _server_8cs_source.html | 8 +- _server_client_8cs_source.html | 4 +- _server_client_factory_8cs_source.html | 5 +- _server_factory_8cs_source.html | 4 +- _server_service_8cs_source.html | 8 +- ...vice_collection_extensions_8cs_source.html | 2 +- _session_controller_8cs_source.html | 60 +-- _session_controller_factory_8cs_source.html | 42 +- _setup_wizard_8cs_source.html | 14 +- _setup_wizard_mode_8cs_source.html | 2 +- _sql_server_database_context_8cs_source.html | 2 +- ...gn_time_db_context_factory_8cs_source.html | 4 +- _synchronous_i_o_manager_8cs_source.html | 4 +- _temporary_dmb_provider_8cs_source.html | 2 +- _test_application_8cs_source.html | 2 +- _test_discord_provider_8cs_source.html | 2 +- _test_job_handler_8cs_source.html | 2 +- _test_posix_byond_installer_8cs_source.html | 2 +- _test_post_write_handler_8cs_source.html | 2 +- _test_server_service_8cs_source.html | 2 +- _tgs_authorize_attribute_8cs_source.html | 15 +- ..._8_api_2_models_2_instance_8cs_source.html | 2 +- ...nal_2_revision_information_8cs_source.html | 2 +- ...on_8_server_8_api_8_assembly_info_8cs.html | 4 +- ...rver_8_api_8_assembly_info_8cs_source.html | 6 +- ...ver_8_api_8_tests_8_assembly_info_8cs.html | 4 +- ...pi_8_tests_8_assembly_info_8cs_source.html | 6 +- ..._8_server_8_api_8_tests_8_program_8cs.html | 4 +- ...er_8_api_8_tests_8_program_8cs_source.html | 4 +- ...8_server_8_client_8_assembly_info_8cs.html | 4 +- ...r_8_client_8_assembly_info_8cs_source.html | 6 +- ..._8_client_8_tests_8_assembly_info_8cs.html | 4 +- ...nt_8_tests_8_assembly_info_8cs_source.html | 6 +- ...server_8_client_8_tests_8_program_8cs.html | 4 +- ...8_client_8_tests_8_program_8cs_source.html | 4 +- ...s_2_compiler_2_dream_maker_8cs_source.html | 36 +- ...st_2_components_2_instance_8cs_source.html | 70 ++-- ..._2_repository_2_repository_8cs_source.html | 22 +- ...8_host_2_models_2_instance_8cs_source.html | 4 +- ...dels_2_repository_settings_8cs_source.html | 4 +- ...els_2_revision_information_8cs_source.html | 2 +- ...n_8_server_8_host_8_assembly_info_8cs.html | 4 +- ...ver_8_host_8_assembly_info_8cs_source.html | 6 +- ..._8_host_8_console_8_assembly_info_8cs.html | 4 +- ..._8_console_8_assembly_info_8cs_source.html | 6 +- ...8_console_8_tests_8_assembly_info_8cs.html | 4 +- ...le_8_tests_8_assembly_info_8cs_source.html | 6 +- ..._host_8_console_8_tests_8_program_8cs.html | 4 +- ..._console_8_tests_8_program_8cs_source.html | 4 +- ...rver_8_host_8_razor_assembly_info_8cs.html | 4 +- ...host_8_razor_assembly_info_8cs_source.html | 4 +- ...2_watchdog_2_test_watchdog_8cs_source.html | 4 +- ...er_8_host_8_tests_8_assembly_info_8cs.html | 4 +- ...st_8_tests_8_assembly_info_8cs_source.html | 6 +- ...8_server_8_host_8_tests_8_program_8cs.html | 4 +- ...r_8_host_8_tests_8_program_8cs_source.html | 4 +- ...8_host_8_watchdog_8_assembly_info_8cs.html | 4 +- ...8_watchdog_8_assembly_info_8cs_source.html | 6 +- ..._watchdog_8_tests_8_assembly_info_8cs.html | 4 +- ...og_8_tests_8_assembly_info_8cs_source.html | 6 +- ...host_8_watchdog_8_tests_8_program_8cs.html | 4 +- ...watchdog_8_tests_8_program_8cs_source.html | 4 +- ..._8_server_8_tests_8_assembly_info_8cs.html | 4 +- ...er_8_tests_8_assembly_info_8cs_source.html | 6 +- ...tation_8_server_8_tests_8_program_8cs.html | 4 +- ...8_server_8_tests_8_program_8cs_source.html | 4 +- _token_factory_8cs_source.html | 2 +- _user_controller_8cs_source.html | 4 +- _watchdog_2_watchdog_8cs_source.html | 6 +- _windows_byond_installer_8cs_source.html | 18 +- ...dows_network_prompt_reaper_8cs_source.html | 14 +- _windows_post_write_handler_8cs.html | 2 +- _windows_post_write_handler_8cs_source.html | 2 +- _windows_symlink_factory_8cs_source.html | 6 +- _windows_system_identity_8cs_source.html | 2 +- ...ws_system_identity_factory_8cs_source.html | 4 +- annotated.html | 12 +- class_auto_generated_program.html | 12 +- ...erver_1_1_api_1_1_api_headers-members.html | 10 +- ...on_1_1_server_1_1_api_1_1_api_headers.html | 100 ++--- ...1_1_api_1_1_models_1_1_administration.html | 15 +- ...r_1_1_api_1_1_models_1_1_chat_channel.html | 6 +- ...er_1_1_api_1_1_models_1_1_compile_job.html | 2 +- ...api_1_1_models_1_1_configuration_file.html | 8 +- ..._1_1_api_1_1_models_1_1_error_message.html | 2 +- ...erver_1_1_api_1_1_models_1_1_instance.html | 6 +- ..._1_1_api_1_1_models_1_1_instance_user.html | 4 +- ..._1_1_models_1_1_internal_1_1_chat_bot.html | 4 +- ...al_1_1_chat_connection_string_builder.html | 2 +- ...1_models_1_1_internal_1_1_compile_job.html | 4 +- ...al_1_1_dream_daemon_launch_parameters.html | 12 +- ..._1_internal_1_1_dream_daemon_settings.html | 2 +- ...1_api_1_1_models_1_1_internal_1_1_job.html | 4 +- ..._1_1_internal_1_1_repository_settings.html | 2 +- ...1_1_internal_1_1_revision_information.html | 8 +- ...dels_1_1_internal_1_1_test_merge_base.html | 2 +- ..._api_1_1_models_1_1_internal_1_1_user.html | 6 +- ...els_1_1_irc_connection_string_builder.html | 6 +- ...ver_1_1_api_1_1_models_1_1_repository.html | 10 +- ..._1_1_models_1_1_test_merge_parameters.html | 6 +- ...1_server_1_1_api_1_1_models_1_1_token.html | 2 +- ..._1_1_rights_1_1_rights_helper-members.html | 2 +- ..._1_1_api_1_1_rights_1_1_rights_helper.html | 18 +- ...station_1_1_server_1_1_api_1_1_routes.html | 26 +- ...ver_1_1_client_1_1_api_client-members.html | 2 + ..._1_1_server_1_1_client_1_1_api_client.html | 140 +++++-- ...lient_1_1_components_1_1_byond_client.html | 6 +- ...t_1_1_components_1_1_chat_bots_client.html | 6 +- ...1_components_1_1_configuration_client.html | 18 +- ..._1_components_1_1_dream_daemon_client.html | 6 +- ...1_components_1_1_instance_user_client.html | 18 +- ...client_1_1_components_1_1_jobs_client.html | 6 +- ..._1_1_components_1_1_repository_client.html | 8 +- ..._1_client_1_1_instance_manager_client.html | 4 +- ...1_server_1_1_client_1_1_server_client.html | 8 +- ...ent_1_1_server_client_factory-members.html | 2 +- ..._1_1_client_1_1_server_client_factory.html | 20 +- ...omponents_1_1_byond_1_1_byond_manager.html | 86 ++-- ...ond_1_1_posix_byond_installer-members.html | 2 +- ...s_1_1_byond_1_1_posix_byond_installer.html | 68 ++-- ...d_1_1_windows_byond_installer-members.html | 2 +- ...1_1_byond_1_1_windows_byond_installer.html | 74 ++-- ...t_1_1_components_1_1_chat_1_1_channel.html | 10 +- ...mponents_1_1_chat_1_1_channel_mapping.html | 2 +- ...host_1_1_components_1_1_chat_1_1_chat.html | 228 +++++------ ..._1_commands_1_1_pull_requests_command.html | 4 +- ...chat_1_1_commands_1_1_version_command.html | 2 +- ...ts_1_1_chat_1_1_json_tracking_context.html | 32 +- ...at_1_1_providers_1_1_discord_provider.html | 18 +- ...1_chat_1_1_providers_1_1_irc_provider.html | 58 +-- ...ts_1_1_chat_1_1_providers_1_1_message.html | 4 +- ...host_1_1_components_1_1_chat_1_1_user.html | 2 +- ...mponents_1_1_compiler_1_1_dmb_factory.html | 90 ++--- ...1_1_compiler_1_1_dmb_provider-members.html | 3 +- ...ponents_1_1_compiler_1_1_dmb_provider.html | 30 +- ...mponents_1_1_compiler_1_1_dream_maker.html | 102 +++-- ...1_compiler_1_1_temporary_dmb_provider.html | 4 +- ...ost_1_1_components_1_1_event_consumer.html | 10 +- ...t_1_1_components_1_1_instance-members.html | 4 +- ..._1_1_host_1_1_components_1_1_instance.html | 280 ++++++------- ...t_1_1_components_1_1_instance_factory.html | 10 +- ...t_1_1_components_1_1_instance_manager.html | 28 +- ...mponents_1_1_interop_1_1_chat_command.html | 23 +- ...mponents_1_1_interop_1_1_comm_command.html | 4 +- ...mponents_1_1_interop_1_1_comm_context.html | 2 +- ..._components_1_1_interop_1_1_constants.html | 123 ++++-- ...ts_1_1_interop_1_1_json_sub_file_list.html | 6 +- ...1_repository_1_1_credentials_provider.html | 2 +- ...ponents_1_1_repository_1_1_repository.html | 48 +-- ...1_1_repository_1_1_repository_manager.html | 16 +- ...ts_1_1_static_files_1_1_configuration.html | 162 ++++---- ..._watchdog_1_1_dead_session_controller.html | 2 +- ...onents_1_1_watchdog_1_1_monitor_state.html | 10 +- ...chdog_1_1_posix_network_prompt_reaper.html | 6 +- ...1_1_watchdog_1_1_reattach_information.html | 2 +- ...s_1_1_watchdog_1_1_session_controller.html | 153 ++++---- ...tchdog_1_1_session_controller_factory.html | 86 ++-- ..._watchdog_1_1_tests_1_1_test_watchdog.html | 2 +- ..._components_1_1_watchdog_1_1_watchdog.html | 370 +++++++++--------- ...dog_1_1_watchdog_reattach_information.html | 4 +- ...dog_1_1_windows_network_prompt_reaper.html | 22 +- ...ation_1_1_control_panel_configuration.html | 6 +- ...figuration_1_1_database_configuration.html | 8 +- ...ration_1_1_file_logging_configuration.html | 12 +- ...nfiguration_1_1_general_configuration.html | 4 +- ...rollers_1_1_administration_controller.html | 76 ++-- ...st_1_1_controllers_1_1_api_controller.html | 6 +- ..._1_1_controllers_1_1_byond_controller.html | 2 +- ...t_1_1_controllers_1_1_chat_controller.html | 52 +-- ...trollers_1_1_configuration_controller.html | 34 +- ...ntrollers_1_1_dream_daemon_controller.html | 60 +-- ...rs_1_1_dream_maker_controller-members.html | 8 +- ...ontrollers_1_1_dream_maker_controller.html | 64 +-- ...controllers_1_1_dream_maker_controller.png | Bin 1399 -> 1321 bytes ...t_1_1_controllers_1_1_home_controller.html | 42 +- ...1_controllers_1_1_instance_controller.html | 110 +++--- ...trollers_1_1_instance_user_controller.html | 42 +- ...st_1_1_controllers_1_1_job_controller.html | 28 +- ..._1_1_controllers_1_1_model_controller.html | 2 +- ...controllers_1_1_repository_controller.html | 90 ++--- ...ntrollers_1_1_tgs_authorize_attribute.html | 33 +- ...t_1_1_controllers_1_1_user_controller.html | 24 +- ...ver_1_1_host_1_1_core_1_1_application.html | 83 ++-- ...ver_1_1_host_1_1_core_1_1_job_handler.html | 21 +- ...ver_1_1_host_1_1_core_1_1_job_manager.html | 30 +- ..._server_1_1_host_1_1_core_1_1_process.html | 43 +- ..._1_host_1_1_core_1_1_process_executor.html | 14 +- ...t_1_1_core_1_1_semaphore_slim_context.html | 15 +- ...ore_1_1_service_collection_extensions.html | 2 +- ...er_1_1_host_1_1_core_1_1_setup_wizard.html | 36 +- ...est_application_1_1_mock_setup_wizard.html | 4 +- ...1_i_o_1_1_default_i_o_manager-members.html | 12 +- ..._host_1_1_i_o_1_1_default_i_o_manager.html | 122 ++---- ..._1_1_i_o_1_1_posix_post_write_handler.html | 8 +- ...i_o_1_1_resolving_i_o_manager-members.html | 12 +- ...ost_1_1_i_o_1_1_resolving_i_o_manager.html | 20 +- ...t_1_1_i_o_1_1_synchronous_i_o_manager.html | 6 +- ..._1_i_o_1_1_windows_post_write_handler.html | 6 +- ...r_1_1_host_1_1_models_1_1_compile_job.html | 4 +- ..._host_1_1_models_1_1_database_context.html | 82 ++-- ...rver_1_1_host_1_1_models_1_1_instance.html | 21 +- ..._1_server_1_1_host_1_1_models_1_1_job.html | 4 +- ...esign_time_db_context_factory_helpers.html | 12 +- ...1_1_migrations_1_1_m_s_initial_create.html | 10 +- ...1_1_migrations_1_1_m_y_initial_create.html | 10 +- ..._1_models_1_1_my_sql_database_context.html | 4 +- ..._models_1_1_reattach_information_base.html | 6 +- ...st_1_1_models_1_1_repository_settings.html | 10 +- ...t_1_1_models_1_1_revision_information.html | 4 +- ...odels_1_1_sql_server_database_context.html | 2 +- ...er_1_1_host_1_1_models_1_1_test_merge.html | 2 +- ...1_server_1_1_host_1_1_models_1_1_user.html | 4 +- ..._1_watchdog_reattach_information_base.html | 2 +- ..._1_server_1_1_host_1_1_native_methods.html | 14 +- ...ation_1_1_server_1_1_host_1_1_program.html | 4 +- ...1_security_1_1_authentication_context.html | 8 +- ...ty_1_1_authentication_context_factory.html | 2 +- ...host_1_1_security_1_1_claims_injector.html | 2 +- ...t_1_1_security_1_1_cryptography_suite.html | 28 +- ..._host_1_1_security_1_1_identity_cache.html | 6 +- ...1_host_1_1_security_1_1_token_factory.html | 2 +- ..._security_1_1_windows_system_identity.html | 2 +- ...tation_1_1_server_1_1_host_1_1_server.html | 16 +- ..._1_server_1_1_host_1_1_server_factory.html | 4 +- ..._host_1_1_service_1_1_program-members.html | 1 - ...rver_1_1_host_1_1_service_1_1_program.html | 62 +-- ...1_host_1_1_service_1_1_server_service.html | 16 +- ...ice_1_1_tests_1_1_test_server_service.html | 2 +- ...er_1_1_host_1_1_watchdog_1_1_watchdog.html | 8 +- ...ver_1_1_tests_1_1_administration_test.html | 8 +- ...r_1_1_tests_1_1_instance_manager_test.html | 2 +- ...server_1_1_tests_1_1_integration_test.html | 2 +- classes.html | 18 +- ... dir_009a3598be1715e27911e1588bdd551c.html | 4 +- ... dir_019c6d3af924151b681494cc466be1ac.html | 8 +- ... dir_0b6a380fe7fc1e1d640d1ac854fc3fb6.html | 8 +- dir_103b33cf1ab7043de6f87dfa61afa23f.html | 2 +- ... dir_1083fcf52a7d94a2efffb6ca88246acf.html | 4 +- dir_177c182c6d1f29bec543c3fdad3072d5.html | 2 +- ... dir_2553b13cbb871373b0c30fb90d66217c.html | 4 +- ... dir_2b9d136d9ffdab429a63a74fca28b300.html | 8 +- ... dir_2e39d68cad3f83629c74ea67329dead9.html | 8 +- ... dir_2e67631cb23401d696bcfdbe81f86f0f.html | 8 +- dir_446bdfc1cd19b69c11765b5665ebfd45.html | 2 +- ... dir_48968f849f7ddb061f7130e079fe66a6.html | 8 +- ... dir_4c0c7e2f4fc60f51e2347a2d8dda94ce.html | 4 +- dir_685b5aa2d8a661ab5bc3f68b331887b0.html | 2 +- dir_689f0584ee97ae9ca308532531d9c5d2.html | 2 +- dir_74017a19412e2f90cc6ad11fcac82495.html | 2 +- ... dir_87039fa7b841d848ca75981d64237477.html | 8 +- dir_87a4f2035ed18e30422f4b60aebc4381.html | 2 +- ... dir_9257d56e926145e494a5aa5279887044.html | 4 +- ... dir_a3714ab037275c284988fc90b4d045a5.html | 4 +- ... dir_a8807eef6592489c6a360d587f5b1a66.html | 4 +- dir_b524b00ec2259c3c83b44909fd47f6e3.html | 2 +- ... dir_b551a02288ef39e099594710382363a0.html | 4 +- ... dir_b87cbb2c60cf682b61fbfcceb9bced43.html | 4 +- ... dir_b92573f47b933d7a3661c26d1af2098b.html | 8 +- ... dir_be9f3ed3485a880e3213506a311823d1.html | 8 +- dir_bf23e1e49f40264f2966f02ed35d8142.html | 2 +- ... dir_c5f98c05147bcbba78e71856485f5049.html | 4 +- ... dir_d8a279f7113e0073f6cd65dd050c1475.html | 4 +- dir_e0c1bccb03249d93bc42ac0968ce43fd.html | 2 +- ... dir_e97d380c3eebaf0856a4ec180d749620.html | 8 +- ... dir_ea36b5ab01ad6e8b607438a02ae91964.html | 8 +- dir_f0581cdd528a911f791df57d0f438bcb.html | 2 +- ... dir_f88948f9bde0555374e8a59a663bdfc9.html | 4 +- ... dir_fab1362e86740a2c573ef875dd474a67.html | 8 +- dir_fb12273b841438c2f2c9b5bc5398c636.html | 2 +- files.html | 48 +-- functions.html | 13 +- functions_b.html | 6 +- functions_c.html | 15 +- functions_d.html | 4 +- functions_func.html | 7 +- functions_func_c.html | 11 +- functions_func_g.html | 3 + functions_func_h.html | 3 + functions_func_r.html | 2 +- functions_g.html | 3 + functions_h.html | 3 + functions_i.html | 6 +- functions_j.html | 4 +- functions_p.html | 4 +- functions_prop_c.html | 3 - functions_prop_d.html | 1 - functions_r.html | 9 +- functions_t.html | 4 +- functions_u.html | 4 +- functions_vars.html | 8 +- functions_vars_b.html | 6 +- functions_vars_c.html | 1 + functions_vars_d.html | 3 + functions_vars_i.html | 4 +- functions_vars_j.html | 4 +- functions_vars_p.html | 4 +- functions_vars_r.html | 3 - functions_vars_t.html | 4 +- functions_vars_u.html | 4 +- functions_vars_w.html | 1 - functions_w.html | 5 +- hierarchy.html | 46 +-- ..._1_server_1_1_client_1_1_i_api_client.html | 276 ++++++++++++- ...server_1_1_client_1_1_i_server_client.html | 7 + ..._1_client_1_1_i_server_client_factory.html | 2 +- ...nents_1_1_byond_1_1_i_byond_installer.html | 2 +- ...ponents_1_1_byond_1_1_i_byond_manager.html | 1 + ...st_1_1_components_1_1_chat_1_1_i_chat.html | 2 +- ..._1_1_chat_1_1_i_json_tracking_context.html | 2 +- ...1_1_chat_1_1_providers_1_1_i_provider.html | 6 +- ...ost_1_1_components_1_1_i_dmb_provider.html | 6 +- ..._1_host_1_1_components_1_1_i_instance.html | 4 +- ...onents_1_1_interop_1_1_i_comm_context.html | 2 +- ...nents_1_1_repository_1_1_i_repository.html | 8 +- ...1_1_watchdog_1_1_i_session_controller.html | 24 +- ...r_1_1_host_1_1_core_1_1_i_application.html | 2 +- ...r_1_1_host_1_1_core_1_1_i_job_manager.html | 11 +- ...st_1_1_core_1_1_i_platform_identifier.html | 2 +- ...erver_1_1_host_1_1_core_1_1_i_process.html | 6 +- ..._1_1_host_1_1_core_1_1_i_process_base.html | 4 +- ..._host_1_1_core_1_1_i_process_executor.html | 2 +- ..._1_host_1_1_core_1_1_i_server_control.html | 6 +- ..._1_1_host_1_1_core_1_1_i_setup_wizard.html | 6 +- ...er_1_1_host_1_1_i_o_1_1_i_i_o_manager.html | 8 +- ...host_1_1_i_o_1_1_i_post_write_handler.html | 8 +- ...1_1_i_o_1_1_i_synchronous_i_o_manager.html | 4 +- ...tion_1_1_server_1_1_host_1_1_i_server.html | 2 +- ..._server_1_1_host_1_1_i_server_factory.html | 2 +- ...ost_1_1_models_1_1_i_database_context.html | 12 +- ...st_1_1_security_1_1_i_system_identity.html | 6 +- ...host_1_1_security_1_1_i_token_factory.html | 2 +- ...station_1_1_server_1_1_api_1_1_models.html | 11 +- ...station_1_1_server_1_1_api_1_1_rights.html | 22 +- ...on_1_1_server_1_1_host_1_1_components.html | 2 +- ...r_1_1_host_1_1_components_1_1_interop.html | 2 + ..._1_1_host_1_1_components_1_1_watchdog.html | 8 +- ...1_1_server_1_1_host_1_1_configuration.html | 4 +- ...tgstation_1_1_server_1_1_host_1_1_i_o.html | 5 +- search/all_1.js | 19 +- search/all_10.js | 4 +- search/all_12.js | 8 +- search/all_14.js | 4 +- search/all_15.js | 6 +- search/all_17.js | 8 +- search/all_2.js | 2 +- search/all_3.js | 9 +- search/all_4.js | 2 +- search/all_7.js | 1 + search/all_8.js | 3 +- search/all_9.js | 4 +- search/all_a.js | 4 +- search/all_d.js | 2 +- search/classes_12.js | 2 +- search/classes_14.js | 6 +- search/classes_c.js | 2 +- search/functions_0.js | 1 - search/functions_10.js | 2 +- search/functions_2.js | 1 - search/functions_6.js | 1 + search/functions_7.js | 1 + search/properties_2.js | 1 - search/properties_3.js | 2 +- search/variables_0.js | 4 +- search/variables_1.js | 2 +- search/variables_11.js | 3 +- search/variables_13.js | 2 +- search/variables_14.js | 2 +- search/variables_16.js | 2 +- search/variables_2.js | 2 +- search/variables_3.js | 2 +- search/variables_8.js | 2 +- search/variables_9.js | 4 +- search/variables_f.js | 2 +- ..._8_server_8_host_2_program_8cs_source.html | 4 +- ...8_host_8_console_2_program_8cs_source.html | 2 +- ...8_host_8_service_2_program_8cs_source.html | 4 +- 493 files changed, 3617 insertions(+), 3220 deletions(-) rename dir_4e036e007444785d010e90b5c9a24acd.html => dir_009a3598be1715e27911e1588bdd551c.html (94%) rename dir_91e2bbee1291fcb0db7e0417b617d01e.html => dir_019c6d3af924151b681494cc466be1ac.html (93%) rename dir_53c056aa6a96f034cca75f793964be6e.html => dir_0b6a380fe7fc1e1d640d1ac854fc3fb6.html (92%) rename dir_b2c89c850c962495793d09ea3a2ee385.html => dir_1083fcf52a7d94a2efffb6ca88246acf.html (95%) rename dir_53aae571c66a052729f8e64ccc397130.html => dir_2553b13cbb871373b0c30fb90d66217c.html (94%) rename dir_90400719279795a0c5ea912516a4c874.html => dir_2b9d136d9ffdab429a63a74fca28b300.html (92%) rename dir_ec133cefc1442f53ec97e0d7a57cf129.html => dir_2e39d68cad3f83629c74ea67329dead9.html (92%) rename dir_7ae0efc0613c36c848fa310214e0aee3.html => dir_2e67631cb23401d696bcfdbe81f86f0f.html (92%) rename dir_4122b363a9d577bed19bef337e757ff3.html => dir_48968f849f7ddb061f7130e079fe66a6.html (92%) rename dir_d45f494620bc0048ee4a31fa4f7391e1.html => dir_4c0c7e2f4fc60f51e2347a2d8dda94ce.html (94%) rename dir_0e792d76844bf2d7c79795a56a50ebec.html => dir_87039fa7b841d848ca75981d64237477.html (92%) rename dir_6db5af7fcfa17a34c7abf23e195af4db.html => dir_9257d56e926145e494a5aa5279887044.html (94%) rename dir_13bee0ed20ecc83b9bcd4b869b726d42.html => dir_a3714ab037275c284988fc90b4d045a5.html (95%) rename dir_fcc41a8faa41492aba200a49fc7d0aed.html => dir_a8807eef6592489c6a360d587f5b1a66.html (93%) rename dir_900257602d294c4a4799e88420919068.html => dir_b551a02288ef39e099594710382363a0.html (95%) rename dir_e928c7e494884ad322b5f6005c2954e1.html => dir_b87cbb2c60cf682b61fbfcceb9bced43.html (94%) rename dir_40138fd4849573e743490d583f6d06d6.html => dir_b92573f47b933d7a3661c26d1af2098b.html (93%) rename dir_18a5e30c14613227f95d12bd9fddc614.html => dir_be9f3ed3485a880e3213506a311823d1.html (92%) rename dir_298f2a364723f4f4cea115538f944a94.html => dir_c5f98c05147bcbba78e71856485f5049.html (95%) rename dir_83a6f600c846cd444f33e44bb9b4f9cb.html => dir_d8a279f7113e0073f6cd65dd050c1475.html (94%) rename dir_c65d377d3c73fb4a07b3637f9c0caf4d.html => dir_e97d380c3eebaf0856a4ec180d749620.html (93%) rename dir_6c8d8f20501e8e412458f542d0998195.html => dir_ea36b5ab01ad6e8b607438a02ae91964.html (93%) rename dir_383ca23c40357ede4cca827b4a365c76.html => dir_f88948f9bde0555374e8a59a663bdfc9.html (94%) rename dir_7a9d035dcfcd6d9c7aa1efef4b6b58a4.html => dir_fab1362e86740a2c573ef875dd474a67.html (92%) diff --git a/20180906135553___m_s_initial_create_8cs_source.html b/20180906135553___m_s_initial_create_8cs_source.html index fe7477c746..01dba73725 100644 --- a/20180906135553___m_s_initial_create_8cs_source.html +++ b/20180906135553___m_s_initial_create_8cs_source.html @@ -92,9 +92,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
20180906135553_MSInitialCreate.cs
-Go to the documentation of this file.
1 using System;
2 using Microsoft.EntityFrameworkCore.Metadata;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 
5 namespace Tgstation.Server.Host.Models.Migrations
6 {
10  public partial class MSInitialCreate : Migration
11  {
16  protected override void Up(MigrationBuilder migrationBuilder)
17  {
18  migrationBuilder.CreateTable(
19  name: "Users",
20  columns: table => new
21  {
22  Id = table.Column<long>(nullable: false)
23  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
24  Enabled = table.Column<bool>(nullable: false),
25  CreatedAt = table.Column<DateTimeOffset>(nullable: false),
26  SystemIdentifier = table.Column<string>(nullable: true),
27  Name = table.Column<string>(nullable: false),
28  AdministrationRights = table.Column<decimal>(nullable: false),
29  InstanceManagerRights = table.Column<decimal>(nullable: false),
30  PasswordHash = table.Column<string>(nullable: true),
31  CreatedById = table.Column<long>(nullable: true),
32  CanonicalName = table.Column<string>(nullable: false),
33  LastPasswordUpdate = table.Column<DateTimeOffset>(nullable: true)
34  },
35  constraints: table =>
36  {
37  table.PrimaryKey("PK_Users", x => x.Id);
38  table.ForeignKey(
39  name: "FK_Users_Users_CreatedById",
40  column: x => x.CreatedById,
41  principalTable: "Users",
42  principalColumn: "Id",
43  onDelete: ReferentialAction.Restrict);
44  });
45 
46  migrationBuilder.CreateTable(
47  name: "ChatChannels",
48  columns: table => new
49  {
50  IrcChannel = table.Column<string>(nullable: true),
51  DiscordChannelId = table.Column<decimal>(nullable: true),
52  IsAdminChannel = table.Column<bool>(nullable: false),
53  IsWatchdogChannel = table.Column<bool>(nullable: false),
54  IsUpdatesChannel = table.Column<bool>(nullable: false),
55  Tag = table.Column<string>(nullable: true),
56  Id = table.Column<long>(nullable: false)
57  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
58  ChatSettingsId = table.Column<long>(nullable: false)
59  },
60  constraints: table =>
61  {
62  table.PrimaryKey("PK_ChatChannels", x => x.Id);
63  });
64 
65  migrationBuilder.CreateTable(
66  name: "ReattachInformations",
67  columns: table => new
68  {
69  ChatCommandsJson = table.Column<string>(nullable: false),
70  ChatChannelsJson = table.Column<string>(nullable: false),
71  ServerCommandsJson = table.Column<string>(nullable: false),
72  AccessIdentifier = table.Column<string>(nullable: false),
73  ProcessId = table.Column<int>(nullable: false),
74  IsPrimary = table.Column<bool>(nullable: false),
75  Port = table.Column<int>(nullable: false),
76  RebootState = table.Column<int>(nullable: false),
77  Id = table.Column<long>(nullable: false)
78  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
79  CompileJobId = table.Column<long>(nullable: true)
80  },
81  constraints: table =>
82  {
83  table.PrimaryKey("PK_ReattachInformations", x => x.Id);
84  });
85 
86  migrationBuilder.CreateTable(
87  name: "WatchdogReattachInformations",
88  columns: table => new
89  {
90  AlphaIsActive = table.Column<bool>(nullable: false),
91  Id = table.Column<long>(nullable: false)
92  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
93  AlphaId = table.Column<long>(nullable: true),
94  BravoId = table.Column<long>(nullable: true)
95  },
96  constraints: table =>
97  {
98  table.PrimaryKey("PK_WatchdogReattachInformations", x => x.Id);
99  table.ForeignKey(
100  name: "FK_WatchdogReattachInformations_ReattachInformations_AlphaId",
101  column: x => x.AlphaId,
102  principalTable: "ReattachInformations",
103  principalColumn: "Id",
104  onDelete: ReferentialAction.Restrict);
105  table.ForeignKey(
106  name: "FK_WatchdogReattachInformations_ReattachInformations_BravoId",
107  column: x => x.BravoId,
108  principalTable: "ReattachInformations",
109  principalColumn: "Id",
110  onDelete: ReferentialAction.Restrict);
111  });
112 
113  migrationBuilder.CreateTable(
114  name: "Instances",
115  columns: table => new
116  {
117  Id = table.Column<long>(nullable: false)
118  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
119  Name = table.Column<string>(nullable: false),
120  Path = table.Column<string>(nullable: false),
121  Online = table.Column<bool>(nullable: false),
122  ConfigurationType = table.Column<int>(nullable: false),
123  AutoUpdateInterval = table.Column<long>(nullable: false),
124  WatchdogReattachInformationId = table.Column<long>(nullable: true)
125  },
126  constraints: table =>
127  {
128  table.PrimaryKey("PK_Instances", x => x.Id);
129  table.ForeignKey(
130  name: "FK_Instances_WatchdogReattachInformations_WatchdogReattachInformationId",
131  column: x => x.WatchdogReattachInformationId,
132  principalTable: "WatchdogReattachInformations",
133  principalColumn: "Id",
134  onDelete: ReferentialAction.Restrict);
135  });
136 
137  migrationBuilder.CreateTable(
138  name: "ChatBots",
139  columns: table => new
140  {
141  Id = table.Column<long>(nullable: false)
142  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
143  Name = table.Column<string>(nullable: false),
144  Enabled = table.Column<bool>(nullable: true),
145  Provider = table.Column<int>(nullable: true),
146  ConnectionString = table.Column<string>(nullable: false),
147  InstanceId = table.Column<long>(nullable: false)
148  },
149  constraints: table =>
150  {
151  table.PrimaryKey("PK_ChatBots", x => x.Id);
152  table.ForeignKey(
153  name: "FK_ChatBots_Instances_InstanceId",
154  column: x => x.InstanceId,
155  principalTable: "Instances",
156  principalColumn: "Id",
157  onDelete: ReferentialAction.Cascade);
158  });
159 
160  migrationBuilder.CreateTable(
161  name: "DreamDaemonSettings",
162  columns: table => new
163  {
164  AllowWebClient = table.Column<bool>(nullable: false),
165  SecurityLevel = table.Column<int>(nullable: false),
166  PrimaryPort = table.Column<int>(nullable: false),
167  SecondaryPort = table.Column<int>(nullable: false),
168  StartupTimeout = table.Column<long>(nullable: false),
169  AutoStart = table.Column<bool>(nullable: false),
170  SoftRestart = table.Column<bool>(nullable: false),
171  SoftShutdown = table.Column<bool>(nullable: false),
172  Id = table.Column<long>(nullable: false)
173  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
174  ProcessId = table.Column<int>(nullable: true),
175  AccessToken = table.Column<string>(nullable: true),
176  InstanceId = table.Column<long>(nullable: false)
177  },
178  constraints: table =>
179  {
180  table.PrimaryKey("PK_DreamDaemonSettings", x => x.Id);
181  table.ForeignKey(
182  name: "FK_DreamDaemonSettings_Instances_InstanceId",
183  column: x => x.InstanceId,
184  principalTable: "Instances",
185  principalColumn: "Id",
186  onDelete: ReferentialAction.Cascade);
187  });
188 
189  migrationBuilder.CreateTable(
190  name: "DreamMakerSettings",
191  columns: table => new
192  {
193  ProjectName = table.Column<string>(nullable: true),
194  ApiValidationPort = table.Column<int>(nullable: false),
195  Id = table.Column<long>(nullable: false)
196  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
197  InstanceId = table.Column<long>(nullable: false)
198  },
199  constraints: table =>
200  {
201  table.PrimaryKey("PK_DreamMakerSettings", x => x.Id);
202  table.ForeignKey(
203  name: "FK_DreamMakerSettings_Instances_InstanceId",
204  column: x => x.InstanceId,
205  principalTable: "Instances",
206  principalColumn: "Id",
207  onDelete: ReferentialAction.Cascade);
208  });
209 
210  migrationBuilder.CreateTable(
211  name: "InstanceUsers",
212  columns: table => new
213  {
214  UserId = table.Column<long>(nullable: false),
215  InstanceUserRights = table.Column<decimal>(nullable: false),
216  ByondRights = table.Column<decimal>(nullable: false),
217  DreamDaemonRights = table.Column<decimal>(nullable: false),
218  DreamMakerRights = table.Column<decimal>(nullable: false),
219  RepositoryRights = table.Column<decimal>(nullable: false),
220  ChatBotRights = table.Column<decimal>(nullable: false),
221  ConfigurationRights = table.Column<decimal>(nullable: false),
222  Id = table.Column<long>(nullable: false)
223  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
224  InstanceId = table.Column<long>(nullable: false)
225  },
226  constraints: table =>
227  {
228  table.PrimaryKey("PK_InstanceUsers", x => x.Id);
229  table.ForeignKey(
230  name: "FK_InstanceUsers_Instances_InstanceId",
231  column: x => x.InstanceId,
232  principalTable: "Instances",
233  principalColumn: "Id",
234  onDelete: ReferentialAction.Cascade);
235  table.ForeignKey(
236  name: "FK_InstanceUsers_Users_UserId",
237  column: x => x.UserId,
238  principalTable: "Users",
239  principalColumn: "Id",
240  onDelete: ReferentialAction.Cascade);
241  });
242 
243  migrationBuilder.CreateTable(
244  name: "Jobs",
245  columns: table => new
246  {
247  Id = table.Column<long>(nullable: false)
248  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
249  Description = table.Column<string>(nullable: false),
250  ExceptionDetails = table.Column<string>(nullable: true),
251  StartedAt = table.Column<DateTimeOffset>(nullable: false),
252  StoppedAt = table.Column<DateTimeOffset>(nullable: true),
253  Cancelled = table.Column<bool>(nullable: false),
254  CancelRightsType = table.Column<decimal>(nullable: true),
255  CancelRight = table.Column<decimal>(nullable: true),
256  StartedById = table.Column<long>(nullable: false),
257  CancelledById = table.Column<long>(nullable: true),
258  InstanceId = table.Column<long>(nullable: false)
259  },
260  constraints: table =>
261  {
262  table.PrimaryKey("PK_Jobs", x => x.Id);
263  table.ForeignKey(
264  name: "FK_Jobs_Users_CancelledById",
265  column: x => x.CancelledById,
266  principalTable: "Users",
267  principalColumn: "Id",
268  onDelete: ReferentialAction.Restrict);
269  table.ForeignKey(
270  name: "FK_Jobs_Instances_InstanceId",
271  column: x => x.InstanceId,
272  principalTable: "Instances",
273  principalColumn: "Id",
274  onDelete: ReferentialAction.Cascade);
275  table.ForeignKey(
276  name: "FK_Jobs_Users_StartedById",
277  column: x => x.StartedById,
278  principalTable: "Users",
279  principalColumn: "Id",
280  onDelete: ReferentialAction.Cascade);
281  });
282 
283  migrationBuilder.CreateTable(
284  name: "RepositorySettings",
285  columns: table => new
286  {
287  CommitterName = table.Column<string>(nullable: false),
288  CommitterEmail = table.Column<string>(nullable: false),
289  AccessUser = table.Column<string>(nullable: true),
290  AccessToken = table.Column<string>(nullable: true),
291  PushTestMergeCommits = table.Column<bool>(nullable: false),
292  ShowTestMergeCommitters = table.Column<bool>(nullable: false),
293  AutoUpdatesKeepTestMerges = table.Column<bool>(nullable: false),
294  AutoUpdatesSynchronize = table.Column<bool>(nullable: false),
295  Id = table.Column<long>(nullable: false)
296  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
297  InstanceId = table.Column<long>(nullable: false)
298  },
299  constraints: table =>
300  {
301  table.PrimaryKey("PK_RepositorySettings", x => x.Id);
302  table.ForeignKey(
303  name: "FK_RepositorySettings_Instances_InstanceId",
304  column: x => x.InstanceId,
305  principalTable: "Instances",
306  principalColumn: "Id",
307  onDelete: ReferentialAction.Cascade);
308  });
309 
310  migrationBuilder.CreateTable(
311  name: "RevisionInformations",
312  columns: table => new
313  {
314  CommitSha = table.Column<string>(maxLength: 40, nullable: false),
315  OriginCommitSha = table.Column<string>(maxLength: 40, nullable: false),
316  Id = table.Column<long>(nullable: false)
317  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
318  InstanceId = table.Column<long>(nullable: false)
319  },
320  constraints: table =>
321  {
322  table.PrimaryKey("PK_RevisionInformations", x => x.Id);
323  table.ForeignKey(
324  name: "FK_RevisionInformations_Instances_InstanceId",
325  column: x => x.InstanceId,
326  principalTable: "Instances",
327  principalColumn: "Id",
328  onDelete: ReferentialAction.Cascade);
329  });
330 
331  migrationBuilder.CreateTable(
332  name: "CompileJobs",
333  columns: table => new
334  {
335  Id = table.Column<long>(nullable: false)
336  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
337  DmeName = table.Column<string>(nullable: true),
338  Output = table.Column<string>(nullable: true),
339  DirectoryName = table.Column<Guid>(nullable: true),
340  JobId = table.Column<long>(nullable: true),
341  RevisionInformationId = table.Column<long>(nullable: false),
342  ByondVersion = table.Column<string>(nullable: false)
343  },
344  constraints: table =>
345  {
346  table.PrimaryKey("PK_CompileJobs", x => x.Id);
347  table.ForeignKey(
348  name: "FK_CompileJobs_Jobs_JobId",
349  column: x => x.JobId,
350  principalTable: "Jobs",
351  principalColumn: "Id",
352  onDelete: ReferentialAction.Restrict);
353  table.ForeignKey(
354  name: "FK_CompileJobs_RevisionInformations_RevisionInformationId",
355  column: x => x.RevisionInformationId,
356  principalTable: "RevisionInformations",
357  principalColumn: "Id",
358  onDelete: ReferentialAction.Cascade);
359  });
360 
361  migrationBuilder.CreateTable(
362  name: "TestMerges",
363  columns: table => new
364  {
365  Number = table.Column<int>(nullable: false),
366  PullRequestRevision = table.Column<string>(nullable: false),
367  Comment = table.Column<string>(nullable: true),
368  TitleAtMerge = table.Column<string>(nullable: false),
369  BodyAtMerge = table.Column<string>(nullable: false),
370  Url = table.Column<string>(nullable: false),
371  Author = table.Column<string>(nullable: false),
372  Id = table.Column<long>(nullable: false)
373  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
374  MergedAt = table.Column<DateTimeOffset>(nullable: false),
375  MergedById = table.Column<long>(nullable: false),
376  PrimaryRevisionInformationId = table.Column<long>(nullable: true)
377  },
378  constraints: table =>
379  {
380  table.PrimaryKey("PK_TestMerges", x => x.Id);
381  table.ForeignKey(
382  name: "FK_TestMerges_Users_MergedById",
383  column: x => x.MergedById,
384  principalTable: "Users",
385  principalColumn: "Id",
386  onDelete: ReferentialAction.Restrict);
387  table.ForeignKey(
388  name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId",
389  column: x => x.PrimaryRevisionInformationId,
390  principalTable: "RevisionInformations",
391  principalColumn: "Id",
392  onDelete: ReferentialAction.SetNull);
393  });
394 
395  migrationBuilder.CreateTable(
396  name: "RevInfoTestMerges",
397  columns: table => new
398  {
399  Id = table.Column<long>(nullable: false)
400  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
401  TestMergeId = table.Column<long>(nullable: false),
402  RevisionInformationId = table.Column<long>(nullable: false)
403  },
404  constraints: table =>
405  {
406  table.PrimaryKey("PK_RevInfoTestMerges", x => x.Id);
407  table.ForeignKey(
408  name: "FK_RevInfoTestMerges_RevisionInformations_RevisionInformationId",
409  column: x => x.RevisionInformationId,
410  principalTable: "RevisionInformations",
411  principalColumn: "Id",
412  onDelete: ReferentialAction.Cascade);
413  table.ForeignKey(
414  name: "FK_RevInfoTestMerges_TestMerges_TestMergeId",
415  column: x => x.TestMergeId,
416  principalTable: "TestMerges",
417  principalColumn: "Id",
418  onDelete: ReferentialAction.Cascade);
419  });
420 
421  migrationBuilder.CreateIndex(
422  name: "IX_ChatBots_InstanceId",
423  table: "ChatBots",
424  column: "InstanceId");
425 
426  migrationBuilder.CreateIndex(
427  name: "IX_ChatBots_Name",
428  table: "ChatBots",
429  column: "Name",
430  unique: true);
431 
432  migrationBuilder.CreateIndex(
433  name: "IX_ChatChannels_ChatSettingsId_DiscordChannelId",
434  table: "ChatChannels",
435  columns: new[] { "ChatSettingsId", "DiscordChannelId" },
436  unique: true,
437  filter: "[DiscordChannelId] IS NOT NULL");
438 
439  migrationBuilder.CreateIndex(
440  name: "IX_ChatChannels_ChatSettingsId_IrcChannel",
441  table: "ChatChannels",
442  columns: new[] { "ChatSettingsId", "IrcChannel" },
443  unique: true,
444  filter: "[IrcChannel] IS NOT NULL");
445 
446  migrationBuilder.CreateIndex(
447  name: "IX_CompileJobs_DirectoryName",
448  table: "CompileJobs",
449  column: "DirectoryName");
450 
451  migrationBuilder.CreateIndex(
452  name: "IX_CompileJobs_JobId",
453  table: "CompileJobs",
454  column: "JobId");
455 
456  migrationBuilder.CreateIndex(
457  name: "IX_CompileJobs_RevisionInformationId",
458  table: "CompileJobs",
459  column: "RevisionInformationId");
460 
461  migrationBuilder.CreateIndex(
462  name: "IX_DreamDaemonSettings_InstanceId",
463  table: "DreamDaemonSettings",
464  column: "InstanceId",
465  unique: true);
466 
467  migrationBuilder.CreateIndex(
468  name: "IX_DreamMakerSettings_InstanceId",
469  table: "DreamMakerSettings",
470  column: "InstanceId",
471  unique: true);
472 
473  migrationBuilder.CreateIndex(
474  name: "IX_Instances_Path",
475  table: "Instances",
476  column: "Path",
477  unique: true);
478 
479  migrationBuilder.CreateIndex(
480  name: "IX_Instances_WatchdogReattachInformationId",
481  table: "Instances",
482  column: "WatchdogReattachInformationId");
483 
484  migrationBuilder.CreateIndex(
485  name: "IX_InstanceUsers_InstanceId",
486  table: "InstanceUsers",
487  column: "InstanceId");
488 
489  migrationBuilder.CreateIndex(
490  name: "IX_InstanceUsers_UserId_InstanceId",
491  table: "InstanceUsers",
492  columns: new[] { "UserId", "InstanceId" },
493  unique: true);
494 
495  migrationBuilder.CreateIndex(
496  name: "IX_Jobs_CancelledById",
497  table: "Jobs",
498  column: "CancelledById");
499 
500  migrationBuilder.CreateIndex(
501  name: "IX_Jobs_InstanceId",
502  table: "Jobs",
503  column: "InstanceId");
504 
505  migrationBuilder.CreateIndex(
506  name: "IX_Jobs_StartedById",
507  table: "Jobs",
508  column: "StartedById");
509 
510  migrationBuilder.CreateIndex(
511  name: "IX_ReattachInformations_CompileJobId",
512  table: "ReattachInformations",
513  column: "CompileJobId");
514 
515  migrationBuilder.CreateIndex(
516  name: "IX_RepositorySettings_InstanceId",
517  table: "RepositorySettings",
518  column: "InstanceId",
519  unique: true);
520 
521  migrationBuilder.CreateIndex(
522  name: "IX_RevInfoTestMerges_RevisionInformationId",
523  table: "RevInfoTestMerges",
524  column: "RevisionInformationId");
525 
526  migrationBuilder.CreateIndex(
527  name: "IX_RevInfoTestMerges_TestMergeId",
528  table: "RevInfoTestMerges",
529  column: "TestMergeId");
530 
531  migrationBuilder.CreateIndex(
532  name: "IX_RevisionInformations_CommitSha",
533  table: "RevisionInformations",
534  column: "CommitSha",
535  unique: true);
536 
537  migrationBuilder.CreateIndex(
538  name: "IX_RevisionInformations_InstanceId",
539  table: "RevisionInformations",
540  column: "InstanceId");
541 
542  migrationBuilder.CreateIndex(
543  name: "IX_TestMerges_MergedById",
544  table: "TestMerges",
545  column: "MergedById");
546 
547  migrationBuilder.CreateIndex(
548  name: "IX_TestMerges_PrimaryRevisionInformationId",
549  table: "TestMerges",
550  column: "PrimaryRevisionInformationId",
551  unique: true,
552  filter: "[PrimaryRevisionInformationId] IS NOT NULL");
553 
554  migrationBuilder.CreateIndex(
555  name: "IX_Users_CanonicalName",
556  table: "Users",
557  column: "CanonicalName",
558  unique: true);
559 
560  migrationBuilder.CreateIndex(
561  name: "IX_Users_CreatedById",
562  table: "Users",
563  column: "CreatedById");
564 
565  migrationBuilder.CreateIndex(
566  name: "IX_WatchdogReattachInformations_AlphaId",
567  table: "WatchdogReattachInformations",
568  column: "AlphaId");
569 
570  migrationBuilder.CreateIndex(
571  name: "IX_WatchdogReattachInformations_BravoId",
572  table: "WatchdogReattachInformations",
573  column: "BravoId");
574 
575  migrationBuilder.AddForeignKey(
576  name: "FK_ChatChannels_ChatBots_ChatSettingsId",
577  table: "ChatChannels",
578  column: "ChatSettingsId",
579  principalTable: "ChatBots",
580  principalColumn: "Id",
581  onDelete: ReferentialAction.Cascade);
582 
583  migrationBuilder.AddForeignKey(
584  name: "FK_ReattachInformations_CompileJobs_CompileJobId",
585  table: "ReattachInformations",
586  column: "CompileJobId",
587  principalTable: "CompileJobs",
588  principalColumn: "Id",
589  onDelete: ReferentialAction.Restrict);
590  }
591 
596  protected override void Down(MigrationBuilder migrationBuilder)
597  {
598  migrationBuilder.DropForeignKey(
599  name: "FK_Jobs_Instances_InstanceId",
600  table: "Jobs");
601 
602  migrationBuilder.DropForeignKey(
603  name: "FK_RevisionInformations_Instances_InstanceId",
604  table: "RevisionInformations");
605 
606  migrationBuilder.DropTable(
607  name: "ChatChannels");
608 
609  migrationBuilder.DropTable(
610  name: "DreamDaemonSettings");
611 
612  migrationBuilder.DropTable(
613  name: "DreamMakerSettings");
614 
615  migrationBuilder.DropTable(
616  name: "InstanceUsers");
617 
618  migrationBuilder.DropTable(
619  name: "RepositorySettings");
620 
621  migrationBuilder.DropTable(
622  name: "RevInfoTestMerges");
623 
624  migrationBuilder.DropTable(
625  name: "ChatBots");
626 
627  migrationBuilder.DropTable(
628  name: "TestMerges");
629 
630  migrationBuilder.DropTable(
631  name: "Instances");
632 
633  migrationBuilder.DropTable(
634  name: "WatchdogReattachInformations");
635 
636  migrationBuilder.DropTable(
637  name: "ReattachInformations");
638 
639  migrationBuilder.DropTable(
640  name: "CompileJobs");
641 
642  migrationBuilder.DropTable(
643  name: "Jobs");
644 
645  migrationBuilder.DropTable(
646  name: "RevisionInformations");
647 
648  migrationBuilder.DropTable(
649  name: "Users");
650  }
651  }
652 }
ConfigurationRights
Rights for Models.ConfigurationFile
-
override void Down(MigrationBuilder migrationBuilder)
Unapplies the migration
-
override void Up(MigrationBuilder migrationBuilder)
Applies the migration
+Go to the documentation of this file.
1 using System;
2 using Microsoft.EntityFrameworkCore.Metadata;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 
5 namespace Tgstation.Server.Host.Models.Migrations
6 {
10  #pragma warning disable CA1506
11  public partial class MSInitialCreate : Migration
12  {
17  protected override void Up(MigrationBuilder migrationBuilder)
18  {
19  migrationBuilder.CreateTable(
20  name: "Users",
21  columns: table => new
22  {
23  Id = table.Column<long>(nullable: false)
24  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
25  Enabled = table.Column<bool>(nullable: false),
26  CreatedAt = table.Column<DateTimeOffset>(nullable: false),
27  SystemIdentifier = table.Column<string>(nullable: true),
28  Name = table.Column<string>(nullable: false),
29  AdministrationRights = table.Column<decimal>(nullable: false),
30  InstanceManagerRights = table.Column<decimal>(nullable: false),
31  PasswordHash = table.Column<string>(nullable: true),
32  CreatedById = table.Column<long>(nullable: true),
33  CanonicalName = table.Column<string>(nullable: false),
34  LastPasswordUpdate = table.Column<DateTimeOffset>(nullable: true)
35  },
36  constraints: table =>
37  {
38  table.PrimaryKey("PK_Users", x => x.Id);
39  table.ForeignKey(
40  name: "FK_Users_Users_CreatedById",
41  column: x => x.CreatedById,
42  principalTable: "Users",
43  principalColumn: "Id",
44  onDelete: ReferentialAction.Restrict);
45  });
46 
47  migrationBuilder.CreateTable(
48  name: "ChatChannels",
49  columns: table => new
50  {
51  IrcChannel = table.Column<string>(nullable: true),
52  DiscordChannelId = table.Column<decimal>(nullable: true),
53  IsAdminChannel = table.Column<bool>(nullable: false),
54  IsWatchdogChannel = table.Column<bool>(nullable: false),
55  IsUpdatesChannel = table.Column<bool>(nullable: false),
56  Tag = table.Column<string>(nullable: true),
57  Id = table.Column<long>(nullable: false)
58  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
59  ChatSettingsId = table.Column<long>(nullable: false)
60  },
61  constraints: table =>
62  {
63  table.PrimaryKey("PK_ChatChannels", x => x.Id);
64  });
65 
66  migrationBuilder.CreateTable(
67  name: "ReattachInformations",
68  columns: table => new
69  {
70  ChatCommandsJson = table.Column<string>(nullable: false),
71  ChatChannelsJson = table.Column<string>(nullable: false),
72  ServerCommandsJson = table.Column<string>(nullable: false),
73  AccessIdentifier = table.Column<string>(nullable: false),
74  ProcessId = table.Column<int>(nullable: false),
75  IsPrimary = table.Column<bool>(nullable: false),
76  Port = table.Column<int>(nullable: false),
77  RebootState = table.Column<int>(nullable: false),
78  Id = table.Column<long>(nullable: false)
79  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
80  CompileJobId = table.Column<long>(nullable: true)
81  },
82  constraints: table =>
83  {
84  table.PrimaryKey("PK_ReattachInformations", x => x.Id);
85  });
86 
87  migrationBuilder.CreateTable(
88  name: "WatchdogReattachInformations",
89  columns: table => new
90  {
91  AlphaIsActive = table.Column<bool>(nullable: false),
92  Id = table.Column<long>(nullable: false)
93  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
94  AlphaId = table.Column<long>(nullable: true),
95  BravoId = table.Column<long>(nullable: true)
96  },
97  constraints: table =>
98  {
99  table.PrimaryKey("PK_WatchdogReattachInformations", x => x.Id);
100  table.ForeignKey(
101  name: "FK_WatchdogReattachInformations_ReattachInformations_AlphaId",
102  column: x => x.AlphaId,
103  principalTable: "ReattachInformations",
104  principalColumn: "Id",
105  onDelete: ReferentialAction.Restrict);
106  table.ForeignKey(
107  name: "FK_WatchdogReattachInformations_ReattachInformations_BravoId",
108  column: x => x.BravoId,
109  principalTable: "ReattachInformations",
110  principalColumn: "Id",
111  onDelete: ReferentialAction.Restrict);
112  });
113 
114  migrationBuilder.CreateTable(
115  name: "Instances",
116  columns: table => new
117  {
118  Id = table.Column<long>(nullable: false)
119  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
120  Name = table.Column<string>(nullable: false),
121  Path = table.Column<string>(nullable: false),
122  Online = table.Column<bool>(nullable: false),
123  ConfigurationType = table.Column<int>(nullable: false),
124  AutoUpdateInterval = table.Column<long>(nullable: false),
125  WatchdogReattachInformationId = table.Column<long>(nullable: true)
126  },
127  constraints: table =>
128  {
129  table.PrimaryKey("PK_Instances", x => x.Id);
130  table.ForeignKey(
131  name: "FK_Instances_WatchdogReattachInformations_WatchdogReattachInformationId",
132  column: x => x.WatchdogReattachInformationId,
133  principalTable: "WatchdogReattachInformations",
134  principalColumn: "Id",
135  onDelete: ReferentialAction.Restrict);
136  });
137 
138  migrationBuilder.CreateTable(
139  name: "ChatBots",
140  columns: table => new
141  {
142  Id = table.Column<long>(nullable: false)
143  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
144  Name = table.Column<string>(nullable: false),
145  Enabled = table.Column<bool>(nullable: true),
146  Provider = table.Column<int>(nullable: true),
147  ConnectionString = table.Column<string>(nullable: false),
148  InstanceId = table.Column<long>(nullable: false)
149  },
150  constraints: table =>
151  {
152  table.PrimaryKey("PK_ChatBots", x => x.Id);
153  table.ForeignKey(
154  name: "FK_ChatBots_Instances_InstanceId",
155  column: x => x.InstanceId,
156  principalTable: "Instances",
157  principalColumn: "Id",
158  onDelete: ReferentialAction.Cascade);
159  });
160 
161  migrationBuilder.CreateTable(
162  name: "DreamDaemonSettings",
163  columns: table => new
164  {
165  AllowWebClient = table.Column<bool>(nullable: false),
166  SecurityLevel = table.Column<int>(nullable: false),
167  PrimaryPort = table.Column<int>(nullable: false),
168  SecondaryPort = table.Column<int>(nullable: false),
169  StartupTimeout = table.Column<long>(nullable: false),
170  AutoStart = table.Column<bool>(nullable: false),
171  SoftRestart = table.Column<bool>(nullable: false),
172  SoftShutdown = table.Column<bool>(nullable: false),
173  Id = table.Column<long>(nullable: false)
174  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
175  ProcessId = table.Column<int>(nullable: true),
176  AccessToken = table.Column<string>(nullable: true),
177  InstanceId = table.Column<long>(nullable: false)
178  },
179  constraints: table =>
180  {
181  table.PrimaryKey("PK_DreamDaemonSettings", x => x.Id);
182  table.ForeignKey(
183  name: "FK_DreamDaemonSettings_Instances_InstanceId",
184  column: x => x.InstanceId,
185  principalTable: "Instances",
186  principalColumn: "Id",
187  onDelete: ReferentialAction.Cascade);
188  });
189 
190  migrationBuilder.CreateTable(
191  name: "DreamMakerSettings",
192  columns: table => new
193  {
194  ProjectName = table.Column<string>(nullable: true),
195  ApiValidationPort = table.Column<int>(nullable: false),
196  Id = table.Column<long>(nullable: false)
197  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
198  InstanceId = table.Column<long>(nullable: false)
199  },
200  constraints: table =>
201  {
202  table.PrimaryKey("PK_DreamMakerSettings", x => x.Id);
203  table.ForeignKey(
204  name: "FK_DreamMakerSettings_Instances_InstanceId",
205  column: x => x.InstanceId,
206  principalTable: "Instances",
207  principalColumn: "Id",
208  onDelete: ReferentialAction.Cascade);
209  });
210 
211  migrationBuilder.CreateTable(
212  name: "InstanceUsers",
213  columns: table => new
214  {
215  UserId = table.Column<long>(nullable: false),
216  InstanceUserRights = table.Column<decimal>(nullable: false),
217  ByondRights = table.Column<decimal>(nullable: false),
218  DreamDaemonRights = table.Column<decimal>(nullable: false),
219  DreamMakerRights = table.Column<decimal>(nullable: false),
220  RepositoryRights = table.Column<decimal>(nullable: false),
221  ChatBotRights = table.Column<decimal>(nullable: false),
222  ConfigurationRights = table.Column<decimal>(nullable: false),
223  Id = table.Column<long>(nullable: false)
224  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
225  InstanceId = table.Column<long>(nullable: false)
226  },
227  constraints: table =>
228  {
229  table.PrimaryKey("PK_InstanceUsers", x => x.Id);
230  table.ForeignKey(
231  name: "FK_InstanceUsers_Instances_InstanceId",
232  column: x => x.InstanceId,
233  principalTable: "Instances",
234  principalColumn: "Id",
235  onDelete: ReferentialAction.Cascade);
236  table.ForeignKey(
237  name: "FK_InstanceUsers_Users_UserId",
238  column: x => x.UserId,
239  principalTable: "Users",
240  principalColumn: "Id",
241  onDelete: ReferentialAction.Cascade);
242  });
243 
244  migrationBuilder.CreateTable(
245  name: "Jobs",
246  columns: table => new
247  {
248  Id = table.Column<long>(nullable: false)
249  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
250  Description = table.Column<string>(nullable: false),
251  ExceptionDetails = table.Column<string>(nullable: true),
252  StartedAt = table.Column<DateTimeOffset>(nullable: false),
253  StoppedAt = table.Column<DateTimeOffset>(nullable: true),
254  Cancelled = table.Column<bool>(nullable: false),
255  CancelRightsType = table.Column<decimal>(nullable: true),
256  CancelRight = table.Column<decimal>(nullable: true),
257  StartedById = table.Column<long>(nullable: false),
258  CancelledById = table.Column<long>(nullable: true),
259  InstanceId = table.Column<long>(nullable: false)
260  },
261  constraints: table =>
262  {
263  table.PrimaryKey("PK_Jobs", x => x.Id);
264  table.ForeignKey(
265  name: "FK_Jobs_Users_CancelledById",
266  column: x => x.CancelledById,
267  principalTable: "Users",
268  principalColumn: "Id",
269  onDelete: ReferentialAction.Restrict);
270  table.ForeignKey(
271  name: "FK_Jobs_Instances_InstanceId",
272  column: x => x.InstanceId,
273  principalTable: "Instances",
274  principalColumn: "Id",
275  onDelete: ReferentialAction.Cascade);
276  table.ForeignKey(
277  name: "FK_Jobs_Users_StartedById",
278  column: x => x.StartedById,
279  principalTable: "Users",
280  principalColumn: "Id",
281  onDelete: ReferentialAction.Cascade);
282  });
283 
284  migrationBuilder.CreateTable(
285  name: "RepositorySettings",
286  columns: table => new
287  {
288  CommitterName = table.Column<string>(nullable: false),
289  CommitterEmail = table.Column<string>(nullable: false),
290  AccessUser = table.Column<string>(nullable: true),
291  AccessToken = table.Column<string>(nullable: true),
292  PushTestMergeCommits = table.Column<bool>(nullable: false),
293  ShowTestMergeCommitters = table.Column<bool>(nullable: false),
294  AutoUpdatesKeepTestMerges = table.Column<bool>(nullable: false),
295  AutoUpdatesSynchronize = table.Column<bool>(nullable: false),
296  Id = table.Column<long>(nullable: false)
297  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
298  InstanceId = table.Column<long>(nullable: false)
299  },
300  constraints: table =>
301  {
302  table.PrimaryKey("PK_RepositorySettings", x => x.Id);
303  table.ForeignKey(
304  name: "FK_RepositorySettings_Instances_InstanceId",
305  column: x => x.InstanceId,
306  principalTable: "Instances",
307  principalColumn: "Id",
308  onDelete: ReferentialAction.Cascade);
309  });
310 
311  migrationBuilder.CreateTable(
312  name: "RevisionInformations",
313  columns: table => new
314  {
315  CommitSha = table.Column<string>(maxLength: 40, nullable: false),
316  OriginCommitSha = table.Column<string>(maxLength: 40, nullable: false),
317  Id = table.Column<long>(nullable: false)
318  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
319  InstanceId = table.Column<long>(nullable: false)
320  },
321  constraints: table =>
322  {
323  table.PrimaryKey("PK_RevisionInformations", x => x.Id);
324  table.ForeignKey(
325  name: "FK_RevisionInformations_Instances_InstanceId",
326  column: x => x.InstanceId,
327  principalTable: "Instances",
328  principalColumn: "Id",
329  onDelete: ReferentialAction.Cascade);
330  });
331 
332  migrationBuilder.CreateTable(
333  name: "CompileJobs",
334  columns: table => new
335  {
336  Id = table.Column<long>(nullable: false)
337  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
338  DmeName = table.Column<string>(nullable: true),
339  Output = table.Column<string>(nullable: true),
340  DirectoryName = table.Column<Guid>(nullable: true),
341  JobId = table.Column<long>(nullable: true),
342  RevisionInformationId = table.Column<long>(nullable: false),
343  ByondVersion = table.Column<string>(nullable: false)
344  },
345  constraints: table =>
346  {
347  table.PrimaryKey("PK_CompileJobs", x => x.Id);
348  table.ForeignKey(
349  name: "FK_CompileJobs_Jobs_JobId",
350  column: x => x.JobId,
351  principalTable: "Jobs",
352  principalColumn: "Id",
353  onDelete: ReferentialAction.Restrict);
354  table.ForeignKey(
355  name: "FK_CompileJobs_RevisionInformations_RevisionInformationId",
356  column: x => x.RevisionInformationId,
357  principalTable: "RevisionInformations",
358  principalColumn: "Id",
359  onDelete: ReferentialAction.Cascade);
360  });
361 
362  migrationBuilder.CreateTable(
363  name: "TestMerges",
364  columns: table => new
365  {
366  Number = table.Column<int>(nullable: false),
367  PullRequestRevision = table.Column<string>(nullable: false),
368  Comment = table.Column<string>(nullable: true),
369  TitleAtMerge = table.Column<string>(nullable: false),
370  BodyAtMerge = table.Column<string>(nullable: false),
371  Url = table.Column<string>(nullable: false),
372  Author = table.Column<string>(nullable: false),
373  Id = table.Column<long>(nullable: false)
374  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
375  MergedAt = table.Column<DateTimeOffset>(nullable: false),
376  MergedById = table.Column<long>(nullable: false),
377  PrimaryRevisionInformationId = table.Column<long>(nullable: true)
378  },
379  constraints: table =>
380  {
381  table.PrimaryKey("PK_TestMerges", x => x.Id);
382  table.ForeignKey(
383  name: "FK_TestMerges_Users_MergedById",
384  column: x => x.MergedById,
385  principalTable: "Users",
386  principalColumn: "Id",
387  onDelete: ReferentialAction.Restrict);
388  table.ForeignKey(
389  name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId",
390  column: x => x.PrimaryRevisionInformationId,
391  principalTable: "RevisionInformations",
392  principalColumn: "Id",
393  onDelete: ReferentialAction.SetNull);
394  });
395 
396  migrationBuilder.CreateTable(
397  name: "RevInfoTestMerges",
398  columns: table => new
399  {
400  Id = table.Column<long>(nullable: false)
401  .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
402  TestMergeId = table.Column<long>(nullable: false),
403  RevisionInformationId = table.Column<long>(nullable: false)
404  },
405  constraints: table =>
406  {
407  table.PrimaryKey("PK_RevInfoTestMerges", x => x.Id);
408  table.ForeignKey(
409  name: "FK_RevInfoTestMerges_RevisionInformations_RevisionInformationId",
410  column: x => x.RevisionInformationId,
411  principalTable: "RevisionInformations",
412  principalColumn: "Id",
413  onDelete: ReferentialAction.Cascade);
414  table.ForeignKey(
415  name: "FK_RevInfoTestMerges_TestMerges_TestMergeId",
416  column: x => x.TestMergeId,
417  principalTable: "TestMerges",
418  principalColumn: "Id",
419  onDelete: ReferentialAction.Cascade);
420  });
421 
422  migrationBuilder.CreateIndex(
423  name: "IX_ChatBots_InstanceId",
424  table: "ChatBots",
425  column: "InstanceId");
426 
427  migrationBuilder.CreateIndex(
428  name: "IX_ChatBots_Name",
429  table: "ChatBots",
430  column: "Name",
431  unique: true);
432 
433  migrationBuilder.CreateIndex(
434  name: "IX_ChatChannels_ChatSettingsId_DiscordChannelId",
435  table: "ChatChannels",
436  columns: new[] { "ChatSettingsId", "DiscordChannelId" },
437  unique: true,
438  filter: "[DiscordChannelId] IS NOT NULL");
439 
440  migrationBuilder.CreateIndex(
441  name: "IX_ChatChannels_ChatSettingsId_IrcChannel",
442  table: "ChatChannels",
443  columns: new[] { "ChatSettingsId", "IrcChannel" },
444  unique: true,
445  filter: "[IrcChannel] IS NOT NULL");
446 
447  migrationBuilder.CreateIndex(
448  name: "IX_CompileJobs_DirectoryName",
449  table: "CompileJobs",
450  column: "DirectoryName");
451 
452  migrationBuilder.CreateIndex(
453  name: "IX_CompileJobs_JobId",
454  table: "CompileJobs",
455  column: "JobId");
456 
457  migrationBuilder.CreateIndex(
458  name: "IX_CompileJobs_RevisionInformationId",
459  table: "CompileJobs",
460  column: "RevisionInformationId");
461 
462  migrationBuilder.CreateIndex(
463  name: "IX_DreamDaemonSettings_InstanceId",
464  table: "DreamDaemonSettings",
465  column: "InstanceId",
466  unique: true);
467 
468  migrationBuilder.CreateIndex(
469  name: "IX_DreamMakerSettings_InstanceId",
470  table: "DreamMakerSettings",
471  column: "InstanceId",
472  unique: true);
473 
474  migrationBuilder.CreateIndex(
475  name: "IX_Instances_Path",
476  table: "Instances",
477  column: "Path",
478  unique: true);
479 
480  migrationBuilder.CreateIndex(
481  name: "IX_Instances_WatchdogReattachInformationId",
482  table: "Instances",
483  column: "WatchdogReattachInformationId");
484 
485  migrationBuilder.CreateIndex(
486  name: "IX_InstanceUsers_InstanceId",
487  table: "InstanceUsers",
488  column: "InstanceId");
489 
490  migrationBuilder.CreateIndex(
491  name: "IX_InstanceUsers_UserId_InstanceId",
492  table: "InstanceUsers",
493  columns: new[] { "UserId", "InstanceId" },
494  unique: true);
495 
496  migrationBuilder.CreateIndex(
497  name: "IX_Jobs_CancelledById",
498  table: "Jobs",
499  column: "CancelledById");
500 
501  migrationBuilder.CreateIndex(
502  name: "IX_Jobs_InstanceId",
503  table: "Jobs",
504  column: "InstanceId");
505 
506  migrationBuilder.CreateIndex(
507  name: "IX_Jobs_StartedById",
508  table: "Jobs",
509  column: "StartedById");
510 
511  migrationBuilder.CreateIndex(
512  name: "IX_ReattachInformations_CompileJobId",
513  table: "ReattachInformations",
514  column: "CompileJobId");
515 
516  migrationBuilder.CreateIndex(
517  name: "IX_RepositorySettings_InstanceId",
518  table: "RepositorySettings",
519  column: "InstanceId",
520  unique: true);
521 
522  migrationBuilder.CreateIndex(
523  name: "IX_RevInfoTestMerges_RevisionInformationId",
524  table: "RevInfoTestMerges",
525  column: "RevisionInformationId");
526 
527  migrationBuilder.CreateIndex(
528  name: "IX_RevInfoTestMerges_TestMergeId",
529  table: "RevInfoTestMerges",
530  column: "TestMergeId");
531 
532  migrationBuilder.CreateIndex(
533  name: "IX_RevisionInformations_CommitSha",
534  table: "RevisionInformations",
535  column: "CommitSha",
536  unique: true);
537 
538  migrationBuilder.CreateIndex(
539  name: "IX_RevisionInformations_InstanceId",
540  table: "RevisionInformations",
541  column: "InstanceId");
542 
543  migrationBuilder.CreateIndex(
544  name: "IX_TestMerges_MergedById",
545  table: "TestMerges",
546  column: "MergedById");
547 
548  migrationBuilder.CreateIndex(
549  name: "IX_TestMerges_PrimaryRevisionInformationId",
550  table: "TestMerges",
551  column: "PrimaryRevisionInformationId",
552  unique: true,
553  filter: "[PrimaryRevisionInformationId] IS NOT NULL");
554 
555  migrationBuilder.CreateIndex(
556  name: "IX_Users_CanonicalName",
557  table: "Users",
558  column: "CanonicalName",
559  unique: true);
560 
561  migrationBuilder.CreateIndex(
562  name: "IX_Users_CreatedById",
563  table: "Users",
564  column: "CreatedById");
565 
566  migrationBuilder.CreateIndex(
567  name: "IX_WatchdogReattachInformations_AlphaId",
568  table: "WatchdogReattachInformations",
569  column: "AlphaId");
570 
571  migrationBuilder.CreateIndex(
572  name: "IX_WatchdogReattachInformations_BravoId",
573  table: "WatchdogReattachInformations",
574  column: "BravoId");
575 
576  migrationBuilder.AddForeignKey(
577  name: "FK_ChatChannels_ChatBots_ChatSettingsId",
578  table: "ChatChannels",
579  column: "ChatSettingsId",
580  principalTable: "ChatBots",
581  principalColumn: "Id",
582  onDelete: ReferentialAction.Cascade);
583 
584  migrationBuilder.AddForeignKey(
585  name: "FK_ReattachInformations_CompileJobs_CompileJobId",
586  table: "ReattachInformations",
587  column: "CompileJobId",
588  principalTable: "CompileJobs",
589  principalColumn: "Id",
590  onDelete: ReferentialAction.Restrict);
591  }
592 
597  protected override void Down(MigrationBuilder migrationBuilder)
598  {
599  migrationBuilder.DropForeignKey(
600  name: "FK_Jobs_Instances_InstanceId",
601  table: "Jobs");
602 
603  migrationBuilder.DropForeignKey(
604  name: "FK_RevisionInformations_Instances_InstanceId",
605  table: "RevisionInformations");
606 
607  migrationBuilder.DropTable(
608  name: "ChatChannels");
609 
610  migrationBuilder.DropTable(
611  name: "DreamDaemonSettings");
612 
613  migrationBuilder.DropTable(
614  name: "DreamMakerSettings");
615 
616  migrationBuilder.DropTable(
617  name: "InstanceUsers");
618 
619  migrationBuilder.DropTable(
620  name: "RepositorySettings");
621 
622  migrationBuilder.DropTable(
623  name: "RevInfoTestMerges");
624 
625  migrationBuilder.DropTable(
626  name: "ChatBots");
627 
628  migrationBuilder.DropTable(
629  name: "TestMerges");
630 
631  migrationBuilder.DropTable(
632  name: "Instances");
633 
634  migrationBuilder.DropTable(
635  name: "WatchdogReattachInformations");
636 
637  migrationBuilder.DropTable(
638  name: "ReattachInformations");
639 
640  migrationBuilder.DropTable(
641  name: "CompileJobs");
642 
643  migrationBuilder.DropTable(
644  name: "Jobs");
645 
646  migrationBuilder.DropTable(
647  name: "RevisionInformations");
648 
649  migrationBuilder.DropTable(
650  name: "Users");
651  }
652  }
653 }
ConfigurationRights
Rights for Models.ConfigurationFile
+
override void Down(MigrationBuilder migrationBuilder)
Unapplies the migration
+
override void Up(MigrationBuilder migrationBuilder)
Applies the migration
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
InstanceUserRights
Rights for an Models.Instance
@@ -105,7 +105,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManagerRights
Rights for managing Models.Instances
DreamMakerRights
Rights for Models.DreamMaker
RepositoryRights
Rights for a Models.Repository
- +
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
diff --git a/20180906143029___m_y_initial_create_8cs_source.html b/20180906143029___m_y_initial_create_8cs_source.html index 4d893365cd..f3d34f2f84 100644 --- a/20180906143029___m_y_initial_create_8cs_source.html +++ b/20180906143029___m_y_initial_create_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
20180906143029_MYInitialCreate.cs
-Go to the documentation of this file.
1 using System;
2 using Microsoft.EntityFrameworkCore.Metadata;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 
5 namespace Tgstation.Server.Host.Models.Migrations
6 {
10  public partial class MYInitialCreate : Migration
11  {
16  protected override void Up(MigrationBuilder migrationBuilder)
17  {
18  migrationBuilder.CreateTable(
19  name: "Users",
20  columns: table => new
21  {
22  Id = table.Column<long>(nullable: false)
23  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
24  Enabled = table.Column<bool>(nullable: false),
25  CreatedAt = table.Column<DateTimeOffset>(nullable: false),
26  SystemIdentifier = table.Column<string>(nullable: true),
27  Name = table.Column<string>(nullable: false),
28  AdministrationRights = table.Column<ulong>(nullable: false),
29  InstanceManagerRights = table.Column<ulong>(nullable: false),
30  PasswordHash = table.Column<string>(nullable: true),
31  CreatedById = table.Column<long>(nullable: true),
32  CanonicalName = table.Column<string>(nullable: false),
33  LastPasswordUpdate = table.Column<DateTimeOffset>(nullable: true)
34  },
35  constraints: table =>
36  {
37  table.PrimaryKey("PK_Users", x => x.Id);
38  table.ForeignKey(
39  name: "FK_Users_Users_CreatedById",
40  column: x => x.CreatedById,
41  principalTable: "Users",
42  principalColumn: "Id",
43  onDelete: ReferentialAction.Restrict);
44  });
45 
46  migrationBuilder.CreateTable(
47  name: "ChatChannels",
48  columns: table => new
49  {
50  IrcChannel = table.Column<string>(nullable: true),
51  DiscordChannelId = table.Column<ulong>(nullable: true),
52  IsAdminChannel = table.Column<bool>(nullable: false),
53  IsWatchdogChannel = table.Column<bool>(nullable: false),
54  IsUpdatesChannel = table.Column<bool>(nullable: false),
55  Tag = table.Column<string>(nullable: true),
56  Id = table.Column<long>(nullable: false)
57  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
58  ChatSettingsId = table.Column<long>(nullable: false)
59  },
60  constraints: table =>
61  {
62  table.PrimaryKey("PK_ChatChannels", x => x.Id);
63  });
64 
65  migrationBuilder.CreateTable(
66  name: "ReattachInformations",
67  columns: table => new
68  {
69  ChatCommandsJson = table.Column<string>(nullable: false),
70  ChatChannelsJson = table.Column<string>(nullable: false),
71  ServerCommandsJson = table.Column<string>(nullable: false),
72  AccessIdentifier = table.Column<string>(nullable: false),
73  ProcessId = table.Column<int>(nullable: false),
74  IsPrimary = table.Column<bool>(nullable: false),
75  Port = table.Column<ushort>(nullable: false),
76  RebootState = table.Column<int>(nullable: false),
77  Id = table.Column<long>(nullable: false)
78  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
79  CompileJobId = table.Column<long>(nullable: true)
80  },
81  constraints: table =>
82  {
83  table.PrimaryKey("PK_ReattachInformations", x => x.Id);
84  });
85 
86  migrationBuilder.CreateTable(
87  name: "WatchdogReattachInformations",
88  columns: table => new
89  {
90  AlphaIsActive = table.Column<bool>(nullable: false),
91  Id = table.Column<long>(nullable: false)
92  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
93  AlphaId = table.Column<long>(nullable: true),
94  BravoId = table.Column<long>(nullable: true)
95  },
96  constraints: table =>
97  {
98  table.PrimaryKey("PK_WatchdogReattachInformations", x => x.Id);
99  table.ForeignKey(
100  name: "FK_WatchdogReattachInformations_ReattachInformations_AlphaId",
101  column: x => x.AlphaId,
102  principalTable: "ReattachInformations",
103  principalColumn: "Id",
104  onDelete: ReferentialAction.Restrict);
105  table.ForeignKey(
106  name: "FK_WatchdogReattachInformations_ReattachInformations_BravoId",
107  column: x => x.BravoId,
108  principalTable: "ReattachInformations",
109  principalColumn: "Id",
110  onDelete: ReferentialAction.Restrict);
111  });
112 
113  migrationBuilder.CreateTable(
114  name: "Instances",
115  columns: table => new
116  {
117  Id = table.Column<long>(nullable: false)
118  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
119  Name = table.Column<string>(nullable: false),
120  Path = table.Column<string>(nullable: false),
121  Online = table.Column<bool>(nullable: false),
122  ConfigurationType = table.Column<int>(nullable: false),
123  AutoUpdateInterval = table.Column<uint>(nullable: false),
124  WatchdogReattachInformationId = table.Column<long>(nullable: true)
125  },
126  constraints: table =>
127  {
128  table.PrimaryKey("PK_Instances", x => x.Id);
129  table.ForeignKey(
130  name: "FK_Instances_WatchdogReattachInformations_WatchdogReattachInfor~",
131  column: x => x.WatchdogReattachInformationId,
132  principalTable: "WatchdogReattachInformations",
133  principalColumn: "Id",
134  onDelete: ReferentialAction.Restrict);
135  });
136 
137  migrationBuilder.CreateTable(
138  name: "ChatBots",
139  columns: table => new
140  {
141  Id = table.Column<long>(nullable: false)
142  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
143  Name = table.Column<string>(nullable: false),
144  Enabled = table.Column<bool>(nullable: true),
145  Provider = table.Column<int>(nullable: true),
146  ConnectionString = table.Column<string>(nullable: false),
147  InstanceId = table.Column<long>(nullable: false)
148  },
149  constraints: table =>
150  {
151  table.PrimaryKey("PK_ChatBots", x => x.Id);
152  table.ForeignKey(
153  name: "FK_ChatBots_Instances_InstanceId",
154  column: x => x.InstanceId,
155  principalTable: "Instances",
156  principalColumn: "Id",
157  onDelete: ReferentialAction.Cascade);
158  });
159 
160  migrationBuilder.CreateTable(
161  name: "DreamDaemonSettings",
162  columns: table => new
163  {
164  AllowWebClient = table.Column<bool>(nullable: false),
165  SecurityLevel = table.Column<int>(nullable: false),
166  PrimaryPort = table.Column<ushort>(nullable: false),
167  SecondaryPort = table.Column<ushort>(nullable: false),
168  StartupTimeout = table.Column<uint>(nullable: false),
169  AutoStart = table.Column<bool>(nullable: false),
170  SoftRestart = table.Column<bool>(nullable: false),
171  SoftShutdown = table.Column<bool>(nullable: false),
172  Id = table.Column<long>(nullable: false)
173  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
174  ProcessId = table.Column<int>(nullable: true),
175  AccessToken = table.Column<string>(nullable: true),
176  InstanceId = table.Column<long>(nullable: false)
177  },
178  constraints: table =>
179  {
180  table.PrimaryKey("PK_DreamDaemonSettings", x => x.Id);
181  table.ForeignKey(
182  name: "FK_DreamDaemonSettings_Instances_InstanceId",
183  column: x => x.InstanceId,
184  principalTable: "Instances",
185  principalColumn: "Id",
186  onDelete: ReferentialAction.Cascade);
187  });
188 
189  migrationBuilder.CreateTable(
190  name: "DreamMakerSettings",
191  columns: table => new
192  {
193  ProjectName = table.Column<string>(nullable: true),
194  ApiValidationPort = table.Column<ushort>(nullable: false),
195  Id = table.Column<long>(nullable: false)
196  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
197  InstanceId = table.Column<long>(nullable: false)
198  },
199  constraints: table =>
200  {
201  table.PrimaryKey("PK_DreamMakerSettings", x => x.Id);
202  table.ForeignKey(
203  name: "FK_DreamMakerSettings_Instances_InstanceId",
204  column: x => x.InstanceId,
205  principalTable: "Instances",
206  principalColumn: "Id",
207  onDelete: ReferentialAction.Cascade);
208  });
209 
210  migrationBuilder.CreateTable(
211  name: "InstanceUsers",
212  columns: table => new
213  {
214  UserId = table.Column<long>(nullable: false),
215  InstanceUserRights = table.Column<ulong>(nullable: false),
216  ByondRights = table.Column<ulong>(nullable: false),
217  DreamDaemonRights = table.Column<ulong>(nullable: false),
218  DreamMakerRights = table.Column<ulong>(nullable: false),
219  RepositoryRights = table.Column<ulong>(nullable: false),
220  ChatBotRights = table.Column<ulong>(nullable: false),
221  ConfigurationRights = table.Column<ulong>(nullable: false),
222  Id = table.Column<long>(nullable: false)
223  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
224  InstanceId = table.Column<long>(nullable: false)
225  },
226  constraints: table =>
227  {
228  table.PrimaryKey("PK_InstanceUsers", x => x.Id);
229  table.ForeignKey(
230  name: "FK_InstanceUsers_Instances_InstanceId",
231  column: x => x.InstanceId,
232  principalTable: "Instances",
233  principalColumn: "Id",
234  onDelete: ReferentialAction.Cascade);
235  table.ForeignKey(
236  name: "FK_InstanceUsers_Users_UserId",
237  column: x => x.UserId,
238  principalTable: "Users",
239  principalColumn: "Id",
240  onDelete: ReferentialAction.Cascade);
241  });
242 
243  migrationBuilder.CreateTable(
244  name: "Jobs",
245  columns: table => new
246  {
247  Id = table.Column<long>(nullable: false)
248  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
249  Description = table.Column<string>(nullable: false),
250  ExceptionDetails = table.Column<string>(nullable: true),
251  StartedAt = table.Column<DateTimeOffset>(nullable: false),
252  StoppedAt = table.Column<DateTimeOffset>(nullable: true),
253  Cancelled = table.Column<bool>(nullable: false),
254  CancelRightsType = table.Column<ulong>(nullable: true),
255  CancelRight = table.Column<ulong>(nullable: true),
256  StartedById = table.Column<long>(nullable: false),
257  CancelledById = table.Column<long>(nullable: true),
258  InstanceId = table.Column<long>(nullable: false)
259  },
260  constraints: table =>
261  {
262  table.PrimaryKey("PK_Jobs", x => x.Id);
263  table.ForeignKey(
264  name: "FK_Jobs_Users_CancelledById",
265  column: x => x.CancelledById,
266  principalTable: "Users",
267  principalColumn: "Id",
268  onDelete: ReferentialAction.Restrict);
269  table.ForeignKey(
270  name: "FK_Jobs_Instances_InstanceId",
271  column: x => x.InstanceId,
272  principalTable: "Instances",
273  principalColumn: "Id",
274  onDelete: ReferentialAction.Cascade);
275  table.ForeignKey(
276  name: "FK_Jobs_Users_StartedById",
277  column: x => x.StartedById,
278  principalTable: "Users",
279  principalColumn: "Id",
280  onDelete: ReferentialAction.Cascade);
281  });
282 
283  migrationBuilder.CreateTable(
284  name: "RepositorySettings",
285  columns: table => new
286  {
287  CommitterName = table.Column<string>(nullable: false),
288  CommitterEmail = table.Column<string>(nullable: false),
289  AccessUser = table.Column<string>(nullable: true),
290  AccessToken = table.Column<string>(nullable: true),
291  PushTestMergeCommits = table.Column<bool>(nullable: false),
292  ShowTestMergeCommitters = table.Column<bool>(nullable: false),
293  AutoUpdatesKeepTestMerges = table.Column<bool>(nullable: false),
294  AutoUpdatesSynchronize = table.Column<bool>(nullable: false),
295  Id = table.Column<long>(nullable: false)
296  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
297  InstanceId = table.Column<long>(nullable: false)
298  },
299  constraints: table =>
300  {
301  table.PrimaryKey("PK_RepositorySettings", x => x.Id);
302  table.ForeignKey(
303  name: "FK_RepositorySettings_Instances_InstanceId",
304  column: x => x.InstanceId,
305  principalTable: "Instances",
306  principalColumn: "Id",
307  onDelete: ReferentialAction.Cascade);
308  });
309 
310  migrationBuilder.CreateTable(
311  name: "RevisionInformations",
312  columns: table => new
313  {
314  CommitSha = table.Column<string>(maxLength: 40, nullable: false),
315  OriginCommitSha = table.Column<string>(maxLength: 40, nullable: false),
316  Id = table.Column<long>(nullable: false)
317  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
318  InstanceId = table.Column<long>(nullable: false)
319  },
320  constraints: table =>
321  {
322  table.PrimaryKey("PK_RevisionInformations", x => x.Id);
323  table.ForeignKey(
324  name: "FK_RevisionInformations_Instances_InstanceId",
325  column: x => x.InstanceId,
326  principalTable: "Instances",
327  principalColumn: "Id",
328  onDelete: ReferentialAction.Cascade);
329  });
330 
331  migrationBuilder.CreateTable(
332  name: "CompileJobs",
333  columns: table => new
334  {
335  Id = table.Column<long>(nullable: false)
336  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
337  DmeName = table.Column<string>(nullable: true),
338  Output = table.Column<string>(nullable: true),
339  DirectoryName = table.Column<Guid>(nullable: true),
340  JobId = table.Column<long>(nullable: true),
341  RevisionInformationId = table.Column<long>(nullable: false),
342  ByondVersion = table.Column<string>(nullable: false)
343  },
344  constraints: table =>
345  {
346  table.PrimaryKey("PK_CompileJobs", x => x.Id);
347  table.ForeignKey(
348  name: "FK_CompileJobs_Jobs_JobId",
349  column: x => x.JobId,
350  principalTable: "Jobs",
351  principalColumn: "Id",
352  onDelete: ReferentialAction.Restrict);
353  table.ForeignKey(
354  name: "FK_CompileJobs_RevisionInformations_RevisionInformationId",
355  column: x => x.RevisionInformationId,
356  principalTable: "RevisionInformations",
357  principalColumn: "Id",
358  onDelete: ReferentialAction.Cascade);
359  });
360 
361  migrationBuilder.CreateTable(
362  name: "TestMerges",
363  columns: table => new
364  {
365  Number = table.Column<int>(nullable: false),
366  PullRequestRevision = table.Column<string>(nullable: false),
367  Comment = table.Column<string>(nullable: true),
368  TitleAtMerge = table.Column<string>(nullable: false),
369  BodyAtMerge = table.Column<string>(nullable: false),
370  Url = table.Column<string>(nullable: false),
371  Author = table.Column<string>(nullable: false),
372  Id = table.Column<long>(nullable: false)
373  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
374  MergedAt = table.Column<DateTimeOffset>(nullable: false),
375  MergedById = table.Column<long>(nullable: false),
376  PrimaryRevisionInformationId = table.Column<long>(nullable: true)
377  },
378  constraints: table =>
379  {
380  table.PrimaryKey("PK_TestMerges", x => x.Id);
381  table.ForeignKey(
382  name: "FK_TestMerges_Users_MergedById",
383  column: x => x.MergedById,
384  principalTable: "Users",
385  principalColumn: "Id",
386  onDelete: ReferentialAction.Restrict);
387  table.ForeignKey(
388  name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId",
389  column: x => x.PrimaryRevisionInformationId,
390  principalTable: "RevisionInformations",
391  principalColumn: "Id",
392  onDelete: ReferentialAction.SetNull);
393  });
394 
395  migrationBuilder.CreateTable(
396  name: "RevInfoTestMerges",
397  columns: table => new
398  {
399  Id = table.Column<long>(nullable: false)
400  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
401  TestMergeId = table.Column<long>(nullable: false),
402  RevisionInformationId = table.Column<long>(nullable: false)
403  },
404  constraints: table =>
405  {
406  table.PrimaryKey("PK_RevInfoTestMerges", x => x.Id);
407  table.ForeignKey(
408  name: "FK_RevInfoTestMerges_RevisionInformations_RevisionInformationId",
409  column: x => x.RevisionInformationId,
410  principalTable: "RevisionInformations",
411  principalColumn: "Id",
412  onDelete: ReferentialAction.Cascade);
413  table.ForeignKey(
414  name: "FK_RevInfoTestMerges_TestMerges_TestMergeId",
415  column: x => x.TestMergeId,
416  principalTable: "TestMerges",
417  principalColumn: "Id",
418  onDelete: ReferentialAction.Cascade);
419  });
420 
421  migrationBuilder.CreateIndex(
422  name: "IX_ChatBots_InstanceId",
423  table: "ChatBots",
424  column: "InstanceId");
425 
426  migrationBuilder.CreateIndex(
427  name: "IX_ChatBots_Name",
428  table: "ChatBots",
429  column: "Name",
430  unique: true);
431 
432  migrationBuilder.CreateIndex(
433  name: "IX_ChatChannels_ChatSettingsId_DiscordChannelId",
434  table: "ChatChannels",
435  columns: new[] { "ChatSettingsId", "DiscordChannelId" },
436  unique: true);
437 
438  migrationBuilder.CreateIndex(
439  name: "IX_ChatChannels_ChatSettingsId_IrcChannel",
440  table: "ChatChannels",
441  columns: new[] { "ChatSettingsId", "IrcChannel" },
442  unique: true);
443 
444  migrationBuilder.CreateIndex(
445  name: "IX_CompileJobs_DirectoryName",
446  table: "CompileJobs",
447  column: "DirectoryName");
448 
449  migrationBuilder.CreateIndex(
450  name: "IX_CompileJobs_JobId",
451  table: "CompileJobs",
452  column: "JobId");
453 
454  migrationBuilder.CreateIndex(
455  name: "IX_CompileJobs_RevisionInformationId",
456  table: "CompileJobs",
457  column: "RevisionInformationId");
458 
459  migrationBuilder.CreateIndex(
460  name: "IX_DreamDaemonSettings_InstanceId",
461  table: "DreamDaemonSettings",
462  column: "InstanceId",
463  unique: true);
464 
465  migrationBuilder.CreateIndex(
466  name: "IX_DreamMakerSettings_InstanceId",
467  table: "DreamMakerSettings",
468  column: "InstanceId",
469  unique: true);
470 
471  migrationBuilder.CreateIndex(
472  name: "IX_Instances_Path",
473  table: "Instances",
474  column: "Path",
475  unique: true);
476 
477  migrationBuilder.CreateIndex(
478  name: "IX_Instances_WatchdogReattachInformationId",
479  table: "Instances",
480  column: "WatchdogReattachInformationId");
481 
482  migrationBuilder.CreateIndex(
483  name: "IX_InstanceUsers_InstanceId",
484  table: "InstanceUsers",
485  column: "InstanceId");
486 
487  migrationBuilder.CreateIndex(
488  name: "IX_InstanceUsers_UserId_InstanceId",
489  table: "InstanceUsers",
490  columns: new[] { "UserId", "InstanceId" },
491  unique: true);
492 
493  migrationBuilder.CreateIndex(
494  name: "IX_Jobs_CancelledById",
495  table: "Jobs",
496  column: "CancelledById");
497 
498  migrationBuilder.CreateIndex(
499  name: "IX_Jobs_InstanceId",
500  table: "Jobs",
501  column: "InstanceId");
502 
503  migrationBuilder.CreateIndex(
504  name: "IX_Jobs_StartedById",
505  table: "Jobs",
506  column: "StartedById");
507 
508  migrationBuilder.CreateIndex(
509  name: "IX_ReattachInformations_CompileJobId",
510  table: "ReattachInformations",
511  column: "CompileJobId");
512 
513  migrationBuilder.CreateIndex(
514  name: "IX_RepositorySettings_InstanceId",
515  table: "RepositorySettings",
516  column: "InstanceId",
517  unique: true);
518 
519  migrationBuilder.CreateIndex(
520  name: "IX_RevInfoTestMerges_RevisionInformationId",
521  table: "RevInfoTestMerges",
522  column: "RevisionInformationId");
523 
524  migrationBuilder.CreateIndex(
525  name: "IX_RevInfoTestMerges_TestMergeId",
526  table: "RevInfoTestMerges",
527  column: "TestMergeId");
528 
529  migrationBuilder.CreateIndex(
530  name: "IX_RevisionInformations_CommitSha",
531  table: "RevisionInformations",
532  column: "CommitSha",
533  unique: true);
534 
535  migrationBuilder.CreateIndex(
536  name: "IX_RevisionInformations_InstanceId",
537  table: "RevisionInformations",
538  column: "InstanceId");
539 
540  migrationBuilder.CreateIndex(
541  name: "IX_TestMerges_MergedById",
542  table: "TestMerges",
543  column: "MergedById");
544 
545  migrationBuilder.CreateIndex(
546  name: "IX_TestMerges_PrimaryRevisionInformationId",
547  table: "TestMerges",
548  column: "PrimaryRevisionInformationId",
549  unique: true);
550 
551  migrationBuilder.CreateIndex(
552  name: "IX_Users_CanonicalName",
553  table: "Users",
554  column: "CanonicalName",
555  unique: true);
556 
557  migrationBuilder.CreateIndex(
558  name: "IX_Users_CreatedById",
559  table: "Users",
560  column: "CreatedById");
561 
562  migrationBuilder.CreateIndex(
563  name: "IX_WatchdogReattachInformations_AlphaId",
564  table: "WatchdogReattachInformations",
565  column: "AlphaId");
566 
567  migrationBuilder.CreateIndex(
568  name: "IX_WatchdogReattachInformations_BravoId",
569  table: "WatchdogReattachInformations",
570  column: "BravoId");
571 
572  migrationBuilder.AddForeignKey(
573  name: "FK_ChatChannels_ChatBots_ChatSettingsId",
574  table: "ChatChannels",
575  column: "ChatSettingsId",
576  principalTable: "ChatBots",
577  principalColumn: "Id",
578  onDelete: ReferentialAction.Cascade);
579 
580  migrationBuilder.AddForeignKey(
581  name: "FK_ReattachInformations_CompileJobs_CompileJobId",
582  table: "ReattachInformations",
583  column: "CompileJobId",
584  principalTable: "CompileJobs",
585  principalColumn: "Id",
586  onDelete: ReferentialAction.Restrict);
587  }
588 
593  protected override void Down(MigrationBuilder migrationBuilder)
594  {
595  migrationBuilder.DropForeignKey(
596  name: "FK_Jobs_Instances_InstanceId",
597  table: "Jobs");
598 
599  migrationBuilder.DropForeignKey(
600  name: "FK_RevisionInformations_Instances_InstanceId",
601  table: "RevisionInformations");
602 
603  migrationBuilder.DropTable(
604  name: "ChatChannels");
605 
606  migrationBuilder.DropTable(
607  name: "DreamDaemonSettings");
608 
609  migrationBuilder.DropTable(
610  name: "DreamMakerSettings");
611 
612  migrationBuilder.DropTable(
613  name: "InstanceUsers");
614 
615  migrationBuilder.DropTable(
616  name: "RepositorySettings");
617 
618  migrationBuilder.DropTable(
619  name: "RevInfoTestMerges");
620 
621  migrationBuilder.DropTable(
622  name: "ChatBots");
623 
624  migrationBuilder.DropTable(
625  name: "TestMerges");
626 
627  migrationBuilder.DropTable(
628  name: "Instances");
629 
630  migrationBuilder.DropTable(
631  name: "WatchdogReattachInformations");
632 
633  migrationBuilder.DropTable(
634  name: "ReattachInformations");
635 
636  migrationBuilder.DropTable(
637  name: "CompileJobs");
638 
639  migrationBuilder.DropTable(
640  name: "Jobs");
641 
642  migrationBuilder.DropTable(
643  name: "RevisionInformations");
644 
645  migrationBuilder.DropTable(
646  name: "Users");
647  }
648  }
649 }
ConfigurationRights
Rights for Models.ConfigurationFile
+Go to the documentation of this file.
1 using System;
2 using Microsoft.EntityFrameworkCore.Metadata;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 
5 namespace Tgstation.Server.Host.Models.Migrations
6 {
10  #pragma warning disable CA1506
11  public partial class MYInitialCreate : Migration
12  {
17  protected override void Up(MigrationBuilder migrationBuilder)
18  {
19  migrationBuilder.CreateTable(
20  name: "Users",
21  columns: table => new
22  {
23  Id = table.Column<long>(nullable: false)
24  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
25  Enabled = table.Column<bool>(nullable: false),
26  CreatedAt = table.Column<DateTimeOffset>(nullable: false),
27  SystemIdentifier = table.Column<string>(nullable: true),
28  Name = table.Column<string>(nullable: false),
29  AdministrationRights = table.Column<ulong>(nullable: false),
30  InstanceManagerRights = table.Column<ulong>(nullable: false),
31  PasswordHash = table.Column<string>(nullable: true),
32  CreatedById = table.Column<long>(nullable: true),
33  CanonicalName = table.Column<string>(nullable: false),
34  LastPasswordUpdate = table.Column<DateTimeOffset>(nullable: true)
35  },
36  constraints: table =>
37  {
38  table.PrimaryKey("PK_Users", x => x.Id);
39  table.ForeignKey(
40  name: "FK_Users_Users_CreatedById",
41  column: x => x.CreatedById,
42  principalTable: "Users",
43  principalColumn: "Id",
44  onDelete: ReferentialAction.Restrict);
45  });
46 
47  migrationBuilder.CreateTable(
48  name: "ChatChannels",
49  columns: table => new
50  {
51  IrcChannel = table.Column<string>(nullable: true),
52  DiscordChannelId = table.Column<ulong>(nullable: true),
53  IsAdminChannel = table.Column<bool>(nullable: false),
54  IsWatchdogChannel = table.Column<bool>(nullable: false),
55  IsUpdatesChannel = table.Column<bool>(nullable: false),
56  Tag = table.Column<string>(nullable: true),
57  Id = table.Column<long>(nullable: false)
58  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
59  ChatSettingsId = table.Column<long>(nullable: false)
60  },
61  constraints: table =>
62  {
63  table.PrimaryKey("PK_ChatChannels", x => x.Id);
64  });
65 
66  migrationBuilder.CreateTable(
67  name: "ReattachInformations",
68  columns: table => new
69  {
70  ChatCommandsJson = table.Column<string>(nullable: false),
71  ChatChannelsJson = table.Column<string>(nullable: false),
72  ServerCommandsJson = table.Column<string>(nullable: false),
73  AccessIdentifier = table.Column<string>(nullable: false),
74  ProcessId = table.Column<int>(nullable: false),
75  IsPrimary = table.Column<bool>(nullable: false),
76  Port = table.Column<ushort>(nullable: false),
77  RebootState = table.Column<int>(nullable: false),
78  Id = table.Column<long>(nullable: false)
79  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
80  CompileJobId = table.Column<long>(nullable: true)
81  },
82  constraints: table =>
83  {
84  table.PrimaryKey("PK_ReattachInformations", x => x.Id);
85  });
86 
87  migrationBuilder.CreateTable(
88  name: "WatchdogReattachInformations",
89  columns: table => new
90  {
91  AlphaIsActive = table.Column<bool>(nullable: false),
92  Id = table.Column<long>(nullable: false)
93  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
94  AlphaId = table.Column<long>(nullable: true),
95  BravoId = table.Column<long>(nullable: true)
96  },
97  constraints: table =>
98  {
99  table.PrimaryKey("PK_WatchdogReattachInformations", x => x.Id);
100  table.ForeignKey(
101  name: "FK_WatchdogReattachInformations_ReattachInformations_AlphaId",
102  column: x => x.AlphaId,
103  principalTable: "ReattachInformations",
104  principalColumn: "Id",
105  onDelete: ReferentialAction.Restrict);
106  table.ForeignKey(
107  name: "FK_WatchdogReattachInformations_ReattachInformations_BravoId",
108  column: x => x.BravoId,
109  principalTable: "ReattachInformations",
110  principalColumn: "Id",
111  onDelete: ReferentialAction.Restrict);
112  });
113 
114  migrationBuilder.CreateTable(
115  name: "Instances",
116  columns: table => new
117  {
118  Id = table.Column<long>(nullable: false)
119  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
120  Name = table.Column<string>(nullable: false),
121  Path = table.Column<string>(nullable: false),
122  Online = table.Column<bool>(nullable: false),
123  ConfigurationType = table.Column<int>(nullable: false),
124  AutoUpdateInterval = table.Column<uint>(nullable: false),
125  WatchdogReattachInformationId = table.Column<long>(nullable: true)
126  },
127  constraints: table =>
128  {
129  table.PrimaryKey("PK_Instances", x => x.Id);
130  table.ForeignKey(
131  name: "FK_Instances_WatchdogReattachInformations_WatchdogReattachInfor~",
132  column: x => x.WatchdogReattachInformationId,
133  principalTable: "WatchdogReattachInformations",
134  principalColumn: "Id",
135  onDelete: ReferentialAction.Restrict);
136  });
137 
138  migrationBuilder.CreateTable(
139  name: "ChatBots",
140  columns: table => new
141  {
142  Id = table.Column<long>(nullable: false)
143  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
144  Name = table.Column<string>(nullable: false),
145  Enabled = table.Column<bool>(nullable: true),
146  Provider = table.Column<int>(nullable: true),
147  ConnectionString = table.Column<string>(nullable: false),
148  InstanceId = table.Column<long>(nullable: false)
149  },
150  constraints: table =>
151  {
152  table.PrimaryKey("PK_ChatBots", x => x.Id);
153  table.ForeignKey(
154  name: "FK_ChatBots_Instances_InstanceId",
155  column: x => x.InstanceId,
156  principalTable: "Instances",
157  principalColumn: "Id",
158  onDelete: ReferentialAction.Cascade);
159  });
160 
161  migrationBuilder.CreateTable(
162  name: "DreamDaemonSettings",
163  columns: table => new
164  {
165  AllowWebClient = table.Column<bool>(nullable: false),
166  SecurityLevel = table.Column<int>(nullable: false),
167  PrimaryPort = table.Column<ushort>(nullable: false),
168  SecondaryPort = table.Column<ushort>(nullable: false),
169  StartupTimeout = table.Column<uint>(nullable: false),
170  AutoStart = table.Column<bool>(nullable: false),
171  SoftRestart = table.Column<bool>(nullable: false),
172  SoftShutdown = table.Column<bool>(nullable: false),
173  Id = table.Column<long>(nullable: false)
174  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
175  ProcessId = table.Column<int>(nullable: true),
176  AccessToken = table.Column<string>(nullable: true),
177  InstanceId = table.Column<long>(nullable: false)
178  },
179  constraints: table =>
180  {
181  table.PrimaryKey("PK_DreamDaemonSettings", x => x.Id);
182  table.ForeignKey(
183  name: "FK_DreamDaemonSettings_Instances_InstanceId",
184  column: x => x.InstanceId,
185  principalTable: "Instances",
186  principalColumn: "Id",
187  onDelete: ReferentialAction.Cascade);
188  });
189 
190  migrationBuilder.CreateTable(
191  name: "DreamMakerSettings",
192  columns: table => new
193  {
194  ProjectName = table.Column<string>(nullable: true),
195  ApiValidationPort = table.Column<ushort>(nullable: false),
196  Id = table.Column<long>(nullable: false)
197  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
198  InstanceId = table.Column<long>(nullable: false)
199  },
200  constraints: table =>
201  {
202  table.PrimaryKey("PK_DreamMakerSettings", x => x.Id);
203  table.ForeignKey(
204  name: "FK_DreamMakerSettings_Instances_InstanceId",
205  column: x => x.InstanceId,
206  principalTable: "Instances",
207  principalColumn: "Id",
208  onDelete: ReferentialAction.Cascade);
209  });
210 
211  migrationBuilder.CreateTable(
212  name: "InstanceUsers",
213  columns: table => new
214  {
215  UserId = table.Column<long>(nullable: false),
216  InstanceUserRights = table.Column<ulong>(nullable: false),
217  ByondRights = table.Column<ulong>(nullable: false),
218  DreamDaemonRights = table.Column<ulong>(nullable: false),
219  DreamMakerRights = table.Column<ulong>(nullable: false),
220  RepositoryRights = table.Column<ulong>(nullable: false),
221  ChatBotRights = table.Column<ulong>(nullable: false),
222  ConfigurationRights = table.Column<ulong>(nullable: false),
223  Id = table.Column<long>(nullable: false)
224  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
225  InstanceId = table.Column<long>(nullable: false)
226  },
227  constraints: table =>
228  {
229  table.PrimaryKey("PK_InstanceUsers", x => x.Id);
230  table.ForeignKey(
231  name: "FK_InstanceUsers_Instances_InstanceId",
232  column: x => x.InstanceId,
233  principalTable: "Instances",
234  principalColumn: "Id",
235  onDelete: ReferentialAction.Cascade);
236  table.ForeignKey(
237  name: "FK_InstanceUsers_Users_UserId",
238  column: x => x.UserId,
239  principalTable: "Users",
240  principalColumn: "Id",
241  onDelete: ReferentialAction.Cascade);
242  });
243 
244  migrationBuilder.CreateTable(
245  name: "Jobs",
246  columns: table => new
247  {
248  Id = table.Column<long>(nullable: false)
249  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
250  Description = table.Column<string>(nullable: false),
251  ExceptionDetails = table.Column<string>(nullable: true),
252  StartedAt = table.Column<DateTimeOffset>(nullable: false),
253  StoppedAt = table.Column<DateTimeOffset>(nullable: true),
254  Cancelled = table.Column<bool>(nullable: false),
255  CancelRightsType = table.Column<ulong>(nullable: true),
256  CancelRight = table.Column<ulong>(nullable: true),
257  StartedById = table.Column<long>(nullable: false),
258  CancelledById = table.Column<long>(nullable: true),
259  InstanceId = table.Column<long>(nullable: false)
260  },
261  constraints: table =>
262  {
263  table.PrimaryKey("PK_Jobs", x => x.Id);
264  table.ForeignKey(
265  name: "FK_Jobs_Users_CancelledById",
266  column: x => x.CancelledById,
267  principalTable: "Users",
268  principalColumn: "Id",
269  onDelete: ReferentialAction.Restrict);
270  table.ForeignKey(
271  name: "FK_Jobs_Instances_InstanceId",
272  column: x => x.InstanceId,
273  principalTable: "Instances",
274  principalColumn: "Id",
275  onDelete: ReferentialAction.Cascade);
276  table.ForeignKey(
277  name: "FK_Jobs_Users_StartedById",
278  column: x => x.StartedById,
279  principalTable: "Users",
280  principalColumn: "Id",
281  onDelete: ReferentialAction.Cascade);
282  });
283 
284  migrationBuilder.CreateTable(
285  name: "RepositorySettings",
286  columns: table => new
287  {
288  CommitterName = table.Column<string>(nullable: false),
289  CommitterEmail = table.Column<string>(nullable: false),
290  AccessUser = table.Column<string>(nullable: true),
291  AccessToken = table.Column<string>(nullable: true),
292  PushTestMergeCommits = table.Column<bool>(nullable: false),
293  ShowTestMergeCommitters = table.Column<bool>(nullable: false),
294  AutoUpdatesKeepTestMerges = table.Column<bool>(nullable: false),
295  AutoUpdatesSynchronize = table.Column<bool>(nullable: false),
296  Id = table.Column<long>(nullable: false)
297  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
298  InstanceId = table.Column<long>(nullable: false)
299  },
300  constraints: table =>
301  {
302  table.PrimaryKey("PK_RepositorySettings", x => x.Id);
303  table.ForeignKey(
304  name: "FK_RepositorySettings_Instances_InstanceId",
305  column: x => x.InstanceId,
306  principalTable: "Instances",
307  principalColumn: "Id",
308  onDelete: ReferentialAction.Cascade);
309  });
310 
311  migrationBuilder.CreateTable(
312  name: "RevisionInformations",
313  columns: table => new
314  {
315  CommitSha = table.Column<string>(maxLength: 40, nullable: false),
316  OriginCommitSha = table.Column<string>(maxLength: 40, nullable: false),
317  Id = table.Column<long>(nullable: false)
318  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
319  InstanceId = table.Column<long>(nullable: false)
320  },
321  constraints: table =>
322  {
323  table.PrimaryKey("PK_RevisionInformations", x => x.Id);
324  table.ForeignKey(
325  name: "FK_RevisionInformations_Instances_InstanceId",
326  column: x => x.InstanceId,
327  principalTable: "Instances",
328  principalColumn: "Id",
329  onDelete: ReferentialAction.Cascade);
330  });
331 
332  migrationBuilder.CreateTable(
333  name: "CompileJobs",
334  columns: table => new
335  {
336  Id = table.Column<long>(nullable: false)
337  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
338  DmeName = table.Column<string>(nullable: true),
339  Output = table.Column<string>(nullable: true),
340  DirectoryName = table.Column<Guid>(nullable: true),
341  JobId = table.Column<long>(nullable: true),
342  RevisionInformationId = table.Column<long>(nullable: false),
343  ByondVersion = table.Column<string>(nullable: false)
344  },
345  constraints: table =>
346  {
347  table.PrimaryKey("PK_CompileJobs", x => x.Id);
348  table.ForeignKey(
349  name: "FK_CompileJobs_Jobs_JobId",
350  column: x => x.JobId,
351  principalTable: "Jobs",
352  principalColumn: "Id",
353  onDelete: ReferentialAction.Restrict);
354  table.ForeignKey(
355  name: "FK_CompileJobs_RevisionInformations_RevisionInformationId",
356  column: x => x.RevisionInformationId,
357  principalTable: "RevisionInformations",
358  principalColumn: "Id",
359  onDelete: ReferentialAction.Cascade);
360  });
361 
362  migrationBuilder.CreateTable(
363  name: "TestMerges",
364  columns: table => new
365  {
366  Number = table.Column<int>(nullable: false),
367  PullRequestRevision = table.Column<string>(nullable: false),
368  Comment = table.Column<string>(nullable: true),
369  TitleAtMerge = table.Column<string>(nullable: false),
370  BodyAtMerge = table.Column<string>(nullable: false),
371  Url = table.Column<string>(nullable: false),
372  Author = table.Column<string>(nullable: false),
373  Id = table.Column<long>(nullable: false)
374  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
375  MergedAt = table.Column<DateTimeOffset>(nullable: false),
376  MergedById = table.Column<long>(nullable: false),
377  PrimaryRevisionInformationId = table.Column<long>(nullable: true)
378  },
379  constraints: table =>
380  {
381  table.PrimaryKey("PK_TestMerges", x => x.Id);
382  table.ForeignKey(
383  name: "FK_TestMerges_Users_MergedById",
384  column: x => x.MergedById,
385  principalTable: "Users",
386  principalColumn: "Id",
387  onDelete: ReferentialAction.Restrict);
388  table.ForeignKey(
389  name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId",
390  column: x => x.PrimaryRevisionInformationId,
391  principalTable: "RevisionInformations",
392  principalColumn: "Id",
393  onDelete: ReferentialAction.SetNull);
394  });
395 
396  migrationBuilder.CreateTable(
397  name: "RevInfoTestMerges",
398  columns: table => new
399  {
400  Id = table.Column<long>(nullable: false)
401  .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
402  TestMergeId = table.Column<long>(nullable: false),
403  RevisionInformationId = table.Column<long>(nullable: false)
404  },
405  constraints: table =>
406  {
407  table.PrimaryKey("PK_RevInfoTestMerges", x => x.Id);
408  table.ForeignKey(
409  name: "FK_RevInfoTestMerges_RevisionInformations_RevisionInformationId",
410  column: x => x.RevisionInformationId,
411  principalTable: "RevisionInformations",
412  principalColumn: "Id",
413  onDelete: ReferentialAction.Cascade);
414  table.ForeignKey(
415  name: "FK_RevInfoTestMerges_TestMerges_TestMergeId",
416  column: x => x.TestMergeId,
417  principalTable: "TestMerges",
418  principalColumn: "Id",
419  onDelete: ReferentialAction.Cascade);
420  });
421 
422  migrationBuilder.CreateIndex(
423  name: "IX_ChatBots_InstanceId",
424  table: "ChatBots",
425  column: "InstanceId");
426 
427  migrationBuilder.CreateIndex(
428  name: "IX_ChatBots_Name",
429  table: "ChatBots",
430  column: "Name",
431  unique: true);
432 
433  migrationBuilder.CreateIndex(
434  name: "IX_ChatChannels_ChatSettingsId_DiscordChannelId",
435  table: "ChatChannels",
436  columns: new[] { "ChatSettingsId", "DiscordChannelId" },
437  unique: true);
438 
439  migrationBuilder.CreateIndex(
440  name: "IX_ChatChannels_ChatSettingsId_IrcChannel",
441  table: "ChatChannels",
442  columns: new[] { "ChatSettingsId", "IrcChannel" },
443  unique: true);
444 
445  migrationBuilder.CreateIndex(
446  name: "IX_CompileJobs_DirectoryName",
447  table: "CompileJobs",
448  column: "DirectoryName");
449 
450  migrationBuilder.CreateIndex(
451  name: "IX_CompileJobs_JobId",
452  table: "CompileJobs",
453  column: "JobId");
454 
455  migrationBuilder.CreateIndex(
456  name: "IX_CompileJobs_RevisionInformationId",
457  table: "CompileJobs",
458  column: "RevisionInformationId");
459 
460  migrationBuilder.CreateIndex(
461  name: "IX_DreamDaemonSettings_InstanceId",
462  table: "DreamDaemonSettings",
463  column: "InstanceId",
464  unique: true);
465 
466  migrationBuilder.CreateIndex(
467  name: "IX_DreamMakerSettings_InstanceId",
468  table: "DreamMakerSettings",
469  column: "InstanceId",
470  unique: true);
471 
472  migrationBuilder.CreateIndex(
473  name: "IX_Instances_Path",
474  table: "Instances",
475  column: "Path",
476  unique: true);
477 
478  migrationBuilder.CreateIndex(
479  name: "IX_Instances_WatchdogReattachInformationId",
480  table: "Instances",
481  column: "WatchdogReattachInformationId");
482 
483  migrationBuilder.CreateIndex(
484  name: "IX_InstanceUsers_InstanceId",
485  table: "InstanceUsers",
486  column: "InstanceId");
487 
488  migrationBuilder.CreateIndex(
489  name: "IX_InstanceUsers_UserId_InstanceId",
490  table: "InstanceUsers",
491  columns: new[] { "UserId", "InstanceId" },
492  unique: true);
493 
494  migrationBuilder.CreateIndex(
495  name: "IX_Jobs_CancelledById",
496  table: "Jobs",
497  column: "CancelledById");
498 
499  migrationBuilder.CreateIndex(
500  name: "IX_Jobs_InstanceId",
501  table: "Jobs",
502  column: "InstanceId");
503 
504  migrationBuilder.CreateIndex(
505  name: "IX_Jobs_StartedById",
506  table: "Jobs",
507  column: "StartedById");
508 
509  migrationBuilder.CreateIndex(
510  name: "IX_ReattachInformations_CompileJobId",
511  table: "ReattachInformations",
512  column: "CompileJobId");
513 
514  migrationBuilder.CreateIndex(
515  name: "IX_RepositorySettings_InstanceId",
516  table: "RepositorySettings",
517  column: "InstanceId",
518  unique: true);
519 
520  migrationBuilder.CreateIndex(
521  name: "IX_RevInfoTestMerges_RevisionInformationId",
522  table: "RevInfoTestMerges",
523  column: "RevisionInformationId");
524 
525  migrationBuilder.CreateIndex(
526  name: "IX_RevInfoTestMerges_TestMergeId",
527  table: "RevInfoTestMerges",
528  column: "TestMergeId");
529 
530  migrationBuilder.CreateIndex(
531  name: "IX_RevisionInformations_CommitSha",
532  table: "RevisionInformations",
533  column: "CommitSha",
534  unique: true);
535 
536  migrationBuilder.CreateIndex(
537  name: "IX_RevisionInformations_InstanceId",
538  table: "RevisionInformations",
539  column: "InstanceId");
540 
541  migrationBuilder.CreateIndex(
542  name: "IX_TestMerges_MergedById",
543  table: "TestMerges",
544  column: "MergedById");
545 
546  migrationBuilder.CreateIndex(
547  name: "IX_TestMerges_PrimaryRevisionInformationId",
548  table: "TestMerges",
549  column: "PrimaryRevisionInformationId",
550  unique: true);
551 
552  migrationBuilder.CreateIndex(
553  name: "IX_Users_CanonicalName",
554  table: "Users",
555  column: "CanonicalName",
556  unique: true);
557 
558  migrationBuilder.CreateIndex(
559  name: "IX_Users_CreatedById",
560  table: "Users",
561  column: "CreatedById");
562 
563  migrationBuilder.CreateIndex(
564  name: "IX_WatchdogReattachInformations_AlphaId",
565  table: "WatchdogReattachInformations",
566  column: "AlphaId");
567 
568  migrationBuilder.CreateIndex(
569  name: "IX_WatchdogReattachInformations_BravoId",
570  table: "WatchdogReattachInformations",
571  column: "BravoId");
572 
573  migrationBuilder.AddForeignKey(
574  name: "FK_ChatChannels_ChatBots_ChatSettingsId",
575  table: "ChatChannels",
576  column: "ChatSettingsId",
577  principalTable: "ChatBots",
578  principalColumn: "Id",
579  onDelete: ReferentialAction.Cascade);
580 
581  migrationBuilder.AddForeignKey(
582  name: "FK_ReattachInformations_CompileJobs_CompileJobId",
583  table: "ReattachInformations",
584  column: "CompileJobId",
585  principalTable: "CompileJobs",
586  principalColumn: "Id",
587  onDelete: ReferentialAction.Restrict);
588  }
589 
594  protected override void Down(MigrationBuilder migrationBuilder)
595  {
596  migrationBuilder.DropForeignKey(
597  name: "FK_Jobs_Instances_InstanceId",
598  table: "Jobs");
599 
600  migrationBuilder.DropForeignKey(
601  name: "FK_RevisionInformations_Instances_InstanceId",
602  table: "RevisionInformations");
603 
604  migrationBuilder.DropTable(
605  name: "ChatChannels");
606 
607  migrationBuilder.DropTable(
608  name: "DreamDaemonSettings");
609 
610  migrationBuilder.DropTable(
611  name: "DreamMakerSettings");
612 
613  migrationBuilder.DropTable(
614  name: "InstanceUsers");
615 
616  migrationBuilder.DropTable(
617  name: "RepositorySettings");
618 
619  migrationBuilder.DropTable(
620  name: "RevInfoTestMerges");
621 
622  migrationBuilder.DropTable(
623  name: "ChatBots");
624 
625  migrationBuilder.DropTable(
626  name: "TestMerges");
627 
628  migrationBuilder.DropTable(
629  name: "Instances");
630 
631  migrationBuilder.DropTable(
632  name: "WatchdogReattachInformations");
633 
634  migrationBuilder.DropTable(
635  name: "ReattachInformations");
636 
637  migrationBuilder.DropTable(
638  name: "CompileJobs");
639 
640  migrationBuilder.DropTable(
641  name: "Jobs");
642 
643  migrationBuilder.DropTable(
644  name: "RevisionInformations");
645 
646  migrationBuilder.DropTable(
647  name: "Users");
648  }
649  }
650 }
ConfigurationRights
Rights for Models.ConfigurationFile
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
InstanceUserRights
Rights for an Models.Instance
@@ -103,11 +103,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonRights
Rights for Models.DreamDaemon
InstanceManagerRights
Rights for managing Models.Instances
DreamMakerRights
Rights for Models.DreamMaker
-
override void Up(MigrationBuilder migrationBuilder)
Applies the migration
+
override void Up(MigrationBuilder migrationBuilder)
Applies the migration
RepositoryRights
Rights for a Models.Repository
-
The initial database migration for MySQL/MariaDB
+
The initial database migration for MySQL/MariaDB
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
-
override void Down(MigrationBuilder migrationBuilder)
Unapplies the migration
+
override void Down(MigrationBuilder migrationBuilder)
Unapplies the migration
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
diff --git a/_administration_8cs.html b/_administration_8cs.html index 3a17812e82..0daa3e5b58 100644 --- a/_administration_8cs.html +++ b/_administration_8cs.html @@ -101,6 +101,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

Classes

class  Tgstation.Server.Api.Models.Administration + Represents administrative server information More...
  +

diff --git a/_administration_8cs_source.html b/_administration_8cs_source.html index 28e4f56be2..25ad29ed95 100644 --- a/_administration_8cs_source.html +++ b/_administration_8cs_source.html @@ -92,9 +92,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Administration.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Models
4 {
6  public sealed class Administration
7  {
11  public bool WindowsHost { get; set; }
12 
16  public Uri TrackedRepositoryUrl { get; set; }
17 
21  public Version LatestVersion { get; set; }
22 
26  public Version NewVersion { get; set; }
27  }
28 }
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Models
4 {
8  public sealed class Administration
9  {
13  public bool WindowsHost { get; set; }
14 
18  public Uri TrackedRepositoryUrl { get; set; }
19 
23  public Version LatestVersion { get; set; }
24 
28  public Version NewVersion { get; set; }
29  }
30 }
- +
Represents administrative server information
diff --git a/_administration_client_8cs_source.html b/_administration_client_8cs_source.html index 11eff202ac..da2a4e7e93 100644 --- a/_administration_client_8cs_source.html +++ b/_administration_client_8cs_source.html @@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
const string Administration
The Models.Administration controller
Definition: Routes.cs:19
readonly IApiClient apiClient
The apiClient for the AdministrationClient
- +
Represents administrative server information
Routes to a server actions
Definition: Routes.cs:9
diff --git a/_administration_controller_8cs_source.html b/_administration_controller_8cs_source.html index 7d1cfc7c7d..28701d414b 100644 --- a/_administration_controller_8cs_source.html +++ b/_administration_controller_8cs_source.html @@ -92,33 +92,33 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
AdministrationController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using Microsoft.Extensions.Primitives;
5 using Octokit;
6 using System;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Linq;
10 using System.Net;
11 using System.Runtime.InteropServices;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
19 using Tgstation.Server.Host.IO;
22 
23 namespace Tgstation.Server.Host.Controllers
24 {
28  [Route(Routes.Administration)]
29  public sealed class AdministrationController : ModelController<Administration>
30  {
31  const string RestartNotSupportedException = "This deployment of tgstation-server is lacking the Tgstation.Server.Host.Watchdog component. Restarts and version changes cannot be completed!";
32 
33  const string OctokitException = "Bad GitHub API response, check configuration! Exception: {0}";
34 
39 
44 
49 
54 
59 
64 
69 
83  public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger<AdministrationController> logger, IOptions<UpdatesConfiguration> updatesConfigurationOptions, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false)
84  {
85  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
86  this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
87  this.application = application ?? throw new ArgumentNullException(nameof(application));
88  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
89  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
90  updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions));
91  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
92  }
93 
94  StatusCodeResult RateLimit(RateLimitExceededException exception)
95  {
96  Logger.LogWarning("Exceeded GitHub rate limit! Exception {0}", exception);
97  var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
98  Response.Headers.Add("Retry-After", new StringValues(secondsString));
99  return StatusCode(429);
100  }
101 
102  IGitHubClient GetGitHubClient() => String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken);
103 
105  [TgsAuthorize]
106  public override async Task<IActionResult> Read(CancellationToken cancellationToken)
107  {
108  try
109  {
110  Version greatestVersion = null;
111  Uri repoUrl = null;
112  try
113  {
114  var gitHubClient = GetGitHubClient();
115  var repositoryTask = gitHubClient.Repository.Get(updatesConfiguration.GitHubRepositoryId);
116  var releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
117 
118  foreach (var I in releases)
119  if (Version.TryParse(I.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)
120  && version.Major == application.Version.Major
121  && (greatestVersion == null || version > greatestVersion))
122  greatestVersion = version;
123  repoUrl = new Uri((await repositoryTask.ConfigureAwait(false)).HtmlUrl);
124  }
125  catch (NotFoundException e)
126  {
127  Logger.LogWarning("Not found exception while retrieving upstream repository info: {0}", e);
128  }
129  return Json(new Administration
130  {
131  LatestVersion = greatestVersion,
132  TrackedRepositoryUrl = repoUrl,
133  WindowsHost = platformIdentifier.IsWindows
134  });
135  }
136  catch (RateLimitExceededException e)
137  {
138  return RateLimit(e);
139  }
140  catch (ApiException e)
141  {
142  Logger.LogWarning(OctokitException, e);
143  return StatusCode((int)HttpStatusCode.FailedDependency);
144  }
145  }
146 
148  [TgsAuthorize(AdministrationRights.ChangeVersion)]
149  public override async Task<IActionResult> Update([FromBody] Administration model, CancellationToken cancellationToken)
150  {
151  if (model == null)
152  throw new ArgumentNullException(nameof(model));
153 
154  if (model.NewVersion == null)
155  return BadRequest(new ErrorMessage { Message = "Missing new version!" });
156 
157  if (model.NewVersion.Major != application.Version.Major)
158  return BadRequest(new ErrorMessage { Message = "Cannot update to a different suite version!" });
159 
160  if(!serverUpdater.WatchdogPresent)
161  return UnprocessableEntity(new ErrorMessage
162  {
163  Message = RestartNotSupportedException
164  });
165 
166  Logger.LogDebug("Looking for GitHub releases version {0}...", model.NewVersion);
167  IEnumerable<Release> releases;
168  try
169  {
170  var gitHubClient = GetGitHubClient();
171  releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
172  cancellationToken.ThrowIfCancellationRequested();
173  }
174  catch (RateLimitExceededException e)
175  {
176  return RateLimit(e);
177  }
178  catch (ApiException e)
179  {
180  Logger.LogWarning(OctokitException, e);
181  return StatusCode((int)HttpStatusCode.FailedDependency);
182  }
183 
184  Logger.LogTrace("Release query complete!");
185  foreach (var release in releases)
186  if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) && version == model.NewVersion)
187  {
188  var asset = release.Assets.Where(x => x.Name == updatesConfiguration.UpdatePackageAssetName).FirstOrDefault();
189  if (asset == default)
190  continue;
191 
192  if (!serverUpdater.ApplyUpdate(version, new Uri(asset.BrowserDownloadUrl), ioManager))
193  return Conflict(new ErrorMessage
194  {
195  Message = "An update operation is already in progress!"
196  });
197  return Accepted(); //gtfo of here before all the cancellation tokens fire
198  }
199 
200  return StatusCode((int)HttpStatusCode.Gone);
201  }
202 
204  [HttpDelete]
205  [TgsAuthorize(AdministrationRights.RestartHost)]
206  public async Task<IActionResult> Delete()
207  {
208  try
209  {
210  if (!serverUpdater.WatchdogPresent)
211  {
212  Logger.LogDebug("Restart request failed due to lack of host watchdog!");
213  return UnprocessableEntity(new ErrorMessage
214  {
215  Message = RestartNotSupportedException
216  });
217  }
218  await serverUpdater.Restart().ConfigureAwait(false);
219  return Ok();
220  }
221  catch (InvalidOperationException)
222  {
223  return StatusCode((int)HttpStatusCode.ServiceUnavailable);
224  }
225  }
226  }
227 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using Microsoft.Extensions.Primitives;
5 using Octokit;
6 using System;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Linq;
10 using System.Net;
11 using System.Threading;
12 using System.Threading.Tasks;
13 using Tgstation.Server.Api;
18 using Tgstation.Server.Host.IO;
21 
22 namespace Tgstation.Server.Host.Controllers
23 {
27  [Route(Routes.Administration)]
28  public sealed class AdministrationController : ModelController<Administration>
29  {
30  const string RestartNotSupportedException = "This deployment of tgstation-server is lacking the Tgstation.Server.Host.Watchdog component. Restarts and version changes cannot be completed!";
31 
32  const string OctokitException = "Bad GitHub API response, check configuration! Exception: {0}";
33 
38 
43 
48 
53 
58 
63 
68 
82  public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger<AdministrationController> logger, IOptions<UpdatesConfiguration> updatesConfigurationOptions, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false)
83  {
84  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
85  this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater));
86  this.application = application ?? throw new ArgumentNullException(nameof(application));
87  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
88  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
89  updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions));
90  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
91  }
92 
93  StatusCodeResult RateLimit(RateLimitExceededException exception)
94  {
95  Logger.LogWarning("Exceeded GitHub rate limit! Exception {0}", exception);
96  var secondsString = Math.Ceiling((exception.Reset - DateTimeOffset.Now).TotalSeconds).ToString(CultureInfo.InvariantCulture);
97  Response.Headers.Add("Retry-After", new StringValues(secondsString));
98  return StatusCode(429);
99  }
100 
101  IGitHubClient GetGitHubClient() => String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken);
102 
104  [TgsAuthorize]
105  public override async Task<IActionResult> Read(CancellationToken cancellationToken)
106  {
107  try
108  {
109  Version greatestVersion = null;
110  Uri repoUrl = null;
111  try
112  {
113  var gitHubClient = GetGitHubClient();
114  var repositoryTask = gitHubClient.Repository.Get(updatesConfiguration.GitHubRepositoryId);
115  var releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
116 
117  foreach (var I in releases)
118  if (Version.TryParse(I.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)
119  && version.Major == application.Version.Major
120  && (greatestVersion == null || version > greatestVersion))
121  greatestVersion = version;
122  repoUrl = new Uri((await repositoryTask.ConfigureAwait(false)).HtmlUrl);
123  }
124  catch (NotFoundException e)
125  {
126  Logger.LogWarning("Not found exception while retrieving upstream repository info: {0}", e);
127  }
128 
129  return Json(new Administration
130  {
131  LatestVersion = greatestVersion,
132  TrackedRepositoryUrl = repoUrl,
133  WindowsHost = platformIdentifier.IsWindows
134  });
135  }
136  catch (RateLimitExceededException e)
137  {
138  return RateLimit(e);
139  }
140  catch (ApiException e)
141  {
142  Logger.LogWarning(OctokitException, e);
143  return StatusCode((int)HttpStatusCode.FailedDependency);
144  }
145  }
146 
148  [TgsAuthorize(AdministrationRights.ChangeVersion)]
149  public override async Task<IActionResult> Update([FromBody] Administration model, CancellationToken cancellationToken)
150  {
151  if (model == null)
152  throw new ArgumentNullException(nameof(model));
153 
154  if (model.NewVersion == null)
155  return BadRequest(new ErrorMessage { Message = "Missing new version!" });
156 
157  if (model.NewVersion.Major != application.Version.Major)
158  return BadRequest(new ErrorMessage { Message = "Cannot update to a different suite version!" });
159 
160  if(!serverUpdater.WatchdogPresent)
161  return UnprocessableEntity(new ErrorMessage
162  {
163  Message = RestartNotSupportedException
164  });
165 
166  Logger.LogDebug("Looking for GitHub releases version {0}...", model.NewVersion);
167  IEnumerable<Release> releases;
168  try
169  {
170  var gitHubClient = GetGitHubClient();
171  releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture));
172  cancellationToken.ThrowIfCancellationRequested();
173  }
174  catch (RateLimitExceededException e)
175  {
176  return RateLimit(e);
177  }
178  catch (ApiException e)
179  {
180  Logger.LogWarning(OctokitException, e);
181  return StatusCode((int)HttpStatusCode.FailedDependency);
182  }
183 
184  Logger.LogTrace("Release query complete!");
185  foreach (var release in releases)
186  if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) && version == model.NewVersion)
187  {
188  var asset = release.Assets.Where(x => x.Name == updatesConfiguration.UpdatePackageAssetName).FirstOrDefault();
189  if (asset == default)
190  continue;
191 
192  if (!serverUpdater.ApplyUpdate(version, new Uri(asset.BrowserDownloadUrl), ioManager))
193  return Conflict(new ErrorMessage
194  {
195  Message = "An update operation is already in progress!"
196  });
197  return Accepted(); // gtfo of here before all the cancellation tokens fire
198  }
199 
200  return StatusCode((int)HttpStatusCode.Gone);
201  }
202 
207  [HttpDelete]
208  [TgsAuthorize(AdministrationRights.RestartHost)]
209  public async Task<IActionResult> Delete()
210  {
211  try
212  {
213  if (!serverUpdater.WatchdogPresent)
214  {
215  Logger.LogDebug("Restart request failed due to lack of host watchdog!");
216  return UnprocessableEntity(new ErrorMessage
217  {
218  Message = RestartNotSupportedException
219  });
220  }
221 
222  await serverUpdater.Restart().ConfigureAwait(false);
223  return Ok();
224  }
225  catch (InvalidOperationException)
226  {
227  return StatusCode((int)HttpStatusCode.ServiceUnavailable);
228  }
229  }
230  }
231 }
-
override async Task< IActionResult > Read(CancellationToken cancellationToken)
Attempt to read a TModel
- +
override async Task< IActionResult > Read(CancellationToken cancellationToken)
Attempt to read a TModel
+
async Task< IActionResult > Delete()
Attempts to restart the server
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
-
readonly IIOManager ioManager
The IIOManager for the AdministrationController
-
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for the AdministrationController
+
readonly IIOManager ioManager
The IIOManager for the AdministrationController
+
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for the AdministrationController
-
readonly UpdatesConfiguration updatesConfiguration
The UpdatesConfiguration for the AdministrationController
+
readonly UpdatesConfiguration updatesConfiguration
The UpdatesConfiguration for the AdministrationController
- -
readonly IApplication application
The IApplication for the AdministrationController
+ +
readonly IApplication application
The IApplication for the AdministrationController
AdministrationRights
Rights for Models.Administration
const string Administration
The Models.Administration controller
Definition: Routes.cs:19
-
StatusCodeResult RateLimit(RateLimitExceededException exception)
+
StatusCodeResult RateLimit(RateLimitExceededException exception)
override async Task< IActionResult > Update([FromBody] Administration model, CancellationToken cancellationToken)
- +
Represents administrative server information
Configuration for the automatic update system
Routes to a server actions
Definition: Routes.cs:9
-
AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger< AdministrationController > logger, IOptions< UpdatesConfiguration > updatesConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct an AdministrationController
-
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the AdministrationController
+
AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, IPlatformIdentifier platformIdentifier, ILogger< AdministrationController > logger, IOptions< UpdatesConfiguration > updatesConfigurationOptions, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct an AdministrationController
+
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the AdministrationController
An ApiController representing a TModel
@@ -128,9 +128,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Interface for using filesystems
Definition: IIOManager.cs:11
For identifying the current platform
Represents an error message returned by the server
Definition: ErrorMessage.cs:8
-
readonly IServerControl serverUpdater
The IServerControl for the AdministrationController
+
readonly IServerControl serverUpdater
The IServerControl for the AdministrationController
-
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the AdministrationController
+
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the AdministrationController
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_administration_rights_8cs_source.html b/_administration_rights_8cs_source.html index 7e905b6e78..449945c4bc 100644 --- a/_administration_rights_8cs_source.html +++ b/_administration_rights_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
AdministrationRights.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum AdministrationRights : ulong
10  {
14  None = 0,
18  WriteUsers = 1,
22  RestartHost = 2,
26  ChangeVersion = 4,
30  EditOwnPassword = 8,
34  ReadUsers = 16
35  }
36 }
User can edit themself and other Models.Users and also create others
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum AdministrationRights : ulong
10  {
14  None = 0,
15 
19  WriteUsers = 1,
20 
24  RestartHost = 2,
25 
29  ChangeVersion = 4,
30 
34  EditOwnPassword = 8,
35 
39  ReadUsers = 16
40  }
41 }
User can edit themself and other Models.Users and also create others
AdministrationRights
Rights for Models.Administration
diff --git a/_administration_test_8cs_source.html b/_administration_test_8cs_source.html index 1b07889e1d..017fb2d535 100644 --- a/_administration_test_8cs_source.html +++ b/_administration_test_8cs_source.html @@ -102,10 +102,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
Represents administrative server information
AdministrationTest(IAdministrationClient client)
-
bool WindowsHost
If the server is running on a windows operating system
-
Version LatestVersion
The latest available version of the Tgstation.Server.Host assembly from the upstream repository...
+
bool WindowsHost
If the server is running on a windows operating system
+
Version LatestVersion
The latest available version of the Tgstation.Server.Host assembly from the upstream repository...
readonly IAdministrationClient client
diff --git a/_api_client_8cs_source.html b/_api_client_8cs_source.html index 06b012459b..c143cd1178 100644 --- a/_api_client_8cs_source.html +++ b/_api_client_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiClient.cs
-Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Newtonsoft.Json.Converters;
3 using Newtonsoft.Json.Serialization;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Net.Http;
9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Client
16 {
18  sealed class ApiClient : IApiClient
19  {
21  public Uri Url { get; }
22 
24  public ApiHeaders Headers
25  {
26  get => headers;
27  set => headers = value ?? throw new InvalidOperationException("Cannot set null headers!");
28  }
29 
31  public TimeSpan Timeout
32  {
33  get => httpClient.Timeout;
34  set => httpClient.Timeout = value;
35  }
36 
41 
45  readonly List<IRequestLogger> requestLoggers;
46 
51 
58  public ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders)
59  {
60  this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
61  Url = url ?? throw new ArgumentNullException(nameof(url));
62  headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders));
63 
64  requestLoggers = new List<IRequestLogger>();
65  }
66 
68  public void Dispose() => httpClient.Dispose();
69 
79  async Task<TResult> RunRequest<TResult>(string route, object body, HttpMethod method, long? instanceId, CancellationToken cancellationToken)
80  {
81  if (route == null)
82  throw new ArgumentNullException(nameof(route));
83  if (method == null)
84  throw new ArgumentNullException(nameof(method));
85  if (body == null && (method == HttpMethod.Post || method == HttpMethod.Put))
86  throw new InvalidOperationException("Body cannot be null for POST or PUT!");
87 
88  var fullUri = new Uri(Url, route);
89 
90  var message = new HttpRequestMessage(method, fullUri);
91 
92  var serializerSettings = new JsonSerializerSettings
93  {
94  ContractResolver = new CamelCasePropertyNamesContractResolver(),
95  Converters = new[] { new VersionConverter() }
96  };
97 
98  if (body != null)
99  message.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
100 
101  headers.SetRequestHeaders(message.Headers, instanceId);
102 
103  await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(message, cancellationToken))).ConfigureAwait(false);
104 
105  var response = await httpClient.SendAsync(message, cancellationToken).ConfigureAwait(false);
106 
107  await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false);
108 
109  var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
110 
111  if (!response.IsSuccessStatusCode)
112  {
113  ErrorMessage errorMessage = null;
114  try
115  {
116  //check if json serializes to an error message
117  errorMessage = JsonConvert.DeserializeObject<ErrorMessage>(json, serializerSettings);
118  }
119  catch (JsonException) { }
120 
121  const string BadSpecExtension = " This is not part of TGS4 communication specification and should be reported if it was returned from a TGS4 server!";
122 
123  switch (response.StatusCode)
124  {
125  case HttpStatusCode.UpgradeRequired:
126  throw new ApiMismatchException(errorMessage ?? new ErrorMessage
127  {
128  Message = "API Mismatch but no current API version provided!" + BadSpecExtension,
129  SeverApiVersion = null
130  });
131  case HttpStatusCode.Unauthorized:
132  throw new UnauthorizedException();
133  case HttpStatusCode.RequestTimeout:
134  throw new RequestTimeoutException();
135  case HttpStatusCode.Forbidden:
137  case HttpStatusCode.ServiceUnavailable:
138  throw new ServiceUnavailableException();
139  case HttpStatusCode.Gone:
140  errorMessage = errorMessage ?? new ErrorMessage
141  {
142  Message = "The requested resource could not be found!",
143  SeverApiVersion = null
144  };
145  goto case HttpStatusCode.Conflict;
146  case HttpStatusCode.NotFound: //our fault somehow
147  errorMessage = errorMessage ?? new ErrorMessage
148  {
149  Message = "This is not a valid route!" + BadSpecExtension,
150  SeverApiVersion = null
151  };
152  goto case HttpStatusCode.Conflict;
153  case HttpStatusCode.Conflict:
154  throw new ConflictException(errorMessage ?? new ErrorMessage
155  {
156  Message = "An undescribed conflict occurred!" + BadSpecExtension,
157  SeverApiVersion = null
158  }, response.StatusCode);
159  case HttpStatusCode.NotImplemented:
160  case (HttpStatusCode)422: //unprocessable entity
161  throw new MethodNotSupportedException();
162  case HttpStatusCode.InternalServerError:
163  //response
164  throw new ServerErrorException(json); //json is html
165  case (HttpStatusCode)429: //rate limited
166  response.Headers.TryGetValues("Retry-After", out var values);
167  throw new RateLimitException(values?.FirstOrDefault());
168  default:
169  throw new ApiConflictException(errorMessage, response.StatusCode);
170  }
171  }
172 
173  if (String.IsNullOrWhiteSpace(json))
174  json = JsonConvert.SerializeObject(new object());
175 
176  try
177  {
178  return JsonConvert.DeserializeObject<TResult>(json, serializerSettings);
179  }
180  catch (JsonException)
181  {
182  throw new UnrecognizedResponseException(json, response.StatusCode);
183  }
184  }
185 
187  public Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, null, cancellationToken);
188 
190  public Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, null, cancellationToken);
191 
193  public Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Post, null, cancellationToken);
194 
196  public Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, null, cancellationToken);
197 
199  public Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Post, null, cancellationToken);
200 
202  public Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, null, cancellationToken);
203 
205  public Task Delete(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, null, cancellationToken);
206 
208  public Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, instanceId, cancellationToken);
209 
211  public Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, instanceId, cancellationToken);
212 
214  public Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, instanceId, cancellationToken);
215 
217  public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, instanceId, cancellationToken);
218 
220  public Task Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Delete, instanceId, cancellationToken);
221 
223  public Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Delete, instanceId, cancellationToken);
224 
226  public Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, instanceId, cancellationToken);
227 
229  public Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), new HttpMethod("PATCH"), instanceId, cancellationToken);
230 
232  public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger)));
233  }
234 }
Occurs when a response is received that did not deserialize to one of the expected Api...
+Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using Newtonsoft.Json.Converters;
3 using Newtonsoft.Json.Serialization;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Net;
8 using System.Net.Http;
9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Client
16 {
18  sealed class ApiClient : IApiClient
19  {
21  public Uri Url { get; }
22 
24  public ApiHeaders Headers
25  {
26  get => headers;
27  set => headers = value ?? throw new InvalidOperationException("Cannot set null headers!");
28  }
29 
31  public TimeSpan Timeout
32  {
33  get => httpClient.Timeout;
34  set => httpClient.Timeout = value;
35  }
36 
41 
45  readonly List<IRequestLogger> requestLoggers;
46 
51 
52  static JsonSerializerSettings GetSerializerSettings() => new JsonSerializerSettings
53  {
54  ContractResolver = new CamelCasePropertyNamesContractResolver(),
55  Converters = new[] { new VersionConverter() }
56  };
57 
58  static void HandleBadResponse(HttpResponseMessage response, string json)
59  {
60  ErrorMessage errorMessage = null;
61  try
62  {
63  // check if json serializes to an error message
64  errorMessage = JsonConvert.DeserializeObject<ErrorMessage>(json, GetSerializerSettings());
65  }
66  catch (JsonException) { }
67 
68  const string BadSpecExtension = " This is not part of TGS4 communication specification and should be reported if it was returned from a TGS4 server!";
69 
70 #pragma warning disable IDE0010 // Add missing cases
71  switch (response.StatusCode)
72 #pragma warning restore IDE0010 // Add missing cases
73  {
74  case HttpStatusCode.UpgradeRequired:
75  throw new ApiMismatchException(errorMessage ?? new ErrorMessage
76  {
77  Message = "API Mismatch but no current API version provided!" + BadSpecExtension,
78  SeverApiVersion = null
79  });
80  case HttpStatusCode.Unauthorized:
81  throw new UnauthorizedException();
82  case HttpStatusCode.RequestTimeout:
83  throw new RequestTimeoutException();
84  case HttpStatusCode.Forbidden:
86  case HttpStatusCode.ServiceUnavailable:
87  throw new ServiceUnavailableException();
88  case HttpStatusCode.Gone:
89  errorMessage = errorMessage ?? new ErrorMessage
90  {
91  Message = "The requested resource could not be found!",
92  SeverApiVersion = null
93  };
94  goto case HttpStatusCode.Conflict;
95  case HttpStatusCode.NotFound:
96  // our fault somehow
97  errorMessage = errorMessage ?? new ErrorMessage
98  {
99  Message = "This is not a valid route!" + BadSpecExtension,
100  SeverApiVersion = null
101  };
102  goto case HttpStatusCode.Conflict;
103  case HttpStatusCode.Conflict:
104  throw new ConflictException(errorMessage ?? new ErrorMessage
105  {
106  Message = "An undescribed conflict occurred!" + BadSpecExtension,
107  SeverApiVersion = null
108  }, response.StatusCode);
109  case HttpStatusCode.NotImplemented:
110  // unprocessable entity
111  case (HttpStatusCode)422:
112  throw new MethodNotSupportedException();
113  case HttpStatusCode.InternalServerError:
114  // response json is html
115  throw new ServerErrorException(json);
116  case (HttpStatusCode)429:
117  // rate limited
118  response.Headers.TryGetValues("Retry-After", out var values);
119  throw new RateLimitException(values?.FirstOrDefault());
120  default:
121  throw new ApiConflictException(errorMessage, response.StatusCode);
122  }
123  }
124 
131  public ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders)
132  {
133  this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
134  Url = url ?? throw new ArgumentNullException(nameof(url));
135  headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders));
136 
137  requestLoggers = new List<IRequestLogger>();
138  }
139 
141  public void Dispose() => httpClient.Dispose();
142 
153  async Task<TResult> RunRequest<TResult>(string route, object body, HttpMethod method, long? instanceId, CancellationToken cancellationToken)
154  {
155  if (route == null)
156  throw new ArgumentNullException(nameof(route));
157  if (method == null)
158  throw new ArgumentNullException(nameof(method));
159  if (body == null && (method == HttpMethod.Post || method == HttpMethod.Put))
160  throw new InvalidOperationException("Body cannot be null for POST or PUT!");
161 
162  HttpResponseMessage response;
163  var fullUri = new Uri(Url, route);
164  var serializerSettings = GetSerializerSettings();
165  using (var request = new HttpRequestMessage(method, fullUri))
166  {
167  if (body != null)
168  request.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
169 
170  headers.SetRequestHeaders(request.Headers, instanceId);
171 
172  await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false);
173 
174  response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
175  }
176 
177  using (response)
178  {
179  await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false);
180 
181  var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
182 
183  if (!response.IsSuccessStatusCode)
184  HandleBadResponse(response, json);
185 
186  if (String.IsNullOrWhiteSpace(json))
187  json = JsonConvert.SerializeObject(new object());
188 
189  try
190  {
191  return JsonConvert.DeserializeObject<TResult>(json, serializerSettings);
192  }
193  catch (JsonException)
194  {
195  throw new UnrecognizedResponseException(json, response.StatusCode);
196  }
197  }
198  }
199 
201  public Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, null, cancellationToken);
202 
204  public Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, null, cancellationToken);
205 
207  public Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Post, null, cancellationToken);
208 
210  public Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, null, cancellationToken);
211 
213  public Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Post, null, cancellationToken);
214 
216  public Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, null, cancellationToken);
217 
219  public Task Delete(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, null, cancellationToken);
220 
222  public Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, instanceId, cancellationToken);
223 
225  public Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, instanceId, cancellationToken);
226 
228  public Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, instanceId, cancellationToken);
229 
231  public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, instanceId, cancellationToken);
232 
234  public Task Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Delete, instanceId, cancellationToken);
235 
237  public Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Delete, instanceId, cancellationToken);
238 
240  public Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, instanceId, cancellationToken);
241 
243  public Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), new HttpMethod("PATCH"), instanceId, cancellationToken);
244 
246  public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger)));
247  }
248 }
Occurs when a response is received that did not deserialize to one of the expected Api...
@@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Occurs when a GitHub rate limit occurs
Occurs when an error occurs in the server
-
void SetRequestHeaders(HttpRequestHeaders headers, long?instanceId=null)
Set HttpRequestHeaders using the ApiHeaders. This initially clears headers
Definition: ApiHeaders.cs:224
+
void SetRequestHeaders(HttpRequestHeaders headers, long?instanceId=null)
Set HttpRequestHeaders using the ApiHeaders. This initially clears headers
Definition: ApiHeaders.cs:225
Occurs when the server returns an unknown response
ApiHeaders headers
Backing field for Headers
Definition: ApiClient.cs:50
@@ -108,8 +108,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Occurs when the client makes a request while the server is starting or stopping
For sending HTTP requests
Definition: IHttpClient.cs:11
readonly List< IRequestLogger > requestLoggers
The IRequestLoggers used by the ApiClient
Definition: ApiClient.cs:45
+
static void HandleBadResponse(HttpResponseMessage response, string json)
Definition: ApiClient.cs:58
Occurs when the client provides invalid credentials
-
ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders)
Construct an ApiClient
Definition: ApiClient.cs:58
+
ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders)
Construct an ApiClient
Definition: ApiClient.cs:131
Occurs when the API version of the client is not compatible with the server&#39;s
Occurs when the client performs an action that would result in data conflict
Occurs when the client attempts to perform an action they do not have the rights for ...
diff --git a/_api_controller_8cs_source.html b/_api_controller_8cs_source.html index 04ddb01e74..a4d8213a8b 100644 --- a/_api_controller_8cs_source.html +++ b/_api_controller_8cs_source.html @@ -92,12 +92,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.AspNetCore.Mvc.Filters;
4 using Microsoft.Extensions.Logging;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Controllers
16 {
20  [Produces(ApiHeaders.ApplicationJson)]
21  [Consumes(ApiHeaders.ApplicationJson)]
22  public abstract class ApiController : Controller
23  {
27  protected ApiHeaders ApiHeaders { get; private set; }
28 
32  protected IDatabaseContext DatabaseContext { get; }
33 
38 
42  protected ILogger Logger { get; }
43 
47  protected Models.Instance Instance { get; }
48 
52  readonly bool requireInstance;
53 
57  readonly bool requireHeaders;
58 
67  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
68  {
69  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
70  if (authenticationContextFactory == null)
71  throw new ArgumentNullException(nameof(authenticationContextFactory));
72  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
75  this.requireInstance = requireInstance;
76  this.requireHeaders = requireHeaders;
77  }
78 
80  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
81  {
82  //ALL valid token and login requests that match a route go through this function
83  //404 is returned before
84 
85  if (AuthenticationContext != null && AuthenticationContext.User == null)
86  {
87  //valid token, expired password
88  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
89  return;
90  }
91 
92  //validate the headers
93  try
94  {
95  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
96 
97  if (!ApiHeaders.Compatible())
98  {
99  await StatusCode((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
100  {
101  Message = "Provided API version is incompatible with server version!"
102  }).ExecuteResultAsync(context).ConfigureAwait(false);
103  return;
104  }
105 
106  if (requireInstance)
107  {
108  if (!ApiHeaders.InstanceId.HasValue)
109  {
110  await BadRequest(new ErrorMessage { Message = "Missing Instance header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
111  return;
112  }
114  {
115  //accessing an instance they don't have access to or one that's disabled
116  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
117  return;
118  }
119  }
120  }
121  catch (InvalidOperationException e)
122  {
123  if (requireHeaders)
124  {
125  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
126  return;
127  }
128  }
129 
130  if (ModelState?.IsValid == false)
131  {
132  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
133  //HACK
134  //do some fuckery to remove RequiredAttribute errors
135  for (var I = 0; I < errorMessages.Count; ++I)
136  {
137  var message = errorMessages[I];
138  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
139  {
140  errorMessages.RemoveAt(I);
141  --I;
142  }
143  }
144  if (errorMessages.Count > 0)
145  {
146  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
147  return;
148  }
149  }
150 
151  if (ApiHeaders != null)
152  Logger.LogDebug("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path, Request.QueryString, ApiHeaders.InstanceId);
153 
154  try
155  {
156  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
157  }
158  catch (OperationCanceledException e)
159  {
160  Logger.LogDebug("Request cancelled! Exception: {0}", e);
161  throw;
162  }
163  }
164  }
165 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.AspNetCore.Mvc.Filters;
4 using Microsoft.Extensions.Logging;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Controllers
16 {
20  [Produces(ApiHeaders.ApplicationJson)]
21  [Consumes(ApiHeaders.ApplicationJson)]
22  public abstract class ApiController : Controller
23  {
27  protected ApiHeaders ApiHeaders { get; private set; }
28 
32  protected IDatabaseContext DatabaseContext { get; }
33 
38 
42  protected ILogger Logger { get; }
43 
47  protected Models.Instance Instance { get; }
48 
52  readonly bool requireInstance;
53 
57  readonly bool requireHeaders;
58 
67  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
68  {
69  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
70  if (authenticationContextFactory == null)
71  throw new ArgumentNullException(nameof(authenticationContextFactory));
72  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
75  this.requireInstance = requireInstance;
76  this.requireHeaders = requireHeaders;
77  }
78 
80  #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((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
100  {
101  Message = "Provided API version is incompatible with server version!"
102  }).ExecuteResultAsync(context).ConfigureAwait(false);
103  return;
104  }
105 
106  if (requireInstance)
107  {
108  if (!ApiHeaders.InstanceId.HasValue)
109  {
110  await BadRequest(new ErrorMessage { Message = "Missing Instance header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
111  return;
112  }
113 
115  {
116  // accessing an instance they don't have access to or one that's disabled
117  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
118  return;
119  }
120  }
121  }
122  catch (InvalidOperationException e)
123  {
124  if (requireHeaders)
125  {
126  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
127  return;
128  }
129  }
130 
131  if (ModelState?.IsValid == false)
132  {
133  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
134 
135  // HACK
136  // do some fuckery to remove RequiredAttribute errors
137  for (var I = 0; I < errorMessages.Count; ++I)
138  {
139  var message = errorMessages[I];
140  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
141  {
142  errorMessages.RemoveAt(I);
143  --I;
144  }
145  }
146 
147  if (errorMessages.Count > 0)
148  {
149  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
150  return;
151  }
152  }
153 
154  if (ApiHeaders != null)
155  Logger.LogDebug("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path, Request.QueryString, ApiHeaders.InstanceId);
156 
157  try
158  {
159  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
160  }
161  catch (OperationCanceledException e)
162  {
163  Logger.LogDebug("Request cancelled! Exception: {0}", e);
164  throw;
165  }
166  }
167  #pragma warning restore CA1506
168  }
169 }
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance, bool requireHeaders)
Construct an ApiController
-
override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
+
override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
const string ApplicationJson
TODO: Remove this when https://github.com/dotnet/corefx/pull/26701 makes it into the sdk ...
Definition: ApiHeaders.cs:21
@@ -112,7 +112,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
string Message
A human readable description of the error
Definition: ErrorMessage.cs:13
- +
readonly bool requireHeaders
If ApiHeaders are required
diff --git a/_api_headers_8cs_source.html b/_api_headers_8cs_source.html index c1401de739..ecfa7f9a16 100644 --- a/_api_headers_8cs_source.html +++ b/_api_headers_8cs_source.html @@ -92,12 +92,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiHeaders.cs
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Globalization;
4 using System.Linq;
5 using System.Net.Http.Headers;
6 using System.Reflection;
7 using Microsoft.AspNetCore.Http.Headers;
8 using Microsoft.Net.Http.Headers;
9 using Microsoft.Extensions.Primitives;
10 
11 namespace Tgstation.Server.Api
12 {
16  public sealed class ApiHeaders
17  {
21  public const string ApplicationJson = "application/json";
22 
26  const string ApiVersionHeader = "Api";
27 
31  const string usernameHeader = "Username";
32 
36  const string instanceIdHeader = "Instance";
37 
41  const string jwtAuthenticationScheme = "Bearer";
42 
46  const string passwordAuthenticationScheme = "Password";
47 
51  static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
52 
56  public static Version Version => assemblyName.Version;
57 
61  public long? InstanceId { get; set; }
62 
66  public ProductHeaderValue UserAgent { get; }
67 
71  public Version ApiVersion { get; }
72 
76  public string Token { get; }
77 
81  public string Username { get; }
82 
86  public string Password { get; }
87 
91  public bool IsTokenAuthentication => Token != null;
92 
98  public static bool CheckCompatibility(Version otherVersion) => !(Version.Major != otherVersion.Major || Version.Minor != otherVersion.Minor || Version.Build > otherVersion.Build);
99 
105  public ApiHeaders(ProductHeaderValue userAgent, string token) : this(userAgent, token, null, null)
106  {
107  if (userAgent == null)
108  throw new ArgumentNullException(nameof(userAgent));
109  if (token == null)
110  throw new ArgumentNullException(nameof(token));
111  }
112 
119  public ApiHeaders(ProductHeaderValue userAgent, string username, string password) : this(userAgent, null, username, password)
120  {
121  if (userAgent == null)
122  throw new ArgumentNullException(nameof(userAgent));
123  if (username == null)
124  throw new ArgumentNullException(nameof(username));
125  if (password == null)
126  throw new ArgumentNullException(nameof(password));
127  }
128 
133  public ApiHeaders(RequestHeaders requestHeaders)
134  {
135  var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJson);
136  if (!requestHeaders.Accept.Any(x => x.MediaType == jsonAccept.MediaType))
137  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Client does not accept {0}!", ApplicationJson));
138 
139  if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || !ProductInfoHeaderValue.TryParse(userAgentValues.FirstOrDefault(), out var clientUserAgent))
140  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} headers!", HeaderNames.UserAgent));
141 
142  //assure the client user agent has a name and version
143  if (String.IsNullOrWhiteSpace(clientUserAgent.Product.Name) || !Version.TryParse(clientUserAgent.Product.Version, out var clientVersion))
144  throw new InvalidOperationException("Malformed client user agent!");
145 
146  //make sure the api header matches ours
147  if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != assemblyName.Name)
148  throw new InvalidOperationException("Missing API version!");
149 
150  if (!Version.TryParse(apiUserAgent.Product.Version, out var apiVersion))
151  throw new InvalidOperationException("Malformed API version!");
152 
153  ApiVersion = apiVersion;
154  UserAgent = clientUserAgent.Product;
155 
156  if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
157  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} header!", HeaderNames.Authorization));
158  var auth = authorization.First();
159  var splits = new List<string>(auth.Split(' '));
160  var scheme = splits.First();
161  if (String.IsNullOrWhiteSpace(scheme))
162  throw new InvalidOperationException("Missing authentication scheme!");
163 
164  splits.RemoveAt(0);
165  var parameter = String.Concat(splits);
166  if (String.IsNullOrEmpty(parameter))
167  throw new InvalidOperationException("Missing authentication parameter!");
168 
169  if (requestHeaders.Headers.TryGetValue(instanceIdHeader, out var instanceIdValues))
170  {
171  var instanceIdString = instanceIdValues.FirstOrDefault();
172  if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
173  InstanceId = instanceId;
174  }
175 
176  switch (scheme)
177  {
178  case jwtAuthenticationScheme:
179  Token = parameter;
180  break;
181  case passwordAuthenticationScheme:
182  Password = parameter;
183  var fail = !requestHeaders.Headers.TryGetValue(usernameHeader, out var values);
184  if (!fail)
185  {
186  Username = values.FirstOrDefault();
187  fail = String.IsNullOrWhiteSpace(Username);
188  }
189  if (fail)
190  throw new InvalidOperationException("Missing Username header!");
191  break;
192  default:
193  throw new InvalidOperationException("Invalid authentication scheme!");
194  }
195  }
196 
204  ApiHeaders(ProductHeaderValue userAgent, string token, string username, string password)
205  {
206  UserAgent = userAgent;
207  Token = token;
208  Username = username;
209  Password = password;
210  ApiVersion = Version;
211  }
212 
217  public bool Compatible() => CheckCompatibility(ApiVersion);
218 
224  public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null)
225  {
226  if (headers == null)
227  throw new ArgumentNullException(nameof(headers));
228  if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
229  throw new InvalidOperationException("Specified instance ID in constructor and SetRequestHeaders!");
230 
231  headers.Clear();
232  headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
233  if (IsTokenAuthentication)
234  headers.Authorization = new AuthenticationHeaderValue(jwtAuthenticationScheme, Token);
235  else
236  {
237  headers.Authorization = new AuthenticationHeaderValue(passwordAuthenticationScheme, Password);
238  headers.Add(usernameHeader, Username);
239  }
240  headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
241  headers.Add(ApiVersionHeader, new ProductHeaderValue(assemblyName.Name, ApiVersion.ToString()).ToString());
242  instanceId = instanceId ?? InstanceId;
243  if (instanceId.HasValue)
244  headers.Add(instanceIdHeader, instanceId.ToString());
245  }
246  }
247 }
-
void SetRequestHeaders(HttpRequestHeaders headers, long?instanceId=null)
Set HttpRequestHeaders using the ApiHeaders. This initially clears headers
Definition: ApiHeaders.cs:224
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http.Headers;
2 using Microsoft.Extensions.Primitives;
3 using Microsoft.Net.Http.Headers;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net.Http.Headers;
9 using System.Reflection;
10 
11 namespace Tgstation.Server.Api
12 {
16  public sealed class ApiHeaders
17  {
21  public const string ApplicationJson = "application/json";
22 
26  const string ApiVersionHeader = "Api";
27 
31  const string UsernameHeader = "Username";
32 
36  const string InstanceIdHeader = "Instance";
37 
41  const string JwtAuthenticationScheme = "Bearer";
42 
46  const string PasswordAuthenticationScheme = "Password";
47 
51  static readonly AssemblyName AssemblyName = Assembly.GetExecutingAssembly().GetName();
52 
56  public static Version Version => AssemblyName.Version;
57 
61  public long? InstanceId { get; set; }
62 
66  public ProductHeaderValue UserAgent { get; }
67 
71  public Version ApiVersion { get; }
72 
76  public string Token { get; }
77 
81  public string Username { get; }
82 
86  public string Password { get; }
87 
91  public bool IsTokenAuthentication => Token != null;
92 
98  public static bool CheckCompatibility(Version otherVersion) => !(Version.Major != otherVersion.Major || Version.Minor != otherVersion.Minor || Version.Build > otherVersion.Build);
99 
105  public ApiHeaders(ProductHeaderValue userAgent, string token) : this(userAgent, token, null, null)
106  {
107  if (userAgent == null)
108  throw new ArgumentNullException(nameof(userAgent));
109  if (token == null)
110  throw new ArgumentNullException(nameof(token));
111  }
112 
119  public ApiHeaders(ProductHeaderValue userAgent, string username, string password) : this(userAgent, null, username, password)
120  {
121  if (userAgent == null)
122  throw new ArgumentNullException(nameof(userAgent));
123  if (username == null)
124  throw new ArgumentNullException(nameof(username));
125  if (password == null)
126  throw new ArgumentNullException(nameof(password));
127  }
128 
133  public ApiHeaders(RequestHeaders requestHeaders)
134  {
135  var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJson);
136  if (!requestHeaders.Accept.Any(x => x.MediaType == jsonAccept.MediaType))
137  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Client does not accept {0}!", ApplicationJson));
138 
139  if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || !ProductInfoHeaderValue.TryParse(userAgentValues.FirstOrDefault(), out var clientUserAgent))
140  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} headers!", HeaderNames.UserAgent));
141 
142  // assure the client user agent has a name and version
143  if (String.IsNullOrWhiteSpace(clientUserAgent.Product.Name) || !Version.TryParse(clientUserAgent.Product.Version, out var clientVersion))
144  throw new InvalidOperationException("Malformed client user agent!");
145 
146  // make sure the api header matches ours
147  if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != AssemblyName.Name)
148  throw new InvalidOperationException("Missing API version!");
149 
150  if (!Version.TryParse(apiUserAgent.Product.Version, out var apiVersion))
151  throw new InvalidOperationException("Malformed API version!");
152 
153  ApiVersion = apiVersion;
154  UserAgent = clientUserAgent.Product;
155 
156  if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
157  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} header!", HeaderNames.Authorization));
158  var auth = authorization.First();
159  var splits = new List<string>(auth.Split(' '));
160  var scheme = splits.First();
161  if (String.IsNullOrWhiteSpace(scheme))
162  throw new InvalidOperationException("Missing authentication scheme!");
163 
164  splits.RemoveAt(0);
165  var parameter = String.Concat(splits);
166  if (String.IsNullOrEmpty(parameter))
167  throw new InvalidOperationException("Missing authentication parameter!");
168 
169  if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
170  {
171  var instanceIdString = instanceIdValues.FirstOrDefault();
172  if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
173  InstanceId = instanceId;
174  }
175 
176  switch (scheme)
177  {
178  case JwtAuthenticationScheme:
179  Token = parameter;
180  break;
181  case PasswordAuthenticationScheme:
182  Password = parameter;
183  var fail = !requestHeaders.Headers.TryGetValue(UsernameHeader, out var values);
184  if (!fail)
185  {
186  Username = values.FirstOrDefault();
187  fail = String.IsNullOrWhiteSpace(Username);
188  }
189 
190  if (fail)
191  throw new InvalidOperationException("Missing Username header!");
192  break;
193  default:
194  throw new InvalidOperationException("Invalid authentication scheme!");
195  }
196  }
197 
205  ApiHeaders(ProductHeaderValue userAgent, string token, string username, string password)
206  {
207  UserAgent = userAgent;
208  Token = token;
209  Username = username;
210  Password = password;
211  ApiVersion = Version;
212  }
213 
218  public bool Compatible() => CheckCompatibility(ApiVersion);
219 
225  public void SetRequestHeaders(HttpRequestHeaders headers, long? instanceId = null)
226  {
227  if (headers == null)
228  throw new ArgumentNullException(nameof(headers));
229  if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
230  throw new InvalidOperationException("Specified instance ID in constructor and SetRequestHeaders!");
231 
232  headers.Clear();
233  headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
234  if (IsTokenAuthentication)
235  headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
236  else
237  {
238  headers.Authorization = new AuthenticationHeaderValue(PasswordAuthenticationScheme, Password);
239  headers.Add(UsernameHeader, Username);
240  }
241 
242  headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
243  headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
244  instanceId = instanceId ?? InstanceId;
245  if (instanceId.HasValue)
246  headers.Add(InstanceIdHeader, instanceId.ToString());
247  }
248  }
249 }
+
void SetRequestHeaders(HttpRequestHeaders headers, long?instanceId=null)
Set HttpRequestHeaders using the ApiHeaders. This initially clears headers
Definition: ApiHeaders.cs:225
ApiHeaders(ProductHeaderValue userAgent, string username, string password)
Construct ApiHeaders for password authentication
Definition: ApiHeaders.cs:119
ApiHeaders(ProductHeaderValue userAgent, string token)
Construct ApiHeaders for JWT authentication
Definition: ApiHeaders.cs:105
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:16
-
ApiHeaders(ProductHeaderValue userAgent, string token, string username, string password)
Construct ApiHeaders
Definition: ApiHeaders.cs:204
+
ApiHeaders(ProductHeaderValue userAgent, string token, string username, string password)
Construct ApiHeaders
Definition: ApiHeaders.cs:205
ApiHeaders(RequestHeaders requestHeaders)
Construct and validates ApiHeaders from a set of requestHeaders
Definition: ApiHeaders.cs:133
static Version Version
Get the version of the Api the caller is using
Definition: ApiHeaders.cs:56
diff --git a/_api_validation_status_8cs_source.html b/_api_validation_status_8cs_source.html index 9ddb06b1fb..45c3bca511 100644 --- a/_api_validation_status_8cs_source.html +++ b/_api_validation_status_8cs_source.html @@ -92,12 +92,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiValidationStatus.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Watchdog
2 {
7  {
32  }
33 }
The DMAPI never contacted the server for validation
+Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Watchdog
2 {
7  {
12 
17 
22 
27 
32 
37  }
38 }
The DMAPI never contacted the server for validation
The server was contacted for validation but it was never requested
Valid API. The game must be run with a minimum security level of Api.Models.DreamDaemonSecurity.Ultrasafe
Valid API. The game must be run with a minimum security level of Api.Models.DreamDaemonSecurity.Safe
- +
Valid API. The game must be run with a security level of Api.Models.DreamDaemonSecurity.Trusted
diff --git a/_application_8cs_source.html b/_application_8cs_source.html index 0ebd634bbf..792e9edaa6 100644 --- a/_application_8cs_source.html +++ b/_application_8cs_source.html @@ -92,21 +92,21 @@ 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 Newtonsoft.Json.Converters;
16 using Serilog;
17 using Serilog.Events;
18 using Serilog.Formatting.Display;
19 using System;
20 using System.Globalization;
21 using System.IdentityModel.Tokens.Jwt;
22 using System.Reflection;
23 using System.Threading.Tasks;
30 using Tgstation.Server.Host.IO;
33 
34 namespace Tgstation.Server.Host.Core
35 {
37  sealed class Application : IApplication
38  {
40  public string VersionPrefix => "tgstation-server";
41 
43  public Version Version { get; }
44 
46  public string VersionString { get; }
47 
51  readonly IConfiguration configuration;
52 
56  readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment;
57 
61  readonly TaskCompletionSource<object> startupTcs;
62 
67 
73  public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
74  {
75  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
76  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
77 
78  startupTcs = new TaskCompletionSource<object>();
79 
80  Version = Assembly.GetExecutingAssembly().GetName().Version;
81  VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
82  }
83 
88  public void ConfigureServices(IServiceCollection services)
89  {
90  if (services == null)
91  throw new ArgumentNullException(nameof(services));
92 
93  //needful
94  services.AddSingleton<IApplication>(this);
95 
96  //configure configuration
97  services.UseStandardConfig<UpdatesConfiguration>(configuration);
98  services.UseStandardConfig<DatabaseConfiguration>(configuration);
99  services.UseStandardConfig<GeneralConfiguration>(configuration);
100  services.UseStandardConfig<FileLoggingConfiguration>(configuration);
101  services.UseStandardConfig<ControlPanelConfiguration>(configuration);
102 
103  //enable options which give us config reloading
104  services.AddOptions();
105 
106  //other stuff needed for for setup wizard and configuration
107  services.AddSingleton<IIOManager, DefaultIOManager>();
108  services.AddSingleton<IConsole, IO.Console>();
109  services.AddSingleton<IDBConnectionFactory, DBConnectionFactory>();
110  services.AddSingleton<ISetupWizard, SetupWizard>();
111  services.AddSingleton<IPlatformIdentifier, PlatformIdentifier>();
112  services.AddSingleton<IAsyncDelayer, AsyncDelayer>();
113 
114  GeneralConfiguration generalConfiguration;
115  DatabaseConfiguration databaseConfiguration;
116  FileLoggingConfiguration fileLoggingConfiguration;
117  ControlPanelConfiguration controlPanelConfiguration;
118  IIOManager ioManager;
119  IPlatformIdentifier platformIdentifier;
120 
121  //temporarily build the service provider in it's current state
122  //do it here so we can run the setup wizard if necessary
123  //also allows us to get some options and other services we need for continued configuration
124  using (var provider = services.BuildServiceProvider())
125  {
126  //run the wizard if necessary
127  var setupWizard = provider.GetRequiredService<ISetupWizard>();
128  var applicationLifetime = provider.GetRequiredService<Microsoft.AspNetCore.Hosting.IApplicationLifetime>();
129  var setupWizardRan = setupWizard.CheckRunWizard(applicationLifetime.ApplicationStopping).GetAwaiter().GetResult();
130 
131  //load the configuration options we need
132  var generalOptions = provider.GetRequiredService<IOptions<GeneralConfiguration>>();
133  generalConfiguration = generalOptions.Value;
134 
135  //unless this is set, in which case, we leave
136  if (setupWizardRan && generalConfiguration.SetupWizardMode == SetupWizardMode.Only)
137  //we don't inject a logger in the constuctor to log this because it's not yet configured
138  throw new OperationCanceledException("Exiting due to SetupWizardMode configuration!");
139 
140  var dbOptions = provider.GetRequiredService<IOptions<DatabaseConfiguration>>();
141  databaseConfiguration = dbOptions.Value;
142 
143  var loggingOptions = provider.GetRequiredService<IOptions<FileLoggingConfiguration>>();
144  fileLoggingConfiguration = loggingOptions.Value;
145 
146  var controlPanelOptions = provider.GetRequiredService<IOptions<ControlPanelConfiguration>>();
147  controlPanelConfiguration = controlPanelOptions.Value;
148 
149  ioManager = provider.GetRequiredService<IIOManager>();
150  platformIdentifier = provider.GetRequiredService<IPlatformIdentifier>();
151  }
152 
153  //setup file logging via serilog
154  if (!fileLoggingConfiguration.Disable)
155  services.AddLogging(builder =>
156  {
157  //common app data is C:/ProgramData on windows, else /usr/shar
158  var logPath = !String.IsNullOrEmpty(fileLoggingConfiguration.Directory) ? fileLoggingConfiguration.Directory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
159 
160  logPath = ioManager.ConcatPath(logPath, "tgs-{Date}.log");
161 
162  LogEventLevel? ConvertLogLevel(LogLevel logLevel)
163  {
164  switch (logLevel)
165  {
166  case LogLevel.Critical:
167  return LogEventLevel.Fatal;
168  case LogLevel.Debug:
169  return LogEventLevel.Debug;
170  case LogLevel.Error:
171  return LogEventLevel.Error;
172  case LogLevel.Information:
173  return LogEventLevel.Information;
174  case LogLevel.Trace:
175  return LogEventLevel.Verbose;
176  case LogLevel.Warning:
177  return LogEventLevel.Warning;
178  case LogLevel.None:
179  return null;
180  default:
181  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel));
182  }
183  };
184 
185  var logEventLevel = ConvertLogLevel(fileLoggingConfiguration.LogLevel);
186  var microsoftEventLevel = ConvertLogLevel(fileLoggingConfiguration.MicrosoftLogLevel);
187 
188  var formatter = new MessageTemplateTextFormatter("{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", null);
189 
190  var configuration = new LoggerConfiguration()
191  .Enrich.FromLogContext()
192  .WriteTo.Async(w => w.RollingFile(formatter, logPath, shared: true, flushToDiskInterval: TimeSpan.FromSeconds(2)));
193 
194  if (logEventLevel.HasValue)
195  configuration.MinimumLevel.Is(logEventLevel.Value);
196 
197  if (microsoftEventLevel.HasValue)
198  configuration.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
199 
200  builder.AddSerilog(configuration.CreateLogger(), true);
201  });
202 
203  //configure bearer token validation
204  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
205  {
206  //this line isn't actually run until the first request is made
207  //at that point tokenFactory will be populated
208  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
209  jwtBearerOptions.Events = new JwtBearerEvents
210  {
211  //Application is our composition root so this monstrosity of a line is okay
212  OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService<IClaimsInjector>().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted)
213  };
214  });
215  //fucking converts 'sub' to M$ bs
216  //can't be done in the above lambda, that's too late
217  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
218 
219  //add mvc, configure the json serializer settings
220  services.AddMvc().AddJsonOptions(options =>
221  {
222  options.AllowInputFormatterExceptionMessages = true;
223  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
224  options.SerializerSettings.CheckAdditionalContent = true;
225  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
226  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
227  options.SerializerSettings.Converters = new[] { new VersionConverter() };
228  });
229 
230  //enable browser detection
231  services.AddDetectionCore().AddBrowser();
232 
233  //enable CORS if necessary
234  if (controlPanelConfiguration.AllowAnyOrigin || controlPanelConfiguration.AllowedOrigins?.Count > 0)
235  services.AddCors();
236 
237  void AddTypedContext<TContext>() where TContext : DatabaseContext<TContext>
238  {
239  services.AddDbContext<TContext>(builder =>
240  {
241  if (hostingEnvironment.IsDevelopment())
242  builder.EnableSensitiveDataLogging();
243  });
244  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
245  }
246 
247  //add the correct database context type
248  var dbType = databaseConfiguration.DatabaseType;
249  switch (dbType)
250  {
251  case DatabaseType.MySql:
252  case DatabaseType.MariaDB:
253  AddTypedContext<MySqlDatabaseContext>();
254  break;
255  case DatabaseType.SqlServer:
256  AddTypedContext<SqlServerDatabaseContext>();
257  break;
258  default:
259  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
260  }
261  //configure other database services
262  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
263  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
264 
265  //configure security services
267  services.AddScoped<IClaimsInjector, ClaimsInjector>();
268  services.AddSingleton<IIdentityCache, IdentityCache>();
269  services.AddSingleton<ICryptographySuite, CryptographySuite>();
270  services.AddSingleton<ITokenFactory, TokenFactory>();
271  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
272 
273  //configure platform specific services
274  if (platformIdentifier.IsWindows)
275  {
276  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
277  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
278  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
279  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
280 
281  services.AddSingleton<WindowsNetworkPromptReaper>();
282  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
283  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
284  }
285  else
286  {
287  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
288  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
289  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
290  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
291  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
292  }
293 
294  //configure misc services
295  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
296  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
297  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
298  services.AddSingleton<IByondTopicSender>(new ByondTopicSender
299  {
300  ReceiveTimeout = generalConfiguration.ByondTopicTimeout,
301  SendTimeout = generalConfiguration.ByondTopicTimeout
302  });
303 
304  //configure component services
305  services.AddSingleton<ICredentialsProvider, CredentialsProvider>();
306  services.AddSingleton<IProviderFactory, ProviderFactory>();
307  services.AddSingleton<IChatFactory, ChatFactory>();
308  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
309  services.AddSingleton<IInstanceFactory, InstanceFactory>();
310 
311  //configure root services
312  services.AddSingleton<InstanceManager>();
313  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
314  services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
315 
316  services.AddSingleton<IJobManager, JobManager>();
317  }
318 
327  public void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions, ILogger<Application> logger)
328  {
329  if (applicationBuilder == null)
330  throw new ArgumentNullException(nameof(applicationBuilder));
331  if (serverControl == null)
332  throw new ArgumentNullException(nameof(serverControl));
333 
334  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
335 
336  var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
337 
338  if (logger == null)
339  throw new ArgumentNullException(nameof(logger));
340 
341  logger.LogInformation(VersionString);
342  logger.LogDebug("Content Root: {0}", hostingEnvironment.ContentRootPath);
343  logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
344 
345  //attempt to restart the server if the configuration changes
346  if(serverControl.WatchdogPresent)
347  ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart());
348 
349  //now setup the HTTP request pipeline
350 
351  //should anything after this throw an exception, catch it and display a detailed html page
352  applicationBuilder.UseDeveloperExceptionPage(); //it is not worth it to limit this, you should only ever get it if you're an authorized user
353 
354  //suppress OperationCancelledExceptions, they are just aborted HTTP requests
355  applicationBuilder.UseCancelledRequestSuppression();
356 
357  //Set up CORS based on configuration if necessary
358  Action<CorsPolicyBuilder> corsBuilder = null;
359  if (controlPanelConfiguration.AllowAnyOrigin)
360  {
361  logger.LogTrace("Access-Control-Allow-Origin: *");
362  corsBuilder = builder => builder.AllowAnyOrigin();
363  }
364  else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
365  {
366  logger.LogTrace("Access-Control-Allow-Origin: ", String.Join(',', controlPanelConfiguration.AllowedOrigins));
367  corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
368  }
369 
370  if (corsBuilder != null)
371  {
372  var originalBuilder = corsBuilder;
373  corsBuilder = builder => originalBuilder(builder.AllowAnyHeader().AllowAnyMethod());
374  applicationBuilder.UseCors(corsBuilder);
375  }
376 
377  //Do not service requests until Ready is called, this will return 503 until that point
378  applicationBuilder.UseAsyncInitialization(async cancellationToken =>
379  {
380  using (cancellationToken.Register(() => startupTcs.SetCanceled()))
381  await startupTcs.Task.ConfigureAwait(false);
382  });
383 
384  //spa loading if necessary
385  if (controlPanelConfiguration.Enable)
386  {
387  logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
388  applicationBuilder.UseStaticFiles();
389  }
390  else
391  logger.LogDebug("Web control panel disabled!");
392 
393  //authenticate JWT tokens using our security pipeline if present, returns 401 if bad
394  applicationBuilder.UseAuthentication();
395 
396  //suppress and log database exceptions
397  applicationBuilder.UseDbConflictHandling();
398 
399  //majority of handling is done in the controllers
400  applicationBuilder.UseMvc();
401 
402  //404 anything that gets this far
403  }
404 
406  public void Ready(Exception initializationError)
407  {
408  lock (startupTcs)
409  {
410  if (startupTcs.Task.IsCompleted)
411  throw new InvalidOperationException("Ready has already been called!");
412  if (initializationError == null)
413  startupTcs.SetResult(null);
414  else
415  startupTcs.SetException(initializationError);
416  }
417  }
418  }
419 }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Cyberboss.AspNetCore.AsyncInitializer;
3 using Microsoft.AspNetCore.Authentication.JwtBearer;
4 using Microsoft.AspNetCore.Builder;
5 using Microsoft.AspNetCore.Cors.Infrastructure;
6 using Microsoft.AspNetCore.Hosting;
7 using Microsoft.AspNetCore.Identity;
8 using Microsoft.Extensions.Configuration;
9 using Microsoft.Extensions.DependencyInjection;
10 using Microsoft.Extensions.Hosting;
11 using Microsoft.Extensions.Logging;
12 using Microsoft.Extensions.Options;
13 using Microsoft.Extensions.Primitives;
14 using Newtonsoft.Json;
15 using Newtonsoft.Json.Converters;
16 using Serilog;
17 using Serilog.Events;
18 using Serilog.Formatting.Display;
19 using System;
20 using System.Globalization;
21 using System.IdentityModel.Tokens.Jwt;
22 using System.Reflection;
23 using System.Threading.Tasks;
30 using Tgstation.Server.Host.IO;
33 
34 namespace Tgstation.Server.Host.Core
35 {
37  #pragma warning disable CA1506
38  sealed class Application : IApplication
39  {
41  public string VersionPrefix => "tgstation-server";
42 
44  public Version Version { get; }
45 
47  public string VersionString { get; }
48 
52  readonly IConfiguration configuration;
53 
57  readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment;
58 
62  readonly TaskCompletionSource<object> startupTcs;
63 
68 
74  public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
75  {
76  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
77  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
78 
79  startupTcs = new TaskCompletionSource<object>();
80 
81  Version = Assembly.GetExecutingAssembly().GetName().Version;
82  VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
83  }
84 
89  public void ConfigureServices(IServiceCollection services)
90  {
91  if (services == null)
92  throw new ArgumentNullException(nameof(services));
93 
94  // needful
95  services.AddSingleton<IApplication>(this);
96 
97  // configure configuration
98  services.UseStandardConfig<UpdatesConfiguration>(configuration);
99  services.UseStandardConfig<DatabaseConfiguration>(configuration);
100  services.UseStandardConfig<GeneralConfiguration>(configuration);
101  services.UseStandardConfig<FileLoggingConfiguration>(configuration);
102  services.UseStandardConfig<ControlPanelConfiguration>(configuration);
103 
104  // enable options which give us config reloading
105  services.AddOptions();
106 
107  // other stuff needed for for setup wizard and configuration
108  services.AddSingleton<IIOManager, DefaultIOManager>();
109  services.AddSingleton<IConsole, IO.Console>();
110  services.AddSingleton<IDBConnectionFactory, DBConnectionFactory>();
111  services.AddSingleton<ISetupWizard, SetupWizard>();
112  services.AddSingleton<IPlatformIdentifier, PlatformIdentifier>();
113  services.AddSingleton<IAsyncDelayer, AsyncDelayer>();
114 
115  GeneralConfiguration generalConfiguration;
116  DatabaseConfiguration databaseConfiguration;
117  FileLoggingConfiguration fileLoggingConfiguration;
118  ControlPanelConfiguration controlPanelConfiguration;
119  IIOManager ioManager;
120  IPlatformIdentifier platformIdentifier;
121 
122  // temporarily build the service provider in it's current state
123  // do it here so we can run the setup wizard if necessary
124  // also allows us to get some options and other services we need for continued configuration
125  using (var provider = services.BuildServiceProvider())
126  {
127  // run the wizard if necessary
128  var setupWizard = provider.GetRequiredService<ISetupWizard>();
129  var applicationLifetime = provider.GetRequiredService<Microsoft.AspNetCore.Hosting.IApplicationLifetime>();
130  var setupWizardRan = setupWizard.CheckRunWizard(applicationLifetime.ApplicationStopping).GetAwaiter().GetResult();
131 
132  // load the configuration options we need
133  var generalOptions = provider.GetRequiredService<IOptions<GeneralConfiguration>>();
134  generalConfiguration = generalOptions.Value;
135 
136  // unless this is set, in which case, we leave
137  if (setupWizardRan && generalConfiguration.SetupWizardMode == SetupWizardMode.Only)
138  throw new OperationCanceledException("Exiting due to SetupWizardMode configuration!"); // we don't inject a logger in the constuctor to log this because it's not yet configured
139 
140  var dbOptions = provider.GetRequiredService<IOptions<DatabaseConfiguration>>();
141  databaseConfiguration = dbOptions.Value;
142 
143  var loggingOptions = provider.GetRequiredService<IOptions<FileLoggingConfiguration>>();
144  fileLoggingConfiguration = loggingOptions.Value;
145 
146  var controlPanelOptions = provider.GetRequiredService<IOptions<ControlPanelConfiguration>>();
147  controlPanelConfiguration = controlPanelOptions.Value;
148 
149  ioManager = provider.GetRequiredService<IIOManager>();
150  platformIdentifier = provider.GetRequiredService<IPlatformIdentifier>();
151  }
152 
153  // setup file logging via serilog
154  if (!fileLoggingConfiguration.Disable)
155  services.AddLogging(builder =>
156  {
157  // common app data is C:/ProgramData on windows, else /usr/shar
158  var logPath = !String.IsNullOrEmpty(fileLoggingConfiguration.Directory) ? fileLoggingConfiguration.Directory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
159 
160  logPath = ioManager.ConcatPath(logPath, "tgs-{Date}.log");
161 
162  LogEventLevel? ConvertLogLevel(LogLevel logLevel)
163  {
164  switch (logLevel)
165  {
166  case LogLevel.Critical:
167  return LogEventLevel.Fatal;
168  case LogLevel.Debug:
169  return LogEventLevel.Debug;
170  case LogLevel.Error:
171  return LogEventLevel.Error;
172  case LogLevel.Information:
173  return LogEventLevel.Information;
174  case LogLevel.Trace:
175  return LogEventLevel.Verbose;
176  case LogLevel.Warning:
177  return LogEventLevel.Warning;
178  case LogLevel.None:
179  return null;
180  default:
181  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel));
182  }
183  }
184 
185  var logEventLevel = ConvertLogLevel(fileLoggingConfiguration.LogLevel);
186  var microsoftEventLevel = ConvertLogLevel(fileLoggingConfiguration.MicrosoftLogLevel);
187 
188  var formatter = new MessageTemplateTextFormatter("{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", null);
189 
190  var configuration = new LoggerConfiguration()
191  .Enrich.FromLogContext()
192  .WriteTo.Async(w => w.RollingFile(formatter, logPath, shared: true, flushToDiskInterval: TimeSpan.FromSeconds(2)));
193 
194  if (logEventLevel.HasValue)
195  configuration.MinimumLevel.Is(logEventLevel.Value);
196 
197  if (microsoftEventLevel.HasValue)
198  configuration.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
199 
200  builder.AddSerilog(configuration.CreateLogger(), true);
201  });
202 
203  // configure bearer token validation
204  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
205  {
206  // this line isn't actually run until the first request is made
207  // at that point tokenFactory will be populated
208  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
209  jwtBearerOptions.Events = new JwtBearerEvents
210  {
211  // Application is our composition root so this monstrosity of a line is okay
212  OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService<IClaimsInjector>().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted)
213  };
214  });
215 
216  // fucking prevents converting 'sub' to M$ bs
217  // can't be done in the above lambda, that's too late
218  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
219 
220  // add mvc, configure the json serializer settings
221  services.AddMvc().AddJsonOptions(options =>
222  {
223  options.AllowInputFormatterExceptionMessages = true;
224  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
225  options.SerializerSettings.CheckAdditionalContent = true;
226  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
227  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
228  options.SerializerSettings.Converters = new[] { new VersionConverter() };
229  });
230 
231  // enable browser detection
232  services.AddDetectionCore().AddBrowser();
233 
234  // enable CORS if necessary
235  if (controlPanelConfiguration.AllowAnyOrigin || controlPanelConfiguration.AllowedOrigins?.Count > 0)
236  services.AddCors();
237 
238  void AddTypedContext<TContext>() where TContext : DatabaseContext<TContext>
239  {
240  services.AddDbContext<TContext>(builder =>
241  {
242  if (hostingEnvironment.IsDevelopment())
243  builder.EnableSensitiveDataLogging();
244  });
245  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
246  }
247 
248  // add the correct database context type
249  var dbType = databaseConfiguration.DatabaseType;
250  switch (dbType)
251  {
252  case DatabaseType.MySql:
253  case DatabaseType.MariaDB:
254  AddTypedContext<MySqlDatabaseContext>();
255  break;
256  case DatabaseType.SqlServer:
257  AddTypedContext<SqlServerDatabaseContext>();
258  break;
259  default:
260  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
261  }
262 
263  // configure other database services
264  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
265  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
266 
267  // configure security services
269  services.AddScoped<IClaimsInjector, ClaimsInjector>();
270  services.AddSingleton<IIdentityCache, IdentityCache>();
271  services.AddSingleton<ICryptographySuite, CryptographySuite>();
272  services.AddSingleton<ITokenFactory, TokenFactory>();
273  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
274 
275  // configure platform specific services
276  if (platformIdentifier.IsWindows)
277  {
278  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
279  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
280  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
281  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
282 
283  services.AddSingleton<WindowsNetworkPromptReaper>();
284  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
285  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
286  }
287  else
288  {
289  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
290  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
291  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
292  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
293  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
294  }
295 
296  // configure misc services
297  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
298  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
299  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
300  services.AddSingleton<IByondTopicSender>(new ByondTopicSender
301  {
302  ReceiveTimeout = generalConfiguration.ByondTopicTimeout,
303  SendTimeout = generalConfiguration.ByondTopicTimeout
304  });
305 
306  // configure component services
307  services.AddSingleton<ICredentialsProvider, CredentialsProvider>();
308  services.AddSingleton<IProviderFactory, ProviderFactory>();
309  services.AddSingleton<IChatFactory, ChatFactory>();
310  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
311  services.AddSingleton<IInstanceFactory, InstanceFactory>();
312 
313  // configure root services
314  services.AddSingleton<InstanceManager>();
315  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
316  services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
317 
318  services.AddSingleton<IJobManager, JobManager>();
319  }
320 
329  public void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions, ILogger<Application> logger)
330  {
331  if (applicationBuilder == null)
332  throw new ArgumentNullException(nameof(applicationBuilder));
333  if (serverControl == null)
334  throw new ArgumentNullException(nameof(serverControl));
335 
336  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
337 
338  var controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
339 
340  if (logger == null)
341  throw new ArgumentNullException(nameof(logger));
342 
343  logger.LogInformation(VersionString);
344  logger.LogDebug("Content Root: {0}", hostingEnvironment.ContentRootPath);
345  logger.LogTrace("Web Root: {0}", hostingEnvironment.WebRootPath);
346 
347  // attempt to restart the server if the configuration changes
348  if(serverControl.WatchdogPresent)
349  ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart());
350 
351  // setup the HTTP request pipeline
352 
353  // should anything after this throw an exception, catch it and display a detailed html page
354  applicationBuilder.UseDeveloperExceptionPage(); // it is not worth it to limit this, you should only ever get it if you're an authorized user
355 
356  // suppress OperationCancelledExceptions, they are just aborted HTTP requests
357  applicationBuilder.UseCancelledRequestSuppression();
358 
359  // Set up CORS based on configuration if necessary
360  Action<CorsPolicyBuilder> corsBuilder = null;
361  if (controlPanelConfiguration.AllowAnyOrigin)
362  {
363  logger.LogTrace("Access-Control-Allow-Origin: *");
364  corsBuilder = builder => builder.AllowAnyOrigin();
365  }
366  else if (controlPanelConfiguration.AllowedOrigins?.Count > 0)
367  {
368  logger.LogTrace("Access-Control-Allow-Origin: ", String.Join(',', controlPanelConfiguration.AllowedOrigins));
369  corsBuilder = builder => builder.WithOrigins(controlPanelConfiguration.AllowedOrigins.ToArray());
370  }
371 
372  if (corsBuilder != null)
373  {
374  var originalBuilder = corsBuilder;
375  corsBuilder = builder => originalBuilder(builder.AllowAnyHeader().AllowAnyMethod());
376  applicationBuilder.UseCors(corsBuilder);
377  }
378 
379  // Do not service requests until Ready is called, this will return 503 until that point
380  applicationBuilder.UseAsyncInitialization(async cancellationToken =>
381  {
382  using (cancellationToken.Register(() => startupTcs.SetCanceled()))
383  await startupTcs.Task.ConfigureAwait(false);
384  });
385 
386  // spa loading if necessary
387  if (controlPanelConfiguration.Enable)
388  {
389  logger.LogWarning("Web control panel enabled. This is a highly WIP feature!");
390  applicationBuilder.UseStaticFiles();
391  }
392  else
393  logger.LogDebug("Web control panel disabled!");
394 
395  // authenticate JWT tokens using our security pipeline if present, returns 401 if bad
396  applicationBuilder.UseAuthentication();
397 
398  // suppress and log database exceptions
399  applicationBuilder.UseDbConflictHandling();
400 
401  // majority of handling is done in the controllers
402  applicationBuilder.UseMvc();
403 
404  // 404 anything that gets this far
405  }
406 
408  public void Ready(Exception initializationError)
409  {
410  lock (startupTcs)
411  {
412  if (startupTcs.Task.IsCompleted)
413  throw new InvalidOperationException("Ready has already been called!");
414  if (initializationError == null)
415  startupTcs.SetResult(null);
416  else
417  startupTcs.SetException(initializationError);
418  }
419  }
420  }
421 }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
Manages the runtime of Jobs
Definition: IJobManager.cs:12
-
readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment
The Microsoft.AspNetCore.Hosting.IHostingEnvironment for the Application
Definition: Application.cs:56
+
readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment
The Microsoft.AspNetCore.Hosting.IHostingEnvironment for the Application
Definition: Application.cs:57
IPostWriteHandler for POSIX systems
-
+
Handles changing file modes/permissions after writing
List< string > AllowedOrigins
Origins allowed for CORS requests
-
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:327
+
void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< Application > logger)
Configure the Application
Definition: Application.cs:329
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
@@ -121,13 +121,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
ITokenFactory tokenFactory
The ITokenFactory for the Application
Definition: Application.cs:66
+
ITokenFactory tokenFactory
The ITokenFactory for the Application
Definition: Application.cs:67
For initially seeding a database
- - + +
ISystemIdentityFactory for windows systems. Uses long running tasks due to potential networked domain...
@@ -144,8 +144,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseType DatabaseType
The Configuration.DatabaseType to create
-
void Ready(Exception initializationError)
Mark the IApplication as ready to run
Definition: Application.cs:406
- +
void Ready(Exception initializationError)
Mark the IApplication as ready to run
Definition: Application.cs:408
+ @@ -154,14 +154,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
bool IsWindows
If the current platform is a Windows platform
-
readonly TaskCompletionSource< object > startupTcs
The TaskCompletionSource<TResult> used for determining when the Application is Ready(Exception) ...
Definition: Application.cs:61
- +
readonly TaskCompletionSource< object > startupTcs
The TaskCompletionSource<TResult> used for determining when the Application is Ready(Exception) ...
Definition: Application.cs:62
+ -
void ConfigureServices(IServiceCollection services)
Configure dependency injected services
Definition: Application.cs:88
+
void ConfigureServices(IServiceCollection services)
Configure dependency injected services
Definition: Application.cs:89
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory
@@ -171,8 +171,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Configuration options for the Models.DatabaseContext<TParentContext>
-
IIOManager that resolves paths to Environment.CurrentDirectory
-
Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
Construct an Application
Definition: Application.cs:73
+
IIOManager that resolves paths to Environment.CurrentDirectory
+
Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
Construct an Application
Definition: Application.cs:74
For injecting System.Security.Claims.Claims that Controllers.TgsAuthorizeAttribute can look for ...
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
@@ -191,7 +191,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
readonly IConfiguration configuration
The IConfiguration for the Application
Definition: Application.cs:51
+
readonly IConfiguration configuration
The IConfiguration for the Application
Definition: Application.cs:52
string ConcatPath(params string[] paths)
Combines an array of strings into a path
@@ -200,7 +200,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
diff --git a/_authentication_context_8cs_source.html b/_authentication_context_8cs_source.html index 6374c20d82..18a001398b 100644 --- a/_authentication_context_8cs_source.html +++ b/_authentication_context_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
AuthenticationContext.cs
-Go to the documentation of this file.
1 using System;
2 using System.Linq;
5 
6 namespace Tgstation.Server.Host.Security
7 {
12  {
14  public User User { get; }
15 
17  public InstanceUser InstanceUser { get; }
18 
20  public ISystemIdentity SystemIdentity { get; }
21 
25  public AuthenticationContext() { }
26 
33  public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstanceUser instanceUser)
34  {
35  User = user ?? throw new ArgumentNullException(nameof(user));
36  if (systemIdentity == null && User.SystemIdentifier != null)
37  throw new ArgumentNullException(nameof(systemIdentity));
38  InstanceUser = instanceUser;
39  SystemIdentity = systemIdentity;
40  }
41 
43  public void Dispose() => SystemIdentity?.Dispose();
44 
46  public ulong GetRight(RightsType rightsType)
47  {
48  var isInstance = RightsHelper.IsInstanceRight(rightsType);
49 
50  if (User == null)
51  throw new InvalidOperationException("Authentication context has no user!");
52 
53  if (isInstance && InstanceUser == null)
54  return 0;
55  var rightsEnum = RightsHelper.RightToType(rightsType);
56  // use the api versions because they're the ones that contain the actual properties
57  var typeToCheck = isInstance ? typeof(InstanceUser) : typeof(User);
58 
59  var nullableType = typeof(Nullable<>);
60  var nullableRightsType = nullableType.MakeGenericType(rightsEnum);
61 
62  var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == nullableRightsType).First();
63 
64  var right = prop.GetMethod.Invoke(isInstance ? (object)InstanceUser : User, Array.Empty<object>());
65 
66  if (right == null)
67  throw new InvalidOperationException("A user right was null!");
68  return (ulong)right;
69  }
70  }
71 }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
+Go to the documentation of this file.
1 using System;
2 using System.Linq;
5 
6 namespace Tgstation.Server.Host.Security
7 {
12  {
14  public User User { get; }
15 
17  public InstanceUser InstanceUser { get; }
18 
20  public ISystemIdentity SystemIdentity { get; }
21 
25  public AuthenticationContext() { }
26 
33  public AuthenticationContext(ISystemIdentity systemIdentity, User user, InstanceUser instanceUser)
34  {
35  User = user ?? throw new ArgumentNullException(nameof(user));
36  if (systemIdentity == null && User.SystemIdentifier != null)
37  throw new ArgumentNullException(nameof(systemIdentity));
38  InstanceUser = instanceUser;
39  SystemIdentity = systemIdentity;
40  }
41 
43  public void Dispose() => SystemIdentity?.Dispose();
44 
46  public ulong GetRight(RightsType rightsType)
47  {
48  var isInstance = RightsHelper.IsInstanceRight(rightsType);
49 
50  if (User == null)
51  throw new InvalidOperationException("Authentication context has no user!");
52 
53  if (isInstance && InstanceUser == null)
54  return 0;
55  var rightsEnum = RightsHelper.RightToType(rightsType);
56 
57  // use the api versions because they're the ones that contain the actual properties
58  var typeToCheck = isInstance ? typeof(InstanceUser) : typeof(User);
59 
60  var nullableType = typeof(Nullable<>);
61  var nullableRightsType = nullableType.MakeGenericType(rightsEnum);
62 
63  var prop = typeToCheck.GetProperties().Where(x => x.PropertyType == nullableRightsType).First();
64 
65  var right = prop.GetMethod.Invoke(isInstance ? (object)InstanceUser : User, Array.Empty<object>());
66 
67  if (right == null)
68  throw new InvalidOperationException("A user right was null!");
69  return (ulong)right;
70  }
71  }
72 }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
AuthenticationContext(ISystemIdentity systemIdentity, User user, InstanceUser instanceUser)
Construct an AuthenticationContext
AuthenticationContext()
Construct an empty AuthenticationContext
diff --git a/_authentication_context_factory_8cs_source.html b/_authentication_context_factory_8cs_source.html index f0a6ac58fa..951311cdef 100644 --- a/_authentication_context_factory_8cs_source.html +++ b/_authentication_context_factory_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
AuthenticationContextFactory.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Security
9 {
12  {
14  public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
15 
20 
25 
30 
37  public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext, IIdentityCache identityCache)
38  {
39  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
40  this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
41  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
42  }
43 
45  public void Dispose() => CurrentAuthenticationContext?.Dispose();
46 
48  public async Task CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset validAfter, CancellationToken cancellationToken)
49  {
50  if (CurrentAuthenticationContext != null)
51  throw new InvalidOperationException("Authentication context has already been loaded");
52 
53  var userQuery = databaseContext.Users.Where(x => x.Id == userId)
54  .Include(x => x.CreatedBy)
55  .FirstOrDefaultAsync(cancellationToken);
56 
57  var instanceUserQuery = instanceId.HasValue ? databaseContext.InstanceUsers
58  .Where(x => x.UserId == userId && x.InstanceId == instanceId && x.Instance.Online.Value)
59  .Include(x => x.Instance)
60  .FirstOrDefaultAsync(cancellationToken) : Task.FromResult<InstanceUser>(null);
61 
62  var user = await userQuery.ConfigureAwait(false);
63  if (user == default)
64  {
65  CurrentAuthenticationContext = new AuthenticationContext();
66  return;
67  }
68 
69  ISystemIdentity systemIdentity;
70  if (user.SystemIdentifier != null)
71  systemIdentity = identityCache.LoadCachedIdentity(user);
72  else
73  {
74  if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > validAfter)
75  {
76  CurrentAuthenticationContext = new AuthenticationContext();
77  return;
78  }
79  systemIdentity = null;
80  }
81 
82  try
83  {
84  var instanceUser = await instanceUserQuery.ConfigureAwait(false);
85 
86  CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instanceUser);
87  }
88  catch
89  {
90  systemIdentity?.Dispose();
91  throw;
92  }
93  }
94  }
95 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Security
9 {
12  {
14  public IAuthenticationContext CurrentAuthenticationContext { get; private set; }
15 
20 
25 
30 
37  public AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext, IIdentityCache identityCache)
38  {
39  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
40  this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
41  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
42  }
43 
45  public void Dispose() => CurrentAuthenticationContext?.Dispose();
46 
48  public async Task CreateAuthenticationContext(long userId, long? instanceId, DateTimeOffset validAfter, CancellationToken cancellationToken)
49  {
50  if (CurrentAuthenticationContext != null)
51  throw new InvalidOperationException("Authentication context has already been loaded");
52 
53  var userQuery = databaseContext.Users.Where(x => x.Id == userId)
54  .Include(x => x.CreatedBy)
55  .FirstOrDefaultAsync(cancellationToken);
56 
57  var instanceUserQuery = instanceId.HasValue ? databaseContext.InstanceUsers
58  .Where(x => x.UserId == userId && x.InstanceId == instanceId && x.Instance.Online.Value)
59  .Include(x => x.Instance)
60  .FirstOrDefaultAsync(cancellationToken) : Task.FromResult<InstanceUser>(null);
61 
62  var user = await userQuery.ConfigureAwait(false);
63  if (user == default)
64  {
65  CurrentAuthenticationContext = new AuthenticationContext();
66  return;
67  }
68 
69  ISystemIdentity systemIdentity;
70  if (user.SystemIdentifier != null)
71  systemIdentity = identityCache.LoadCachedIdentity(user);
72  else
73  {
74  if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate > validAfter)
75  {
76  CurrentAuthenticationContext = new AuthenticationContext();
77  return;
78  }
79 
80  systemIdentity = null;
81  }
82 
83  try
84  {
85  var instanceUser = await instanceUserQuery.ConfigureAwait(false);
86 
87  CurrentAuthenticationContext = new AuthenticationContext(systemIdentity, user, instanceUser);
88  }
89  catch
90  {
91  systemIdentity?.Dispose();
92  throw;
93  }
94  }
95  }
96 }
AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext, IIdentityCache identityCache)
Construct an AuthenticationContextFactory
diff --git a/_bool_converter_8cs_source.html b/_bool_converter_8cs_source.html index b11fbad952..3f361bfc58 100644 --- a/_bool_converter_8cs_source.html +++ b/_bool_converter_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
BoolConverter.cs
-Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using System;
3 
4 namespace Tgstation.Server.Host.Components.Chat.Commands
5 {
9  sealed class BoolConverter : JsonConverter
10  {
12  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue(((bool)value) ? 1 : 0);
13 
15  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => reader.Value.ToString() == "1";
16 
18  public override bool CanConvert(Type objectType) => objectType == typeof(bool);
19  }
20 }
+Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using System;
3 
4 namespace Tgstation.Server.Host.Components.Chat.Commands
5 {
9  sealed class BoolConverter : JsonConverter
10  {
12  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => writer.WriteValue(((bool)value) ? 1 : 0);
13 
15  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => reader.Value.ToString() == "1";
16 
18  public override bool CanConvert(Type objectType) => objectType == typeof(bool);
19  }
20 }
JsonConverter for decoding bools returned by BYOND
Definition: BoolConverter.cs:9
diff --git a/_byond_client_8cs_source.html b/_byond_client_8cs_source.html index 6103aacb4f..951395406f 100644 --- a/_byond_client_8cs_source.html +++ b/_byond_client_8cs_source.html @@ -92,16 +92,16 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ByondClient.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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
11  sealed class ByondClient : IByondClient
12  {
20  readonly Instance instance;
21 
27  public ByondClient(IApiClient apiClient, Instance instance)
28  {
29  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
30  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
31  }
32 
34  public Task<Byond> ActiveVersion(CancellationToken cancellationToken) => apiClient.Read<Byond>(Routes.Byond, instance.Id, cancellationToken);
35 
37  public Task<IReadOnlyList<Byond>> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Byond>>(Routes.List(Routes.Byond), instance.Id, cancellationToken);
38 
40  public Task<Byond> SetActiveVersion(Byond byond, CancellationToken cancellationToken) => apiClient.Update<Byond, Byond>(Routes.Byond, byond ?? throw new ArgumentNullException(nameof(byond)), instance.Id, cancellationToken);
41  }
42 }
const string Byond
The Models.Byond controller
Definition: Routes.cs:34
+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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
11  sealed class ByondClient : IByondClient
12  {
17 
21  readonly Instance instance;
22 
28  public ByondClient(IApiClient apiClient, Instance instance)
29  {
30  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
31  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
32  }
33 
35  public Task<Byond> ActiveVersion(CancellationToken cancellationToken) => apiClient.Read<Byond>(Routes.Byond, instance.Id, cancellationToken);
36 
38  public Task<IReadOnlyList<Byond>> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Byond>>(Routes.List(Routes.Byond), instance.Id, cancellationToken);
39 
41  public Task<Byond> SetActiveVersion(Byond byond, CancellationToken cancellationToken) => apiClient.Update<Byond, Byond>(Routes.Byond, byond ?? throw new ArgumentNullException(nameof(byond)), instance.Id, cancellationToken);
42  }
43 }
const string Byond
The Models.Byond controller
Definition: Routes.cs:34
For managing the Byond installation
Definition: IByondClient.cs:11
-
ByondClient(IApiClient apiClient, Instance instance)
Construct a ByondClient
Definition: ByondClient.cs:27
+
ByondClient(IApiClient apiClient, Instance instance)
Construct a ByondClient
Definition: ByondClient.cs:28
readonly IApiClient apiClient
The IApiClient for the ByondClient
Definition: ByondClient.cs:16
Routes to a server actions
Definition: Routes.cs:9
-
readonly Instance instance
The Instance for the ByondClient
Definition: ByondClient.cs:20
+
readonly Instance instance
The Instance for the ByondClient
Definition: ByondClient.cs:21
Metadata about a server instance
Definition: Instance.cs:9
Represents a BYOND installation
Definition: Byond.cs:8
diff --git a/_byond_controller_8cs_source.html b/_byond_controller_8cs_source.html index 48e407eee5..54559f0ab0 100644 --- a/_byond_controller_8cs_source.html +++ b/_byond_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ByondController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Globalization;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
8 using Tgstation.Server.Api;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.Byond)]
22  public sealed class ByondController : ModelController<Api.Models.Byond>
23  {
28 
33 
42  public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger<ByondController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
43  {
44  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
45  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
46  }
47 
49  [TgsAuthorize(ByondRights.ReadActive)]
50  public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)
51  Json(new Api.Models.Byond
52  {
53  Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion
54  }));
55 
57  [TgsAuthorize(ByondRights.ListInstalled)]
58  public override Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)
59  Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond
60  {
61  Version = x
62  })));
63 
65  [TgsAuthorize(ByondRights.ChangeVersion)]
66  public override async Task<IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
67  {
68  if (model == null)
69  throw new ArgumentNullException(nameof(model));
70 
71  if (model.Version == null)
72  return BadRequest(new ErrorMessage { Message = "Missing version!" });
73 
74  var byondManager = instanceManager.GetInstance(Instance).ByondManager;
75 
76  //remove cruff fields
77  var installingVersion = new Version(model.Version.Major, model.Version.Minor);
78 
79  var result = new Api.Models.Byond();
80 
81  if (byondManager.InstalledVersions.Any(x => x == model.Version))
82  {
83  Logger.LogInformation("User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
84  await byondManager.ChangeVersion(model.Version, cancellationToken).ConfigureAwait(false);
85  }
86  else
87  {
88  Logger.LogInformation("User ID {0} installing BYOND version to {2} on instance ID {1}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
89  //run the install through the job manager
90  var job = new Models.Job
91  {
92  Description = String.Format(CultureInfo.InvariantCulture, "Install BYOND version {0}", installingVersion),
93  StartedBy = AuthenticationContext.User,
94  CancelRightsType = RightsType.Byond,
95  CancelRight = (ulong)ByondRights.CancelInstall,
97  };
98  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false);
99  result.InstallJob = job.ToApi();
100  }
101  if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0)
102  result.Version = byondManager.ActiveVersion;
103  return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result);
104  }
105  }
106 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Globalization;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
8 using Tgstation.Server.Api;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.Byond)]
22  public sealed class ByondController : ModelController<Api.Models.Byond>
23  {
28 
33 
42  public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger<ByondController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
43  {
44  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
45  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
46  }
47 
49  [TgsAuthorize(ByondRights.ReadActive)]
50  public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult<IActionResult>(
51  Json(new Api.Models.Byond
52  {
53  Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion
54  }));
55 
57  [TgsAuthorize(ByondRights.ListInstalled)]
58  public override Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult<IActionResult>(
59  Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond
60  {
61  Version = x
62  })));
63 
65  [TgsAuthorize(ByondRights.ChangeVersion)]
66  public override async Task<IActionResult> Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
67  {
68  if (model == null)
69  throw new ArgumentNullException(nameof(model));
70 
71  if (model.Version == null)
72  return BadRequest(new ErrorMessage { Message = "Missing version!" });
73 
74  var byondManager = instanceManager.GetInstance(Instance).ByondManager;
75 
76  // remove cruff fields
77  var installingVersion = new Version(model.Version.Major, model.Version.Minor);
78 
79  var result = new Api.Models.Byond();
80 
81  if (byondManager.InstalledVersions.Any(x => x == model.Version))
82  {
83  Logger.LogInformation("User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
84  await byondManager.ChangeVersion(model.Version, cancellationToken).ConfigureAwait(false);
85  }
86  else
87  {
88  Logger.LogInformation("User ID {0} installing BYOND version to {2} on instance ID {1}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
89 
90  // run the install through the job manager
91  var job = new Models.Job
92  {
93  Description = String.Format(CultureInfo.InvariantCulture, "Install BYOND version {0}", installingVersion),
94  StartedBy = AuthenticationContext.User,
95  CancelRightsType = RightsType.Byond,
96  CancelRight = (ulong)ByondRights.CancelInstall,
98  };
99  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false);
100  result.InstallJob = job.ToApi();
101  }
102 
103  if ((AuthenticationContext.GetRight(RightsType.Byond) & (ulong)ByondRights.ReadActive) != 0)
104  result.Version = byondManager.ActiveVersion;
105  return result.InstallJob != null ? (IActionResult)Accepted(result) : Json(result);
106  }
107  }
108 }
const string Byond
The Models.Byond controller
Definition: Routes.cs:34
Manages the runtime of Jobs
Definition: IJobManager.cs:12
diff --git a/_byond_executable_lock_8cs_source.html b/_byond_executable_lock_8cs_source.html index 93df1cb913..fde768175e 100644 --- a/_byond_executable_lock_8cs_source.html +++ b/_byond_executable_lock_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ByondExecutableLock.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Host.Components.Byond
4 {
7  {
9  public Version Version { get; }
10 
12  public string DreamDaemonPath { get; }
13 
15  public string DreamMakerPath { get; }
16 
23  public ByondExecutableLock(Version version, string dreamDaemonPath, string dreamMakerPath)
24  {
25  Version = version ?? throw new ArgumentNullException(nameof(version));
26  DreamDaemonPath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath));
27  DreamMakerPath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath));
28  }
29 
30  //at one point in design, byond versions were to delete themselves if they werent the active version
31  //That changed at some point so these functions are intentioanlly left blank
32 
34  public void Dispose() { }
35 
37  public void DoNotDeleteThisSession() { }
38  }
39 }
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Host.Components.Byond
4 {
7  {
9  public Version Version { get; }
10 
12  public string DreamDaemonPath { get; }
13 
15  public string DreamMakerPath { get; }
16 
23  public ByondExecutableLock(Version version, string dreamDaemonPath, string dreamMakerPath)
24  {
25  Version = version ?? throw new ArgumentNullException(nameof(version));
26  DreamDaemonPath = dreamDaemonPath ?? throw new ArgumentNullException(nameof(dreamDaemonPath));
27  DreamMakerPath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath));
28  }
29 
30  // at one point in design, byond versions were to delete themselves if they weren't the active version
31  // That changed at some point so these functions are intentioanlly left blank
32 
34  public void Dispose() { }
35 
37  public void DoNotDeleteThisSession() { }
38  }
39 }
ByondExecutableLock(Version version, string dreamDaemonPath, string dreamMakerPath)
Construct a ByondExecutableLock
void DoNotDeleteThisSession()
Call if, during a detach, this version should not be deleted
Represents usage of the two primary BYOND server executables
diff --git a/_byond_manager_8cs_source.html b/_byond_manager_8cs_source.html index 410613956f..0a983e89c7 100644 --- a/_byond_manager_8cs_source.html +++ b/_byond_manager_8cs_source.html @@ -92,32 +92,32 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ByondManager.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Linq;
6 using System.Net;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Byond
14 {
16  sealed class ByondManager : IByondManager
17  {
21  public const string BinPath = "byond/bin";
22 
26  const string VersionFileName = "Version.txt";
30  const string ActiveVersionFileName = "ActiveVersion.txt";
31 
33  public Version ActiveVersion { get; private set; }
34 
36  public IReadOnlyList<Version> InstalledVersions
37  {
38  get
39  {
40  lock (installedVersions)
41  return installedVersions.Select(x => Version.Parse(x.Key)).ToList();
42  }
43  }
44 
49 
54 
59 
63  readonly ILogger<ByondManager> logger;
64 
68  readonly Dictionary<string, Task> installedVersions;
69 
73  readonly SemaphoreSlim semaphore;
74 
80  static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
81 
89  public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger<ByondManager> logger)
90  {
91  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
92  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
93  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
94  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
95 
96  installedVersions = new Dictionary<string, Task>();
97  semaphore = new SemaphoreSlim(1);
98  }
99 
101  public void Dispose() => semaphore.Dispose();
102 
109  async Task InstallVersion(Version version, CancellationToken cancellationToken)
110  {
111  var ourTcs = new TaskCompletionSource<object>();
112  Task inProgressTask;
113 
114  var versionKey = VersionKey(version);
115  bool installed;
116  lock (installedVersions)
117  {
118  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
119  if (!installed)
120  installedVersions.Add(versionKey, ourTcs.Task);
121  }
122  if (installed)
123  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
124  {
125  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
126  cancellationToken.ThrowIfCancellationRequested();
127  return;
128  }
129  //okay up to us to install it then
130  try
131  {
132  await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
133  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
134 
135  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
136  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
137 
138  try
139  {
140  var download = await downloadTask.ConfigureAwait(false);
141  await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
142  await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
143 
144  //make sure to do this last because this is what tells us we have a valid version in the future
145  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
146  }
147  catch (WebException e)
148  {
149  //since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
150  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message));
151  }
152  catch (OperationCanceledException)
153  {
154  throw;
155  }
156  catch
157  {
158  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
159  throw;
160  }
161 
162  ourTcs.SetResult(null);
163  }
164  catch (Exception e)
165  {
166  if (!(e is OperationCanceledException))
167  await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken).ConfigureAwait(false);
168  lock (installedVersions)
169  installedVersions.Remove(versionKey);
170  ourTcs.SetException(e);
171  throw;
172  }
173  }
174 
176  public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
177  {
178  if (version == null)
179  throw new ArgumentNullException(nameof(version));
180  var versionKey = VersionKey(version);
181  await InstallVersion(version, cancellationToken).ConfigureAwait(false);
182  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
183  {
184  await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false);
185  await eventConsumer.HandleEvent(EventType.ByondActiveVersionChange, new List<string> { ActiveVersion != null ? VersionKey(ActiveVersion) : null, versionKey }, cancellationToken).ConfigureAwait(false);
186  ActiveVersion = version;
187  }
188  }
189 
191  public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
192  {
193  var versionToUse = requiredVersion ?? ActiveVersion;
194  if (versionToUse == null)
195  throw new JobException("No BYOND versions installed!");
196  await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false);
197 
198  var versionKey = VersionKey(versionToUse);
199 
200  return new ByondExecutableLock(versionToUse, ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)), ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)));
201  }
202 
204  public async Task StartAsync(CancellationToken cancellationToken)
205  {
206  async Task<byte[]> GetActiveVersion()
207  {
208  if (!await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false))
209  return null;
210  return await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
211  }
212 
213  var activeVersionBytesTask = GetActiveVersion();
214 
215  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
216  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
217 
218  async Task ReadVersion(string path)
219  {
220  var versionFile = ioManager.ConcatPath(path, VersionFileName);
221  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
222  {
223  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
224  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); //cleanup
225  return;
226  }
227  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
228  var text = Encoding.UTF8.GetString(bytes);
229  if (Version.TryParse(text, out var version))
230  {
231  var key = VersionKey(version);
232  lock (installedVersions)
233  if (!installedVersions.ContainsKey(key))
234  {
235  installedVersions.Add(key, Task.CompletedTask);
236  return;
237  }
238  }
239  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
240  };
241 
242  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
243 
244  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
245  if (activeVersionBytes != null)
246  {
247  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
248  if (Version.TryParse(activeVersionString, out var activeVersion))
249  ActiveVersion = activeVersion;
250  else
251  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
252  }
253  }
254 
256  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
257  }
258 }
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:48
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Linq;
6 using System.Net;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Byond
14 {
16  sealed class ByondManager : IByondManager
17  {
21  public const string BinPath = "byond/bin";
22 
26  const string VersionFileName = "Version.txt";
27 
31  const string ActiveVersionFileName = "ActiveVersion.txt";
32 
34  public Version ActiveVersion { get; private set; }
35 
37  public IReadOnlyList<Version> InstalledVersions
38  {
39  get
40  {
41  lock (installedVersions)
42  return installedVersions.Select(x => Version.Parse(x.Key)).ToList();
43  }
44  }
45 
50 
55 
60 
64  readonly ILogger<ByondManager> logger;
65 
69  readonly Dictionary<string, Task> installedVersions;
70 
74  readonly SemaphoreSlim semaphore;
75 
81  static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
82 
90  public ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger<ByondManager> logger)
91  {
92  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
93  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
94  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
95  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
96 
97  installedVersions = new Dictionary<string, Task>();
98  semaphore = new SemaphoreSlim(1);
99  }
100 
102  public void Dispose() => semaphore.Dispose();
103 
110  async Task InstallVersion(Version version, CancellationToken cancellationToken)
111  {
112  var ourTcs = new TaskCompletionSource<object>();
113  Task inProgressTask;
114 
115  var versionKey = VersionKey(version);
116  bool installed;
117  lock (installedVersions)
118  {
119  installed = installedVersions.TryGetValue(versionKey, out inProgressTask);
120  if (!installed)
121  installedVersions.Add(versionKey, ourTcs.Task);
122  }
123 
124  if (installed)
125  using (cancellationToken.Register(() => ourTcs.SetCanceled()))
126  {
127  await Task.WhenAny(ourTcs.Task, inProgressTask).ConfigureAwait(false);
128  cancellationToken.ThrowIfCancellationRequested();
129  return;
130  }
131 
132  // okay up to us to install it then
133  try
134  {
135  await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
136  var downloadTask = byondInstaller.DownloadVersion(version, cancellationToken);
137 
138  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
139  await ioManager.CreateDirectory(versionKey, cancellationToken).ConfigureAwait(false);
140 
141  try
142  {
143  var download = await downloadTask.ConfigureAwait(false);
144  await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
145  await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
146 
147  // make sure to do this last because this is what tells us we have a valid version in the future
148  await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
149  }
150  catch (WebException e)
151  {
152  // since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
153  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message));
154  }
155  catch (OperationCanceledException)
156  {
157  throw;
158  }
159  catch
160  {
161  await ioManager.DeleteDirectory(versionKey, cancellationToken).ConfigureAwait(false);
162  throw;
163  }
164 
165  ourTcs.SetResult(null);
166  }
167  catch (Exception e)
168  {
169  if (!(e is OperationCanceledException))
170  await eventConsumer.HandleEvent(EventType.ByondInstallFail, new List<string> { e.Message }, cancellationToken).ConfigureAwait(false);
171  lock (installedVersions)
172  installedVersions.Remove(versionKey);
173  ourTcs.SetException(e);
174  throw;
175  }
176  }
177 
179  public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
180  {
181  if (version == null)
182  throw new ArgumentNullException(nameof(version));
183  var versionKey = VersionKey(version);
184  await InstallVersion(version, cancellationToken).ConfigureAwait(false);
185  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
186  {
187  await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(versionKey), cancellationToken).ConfigureAwait(false);
188  await eventConsumer.HandleEvent(EventType.ByondActiveVersionChange, new List<string> { ActiveVersion != null ? VersionKey(ActiveVersion) : null, versionKey }, cancellationToken).ConfigureAwait(false);
189  ActiveVersion = version;
190  }
191  }
192 
194  public async Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
195  {
196  var versionToUse = requiredVersion ?? ActiveVersion;
197  if (versionToUse == null)
198  throw new JobException("No BYOND versions installed!");
199  await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false);
200 
201  var versionKey = VersionKey(versionToUse);
202 
203  return new ByondExecutableLock(versionToUse, ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamDaemonName)), ioManager.ResolvePath(ioManager.ConcatPath(versionKey, BinPath, byondInstaller.DreamMakerName)));
204  }
205 
207  public async Task StartAsync(CancellationToken cancellationToken)
208  {
209  async Task<byte[]> GetActiveVersion()
210  {
211  var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
212  return !activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
213  }
214 
215  var activeVersionBytesTask = GetActiveVersion();
216 
217  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
218  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
219 
220  async Task ReadVersion(string path)
221  {
222  var versionFile = ioManager.ConcatPath(path, VersionFileName);
223  if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
224  {
225  logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
226  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); // cleanup
227  return;
228  }
229 
230  var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
231  var text = Encoding.UTF8.GetString(bytes);
232  if (Version.TryParse(text, out var version))
233  {
234  var key = VersionKey(version);
235  lock (installedVersions)
236  if (!installedVersions.ContainsKey(key))
237  {
238  installedVersions.Add(key, Task.CompletedTask);
239  return;
240  }
241  }
242 
243  await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
244  }
245 
246  await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
247 
248  var activeVersionBytes = await activeVersionBytesTask.ConfigureAwait(false);
249  if (activeVersionBytes != null)
250  {
251  var activeVersionString = Encoding.UTF8.GetString(activeVersionBytes);
252  if (Version.TryParse(activeVersionString, out var activeVersion))
253  ActiveVersion = activeVersion;
254  else
255  await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
256  }
257  }
258 
260  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
261  }
262 }
readonly IIOManager ioManager
The IIOManager for the ByondManager
Definition: ByondManager.cs:49
Consumes EventTypes and takes the appropriate actions
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
-
async Task StartAsync(CancellationToken cancellationToken)
+
async Task StartAsync(CancellationToken cancellationToken)
-
async Task< IByondExecutableLock > UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
Lock the current installation&#39;s location and return a IByondExecutableLock
+
async Task< IByondExecutableLock > UseExecutables(Version requiredVersion, CancellationToken cancellationToken)
Lock the current installation&#39;s location and return a IByondExecutableLock
For downloading and installing BYOND extractions for a given system
-
async Task InstallVersion(Version version, CancellationToken cancellationToken)
Installs a BYOND version if it isn&#39;t already
+
async Task InstallVersion(Version version, CancellationToken cancellationToken)
Installs a BYOND version if it isn&#39;t already
For managing the BYOND installation
-
readonly Dictionary< string, Task > installedVersions
Map of byond Versions to Tasks that complete when they are installed
Definition: ByondManager.cs:68
-
ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger< ByondManager > logger)
Construct a ByondManager
Definition: ByondManager.cs:89
-
readonly ILogger< ByondManager > logger
The ILogger for the ByondManager
Definition: ByondManager.cs:63
+
readonly Dictionary< string, Task > installedVersions
Map of byond Versions to Tasks that complete when they are installed
Definition: ByondManager.cs:69
+
ByondManager(IIOManager ioManager, IByondInstaller byondInstaller, IEventConsumer eventConsumer, ILogger< ByondManager > logger)
Construct a ByondManager
Definition: ByondManager.cs:90
+
readonly ILogger< ByondManager > logger
The ILogger for the ByondManager
Definition: ByondManager.cs:64
-
async Task ChangeVersion(Version version, CancellationToken cancellationToken)
Change the active BYOND version
+
async Task ChangeVersion(Version version, CancellationToken cancellationToken)
Change the active BYOND version
-
readonly IByondInstaller byondInstaller
The IByondInstaller for the ByondManager
Definition: ByondManager.cs:53
+
readonly IByondInstaller byondInstaller
The IByondInstaller for the ByondManager
Definition: ByondManager.cs:54
Interface for using filesystems
Definition: IIOManager.cs:11
-
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the ByondManager
Definition: ByondManager.cs:73
+
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the ByondManager
Definition: ByondManager.cs:74
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:8
-
readonly IEventConsumer eventConsumer
The IEventConsumer for the ByondManager
Definition: ByondManager.cs:58
+
readonly IEventConsumer eventConsumer
The IEventConsumer for the ByondManager
Definition: ByondManager.cs:59
diff --git a/_byond_rights_8cs_source.html b/_byond_rights_8cs_source.html index 9fa07a00b3..2285d35a3a 100644 --- a/_byond_rights_8cs_source.html +++ b/_byond_rights_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ByondRights.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum ByondRights : ulong
10  {
14  None = 0,
18  ReadActive = 1,
22  ListInstalled = 2,
26  ChangeVersion = 4,
30  CancelInstall = 8
31  }
32 }
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum ByondRights : ulong
10  {
14  None = 0,
15 
19  ReadActive = 1,
20 
24  ListInstalled = 2,
25 
29  ChangeVersion = 4,
30 
34  CancelInstall = 8
35  }
36 }
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
User may list all installed BYOND versions
diff --git a/_channel_8cs_source.html b/_channel_8cs_source.html index db6449eddc..0f50fae1ec 100644 --- a/_channel_8cs_source.html +++ b/_channel_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Channel.cs
-Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using System;
3 using System.Globalization;
4 
5 namespace Tgstation.Server.Host.Components.Chat
6 {
10  public sealed class Channel
11  {
15  public string Id { get; set; }
16 
21  [JsonIgnore]
22  public ulong RealId
23  {
24  get => UInt64.Parse(Id, CultureInfo.InvariantCulture);
25  set => Id = value.ToString(CultureInfo.InvariantCulture);
26  }
27 
31  public string FriendlyName { get; set; }
32 
36  public string ConnectionName { get; set; }
37 
41  public bool IsAdminChannel { get; set; }
42 
46  public bool IsPrivateChannel { get; set; }
47 
51  public string Tag { get; set; }
52  }
53 }
+Go to the documentation of this file.
1 using Newtonsoft.Json;
2 using System;
3 using System.Globalization;
4 
5 namespace Tgstation.Server.Host.Components.Chat
6 {
10  public sealed class Channel
11  {
15  public string Id { get; set; }
16 
21  [JsonIgnore]
22  public ulong RealId
23  {
24  get => UInt64.Parse(Id, CultureInfo.InvariantCulture);
25  set => Id = value.ToString(CultureInfo.InvariantCulture);
26  }
27 
31  public string FriendlyName { get; set; }
32 
36  public string ConnectionName { get; set; }
37 
41  public bool IsAdminChannel { get; set; }
42 
46  public bool IsPrivateChannel { get; set; }
47 
51  public string Tag { get; set; }
52  }
53 }
Represents a Providers.IProvider channel
Definition: Channel.cs:10
diff --git a/_chat_8cs_source.html b/_chat_8cs_source.html index b170b56794..1eb2e69db6 100644 --- a/_chat_8cs_source.html +++ b/_chat_8cs_source.html @@ -92,84 +92,84 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Chat.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host.Components.Chat
16 {
18  sealed class Chat : IChat, IRestartHandler
19  {
20  const string CommonMention = "!tgs";
21 
26 
31 
36 
41 
46 
50  readonly ILoggerFactory loggerFactory;
51 
55  readonly ILogger<Chat> logger;
56 
60  readonly Dictionary<string, ICommand> builtinCommands;
61 
65  readonly Dictionary<long, IProvider> providers;
66 
70  readonly Dictionary<ulong, ChannelMapping> mappedChannels;
71 
75  readonly List<IJsonTrackingContext> trackingContexts;
76 
80  readonly CancellationTokenSource handlerCts;
81 
85  readonly List<Models.ChatBot> initialChatBots;
86 
91 
96 
100  TaskCompletionSource<object> connectionsUpdated;
101 
106 
110  bool started;
111 
123  public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<Chat> logger, IEnumerable<Models.ChatBot> initialChatBots)
124  {
125  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
126  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
127  this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
128  if (serverControl == null)
129  throw new ArgumentNullException(nameof(serverControl));
130  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
131  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
132  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
133  this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
134 
135  restartRegistration = serverControl.RegisterForRestart(this);
136 
137  builtinCommands = new Dictionary<string, ICommand>();
138  providers = new Dictionary<long, IProvider>();
139  mappedChannels = new Dictionary<ulong, ChannelMapping>();
140  trackingContexts = new List<IJsonTrackingContext>();
141  handlerCts = new CancellationTokenSource();
142  connectionsUpdated = new TaskCompletionSource<object>();
143  channelIdCounter = 1;
144  }
145 
147  public void Dispose()
148  {
149  restartRegistration.Dispose();
150  handlerCts.Dispose();
151  foreach (var I in providers)
152  I.Value.Dispose();
153  }
154 
162  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
163  {
164  IProvider provider;
165  lock (providers)
166  if (!providers.TryGetValue(connectionId, out provider))
167  return null;
168  Task task;
169  lock (mappedChannels)
170  {
171  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
172  mappedChannels.Remove(I);
173 
174  if (updateTrackings)
175  lock (trackingContexts)
176  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
177  else
178  task = Task.CompletedTask;
179  }
180  await task.ConfigureAwait(false);
181  return provider;
182  }
183 
191  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
192  {
193  //map the channel if it's private and we haven't seen it
194  lock (providers)
195  {
196  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
197  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
198  if (message.User.Channel.IsPrivateChannel)
199  lock (mappedChannels)
200  {
201  if (!provider.Connected)
202  return;
203  if (!enumerable.Any())
204  {
205  ulong newId;
206  lock (this)
207  newId = channelIdCounter++;
208  mappedChannels.Add(newId, new ChannelMapping
209  {
210  IsWatchdogChannel = false,
211  ProviderChannelId = message.User.Channel.RealId,
212  ProviderId = providerId,
213  Channel = message.User.Channel
214  });
215  message.User.Channel.RealId = newId;
216  }
217  else
218  message.User.Channel.RealId = enumerable.First().Key;
219  }
220  else
221  {
222  //need to add tag and isAdminChannel
223  var mapping = enumerable.First().Value;
224  message.User.Channel.Id = mapping.Channel.Id;
225  message.User.Channel.Tag = mapping.Channel.Tag;
226  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
227  }
228  }
229 
230  var splits = new List<string>(message.Content.Trim().Split(' '));
231  var address = splits[0];
232  if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ','))
233  address = address.Substring(0, address.Length - 1);
234 
235  address = address.ToUpperInvariant();
236 
237  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
238 
239  if (!addressed && !message.User.Channel.IsPrivateChannel)
240  //no mention
241  return;
242 
243  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
244 
245  if (addressed)
246  splits.RemoveAt(0);
247 
248  if (splits.Count == 0)
249  {
250  //just a mention
251  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
252  return;
253  }
254 
255  var command = splits[0].ToUpperInvariant();
256  splits.RemoveAt(0);
257  var arguments = String.Join(" ", splits);
258 
259  try
260  {
261  async Task<ICommand> GetCommand(string commandName)
262  {
263  if (!builtinCommands.TryGetValue(commandName, out var handler))
264  {
265  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken));
266  await Task.WhenAll(tasks).ConfigureAwait(false);
267  handler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == commandName).FirstOrDefault();
268  }
269  return handler;
270  };
271 
272  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
273 
274  if (command == "HELP" || command == "?")
275  {
276  string helpText;
277  if (splits.Count == 0)
278  {
279  var allCommands = builtinCommands.Select(x => x.Value).ToList();
280  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)).ToList();
281  await Task.WhenAll(tasks).ConfigureAwait(false);
282  allCommands.AddRange(tasks.SelectMany(x => x.Result));
283  helpText = String.Format(CultureInfo.InvariantCulture, "Available commands (Type '?' or 'help' and then a command name for more details): {0}", String.Join(", ", allCommands.Select(x => x.Name)));
284  }
285  else
286  {
287  var helpHandler = await GetCommand(splits[0].ToUpperInvariant()).ConfigureAwait(false);
288  if (helpHandler != default)
289  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
290  else
291  helpText = UnknownCommandMessage;
292  }
293  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
294  return;
295  }
296 
297  var commandHandler = await GetCommand(command).ConfigureAwait(false);
298 
299  if (commandHandler == default)
300  {
301  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
302  return;
303  }
304 
305  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
306  {
307  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
308  return;
309  }
310 
311  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
312  if (result != null)
313  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
314  }
315  catch (OperationCanceledException)
316  {
317  throw;
318  }
319  catch (Exception e)
320  {
321  //error bc custom commands should reply about why it failed
322  logger.LogError("Error processing chat command: {0}", e);
323  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
324  }
325  }
326 
332  async Task MonitorMessages(CancellationToken cancellationToken)
333  {
334  var messageTasks = new Dictionary<IProvider, Task<Message>>();
335  try
336  {
337  while (!cancellationToken.IsCancellationRequested)
338  {
339  //prune disconnected providers
340  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
341  messageTasks.Remove(I.Key);
342 
343  //add new ones
344  Task updatedTask;
345  lock (this)
346  updatedTask = connectionsUpdated.Task;
347  lock (providers)
348  foreach (var I in providers)
349  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
350  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
351 
352  if (messageTasks.Count == 0)
353  {
354  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
355  continue;
356  }
357 
358  //wait for a message
359  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
360 
361  //process completed ones
362  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
363  {
364  var message = await I.Value.ConfigureAwait(false);
365 
366  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
367 
368  messageTasks.Remove(I.Key);
369  }
370  }
371  }
372  catch (OperationCanceledException) { }
373  catch (Exception e)
374  {
375  logger.LogError("Message monitor crashed!: Exception: {0}", e);
376  }
377  }
378 
380  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
381  {
382  if (newChannels == null)
383  throw new ArgumentNullException(nameof(newChannels));
384  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
385  if (provider == null)
386  return;
387  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
388  if (results == null) //aborted
389  return;
390  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
391  {
392  IsWatchdogChannel = x.IsWatchdogChannel == true,
393  IsUpdatesChannel = x.IsUpdatesChannel == true,
394  ProviderChannelId = y.RealId,
395  ProviderId = connectionId,
396  Channel = y
397  });
398 
399  ulong baseId;
400  lock (this)
401  {
402  baseId = channelIdCounter;
403  channelIdCounter += (ulong)results.Count;
404  }
405 
406  Task task;
407  lock (mappedChannels)
408  {
409  lock (providers)
410  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again
411  return;
412  foreach (var I in mappings)
413  {
414  var newId = baseId++;
415  mappedChannels.Add(newId, I);
416  I.Channel.RealId = newId;
417  }
418 
419  lock (trackingContexts)
420  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
421  }
422  await task.ConfigureAwait(false);
423  }
424 
426  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
427  {
428  if (newSettings == null)
429  throw new ArgumentNullException(nameof(newSettings));
430  IProvider provider;
431 
432  async Task DisconnectProvider(IProvider p)
433  {
434  try
435  {
436  await p.Disconnect(cancellationToken).ConfigureAwait(false);
437  }
438  finally
439  {
440  p.Dispose();
441  }
442  }
443 
444  Task disconnectTask;
445  lock (providers)
446  {
447  //raw settings changes forces a rebuild of the provider
448  if (providers.TryGetValue(newSettings.Id, out provider))
449  {
450  providers.Remove(newSettings.Id);
451  disconnectTask = DisconnectProvider(provider);
452  }
453  else
454  disconnectTask = Task.CompletedTask;
455  if (newSettings.Enabled.Value)
456  {
457  provider = providerFactory.CreateProvider(newSettings);
458  providers.Add(newSettings.Id, provider);
459  }
460  }
461 
462  lock (mappedChannels)
463  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
464  mappedChannels.Remove(I);
465 
466  await disconnectTask.ConfigureAwait(false);
467 
468  if (started)
469  {
470  if (newSettings.Enabled.Value)
471  await provider.Connect(cancellationToken).ConfigureAwait(false);
472  lock (this)
473  {
474  //same thread shennanigans
475  var oldOne = connectionsUpdated;
476  connectionsUpdated = new TaskCompletionSource<object>();
477  oldOne.SetResult(null);
478  }
479  }
480  }
481 
483  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
484  {
485  if (message == null)
486  throw new ArgumentNullException(nameof(message));
487  if (channelIds == null)
488  throw new ArgumentNullException(nameof(channelIds));
489 
490  return Task.WhenAll(channelIds.Select(x =>
491  {
492  ChannelMapping channelMapping;
493  lock (mappedChannels)
494  if (!mappedChannels.TryGetValue(x, out channelMapping))
495  return Task.CompletedTask;
496  IProvider provider;
497  lock (providers)
498  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
499  return Task.CompletedTask;
500  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
501  }));
502  }
503 
505  public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
506  {
507  List<ulong> wdChannels;
508  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
509  lock (mappedChannels) //so it doesn't change while we're using it
510  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
511  return SendMessage(message, wdChannels, cancellationToken);
512  }
513 
515  public Task SendUpdateMessage(string message, CancellationToken cancellationToken)
516  {
517  List<ulong> wdChannels;
518  message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message);
519  lock (mappedChannels) //so it doesn't change while we're using it
520  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
521  return SendMessage(message, wdChannels, cancellationToken);
522  }
523 
525  public async Task StartAsync(CancellationToken cancellationToken)
526  {
527  foreach (var I in commandFactory.GenerateCommands())
528  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
529  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
530  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
531  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
532  chatHandler = MonitorMessages(handlerCts.Token);
533  started = true;
534  }
535 
537  public async Task StopAsync(CancellationToken cancellationToken)
538  {
539  handlerCts.Cancel();
540  await chatHandler.ConfigureAwait(false);
541  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
542  }
543 
545  public async Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
546  {
547  if (customCommandHandler == null)
548  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
549  JsonTrackingContext context = null;
550  context = new JsonTrackingContext(ioManager, customCommandHandler, loggerFactory.CreateLogger<JsonTrackingContext>(), () =>
551  {
552  lock (trackingContexts)
553  trackingContexts.Remove(context);
554  }, ioManager.ConcatPath(basePath, commandsJsonName), ioManager.ConcatPath(basePath, channelsJsonName));
555  Task task;
556  lock (trackingContexts)
557  {
558  trackingContexts.Add(context);
559  lock (mappedChannels)
560  task = context.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken);
561  }
562  await task.ConfigureAwait(false);
563  return context;
564  }
565 
567  public bool Connected(long connectionId)
568  {
569  lock (providers)
570  return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
571  }
572 
574  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
575  {
576  if (this.customCommandHandler != null)
577  throw new InvalidOperationException("RegisterCommandHandler() already called!");
578  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
579  }
580 
582  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
583  {
584  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
585  if (provider != null)
586  try
587  {
588  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
589  }
590  finally
591  {
592  provider.Dispose();
593  }
594  }
595 
597  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
598  {
599  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
600  List<ulong> wdChannels;
601  lock (mappedChannels) //so it doesn't change while we're using it
602  wdChannels = mappedChannels.Select(x => x.Key).ToList();
603  return SendMessage(message, wdChannels, cancellationToken);
604  }
605  }
606 }
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: Chat.cs:574
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host.Components.Chat
16 {
18  // TODO: Decomplexify
19  #pragma warning disable CA1506
20  sealed class Chat : IChat, IRestartHandler
21  {
22  const string CommonMention = "!tgs";
23 
28 
33 
38 
43 
48 
52  readonly ILoggerFactory loggerFactory;
53 
57  readonly ILogger<Chat> logger;
58 
62  readonly Dictionary<string, ICommand> builtinCommands;
63 
67  readonly Dictionary<long, IProvider> providers;
68 
72  readonly Dictionary<ulong, ChannelMapping> mappedChannels;
73 
77  readonly List<IJsonTrackingContext> trackingContexts;
78 
82  readonly CancellationTokenSource handlerCts;
83 
87  readonly List<Models.ChatBot> initialChatBots;
88 
93 
98 
102  TaskCompletionSource<object> connectionsUpdated;
103 
108 
112  bool started;
113 
125  public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<Chat> logger, IEnumerable<Models.ChatBot> initialChatBots)
126  {
127  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
128  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
129  this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
130  if (serverControl == null)
131  throw new ArgumentNullException(nameof(serverControl));
132  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
133  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
134  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
135  this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
136 
137  restartRegistration = serverControl.RegisterForRestart(this);
138 
139  builtinCommands = new Dictionary<string, ICommand>();
140  providers = new Dictionary<long, IProvider>();
141  mappedChannels = new Dictionary<ulong, ChannelMapping>();
142  trackingContexts = new List<IJsonTrackingContext>();
143  handlerCts = new CancellationTokenSource();
144  connectionsUpdated = new TaskCompletionSource<object>();
145  channelIdCounter = 1;
146  }
147 
149  public void Dispose()
150  {
151  restartRegistration.Dispose();
152  handlerCts.Dispose();
153  foreach (var I in providers)
154  I.Value.Dispose();
155  }
156 
164  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
165  {
166  IProvider provider;
167  lock (providers)
168  if (!providers.TryGetValue(connectionId, out provider))
169  return null;
170  Task task;
171  lock (mappedChannels)
172  {
173  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
174  mappedChannels.Remove(I);
175 
176  if (updateTrackings)
177  lock (trackingContexts)
178  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
179  else
180  task = Task.CompletedTask;
181  }
182 
183  await task.ConfigureAwait(false);
184  return provider;
185  }
186 
194  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
195  {
196  // map the channel if it's private and we haven't seen it
197  lock (providers)
198  {
199  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
200  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
201  if (message.User.Channel.IsPrivateChannel)
202  lock (mappedChannels)
203  {
204  if (!provider.Connected)
205  return;
206  if (!enumerable.Any())
207  {
208  ulong newId;
209  lock (this)
210  newId = channelIdCounter++;
211  mappedChannels.Add(newId, new ChannelMapping
212  {
213  IsWatchdogChannel = false,
214  ProviderChannelId = message.User.Channel.RealId,
215  ProviderId = providerId,
216  Channel = message.User.Channel
217  });
218  message.User.Channel.RealId = newId;
219  }
220  else
221  message.User.Channel.RealId = enumerable.First().Key;
222  }
223  else
224  {
225  // need to add tag and isAdminChannel
226  var mapping = enumerable.First().Value;
227  message.User.Channel.Id = mapping.Channel.Id;
228  message.User.Channel.Tag = mapping.Channel.Tag;
229  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
230  }
231  }
232 
233  var splits = new List<string>(message.Content.Trim().Split(' '));
234  var address = splits[0];
235  if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ','))
236  address = address.Substring(0, address.Length - 1);
237 
238  address = address.ToUpperInvariant();
239 
240  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
241 
242  // no mention
243  if (!addressed && !message.User.Channel.IsPrivateChannel)
244  return;
245 
246  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
247 
248  if (addressed)
249  splits.RemoveAt(0);
250 
251  if (splits.Count == 0)
252  {
253  // just a mention
254  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
255  return;
256  }
257 
258  var command = splits[0].ToUpperInvariant();
259  splits.RemoveAt(0);
260  var arguments = String.Join(" ", splits);
261 
262  try
263  {
264  async Task<ICommand> GetCommand(string commandName)
265  {
266  if (!builtinCommands.TryGetValue(commandName, out var handler))
267  {
268  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken));
269  await Task.WhenAll(tasks).ConfigureAwait(false);
270  handler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == commandName).FirstOrDefault();
271  }
272 
273  return handler;
274  }
275 
276  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
277 
278  if (command == "HELP" || command == "?")
279  {
280  string helpText;
281  if (splits.Count == 0)
282  {
283  var allCommands = builtinCommands.Select(x => x.Value).ToList();
284  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)).ToList();
285  await Task.WhenAll(tasks).ConfigureAwait(false);
286  allCommands.AddRange(tasks.SelectMany(x => x.Result));
287  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)));
288  }
289  else
290  {
291  var helpHandler = await GetCommand(splits[0].ToUpperInvariant()).ConfigureAwait(false);
292  if (helpHandler != default)
293  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
294  else
295  helpText = UnknownCommandMessage;
296  }
297 
298  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
299  return;
300  }
301 
302  var commandHandler = await GetCommand(command).ConfigureAwait(false);
303 
304  if (commandHandler == default)
305  {
306  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
307  return;
308  }
309 
310  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
311  {
312  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
313  return;
314  }
315 
316  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
317  if (result != null)
318  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
319  }
320  catch (OperationCanceledException)
321  {
322  throw;
323  }
324  catch (Exception e)
325  {
326  // error bc custom commands should reply about why it failed
327  logger.LogError("Error processing chat command: {0}", e);
328  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
329  }
330  }
331 
337  async Task MonitorMessages(CancellationToken cancellationToken)
338  {
339  var messageTasks = new Dictionary<IProvider, Task<Message>>();
340  try
341  {
342  while (!cancellationToken.IsCancellationRequested)
343  {
344  // prune disconnected providers
345  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
346  messageTasks.Remove(I.Key);
347 
348  // add new ones
349  Task updatedTask;
350  lock (this)
351  updatedTask = connectionsUpdated.Task;
352  lock (providers)
353  foreach (var I in providers)
354  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
355  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
356 
357  if (messageTasks.Count == 0)
358  {
359  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
360  continue;
361  }
362 
363  // wait for a message
364  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
365 
366  // process completed ones
367  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
368  {
369  var message = await I.Value.ConfigureAwait(false);
370 
371  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
372 
373  messageTasks.Remove(I.Key);
374  }
375  }
376  }
377  catch (OperationCanceledException) { }
378  catch (Exception e)
379  {
380  logger.LogError("Message monitor crashed!: Exception: {0}", e);
381  }
382  }
383 
385  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
386  {
387  if (newChannels == null)
388  throw new ArgumentNullException(nameof(newChannels));
389  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
390  if (provider == null)
391  return;
392  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
393  if (results == null) // aborted
394  return;
395  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
396  {
397  IsWatchdogChannel = x.IsWatchdogChannel == true,
398  IsUpdatesChannel = x.IsUpdatesChannel == true,
399  ProviderChannelId = y.RealId,
400  ProviderId = connectionId,
401  Channel = y
402  });
403 
404  ulong baseId;
405  lock (this)
406  {
407  baseId = channelIdCounter;
408  channelIdCounter += (ulong)results.Count;
409  }
410 
411  Task task;
412  lock (mappedChannels)
413  {
414  lock (providers)
415  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) // aborted again
416  return;
417  foreach (var I in mappings)
418  {
419  var newId = baseId++;
420  mappedChannels.Add(newId, I);
421  I.Channel.RealId = newId;
422  }
423 
424  lock (trackingContexts)
425  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
426  }
427 
428  await task.ConfigureAwait(false);
429  }
430 
432  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
433  {
434  if (newSettings == null)
435  throw new ArgumentNullException(nameof(newSettings));
436  IProvider provider;
437 
438  async Task DisconnectProvider(IProvider p)
439  {
440  try
441  {
442  await p.Disconnect(cancellationToken).ConfigureAwait(false);
443  }
444  finally
445  {
446  p.Dispose();
447  }
448  }
449 
450  Task disconnectTask;
451  lock (providers)
452  {
453  // raw settings changes forces a rebuild of the provider
454  if (providers.TryGetValue(newSettings.Id, out provider))
455  {
456  providers.Remove(newSettings.Id);
457  disconnectTask = DisconnectProvider(provider);
458  }
459  else
460  disconnectTask = Task.CompletedTask;
461  if (newSettings.Enabled.Value)
462  {
463  provider = providerFactory.CreateProvider(newSettings);
464  providers.Add(newSettings.Id, provider);
465  }
466  }
467 
468  lock (mappedChannels)
469  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
470  mappedChannels.Remove(I);
471 
472  await disconnectTask.ConfigureAwait(false);
473 
474  if (started)
475  {
476  if (newSettings.Enabled.Value)
477  await provider.Connect(cancellationToken).ConfigureAwait(false);
478  lock (this)
479  {
480  // same thread shennanigans
481  var oldOne = connectionsUpdated;
482  connectionsUpdated = new TaskCompletionSource<object>();
483  oldOne.SetResult(null);
484  }
485  }
486  }
487 
489  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
490  {
491  if (message == null)
492  throw new ArgumentNullException(nameof(message));
493  if (channelIds == null)
494  throw new ArgumentNullException(nameof(channelIds));
495 
496  return Task.WhenAll(channelIds.Select(x =>
497  {
498  ChannelMapping channelMapping;
499  lock (mappedChannels)
500  if (!mappedChannels.TryGetValue(x, out channelMapping))
501  return Task.CompletedTask;
502  IProvider provider;
503  lock (providers)
504  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
505  return Task.CompletedTask;
506  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
507  }));
508  }
509 
511  public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
512  {
513  List<ulong> wdChannels;
514  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
515  lock (mappedChannels) // so it doesn't change while we're using it
516  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
517  return SendMessage(message, wdChannels, cancellationToken);
518  }
519 
521  public Task SendUpdateMessage(string message, CancellationToken cancellationToken)
522  {
523  List<ulong> wdChannels;
524  message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message);
525  lock (mappedChannels) // so it doesn't change while we're using it
526  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
527  return SendMessage(message, wdChannels, cancellationToken);
528  }
529 
531  public async Task StartAsync(CancellationToken cancellationToken)
532  {
533  foreach (var I in commandFactory.GenerateCommands())
534  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
535  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
536  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
537  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
538  chatHandler = MonitorMessages(handlerCts.Token);
539  started = true;
540  }
541 
543  public async Task StopAsync(CancellationToken cancellationToken)
544  {
545  handlerCts.Cancel();
546  await chatHandler.ConfigureAwait(false);
547  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
548  }
549 
551  public async Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
552  {
553  if (customCommandHandler == null)
554  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
555  JsonTrackingContext context = null;
556  context = new JsonTrackingContext(
557  ioManager,
558  customCommandHandler,
559  loggerFactory.CreateLogger<JsonTrackingContext>(),
560  () =>
561  {
562  lock (trackingContexts)
563  trackingContexts.Remove(context);
564  },
565  ioManager.ConcatPath(basePath, commandsJsonName),
566  ioManager.ConcatPath(basePath, channelsJsonName));
567  Task task;
568  lock (trackingContexts)
569  {
570  trackingContexts.Add(context);
571  lock (mappedChannels)
572  task = context.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken);
573  }
574 
575  await task.ConfigureAwait(false);
576  return context;
577  }
578 
580  public bool Connected(long connectionId)
581  {
582  lock (providers)
583  return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
584  }
585 
587  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
588  {
589  if (this.customCommandHandler != null)
590  throw new InvalidOperationException("RegisterCommandHandler() already called!");
591  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
592  }
593 
595  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
596  {
597  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
598  if (provider != null)
599  try
600  {
601  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
602  }
603  finally
604  {
605  provider.Dispose();
606  }
607  }
608 
610  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
611  {
612  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
613  List<ulong> wdChannels;
614  lock (mappedChannels) // so it doesn't change while we're using it
615  wdChannels = mappedChannels.Select(x => x.Key).ToList();
616  return SendMessage(message, wdChannels, cancellationToken);
617  }
618  }
619 }
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: Chat.cs:587
bool IsWatchdogChannel
If Channel is a watchdog channel
Represents a mapping of a Channel.RealId
User User
The Components.Chat.User who sent the Message
Definition: Message.cs:16
-
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the Chat
Definition: Chat.cs:50
+
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the Chat
Definition: Chat.cs:52
- -
async Task SetChannels(IEnumerable< Channel > channels, CancellationToken cancellationToken)
Writes information about connected channels to the IJsonTrackingContext
- + +
async Task SetChannels(IEnumerable< Channel > channels, CancellationToken cancellationToken)
Writes information about connected channels to the IJsonTrackingContext
+
Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
Channel Channel
The Components.Chat.Channel the user spoke from
Definition: User.cs:40
-
Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
Definition: Chat.cs:505
-
bool started
If StartAsync(CancellationToken) has been called
Definition: Chat.cs:110
-
readonly Dictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBot.Id
Definition: Chat.cs:65
+
Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
Definition: Chat.cs:511
+
bool started
If StartAsync(CancellationToken) has been called
Definition: Chat.cs:112
+
readonly Dictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBot.Id
Definition: Chat.cs:67
Represents a message recieved by a IProvider
Definition: Message.cs:6
-
readonly List< Models.ChatBot > initialChatBots
The initial Models.ChatBot for the Chat
Definition: Chat.cs:85
-
Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger< Chat > logger, IEnumerable< Models.ChatBot > initialChatBots)
Construct a Chat
Definition: Chat.cs:123
+
readonly List< Models.ChatBot > initialChatBots
The initial Models.ChatBot for the Chat
Definition: Chat.cs:87
+
Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger< Chat > logger, IEnumerable< Models.ChatBot > initialChatBots)
Construct a Chat
Definition: Chat.cs:125
For interacting with a chat service
Definition: IProvider.cs:11
Manage the server chat bots
Definition: ChatBot.cs:10
-
bool Connected(long connectionId)
If a given set of ChatBot is connected
Definition: Chat.cs:567
+
bool Connected(long connectionId)
If a given set of ChatBot is connected
Definition: Chat.cs:580
For managing connected chat services
Definition: IChat.cs:13
-
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection
Definition: Chat.cs:582
+
async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
Disconnects and deletes a given connection
Definition: Chat.cs:595
-
async Task< IProvider > RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
Remove a IProvider from providers and mappedChannels optionally updating the trackingContexts as well...
Definition: Chat.cs:162
+
async Task< IProvider > RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
Remove a IProvider from providers and mappedChannels optionally updating the trackingContexts as well...
Definition: Chat.cs:164
-
readonly IProviderFactory providerFactory
The IProviderFactory for the Chat
Definition: Chat.cs:25
+
readonly IProviderFactory providerFactory
The IProviderFactory for the Chat
Definition: Chat.cs:27
bool IsPrivateChannel
If this is a 1-to-1 chat channel
Definition: Channel.cs:46
-
TaskCompletionSource< object > connectionsUpdated
The TaskCompletionSource<TResult> that completes when ChatBots change
Definition: Chat.cs:100
-
readonly ILogger< Chat > logger
The ILogger for the Chat
Definition: Chat.cs:55
+
TaskCompletionSource< object > connectionsUpdated
The TaskCompletionSource<TResult> that completes when ChatBots change
Definition: Chat.cs:102
+
readonly ILogger< Chat > logger
The ILogger for the Chat
Definition: Chat.cs:57
-
readonly ICommandFactory commandFactory
The ICommandFactory for the Chat
Definition: Chat.cs:35
-
Task SendUpdateMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured update channels
Definition: Chat.cs:515
+
readonly ICommandFactory commandFactory
The ICommandFactory for the Chat
Definition: Chat.cs:37
+
Task SendUpdateMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured update channels
Definition: Chat.cs:521
-
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
Processes a message
Definition: Chat.cs:191
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: Chat.cs:537
+
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
Processes a message
Definition: Chat.cs:194
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: Chat.cs:543
Represents a Providers.IProvider channel
Definition: Channel.cs:10
string Id
Backing field for RealId. Represented as a string to avoid BYOND percision loss
Definition: Channel.cs:15
-
async Task MonitorMessages(CancellationToken cancellationToken)
Monitors active providers for new Messages
Definition: Chat.cs:332
+
async Task MonitorMessages(CancellationToken cancellationToken)
Monitors active providers for new Messages
Definition: Chat.cs:337
-
readonly IIOManager ioManager
The IIOManager for the Chat
Definition: Chat.cs:30
-
readonly Dictionary< ulong, ChannelMapping > mappedChannels
Map of Channel.RealIds to ChannelMappings
Definition: Chat.cs:70
+
readonly IIOManager ioManager
The IIOManager for the Chat
Definition: Chat.cs:32
+
readonly Dictionary< ulong, ChannelMapping > mappedChannels
Map of Channel.RealIds to ChannelMappings
Definition: Chat.cs:72
string BotMention
The string that indicates the IProvider was mentioned
Definition: IProvider.cs:21
-
ulong channelIdCounter
Used for remapping Channel.RealIds
Definition: Chat.cs:105
+
ulong channelIdCounter
Used for remapping Channel.RealIds
Definition: Chat.cs:107
ulong RealId
The Providers.IProvider channel Id.
Definition: Channel.cs:23
-
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: Chat.cs:426
-
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the Chat
Definition: Chat.cs:45
+
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: Chat.cs:432
+
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the Chat
Definition: Chat.cs:47
Handles Commands.ICommands that map to those defined in a IJsonTrackingContext
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Chat.cs:525
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Chat.cs:531
-
Task chatHandler
The Task that monitors incoming chat messages
Definition: Chat.cs:95
-
readonly Dictionary< string, ICommand > builtinCommands
Unchanging ICommands in the Chat mapped by ICommand.Name
Definition: Chat.cs:60
-
async Task< IJsonTrackingContext > TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
Start tracking json files for commands and channels
Definition: Chat.cs:545
+
Task chatHandler
The Task that monitors incoming chat messages
Definition: Chat.cs:97
+
readonly Dictionary< string, ICommand > builtinCommands
Unchanging ICommands in the Chat mapped by ICommand.Name
Definition: Chat.cs:62
+
async Task< IJsonTrackingContext > TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
Start tracking json files for commands and channels
Definition: Chat.cs:551
Interface for using filesystems
Definition: IIOManager.cs:11
-
Task SendMessage(string message, IEnumerable< ulong > channelIds, CancellationToken cancellationToken)
Send a chat message to a given set of channelIds
Definition: Chat.cs:483
+
Task SendMessage(string message, IEnumerable< ulong > channelIds, CancellationToken cancellationToken)
Send a chat message to a given set of channelIds
Definition: Chat.cs:489
Represents the lifetime of a IRestartHandler registration
-
readonly List< IJsonTrackingContext > trackingContexts
The active IJsonTrackingContexts for the Chat
Definition: Chat.cs:75
+
readonly List< IJsonTrackingContext > trackingContexts
The active IJsonTrackingContexts for the Chat
Definition: Chat.cs:77
bool IsAdminChannel
If this is considered a channel for admin commands
Definition: Channel.cs:41
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
bool Enabled
If the connection is enabled
Definition: ChatBot.cs:26
-
async Task ChangeChannels(long connectionId, IEnumerable< Api.Models.ChatChannel > newChannels, CancellationToken cancellationToken)
Change chat channels
Definition: Chat.cs:380
-
ICustomCommandHandler customCommandHandler
The ICustomCommandHandler for the ChangeChannels(long, IEnumerable<Api.Models.ChatChannel>, CancellationToken)
Definition: Chat.cs:90
-
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: Chat.cs:597
-
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the Chat
Definition: Chat.cs:40
-
readonly CancellationTokenSource handlerCts
The CancellationTokenSource for chatHandler
Definition: Chat.cs:80
+
async Task ChangeChannels(long connectionId, IEnumerable< Api.Models.ChatChannel > newChannels, CancellationToken cancellationToken)
Change chat channels
Definition: Chat.cs:385
+
ICustomCommandHandler customCommandHandler
The ICustomCommandHandler for the ChangeChannels(long, IEnumerable<Api.Models.ChatChannel>, CancellationToken)
Definition: Chat.cs:92
+
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: Chat.cs:610
+
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the Chat
Definition: Chat.cs:42
+
readonly CancellationTokenSource handlerCts
The CancellationTokenSource for chatHandler
Definition: Chat.cs:82
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_chat_bot_rights_8cs_source.html b/_chat_bot_rights_8cs_source.html index 62ea8fde09..c63b272aa9 100644 --- a/_chat_bot_rights_8cs_source.html +++ b/_chat_bot_rights_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ChatBotRights.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum ChatBotRights : ulong
10  {
14  None = 0,
18  WriteEnabled = 1,
22  WriteProvider = 2,
26  WriteChannels = 4,
38  Read = 32,
42  Create = 64,
46  Delete = 128,
50  WriteName = 256,
51  }
52 }
User can read Models.Internal.ChatBot.ConnectionString requires Read
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum ChatBotRights : ulong
10  {
14  None = 0,
15 
19  WriteEnabled = 1,
20 
24  WriteProvider = 2,
25 
29  WriteChannels = 4,
30 
35 
40 
44  Read = 32,
45 
49  Create = 64,
50 
54  Delete = 128,
55 
59  WriteName = 256,
60  }
61 }
User can read Models.Internal.ChatBot.ConnectionString requires Read
User can change Models.Internal.ChatBot.Name
User can change Models.Internal.ChatBot.Provider
diff --git a/_chat_bots_client_8cs_source.html b/_chat_bots_client_8cs_source.html index 4d458b2e7f..e85f4de402 100644 --- a/_chat_bots_client_8cs_source.html +++ b/_chat_bots_client_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ChatBotsClient.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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
12  {
20  readonly Instance instance;
21 
27  public ChatBotsClient(IApiClient apiClient, Instance instance)
28  {
29  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
30  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
31  }
32 
34  public Task<ChatBot> Create(ChatBot settings, CancellationToken cancellationToken) => apiClient.Create<ChatBot, ChatBot>(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
35 
37  public Task Delete(ChatBot settings, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken);
38 
40  public Task<IReadOnlyList<ChatBot>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ChatBot>>(Routes.List(Routes.Chat), instance.Id, cancellationToken);
41 
43  public Task<ChatBot> Update(ChatBot settings, CancellationToken cancellationToken) => apiClient.Update<ChatBot, ChatBot>(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
44 
46  public Task<ChatBot> GetId(ChatBot settings, CancellationToken cancellationToken) => apiClient.Read<ChatBot>(Routes.SetID(Routes.Chat, (settings ?? throw new ArgumentNullException(nameof(settings))).Id), instance.Id, cancellationToken);
47  }
48 }
+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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
12  {
17 
21  readonly Instance instance;
22 
28  public ChatBotsClient(IApiClient apiClient, Instance instance)
29  {
30  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
31  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
32  }
33 
35  public Task<ChatBot> Create(ChatBot settings, CancellationToken cancellationToken) => apiClient.Create<ChatBot, ChatBot>(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
36 
38  public Task Delete(ChatBot settings, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken);
39 
41  public Task<IReadOnlyList<ChatBot>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ChatBot>>(Routes.List(Routes.Chat), instance.Id, cancellationToken);
42 
44  public Task<ChatBot> Update(ChatBot settings, CancellationToken cancellationToken) => apiClient.Update<ChatBot, ChatBot>(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken);
45 
47  public Task<ChatBot> GetId(ChatBot settings, CancellationToken cancellationToken) => apiClient.Read<ChatBot>(Routes.SetID(Routes.Chat, (settings ?? throw new ArgumentNullException(nameof(settings))).Id), instance.Id, cancellationToken);
48  }
49 }
@@ -106,10 +106,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
static string SetID(string route, long id)
Apply an id postfix to a route
static string List(string route)
Get the /List postfix for a route
-
readonly Instance instance
The Instance for the ChatBotsClient
+
readonly Instance instance
The Instance for the ChatBotsClient
Web interface for the API
Definition: IApiClient.cs:11
readonly IApiClient apiClient
The IApiClient for the ChatBotsClient
-
ChatBotsClient(IApiClient apiClient, Instance instance)
Construct a ChatBotsClient
+
ChatBotsClient(IApiClient apiClient, Instance instance)
Construct a ChatBotsClient
diff --git a/_chat_command_8cs.html b/_chat_command_8cs.html index cfffb54bab..758eab356b 100644 --- a/_chat_command_8cs.html +++ b/_chat_command_8cs.html @@ -101,6 +101,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

Classes

class  Tgstation.Server.Host.Components.Interop.ChatCommand
 Represents a chat command to be handled by DD More...
 
+

diff --git a/_chat_command_8cs_source.html b/_chat_command_8cs_source.html index 4fda24028f..045a2d3d06 100644 --- a/_chat_command_8cs_source.html +++ b/_chat_command_8cs_source.html @@ -92,8 +92,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ChatCommand.cs
-Go to the documentation of this file.
2 
3 namespace Tgstation.Server.Host.Components.Interop
4 {
5  sealed class ChatCommand
6  {
7  public string Command { get; set; }
8  public string Params { get; set; }
9  public User User { get; set; }
10  }
11 }
- +Go to the documentation of this file.
2 
3 namespace Tgstation.Server.Host.Components.Interop
4 {
8  sealed class ChatCommand
9  {
13  public string Command { get; set; }
14 
18  public string Params { get; set; }
19 
23  public User User { get; set; }
24  }
25 }
+
Represents a chat command to be handled by DD
Definition: ChatCommand.cs:8
Represents a tgs_chat_user datum
Definition: User.cs:10
diff --git a/_chat_controller_8cs_source.html b/_chat_controller_8cs_source.html index 4a2838d659..2d48262897 100644 --- a/_chat_controller_8cs_source.html +++ b/_chat_controller_8cs_source.html @@ -92,35 +92,35 @@ 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;
18 using Z.EntityFramework.Plus;
19 
20 namespace Tgstation.Server.Host.Controllers
21 {
25  [Route(Routes.Chat)]
26  public sealed class ChatController : ModelController<Api.Models.ChatBot>
27  {
32 
40  public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
41  {
42  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
43  }
44 
50  static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api) => new Models.ChatChannel
51  {
52  DiscordChannelId = api.DiscordChannelId,
53  IrcChannel = api.IrcChannel,
54  IsAdminChannel = api.IsAdminChannel ?? false,
55  IsWatchdogChannel = api.IsWatchdogChannel ?? false,
56  IsUpdatesChannel = api.IsUpdatesChannel ?? false,
57  Tag = api.Tag
58  };
59 
61  [TgsAuthorize(ChatBotRights.Create)]
62  public override async Task<IActionResult> Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
63  {
64  if (model == null)
65  throw new ArgumentNullException(nameof(model));
66 
67  if (String.IsNullOrWhiteSpace(model.Name))
68  return BadRequest(new ErrorMessage { Message = "name cannot be null or whitespace!" });
69 
70  if (String.IsNullOrWhiteSpace(model.ConnectionString))
71  return BadRequest(new ErrorMessage { Message = "connection_string cannot be null or whitespace!" });
72 
73  if (!model.Provider.HasValue)
74  return BadRequest(new ErrorMessage { Message = "provider cannot be null!" });
75 
76  switch (model.Provider)
77  {
78  case ChatProvider.Discord:
79  case ChatProvider.Irc:
80  break;
81  default:
82  return BadRequest(new ErrorMessage { Message = "Invalid provider!" });
83  }
84 
85  if (!model.ValidateProviderChannelTypes())
86  return BadRequest(new ErrorMessage { Message = "One or more of channels aren't formatted correctly for the given provider!" });
87 
88  model.Enabled = model.Enabled ?? false;
89 
90  //try to update das db first
91  var dbModel = new Models.ChatBot
92  {
93  Name = model.Name,
94  ConnectionString = model.ConnectionString,
95  Enabled = model.Enabled,
96  Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).ToList() ?? new List<Models.ChatChannel>(), //important that this isn't null
97  InstanceId = Instance.Id,
98  Provider = model.Provider,
99  };
100 
101  DatabaseContext.ChatBots.Add(dbModel);
102 
103  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
104 
105  try
106  {
107  try
108  {
109  //try to create it
110  var instance = instanceManager.GetInstance(Instance);
111  await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false);
112 
113  if (dbModel.Channels.Count > 0)
114  await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false);
115  }
116  catch
117  {
118  //undo the add
119  DatabaseContext.ChatBots.Remove(dbModel);
120  await DatabaseContext.Save(default).ConfigureAwait(false);
121  throw;
122  }
123  }
124  catch (InvalidOperationException e)
125  {
126  return BadRequest(new ErrorMessage { Message = e.Message });
127  }
128  return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
129  }
130 
132  [TgsAuthorize(ChatBotRights.Delete)]
133  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
134  {
135  var instance = instanceManager.GetInstance(Instance);
136  await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatBots.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false);
137 
138  return Ok();
139  }
140 
142  [TgsAuthorize(ChatBotRights.Read)]
143  public override async Task<IActionResult> List(CancellationToken cancellationToken)
144  {
145  var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels);
146 
147  var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
148 
149  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
150 
151  if (!connectionStrings)
152  foreach (var I in results)
153  I.ConnectionString = null;
154 
155  return Json(results.Select(x => x.ToApi()));
156  }
157 
159  [TgsAuthorize(ChatBotRights.Read)]
160  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
161  {
162  var query = DatabaseContext.ChatBots.Where(x => x.Id == id).Include(x => x.Channels);
163 
164  var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
165  if (results == default)
166  return StatusCode((int)HttpStatusCode.Gone);
167 
168  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
169 
170  if (!connectionStrings)
171  results.ConnectionString = null;
172 
173  return Json(results.ToApi());
174  }
175 
177  [TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)]
178  public override async Task<IActionResult> Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
179  {
180  if (model == null)
181  throw new ArgumentNullException(nameof(model));
182 
183  if (model.Provider.HasValue && !model.ValidateProviderChannelTypes())
184  return BadRequest(new ErrorMessage { Message = "One or more of channels aren't formatted correctly for the given provider!" });
185 
186  var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id && x.Id == model.Id).Include(x => x.Channels);
187 
188  var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
189 
190  if (current == default)
191  return StatusCode((int)HttpStatusCode.Gone);
192 
193  var userRights = (ChatBotRights)AuthenticationContext.GetRight(RightsType.ChatBots);
194 
195  bool anySettingsModified = false;
196 
197  bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatBot, T>> expression, ChatBotRights requiredRight)
198  {
199  var memberSelectorExpression = (MemberExpression)expression.Body;
200  var property = (PropertyInfo)memberSelectorExpression.Member;
201 
202  var newVal = property.GetValue(model);
203  if (newVal == null)
204  return false;
205  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
206  return true;
207 
208  property.SetValue(current, newVal);
209  anySettingsModified = true;
210  return false;
211  };
212 
213  var oldProvider = current.Provider;
214 
215  if (CheckModified(x => x.ConnectionString, ChatBotRights.WriteConnectionString)
216  || CheckModified(x => x.Enabled, ChatBotRights.WriteEnabled)
217  || CheckModified(x => x.Name, ChatBotRights.WriteName)
218  || CheckModified(x => x.Provider, ChatBotRights.WriteProvider)
219  || (model.Channels != null && !userRights.HasFlag(ChatBotRights.WriteChannels)))
220  return Forbid();
221 
222  var hasChannels = model.Channels != null;
223  if (hasChannels || (model.Provider.HasValue && model.Provider != oldProvider))
224  {
225  DatabaseContext.ChatChannels.RemoveRange(current.Channels);
226  if (hasChannels)
227  {
228  var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList();
229  DatabaseContext.ChatChannels.AddRange(dbChannels);
230  current.Channels = dbChannels;
231  }
232  else
233  current.Channels.Clear();
234  }
235 
236  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
237 
238  var chat = instanceManager.GetInstance(Instance).Chat;
239 
240  if (anySettingsModified)
241  //have to rebuild the thing first
242  await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false);
243 
244  if (model.Channels != null || anySettingsModified)
245  await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false);
246 
247  if (userRights.HasFlag(ChatBotRights.Read))
248  {
249  if (!userRights.HasFlag(ChatBotRights.ReadConnectionString))
250  current.ConnectionString = null;
251  return Json(current.ToApi());
252  }
253  return Ok();
254  }
255  }
256 }
+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;
18 using Z.EntityFramework.Plus;
19 
20 namespace Tgstation.Server.Host.Controllers
21 {
25  [Route(Routes.Chat)]
26  public sealed class ChatController : ModelController<Api.Models.ChatBot>
27  {
32 
40  public ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger<ChatController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
41  {
42  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
43  }
44 
50  static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api) => new Models.ChatChannel
51  {
52  DiscordChannelId = api.DiscordChannelId,
53  IrcChannel = api.IrcChannel,
54  IsAdminChannel = api.IsAdminChannel ?? false,
55  IsWatchdogChannel = api.IsWatchdogChannel ?? false,
56  IsUpdatesChannel = api.IsUpdatesChannel ?? false,
57  Tag = api.Tag
58  };
59 
61  [TgsAuthorize(ChatBotRights.Create)]
62  public override async Task<IActionResult> Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
63  {
64  if (model == null)
65  throw new ArgumentNullException(nameof(model));
66 
67  if (String.IsNullOrWhiteSpace(model.Name))
68  return BadRequest(new ErrorMessage { Message = "name cannot be null or whitespace!" });
69 
70  if (String.IsNullOrWhiteSpace(model.ConnectionString))
71  return BadRequest(new ErrorMessage { Message = "connection_string cannot be null or whitespace!" });
72 
73  if (!model.Provider.HasValue)
74  return BadRequest(new ErrorMessage { Message = "provider cannot be null!" });
75 
76  switch (model.Provider)
77  {
78  case ChatProvider.Discord:
79  case ChatProvider.Irc:
80  break;
81  default:
82  return BadRequest(new ErrorMessage { Message = "Invalid provider!" });
83  }
84 
85  if (!model.ValidateProviderChannelTypes())
86  return BadRequest(new ErrorMessage { Message = "One or more of channels aren't formatted correctly for the given provider!" });
87 
88  model.Enabled = model.Enabled ?? false;
89 
90  // try to update das db first
91  var dbModel = new Models.ChatBot
92  {
93  Name = model.Name,
94  ConnectionString = model.ConnectionString,
95  Enabled = model.Enabled,
96  Channels = model.Channels?.Select(x => ConvertApiChatChannel(x)).ToList() ?? new List<Models.ChatChannel>(), // important that this isn't null
97  InstanceId = Instance.Id,
98  Provider = model.Provider,
99  };
100 
101  DatabaseContext.ChatBots.Add(dbModel);
102 
103  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
104 
105  try
106  {
107  try
108  {
109  // try to create it
110  var instance = instanceManager.GetInstance(Instance);
111  await instance.Chat.ChangeSettings(dbModel, cancellationToken).ConfigureAwait(false);
112 
113  if (dbModel.Channels.Count > 0)
114  await instance.Chat.ChangeChannels(dbModel.Id, dbModel.Channels, cancellationToken).ConfigureAwait(false);
115  }
116  catch
117  {
118  // undo the add
119  DatabaseContext.ChatBots.Remove(dbModel);
120  await DatabaseContext.Save(default).ConfigureAwait(false);
121  throw;
122  }
123  }
124  catch (InvalidOperationException e)
125  {
126  return BadRequest(new ErrorMessage { Message = e.Message });
127  }
128 
129  return StatusCode((int)HttpStatusCode.Created, dbModel.ToApi());
130  }
131 
133  [TgsAuthorize(ChatBotRights.Delete)]
134  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
135  {
136  var instance = instanceManager.GetInstance(Instance);
137  await Task.WhenAll(instance.Chat.DeleteConnection(id, cancellationToken), DatabaseContext.ChatBots.Where(x => x.Id == id).DeleteAsync(cancellationToken)).ConfigureAwait(false);
138 
139  return Ok();
140  }
141 
143  [TgsAuthorize(ChatBotRights.Read)]
144  public override async Task<IActionResult> List(CancellationToken cancellationToken)
145  {
146  var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id).Include(x => x.Channels);
147 
148  var results = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
149 
150  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
151 
152  if (!connectionStrings)
153  foreach (var I in results)
154  I.ConnectionString = null;
155 
156  return Json(results.Select(x => x.ToApi()));
157  }
158 
160  [TgsAuthorize(ChatBotRights.Read)]
161  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
162  {
163  var query = DatabaseContext.ChatBots.Where(x => x.Id == id).Include(x => x.Channels);
164 
165  var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
166  if (results == default)
167  return StatusCode((int)HttpStatusCode.Gone);
168 
169  var connectionStrings = (AuthenticationContext.GetRight(RightsType.ChatBots) & (ulong)ChatBotRights.ReadConnectionString) != 0;
170 
171  if (!connectionStrings)
172  results.ConnectionString = null;
173 
174  return Json(results.ToApi());
175  }
176 
178  #pragma warning disable CA1506 // TODO: Decomplexify
179  [TgsAuthorize(ChatBotRights.WriteChannels | ChatBotRights.WriteConnectionString | ChatBotRights.WriteEnabled | ChatBotRights.WriteName | ChatBotRights.WriteProvider)]
180  public override async Task<IActionResult> Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
181  {
182  if (model == null)
183  throw new ArgumentNullException(nameof(model));
184 
185  if (model.Provider.HasValue && !model.ValidateProviderChannelTypes())
186  return BadRequest(new ErrorMessage { Message = "One or more of channels aren't formatted correctly for the given provider!" });
187 
188  var query = DatabaseContext.ChatBots.Where(x => x.InstanceId == Instance.Id && x.Id == model.Id).Include(x => x.Channels);
189 
190  var current = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
191 
192  if (current == default)
193  return StatusCode((int)HttpStatusCode.Gone);
194 
195  var userRights = (ChatBotRights)AuthenticationContext.GetRight(RightsType.ChatBots);
196 
197  bool anySettingsModified = false;
198 
199  bool CheckModified<T>(Expression<Func<Api.Models.Internal.ChatBot, T>> expression, ChatBotRights requiredRight)
200  {
201  var memberSelectorExpression = (MemberExpression)expression.Body;
202  var property = (PropertyInfo)memberSelectorExpression.Member;
203 
204  var newVal = property.GetValue(model);
205  if (newVal == null)
206  return false;
207  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
208  return true;
209 
210  property.SetValue(current, newVal);
211  anySettingsModified = true;
212  return false;
213  }
214 
215  var oldProvider = current.Provider;
216 
217  if (CheckModified(x => x.ConnectionString, ChatBotRights.WriteConnectionString)
218  || CheckModified(x => x.Enabled, ChatBotRights.WriteEnabled)
219  || CheckModified(x => x.Name, ChatBotRights.WriteName)
220  || CheckModified(x => x.Provider, ChatBotRights.WriteProvider)
221  || (model.Channels != null && !userRights.HasFlag(ChatBotRights.WriteChannels)))
222  return Forbid();
223 
224  var hasChannels = model.Channels != null;
225  if (hasChannels || (model.Provider.HasValue && model.Provider != oldProvider))
226  {
227  DatabaseContext.ChatChannels.RemoveRange(current.Channels);
228  if (hasChannels)
229  {
230  var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList();
231  DatabaseContext.ChatChannels.AddRange(dbChannels);
232  current.Channels = dbChannels;
233  }
234  else
235  current.Channels.Clear();
236  }
237 
238  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
239 
240  var chat = instanceManager.GetInstance(Instance).Chat;
241 
242  if (anySettingsModified)
243  await chat.ChangeSettings(current, cancellationToken).ConfigureAwait(false); // have to rebuild the thing first
244 
245  if (model.Channels != null || anySettingsModified)
246  await chat.ChangeChannels(current.Id, current.Channels, cancellationToken).ConfigureAwait(false);
247 
248  if (userRights.HasFlag(ChatBotRights.Read))
249  {
250  if (!userRights.HasFlag(ChatBotRights.ReadConnectionString))
251  current.ConnectionString = null;
252  return Json(current.ToApi());
253  }
254 
255  return Ok();
256  }
257  #pragma warning restore CA1506
258  }
259 }
- +
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
- -
override async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Attempt to delete a model with a particular id
+ +
override async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Attempt to delete a model with a particular id
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
Routes to a server actions
Definition: Routes.cs:9
ChatController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, ILogger< ChatController > logger)
Construct a ChatController
-
override async Task< IActionResult > Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
+
override async Task< IActionResult > Update([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
string Message
A human readable description of the error
Definition: ErrorMessage.cs:13
const string Chat
The Models.ChatBot controller
Definition: Routes.cs:69
-
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
- +
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
+
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
override async Task< IActionResult > Create([FromBody] Api.Models.ChatBot model, CancellationToken cancellationToken)
An ApiController representing a TModel
-
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
+
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
Metadata about a server instance
Definition: Instance.cs:9
ModelController<TModel> for managing Api.Models.ChatBots
diff --git a/_chat_factory_8cs_source.html b/_chat_factory_8cs_source.html index 8fbe77c1d0..f095422d3d 100644 --- a/_chat_factory_8cs_source.html +++ b/_chat_factory_8cs_source.html @@ -93,7 +93,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;
6 using Tgstation.Server.Host.IO;
7 
8 namespace Tgstation.Server.Host.Components.Chat
9 {
11  sealed class ChatFactory : IChatFactory
12  {
16  readonly ILoggerFactory loggerFactory;
17 
22 
27 
32 
40  public ChatFactory(IProviderFactory providerFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory)
41  {
42  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
43  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
44  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
45  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
46  }
47 
49  public IChat CreateChat(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable<Models.ChatBot> initialChatBots) => new Chat(providerFactory, ioManager, commandFactory, serverControl, asyncDelayer, loggerFactory, loggerFactory.CreateLogger<Chat>(), initialChatBots);
50  }
51 }
- + diff --git a/_chat_provider_8cs_source.html b/_chat_provider_8cs_source.html index e1274ce1b6..6a2169454e 100644 --- a/_chat_provider_8cs_source.html +++ b/_chat_provider_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ChatProvider.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public enum ChatProvider
7  {
11  Irc,
15  Discord
16  }
17 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public enum ChatProvider
7  {
11  Irc,
12 
16  Discord
17  }
18 }
ChatProvider
Represents a chat service provider
Definition: ChatProvider.cs:6
diff --git a/_claims_injector_8cs_source.html b/_claims_injector_8cs_source.html index cae8a262ee..dbb2646d02 100644 --- a/_claims_injector_8cs_source.html +++ b/_claims_injector_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ClaimsInjector.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authentication.JwtBearer;
2 using Microsoft.AspNetCore.Http;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.IdentityModel.Tokens.Jwt;
7 using System.Security.Claims;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Security
15 {
18  {
23 
28 
34  public ClaimsInjector(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory)
35  {
36  this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
37  this.authenticationContextFactory = authenticationContextFactory ?? throw new ArgumentNullException(nameof(authenticationContextFactory));
38  }
39 
41  public async Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
42  {
43  if (tokenValidatedContext == null)
44  throw new ArgumentNullException(nameof(tokenValidatedContext));
45 
46  //Find the user id in the token
47  var userIdClaim = tokenValidatedContext.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
48  if (userIdClaim == default)
49  throw new InvalidOperationException("Missing required claim!");
50 
51  long userId;
52  try
53  {
54  userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
55  }
56  catch (Exception e)
57  {
58  throw new InvalidOperationException("Failed to parse user ID!", e);
59  }
60 
61  ApiHeaders apiHeaders;
62  try
63  {
64  apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders());
65  }
66  catch (InvalidOperationException)
67  {
68  //we are not responsible for handling header validation issues
69  return;
70  }
71 
72  //This populates the CurrentAuthenticationContext field for use by us and subsequent controllers
73  await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, tokenValidatedContext.SecurityToken.ValidFrom, cancellationToken).ConfigureAwait(false);
74 
75  var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
76 
77  var enumerator = Enum.GetValues(typeof(RightsType));
78  var claims = new List<Claim>();
79  foreach (RightsType I in enumerator)
80  {
81  //if there's no instance user, do a weird thing and add all the instance roles
82  //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
83  //if user is null that means they got the token with an expired password
84  var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I);
85  var rightEnum = RightsHelper.RightToType(I);
86  var right = (Enum)Enum.ToObject(rightEnum, rightInt);
87  foreach (Enum J in Enum.GetValues(rightEnum))
88  if (right.HasFlag(J))
89  claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
90  }
91 
92  tokenValidatedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
93  }
94  }
95 }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authentication.JwtBearer;
2 using Microsoft.AspNetCore.Http;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.IdentityModel.Tokens.Jwt;
7 using System.Security.Claims;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Security
15 {
18  {
23 
28 
34  public ClaimsInjector(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory)
35  {
36  this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
37  this.authenticationContextFactory = authenticationContextFactory ?? throw new ArgumentNullException(nameof(authenticationContextFactory));
38  }
39 
41  public async Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
42  {
43  if (tokenValidatedContext == null)
44  throw new ArgumentNullException(nameof(tokenValidatedContext));
45 
46  // Find the user id in the token
47  var userIdClaim = tokenValidatedContext.Principal.FindFirst(JwtRegisteredClaimNames.Sub);
48  if (userIdClaim == default)
49  throw new InvalidOperationException("Missing required claim!");
50 
51  long userId;
52  try
53  {
54  userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
55  }
56  catch (Exception e)
57  {
58  throw new InvalidOperationException("Failed to parse user ID!", e);
59  }
60 
61  ApiHeaders apiHeaders;
62  try
63  {
64  apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders());
65  }
66  catch (InvalidOperationException)
67  {
68  // we are not responsible for handling header validation issues
69  return;
70  }
71 
72  // This populates the CurrentAuthenticationContext field for use by us and subsequent controllers
73  await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, tokenValidatedContext.SecurityToken.ValidFrom, cancellationToken).ConfigureAwait(false);
74 
75  var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
76 
77  var enumerator = Enum.GetValues(typeof(RightsType));
78  var claims = new List<Claim>();
79  foreach (RightsType I in enumerator)
80  {
81  // if there's no instance user, do a weird thing and add all the instance roles
82  // we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid
83  // if user is null that means they got the token with an expired password
84  var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I);
85  var rightEnum = RightsHelper.RightToType(I);
86  var right = (Enum)Enum.ToObject(rightEnum, rightInt);
87  foreach (Enum J in Enum.GetValues(rightEnum))
88  if (right.HasFlag(J))
89  claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J)));
90  }
91 
92  tokenValidatedContext.Principal.AddIdentity(new ClaimsIdentity(claims));
93  }
94  }
95 }
static bool IsInstanceRight(RightsType rightsType)
Check if a given rightsType is meant for an Models.Instance
diff --git a/_comm_context_8cs_source.html b/_comm_context_8cs_source.html index 63f9d41ef5..cb9fb4c843 100644 --- a/_comm_context_8cs_source.html +++ b/_comm_context_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
CommContext.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using System;
4 using System.Collections.Generic;
5 using System.IO;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
10 
11 namespace Tgstation.Server.Host.Components.Interop
12 {
14  sealed class CommContext : ICommContext
15  {
20 
24  readonly ILogger<CommContext> logger;
25 
29  readonly FileSystemWatcher fileSystemWatcher;
30 
34  readonly CancellationTokenSource cancellationTokenSource;
35 
39  readonly CancellationToken cancellationToken;
40 
45 
49  bool disposed;
50 
58  public CommContext(IIOManager ioManager, ILogger<CommContext> logger, string directory, string filter)
59  {
60  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
61  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
62 
63  directory = ioManager.ResolvePath(directory) ?? throw new ArgumentNullException(nameof(directory));
64  if (filter == null)
65  throw new ArgumentNullException(nameof(filter));
66 
67  fileSystemWatcher = new FileSystemWatcher(directory, filter)
68  {
69  EnableRaisingEvents = true,
70  IncludeSubdirectories = false,
71  NotifyFilter = NotifyFilters.LastWrite
72  };
73 
74  fileSystemWatcher.Created += HandleWrite;
75  fileSystemWatcher.Changed += HandleWrite;
76 
77  cancellationTokenSource = new CancellationTokenSource();
78  cancellationToken = cancellationTokenSource.Token;
79  disposed = false;
80  }
81 
83  public void Dispose()
84  {
85  if (disposed)
86  return;
87  disposed = true;
88  fileSystemWatcher.Dispose();
89  cancellationTokenSource.Cancel();
90  cancellationTokenSource.Dispose();
91  }
92 
98  async void HandleWrite(object sender, FileSystemEventArgs e) //this is what async void was made for
99  {
100  try
101  {
102  var fileBytes = await ioManager.ReadAllBytes(e.FullPath, cancellationToken).ConfigureAwait(false);
103  var file = Encoding.UTF8.GetString(fileBytes);
104 
105  logger.LogTrace("Read interop command json: {0}", file);
106 
107  CommCommand command;
108  try
109  {
110  command = new CommCommand
111  {
112  Parameters = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, object>>(file),
113  RawJson = file
114  };
115  }
116  catch (JsonException ex)
117  {
118  //file not fully written yet
119  logger.LogDebug("Suppressing json convert exception for command file write: {0}", ex);
120  return;
121  }
122 
123  await (handler?.HandleInterop(command, cancellationToken) ?? Task.CompletedTask).ConfigureAwait(false);
124  }
125  catch (OperationCanceledException) { }
126  catch (Exception ex)
127  {
128  logger.LogDebug("Exception while trying to handle command json write: {0}", ex);
129  }
130  }
131 
133  public void RegisterHandler(ICommHandler handler)
134  {
135  if (this.handler != null)
136  throw new InvalidOperationException("RegisterHandler already called!");
137  this.handler = handler ?? throw new ArgumentNullException(nameof(handler));
138  }
139  }
140 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using System;
4 using System.Collections.Generic;
5 using System.IO;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
10 
11 namespace Tgstation.Server.Host.Components.Interop
12 {
14  sealed class CommContext : ICommContext
15  {
20 
24  readonly ILogger<CommContext> logger;
25 
29  readonly FileSystemWatcher fileSystemWatcher;
30 
34  readonly CancellationTokenSource cancellationTokenSource;
35 
39  readonly CancellationToken cancellationToken;
40 
45 
49  bool disposed;
50 
58  public CommContext(IIOManager ioManager, ILogger<CommContext> logger, string directory, string filter)
59  {
60  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
61  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
62 
63  directory = ioManager.ResolvePath(directory) ?? throw new ArgumentNullException(nameof(directory));
64  if (filter == null)
65  throw new ArgumentNullException(nameof(filter));
66 
67  fileSystemWatcher = new FileSystemWatcher(directory, filter)
68  {
69  EnableRaisingEvents = true,
70  IncludeSubdirectories = false,
71  NotifyFilter = NotifyFilters.LastWrite
72  };
73 
74  fileSystemWatcher.Created += HandleWrite;
75  fileSystemWatcher.Changed += HandleWrite;
76 
77  cancellationTokenSource = new CancellationTokenSource();
78  cancellationToken = cancellationTokenSource.Token;
79  disposed = false;
80  }
81 
83  public void Dispose()
84  {
85  if (disposed)
86  return;
87  disposed = true;
88  fileSystemWatcher.Dispose();
89  cancellationTokenSource.Cancel();
90  cancellationTokenSource.Dispose();
91  }
92 
98  async void HandleWrite(object sender, FileSystemEventArgs e) // this is what async void was made for
99  {
100  try
101  {
102  var fileBytes = await ioManager.ReadAllBytes(e.FullPath, cancellationToken).ConfigureAwait(false);
103  var file = Encoding.UTF8.GetString(fileBytes);
104 
105  logger.LogTrace("Read interop command json: {0}", file);
106 
107  CommCommand command;
108  try
109  {
110  command = new CommCommand
111  {
112  Parameters = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, object>>(file),
113  RawJson = file
114  };
115  }
116  catch (JsonException ex)
117  {
118  // file not fully written yet
119  logger.LogDebug("Suppressing json convert exception for command file write: {0}", ex);
120  return;
121  }
122 
123  await (handler?.HandleInterop(command, cancellationToken) ?? Task.CompletedTask).ConfigureAwait(false);
124  }
125  catch (OperationCanceledException) { }
126  catch (Exception ex)
127  {
128  logger.LogDebug("Exception while trying to handle command json write: {0}", ex);
129  }
130  }
131 
133  public void RegisterHandler(ICommHandler handler)
134  {
135  if (this.handler != null)
136  throw new InvalidOperationException("RegisterHandler already called!");
137  this.handler = handler ?? throw new ArgumentNullException(nameof(handler));
138  }
139  }
140 }
readonly CancellationToken cancellationToken
The CancellationToken for the CommContext
Definition: CommContext.cs:39
Represents a registration of an interop session
Definition: ICommContext.cs:8
diff --git a/_components_2_watchdog_2_watchdog_8cs_source.html b/_components_2_watchdog_2_watchdog_8cs_source.html index 4ae910f2da..2e9bbf74a8 100644 --- a/_components_2_watchdog_2_watchdog_8cs_source.html +++ b/_components_2_watchdog_2_watchdog_8cs_source.html @@ -92,24 +92,24 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Watchdog.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Newtonsoft.Json;
5 using Newtonsoft.Json.Serialization;
6 using System;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Linq;
10 using System.Text;
11 using System.Threading;
12 using System.Threading.Tasks;
19 
20 namespace Tgstation.Server.Host.Components.Watchdog
21 {
24  {
28  const int AlphaBravoStartupSeperationInterval = 10;
29 
31  public bool Running { get; private set; }
32 
34  public bool AlphaIsActive { get; private set; }
35 
37  public Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
38 
40  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; private set; }
41 
43  public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; }
44 
46  public RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
47 
51  readonly IChat chat;
52 
57 
62 
67 
72 
76  readonly IByondTopicSender byondTopicSender;
77 
82 
87 
92 
97 
101  readonly ILogger<Watchdog> logger;
102 
106  readonly SemaphoreSlim semaphore;
107 
111  readonly Api.Models.Instance instance;
112 
116  readonly bool autoStart;
117 
121  CancellationTokenSource monitorCts;
122 
127 
131  TaskCompletionSource<object> activeParametersUpdated;
132 
141 
146 
164  public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger<Watchdog> logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
165  {
166  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
167  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
168  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
169  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
170  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
171  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
172  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
173  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
174  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
175  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
176  ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
177  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
178  this.autoStart = autoStart;
179 
180  if (serverControl == null)
181  throw new ArgumentNullException(nameof(serverControl));
182 
183  chat.RegisterCommandHandler(this);
184 
185  AlphaIsActive = true;
186  ActiveLaunchParameters = initialLaunchParameters;
187  releaseServers = false;
188  activeParametersUpdated = new TaskCompletionSource<object>();
189 
190  restartRegistration = serverControl.RegisterForRestart(this);
191  try
192  {
193  semaphore = new SemaphoreSlim(1);
194  }
195  catch
196  {
197  restartRegistration.Dispose();
198  throw;
199  }
200  }
201 
203  public void Dispose()
204  {
205  DisposeAndNullControllers();
206  semaphore.Dispose();
207  restartRegistration.Dispose();
208  }
209 
214  {
215  alphaServer?.Dispose();
216  alphaServer = null;
217  bravoServer?.Dispose();
218  bravoServer = null;
219  Running = false;
220  }
221 
228  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
229  {
230  if (!Running)
231  return;
232  if (!graceful)
233  {
234  var chatTask = announce ? chat.SendWatchdogMessage("Terminating...", cancellationToken) : Task.CompletedTask;
235  await StopMonitor().ConfigureAwait(false);
236  DisposeAndNullControllers();
237  LastLaunchParameters = null;
238  await chatTask.ConfigureAwait(false);
239  return;
240  }
241 
242  //merely set the reboot state
243  var toKill = AlphaIsActive ? alphaServer : bravoServer;
244  var other = AlphaIsActive ? bravoServer : alphaServer;
245  if (toKill != null)
246  await toKill.SetRebootState(Components.Watchdog.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
247  }
248 
256  async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
257  {
258  logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
259 
260  //this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
261  //I'll do my best to walk you through it
262 
263  //returns true if the inactive server can't be used immediately
264  //also sets monitor to restart if the above holds
265  bool FullRestartDeadInactive()
266  {
267  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
268  {
269  logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
270  monitorState.NextAction = MonitorAction.Restart; //will dispose server
271  return true;
272  }
273  return false;
274  };
275 
276  //trys to set inactive server's port to the public game port
277  //doesn't handle closing active server's port
278  //returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
279  //on failure, sets monitor to restart
280  async Task<bool> MakeInactiveActive()
281  {
282  logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
283  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
284 
285  if (!result)
286  {
287  logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
288  monitorState.NextAction = MonitorAction.Restart; //will dispose server
289  return false;
290  }
291 
292  //inactive server should always be using active launch parameters
293  LastLaunchParameters = ActiveLaunchParameters;
294 
295  var tmp = monitorState.ActiveServer;
296  monitorState.ActiveServer = monitorState.InactiveServer;
297  monitorState.InactiveServer = tmp;
298  AlphaIsActive = !AlphaIsActive;
299  monitorState.ActiveServer.EnableCustomChatCommands();
300  return true;
301  }
302 
303  // Kills and tries to launch inactive server with the latest dmb
304  // falls back to current dmb on failure
305  // Sets critfail on inactive server failing that
306  // returns false if the backup dmb was used successfully, true otherwise
307  async Task UpdateAndRestartInactiveServer(bool breakAfter)
308  {
309  activeParametersUpdated = new TaskCompletionSource<object>();
310  monitorState.InactiveServer.Dispose(); //kill or recycle it
311  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
312  monitorState.NextAction = desiredNextAction;
313 
314  logger.LogInformation("Rebooting inactive server...");
315  var newDmb = dmbFactory.LockNextDmb(1);
316  try
317  {
318  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, newDmb, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
319  monitorState.InactiveServer.SetHighPriority();
320  }
321  catch (OperationCanceledException)
322  {
323  throw;
324  }
325  catch (Exception e)
326  {
327  logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
328  //ahh jeez, what do we do here?
329  //this is our fault, so it should never happen but
330  //idk maybe a database error while handling the newest dmb?
331  //either way try to start it using the active server's dmb as a backup
332  try
333  {
334  var dmbBackup = await dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
335 
336  if (dmbBackup == null) //NANI!?
337  //just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
338  throw new JobException("Creating backup DMB provider failed!");
339 
340  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
341  monitorState.InactiveServer.SetHighPriority();
342  await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
343  }
344  catch (OperationCanceledException)
345  {
346  throw;
347  }
348  catch (Exception e2)
349  {
350  //fuuuuucckkk
351  logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
352  monitorState.InactiveServerCritFail = true;
353  await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
354  return;
355  }
356  }
357 
358  logger.LogInformation("Successfully relaunched inactive server!");
359  monitorState.RebootingInactiveServer = true;
360  }
361 
362  string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
363 
364  //reason handling
365  switch (activationReason)
366  {
367  case MonitorActivationReason.ActiveServerCrashed:
368  if (monitorState.ActiveServer.RebootState == Components.Watchdog.RebootState.Shutdown)
369  {
370  //the time for graceful shutdown is now
371  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Exiting due to graceful termination request...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
372  DisposeAndNullControllers();
373  monitorState.NextAction = MonitorAction.Exit;
374  break;
375  }
376 
377  if (FullRestartDeadInactive())
378  {
379  //tell chat about it and go ahead
380  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Inactive server unable to online!", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
381  //we've already been set to restart
382  break;
383  }
384 
385  //tell chat about it
386  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Onlining inactive server...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
387 
388  //try to active the inactive server
389  if (!await MakeInactiveActive().ConfigureAwait(false))
390  //failing that, we've already been set to restart
391  break;
392 
393  //bring up another inactive server
394  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
395  break;
396  case MonitorActivationReason.InactiveServerCrashed:
397  //just announce and try to bring it back
398  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Inactive server {0}! Rebooting...", ExitWord(monitorState.InactiveServer)), cancellationToken).ConfigureAwait(false);
399  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
400  break;
401  case MonitorActivationReason.ActiveServerRebooted:
402  //ideal goal: active server just closed its port
403  //tell inactive server to open it's port and that's now the active server
404  var rebootState = monitorState.ActiveServer.RebootState;
405  monitorState.ActiveServer.ResetRebootState(); //the DMAPI has already done this internally
406 
407  if (FullRestartDeadInactive() && rebootState != Components.Watchdog.RebootState.Shutdown)
408  //full restart if the inactive server is being fucky
409  break;
410 
411  //what matters here is the RebootState
412  var restartOnceSwapped = false;
413 
414  switch (rebootState)
415  {
416  case Components.Watchdog.RebootState.Normal:
417  //life as normal
418  break;
419  case Components.Watchdog.RebootState.Restart:
420  //reboot the current active server once the inactive one activates
421  restartOnceSwapped = true;
422  break;
423  case Components.Watchdog.RebootState.Shutdown:
424  //graceful shutdown time
425  await chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
426  DisposeAndNullControllers();
427  monitorState.NextAction = MonitorAction.Exit;
428  return;
429  }
430 
431  //are both servers now running the same CompileJob?
432  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
433 
434  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
435  //need a new launch to update either settings or compile job
436  restartOnceSwapped = true;
437 
438  if (restartOnceSwapped)
439  //we need to manually restart active server
440  //just kill it here, easier that way
441  monitorState.ActiveServer.Dispose();
442 
443  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
444 
445  if (activeServerStillHasPortOpen)
446  //we didn't want active server to swap for some reason and it still has it's port open
447  //just continue as normal
448  break;
449 
450  if (!await MakeInactiveActive().ConfigureAwait(false))
451  //monitor will restart
452  break;
453 
454  //servers now swapped
455 
456  //enable this now if inactive server is not still valid
457  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
458 
459  if (!restartOnceSwapped)
460  //now try to reopen it on the private port
461  //failing that, just reboot it
462  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
463 
464  //break either way because any issues past this point would be solved by the reboot
465  if (restartOnceSwapped)
466  //for one reason or another
467  //update and reboot
468  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
469  else
470  //only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
471  monitorState.NextAction = MonitorAction.Skip;
472  break;
473  case MonitorActivationReason.InactiveServerRebooted:
474  //just don't let the active server close it's port if the inactive server isn't ready
475  monitorState.RebootingInactiveServer = true;
476  monitorState.InactiveServer.ResetRebootState();
477  monitorState.ActiveServer.ClosePortOnReboot = false;
478  monitorState.NextAction = MonitorAction.Continue;
479  break;
480  case MonitorActivationReason.InactiveServerStartupComplete:
481  //opposite of above case
482  monitorState.RebootingInactiveServer = false;
483  monitorState.ActiveServer.ClosePortOnReboot = true;
484  monitorState.NextAction = MonitorAction.Continue;
485  break;
486  case MonitorActivationReason.NewDmbAvailable:
487  case MonitorActivationReason.ActiveLaunchParametersUpdated:
488  //just reload the inactive server and wait for a swap to apply the changes
489  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
490  break;
491  }
492  }
493 
499  async Task MonitorLifetimes(CancellationToken cancellationToken)
500  {
501  logger.LogTrace("Entered MonitorLifetimes");
502 
503  //this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
504 
505  var iteration = 1;
506  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
507  {
508  //always start out with continue
509  monitorState.NextAction = MonitorAction.Continue;
510 
511  //dump some info to the logs
512  logger.LogDebug("Iteration {0} of monitor loop", iteration);
513  try
514  {
515  if (AlphaIsActive)
516  logger.LogDebug("Alpha is the active server");
517  else
518  logger.LogDebug("Bravo is the active server");
519 
520 
521  if (monitorState.RebootingInactiveServer)
522  logger.LogDebug("Inactive server is rebooting");
523 
524  //update the monitor state with the inactive/active servers
525  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
526  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
527 
528  if (monitorState.ActiveServer.ClosePortOnReboot)
529  logger.LogDebug("Active server will close port on reboot");
530  if (monitorState.InactiveServer.ClosePortOnReboot)
531  logger.LogDebug("Inactive server will close port on reboot");
532 
533  logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
534  logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
535 
536  //load the activation tasks into local variables
537  Task activeServerLifetime = monitorState.ActiveServer.Lifetime;
538  Task inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
539  var activeServerReboot = monitorState.ActiveServer.OnReboot;
540  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
541  Task inactiveServerStartup = monitorState.RebootingInactiveServer ? monitorState.InactiveServer.LaunchResult : null;
542  Task activeLaunchParametersChanged = activeParametersUpdated.Task;
543  var newDmbAvailable = dmbFactory.OnNewerDmb;
544 
545  //cancel waiting if requested
546  var cancelTcs = new TaskCompletionSource<object>();
547  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
548  {
549  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
550  if (monitorState.RebootingInactiveServer)
551  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
552  //wait for something to happen
553  await toWaitOn.ConfigureAwait(false);
554  cancellationToken.ThrowIfCancellationRequested();
555  }
556 
557  var chatTask = Task.CompletedTask;
558  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
559  {
560  //always run HandleMonitorWakeup from the context of the semaphore lock
561  //multiple things may have happened, handle them one at a time
562  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
563  {
564  MonitorActivationReason activationReason = default; //this will always be assigned before being used
565 
566  //process the tasks in this order and call HandlerMonitorWakup for each
567 
568  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
569  {
570  var taskCompleted = task?.IsCompleted == true;
571  task = null;
572  if (monitorState.NextAction == MonitorAction.Skip)
573  monitorState.NextAction = MonitorAction.Continue;
574  else if (taskCompleted)
575  {
576  activationReason = testActivationReason;
577  return true;
578  }
579  return false;
580  };
581 
582  if (CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
583  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
584  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
585  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
586  || CheckActivationReason(ref inactiveServerStartup, MonitorActivationReason.InactiveServerStartupComplete)
587  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
588  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated))
589  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
590  else
591  moreActivationsToProcess = false;
592  }
593 
594  //writeback alphaServer and bravoServer from monitor state in case they changesd
595  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
596  bravoServer = !AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
597  }
598 
599  //full reboot required
600  if (monitorState.NextAction == MonitorAction.Restart)
601  {
602  logger.LogDebug("Next state action is to restart");
603  DisposeAndNullControllers();
604  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
605 
606  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
607  {
608  Exception launchException = null;
609  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
610  try
611  {
612  //use LaunchImplNoLock without announcements or restarting the monitor
613  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
614  if (Running)
615  {
616  logger.LogDebug("Relaunch successful, resetting monitor state...");
617  monitorState = new MonitorState(); //clean the slate and continue
618  }
619  }
620  catch (OperationCanceledException)
621  {
622  throw;
623  }
624  catch (Exception e)
625  {
626  launchException = e;
627  }
628 
629  await chatTask.ConfigureAwait(false);
630  if (!Running)
631  {
632  if (launchException == null)
633  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
634  else
635  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
636  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); //max of one hour, increasing by a power of 2 each time
637  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
638  await Task.WhenAll(asyncDelayer.Delay(TimeSpan.FromSeconds(retryDelay), cancellationToken), chatTask).ConfigureAwait(false);
639  }
640  }
641  }
642  }
643  catch (OperationCanceledException)
644  {
645  logger.LogDebug("Monitor cancelled");
646  break;
647  }
648  catch (Exception e)
649  {
650  //really, this should NEVER happen
651  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}, Exception: {2}", iteration, JsonConvert.SerializeObject(monitorState), e);
652  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Monitor crashed, this should NEVER happen! Please report this, full details in logs! Restarting monitor... Error: {0}", e.Message), cancellationToken).ConfigureAwait(false);
653  }
654  }
655  logger.LogTrace("Monitor exiting...");
656  }
657 
662  async Task<bool> StopMonitor()
663  {
664  logger.LogTrace("StopMonitor");
665  if (monitorTask == null)
666  return false;
667  monitorCts.Cancel();
668  await monitorTask.ConfigureAwait(false);
669  monitorCts.Dispose();
670  monitorTask = null;
671  return true;
672  }
673 
674 
676  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
677  {
678  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
679  {
680  if (launchParameters.Match(ActiveLaunchParameters))
681  return;
682  ActiveLaunchParameters = launchParameters;
683  if (Running)
684  //queue an update
685  activeParametersUpdated.TrySetResult(null);
686  }
687  }
688 
697  async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
698  {
699  logger.LogTrace("Begin LaunchNoLock");
700 
701  if (Running)
702  throw new JobException("Watchdog already running!");
703 
704  Task chatTask;
705  //this is necessary, the monitor could be in it's sleep loop trying to restart, if so cancel THAT monitor and start our own with blackjack and hookers
706  if (startMonitor && await StopMonitor().ConfigureAwait(false))
707  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
708  else if (announce)
709  //simple announce
710  chatTask = chat.SendWatchdogMessage(reattachInfo == null ? "Starting..." : "Reattaching...", cancellationToken);
711  else
712  //no announce
713  chatTask = Task.CompletedTask;
714 
715  //since neither server is running, this is safe to do
716  LastLaunchParameters = ActiveLaunchParameters;
717 
718  //for when we call ourself and want to not catch thrown exceptions
719  var ignoreNestedException = false;
720  try
721  {
722  //good ole sanity, should never fucking trigger but i don't trust myself even though I should
723  if (alphaServer != null || bravoServer != null)
724  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
725 
726  //don't need a new dmb if reattaching
727  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
728  var dmbToUse = doesntNeedNewDmb ? null : dmbFactory.LockNextDmb(2);
729 
730  //if this try catches something, both servers are killed
731  try
732  {
733  //start the alpha server task, either by launch a new process or attaching to an existing one
734  //The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
735  //The tasks pertaining to server startup times are in the ISessionControllers
736  Task<ISessionController> alphaServerTask;
737  if (!doesntNeedNewDmb)
738  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken);
739  else
740  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
741 
742  //retrieve the session controller
743  var startTime = DateTimeOffset.Now;
744  alphaServer = await alphaServerTask.ConfigureAwait(false);
745  //failed reattaches will return null
746  alphaServer?.SetHighPriority();
747 
748  //extra delay for total ordering
749  var now = DateTimeOffset.Now;
750  var delay = now - startTime;
751 
752  //definitely not if reattaching though
753  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
754  await asyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
755 
756  //now bring bravo up
757  if (!doesntNeedNewDmb)
758  bravoServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken).ConfigureAwait(false);
759  else
760  bravoServer = await sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
761  //failed reattaches will return null
762  bravoServer?.SetHighPriority();
763 
764  //possiblity of null servers due to failed reattaches
765  if (alphaServer == null || bravoServer == null)
766  {
767  await chatTask.ConfigureAwait(false);
768  var bothServersDead = alphaServer == null && bravoServer == null;
769  if (bothServersDead
770  || (alphaServer == null && reattachInfo.AlphaIsActive)
771  || (bravoServer == null && !reattachInfo.AlphaIsActive))
772  {
773  //we lost the active server, just restart entirely
774  DisposeAndNullControllers();
775  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
776  logger.LogWarning(FailReattachMessage);
777  logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
778  chatTask = chat.SendWatchdogMessage(FailReattachMessage, cancellationToken);
779  ignoreNestedException = true;
780  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
781  return;
782  }
783 
784  //we still have the active server but the other one is dead to us, hand it off to the monitor to restart
785  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
786  chatTask = chat.SendWatchdogMessage(InactiveReattachFailureMessage, cancellationToken);
787  logger.LogWarning(InactiveReattachFailureMessage);
788 
789  if (reattachInfo.AlphaIsActive)
790  bravoServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
791  else
792  alphaServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
793  }
794 
795  //throws a JobException if something went wrong with a launch
796  //Dead sessions won't trigger this
797  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
798  {
799  var launch = await controller.LaunchResult.ConfigureAwait(false);
800  if (launch.ExitCode.HasValue)
801  //you killed us ray...
802  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
803  if (!launch.StartupTime.HasValue)
804  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server timed out on startup: {0}s", launch.ToString(), ActiveLaunchParameters.StartupTimeout.Value));
805  return launch;
806  }
807 
808  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
809  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
810  //this task completes when both serers have finished booting
811  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
812 
813  //don't forget about the cancellationToken
814  var cancelTcs = new TaskCompletionSource<object>();
815  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
816  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
817  cancellationToken.ThrowIfCancellationRequested();
818 
819  await allTask.ConfigureAwait(false);
820 
821  //both servers are now running, alpha is the active server(unless reattach), huzzah
822  AlphaIsActive = reattachInfo?.AlphaIsActive ?? true;
823 
824  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
825  activeServer.EnableCustomChatCommands();
826  activeServer.ClosePortOnReboot = true;
827 
828  logger.LogInformation("Launched servers successfully");
829  Running = true;
830 
831  if (startMonitor)
832  {
833  monitorCts = new CancellationTokenSource();
834  monitorTask = MonitorLifetimes(monitorCts.Token);
835  }
836  }
837  catch
838  {
839  if (dmbToUse != null)
840  {
841  //we locked 2 dmbs
842  if (bravoServer == null)
843  {
844  //bravo didn't get control of his
845  dmbToUse.Dispose();
846  if (alphaServer == null)
847  //alpha didn't get control of his
848  dmbToUse.Dispose();
849  }
850  }
851  else if (doesntNeedNewDmb)
852  //we have reattachInfo
853  if (bravoServer == null)
854  {
855  //bravo didn't get control of his
856  reattachInfo.Bravo?.Dmb.Dispose();
857  if (alphaServer == null)
858  //alpha didn't get control of his
859  reattachInfo.Alpha?.Dmb.Dispose();
860  }
861  //kill the controllers
862  DisposeAndNullControllers();
863  throw;
864  }
865  }
866  catch (Exception e)
867  {
868  //don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
869  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
870  {
871  var originalChatTask = chatTask;
872  async Task ChainChatTaskWithErrorMessage()
873  {
874  await originalChatTask.ConfigureAwait(false);
875  await chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false);
876  }
877  chatTask = ChainChatTaskWithErrorMessage();
878  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
879  }
880  throw;
881  }
882  finally
883  {
884  //finish the chat task that's in flight
885  try
886  {
887  await chatTask.ConfigureAwait(false);
888  }
889  catch (OperationCanceledException) { }
890  }
891  }
892 
894  public async Task Launch(CancellationToken cancellationToken)
895  {
896  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
897  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
898  }
899 
901  public async Task ResetRebootState(CancellationToken cancellationToken)
902  {
903  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
904  {
905  if (!Running)
906  return;
907  var toClear = AlphaIsActive ? alphaServer : bravoServer;
908  if (toClear != null)
909  toClear.ResetRebootState();
910  }
911  }
912 
914  public async Task Restart(bool graceful, CancellationToken cancellationToken)
915  {
916  logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
917  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
918  {
919  if (!graceful || !Running)
920  {
921  Task chatTask;
922  if (Running)
923  {
924  chatTask = chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken);
925  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
926  }
927  else
928  chatTask = Task.CompletedTask;
929  await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
930  await chatTask.ConfigureAwait(false);
931  }
932  var toReboot = AlphaIsActive ? alphaServer : bravoServer;
933  if (toReboot != null)
934  {
935  if (!await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false))
936  logger.LogWarning("Unable to send reboot state change event!");
937  }
938  }
939  }
940 
942  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
943  {
944  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
945  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
946  }
947 
949  public async Task StartAsync(CancellationToken cancellationToken)
950  {
951  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
952  if (!autoStart && reattachInfo == null)
953  return;
954 
955  long? adminUserId = null;
956 
957  await databaseContextFactory.UseContext(async db => adminUserId = await db.Users
958  .Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant())
959  .Select(x => x.Id)
960  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
961  var job = new Models.Job
962  {
963  StartedBy = new Models.User
964  {
965  Id = adminUserId.Value
966  },
967  Instance = new Models.Instance
968  {
969  Id = instance.Id
970  },
971  Description = "Instance startup watchdog launch",
972  CancelRight = (ulong)DreamDaemonRights.Shutdown,
973  CancelRightsType = RightsType.DreamDaemon
974  };
975  await jobManager.RegisterOperation(job, async (j, databaseContext, progressFunction, ct) =>
976  {
977  using (await SemaphoreSlimContext.Lock(semaphore, ct).ConfigureAwait(false))
978  await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
979  }, cancellationToken).ConfigureAwait(false);
980  }
981 
983  public async Task StopAsync(CancellationToken cancellationToken)
984  {
985  if (releaseServers && Running)
986  {
987  await StopMonitor().ConfigureAwait(false);
988 
989  var reattachInformation = new WatchdogReattachInformation
990  {
991  AlphaIsActive = AlphaIsActive
992  };
993  reattachInformation.Alpha = alphaServer?.Release();
994  reattachInformation.Bravo = bravoServer?.Release();
995  await reattachInfoHandler.Save(reattachInformation, cancellationToken).ConfigureAwait(false);
996  }
997  await Terminate(false, cancellationToken).ConfigureAwait(false);
998  }
999 
1001  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
1002  {
1003  string results;
1004  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1005  {
1006  if (!Running)
1007  return true;
1008 
1009  var builder = new StringBuilder(Constants.DMTopicEvent);
1010  builder.Append('&');
1011  var notification = new EventNotification
1012  {
1013  Type = eventType,
1014  Parameters = parameters
1015  };
1016  var json = JsonConvert.SerializeObject(notification);
1017  builder.Append(byondTopicSender.SanitizeString(Constants.DMParameterData));
1018  builder.Append('=');
1019  builder.Append(byondTopicSender.SanitizeString(json));
1020 
1021  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1022  results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
1023  }
1024 
1025  if (results == Constants.DMResponseSuccess)
1026  return true;
1027 
1028  List<Response> responses;
1029  try
1030  {
1031  responses = JsonConvert.DeserializeObject<List<Response>>(results);
1032  }
1033  catch
1034  {
1035  logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
1036  return true;
1037  }
1038 
1039  await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
1040 
1041  return true;
1042  }
1043 
1045  public async Task<string> HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
1046  {
1047  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1048  {
1049  if (!Running)
1050  return "ERROR: Server offline!";
1051 
1052  var commandObject = new ChatCommand
1053  {
1054  Command = commandName,
1055  Params = arguments,
1056  User = sender
1057  };
1058 
1059  var json = JsonConvert.SerializeObject(commandObject, new JsonSerializerSettings
1060  {
1061  ContractResolver = new CamelCasePropertyNamesContractResolver()
1062  });
1063 
1064  var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json));
1065 
1066  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1067  return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
1068  }
1069  }
1070 
1072  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
1073  {
1074  releaseServers = true;
1075  if (Running)
1076  await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false);
1077  }
1078  }
1079 }
bool InactiveServerCritFail
If the inactive server is in an unrecoverable state
Definition: MonitorState.cs:18
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Newtonsoft.Json;
5 using Newtonsoft.Json.Serialization;
6 using System;
7 using System.Collections.Generic;
8 using System.Diagnostics;
9 using System.Globalization;
10 using System.Linq;
11 using System.Text;
12 using System.Threading;
13 using System.Threading.Tasks;
20 
21 namespace Tgstation.Server.Host.Components.Watchdog
22 {
24  #pragma warning disable CA1506 // TODO: Decomplexify
26  {
30  const int AlphaBravoStartupSeperationInterval = 10;
31 
33  public bool Running { get; private set; }
34 
36  public bool AlphaIsActive { get; private set; }
37 
39  public Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
40 
42  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; private set; }
43 
45  public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; }
46 
48  public RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
49 
53  readonly IChat chat;
54 
59 
64 
69 
74 
78  readonly IByondTopicSender byondTopicSender;
79 
84 
89 
94 
99 
103  readonly ILogger<Watchdog> logger;
104 
108  readonly SemaphoreSlim semaphore;
109 
113  readonly Api.Models.Instance instance;
114 
118  readonly bool autoStart;
119 
123  CancellationTokenSource monitorCts;
124 
129 
133  TaskCompletionSource<object> activeParametersUpdated;
134 
139 
144 
149 
167  public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger<Watchdog> logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
168  {
169  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
170  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
171  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
172  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
173  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
174  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
175  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
176  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
177  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
178  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
179  ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
180  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
181  this.autoStart = autoStart;
182 
183  if (serverControl == null)
184  throw new ArgumentNullException(nameof(serverControl));
185 
186  chat.RegisterCommandHandler(this);
187 
188  AlphaIsActive = true;
189  ActiveLaunchParameters = initialLaunchParameters;
190  releaseServers = false;
191  activeParametersUpdated = new TaskCompletionSource<object>();
192 
193  restartRegistration = serverControl.RegisterForRestart(this);
194  try
195  {
196  semaphore = new SemaphoreSlim(1);
197  }
198  catch
199  {
200  restartRegistration.Dispose();
201  throw;
202  }
203  }
204 
206  public void Dispose()
207  {
208  DisposeAndNullControllers();
209  semaphore.Dispose();
210  restartRegistration.Dispose();
211  }
212 
217  {
218  alphaServer?.Dispose();
219  alphaServer = null;
220  bravoServer?.Dispose();
221  bravoServer = null;
222  Running = false;
223  }
224 
231  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
232  {
233  if (!Running)
234  return;
235  if (!graceful)
236  {
237  var chatTask = announce ? chat.SendWatchdogMessage("Terminating...", cancellationToken) : Task.CompletedTask;
238  await StopMonitor().ConfigureAwait(false);
239  DisposeAndNullControllers();
240  LastLaunchParameters = null;
241  await chatTask.ConfigureAwait(false);
242  return;
243  }
244 
245  // merely set the reboot state
246  var toKill = AlphaIsActive ? alphaServer : bravoServer;
247  var other = AlphaIsActive ? bravoServer : alphaServer;
248  if (toKill != null)
249  await toKill.SetRebootState(Components.Watchdog.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
250  }
251 
259  #pragma warning disable CA1502 // TODO: Decomplexify
260  async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
261  {
262  logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
263 
264  // this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
265  // I'll do my best to walk you through it
266 
267  // returns true if the inactive server can't be used immediately
268  // also sets monitor to restart if the above holds
269  bool FullRestartDeadInactive()
270  {
271  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
272  {
273  logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
274  monitorState.NextAction = MonitorAction.Restart; // will dispose server
275  return true;
276  }
277 
278  return false;
279  }
280 
281  // trys to set inactive server's port to the public game port
282  // doesn't handle closing active server's port
283  // returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
284  // on failure, sets monitor to restart
285  async Task<bool> MakeInactiveActive()
286  {
287  logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
288  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
289 
290  if (!result)
291  {
292  logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
293  monitorState.NextAction = MonitorAction.Restart; // will dispose server
294  return false;
295  }
296 
297  // inactive server should always be using active launch parameters
298  LastLaunchParameters = ActiveLaunchParameters;
299 
300  var tmp = monitorState.ActiveServer;
301  monitorState.ActiveServer = monitorState.InactiveServer;
302  monitorState.InactiveServer = tmp;
303  AlphaIsActive = !AlphaIsActive;
304  monitorState.ActiveServer.EnableCustomChatCommands();
305  return true;
306  }
307 
308  // Kills and tries to launch inactive server with the latest dmb
309  // falls back to current dmb on failure
310  // Sets critfail on inactive server failing that
311  // returns false if the backup dmb was used successfully, true otherwise
312  async Task UpdateAndRestartInactiveServer(bool breakAfter)
313  {
314  activeParametersUpdated = new TaskCompletionSource<object>();
315  monitorState.InactiveServer.Dispose(); // kill or recycle it
316  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
317  monitorState.NextAction = desiredNextAction;
318 
319  logger.LogInformation("Rebooting inactive server...");
320  var newDmb = dmbFactory.LockNextDmb(1);
321  try
322  {
323  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, newDmb, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
324  monitorState.InactiveServer.SetHighPriority();
325  }
326  catch (OperationCanceledException)
327  {
328  throw;
329  }
330  catch (Exception e)
331  {
332  logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
333 
334  // ahh jeez, what do we do here?
335  // this is our fault, so it should never happen but
336  // idk maybe a database error while handling the newest dmb?
337  // either way try to start it using the active server's dmb as a backup
338  try
339  {
340  var dmbBackup = await dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
341 
342  if (dmbBackup == null) // NANI!?
343  throw new JobException("Creating backup DMB provider failed!"); // just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
344 
345  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
346  monitorState.InactiveServer.SetHighPriority();
347  await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
348  }
349  catch (OperationCanceledException)
350  {
351  throw;
352  }
353  catch (Exception e2)
354  {
355  // fuuuuucckkk
356  logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
357  monitorState.InactiveServerCritFail = true;
358  await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
359  return;
360  }
361  }
362 
363  logger.LogInformation("Successfully relaunched inactive server!");
364  monitorState.RebootingInactiveServer = true;
365  }
366 
367  string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
368 
369  // reason handling
370  switch (activationReason)
371  {
372  case MonitorActivationReason.ActiveServerCrashed:
373  if (monitorState.ActiveServer.RebootState == Components.Watchdog.RebootState.Shutdown)
374  {
375  // the time for graceful shutdown is now
376  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Exiting due to graceful termination request...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
377  DisposeAndNullControllers();
378  monitorState.NextAction = MonitorAction.Exit;
379  break;
380  }
381 
382  if (FullRestartDeadInactive())
383  {
384  // tell chat about it and go ahead
385  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Inactive server unable to online!", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
386 
387  // we've already been set to restart
388  break;
389  }
390 
391  // tell chat about it
392  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Onlining inactive server...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
393 
394  // try to activate the inactive server
395  if (!await MakeInactiveActive().ConfigureAwait(false))
396  break; // failing that, we've already been set to restart
397 
398  // bring up another inactive server
399  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
400  break;
401  case MonitorActivationReason.InactiveServerCrashed:
402  // just announce and try to bring it back
403  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Inactive server {0}! Rebooting...", ExitWord(monitorState.InactiveServer)), cancellationToken).ConfigureAwait(false);
404  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
405  break;
406  case MonitorActivationReason.ActiveServerRebooted:
407  // ideal goal: active server just closed its port
408  // tell inactive server to open it's port and that's now the active server
409  var rebootState = monitorState.ActiveServer.RebootState;
410  monitorState.ActiveServer.ResetRebootState(); // the DMAPI has already done this internally
411 
412  if (FullRestartDeadInactive() && rebootState != Components.Watchdog.RebootState.Shutdown)
413  break; // full restart if the inactive server is being fucky
414 
415  // what matters here is the RebootState
416  var restartOnceSwapped = false;
417 
418  switch (rebootState)
419  {
420  case Components.Watchdog.RebootState.Normal:
421  // life as normal
422  break;
423  case Components.Watchdog.RebootState.Restart:
424  // reboot the current active server once the inactive one activates
425  restartOnceSwapped = true;
426  break;
427  case Components.Watchdog.RebootState.Shutdown:
428  // graceful shutdown time
429  await chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
430  DisposeAndNullControllers();
431  monitorState.NextAction = MonitorAction.Exit;
432  return;
433  default:
434  Trace.Assert(false, String.Format(CultureInfo.InvariantCulture, "Invalid RebootState: {0}!", rebootState));
435  break;
436  }
437 
438  // are both servers now running the same CompileJob?
439  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
440 
441  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
442  restartOnceSwapped = true; // need a new launch to update either settings or compile job
443 
444  if (restartOnceSwapped)
445  /*
446  * we need to manually restart active server
447  * just kill it here, easier that way
448  */
449  monitorState.ActiveServer.Dispose();
450 
451  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
452 
453  if (activeServerStillHasPortOpen)
454  /* we didn't want active server to swap for some reason and it still has it's port open
455  * just continue as normal
456  */
457  break;
458 
459  if (!await MakeInactiveActive().ConfigureAwait(false))
460  break; // monitor will restart
461 
462  // servers now swapped
463  // enable this now if inactive server is not still valid
464  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
465 
466  if (!restartOnceSwapped)
467  /*
468  * now try to reopen it on the private port
469  * failing that, just reboot it
470  */
471  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
472 
473  // break either way because any issues past this point would be solved by the reboot
474  if (restartOnceSwapped) // for one reason or another
475  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); // update and reboot
476  else
477  monitorState.NextAction = MonitorAction.Skip; // only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
478  break;
479  case MonitorActivationReason.InactiveServerRebooted:
480  // just don't let the active server close it's port if the inactive server isn't ready
481  monitorState.RebootingInactiveServer = true;
482  monitorState.InactiveServer.ResetRebootState();
483  monitorState.ActiveServer.ClosePortOnReboot = false;
484  monitorState.NextAction = MonitorAction.Continue;
485  break;
486  case MonitorActivationReason.InactiveServerStartupComplete:
487  // opposite of above case
488  monitorState.RebootingInactiveServer = false;
489  monitorState.ActiveServer.ClosePortOnReboot = true;
490  monitorState.NextAction = MonitorAction.Continue;
491  break;
492  case MonitorActivationReason.NewDmbAvailable:
493  case MonitorActivationReason.ActiveLaunchParametersUpdated:
494  // just reload the inactive server and wait for a swap to apply the changes
495  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
496  break;
497  default:
498  Trace.Assert(false, String.Format(CultureInfo.InvariantCulture, "Invalid monitor activation reason: {0}!", activationReason));
499  break;
500  }
501  }
502  #pragma warning restore CA1502
503 
509 #pragma warning disable CA1502 // TODO: Decomplexify
510  async Task MonitorLifetimes(CancellationToken cancellationToken)
511  {
512  logger.LogTrace("Entered MonitorLifetimes");
513 
514  // this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
515  var iteration = 1;
516  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
517  {
518  // always start out with continue
519  monitorState.NextAction = MonitorAction.Continue;
520 
521  // dump some info to the logs
522  logger.LogDebug("Iteration {0} of monitor loop", iteration);
523  try
524  {
525  if (AlphaIsActive)
526  logger.LogDebug("Alpha is the active server");
527  else
528  logger.LogDebug("Bravo is the active server");
529 
530  if (monitorState.RebootingInactiveServer)
531  logger.LogDebug("Inactive server is rebooting");
532 
533  // update the monitor state with the inactive/active servers
534  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
535  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
536 
537  if (monitorState.ActiveServer.ClosePortOnReboot)
538  logger.LogDebug("Active server will close port on reboot");
539  if (monitorState.InactiveServer.ClosePortOnReboot)
540  logger.LogDebug("Inactive server will close port on reboot");
541 
542  logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
543  logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
544 
545  // load the activation tasks into local variables
546  Task activeServerLifetime = monitorState.ActiveServer.Lifetime;
547  Task inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
548  var activeServerReboot = monitorState.ActiveServer.OnReboot;
549  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
550  Task inactiveServerStartup = monitorState.RebootingInactiveServer ? monitorState.InactiveServer.LaunchResult : null;
551  Task activeLaunchParametersChanged = activeParametersUpdated.Task;
552  var newDmbAvailable = dmbFactory.OnNewerDmb;
553 
554  // cancel waiting if requested
555  var cancelTcs = new TaskCompletionSource<object>();
556  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
557  {
558  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
559  if (monitorState.RebootingInactiveServer)
560  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
561 
562  // wait for something to happen
563  await toWaitOn.ConfigureAwait(false);
564  cancellationToken.ThrowIfCancellationRequested();
565  }
566 
567  var chatTask = Task.CompletedTask;
568  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
569  {
570  // always run HandleMonitorWakeup from the context of the semaphore lock
571  // multiple things may have happened, handle them one at a time
572  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
573  {
574  MonitorActivationReason activationReason = default; // this will always be assigned before being used
575 
576  // process the tasks in this order and call HandlerMonitorWakup for each
577  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
578  {
579  var taskCompleted = task?.IsCompleted == true;
580  task = null;
581  if (monitorState.NextAction == MonitorAction.Skip)
582  monitorState.NextAction = MonitorAction.Continue;
583  else if (taskCompleted)
584  {
585  activationReason = testActivationReason;
586  return true;
587  }
588 
589  return false;
590  }
591 
592  if (CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
593  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
594  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
595  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
596  || CheckActivationReason(ref inactiveServerStartup, MonitorActivationReason.InactiveServerStartupComplete)
597  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
598  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated))
599  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
600  else
601  moreActivationsToProcess = false;
602  }
603 
604  // writeback alphaServer and bravoServer from monitor state in case they changesd
605  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
606  bravoServer = !AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
607  }
608 
609  // full reboot required
610  if (monitorState.NextAction == MonitorAction.Restart)
611  {
612  logger.LogDebug("Next state action is to restart");
613  DisposeAndNullControllers();
614  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
615 
616  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
617  {
618  Exception launchException = null;
619  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
620  try
621  {
622  // use LaunchImplNoLock without announcements or restarting the monitor
623  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
624  if (Running)
625  {
626  logger.LogDebug("Relaunch successful, resetting monitor state...");
627  monitorState = new MonitorState(); // clean the slate and continue
628  }
629  }
630  catch (OperationCanceledException)
631  {
632  throw;
633  }
634  catch (Exception e)
635  {
636  launchException = e;
637  }
638 
639  await chatTask.ConfigureAwait(false);
640  if (!Running)
641  {
642  if (launchException == null)
643  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
644  else
645  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
646  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); // max of one hour, increasing by a power of 2 each time
647  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
648  await Task.WhenAll(asyncDelayer.Delay(TimeSpan.FromSeconds(retryDelay), cancellationToken), chatTask).ConfigureAwait(false);
649  }
650  }
651  }
652  }
653  catch (OperationCanceledException)
654  {
655  logger.LogDebug("Monitor cancelled");
656  break;
657  }
658  catch (Exception e)
659  {
660  // really, this should NEVER happen
661  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}, Exception: {2}", iteration, JsonConvert.SerializeObject(monitorState), e);
662  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Monitor crashed, this should NEVER happen! Please report this, full details in logs! Restarting monitor... Error: {0}", e.Message), cancellationToken).ConfigureAwait(false);
663  }
664  }
665 
666  logger.LogTrace("Monitor exiting...");
667  }
668  #pragma warning restore CA1502
669 
674  async Task<bool> StopMonitor()
675  {
676  logger.LogTrace("StopMonitor");
677  if (monitorTask == null)
678  return false;
679  monitorCts.Cancel();
680  await monitorTask.ConfigureAwait(false);
681  monitorCts.Dispose();
682  monitorTask = null;
683  return true;
684  }
685 
687  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
688  {
689  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
690  {
691  if (launchParameters.Match(ActiveLaunchParameters))
692  return;
693  ActiveLaunchParameters = launchParameters;
694  if (Running)
695  activeParametersUpdated.TrySetResult(null); // queue an update
696  }
697  }
698 
707  #pragma warning disable CA1502 // TODO: Decomplexify
708  async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
709  {
710  logger.LogTrace("Begin LaunchNoLock");
711 
712  if (Running)
713  throw new JobException("Watchdog already running!");
714 
715  // 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
716  Task chatTask;
717  if (startMonitor && await StopMonitor().ConfigureAwait(false))
718  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
719  else if (announce)
720  chatTask = chat.SendWatchdogMessage(reattachInfo == null ? "Starting..." : "Reattaching...", cancellationToken); // simple announce
721  else
722  chatTask = Task.CompletedTask; // no announce
723 
724  // since neither server is running, this is safe to do
725  LastLaunchParameters = ActiveLaunchParameters;
726 
727  // for when we call ourself and want to not catch thrown exceptions
728  var ignoreNestedException = false;
729  try
730  {
731  // good ole sanity, should never fucking trigger but i don't trust myself even though I should
732  // TODO: Unit test this instead?
733  if (alphaServer != null || bravoServer != null)
734  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
735 
736  // don't need a new dmb if reattaching
737  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
738  var dmbToUse = doesntNeedNewDmb ? null : dmbFactory.LockNextDmb(2);
739 
740  // if this try catches something, both servers are killed
741  try
742  {
743  // start the alpha server task, either by launch a new process or attaching to an existing one
744  // The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
745  // The tasks pertaining to server startup times are in the ISessionControllers
746  Task<ISessionController> alphaServerTask;
747  if (!doesntNeedNewDmb)
748  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken);
749  else
750  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
751 
752  // retrieve the session controller
753  var startTime = DateTimeOffset.Now;
754  alphaServer = await alphaServerTask.ConfigureAwait(false);
755 
756  // failed reattaches will return null
757  alphaServer?.SetHighPriority();
758 
759  // extra delay for total ordering
760  var now = DateTimeOffset.Now;
761  var delay = now - startTime;
762 
763  // definitely not if reattaching though
764  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
765  await asyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
766 
767  // now bring bravo up
768  if (!doesntNeedNewDmb)
769  bravoServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken).ConfigureAwait(false);
770  else
771  bravoServer = await sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
772 
773  // failed reattaches will return null
774  bravoServer?.SetHighPriority();
775 
776  // possiblity of null servers due to failed reattaches
777  if (alphaServer == null || bravoServer == null)
778  {
779  await chatTask.ConfigureAwait(false);
780  var bothServersDead = alphaServer == null && bravoServer == null;
781  if (bothServersDead
782  || (alphaServer == null && reattachInfo.AlphaIsActive)
783  || (bravoServer == null && !reattachInfo.AlphaIsActive))
784  {
785  // we lost the active server, just restart entirely
786  DisposeAndNullControllers();
787  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
788  logger.LogWarning(FailReattachMessage);
789  logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
790  chatTask = chat.SendWatchdogMessage(FailReattachMessage, cancellationToken);
791  ignoreNestedException = true;
792  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
793  return;
794  }
795 
796  // we still have the active server but the other one is dead to us, hand it off to the monitor to restart
797  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
798  chatTask = chat.SendWatchdogMessage(InactiveReattachFailureMessage, cancellationToken);
799  logger.LogWarning(InactiveReattachFailureMessage);
800 
801  if (reattachInfo.AlphaIsActive)
802  bravoServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
803  else
804  alphaServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
805  }
806 
807  // throws a JobException if something went wrong with a launch
808  // Dead sessions won't trigger this
809  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
810  {
811  var launch = await controller.LaunchResult.ConfigureAwait(false);
812  if (launch.ExitCode.HasValue) // you killed us ray...
813  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
814  if (!launch.StartupTime.HasValue)
815  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server timed out on startup: {0}s", launch.ToString(), ActiveLaunchParameters.StartupTimeout.Value));
816  return launch;
817  }
818 
819  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
820  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
821 
822  // this task completes when both serers have finished booting
823  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
824 
825  // don't forget about the cancellationToken
826  var cancelTcs = new TaskCompletionSource<object>();
827  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
828  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
829  cancellationToken.ThrowIfCancellationRequested();
830 
831  await allTask.ConfigureAwait(false);
832 
833  // both servers are now running, alpha is the active server(unless reattach), huzzah
834  AlphaIsActive = reattachInfo?.AlphaIsActive ?? true;
835 
836  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
837  activeServer.EnableCustomChatCommands();
838  activeServer.ClosePortOnReboot = true;
839 
840  logger.LogInformation("Launched servers successfully");
841  Running = true;
842 
843  if (startMonitor)
844  {
845  monitorCts = new CancellationTokenSource();
846  monitorTask = MonitorLifetimes(monitorCts.Token);
847  }
848  }
849  catch
850  {
851  if (dmbToUse != null)
852  {
853  // we locked 2 dmbs
854  if (bravoServer == null)
855  {
856  // bravo didn't get control of his
857  dmbToUse.Dispose();
858  if (alphaServer == null)
859  dmbToUse.Dispose(); // alpha didn't get control of his
860  }
861  }
862  else if (doesntNeedNewDmb) // we have reattachInfo
863  if (bravoServer == null)
864  {
865  // bravo didn't get control of his
866  reattachInfo.Bravo?.Dmb.Dispose();
867  if (alphaServer == null)
868  reattachInfo.Alpha?.Dmb.Dispose(); // alpha didn't get control of his
869  }
870 
871  // kill the controllers
872  DisposeAndNullControllers();
873  throw;
874  }
875  }
876  catch (Exception e)
877  {
878  // don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
879  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
880  {
881  var originalChatTask = chatTask;
882  async Task ChainChatTaskWithErrorMessage()
883  {
884  await originalChatTask.ConfigureAwait(false);
885  await chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false);
886  }
887 
888  chatTask = ChainChatTaskWithErrorMessage();
889  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
890  }
891 
892  throw;
893  }
894  finally
895  {
896  // finish the chat task that's in flight
897  try
898  {
899  await chatTask.ConfigureAwait(false);
900  }
901  catch (OperationCanceledException) { }
902  }
903  }
904  #pragma warning restore CA1502
905 
907  public async Task Launch(CancellationToken cancellationToken)
908  {
909  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
910  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
911  }
912 
914  public async Task ResetRebootState(CancellationToken cancellationToken)
915  {
916  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
917  {
918  if (!Running)
919  return;
920  var toClear = AlphaIsActive ? alphaServer : bravoServer;
921  if (toClear != null)
922  toClear.ResetRebootState();
923  }
924  }
925 
927  public async Task Restart(bool graceful, CancellationToken cancellationToken)
928  {
929  logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
930  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
931  {
932  if (!graceful || !Running)
933  {
934  Task chatTask;
935  if (Running)
936  {
937  chatTask = chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken);
938  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
939  }
940  else
941  chatTask = Task.CompletedTask;
942  await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
943  await chatTask.ConfigureAwait(false);
944  }
945 
946  var toReboot = AlphaIsActive ? alphaServer : bravoServer;
947  if (toReboot != null)
948  {
949  if (!await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false))
950  logger.LogWarning("Unable to send reboot state change event!");
951  }
952  }
953  }
954 
956  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
957  {
958  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
959  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
960  }
961 
963  public async Task StartAsync(CancellationToken cancellationToken)
964  {
965  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
966  if (!autoStart && reattachInfo == null)
967  return;
968 
969  long? adminUserId = null;
970 
971  await databaseContextFactory.UseContext(async db => adminUserId = await db.Users
972  .Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant())
973  .Select(x => x.Id)
974  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
975  var job = new Models.Job
976  {
977  StartedBy = new Models.User
978  {
979  Id = adminUserId.Value
980  },
981  Instance = new Models.Instance
982  {
983  Id = instance.Id
984  },
985  Description = "Instance startup watchdog launch",
986  CancelRight = (ulong)DreamDaemonRights.Shutdown,
987  CancelRightsType = RightsType.DreamDaemon
988  };
989  await jobManager.RegisterOperation(job, async (j, databaseContext, progressFunction, ct) =>
990  {
991  using (await SemaphoreSlimContext.Lock(semaphore, ct).ConfigureAwait(false))
992  await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
993  }, cancellationToken).ConfigureAwait(false);
994  }
995 
997  public async Task StopAsync(CancellationToken cancellationToken)
998  {
999  if (releaseServers && Running)
1000  {
1001  await StopMonitor().ConfigureAwait(false);
1002 
1003  var reattachInformation = new WatchdogReattachInformation
1004  {
1005  AlphaIsActive = AlphaIsActive
1006  };
1007  reattachInformation.Alpha = alphaServer?.Release();
1008  reattachInformation.Bravo = bravoServer?.Release();
1009  await reattachInfoHandler.Save(reattachInformation, cancellationToken).ConfigureAwait(false);
1010  }
1011 
1012  await Terminate(false, cancellationToken).ConfigureAwait(false);
1013  }
1014 
1016  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
1017  {
1018  string results;
1019  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1020  {
1021  if (!Running)
1022  return true;
1023 
1024  var builder = new StringBuilder(Constants.DMTopicEvent);
1025  builder.Append('&');
1026  var notification = new EventNotification
1027  {
1028  Type = eventType,
1029  Parameters = parameters
1030  };
1031  var json = JsonConvert.SerializeObject(notification);
1032  builder.Append(byondTopicSender.SanitizeString(Constants.DMParameterData));
1033  builder.Append('=');
1034  builder.Append(byondTopicSender.SanitizeString(json));
1035 
1036  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1037  results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
1038  }
1039 
1040  if (results == Constants.DMResponseSuccess)
1041  return true;
1042 
1043  List<Response> responses;
1044  try
1045  {
1046  responses = JsonConvert.DeserializeObject<List<Response>>(results);
1047  }
1048  catch
1049  {
1050  logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
1051  return true;
1052  }
1053 
1054  await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
1055 
1056  return true;
1057  }
1058 
1060  public async Task<string> HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
1061  {
1062  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1063  {
1064  if (!Running)
1065  return "ERROR: Server offline!";
1066 
1067  var commandObject = new ChatCommand
1068  {
1069  Command = commandName,
1070  Params = arguments,
1071  User = sender
1072  };
1073 
1074  var json = JsonConvert.SerializeObject(commandObject, new JsonSerializerSettings
1075  {
1076  ContractResolver = new CamelCasePropertyNamesContractResolver()
1077  });
1078 
1079  var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json));
1080 
1081  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1082  return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
1083  }
1084  }
1085 
1087  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
1088  {
1089  releaseServers = true;
1090  if (Running)
1091  await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false);
1092  }
1093  }
1094 }
bool InactiveServerCritFail
If the inactive server is in an unrecoverable state
Definition: MonitorState.cs:18
Manages the runtime of Jobs
Definition: IJobManager.cs:12
-
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Watchdog
Definition: Watchdog.cs:71
-
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
Definition: Watchdog.cs:894
+
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Watchdog
Definition: Watchdog.cs:73
+
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
Definition: Watchdog.cs:907
- -
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:101
- +
Represents a chat command to be handled by DD
Definition: ChatCommand.cs:8
+
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:103
+
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
MonitorAction NextAction
The next MonitorAction to take in Watchdog.MonitorLifetimes(System.Threading.CancellationToken) ...
Definition: MonitorState.cs:23
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
-
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Definition: Watchdog.cs:942
+
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Definition: Watchdog.cs:956
-
bool releaseServers
If the servers should be released instead of shutdown
Definition: Watchdog.cs:145
-
async Task Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
Definition: Watchdog.cs:914
+
bool releaseServers
If the servers should be released instead of shutdown
Definition: Watchdog.cs:148
+
async Task Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
Definition: Watchdog.cs:927
Consumes EventTypes and takes the appropriate actions
Task< LaunchResult > LaunchResult
A Task that completes when DreamDaemon starts pumping the windows message queue after loading a ...
@@ -117,94 +117,94 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
void EnableCustomChatCommands()
Enables the reading of custom chat commands from the ISessionController
Task< bool > SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
Attempts to change the current RebootState to newRebootState
- +
const string DMTopicChatCommand
Chat command
Definition: Constants.cs:41
bool ClosePortOnReboot
If the port should be rotated off when the world reboots
Represents a server User
Definition: User.cs:10
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
- +
const string DMTopicEvent
Notify of an EventType
Definition: Constants.cs:46
-
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:697
+
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:708
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
-
Task monitorTask
The Task running the monitor loop
Definition: Watchdog.cs:126
+
Task monitorTask
The Task running the monitor loop
Definition: Watchdog.cs:128
-
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently Running triggers a graceful restart ...
Definition: Watchdog.cs:676
-
async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: Watchdog.cs:1072
+
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently Running triggers a graceful restart ...
Definition: Watchdog.cs:687
+
async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: Watchdog.cs:1087
For managing connected chat services
Definition: IChat.cs:13
-
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
+
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:108
Handles communication with a DreamDaemon IProcess
bool IsPrimary
If the IDmbProvider.PrimaryDirectory of Dmb is being used
-
readonly IEventConsumer eventConsumer
The IEventConsumer for the Watchdog
Definition: Watchdog.cs:81
+
readonly IEventConsumer eventConsumer
The IEventConsumer for the Watchdog
Definition: Watchdog.cs:83
void SetHighPriority()
Set&#39;s the owned System.Diagnostics.Process.PriorityClass to System.Diagnostics.ProcessPriorityClass.AboveNormal
bool TerminationWasRequested
If the DreamDaemon instance sent a
-
async Task< string > HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
Definition: Watchdog.cs:1045
+
async Task< string > HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
Definition: Watchdog.cs:1060
-
void DisposeAndNullControllers()
Call IDisposable.Dispose on alphaServer and bravoServer and set them to
Definition: Watchdog.cs:213
+
void DisposeAndNullControllers()
Call IDisposable.Dispose on alphaServer and bravoServer and set them to
Definition: Watchdog.cs:216
bool RebootingInactiveServer
If the inactive server is being rebooted
Definition: MonitorState.cs:13
-
readonly IJobManager jobManager
The IJobManager for the Watchdog
Definition: Watchdog.cs:86
+
readonly IJobManager jobManager
The IJobManager for the Watchdog
Definition: Watchdog.cs:88
DreamDaemonRights
Rights for Models.DreamDaemon
-
readonly Api.Models.Instance instance
The Api.Models.Instance for the Watchdog
Definition: Watchdog.cs:111
-
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IGitHubClientFactory gitHubClientFactory, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:106
+
readonly Api.Models.Instance instance
The Api.Models.Instance for the Watchdog
Definition: Watchdog.cs:113
+
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IGitHubClientFactory gitHubClientFactory, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:112
-
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
Definition: Watchdog.cs:662
+
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
Definition: Watchdog.cs:674
-
async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
Definition: Watchdog.cs:901
+
async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
Definition: Watchdog.cs:914
ReattachInformation Alpha
ReattachInformation for the Alpha session
-
async Task MonitorLifetimes(CancellationToken cancellationToken)
The loop that watches the watchdog
Definition: Watchdog.cs:499
+
async Task MonitorLifetimes(CancellationToken cancellationToken)
The loop that watches the watchdog
Definition: Watchdog.cs:510
-
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for the Watchdog
Definition: Watchdog.cs:56
- -
async Task StopAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:983
+
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for the Watchdog
Definition: Watchdog.cs:58
+ +
async Task StopAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:997
MonitorActivationReason
Reasons for the monitor to wake up
Task< bool > SetPort(ushort newPort, CancellationToken cancellatonToken)
Causes the world to start listening on a newPort
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon
ReattachInformation Release()
Releases the IProcess without terminating it. Also calls System.IDisposable.Dispose ...
-
readonly IChat chat
The IChat for the Watchdog
Definition: Watchdog.cs:51
+
readonly IChat chat
The IChat for the Watchdog
Definition: Watchdog.cs:53
- +
ISessionController ActiveServer
The active ISessionController
Definition: MonitorState.cs:29
- +
Constants used for communication with the DMAPI
Definition: Constants.cs:6
- +
ISessionController InactiveServer
The inactive ISessionController
Definition: MonitorState.cs:35
- -
async Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
Definition: Watchdog.cs:1001
-
Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger< Watchdog > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Construct a IWatchdog
Definition: Watchdog.cs:164
+
const string DMResponseSuccess
Generic OK response
Definition: Constants.cs:26
+
async Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
Definition: Watchdog.cs:1016
+
Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger< Watchdog > logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
Construct a IWatchdog
Definition: Watchdog.cs:167
-
readonly bool autoStart
If the Watchdog should LaunchImplNoLock(bool, bool, WatchdogReattachInformation, CancellationToken) i...
Definition: Watchdog.cs:116
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:949
-
ISessionController alphaServer
Server designation alpha
Definition: Watchdog.cs:136
+
readonly bool autoStart
If the Watchdog should LaunchImplNoLock(bool, bool, WatchdogReattachInformation, CancellationToken) i...
Definition: Watchdog.cs:118
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:963
+
ISessionController alphaServer
Server designation alpha
Definition: Watchdog.cs:138
Handles Commands.ICommands that map to those defined in a IJsonTrackingContext
-
async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
Handles the actions to take when the monitor has to "wake up"
Definition: Watchdog.cs:256
+
async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
Handles the actions to take when the monitor has to "wake up"
Definition: Watchdog.cs:260
-
readonly IReattachInfoHandler reattachInfoHandler
The IReattachInfoHandler for the Watchdog
Definition: Watchdog.cs:66
+
readonly IReattachInfoHandler reattachInfoHandler
The IReattachInfoHandler for the Watchdog
Definition: Watchdog.cs:68
Task< string > SendCommand(string command, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
ReattachInformation Bravo
ReattachInformation for the Bravo session
-
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock semaphore
Definition: Watchdog.cs:228
-
readonly IDmbFactory dmbFactory
The IDmbFactory for the Watchdog
Definition: Watchdog.cs:61
+
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock semaphore
Definition: Watchdog.cs:231
+
readonly IDmbFactory dmbFactory
The IDmbFactory for the Watchdog
Definition: Watchdog.cs:63
bool Match(DreamDaemonLaunchParameters otherParameters)
Check if we match a given set of otherParameters
Represents the lifetime of a IRestartHandler registration
-
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
Definition: Watchdog.cs:121
+
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
Definition: Watchdog.cs:123
MonitorAction
The action for the monitor loop to take when control is returned to it
Definition: MonitorAction.cs:6
-
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the Watchdog
Definition: Watchdog.cs:91
-
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the Watchdog
Definition: Watchdog.cs:96
- +
readonly IRestartRegistration restartRegistration
The IRestartRegistration for the Watchdog
Definition: Watchdog.cs:93
+
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the Watchdog
Definition: Watchdog.cs:98
+
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
-
TaskCompletionSource< object > activeParametersUpdated
TaskCompletionSource<TResult> that completes when ActiveLaunchParameters are changed and we are Runni...
Definition: Watchdog.cs:131
+
TaskCompletionSource< object > activeParametersUpdated
TaskCompletionSource<TResult> that completes when ActiveLaunchParameters are changed and we are Runni...
Definition: Watchdog.cs:133
The (absolute) state of the Watchdog
Definition: MonitorState.cs:8
Handles saving and loading WatchdogReattachInformation
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
-
ISessionController bravoServer
Server designation bravo
Definition: Watchdog.cs:140
+
ISessionController bravoServer
Server designation bravo
Definition: Watchdog.cs:143
void ResetRebootState()
Changes RebootState to Components.Watchdog.RebootState.Normal without telling the DMAPI ...
-
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the Watchdog
Definition: Watchdog.cs:76
+
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the Watchdog
Definition: Watchdog.cs:78
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:8
diff --git a/_components_2_watchdog_2_watchdog_factory_8cs_source.html b/_components_2_watchdog_2_watchdog_factory_8cs_source.html index 671801a97c..8ba096e733 100644 --- a/_components_2_watchdog_2_watchdog_factory_8cs_source.html +++ b/_components_2_watchdog_2_watchdog_factory_8cs_source.html @@ -116,7 +116,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the WatchdogFactory
- +
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the WatchdogFactory
diff --git a/_configuration_8cs_source.html b/_configuration_8cs_source.html index ef376e223d..80e7fcb964 100644 --- a/_configuration_8cs_source.html +++ b/_configuration_8cs_source.html @@ -92,46 +92,46 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Configuration.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.IO;
6 using System.Linq;
7 using System.Runtime.InteropServices;
8 using System.Security.Cryptography;
9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
14 using Tgstation.Server.Host.IO;
16 
17 namespace Tgstation.Server.Host.Components.StaticFiles
18 {
21  {
22  const string CodeModificationsSubdirectory = "CodeModifications";
23  const string EventScriptsSubdirectory = "EventScripts";
24  const string GameStaticFilesSubdirectory = "GameStaticFiles";
25 
29  const string StaticIgnoreFile = ".tgsignore";
30 
31  const string CodeModificationsHeadFile = "HeadInclude.dm";
32  const string CodeModificationsTailFile = "TailInclude.dm";
33 
34  static readonly IReadOnlyDictionary<EventType, string> EventTypeScriptFileNameMap = new Dictionary<EventType, string>
35  {
36  { EventType.CompileStart, "PreCompile" },
37  { EventType.CompileComplete, "PostCompile" },
38  { EventType.RepoPreSynchronize, "PreSynchronize" }
39  };
40 
45 
50 
55 
60 
65 
70 
74  readonly ILogger<Configuration> logger;
75 
79  readonly SemaphoreSlim semaphore;
80 
91  public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, ILogger<Configuration> logger)
92  {
93  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
94  this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
95  this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
96  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
97  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
98  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
99  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
100 
101  semaphore = new SemaphoreSlim(1);
102  }
103 
105  public void Dispose() => semaphore.Dispose();
106 
111  string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
112 
118  async Task EnsureDirectories(CancellationToken cancellationToken)
119  {
120  async Task ValidateStaticFolder()
121  {
122  await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken).ConfigureAwait(false);
123  var staticIgnorePath = StaticIgnorePath();
124  if(!await ioManager.FileExists(staticIgnorePath, cancellationToken).ConfigureAwait(false))
125  await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
126  }
127 
128  await Task.WhenAll(ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken), ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken), ValidateStaticFolder()).ConfigureAwait(false);
129  }
130 
132  public async Task<ServerSideModifications> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
133  {
134 
135  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
136  {
137  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
138 
139  //just assume no other fs race conditions here
140  var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken);
141  var headFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsHeadFile), cancellationToken);
142  var tailFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsTailFile), cancellationToken);
143  var copyTask = ioManager.CopyDirectory(CodeModificationsSubdirectory, destination, null, cancellationToken);
144 
145  await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask).ConfigureAwait(false);
146 
147  if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
148  return null;
149 
150  if (dmeExistsTask.Result)
151  return new ServerSideModifications(null, null, true);
152 
153  if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
154  return null;
155 
156  string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath);
157 
158  return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false);
159  }
160  }
161 
162  string ValidateConfigRelativePath(string configurationRelativePath)
163  {
164  var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
165  if (nullOrEmptyCheck)
166  configurationRelativePath = ".";
167  if (configurationRelativePath[0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
168  configurationRelativePath = '.' + configurationRelativePath;
169  var resolved = ioManager.ResolvePath(configurationRelativePath);
170  var local = !nullOrEmptyCheck ? ioManager.ResolvePath(".") : null;
171  if (!nullOrEmptyCheck && resolved.Length < local.Length) //.. fuccbois
172  throw new InvalidOperationException("Attempted to access file outside of configuration manager!");
173  return resolved;
174  }
175 
177  public async Task<IReadOnlyList<ConfigurationFile>> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
178  {
179  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
180  var path = ValidateConfigRelativePath(configurationRelativePath);
181 
182  if (configurationRelativePath == null)
183  configurationRelativePath = "/";
184 
185  List<ConfigurationFile> result = new List<ConfigurationFile>();
186 
187  void ListImpl()
188  {
189  var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
190  try
191  {
192  result.AddRange(enumerator.Select(x => new ConfigurationFile
193  {
194  IsDirectory = true,
195  Path = ioManager.ConcatPath(configurationRelativePath, x),
196  }));
197  }
198  catch (IOException e)
199  {
200  logger.LogDebug("IOException while writing {0}: {1}", path, e);
201  result = null;
202  return;
203  }
204  enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
205  result.AddRange(enumerator.Select(x => new ConfigurationFile
206  {
207  IsDirectory = false,
208  Path = ioManager.ConcatPath(configurationRelativePath, x),
209  }));
210  }
211 
212  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
213  if (systemIdentity == null)
214  ListImpl();
215  else
216  await systemIdentity.RunImpersonated(ListImpl, cancellationToken).ConfigureAwait(false);
217 
218  return result;
219  }
220 
222  public async Task<ConfigurationFile> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
223  {
224  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
225  var path = ValidateConfigRelativePath(configurationRelativePath);
226 
227  ConfigurationFile result = null;
228 
229  void ReadImpl()
230  {
231  lock (this)
232  try
233  {
234  var content = synchronousIOManager.ReadFile(path);
235  string sha1String;
236 #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1.
237  using (var sha1 = new SHA1Managed())
238 #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1.
239  sha1String = String.Join("", sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
240  result = new ConfigurationFile
241  {
242  Content = content,
243  IsDirectory = false,
244  LastReadHash = sha1String,
245  AccessDenied = false,
246  Path = configurationRelativePath
247  };
248  }
249  catch (UnauthorizedAccessException)
250  {
251  //this happens on windows, dunno about linux
252  bool isDirectory;
253  try
254  {
255  isDirectory = synchronousIOManager.IsDirectory(path);
256  }
257  catch
258  {
259  isDirectory = false;
260  }
261 
262  result = new ConfigurationFile
263  {
264  Path = configurationRelativePath
265  };
266  if (!isDirectory)
267  result.AccessDenied = true;
268  else
269  result.IsDirectory = true;
270  }
271  }
272 
273  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
274  if (systemIdentity == null)
275  await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
276  else
277  await systemIdentity.RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(false);
278 
279  return result;
280  }
281 
283  public async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
284  {
285  async Task<IReadOnlyList<string>> GetIgnoreFiles()
286  {
287  var ignoreFileBytes = await ioManager.ReadAllBytes(StaticIgnorePath(), cancellationToken).ConfigureAwait(false);
288  var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
289 
290  var results = new List<string> { StaticIgnoreFile };
291 
292  //we don't want to lose trailing whitespace on linux
293  using (var reader = new StringReader(ignoreFileText))
294  {
295  cancellationToken.ThrowIfCancellationRequested();
296  var line = await reader.ReadLineAsync().ConfigureAwait(false);
297  if (!String.IsNullOrEmpty(line))
298  results.Add(line);
299  }
300 
301  return results;
302  };
303 
304  IReadOnlyList<string> ignoreFiles;
305 
306  async Task SymlinkBase(bool files)
307  {
308  Task<IReadOnlyList<string>> task;
309  if (files)
310  task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken);
311  else
312  task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken);
313  var entries = await task.ConfigureAwait(false);
314 
315  await Task.WhenAll(entries.Select(async x =>
316  {
317  var fileName = ioManager.GetFileName(x);
318 
319  bool ignored;
320  if (platformIdentifier.IsWindows)
321  //need to normalize
322  ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant());
323  else
324  ignored = ignoreFiles.Any(y => fileName == y);
325 
326  if (ignored)
327  {
328  logger.LogTrace("Ignoring static file {0}...", fileName);
329  return;
330  }
331 
332  var destPath = ioManager.ConcatPath(destination, fileName);
333  logger.LogTrace("Symlinking {0} to {1}...", x, destPath);
334  var fileExistsTask = ioManager.FileExists(destPath, cancellationToken);
335  if (await ioManager.DirectoryExists(destPath, cancellationToken).ConfigureAwait(false))
336  await ioManager.DeleteDirectory(destPath, cancellationToken).ConfigureAwait(false);
337  var fileExists = await fileExistsTask.ConfigureAwait(false);
338  if (fileExists)
339  await ioManager.DeleteFile(destPath, cancellationToken).ConfigureAwait(false);
340  await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ResolvePath(destPath), cancellationToken).ConfigureAwait(false);
341  })).ConfigureAwait(false);
342  }
343 
344  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
345  {
346  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
347  ignoreFiles = await GetIgnoreFiles().ConfigureAwait(false);
348  await Task.WhenAll(SymlinkBase(true), SymlinkBase(false)).ConfigureAwait(false);
349  }
350  }
351 
353  public async Task<ConfigurationFile> Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken)
354  {
355  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
356  var path = ValidateConfigRelativePath(configurationRelativePath);
357 
358  ConfigurationFile result = null;
359 
360  void WriteImpl()
361  {
362  lock (this)
363  try
364  {
365  var fileHash = previousHash;
366  var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken);
367  if (!success)
368  return;
369  if (data != null)
370  postWriteHandler.HandleWrite(path);
371  result = new ConfigurationFile
372  {
373  Content = data,
374  IsDirectory = false,
375  LastReadHash = fileHash,
376  AccessDenied = false,
377  Path = configurationRelativePath
378  };
379  }
380  catch (UnauthorizedAccessException)
381  {
382  //this happens on windows, dunno about linux
383  bool isDirectory;
384  try
385  {
386  isDirectory = synchronousIOManager.IsDirectory(path);
387  }
388  catch
389  {
390  isDirectory = false;
391  }
392 
393  result = new ConfigurationFile
394  {
395  Path = configurationRelativePath
396  };
397  if (!isDirectory)
398  result.AccessDenied = true;
399  else
400  result.IsDirectory = true;
401  }
402  }
403 
404  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
405  if (systemIdentity == null)
406  await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
407  else
408  await systemIdentity.RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(false);
409 
410  return result;
411  }
412 
414  public async Task<bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
415  {
416  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
417  var path = ValidateConfigRelativePath(configurationRelativePath);
418 
419  bool? result = null;
420  void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
421 
422  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
423  if (systemIdentity == null)
424  await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
425  else
426  await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false);
427 
428  return result.Value;
429  }
430 
432  public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
433 
435  public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
436 
438  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
439  {
440  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
441 
442  if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName))
443  return true;
444 
445  //always execute in serial
446  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
447  {
448  var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, cancellationToken).ConfigureAwait(false);
449  var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
450 
451  foreach (var I in files.Select(x => ioManager.GetFileName(x)).Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)))
452  using (var script = processExecutor.LaunchProcess(ioManager.ConcatPath(resolvedScriptsDir, I), resolvedScriptsDir, String.Join(' ', parameters), noShellExecute: true))
453  using (cancellationToken.Register(() => script.Terminate()))
454  {
455  var exitCode = await script.Lifetime.ConfigureAwait(false);
456  cancellationToken.ThrowIfCancellationRequested();
457  if (exitCode != 0)
458  return false;
459  }
460  }
461  return true;
462  }
463 
465  public async Task<bool> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
466  {
467  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
468  var path = ValidateConfigRelativePath(configurationRelativePath);
469 
470  var result = false;
471  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
472  {
473  void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
474 
475  if (systemIdentity != null)
476  await systemIdentity.RunImpersonated(CheckDeleteImpl, cancellationToken).ConfigureAwait(false);
477  else
478  CheckDeleteImpl();
479  }
480  return result;
481  }
482  }
483 }
readonly ISynchronousIOManager synchronousIOManager
The ISynchronousIOManager for Configuration
- +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.IO;
6 using System.Linq;
7 using System.Security.Cryptography;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
15 
16 namespace Tgstation.Server.Host.Components.StaticFiles
17 {
20  {
21  const string CodeModificationsSubdirectory = "CodeModifications";
22  const string EventScriptsSubdirectory = "EventScripts";
23  const string GameStaticFilesSubdirectory = "GameStaticFiles";
24 
28  const string StaticIgnoreFile = ".tgsignore";
29 
30  const string CodeModificationsHeadFile = "HeadInclude.dm";
31  const string CodeModificationsTailFile = "TailInclude.dm";
32 
33  static readonly IReadOnlyDictionary<EventType, string> EventTypeScriptFileNameMap = new Dictionary<EventType, string>
34  {
35  { EventType.CompileStart, "PreCompile" },
36  { EventType.CompileComplete, "PostCompile" },
37  { EventType.RepoPreSynchronize, "PreSynchronize" }
38  };
39 
44 
49 
54 
59 
64 
69 
73  readonly ILogger<Configuration> logger;
74 
78  readonly SemaphoreSlim semaphore;
79 
90  public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, ILogger<Configuration> logger)
91  {
92  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
93  this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
94  this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
95  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
96  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
97  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
98  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
99 
100  semaphore = new SemaphoreSlim(1);
101  }
102 
104  public void Dispose() => semaphore.Dispose();
105 
110  string StaticIgnorePath() => ioManager.ConcatPath(GameStaticFilesSubdirectory, StaticIgnoreFile);
111 
117  async Task EnsureDirectories(CancellationToken cancellationToken)
118  {
119  async Task ValidateStaticFolder()
120  {
121  await ioManager.CreateDirectory(GameStaticFilesSubdirectory, cancellationToken).ConfigureAwait(false);
122  var staticIgnorePath = StaticIgnorePath();
123  if(!await ioManager.FileExists(staticIgnorePath, cancellationToken).ConfigureAwait(false))
124  await ioManager.WriteAllBytes(staticIgnorePath, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
125  }
126 
127  await Task.WhenAll(ioManager.CreateDirectory(CodeModificationsSubdirectory, cancellationToken), ioManager.CreateDirectory(EventScriptsSubdirectory, cancellationToken), ValidateStaticFolder()).ConfigureAwait(false);
128  }
129 
131  public async Task<ServerSideModifications> CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
132  {
133  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
134  {
135  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
136 
137  // just assume no other fs race conditions here
138  var dmeExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, dmeFile), cancellationToken);
139  var headFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsHeadFile), cancellationToken);
140  var tailFileExistsTask = ioManager.FileExists(ioManager.ConcatPath(CodeModificationsSubdirectory, CodeModificationsTailFile), cancellationToken);
141  var copyTask = ioManager.CopyDirectory(CodeModificationsSubdirectory, destination, null, cancellationToken);
142 
143  await Task.WhenAll(dmeExistsTask, headFileExistsTask, tailFileExistsTask, copyTask).ConfigureAwait(false);
144 
145  if (!dmeExistsTask.Result && !headFileExistsTask.Result && !tailFileExistsTask.Result)
146  return null;
147 
148  if (dmeExistsTask.Result)
149  return new ServerSideModifications(null, null, true);
150 
151  if (!headFileExistsTask.Result && !tailFileExistsTask.Result)
152  return null;
153 
154  string IncludeLine(string filePath) => String.Format(CultureInfo.InvariantCulture, "#include \"{0}\"", filePath);
155 
156  return new ServerSideModifications(headFileExistsTask.Result ? IncludeLine(CodeModificationsHeadFile) : null, tailFileExistsTask.Result ? IncludeLine(CodeModificationsTailFile) : null, false);
157  }
158  }
159 
160  string ValidateConfigRelativePath(string configurationRelativePath)
161  {
162  var nullOrEmptyCheck = String.IsNullOrEmpty(configurationRelativePath);
163  if (nullOrEmptyCheck)
164  configurationRelativePath = ".";
165  if (configurationRelativePath[0] == Path.DirectorySeparatorChar || configurationRelativePath[0] == Path.AltDirectorySeparatorChar)
166  configurationRelativePath = '.' + configurationRelativePath;
167  var resolved = ioManager.ResolvePath(configurationRelativePath);
168  var local = !nullOrEmptyCheck ? ioManager.ResolvePath(".") : null;
169  if (!nullOrEmptyCheck && resolved.Length < local.Length) // .. fuccbois
170  throw new InvalidOperationException("Attempted to access file outside of configuration manager!");
171  return resolved;
172  }
173 
175  public async Task<IReadOnlyList<ConfigurationFile>> ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
176  {
177  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
178  var path = ValidateConfigRelativePath(configurationRelativePath);
179 
180  if (configurationRelativePath == null)
181  configurationRelativePath = "/";
182 
183  List<ConfigurationFile> result = new List<ConfigurationFile>();
184 
185  void ListImpl()
186  {
187  var enumerator = synchronousIOManager.GetDirectories(path, cancellationToken);
188  try
189  {
190  result.AddRange(enumerator.Select(x => new ConfigurationFile
191  {
192  IsDirectory = true,
193  Path = ioManager.ConcatPath(configurationRelativePath, x),
194  }));
195  }
196  catch (IOException e)
197  {
198  logger.LogDebug("IOException while writing {0}: {1}", path, e);
199  result = null;
200  return;
201  }
202 
203  enumerator = synchronousIOManager.GetFiles(path, cancellationToken);
204  result.AddRange(enumerator.Select(x => new ConfigurationFile
205  {
206  IsDirectory = false,
207  Path = ioManager.ConcatPath(configurationRelativePath, x),
208  }));
209  }
210 
211  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
212  if (systemIdentity == null)
213  ListImpl();
214  else
215  await systemIdentity.RunImpersonated(ListImpl, cancellationToken).ConfigureAwait(false);
216 
217  return result;
218  }
219 
221  public async Task<ConfigurationFile> Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
222  {
223  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
224  var path = ValidateConfigRelativePath(configurationRelativePath);
225 
226  ConfigurationFile result = null;
227 
228  void ReadImpl()
229  {
230  lock (this)
231  try
232  {
233  var content = synchronousIOManager.ReadFile(path);
234  string sha1String;
235 #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1.
236  using (var sha1 = new SHA1Managed())
237 #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1.
238  sha1String = String.Join(String.Empty, sha1.ComputeHash(content).Select(b => b.ToString("x2", CultureInfo.InvariantCulture)));
239  result = new ConfigurationFile
240  {
241  Content = content,
242  IsDirectory = false,
243  LastReadHash = sha1String,
244  AccessDenied = false,
245  Path = configurationRelativePath
246  };
247  }
248  catch (UnauthorizedAccessException)
249  {
250  // this happens on windows, dunno about linux
251  bool isDirectory;
252  try
253  {
254  isDirectory = synchronousIOManager.IsDirectory(path);
255  }
256  catch
257  {
258  isDirectory = false;
259  }
260 
261  result = new ConfigurationFile
262  {
263  Path = configurationRelativePath
264  };
265  if (!isDirectory)
266  result.AccessDenied = true;
267  else
268  result.IsDirectory = true;
269  }
270  }
271 
272  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
273  if (systemIdentity == null)
274  await Task.Factory.StartNew(ReadImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
275  else
276  await systemIdentity.RunImpersonated(ReadImpl, cancellationToken).ConfigureAwait(false);
277 
278  return result;
279  }
280 
282  public async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
283  {
284  async Task<IReadOnlyList<string>> GetIgnoreFiles()
285  {
286  var ignoreFileBytes = await ioManager.ReadAllBytes(StaticIgnorePath(), cancellationToken).ConfigureAwait(false);
287  var ignoreFileText = Encoding.UTF8.GetString(ignoreFileBytes);
288 
289  var results = new List<string> { StaticIgnoreFile };
290 
291  // we don't want to lose trailing whitespace on linux
292  using (var reader = new StringReader(ignoreFileText))
293  {
294  cancellationToken.ThrowIfCancellationRequested();
295  var line = await reader.ReadLineAsync().ConfigureAwait(false);
296  if (!String.IsNullOrEmpty(line))
297  results.Add(line);
298  }
299 
300  return results;
301  }
302 
303  IReadOnlyList<string> ignoreFiles;
304 
305  async Task SymlinkBase(bool files)
306  {
307  Task<IReadOnlyList<string>> task;
308  if (files)
309  task = ioManager.GetFiles(GameStaticFilesSubdirectory, cancellationToken);
310  else
311  task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken);
312  var entries = await task.ConfigureAwait(false);
313 
314  await Task.WhenAll(entries.Select(async x =>
315  {
316  var fileName = ioManager.GetFileName(x);
317 
318  // need to normalize
319  bool ignored;
320  if (platformIdentifier.IsWindows)
321  ignored = ignoreFiles.Any(y => fileName.ToUpperInvariant() == y.ToUpperInvariant());
322  else
323  ignored = ignoreFiles.Any(y => fileName == y);
324 
325  if (ignored)
326  {
327  logger.LogTrace("Ignoring static file {0}...", fileName);
328  return;
329  }
330 
331  var destPath = ioManager.ConcatPath(destination, fileName);
332  logger.LogTrace("Symlinking {0} to {1}...", x, destPath);
333  var fileExistsTask = ioManager.FileExists(destPath, cancellationToken);
334  if (await ioManager.DirectoryExists(destPath, cancellationToken).ConfigureAwait(false))
335  await ioManager.DeleteDirectory(destPath, cancellationToken).ConfigureAwait(false);
336  var fileExists = await fileExistsTask.ConfigureAwait(false);
337  if (fileExists)
338  await ioManager.DeleteFile(destPath, cancellationToken).ConfigureAwait(false);
339  await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ResolvePath(destPath), cancellationToken).ConfigureAwait(false);
340  })).ConfigureAwait(false);
341  }
342 
343  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
344  {
345  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
346  ignoreFiles = await GetIgnoreFiles().ConfigureAwait(false);
347  await Task.WhenAll(SymlinkBase(true), SymlinkBase(false)).ConfigureAwait(false);
348  }
349  }
350 
352  public async Task<ConfigurationFile> Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken)
353  {
354  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
355  var path = ValidateConfigRelativePath(configurationRelativePath);
356 
357  ConfigurationFile result = null;
358 
359  void WriteImpl()
360  {
361  lock (this)
362  try
363  {
364  var fileHash = previousHash;
365  var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken);
366  if (!success)
367  return;
368  if (data != null)
369  postWriteHandler.HandleWrite(path);
370  result = new ConfigurationFile
371  {
372  Content = data,
373  IsDirectory = false,
374  LastReadHash = fileHash,
375  AccessDenied = false,
376  Path = configurationRelativePath
377  };
378  }
379  catch (UnauthorizedAccessException)
380  {
381  // this happens on windows, dunno about linux
382  bool isDirectory;
383  try
384  {
385  isDirectory = synchronousIOManager.IsDirectory(path);
386  }
387  catch
388  {
389  isDirectory = false;
390  }
391 
392  result = new ConfigurationFile
393  {
394  Path = configurationRelativePath
395  };
396  if (!isDirectory)
397  result.AccessDenied = true;
398  else
399  result.IsDirectory = true;
400  }
401  }
402 
403  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
404  if (systemIdentity == null)
405  await Task.Factory.StartNew(WriteImpl, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
406  else
407  await systemIdentity.RunImpersonated(WriteImpl, cancellationToken).ConfigureAwait(false);
408 
409  return result;
410  }
411 
413  public async Task<bool> CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
414  {
415  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
416  var path = ValidateConfigRelativePath(configurationRelativePath);
417 
418  bool? result = null;
419  void DoCreate() => result = synchronousIOManager.CreateDirectory(path, cancellationToken);
420 
421  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
422  if (systemIdentity == null)
423  await Task.Factory.StartNew(DoCreate, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
424  else
425  await systemIdentity.RunImpersonated(DoCreate, cancellationToken).ConfigureAwait(false);
426 
427  return result.Value;
428  }
429 
431  public Task StartAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
432 
434  public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken);
435 
437  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
438  {
439  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
440 
441  if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName))
442  return true;
443 
444  // always execute in serial
445  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
446  {
447  var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, platformIdentifier.ScriptFileExtension, cancellationToken).ConfigureAwait(false);
448  var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
449 
450  foreach (var I in files.Select(x => ioManager.GetFileName(x)).Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)))
451  using (var script = processExecutor.LaunchProcess(ioManager.ConcatPath(resolvedScriptsDir, I), resolvedScriptsDir, String.Join(' ', parameters), noShellExecute: true))
452  using (cancellationToken.Register(() => script.Terminate()))
453  {
454  var exitCode = await script.Lifetime.ConfigureAwait(false);
455  cancellationToken.ThrowIfCancellationRequested();
456  if (exitCode != 0)
457  return false;
458  }
459  }
460 
461  return true;
462  }
463 
465  public async Task<bool> DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
466  {
467  await EnsureDirectories(cancellationToken).ConfigureAwait(false);
468  var path = ValidateConfigRelativePath(configurationRelativePath);
469 
470  var result = false;
471  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
472  {
473  void CheckDeleteImpl() => result = synchronousIOManager.DeleteDirectory(path);
474 
475  if (systemIdentity != null)
476  await systemIdentity.RunImpersonated(CheckDeleteImpl, cancellationToken).ConfigureAwait(false);
477  else
478  CheckDeleteImpl();
479  }
480 
481  return result;
482  }
483  }
484 }
readonly ISynchronousIOManager synchronousIOManager
The ISynchronousIOManager for Configuration
+
async Task< bool > DeleteDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Attempt to delete an empty directory at configurationRelativePath
-
-
async Task< ServerSideModifications > CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
Copies all files in the CodeModifications directory to destination
-
async Task EnsureDirectories(CancellationToken cancellationToken)
Ensures standard configuration directories exist
-
async Task< bool > CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Create an empty directory at configurationRelativePath
-
async Task< ConfigurationFile > Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken)
Writes to a given configurationRelativePath
+
Handles changing file modes/permissions after writing
+
async Task< ServerSideModifications > CopyDMFilesTo(string dmeFile, string destination, CancellationToken cancellationToken)
Copies all files in the CodeModifications directory to destination
+
async Task EnsureDirectories(CancellationToken cancellationToken)
Ensures standard configuration directories exist
+
async Task< bool > CreateDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Create an empty directory at configurationRelativePath
+
async Task< ConfigurationFile > Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken)
Writes to a given configurationRelativePath
Represents a user on the current System.Runtime.InteropServices.OSPlatform
-
Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, ILogger< Configuration > logger)
Construct Configuration
-
async Task< IReadOnlyList< ConfigurationFile > > ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Get ConfigurationFile for all items in a given configurationRelativePath
+
Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IPlatformIdentifier platformIdentifier, ILogger< Configuration > logger)
Construct Configuration
+
async Task< IReadOnlyList< ConfigurationFile > > ListDirectory(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Get ConfigurationFile for all items in a given configurationRelativePath
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
-
readonly IProcessExecutor processExecutor
The IProcessExecutor for Configuration
+
readonly IProcessExecutor processExecutor
The IProcessExecutor for Configuration
bool IsDirectory
If Path represents a directory
-
readonly ILogger< Configuration > logger
The ILogger for Configuration
-
async Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
+
readonly ILogger< Configuration > logger
The ILogger for Configuration
+
async Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
-
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for Configuration
+
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for Configuration
For accessing the disk in a synchronous manner
-
string ValidateConfigRelativePath(string configurationRelativePath)
-
readonly ISymlinkFactory symlinkFactory
The ISymlinkFactory for Configuration
+
string ValidateConfigRelativePath(string configurationRelativePath)
+
readonly ISymlinkFactory symlinkFactory
The ISymlinkFactory for Configuration
Task RunImpersonated(Action action, CancellationToken cancellationToken)
Runs a given action in the context of the ISystemIdentity
-
readonly IIOManager ioManager
The IIOManager for Configuration
-
async Task< ConfigurationFile > Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Reads a given configurationRelativePath
-
readonly SemaphoreSlim semaphore
The SemaphoreSlim for Configuration
+
readonly IIOManager ioManager
The IIOManager for Configuration
+
async Task< ConfigurationFile > Read(string configurationRelativePath, ISystemIdentity systemIdentity, CancellationToken cancellationToken)
Reads a given configurationRelativePath
+
readonly SemaphoreSlim semaphore
The SemaphoreSlim for Configuration
Represents a game configuration file. Create and delete actions uncerimonuously overwrite/delete file...
For managing the Configuration directory
-
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for Configuration
+
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for Configuration
bool AccessDenied
If access to the ConfigurationFile file was denied for the operation
Interface for using filesystems
Definition: IIOManager.cs:11
-
async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
Symlinks all directories in the GameData directory to destination
+
async Task SymlinkStaticFilesTo(string destination, CancellationToken cancellationToken)
Symlinks all directories in the GameData directory to destination
For identifying the current platform
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
diff --git a/_configuration_client_8cs_source.html b/_configuration_client_8cs_source.html index fbba812d79..e7ed334084 100644 --- a/_configuration_client_8cs_source.html +++ b/_configuration_client_8cs_source.html @@ -92,19 +92,19 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ConfigurationClient.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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
12  {
20  readonly Instance instance;
21 
27  static string SanitizeGetPath(string path)
28  {
29  if (path == null)
30  path = String.Empty;
31  if (path.Length == 0 || path[0] != '/')
32  path = '/' + path;
33  return path;
34  }
35 
41  public ConfigurationClient(IApiClient apiClient, Instance instance)
42  {
43  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
44  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
45  }
46 
48  public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken);
49 
51  public Task<ConfigurationFile> CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create<ConfigurationFile, ConfigurationFile>(Routes.Configuration, directory, instance.Id, cancellationToken);
52 
54  public Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ConfigurationFile>>(Routes.List(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken);
55 
57  public Task<ConfigurationFile> Read(ConfigurationFile file, CancellationToken cancellationToken)
58  {
59  if (file == null)
60  throw new ArgumentNullException(nameof(file));
61  return apiClient.Read<ConfigurationFile>(Routes.ConfigurationFile + SanitizeGetPath(file.Path), instance.Id, cancellationToken);
62  }
63 
65  public Task<ConfigurationFile> Write(ConfigurationFile file, CancellationToken cancellationToken) => apiClient.Update<ConfigurationFile, ConfigurationFile>(Routes.Configuration, file ?? throw new ArgumentNullException(nameof(file)), instance.Id, cancellationToken);
66  }
67 }
const string Configuration
The Models.ConfigurationFile controller
Definition: Routes.cs:49
+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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
12  {
17 
21  readonly Instance instance;
22 
28  static string SanitizeGetPath(string path)
29  {
30  if (path == null)
31  path = String.Empty;
32  if (path.Length == 0 || path[0] != '/')
33  path = '/' + path;
34  return path;
35  }
36 
42  public ConfigurationClient(IApiClient apiClient, Instance instance)
43  {
44  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
45  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
46  }
47 
49  public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken);
50 
52  public Task<ConfigurationFile> CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create<ConfigurationFile, ConfigurationFile>(Routes.Configuration, directory, instance.Id, cancellationToken);
53 
55  public Task<IReadOnlyList<ConfigurationFile>> List(string directory, CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<ConfigurationFile>>(Routes.List(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken);
56 
58  public Task<ConfigurationFile> Read(ConfigurationFile file, CancellationToken cancellationToken)
59  {
60  if (file == null)
61  throw new ArgumentNullException(nameof(file));
62  return apiClient.Read<ConfigurationFile>(Routes.ConfigurationFile + SanitizeGetPath(file.Path), instance.Id, cancellationToken);
63  }
64 
66  public Task<ConfigurationFile> Write(ConfigurationFile file, CancellationToken cancellationToken) => apiClient.Update<ConfigurationFile, ConfigurationFile>(Routes.Configuration, file ?? throw new ArgumentNullException(nameof(file)), instance.Id, cancellationToken);
67  }
68 }
const string Configuration
The Models.ConfigurationFile controller
Definition: Routes.cs:49
readonly IApiClient apiClient
The IApiClient for the ConfigurationClient
-
Task< ConfigurationFile > Read(ConfigurationFile file, CancellationToken cancellationToken)
Read a ConfigurationFile file
+
Task< ConfigurationFile > Read(ConfigurationFile file, CancellationToken cancellationToken)
Read a ConfigurationFile file
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
+
readonly Instance instance
The Instance for the ConfigurationClient
+
static string SanitizeGetPath(string path)
Sanitize a ConfigurationFile path for use in a GET Uri
Represents a game configuration file. Create and delete actions uncerimonuously overwrite/delete file...
-
ConfigurationClient(IApiClient apiClient, Instance instance)
Construct a ConfigurationClient
+
ConfigurationClient(IApiClient apiClient, Instance instance)
Construct a ConfigurationClient
Metadata about a server instance
Definition: Instance.cs:9
diff --git a/_configuration_controller_8cs_source.html b/_configuration_controller_8cs_source.html index b1b7b44e70..ac24d6fd22 100644 --- a/_configuration_controller_8cs_source.html +++ b/_configuration_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ConfigurationController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.IO;
5 using System.Net;
6 using System.Threading;
7 using System.Threading.Tasks;
8 using Tgstation.Server.Api;
12 using Tgstation.Server.Host.IO;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.Configuration)]
22  public sealed class ConfigurationController : ModelController<ConfigurationFile>
23  {
28 
33 
42  public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger<ConfigurationController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
43  {
44  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
45  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
46  }
47 
54  bool ForbidDueToModeConflicts(string path, out ISystemIdentity systemIdentityToUse)
55  {
56  if (Instance.ConfigurationType == ConfigurationType.Disallowed || (Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite && AuthenticationContext.SystemIdentity == null) || (path != null && ioManager.PathContainsParentAccess(path)))
57  {
58  systemIdentityToUse = null;
59  return true;
60  }
61  systemIdentityToUse = Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite ? AuthenticationContext.SystemIdentity : null;
62  return false;
63  }
64 
66  [TgsAuthorize(ConfigurationRights.Write)]
67  public override async Task<IActionResult> Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
68  {
69  if (model == null)
70  throw new ArgumentNullException(nameof(model));
71  if (ForbidDueToModeConflicts(model.Path, out var systemIdentity))
72  return Forbid();
73 
74  var config = instanceManager.GetInstance(Instance).Configuration;
75  try
76  {
77  var newFile = await config.Write(model.Path, systemIdentity, model.Content, model.LastReadHash, cancellationToken).ConfigureAwait(false);
78  if (newFile == null)
79  return Conflict(new ErrorMessage
80  {
81  Message = "This file has been updated since you last viewed it!"
82  });
83 
84  newFile.Content = null;
85 
86  return model.LastReadHash == null ? (IActionResult)StatusCode((int)HttpStatusCode.Created, newFile) : Json(newFile);
87  }
88  catch(IOException e)
89  {
90  Logger.LogInformation("IOException while updating file {0}: {1}", model.Path, e);
91  return Conflict(new ErrorMessage
92  {
93  Message = e.Message
94  });
95  }
96  catch (NotImplementedException)
97  {
98  return StatusCode((int)HttpStatusCode.NotImplemented);
99  }
100  }
101 
108  [HttpGet(Routes.File + "/{*filePath}")]
109  [TgsAuthorize(ConfigurationRights.Read)]
110  public async Task<IActionResult> File(string filePath, CancellationToken cancellationToken)
111  {
112  if (ForbidDueToModeConflicts(filePath, out var systemIdentity))
113  return Forbid();
114 
115  try
116  {
117  var result = await instanceManager.GetInstance(Instance).Configuration.Read(filePath, systemIdentity, cancellationToken).ConfigureAwait(false);
118  if (result == null)
119  return StatusCode((int)HttpStatusCode.Gone);
120 
121  return Json(result);
122  }
123  catch (IOException e)
124  {
125  Logger.LogInformation("IOException while reading file {0}: {1}", filePath, e);
126  return Conflict(new ErrorMessage
127  {
128  Message = e.Message
129  });
130  }
131  catch (NotImplementedException)
132  {
133  return StatusCode((int)HttpStatusCode.NotImplemented);
134  }
135  }
136 
143  [HttpGet("List/{*directoryPath}")]
144  [TgsAuthorize(ConfigurationRights.List)]
145  public async Task<IActionResult> Directory(string directoryPath, CancellationToken cancellationToken)
146  {
147  if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity))
148  return Forbid();
149 
150  try
151  {
152  var result = await instanceManager.GetInstance(Instance).Configuration.ListDirectory(directoryPath, systemIdentity, cancellationToken).ConfigureAwait(false);
153  if (result == null)
154  return StatusCode((int)HttpStatusCode.Gone);
155 
156  return Json(result);
157  }
158  catch (NotImplementedException)
159  {
160  return StatusCode((int)HttpStatusCode.NotImplemented);
161  }
162  catch (UnauthorizedAccessException)
163  {
164  return Forbid();
165  }
166  }
167 
169  [TgsAuthorize(ConfigurationRights.List)]
170  public override Task<IActionResult> List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
171 
173  [TgsAuthorize(ConfigurationRights.Write)]
174  public override async Task<IActionResult> Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
175  {
176  if (model == null)
177  throw new ArgumentNullException(nameof(model));
178 
179  if (ForbidDueToModeConflicts(model.Path, out var systemIdentity))
180  return Forbid();
181 
182  try
183  {
184  model.IsDirectory = true;
185  return await instanceManager.GetInstance(Instance).Configuration.CreateDirectory(model.Path, systemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Json(model) : StatusCode((int)HttpStatusCode.Created, model);
186  }
187  catch (NotImplementedException)
188  {
189  return StatusCode((int)HttpStatusCode.NotImplemented);
190  }
191  catch (UnauthorizedAccessException)
192  {
193  return Forbid();
194  }
195  }
196 
203  [HttpDelete]
204  [TgsAuthorize(ConfigurationRights.Delete)]
205  public async Task<IActionResult> Delete([FromBody] ConfigurationFile directory, CancellationToken cancellationToken)
206  {
207  if (directory == null)
208  throw new ArgumentNullException(nameof(directory));
209 
210  if (ForbidDueToModeConflicts(directory.Path, out var systemIdentity))
211  return Forbid();
212 
213  try
214  {
215  return await instanceManager.GetInstance(Instance).Configuration.DeleteDirectory(directory.Path, systemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Ok() : Conflict(new ErrorMessage
216  {
217  Message = "Directory not empty!"
218  });
219  }
220  catch (NotImplementedException)
221  {
222  return StatusCode((int)HttpStatusCode.NotImplemented);
223  }
224  catch (UnauthorizedAccessException)
225  {
226  return Forbid();
227  }
228  }
229  }
230 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.IO;
5 using System.Net;
6 using System.Threading;
7 using System.Threading.Tasks;
8 using Tgstation.Server.Api;
12 using Tgstation.Server.Host.IO;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.Configuration)]
22  public sealed class ConfigurationController : ModelController<ConfigurationFile>
23  {
28 
33 
42  public ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger<ConfigurationController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
43  {
44  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
45  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
46  }
47 
54  bool ForbidDueToModeConflicts(string path, out ISystemIdentity systemIdentityToUse)
55  {
56  if (Instance.ConfigurationType == ConfigurationType.Disallowed || (Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite && AuthenticationContext.SystemIdentity == null) || (path != null && ioManager.PathContainsParentAccess(path)))
57  {
58  systemIdentityToUse = null;
59  return true;
60  }
61 
62  systemIdentityToUse = Instance.ConfigurationType == ConfigurationType.SystemIdentityWrite ? AuthenticationContext.SystemIdentity : null;
63  return false;
64  }
65 
67  [TgsAuthorize(ConfigurationRights.Write)]
68  public override async Task<IActionResult> Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
69  {
70  if (model == null)
71  throw new ArgumentNullException(nameof(model));
72  if (ForbidDueToModeConflicts(model.Path, out var systemIdentity))
73  return Forbid();
74 
75  var config = instanceManager.GetInstance(Instance).Configuration;
76  try
77  {
78  var newFile = await config.Write(model.Path, systemIdentity, model.Content, model.LastReadHash, cancellationToken).ConfigureAwait(false);
79  if (newFile == null)
80  return Conflict(new ErrorMessage
81  {
82  Message = "This file has been updated since you last viewed it!"
83  });
84 
85  newFile.Content = null;
86 
87  return model.LastReadHash == null ? (IActionResult)StatusCode((int)HttpStatusCode.Created, newFile) : Json(newFile);
88  }
89  catch(IOException e)
90  {
91  Logger.LogInformation("IOException while updating file {0}: {1}", model.Path, e);
92  return Conflict(new ErrorMessage
93  {
94  Message = e.Message
95  });
96  }
97  catch (NotImplementedException)
98  {
99  return StatusCode((int)HttpStatusCode.NotImplemented);
100  }
101  }
102 
109  [HttpGet(Routes.File + "/{*filePath}")]
110  [TgsAuthorize(ConfigurationRights.Read)]
111  public async Task<IActionResult> File(string filePath, CancellationToken cancellationToken)
112  {
113  if (ForbidDueToModeConflicts(filePath, out var systemIdentity))
114  return Forbid();
115 
116  try
117  {
118  var result = await instanceManager.GetInstance(Instance).Configuration.Read(filePath, systemIdentity, cancellationToken).ConfigureAwait(false);
119  if (result == null)
120  return StatusCode((int)HttpStatusCode.Gone);
121 
122  return Json(result);
123  }
124  catch (IOException e)
125  {
126  Logger.LogInformation("IOException while reading file {0}: {1}", filePath, e);
127  return Conflict(new ErrorMessage
128  {
129  Message = e.Message
130  });
131  }
132  catch (NotImplementedException)
133  {
134  return StatusCode((int)HttpStatusCode.NotImplemented);
135  }
136  }
137 
144  [HttpGet("List/{*directoryPath}")]
145  [TgsAuthorize(ConfigurationRights.List)]
146  public async Task<IActionResult> Directory(string directoryPath, CancellationToken cancellationToken)
147  {
148  if (ForbidDueToModeConflicts(directoryPath, out var systemIdentity))
149  return Forbid();
150 
151  try
152  {
153  var result = await instanceManager.GetInstance(Instance).Configuration.ListDirectory(directoryPath, systemIdentity, cancellationToken).ConfigureAwait(false);
154  if (result == null)
155  return StatusCode((int)HttpStatusCode.Gone);
156 
157  return Json(result);
158  }
159  catch (NotImplementedException)
160  {
161  return StatusCode((int)HttpStatusCode.NotImplemented);
162  }
163  catch (UnauthorizedAccessException)
164  {
165  return Forbid();
166  }
167  }
168 
170  [TgsAuthorize(ConfigurationRights.List)]
171  public override Task<IActionResult> List(CancellationToken cancellationToken) => Directory(null, cancellationToken);
172 
174  [TgsAuthorize(ConfigurationRights.Write)]
175  public override async Task<IActionResult> Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
176  {
177  if (model == null)
178  throw new ArgumentNullException(nameof(model));
179 
180  if (ForbidDueToModeConflicts(model.Path, out var systemIdentity))
181  return Forbid();
182 
183  try
184  {
185  model.IsDirectory = true;
186  return await instanceManager.GetInstance(Instance).Configuration.CreateDirectory(model.Path, systemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Json(model) : StatusCode((int)HttpStatusCode.Created, model);
187  }
188  catch (NotImplementedException)
189  {
190  return StatusCode((int)HttpStatusCode.NotImplemented);
191  }
192  catch (UnauthorizedAccessException)
193  {
194  return Forbid();
195  }
196  }
197 
204  [HttpDelete]
205  [TgsAuthorize(ConfigurationRights.Delete)]
206  public async Task<IActionResult> Delete([FromBody] ConfigurationFile directory, CancellationToken cancellationToken)
207  {
208  if (directory == null)
209  throw new ArgumentNullException(nameof(directory));
210 
211  if (ForbidDueToModeConflicts(directory.Path, out var systemIdentity))
212  return Forbid();
213 
214  try
215  {
216  return await instanceManager.GetInstance(Instance).Configuration.DeleteDirectory(directory.Path, systemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Ok() : Conflict(new ErrorMessage
217  {
218  Message = "Directory not empty!"
219  });
220  }
221  catch (NotImplementedException)
222  {
223  return StatusCode((int)HttpStatusCode.NotImplemented);
224  }
225  catch (UnauthorizedAccessException)
226  {
227  return Forbid();
228  }
229  }
230  }
231 }
ConfigurationRights
Rights for Models.ConfigurationFile
@@ -103,11 +103,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
override async Task< IActionResult > Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
+
override async Task< IActionResult > Create([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
bool ForbidDueToModeConflicts(string path, out ISystemIdentity systemIdentityToUse)
If a ForbidResult should be returned from actions due to conflicts with one or both of the Api...
readonly IInstanceManager instanceManager
The IInstanceManager for the ConfigurationController
const string File
To be paired with Configuration for accessing Models.ConfigurationFiles
Definition: Routes.cs:54
-
async Task< IActionResult > Delete([FromBody] ConfigurationFile directory, CancellationToken cancellationToken)
Deletes an empty directory
+
async Task< IActionResult > Delete([FromBody] ConfigurationFile directory, CancellationToken cancellationToken)
Deletes an empty directory
ConfigurationType
The type of configuration allowed on an Instance
ConfigurationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IIOManager ioManager, ILogger< ConfigurationController > logger)
Construct a UserController
@@ -120,16 +120,16 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
async Task< IActionResult > File(string filePath, CancellationToken cancellationToken)
Get the contents of a file at a filePath
+
async Task< IActionResult > File(string filePath, CancellationToken cancellationToken)
Get the contents of a file at a filePath
Metadata about a server instance
Definition: Instance.cs:9
Interface for using filesystems
Definition: IIOManager.cs:11
-
async Task< IActionResult > Directory(string directoryPath, CancellationToken cancellationToken)
Get the contents of a directory at a directoryPath
+
async Task< IActionResult > Directory(string directoryPath, CancellationToken cancellationToken)
Get the contents of a directory at a directoryPath
Represents an error message returned by the server
Definition: ErrorMessage.cs:8
-
override async Task< IActionResult > Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
+
override async Task< IActionResult > Update([FromBody] ConfigurationFile model, CancellationToken cancellationToken)
ConfigurationType ConfigurationType
If ConfigurationFile can be used on the Instance
Definition: Instance.cs:38
diff --git a/_configuration_file_8cs_source.html b/_configuration_file_8cs_source.html index a2b122dded..5e2bd2a11f 100644 --- a/_configuration_file_8cs_source.html +++ b/_configuration_file_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ConfigurationFile.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public sealed class ConfigurationFile
7  {
11  public string Path { get; set; }
12 
16  public bool? AccessDenied { get; set; }
17 
21  public bool? IsDirectory { get; set; }
22 
26  public string LastReadHash { get; set; }
27 
31  public byte[] Content { get; set; }
32  }
33 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public sealed class ConfigurationFile
7  {
11  public string Path { get; set; }
12 
16  public bool? AccessDenied { get; set; }
17 
21  public bool? IsDirectory { get; set; }
22 
26  public string LastReadHash { get; set; }
27 
31 #pragma warning disable CA1819 // Properties should not return arrays
32  public byte[] Content { get; set; }
33 #pragma warning restore CA1819 // Properties should not return arrays
34  }
35 }
Represents a game configuration file. Create and delete actions uncerimonuously overwrite/delete file...
diff --git a/_configuration_rights_8cs_source.html b/_configuration_rights_8cs_source.html index 61c6172dd7..7f32539b8c 100644 --- a/_configuration_rights_8cs_source.html +++ b/_configuration_rights_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ConfigurationRights.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum ConfigurationRights : ulong
10  {
14  None = 0,
18  Read = 1,
22  Write = 2,
26  List = 4,
30  Delete = 8
31  }
32 }
ConfigurationRights
Rights for Models.ConfigurationFile
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum ConfigurationRights : ulong
10  {
14  None = 0,
15 
19  Read = 1,
20 
24  Write = 2,
25 
29  List = 4,
30 
34  Delete = 8
35  }
36 }
ConfigurationRights
Rights for Models.ConfigurationFile
diff --git a/_configuration_type_8cs_source.html b/_configuration_type_8cs_source.html index 4698efdedf..c8d7809734 100644 --- a/_configuration_type_8cs_source.html +++ b/_configuration_type_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ConfigurationType.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public enum ConfigurationType
7  {
11  Disallowed,
15  HostWrite,
20  }
21 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public enum ConfigurationType
7  {
11  Disallowed,
12 
16  HostWrite,
17 
22  }
23 }
ConfigurationType
The type of configuration allowed on an Instance
diff --git a/_console_8cs_source.html b/_console_8cs_source.html index 8636868e7f..fc5bde695f 100644 --- a/_console_8cs_source.html +++ b/_console_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Console.cs
-Go to the documentation of this file.
1 using System;
2 using System.Text;
3 using System.Threading;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.IO
7 {
9  sealed class Console : IConsole
10  {
12  public bool Available => Environment.UserInteractive;
13 
15  {
16  if (!Available)
17  throw new InvalidOperationException("Console unavailable");
18  }
19 
21  public Task PressAnyKeyAsync(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
22  {
23  CheckAvailable();
24  System.Console.Read();
25  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
26 
28  public Task<string> ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
29  {
30  //TODO Make this better: https://stackoverflow.com/questions/9479573/how-to-interrupt-console-readline
31  CheckAvailable();
32  if (!usePasswordChar)
33  return System.Console.ReadLine();
34 
35  var passwordBuilder = new StringBuilder();
36  do
37  {
38  var keyDescription = System.Console.ReadKey(true);
39  if (keyDescription.Key == ConsoleKey.Enter)
40  break;
41  else if (keyDescription.Key == ConsoleKey.Backspace)
42  {
43  if (passwordBuilder.Length > 0)
44  {
45  --passwordBuilder.Length;
46  System.Console.Write("\b \b");
47  }
48  }
49  else if (keyDescription.KeyChar != '\u0000') // KeyChar == '\u0000' if the key pressed does not correspond to a printable character, e.g. F1, Pause-Break, etc
50  {
51  passwordBuilder.Append(keyDescription.KeyChar);
52  System.Console.Write('*');
53  }
54  }
55  while (!cancellationToken.IsCancellationRequested);
56  cancellationToken.ThrowIfCancellationRequested();
57  System.Console.WriteLine();
58  return passwordBuilder.ToString();
59  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
60 
62  public Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
63  {
64  CheckAvailable();
65  if (text == null)
66  {
67  if (!newLine)
68  throw new InvalidOperationException("Cannot write null text without a new line!");
69  System.Console.WriteLine();
70  }
71  else if (newLine)
72  System.Console.WriteLine(text);
73  else
74  System.Console.Write(text);
75  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
76  }
77 }
+Go to the documentation of this file.
1 using System;
2 using System.Text;
3 using System.Threading;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.IO
7 {
9  sealed class Console : IConsole
10  {
12  public bool Available => Environment.UserInteractive;
13 
15  {
16  if (!Available)
17  throw new InvalidOperationException("Console unavailable");
18  }
19 
21  public Task PressAnyKeyAsync(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
22  {
23  CheckAvailable();
24  System.Console.Read();
25  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
26 
28  public Task<string> ReadLineAsync(bool usePasswordChar, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
29  {
30  // TODO: Make this better: https://stackoverflow.com/questions/9479573/how-to-interrupt-console-readline
31  CheckAvailable();
32  if (!usePasswordChar)
33  return System.Console.ReadLine();
34 
35  var passwordBuilder = new StringBuilder();
36  do
37  {
38  var keyDescription = System.Console.ReadKey(true);
39  if (keyDescription.Key == ConsoleKey.Enter)
40  break;
41  else if (keyDescription.Key == ConsoleKey.Backspace)
42  {
43  if (passwordBuilder.Length > 0)
44  {
45  --passwordBuilder.Length;
46  System.Console.Write("\b \b");
47  }
48  }
49  else if (keyDescription.KeyChar != '\u0000')
50  {
51  // KeyChar == '\u0000' if the key pressed does not correspond to a printable character, e.g. F1, Pause-Break, etc
52  passwordBuilder.Append(keyDescription.KeyChar);
53  System.Console.Write('*');
54  }
55  }
56  while (!cancellationToken.IsCancellationRequested);
57 
58  cancellationToken.ThrowIfCancellationRequested();
59  System.Console.WriteLine();
60  return passwordBuilder.ToString();
61  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
62 
64  public Task WriteAsync(string text, bool newLine, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
65  {
66  CheckAvailable();
67  if (text == null)
68  {
69  if (!newLine)
70  throw new InvalidOperationException("Cannot write null text without a new line!");
71  System.Console.WriteLine();
72  }
73  else if (newLine)
74  System.Console.WriteLine(text);
75  else
76  System.Console.Write(text);
77  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
78  }
79 }
Abstraction for System.Console
Definition: IConsole.cs:9
diff --git a/_constants_8cs.html b/_constants_8cs.html index b6134f2859..23194ed624 100644 --- a/_constants_8cs.html +++ b/_constants_8cs.html @@ -101,6 +101,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

Classes

class  Tgstation.Server.Host.Components.Interop.Constants
 Constants used for communication with the DMAPI More...
 
+

diff --git a/_constants_8cs_source.html b/_constants_8cs_source.html index 3f8bdfd448..3fe3d1028f 100644 --- a/_constants_8cs_source.html +++ b/_constants_8cs_source.html @@ -92,8 +92,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Constants.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Interop
2 {
3  static class Constants
4  {
5  //interop values, match them up with the appropriate api.dm
6 
7  //api version 4.0.0.0
8  public const string DMParamHostVersion = "server_service_version";
9  public const string DMParamInfoJson = "tgs_json";
10 
11  public const string DMInteropAccessIdentifier = "tgs_tok";
12 
13  public const string DMResponseSuccess = "tgs_succ";
14 
15  public const string DMTopicChangePort = "tgs_port";
16  public const string DMTopicChangeReboot = "tgs_rmode";
17  public const string DMTopicChatCommand = "tgs_chat_comm";
18  public const string DMTopicEvent = "tgs_event";
19  public const string DMTopicInteropResponse = "tgs_interop";
20 
21  public const string DMCommandNewPort = "tgs_new_port";
22  public const string DMCommandApiValidate = "tgs_validate";
23  public const string DMCommandServerPrimed = "tgs_prime";
24  public const string DMCommandWorldReboot = "tgs_reboot";
25  public const string DMCommandEndProcess = "tgs_kill";
26  public const string DMCommandChat = "tgs_chat_send";
27 
28  public const string DMParameterCommand = "tgs_com";
29  public const string DMParameterData = "tgs_data";
30  }
31 }
- +Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Interop
2 {
6  static class Constants
7  {
11  public const string DMParamHostVersion = "server_service_version";
12 
16  public const string DMParamInfoJson = "tgs_json";
17 
21  public const string DMInteropAccessIdentifier = "tgs_tok";
22 
26  public const string DMResponseSuccess = "tgs_succ";
27 
31  public const string DMTopicChangePort = "tgs_port";
32 
36  public const string DMTopicChangeReboot = "tgs_rmode";
37 
41  public const string DMTopicChatCommand = "tgs_chat_comm";
42 
46  public const string DMTopicEvent = "tgs_event";
47 
51  public const string DMTopicInteropResponse = "tgs_interop";
52 
56  public const string DMCommandNewPort = "tgs_new_port";
57 
61  public const string DMCommandApiValidate = "tgs_validate";
62 
66  public const string DMCommandServerPrimed = "tgs_prime";
67 
71  public const string DMCommandWorldReboot = "tgs_reboot";
72 
76  public const string DMCommandEndProcess = "tgs_kill";
77 
81  public const string DMCommandChat = "tgs_chat_send";
82 
86  public const string DMParameterCommand = "tgs_com";
87 
91  public const string DMParameterData = "tgs_data";
92  }
93 }
+
Constants used for communication with the DMAPI
Definition: Constants.cs:6
diff --git a/_cryptography_suite_8cs_source.html b/_cryptography_suite_8cs_source.html index ab91342cd2..fe526757a7 100644 --- a/_cryptography_suite_8cs_source.html +++ b/_cryptography_suite_8cs_source.html @@ -92,22 +92,22 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
CryptographySuite.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using System;
3 using System.Security.Cryptography;
5 
6 namespace Tgstation.Server.Host.Security
7 {
10  {
14  const uint SecureStringLength = 30;
15 
19  readonly IPasswordHasher<User> passwordHasher;
20 
25  public CryptographySuite(IPasswordHasher<User> passwordHasher)
26  {
27  this.passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher));
28  }
29 
31  public byte[] GetSecureBytes(uint amount)
32  {
33  using (var rng = new RNGCryptoServiceProvider())
34  {
35  var byt = new byte[amount];
36  rng.GetBytes(byt);
37  return byt;
38  }
39  }
40 
42  public void SetUserPassword(User user, string newPassword, bool newUser)
43  {
44  if (user == null)
45  throw new ArgumentNullException(nameof(user));
46  if (newPassword == null)
47  throw new ArgumentNullException(nameof(newPassword));
48  user.PasswordHash = passwordHasher.HashPassword(user, newPassword);
49  if (!newUser)
50  user.LastPasswordUpdate = DateTimeOffset.Now;
51  }
52 
54  public bool CheckUserPassword(User user, string password)
55  {
56  switch (passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password))
57  {
58  case PasswordVerificationResult.Failed:
59  return false;
60  case PasswordVerificationResult.SuccessRehashNeeded:
61  SetUserPassword(user, password, false);
62  break;
63  }
64  return true;
65  }
66 
68  public string GetSecureString() => Convert.ToBase64String(GetSecureBytes(SecureStringLength));
69  }
70 }
string PasswordHash
The hash of the user&#39;s password
Definition: User.cs:13
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using System;
3 using System.Globalization;
4 using System.Security.Cryptography;
6 
7 namespace Tgstation.Server.Host.Security
8 {
11  {
15  const uint SecureStringLength = 30;
16 
20  readonly IPasswordHasher<User> passwordHasher;
21 
26  public CryptographySuite(IPasswordHasher<User> passwordHasher)
27  {
28  this.passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher));
29  }
30 
32  public byte[] GetSecureBytes(uint amount)
33  {
34  using (var rng = new RNGCryptoServiceProvider())
35  {
36  var byt = new byte[amount];
37  rng.GetBytes(byt);
38  return byt;
39  }
40  }
41 
43  public void SetUserPassword(User user, string newPassword, bool newUser)
44  {
45  if (user == null)
46  throw new ArgumentNullException(nameof(user));
47  if (newPassword == null)
48  throw new ArgumentNullException(nameof(newPassword));
49  user.PasswordHash = passwordHasher.HashPassword(user, newPassword);
50  if (!newUser)
51  user.LastPasswordUpdate = DateTimeOffset.Now;
52  }
53 
55  public bool CheckUserPassword(User user, string password)
56  {
57  var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password);
58  switch (result)
59  {
60  case PasswordVerificationResult.Failed:
61  return false;
62  case PasswordVerificationResult.SuccessRehashNeeded:
63  SetUserPassword(user, password, false);
64  break;
65  case PasswordVerificationResult.Success:
66  break;
67  default:
68  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Password hasher return unknown PasswordVerificationResult: {0}", result));
69  }
70 
71  return true;
72  }
73 
75  public string GetSecureString() => Convert.ToBase64String(GetSecureBytes(SecureStringLength));
76  }
77 }
string PasswordHash
The hash of the user&#39;s password
Definition: User.cs:13
-
readonly IPasswordHasher< User > passwordHasher
The IPasswordHasher<TUser> for the CryptographySuite
+
readonly IPasswordHasher< User > passwordHasher
The IPasswordHasher<TUser> for the CryptographySuite
DateTimeOffset LastPasswordUpdate
When PasswordHash was last changed
Definition: User.cs:29
-
void SetUserPassword(User user, string newPassword, bool newUser)
Sets a User.PasswordHash for a given user
+
void SetUserPassword(User user, string newPassword, bool newUser)
Sets a User.PasswordHash for a given user
Contains various cryptographic functions
-
bool CheckUserPassword(User user, string password)
Checks a given password matches a given user &#39;s User.PasswordHash. This may result in User...
+
bool CheckUserPassword(User user, string password)
Checks a given password matches a given user &#39;s User.PasswordHash. This may result in User...
-
byte[] GetSecureBytes(uint amount)
Generates a secure set of bytes
-
CryptographySuite(IPasswordHasher< User > passwordHasher)
Construct a CryptographySuite
+
byte[] GetSecureBytes(uint amount)
Generates a secure set of bytes
+
CryptographySuite(IPasswordHasher< User > passwordHasher)
Construct a CryptographySuite
- +
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.EntityFrameworkCore.Infrastructure;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Logging;
6 using Microsoft.Extensions.Options;
7 using System;
8 using System.Globalization;
9 using System.Linq;
10 using System.Threading;
11 using System.Threading.Tasks;
13 
14 namespace Tgstation.Server.Host.Models
15 {
17  abstract class DatabaseContext<TParentContext> : DbContext, IDatabaseContext where TParentContext : DbContext
18  {
20  public DbSet<User> Users { get; set; }
21 
23  public DbSet<Instance> Instances { get; set; }
24 
26  public DbSet<CompileJob> CompileJobs { get; set; }
27 
29  public DbSet<RevisionInformation> RevisionInformations { get; set; }
30 
32  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
33 
35  public DbSet<ChatBot> ChatBots { get; set; }
36 
38  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
39 
41  public DbSet<RepositorySettings> RepositorySettings { get; set; }
42 
44  public DbSet<InstanceUser> InstanceUsers { get; set; }
45 
47  public DbSet<ChatChannel> ChatChannels { get; set; }
48 
50  public DbSet<Job> Jobs { get; set; }
51 
53  public DbSet<ReattachInformation> ReattachInformations { get; set; }
54 
56  public DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
57 
61  public DbSet<TestMerge> TestMerges { get; set; }
62 
66  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
67 
71  protected ILogger Logger { get; }
72 
77 
82 
90  public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
91  {
92  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
93  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
94  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
95  }
96 
98  protected override void OnModelCreating(ModelBuilder modelBuilder)
99  {
100  Logger.LogTrace("Building entity framework context...");
101  base.OnModelCreating(modelBuilder);
102 
103  var userModel = modelBuilder.Entity<User>();
104  userModel.HasIndex(x => x.CanonicalName).IsUnique();
105  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
106 
107  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
108 
109  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.Cascade);
110 
111  var revInfo = modelBuilder.Entity<RevisionInformation>();
112  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
113  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
114  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Restrict);
115  revInfo.HasIndex(x => x.CommitSha).IsUnique();
116 
117  modelBuilder.Entity<CompileJob>().HasIndex(x => x.DirectoryName);
118 
119  modelBuilder.Entity<Job>().HasOne<CompileJob>().WithOne(x => x.Job).OnDelete(DeleteBehavior.Restrict);
120 
121  var chatChannel = modelBuilder.Entity<ChatChannel>();
122  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
123  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
124  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
125 
126  modelBuilder.Entity<ChatBot>().HasIndex(x => x.Name).IsUnique();
127 
128  var instanceModel = modelBuilder.Entity<Instance>();
129  instanceModel.HasIndex(x => x.Path).IsUnique();
130  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
131  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
132  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
133  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
134  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
135  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
136  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
137  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
138  }
139 
141  public async Task Initialize(CancellationToken cancellationToken)
142  {
144  {
145  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
146  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
147  }
148 
149  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
150  var wasEmpty = !migrations.Any();
151 
152  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
153  {
154  Logger.LogInformation("Migrating database...");
155  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
156  }
157  else
158  Logger.LogDebug("No migrations to apply.");
159 
160  wasEmpty |= (await Users.CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
161 
162  if (wasEmpty)
163  {
164  Logger.LogInformation("Seeding database...");
165  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
166  }
168  {
169  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
170  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
171  }
172  }
173 
175  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
176 
181  protected abstract bool UseMySQLMigrations();
182 
184  public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
185  {
186  if (version == null)
187  throw new ArgumentNullException(nameof(version));
188  if (version < new Version(4, 0))
189  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
190 
191  string targetMigration = null;
192 
193  //Update this with new migrations as they are made
194  //Always use the MS class
195 
196  //TODO: Uncomment once #816 is merged
197  /*
198  if (version < new Version(4, 0, 2))
199  targetMigration = nameof(MSReattachCompileJobRequired);
200  */
201 
202  if (targetMigration == null)
203  return;
204 
205  if (UseMySQLMigrations())
206  targetMigration = String.Format(CultureInfo.InvariantCulture, "MY" + targetMigration.Substring(2));
207 
208  //even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
209  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
210  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
211 
212  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
213  try
214  {
215  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
216  }
217  catch (Exception e)
218  {
219  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
220  }
221  }
222  }
223 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.EntityFrameworkCore.Infrastructure;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Logging;
6 using Microsoft.Extensions.Options;
7 using System;
8 using System.Globalization;
9 using System.Linq;
10 using System.Threading;
11 using System.Threading.Tasks;
13 
14 namespace Tgstation.Server.Host.Models
15 {
17  #pragma warning disable CA1506 // TODO: Decomplexify
18  abstract class DatabaseContext<TParentContext> : DbContext, IDatabaseContext where TParentContext : DbContext
19  {
21  public DbSet<User> Users { get; set; }
22 
24  public DbSet<Instance> Instances { get; set; }
25 
27  public DbSet<CompileJob> CompileJobs { get; set; }
28 
30  public DbSet<RevisionInformation> RevisionInformations { get; set; }
31 
33  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
34 
36  public DbSet<ChatBot> ChatBots { get; set; }
37 
39  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
40 
42  public DbSet<RepositorySettings> RepositorySettings { get; set; }
43 
45  public DbSet<InstanceUser> InstanceUsers { get; set; }
46 
48  public DbSet<ChatChannel> ChatChannels { get; set; }
49 
51  public DbSet<Job> Jobs { get; set; }
52 
54  public DbSet<ReattachInformation> ReattachInformations { get; set; }
55 
57  public DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
58 
62  public DbSet<TestMerge> TestMerges { get; set; }
63 
67  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
68 
72  protected ILogger Logger { get; }
73 
78 
83 
91  public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
92  {
93  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
94  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
95  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
96  }
97 
99  protected override void OnModelCreating(ModelBuilder modelBuilder)
100  {
101  Logger.LogTrace("Building entity framework context...");
102  base.OnModelCreating(modelBuilder);
103 
104  var userModel = modelBuilder.Entity<User>();
105  userModel.HasIndex(x => x.CanonicalName).IsUnique();
106  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
107 
108  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
109 
110  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.Cascade);
111 
112  var revInfo = modelBuilder.Entity<RevisionInformation>();
113  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
114  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
115  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Restrict);
116  revInfo.HasIndex(x => x.CommitSha).IsUnique();
117 
118  modelBuilder.Entity<CompileJob>().HasIndex(x => x.DirectoryName);
119 
120  modelBuilder.Entity<Job>().HasOne<CompileJob>().WithOne(x => x.Job).OnDelete(DeleteBehavior.Restrict);
121 
122  var chatChannel = modelBuilder.Entity<ChatChannel>();
123  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
124  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
125  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
126 
127  modelBuilder.Entity<ChatBot>().HasIndex(x => x.Name).IsUnique();
128 
129  var instanceModel = modelBuilder.Entity<Instance>();
130  instanceModel.HasIndex(x => x.Path).IsUnique();
131  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
132  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
133  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
134  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
135  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
136  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
137  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
138  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
139  }
140 
142  public async Task Initialize(CancellationToken cancellationToken)
143  {
145  {
146  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
147  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
148  }
149 
150  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
151  var wasEmpty = !migrations.Any();
152 
153  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
154  {
155  Logger.LogInformation("Migrating database...");
156  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
157  }
158  else
159  Logger.LogDebug("No migrations to apply.");
160 
161  wasEmpty |= (await Users.CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
162 
163  if (wasEmpty)
164  {
165  Logger.LogInformation("Seeding database...");
166  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
167  }
169  {
170  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
171  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
172  }
173  }
174 
176  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
177 
182  protected abstract bool UseMySQLMigrations();
183 
185  public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
186  {
187  if (version == null)
188  throw new ArgumentNullException(nameof(version));
189  if (version < new Version(4, 0))
190  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
191 
192  string targetMigration = null;
193 
194  // Update this with new migrations as they are made
195  // Always use the MS class
196 
197  // TODO: Uncomment once #816 is merged
198  /*
199  if (version < new Version(4, 0, 2))
200  targetMigration = nameof(MSReattachCompileJobRequired);
201  */
202 
203  if (targetMigration == null)
204  return;
205 
206  if (UseMySQLMigrations())
207  targetMigration = String.Format(CultureInfo.InvariantCulture, "MY" + targetMigration.Substring(2));
208 
209  // even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
210  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
211  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
212 
213  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
214  try
215  {
216  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
217  }
218  catch (Exception e)
219  {
220  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
221  }
222  }
223  }
224 }
- +
For initially seeding a database
-
async Task Initialize(CancellationToken cancellationToken)
Creates and migrates the IDatabaseContext
+
async Task Initialize(CancellationToken cancellationToken)
Creates and migrates the IDatabaseContext
bool ResetAdminPassword
If the admin user should be enabled and have it&#39;s password reset
-
override void OnModelCreating(ModelBuilder modelBuilder)
-
async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
Attempt to downgrade the schema to the migration used for a given server version ...
+
override void OnModelCreating(ModelBuilder modelBuilder)
+
async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
Attempt to downgrade the schema to the migration used for a given server version ...
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
- -
DatabaseContext(DbContextOptions< TParentContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger)
Construct a DatabaseContext<TParentContext>
+ +
DatabaseContext(DbContextOptions< TParentContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger)
Construct a DatabaseContext<TParentContext>
Configuration options for the Models.DatabaseContext<TParentContext>
bool DropDatabase
If the database should be deleted on application startup. Should not be used in production! ...
-
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
+
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
diff --git a/_database_type_8cs_source.html b/_database_type_8cs_source.html index 81d9a27a92..c1cec12924 100644 --- a/_database_type_8cs_source.html +++ b/_database_type_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseType.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Configuration
2 {
7  {
11  SqlServer,
15  MySql,
19  MariaDB
20  }
21 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Configuration
2 {
7  {
11  SqlServer,
12 
16  MySql,
17 
21  MariaDB
22  }
23 }
diff --git a/_dead_session_controller_8cs_source.html b/_dead_session_controller_8cs_source.html index 9e484af7e1..74712de446 100644 --- a/_dead_session_controller_8cs_source.html +++ b/_dead_session_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DeadSessionController.cs
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 
5 namespace Tgstation.Server.Host.Components.Watchdog
6 {
11  {
13  public Task<LaunchResult> LaunchResult { get; }
14 
16  public bool IsPrimary => false;
17 
19  public bool TerminationWasRequested => false;
20 
22  public ApiValidationStatus ApiValidationStatus => throw new NotSupportedException();
23 
25  public IDmbProvider Dmb { get; }
26 
28  public ushort? Port => null;
29 
31  public bool ClosePortOnReboot
32  {
33  get => false;
34  set => throw new NotSupportedException();
35  }
36 
38  public RebootState RebootState => throw new NotSupportedException();
39 
41  public Task OnReboot { get; }
42 
44  public Task<int> Lifetime { get; }
45 
49  bool disposed;
50 
55  public DeadSessionController(IDmbProvider dmbProvider)
56  {
57  Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
58  LaunchResult = Task.FromResult(new LaunchResult
59  {
60  StartupTime = TimeSpan.FromSeconds(0)
61  });
62  Lifetime = Task.FromResult(-1);
63  OnReboot = new TaskCompletionSource<object>().Task;
64  }
65 
67  public void Dispose()
68  {
69  lock (this)
70  {
71  if (disposed)
72  return;
73  disposed = true;
74  }
75  Dmb.Dispose();
76  }
77 
79  public void EnableCustomChatCommands() => throw new NotSupportedException();
80 
82  public ReattachInformation Release() => throw new NotSupportedException();
83 
85  public void ResetRebootState() => throw new NotSupportedException();
86 
88  public Task<string> SendCommand(string command, CancellationToken cancellationToken) => throw new NotSupportedException();
89 
91  public void SetHighPriority() => throw new NotSupportedException();
92 
94  public Task<bool> SetPort(ushort newPort, CancellationToken cancellatonToken) => throw new NotSupportedException();
95 
97  public Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken) => throw new NotSupportedException();
98  }
99 }
Represents the result of trying to start a DD process
Definition: LaunchResult.cs:9
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 
5 namespace Tgstation.Server.Host.Components.Watchdog
6 {
11  {
13  public Task<LaunchResult> LaunchResult { get; }
14 
16  public bool IsPrimary => false;
17 
19  public bool TerminationWasRequested => false;
20 
22  public ApiValidationStatus ApiValidationStatus => throw new NotSupportedException();
23 
25  public IDmbProvider Dmb { get; }
26 
28  public ushort? Port => null;
29 
31  public bool ClosePortOnReboot
32  {
33  get => false;
34  set => throw new NotSupportedException();
35  }
36 
38  public RebootState RebootState => throw new NotSupportedException();
39 
41  public Task OnReboot { get; }
42 
44  public Task<int> Lifetime { get; }
45 
49  bool disposed;
50 
55  public DeadSessionController(IDmbProvider dmbProvider)
56  {
57  Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
58  LaunchResult = Task.FromResult(new LaunchResult
59  {
60  StartupTime = TimeSpan.FromSeconds(0)
61  });
62  Lifetime = Task.FromResult(-1);
63  OnReboot = new TaskCompletionSource<object>().Task;
64  }
65 
67  public void Dispose()
68  {
69  lock (this)
70  {
71  if (disposed)
72  return;
73  disposed = true;
74  }
75 
76  Dmb.Dispose();
77  }
78 
80  public void EnableCustomChatCommands() => throw new NotSupportedException();
81 
83  public ReattachInformation Release() => throw new NotSupportedException();
84 
86  public void ResetRebootState() => throw new NotSupportedException();
87 
89  public Task<string> SendCommand(string command, CancellationToken cancellationToken) => throw new NotSupportedException();
90 
92  public void SetHighPriority() => throw new NotSupportedException();
93 
95  public Task<bool> SetPort(ushort newPort, CancellationToken cancellatonToken) => throw new NotSupportedException();
96 
98  public Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken) => throw new NotSupportedException();
99  }
100 }
Represents the result of trying to start a DD process
Definition: LaunchResult.cs:9
Handles communication with a DreamDaemon IProcess
diff --git a/_default_i_o_manager_8cs_source.html b/_default_i_o_manager_8cs_source.html index 8725a40dd1..e1630b9f80 100644 --- a/_default_i_o_manager_8cs_source.html +++ b/_default_i_o_manager_8cs_source.html @@ -92,19 +92,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DefaultIOManager.cs
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Globalization;
4 using System.IO;
5 using System.IO.Compression;
6 using System.Linq;
7 using System.Net;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.IO
13 {
18  {
22  public const int DefaultBufferSize = 4096;
23 
30  static async Task NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellationToken)
31  {
32  var tasks = new List<Task>();
33 
34  foreach (var subDir in dir.EnumerateDirectories())
35  {
36  cancellationToken.ThrowIfCancellationRequested();
37  if (!subDir.Attributes.HasFlag(FileAttributes.Directory) || subDir.Attributes.HasFlag(FileAttributes.ReparsePoint))
38  //this is probably a symlink
39  subDir.Delete();
40  else
41  tasks.Add(NormalizeAndDelete(subDir, cancellationToken));
42  }
43  foreach (var file in dir.EnumerateFiles())
44  {
45  cancellationToken.ThrowIfCancellationRequested();
46  file.Attributes = FileAttributes.Normal;
47  file.Delete();
48  }
49  await Task.WhenAll(tasks).ConfigureAwait(false);
50  cancellationToken.ThrowIfCancellationRequested();
51  dir.Delete(true);
52  }
53 
59  static FileStream OpenWriteStream(string path) => new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite, DefaultBufferSize, true);
60 
69  IEnumerable<Task> CopyDirectoryImpl(string src, string dest, IEnumerable<string> ignore, CancellationToken cancellationToken)
70  {
71  var dir = new DirectoryInfo(src);
72  var atLeastOneSubDir = false;
73  foreach (var I in dir.EnumerateDirectories())
74  {
75  if (ignore != null && ignore.Contains(I.Name))
76  continue;
77  foreach (var J in CopyDirectoryImpl(I.FullName, Path.Combine(dest, I.Name), null, cancellationToken))
78  {
79  atLeastOneSubDir = true;
80  yield return J;
81  }
82  }
83 
84  async Task CopyThisDirectory()
85  {
86  if (!atLeastOneSubDir)
87  await CreateDirectory(dest, cancellationToken).ConfigureAwait(false); //save on createdir calls
88 
89  var tasks = new List<Task>();
90 
91  await dir.EnumerateFiles().ToAsyncEnumerable().ForEachAsync(I =>
92  {
93  if (ignore != null && ignore.Contains(I.Name))
94  return;
95  tasks.Add(CopyFile(I.FullName, Path.Combine(dest, I.Name), cancellationToken));
96  }).ConfigureAwait(false);
97 
98  await Task.WhenAll(tasks).ConfigureAwait(false);
99  };
100 
101  yield return CopyThisDirectory();
102  }
103 
105  public async Task CopyDirectory(string src, string dest, IEnumerable<string> ignore, CancellationToken cancellationToken)
106  {
107  if (dest == null)
108  throw new ArgumentNullException(nameof(src));
109  if (dest == null)
110  throw new ArgumentNullException(nameof(src));
111 
112  src = ResolvePath(src);
113  dest = ResolvePath(dest);
114  foreach (var directoryCopy in CopyDirectoryImpl(src, dest, ignore, cancellationToken))
115  await directoryCopy.ConfigureAwait(false);
116  }
117 
119  public async Task AppendAllText(string path, string additional_contents, CancellationToken cancellationToken)
120  {
121  if (additional_contents == null)
122  throw new ArgumentNullException(nameof(additional_contents));
123  using (var destStream = new FileStream(ResolvePath(path), FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, DefaultBufferSize, true))
124  {
125  var buf = Encoding.UTF8.GetBytes(additional_contents);
126  await destStream.WriteAsync(buf, 0, buf.Length, cancellationToken).ConfigureAwait(false);
127  }
128  }
129 
131  public string ConcatPath(params string[] paths)
132  {
133  if (paths == null)
134  throw new ArgumentNullException(nameof(paths));
135  return Path.Combine(paths);
136  }
137 
139  public async Task CopyFile(string src, string dest, CancellationToken cancellationToken)
140  {
141  if (src == null)
142  throw new ArgumentNullException(nameof(src));
143  if (dest == null)
144  throw new ArgumentNullException(nameof(dest));
145  using (var srcStream = new FileStream(ResolvePath(src), FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, DefaultBufferSize, true))
146  using (var destStream = new FileStream(ResolvePath(dest), FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, DefaultBufferSize, true))
147  await srcStream.CopyToAsync(destStream, 81920, cancellationToken).ConfigureAwait(false);
148  }
149 
151  public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.CreateDirectory(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
152 
154  public async Task DeleteDirectory(string path, CancellationToken cancellationToken)
155  {
156  path = ResolvePath(path);
157  var di = new DirectoryInfo(path);
158  if (!di.Exists)
159  return;
160  await NormalizeAndDelete(di, cancellationToken).ConfigureAwait(false);
161  }
162 
164  public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Delete(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
165 
167  public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
168 
170  public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
171 
173  public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path)));
174 
176  public string GetFileName(string path) => Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path)));
177 
179  public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path ?? throw new ArgumentNullException(nameof(path)));
180 
182  public Task<List<string>> GetFilesWithExtension(string path, string extension, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
183  {
184  path = ResolvePath(path);
185  if (extension == null)
186  throw new ArgumentNullException(extension);
187  var results = new List<string>();
188  foreach (var I in Directory.EnumerateFiles(path, String.Format(CultureInfo.InvariantCulture, "*.{0}", extension), SearchOption.TopDirectoryOnly))
189  {
190  cancellationToken.ThrowIfCancellationRequested();
191  results.Add(I);
192  }
193  return results;
194  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
195 
197  public Task MoveFile(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
198  {
199  if (destination == null)
200  throw new ArgumentNullException(nameof(destination));
201  source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
202  destination = ResolvePath(destination);
203  File.Move(source, destination);
204  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
205 
207  public Task MoveDirectory(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
208  {
209  if (destination == null)
210  throw new ArgumentNullException(nameof(destination));
211  source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
212  destination = ResolvePath(destination);
213  Directory.Move(source, destination);
214  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
215 
217  public async Task<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
218  {
219  path = ResolvePath(path);
220  using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, DefaultBufferSize, true))
221  {
222  byte[] buf;
223  buf = new byte[file.Length];
224  await file.ReadAsync(buf, 0, (int)file.Length, cancellationToken).ConfigureAwait(false);
225  return buf;
226  }
227  }
228 
230  public virtual string ResolvePath(string path) => Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path)));
231 
233  public async Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
234  {
235  path = ResolvePath(path);
236  using (var file = OpenWriteStream(path))
237  await file.WriteAsync(contents, 0, contents.Length, cancellationToken).ConfigureAwait(false);
238  }
239 
241  public Task<IReadOnlyList<string>> GetDirectories(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
242  {
243  path = ResolvePath(path);
244  var results = new List<string>();
245  cancellationToken.ThrowIfCancellationRequested();
246  foreach (var I in Directory.EnumerateDirectories(path))
247  {
248  results.Add(I);
249  cancellationToken.ThrowIfCancellationRequested();
250  }
251  return (IReadOnlyList<string>)results;
252  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
253 
255  public Task<IReadOnlyList<string>> GetFiles(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
256  {
257  path = ResolvePath(path);
258  var results = new List<string>();
259  cancellationToken.ThrowIfCancellationRequested();
260  foreach (var I in Directory.EnumerateFiles(path))
261  {
262  results.Add(I);
263  cancellationToken.ThrowIfCancellationRequested();
264  }
265  return (IReadOnlyList<string>)results;
266  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
267 
269  public Task CreateSymlink(string target, string link, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
270  {
271  target = ResolvePath(target);
272  link = ResolvePath(link);
273 
274  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
275 
277  public async Task<byte[]> DownloadFile(Uri url, CancellationToken cancellationToken)
278  {
279  //DownloadDataTaskAsync can't be cancelled and is shittily written, don't use it
280  using (var wc = new WebClient())
281  {
282  var tcs = new TaskCompletionSource<byte[]>();
283  wc.DownloadDataCompleted += (a, b) =>
284  {
285  if (b.Error != null)
286  tcs.TrySetException(b.Error);
287  else if (b.Cancelled)
288  tcs.TrySetCanceled();
289  else
290  tcs.TrySetResult(b.Result);
291  };
292  wc.DownloadDataAsync(url);
293  using (cancellationToken.Register(() =>
294  {
295  wc.CancelAsync();
296  tcs.TrySetCanceled();
297  }))
298  return await tcs.Task.ConfigureAwait(false);
299  }
300  }
301 
303  public Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
304  {
305  path = ResolvePath(path);
306  if (zipFileBytes == null)
307  throw new ArgumentNullException(nameof(zipFileBytes));
308 
309  using (var ms = new MemoryStream(zipFileBytes))
310  using (var archive = new ZipArchive(ms, ZipArchiveMode.Read))
311  archive.ExtractToDirectory(path);
312  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
313 
315  public bool PathContainsParentAccess(string path) => path?.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Any(x => x == "..") ?? throw new ArgumentNullException(nameof(path));
316  }
317 }
async Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content
-
string ConcatPath(params string[] paths)
Combines an array of strings into a path
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Globalization;
4 using System.IO;
5 using System.IO.Compression;
6 using System.Linq;
7 using System.Net;
8 using System.Threading;
9 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.IO
12 {
17  {
21  public const int DefaultBufferSize = 4096;
22 
29  static async Task NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellationToken)
30  {
31  var tasks = new List<Task>();
32 
33  foreach (var subDir in dir.EnumerateDirectories())
34  {
35  cancellationToken.ThrowIfCancellationRequested();
36 
37  // if below succeeds this is probably a symlink
38  if (!subDir.Attributes.HasFlag(FileAttributes.Directory) || subDir.Attributes.HasFlag(FileAttributes.ReparsePoint))
39  subDir.Delete();
40  else
41  tasks.Add(NormalizeAndDelete(subDir, cancellationToken));
42  }
43 
44  foreach (var file in dir.EnumerateFiles())
45  {
46  cancellationToken.ThrowIfCancellationRequested();
47  file.Attributes = FileAttributes.Normal;
48  file.Delete();
49  }
50 
51  await Task.WhenAll(tasks).ConfigureAwait(false);
52  cancellationToken.ThrowIfCancellationRequested();
53  dir.Delete(true);
54  }
55 
61  static FileStream OpenWriteStream(string path) => new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite, DefaultBufferSize, true);
62 
71  IEnumerable<Task> CopyDirectoryImpl(string src, string dest, IEnumerable<string> ignore, CancellationToken cancellationToken)
72  {
73  var dir = new DirectoryInfo(src);
74  var atLeastOneSubDir = false;
75  foreach (var I in dir.EnumerateDirectories())
76  {
77  if (ignore != null && ignore.Contains(I.Name))
78  continue;
79  foreach (var J in CopyDirectoryImpl(I.FullName, Path.Combine(dest, I.Name), null, cancellationToken))
80  {
81  atLeastOneSubDir = true;
82  yield return J;
83  }
84  }
85 
86  async Task CopyThisDirectory()
87  {
88  if (!atLeastOneSubDir)
89  await CreateDirectory(dest, cancellationToken).ConfigureAwait(false); // save on createdir calls
90 
91  var tasks = new List<Task>();
92 
93  await dir.EnumerateFiles().ToAsyncEnumerable().ForEachAsync(fileInfo =>
94  {
95  if (ignore != null && ignore.Contains(fileInfo.Name))
96  return;
97  tasks.Add(CopyFile(fileInfo.FullName, Path.Combine(dest, fileInfo.Name), cancellationToken));
98  }).ConfigureAwait(false);
99 
100  await Task.WhenAll(tasks).ConfigureAwait(false);
101  }
102 
103  yield return CopyThisDirectory();
104  }
105 
107  public async Task CopyDirectory(string src, string dest, IEnumerable<string> ignore, CancellationToken cancellationToken)
108  {
109  if (dest == null)
110  throw new ArgumentNullException(nameof(src));
111  if (dest == null)
112  throw new ArgumentNullException(nameof(src));
113 
114  src = ResolvePath(src);
115  dest = ResolvePath(dest);
116  foreach (var directoryCopy in CopyDirectoryImpl(src, dest, ignore, cancellationToken))
117  await directoryCopy.ConfigureAwait(false);
118  }
119 
121  public string ConcatPath(params string[] paths)
122  {
123  if (paths == null)
124  throw new ArgumentNullException(nameof(paths));
125  return Path.Combine(paths);
126  }
127 
129  public async Task CopyFile(string src, string dest, CancellationToken cancellationToken)
130  {
131  if (src == null)
132  throw new ArgumentNullException(nameof(src));
133  if (dest == null)
134  throw new ArgumentNullException(nameof(dest));
135  using (var srcStream = new FileStream(ResolvePath(src), FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, DefaultBufferSize, true))
136  using (var destStream = new FileStream(ResolvePath(dest), FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, DefaultBufferSize, true))
137  await srcStream.CopyToAsync(destStream, 81920, cancellationToken).ConfigureAwait(false);
138  }
139 
141  public Task CreateDirectory(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.CreateDirectory(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
142 
144  public async Task DeleteDirectory(string path, CancellationToken cancellationToken)
145  {
146  path = ResolvePath(path);
147  var di = new DirectoryInfo(path);
148  if (!di.Exists)
149  return;
150  await NormalizeAndDelete(di, cancellationToken).ConfigureAwait(false);
151  }
152 
154  public Task DeleteFile(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Delete(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
155 
157  public Task<bool> FileExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => File.Exists(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
158 
160  public Task<bool> DirectoryExists(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() => Directory.Exists(ResolvePath(path)), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
161 
163  public string GetDirectoryName(string path) => Path.GetDirectoryName(path ?? throw new ArgumentNullException(nameof(path)));
164 
166  public string GetFileName(string path) => Path.GetFileName(path ?? throw new ArgumentNullException(nameof(path)));
167 
169  public string GetFileNameWithoutExtension(string path) => Path.GetFileNameWithoutExtension(path ?? throw new ArgumentNullException(nameof(path)));
170 
172  public Task<List<string>> GetFilesWithExtension(string path, string extension, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
173  {
174  path = ResolvePath(path);
175  if (extension == null)
176  throw new ArgumentNullException(extension);
177  var results = new List<string>();
178  foreach (var I in Directory.EnumerateFiles(path, String.Format(CultureInfo.InvariantCulture, "*.{0}", extension), SearchOption.TopDirectoryOnly))
179  {
180  cancellationToken.ThrowIfCancellationRequested();
181  results.Add(I);
182  }
183 
184  return results;
185  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
186 
188  public Task MoveFile(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
189  {
190  if (destination == null)
191  throw new ArgumentNullException(nameof(destination));
192  source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
193  destination = ResolvePath(destination);
194  File.Move(source, destination);
195  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
196 
198  public Task MoveDirectory(string source, string destination, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
199  {
200  if (destination == null)
201  throw new ArgumentNullException(nameof(destination));
202  source = ResolvePath(source ?? throw new ArgumentNullException(nameof(source)));
203  destination = ResolvePath(destination);
204  Directory.Move(source, destination);
205  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
206 
208  public async Task<byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
209  {
210  path = ResolvePath(path);
211  using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, DefaultBufferSize, true))
212  {
213  byte[] buf;
214  buf = new byte[file.Length];
215  await file.ReadAsync(buf, 0, (int)file.Length, cancellationToken).ConfigureAwait(false);
216  return buf;
217  }
218  }
219 
221  public virtual string ResolvePath(string path) => Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path)));
222 
224  public async Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
225  {
226  path = ResolvePath(path);
227  using (var file = OpenWriteStream(path))
228  await file.WriteAsync(contents, 0, contents.Length, cancellationToken).ConfigureAwait(false);
229  }
230 
232  public Task<IReadOnlyList<string>> GetDirectories(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
233  {
234  path = ResolvePath(path);
235  var results = new List<string>();
236  cancellationToken.ThrowIfCancellationRequested();
237  foreach (var I in Directory.EnumerateDirectories(path))
238  {
239  results.Add(I);
240  cancellationToken.ThrowIfCancellationRequested();
241  }
242 
243  return (IReadOnlyList<string>)results;
244  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
245 
247  public Task<IReadOnlyList<string>> GetFiles(string path, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
248  {
249  path = ResolvePath(path);
250  var results = new List<string>();
251  cancellationToken.ThrowIfCancellationRequested();
252  foreach (var I in Directory.EnumerateFiles(path))
253  {
254  results.Add(I);
255  cancellationToken.ThrowIfCancellationRequested();
256  }
257 
258  return (IReadOnlyList<string>)results;
259  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
260 
262  public async Task<byte[]> DownloadFile(Uri url, CancellationToken cancellationToken)
263  {
264  // DownloadDataTaskAsync can't be cancelled and is shittily written, don't use it
265  using (var wc = new WebClient())
266  {
267  var tcs = new TaskCompletionSource<byte[]>();
268  wc.DownloadDataCompleted += (a, b) =>
269  {
270  if (b.Error != null)
271  tcs.TrySetException(b.Error);
272  else if (b.Cancelled)
273  tcs.TrySetCanceled();
274  else
275  tcs.TrySetResult(b.Result);
276  };
277  wc.DownloadDataAsync(url);
278  using (cancellationToken.Register(() =>
279  {
280  wc.CancelAsync();
281  tcs.TrySetCanceled();
282  }))
283  return await tcs.Task.ConfigureAwait(false);
284  }
285  }
286 
288  public Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
289  {
290  path = ResolvePath(path);
291  if (zipFileBytes == null)
292  throw new ArgumentNullException(nameof(zipFileBytes));
293 
294  using (var ms = new MemoryStream(zipFileBytes))
295  using (var archive = new ZipArchive(ms, ZipArchiveMode.Read))
296  archive.ExtractToDirectory(path);
297  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
298 
300  public bool PathContainsParentAccess(string path) => path?.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }).Any(x => x == "..") ?? throw new ArgumentNullException(nameof(path));
301  }
302 }
async Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content
+
string ConcatPath(params string[] paths)
Combines an array of strings into a path
-
async Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
-
async Task CopyFile(string src, string dest, CancellationToken cancellationToken)
Copy a file from src to dest
+
async Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+
async Task CopyFile(string src, string dest, CancellationToken cancellationToken)
Copy a file from src to dest
-
static async Task NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellationToken)
Recursively empty a directory
-
async Task AppendAllText(string path, string additional_contents, CancellationToken cancellationToken)
-
async Task CopyDirectory(string src, string dest, IEnumerable< string > ignore, CancellationToken cancellationToken)
Copies a directory from src to dest
-
async Task< byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
Returns all the contents of a file at path as a byte array
-
IEnumerable< Task > CopyDirectoryImpl(string src, string dest, IEnumerable< string > ignore, CancellationToken cancellationToken)
Copies a directory from src to dest
-
async Task< byte[]> DownloadFile(Uri url, CancellationToken cancellationToken)
Downloads a file from url
-
IIOManager that resolves paths to Environment.CurrentDirectory
+
static async Task NormalizeAndDelete(DirectoryInfo dir, CancellationToken cancellationToken)
Recursively empty a directory
+
async Task CopyDirectory(string src, string dest, IEnumerable< string > ignore, CancellationToken cancellationToken)
Copies a directory from src to dest
+
async Task< byte[]> ReadAllBytes(string path, CancellationToken cancellationToken)
Returns all the contents of a file at path as a byte array
+
IEnumerable< Task > CopyDirectoryImpl(string src, string dest, IEnumerable< string > ignore, CancellationToken cancellationToken)
Copies a directory from src to dest
+
async Task< byte[]> DownloadFile(Uri url, CancellationToken cancellationToken)
Downloads a file from url
+
IIOManager that resolves paths to Environment.CurrentDirectory
Interface for using filesystems
Definition: IIOManager.cs:11
diff --git a/_design_time_db_context_factory_helpers_8cs_source.html b/_design_time_db_context_factory_helpers_8cs_source.html index 7de992db1d..583948d304 100644 --- a/_design_time_db_context_factory_helpers_8cs_source.html +++ b/_design_time_db_context_factory_helpers_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DesignTimeDbContextFactoryHelpers.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Configuration;
2 using Microsoft.Extensions.Options;
3 using System.IO;
4 using System.Reflection;
6 
7 namespace Tgstation.Server.Host.Models.Migrations
8 {
13  {
17  const string RootJson = "appsettings.json";
21  const string DevJson = "appsettings.Development.json";
22 
24  public static IOptions<DatabaseConfiguration> GetDbContextOptions()
25  {
26  var builder = new ConfigurationBuilder();
27  builder.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
28  builder.AddJsonFile(RootJson);
29  builder.AddJsonFile(DevJson);
30  var configuration = builder.Build();
31  return Options.Create(configuration.GetSection(DatabaseConfiguration.Section).Get<DatabaseConfiguration>());
32  }
33  }
34 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Configuration;
2 using Microsoft.Extensions.Options;
3 using System.IO;
4 using System.Reflection;
6 
7 namespace Tgstation.Server.Host.Models.Migrations
8 {
13  {
17  const string RootJson = "appsettings.json";
18 
22  const string DevJson = "appsettings.Development.json";
23 
28  public static IOptions<DatabaseConfiguration> GetDbContextOptions()
29  {
30  var builder = new ConfigurationBuilder();
31  builder.SetBasePath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
32  builder.AddJsonFile(RootJson);
33  builder.AddJsonFile(DevJson);
34  var configuration = builder.Build();
35  return Options.Create(configuration.GetSection(DatabaseConfiguration.Section).Get<DatabaseConfiguration>());
36  }
37  }
38 }
static IOptions< DatabaseConfiguration > GetDbContextOptions()
Get the IOptions<TOptions> for the DatabaseConfiguration
Contains helpers for creating design time Models.DatabaseContext<TParentContext>s ...
diff --git a/_discord_provider_8cs_source.html b/_discord_provider_8cs_source.html index 3566d21e7d..27e4fa02db 100644 --- a/_discord_provider_8cs_source.html +++ b/_discord_provider_8cs_source.html @@ -92,19 +92,19 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DiscordProvider.cs
-Go to the documentation of this file.
1 using Discord;
2 using Discord.WebSocket;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Components.Chat.Providers
12 {
16  sealed class DiscordProvider : Provider
17  {
19  public override bool Connected => client.ConnectionState == ConnectionState.Connected;
20 
22  public override string BotMention
23  {
24  get
25  {
26  if (!Connected)
27  throw new InvalidOperationException("Provider not connected");
28  return NormalizeMention(client.CurrentUser.Mention);
29  }
30  }
31 
35  readonly ILogger<DiscordProvider> logger;
36 
40  readonly DiscordSocketClient client;
41 
45  readonly string botToken;
46 
50  readonly List<ulong> mappedChannels;
51 
57  static string NormalizeMention(string fromDiscord) => fromDiscord.Replace("!", "", StringComparison.Ordinal);
58 
64  public DiscordProvider(ILogger<DiscordProvider> logger, string botToken)
65  {
66  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
68  client = new DiscordSocketClient();
69  client.MessageReceived += Client_MessageReceived;
70  mappedChannels = new List<ulong>();
71  }
72 
74  public override void Dispose() => client.Dispose();
75 
81  Task Client_MessageReceived(SocketMessage e)
82  {
83  if (e.Author.Id == client.CurrentUser.Id)
84  return Task.CompletedTask;
85 
86  var pm = e.Channel is IPrivateChannel;
87 
88  if (!pm && !mappedChannels.Contains(e.Channel.Id))
89  return e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id) ? SendMessage(e.Channel.Id, "I do not respond to this channel!", default) : Task.CompletedTask;
90 
91  var result = new Message
92  {
93  Content = e.Content,
94  User = new User
95  {
96  RealId = e.Author.Id,
97  Channel = new Channel
98  {
99  RealId = e.Channel.Id,
100  IsPrivateChannel = pm,
101  ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
102  FriendlyName = e.Channel.Name
103  //isAdmin and Tag populated by manager
104  },
105  FriendlyName = e.Author.Username,
106  Mention = NormalizeMention(e.Author.Mention)
107  }
108  };
109  EnqueueMessage(result);
110  return Task.CompletedTask;
111  }
112 
114  public override async Task<bool> Connect(CancellationToken cancellationToken)
115  {
116  if (Connected)
117  return true;
118 
119  try
120  {
121  await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
122 
123  cancellationToken.ThrowIfCancellationRequested();
124 
125  await client.StartAsync().ConfigureAwait(false);
126 
127  var channelsAvailable = new TaskCompletionSource<object>();
128  client.Ready += () =>
129  {
130  channelsAvailable.TrySetResult(null);
131  return Task.CompletedTask;
132  };
133  using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
134  await channelsAvailable.Task.ConfigureAwait(false);
135  }
136  catch (OperationCanceledException)
137  {
138  throw;
139  }
140  catch (Exception e)
141  {
142  logger.LogWarning("Error connecting to Discord: {0}", e);
143  return false;
144  }
145 
146  return true;
147  }
148 
150  public override async Task Disconnect(CancellationToken cancellationToken)
151  {
152  if (!Connected)
153  return;
154 
155  try
156  {
157  await client.StopAsync().ConfigureAwait(false);
158  cancellationToken.ThrowIfCancellationRequested();
159  await client.LogoutAsync().ConfigureAwait(false);
160  }
161  catch (OperationCanceledException)
162  {
163  throw;
164  }
165  catch (Exception e)
166  {
167  logger.LogWarning("Error disconnecting from discord: {0}", e);
168  }
169  }
170 
172  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
173  {
174  if (channels == null)
175  throw new ArgumentNullException(nameof(channels));
176 
177  if (!Connected)
178  throw new InvalidOperationException("Provider not connected!");
179 
180  Channel GetChannelForChatChannel(ChatChannel channel)
181  {
182  if (!channel.DiscordChannelId.HasValue)
183  throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
184 
185  if (!(client.GetChannel(channel.DiscordChannelId.Value) is ITextChannel discordChannel))
186  return null;
187 
188  return new Channel
189  {
190  RealId = discordChannel.Id,
191  IsAdminChannel = channel.IsAdminChannel == true,
192  ConnectionName = discordChannel.Guild.Name,
193  FriendlyName = discordChannel.Name,
194  IsPrivateChannel = false,
195  Tag = channel.Tag
196  };
197  };
198 
199  var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null).ToList();
200 
201  lock (this)
202  {
203  mappedChannels.Clear();
204  mappedChannels.AddRange(enumerator.Select(x => x.RealId));
205  }
206 
207  return Task.FromResult<IReadOnlyList<Channel>>(enumerator);
208  }
209 
211  public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
212  {
213  try
214  {
215  var channel = client.GetChannel(channelId) as IMessageChannel;
216  await (channel?.SendMessageAsync(message, false, null, new RequestOptions
217  {
218  CancelToken = cancellationToken
219  }) ?? Task.CompletedTask).ConfigureAwait(false);
220  }
221  catch (OperationCanceledException)
222  {
223  throw;
224  }
225  catch (Exception e)
226  {
227  logger.LogWarning("Error sending discord message: {0}", e);
228  }
229  }
230  }
231 }
Indicates a chat channel
Definition: ChatChannel.cs:8
+Go to the documentation of this file.
1 using Discord;
2 using Discord.WebSocket;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Components.Chat.Providers
12 {
16  sealed class DiscordProvider : Provider
17  {
19  public override bool Connected => client.ConnectionState == ConnectionState.Connected;
20 
22  public override string BotMention
23  {
24  get
25  {
26  if (!Connected)
27  throw new InvalidOperationException("Provider not connected");
28  return NormalizeMention(client.CurrentUser.Mention);
29  }
30  }
31 
35  readonly ILogger<DiscordProvider> logger;
36 
40  readonly DiscordSocketClient client;
41 
45  readonly string botToken;
46 
50  readonly List<ulong> mappedChannels;
51 
57  static string NormalizeMention(string fromDiscord) => fromDiscord.Replace("!", String.Empty, StringComparison.Ordinal);
58 
64  public DiscordProvider(ILogger<DiscordProvider> logger, string botToken)
65  {
66  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
68  client = new DiscordSocketClient();
69  client.MessageReceived += Client_MessageReceived;
70  mappedChannels = new List<ulong>();
71  }
72 
74  public override void Dispose() => client.Dispose();
75 
81  Task Client_MessageReceived(SocketMessage e)
82  {
83  if (e.Author.Id == client.CurrentUser.Id)
84  return Task.CompletedTask;
85 
86  var pm = e.Channel is IPrivateChannel;
87 
88  if (!pm && !mappedChannels.Contains(e.Channel.Id))
89  return e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id) ? SendMessage(e.Channel.Id, "I do not respond to this channel!", default) : Task.CompletedTask;
90 
91  var result = new Message
92  {
93  Content = e.Content,
94  User = new User
95  {
96  RealId = e.Author.Id,
97  Channel = new Channel
98  {
99  RealId = e.Channel.Id,
100  IsPrivateChannel = pm,
101  ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
102  FriendlyName = e.Channel.Name
103 
104  // isAdmin and Tag populated by manager
105  },
106  FriendlyName = e.Author.Username,
107  Mention = NormalizeMention(e.Author.Mention)
108  }
109  };
110  EnqueueMessage(result);
111  return Task.CompletedTask;
112  }
113 
115  public override async Task<bool> Connect(CancellationToken cancellationToken)
116  {
117  if (Connected)
118  return true;
119 
120  try
121  {
122  await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
123 
124  cancellationToken.ThrowIfCancellationRequested();
125 
126  await client.StartAsync().ConfigureAwait(false);
127 
128  var channelsAvailable = new TaskCompletionSource<object>();
129  client.Ready += () =>
130  {
131  channelsAvailable.TrySetResult(null);
132  return Task.CompletedTask;
133  };
134  using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
135  await channelsAvailable.Task.ConfigureAwait(false);
136  }
137  catch (OperationCanceledException)
138  {
139  throw;
140  }
141  catch (Exception e)
142  {
143  logger.LogWarning("Error connecting to Discord: {0}", e);
144  return false;
145  }
146 
147  return true;
148  }
149 
151  public override async Task Disconnect(CancellationToken cancellationToken)
152  {
153  if (!Connected)
154  return;
155 
156  try
157  {
158  await client.StopAsync().ConfigureAwait(false);
159  cancellationToken.ThrowIfCancellationRequested();
160  await client.LogoutAsync().ConfigureAwait(false);
161  }
162  catch (OperationCanceledException)
163  {
164  throw;
165  }
166  catch (Exception e)
167  {
168  logger.LogWarning("Error disconnecting from discord: {0}", e);
169  }
170  }
171 
173  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
174  {
175  if (channels == null)
176  throw new ArgumentNullException(nameof(channels));
177 
178  if (!Connected)
179  throw new InvalidOperationException("Provider not connected!");
180 
181  Channel GetChannelForChatChannel(ChatChannel channel)
182  {
183  if (!channel.DiscordChannelId.HasValue)
184  throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
185 
186  if (client.GetChannel(channel.DiscordChannelId.Value) is ITextChannel discordChannel)
187  return new Channel
188  {
189  RealId = discordChannel.Id,
190  IsAdminChannel = channel.IsAdminChannel == true,
191  ConnectionName = discordChannel.Guild.Name,
192  FriendlyName = discordChannel.Name,
193  IsPrivateChannel = false,
194  Tag = channel.Tag
195  };
196 
197  return null;
198  }
199 
200  var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null).ToList();
201 
202  lock (this)
203  {
204  mappedChannels.Clear();
205  mappedChannels.AddRange(enumerator.Select(x => x.RealId));
206  }
207 
208  return Task.FromResult<IReadOnlyList<Channel>>(enumerator);
209  }
210 
212  public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
213  {
214  try
215  {
216  var channel = client.GetChannel(channelId) as IMessageChannel;
217  await (channel?.SendMessageAsync(message, false, null, new RequestOptions
218  {
219  CancelToken = cancellationToken
220  }) ?? Task.CompletedTask).ConfigureAwait(false);
221  }
222  catch (OperationCanceledException)
223  {
224  throw;
225  }
226  catch (Exception e)
227  {
228  logger.LogWarning("Error sending discord message: {0}", e);
229  }
230  }
231  }
232 }
Indicates a chat channel
Definition: ChatChannel.cs:8
-
override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
Send a message to the IProvider
+
override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
Send a message to the IProvider
Task Client_MessageReceived(SocketMessage e)
Handle a message recieved from Discord
-
override Task< IReadOnlyList< Channel > > MapChannels(IEnumerable< ChatChannel > channels, CancellationToken cancellationToken)
+
override Task< IReadOnlyList< Channel > > MapChannels(IEnumerable< ChatChannel > channels, CancellationToken cancellationToken)
Represents a message recieved by a IProvider
Definition: Message.cs:6
-
override async Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
+
override async Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
-
override async Task< bool > Connect(CancellationToken cancellationToken)
Attempt to connect the IProvider
+
override async Task< bool > Connect(CancellationToken cancellationToken)
Attempt to connect the IProvider
readonly ILogger< DiscordProvider > logger
The ILogger for the DiscordProvider
readonly string botToken
The token used for connecting to discord
diff --git a/_dmb_factory_8cs_source.html b/_dmb_factory_8cs_source.html index 1a6bb53368..b2115b0564 100644 --- a/_dmb_factory_8cs_source.html +++ b/_dmb_factory_8cs_source.html @@ -92,9 +92,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DmbFactory.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components.Compiler
13 {
18  {
20  public Task OnNewerDmb
21  {
22  get
23  {
24  lock (this)
25  return newerDmbTcs.Task;
26  }
27  }
28 
30  public bool DmbAvailable => nextDmbProvider != null;
31 
43  readonly ILogger<DmbFactory> logger;
44 
48  readonly Api.Models.Instance instance;
52  readonly CancellationTokenSource cleanupCts;
53 
61  TaskCompletionSource<object> newerDmbTcs;
66 
67  Dictionary<long, int> jobLockCounts;
68 
76  public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger<DmbFactory> logger, Api.Models.Instance instance)
77  {
78  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
79  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
80  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
81  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
82 
83  cleanupTask = Task.CompletedTask;
84  newerDmbTcs = new TaskCompletionSource<object>();
85  cleanupCts = new CancellationTokenSource();
86  jobLockCounts = new Dictionary<long, int>();
87  }
88 
90  public void Dispose() => cleanupCts.Dispose(); //we don't dispose nextDmbProvider here, since it might be the only thing we have
91 
96  void CleanJob(CompileJob job)
97  {
98  async Task HandleCleanup()
99  {
100  var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
101  Task otherTask;
102  //lock (this) //already locked below
103  otherTask = cleanupTask;
104  await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
105  }
106  lock (this)
107  {
108  if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
109  {
110  jobLockCounts.Remove(job.Id);
111  logger.LogDebug("Cleaning compile job {0} => {1}", job.Id, job.DirectoryName);
112  cleanupTask = HandleCleanup();
113  }
114  else
115  {
116  var decremented = --jobLockCounts[job.Id];
117  logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented);
118  }
119  }
120  }
121 
123  public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
124  {
125  if (job == null)
126  throw new ArgumentNullException(nameof(job));
127 
128  var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
129  if (newProvider == null)
130  return;
131  lock (this)
132  {
133  nextDmbProvider?.Dispose();
134  nextDmbProvider = newProvider;
135  newerDmbTcs.SetResult(nextDmbProvider);
136  newerDmbTcs = new TaskCompletionSource<object>();
137  }
138  }
139 
141  public IDmbProvider LockNextDmb(int lockCount)
142  {
143  if (!DmbAvailable)
144  throw new InvalidOperationException("No .dmb available!");
145  if (lockCount < 0)
146  throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
147  lock (this)
148  {
149  var jobId = nextDmbProvider.CompileJob.Id;
150  var incremented = jobLockCounts[jobId] += lockCount;
151  logger.LogTrace("Compile job {0} lock count now: {1}", jobId, incremented);
152  return nextDmbProvider;
153  }
154  }
155 
157  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
158  {
159  //where complete clause not necessary, only successful COMPILEjobs get in the db
160  var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id)
161  .OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
162  if (cj == default(CompileJob))
163  return;
164  await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
165  //we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
166  });
167 
169  public async Task StopAsync(CancellationToken cancellationToken)
170  {
171  using (cancellationToken.Register(() => cleanupCts.Cancel()))
172  await cleanupTask.ConfigureAwait(false);
173  }
174 
176  public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
177  {
178  if (compileJob == null)
179  throw new ArgumentNullException(nameof(compileJob));
180 
181  //ensure we have the entire compile job tree
182  await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id)
183  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
184  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
185  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
186  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); //can't wait to see that query
187 
188  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
189  var providerSubmitted = false;
190  var newProvider = new DmbProvider(compileJob, ioManager, () =>
191  {
192  if (providerSubmitted)
193  CleanJob(compileJob);
194  });
195 
196  try
197  {
198  var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
199  var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
200 
201  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
202  {
203  logger.LogWarning("Error loading compile job, .dmb missing!");
204  return null; //omae wa mou shinderu
205  }
206 
207  lock (this)
208  {
209  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
210  {
211  value = 1;
212  jobLockCounts.Add(compileJob.Id, 1);
213  }
214  else
215  jobLockCounts[compileJob.Id] = ++value;
216 
217  logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
218 
219  providerSubmitted = true;
220  return newProvider;
221  }
222  }
223  finally
224  {
225  if (!providerSubmitted)
226  newProvider.Dispose();
227  }
228  }
229 
231  public async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
232  {
233  List<long> jobIdsToSkip;
234  //don't clean locked directories
235  lock (this)
236  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
237 
238  List<string> jobUidsToNotErase = null;
239 
240  //find the uids of locked directories
241  await databaseContextFactory.UseContext(async db =>
242  {
243  jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
244  }).ConfigureAwait(false);
245 
246  //add the other exemption
247  if (exceptThisOne != null)
248  jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
249 
250  //cleanup
251  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
252  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
253  int deleting = 0;
254  var tasks = directories.Select(async x =>
255  {
256  var nameOnly = ioManager.GetFileName(x);
257  if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
258  return;
259  try
260  {
261  ++deleting;
262  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
263  }
264  catch (OperationCanceledException)
265  {
266  throw;
267  }
268  catch (Exception e)
269  {
270  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
271  }
272  }).ToList();
273  if (deleting > 0)
274  {
275  logger.LogDebug("Cleaning {0} unused game folders...", deleting);
276  await Task.WhenAll().ConfigureAwait(false);
277  }
278  }
279  }
280 }
-
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:65
-
Task cleanupTask
Task representing calls to CleanJob(CompileJob)
Definition: DmbFactory.cs:57
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components.Compiler
13 {
18  {
20  public Task OnNewerDmb
21  {
22  get
23  {
24  lock (this)
25  return newerDmbTcs.Task;
26  }
27  }
28 
30  public bool DmbAvailable => nextDmbProvider != null;
31 
36 
41 
45  readonly ILogger<DmbFactory> logger;
46 
50  readonly Api.Models.Instance instance;
51 
55  readonly CancellationTokenSource cleanupCts;
56 
61 
65  TaskCompletionSource<object> newerDmbTcs;
66 
71 
72  Dictionary<long, int> jobLockCounts;
73 
81  public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger<DmbFactory> logger, Api.Models.Instance instance)
82  {
83  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
84  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
85  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
86  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
87 
88  cleanupTask = Task.CompletedTask;
89  newerDmbTcs = new TaskCompletionSource<object>();
90  cleanupCts = new CancellationTokenSource();
91  jobLockCounts = new Dictionary<long, int>();
92  }
93 
95  public void Dispose() => cleanupCts.Dispose(); // we don't dispose nextDmbProvider here, since it might be the only thing we have
96 
102  {
103  async Task HandleCleanup()
104  {
105  var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
106  Task otherTask;
107 
108  // lock (this) //already locked below
109  otherTask = cleanupTask;
110  await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
111  }
112 
113  lock (this)
114  if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
115  {
116  jobLockCounts.Remove(job.Id);
117  logger.LogDebug("Cleaning compile job {0} => {1}", job.Id, job.DirectoryName);
118  cleanupTask = HandleCleanup();
119  }
120  else
121  {
122  var decremented = --jobLockCounts[job.Id];
123  logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented);
124  }
125  }
126 
128  public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
129  {
130  if (job == null)
131  throw new ArgumentNullException(nameof(job));
132 
133  var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
134  if (newProvider == null)
135  return;
136  lock (this)
137  {
138  nextDmbProvider?.Dispose();
139  nextDmbProvider = newProvider;
140  newerDmbTcs.SetResult(nextDmbProvider);
141  newerDmbTcs = new TaskCompletionSource<object>();
142  }
143  }
144 
146  public IDmbProvider LockNextDmb(int lockCount)
147  {
148  if (!DmbAvailable)
149  throw new InvalidOperationException("No .dmb available!");
150  if (lockCount < 0)
151  throw new ArgumentOutOfRangeException(nameof(lockCount), lockCount, "lockCount must be greater than or equal to 0!");
152  lock (this)
153  {
154  var jobId = nextDmbProvider.CompileJob.Id;
155  var incremented = jobLockCounts[jobId] += lockCount;
156  logger.LogTrace("Compile job {0} lock count now: {1}", jobId, incremented);
157  return nextDmbProvider;
158  }
159  }
160 
162  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
163  {
164  // where complete clause not necessary, only successful COMPILEjobs get in the db
165  var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id)
166  .OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
167  if (cj == default(CompileJob))
168  return;
169  await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
170 
171  // we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
172  });
173 
175  public async Task StopAsync(CancellationToken cancellationToken)
176  {
177  using (cancellationToken.Register(() => cleanupCts.Cancel()))
178  await cleanupTask.ConfigureAwait(false);
179  }
180 
182  #pragma warning disable CA1506 // TODO: Decomplexify
183  public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
184  {
185  if (compileJob == null)
186  throw new ArgumentNullException(nameof(compileJob));
187 
188  // ensure we have the entire compile job tree
189  await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id)
190  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
191  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
192  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
193  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); // can't wait to see that query
194 
195  logger.LogTrace("Loading compile job {0}...", compileJob.Id);
196  var providerSubmitted = false;
197  var newProvider = new DmbProvider(compileJob, ioManager, () =>
198  {
199  if (providerSubmitted)
200  CleanJob(compileJob);
201  });
202 
203  try
204  {
205  var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
206  var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
207 
208  if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
209  {
210  logger.LogWarning("Error loading compile job, .dmb missing!");
211  return null; // omae wa mou shinderu
212  }
213 
214  lock (this)
215  {
216  if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
217  {
218  value = 1;
219  jobLockCounts.Add(compileJob.Id, 1);
220  }
221  else
222  jobLockCounts[compileJob.Id] = ++value;
223 
224  logger.LogTrace("Compile job {0} lock count now: {1}", compileJob.Id, value);
225 
226  providerSubmitted = true;
227  return newProvider;
228  }
229  }
230  finally
231  {
232  if (!providerSubmitted)
233  newProvider.Dispose();
234  }
235  }
236  #pragma warning restore CA1506
237 
239  #pragma warning disable CA1506 // TODO: Decomplexify
240  public async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
241  {
242  List<long> jobIdsToSkip;
243 
244  // don't clean locked directories
245  lock (this)
246  jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
247 
248  List<string> jobUidsToNotErase = null;
249 
250  // find the uids of locked directories
251  await databaseContextFactory.UseContext(async db =>
252  {
253  jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
254  }).ConfigureAwait(false);
255 
256  // add the other exemption
257  if (exceptThisOne != null)
258  jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
259 
260  // cleanup
261  await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
262  var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
263  int deleting = 0;
264  var tasks = directories.Select(async x =>
265  {
266  var nameOnly = ioManager.GetFileName(x);
267  if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
268  return;
269  try
270  {
271  ++deleting;
272  await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
273  }
274  catch (OperationCanceledException)
275  {
276  throw;
277  }
278  catch (Exception e)
279  {
280  logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
281  }
282  }).ToList();
283  if (deleting > 0)
284  {
285  logger.LogDebug("Cleaning {0} unused game folders...", deleting);
286  await Task.WhenAll().ConfigureAwait(false);
287  }
288  }
289  #pragma warning restore CA1506
290  }
291 }
+
IDmbProvider nextDmbProvider
The latest DmbProvider
Definition: DmbFactory.cs:70
+
Task cleanupTask
Task representing calls to CleanJob(CompileJob)
Definition: DmbFactory.cs:60
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
@@ -102,25 +102,25 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: DmbFactory.cs:169
- -
readonly IIOManager ioManager
The IIOManager for the DmbFactory
Definition: DmbFactory.cs:39
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: DmbFactory.cs:175
+ +
readonly IIOManager ioManager
The IIOManager for the DmbFactory
Definition: DmbFactory.cs:40
-
async Task< IDmbProvider > FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
Gets a IDmbProvider for a given CompileJob
Definition: DmbFactory.cs:176
-
DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger< DmbFactory > logger, Api.Models.Instance instance)
Construct a DmbFactory
Definition: DmbFactory.cs:76
-
async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder exceptThisOne
Definition: DmbFactory.cs:231
-
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask
Definition: DmbFactory.cs:52
-
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory
Definition: DmbFactory.cs:43
+
async Task< IDmbProvider > FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
Gets a IDmbProvider for a given CompileJob
Definition: DmbFactory.cs:183
+
DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger< DmbFactory > logger, Api.Models.Instance instance)
Construct a DmbFactory
Definition: DmbFactory.cs:81
+
async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder exceptThisOne
Definition: DmbFactory.cs:240
+
readonly CancellationTokenSource cleanupCts
The CancellationTokenSource for cleanupTask
Definition: DmbFactory.cs:55
+
readonly ILogger< DmbFactory > logger
The ILogger for the DmbFactory
Definition: DmbFactory.cs:45
-
readonly Api.Models.Instance instance
The Api.Models.Instance for the DmbFactory
Definition: DmbFactory.cs:48
-
void CleanJob(CompileJob job)
Delete the Api.Models.Internal.CompileJob.DirectoryName of job
Definition: DmbFactory.cs:96
-
IDmbProvider LockNextDmb(int lockCount)
Gets the next IDmbProvider
Definition: DmbFactory.cs:141
+
readonly Api.Models.Instance instance
The Api.Models.Instance for the DmbFactory
Definition: DmbFactory.cs:50
+
void CleanJob(CompileJob job)
Delete the Api.Models.Internal.CompileJob.DirectoryName of job
Definition: DmbFactory.cs:101
+
IDmbProvider LockNextDmb(int lockCount)
Gets the next IDmbProvider
Definition: DmbFactory.cs:146
Guid DirectoryName
The Game folder the results were compiled into
Definition: CompileJob.cs:32
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the DmbFactory
Definition: DmbFactory.cs:35
-
TaskCompletionSource< object > newerDmbTcs
TaskCompletionSource<TResult> resulting in the latest DmbProvider yet to exist
Definition: DmbFactory.cs:61
-
async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
Load a new job into the ICompileJobConsumer
Definition: DmbFactory.cs:123
+
TaskCompletionSource< object > newerDmbTcs
TaskCompletionSource<TResult> resulting in the latest DmbProvider yet to exist
Definition: DmbFactory.cs:65
+
async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
Load a new job into the ICompileJobConsumer
Definition: DmbFactory.cs:128
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
Interface for using filesystems
Definition: IIOManager.cs:11
diff --git a/_dmb_provider_8cs_source.html b/_dmb_provider_8cs_source.html index a937a65a44..d1f650ec5e 100644 --- a/_dmb_provider_8cs_source.html +++ b/_dmb_provider_8cs_source.html @@ -92,24 +92,22 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DmbProvider.cs
-Go to the documentation of this file.
1 using System;
2 using Tgstation.Server.Host.IO;
4 
5 namespace Tgstation.Server.Host.Components.Compiler
6 {
8  sealed class DmbProvider : IDmbProvider
9  {
11  public string DmbName => String.Concat(CompileJob.DmeName, DreamMaker.DmbExtension);
12 
14  public string PrimaryDirectory => ioManager.ResolvePath(ioManager.ConcatPath(CompileJob.DirectoryName.ToString(), DreamMaker.ADirectoryName));
15 
17  public string SecondaryDirectory => ioManager.ResolvePath(ioManager.ConcatPath(CompileJob.DirectoryName.ToString(), DreamMaker.BDirectoryName));
18 
21 
25  public CompileJob CompileJob { get; }
26 
34  Action onDispose;
35 
42  public DmbProvider(CompileJob compileJob, IIOManager ioManager, Action onDispose)
43  {
44  CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob));
45  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
46  this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
47  }
48 
50  public void Dispose() => onDispose?.Invoke();
51 
53  public void KeepAlive() => onDispose = null;
54  }
55 }
string DmeName
The .dme file used for compilation
Definition: CompileJob.cs:20
+Go to the documentation of this file.
1 using System;
2 using Tgstation.Server.Host.IO;
4 
5 namespace Tgstation.Server.Host.Components.Compiler
6 {
8  sealed class DmbProvider : IDmbProvider
9  {
11  public string DmbName => String.Concat(CompileJob.DmeName, DreamMaker.DmbExtension);
12 
14  public string PrimaryDirectory => ioManager.ResolvePath(ioManager.ConcatPath(CompileJob.DirectoryName.ToString(), DreamMaker.ADirectoryName));
15 
17  public string SecondaryDirectory => ioManager.ResolvePath(ioManager.ConcatPath(CompileJob.DirectoryName.ToString(), DreamMaker.BDirectoryName));
18 
22  public CompileJob CompileJob { get; }
23 
28 
32  Action onDispose;
33 
40  public DmbProvider(CompileJob compileJob, IIOManager ioManager, Action onDispose)
41  {
42  CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob));
43  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
44  this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
45  }
46 
48  public void Dispose() => onDispose?.Invoke();
49 
51  public void KeepAlive() => onDispose = null;
52  }
53 }
string DmeName
The .dme file used for compilation
Definition: CompileJob.cs:20
-
const string BDirectoryName
Name of the secondary directory used for compilation
Definition: DreamMaker.cs:30
+
const string BDirectoryName
Name of the secondary directory used for compilation
Definition: DreamMaker.cs:31
const string ADirectoryName
Name of the primary directory used for compilation
Definition: DreamMaker.cs:26
- -
DmbProvider(CompileJob compileJob, IIOManager ioManager, Action onDispose)
Construct a DmbProvider
Definition: DmbProvider.cs:42
-
Action onDispose
The Action to run when Dispose is called
Definition: DmbProvider.cs:34
-
const string DmbExtension
Extension for .dmbs
Definition: DreamMaker.cs:34
-
readonly IIOManager ioManager
The IIOManager for the DmbProvider
Definition: DmbProvider.cs:30
+
DmbProvider(CompileJob compileJob, IIOManager ioManager, Action onDispose)
Construct a DmbProvider
Definition: DmbProvider.cs:40
+
Action onDispose
The Action to run when Dispose is called
Definition: DmbProvider.cs:32
+
const string DmbExtension
Extension for .dmbs
Definition: DreamMaker.cs:36
+
readonly IIOManager ioManager
The IIOManager for the DmbProvider
Definition: DmbProvider.cs:27
Guid DirectoryName
The Game folder the results were compiled into
Definition: CompileJob.cs:32
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
Interface for using filesystems
Definition: IIOManager.cs:11
-
RevisionInformation RevisionInformation
See Api.Models.CompileJob.RevisionInformation
Definition: CompileJob.cs:24
diff --git a/_dream_daemon_client_8cs_source.html b/_dream_daemon_client_8cs_source.html index 186baeb483..928b4730ed 100644 --- a/_dream_daemon_client_8cs_source.html +++ b/_dream_daemon_client_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonClient.cs
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 using Tgstation.Server.Api;
6 
7 namespace Tgstation.Server.Client.Components
8 {
11  {
19  readonly Instance instance;
20 
26  public DreamDaemonClient(IApiClient apiClient, Instance instance)
27  {
28  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
29  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
30  }
31 
33  public Task Shutdown(CancellationToken cancellationToken) => apiClient.Delete(Routes.DreamDaemon, instance.Id, cancellationToken);
34 
36  public Task<Job> Start(CancellationToken cancellationToken) => apiClient.Create<Job>(Routes.DreamDaemon, instance.Id, cancellationToken);
37 
39  public Task<Job> Restart(CancellationToken cancellationToken) => apiClient.Patch<Job>(Routes.DreamDaemon, instance.Id, cancellationToken);
40 
42  public Task<DreamDaemon> Read(CancellationToken cancellationToken) => apiClient.Read<DreamDaemon>(Routes.DreamDaemon, instance.Id, cancellationToken);
43 
45  public Task<DreamDaemon> Update(DreamDaemon dreamDaemon, CancellationToken cancellationToken) => apiClient.Update<DreamDaemon, DreamDaemon>(Routes.DreamDaemon, dreamDaemon ?? throw new ArgumentNullException(nameof(dreamDaemon)), instance.Id, cancellationToken);
46  }
47 }
DreamDaemonClient(IApiClient apiClient, Instance instance)
Construct a DreamDaemonClient
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 using Tgstation.Server.Api;
6 
7 namespace Tgstation.Server.Client.Components
8 {
11  {
16 
20  readonly Instance instance;
21 
27  public DreamDaemonClient(IApiClient apiClient, Instance instance)
28  {
29  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
30  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
31  }
32 
34  public Task Shutdown(CancellationToken cancellationToken) => apiClient.Delete(Routes.DreamDaemon, instance.Id, cancellationToken);
35 
37  public Task<Job> Start(CancellationToken cancellationToken) => apiClient.Create<Job>(Routes.DreamDaemon, instance.Id, cancellationToken);
38 
40  public Task<Job> Restart(CancellationToken cancellationToken) => apiClient.Patch<Job>(Routes.DreamDaemon, instance.Id, cancellationToken);
41 
43  public Task<DreamDaemon> Read(CancellationToken cancellationToken) => apiClient.Read<DreamDaemon>(Routes.DreamDaemon, instance.Id, cancellationToken);
44 
46  public Task<DreamDaemon> Update(DreamDaemon dreamDaemon, CancellationToken cancellationToken) => apiClient.Update<DreamDaemon, DreamDaemon>(Routes.DreamDaemon, dreamDaemon ?? throw new ArgumentNullException(nameof(dreamDaemon)), instance.Id, cancellationToken);
47  }
48 }
DreamDaemonClient(IApiClient apiClient, Instance instance)
Construct a DreamDaemonClient
@@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Routes to a server actions
Definition: Routes.cs:9
const string DreamDaemon
The Models.DreamDaemon controller
Definition: Routes.cs:44
-
readonly Instance instance
The Instance for the DreamDaemonClient
+
readonly Instance instance
The Instance for the DreamDaemonClient
Metadata about a server instance
Definition: Instance.cs:9
diff --git a/_dream_daemon_controller_8cs_source.html b/_dream_daemon_controller_8cs_source.html index c1fecb772f..a261f526ba 100644 --- a/_dream_daemon_controller_8cs_source.html +++ b/_dream_daemon_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Linq.Expressions;
7 using System.Net;
8 using System.Reflection;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
19 
20 namespace Tgstation.Server.Host.Controllers
21 {
25  [Route(Routes.DreamDaemon)]
26  public sealed class DreamDaemonController : ModelController<DreamDaemon>
27  {
36 
45  public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamDaemonController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
46  {
47  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
48  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
49  }
50 
52  [TgsAuthorize(DreamDaemonRights.Start)]
53  public override async Task<IActionResult> Create([FromBody] DreamDaemon model, CancellationToken cancellationToken)
54  {
55  //alias for launching DD
56  var instance = instanceManager.GetInstance(Instance);
57 
58  if (instance.Watchdog.Running)
59  return StatusCode((int)HttpStatusCode.Gone);
60 
61  var job = new Models.Job
62  {
63  Description = "Launch DreamDaemon",
64  CancelRight = (ulong)DreamDaemonRights.Shutdown,
65  CancelRightsType = RightsType.DreamDaemon,
67  StartedBy = AuthenticationContext.User
68  };
69  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false);
70  return Accepted(job.ToApi());
71  }
72 
74  [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)]
75  public override Task<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
76 
83  async Task<IActionResult> ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
84  {
85  var instance = instanceManager.GetInstance(Instance);
86  var dd = instance.Watchdog;
87 
88  var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0;
89  var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0;
90 
91  if (settings == null)
92  {
93  settings = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
94  if (settings == default)
95  return StatusCode((int)HttpStatusCode.Gone);
96  }
97 
98  var result = new DreamDaemon();
99  if (metadata)
100  {
101  var alphaActive = dd.AlphaIsActive;
102  var llp = dd.LastLaunchParameters;
103  var rstate = dd.RebootState;
104  result.AutoStart = settings.AutoStart;
105  result.CurrentPort = alphaActive ? llp?.PrimaryPort : llp?.SecondaryPort;
106  result.CurrentSecurity = llp?.SecurityLevel;
107  result.CurrentAllowWebclient = llp?.AllowWebClient;
108  result.PrimaryPort = settings.PrimaryPort;
109  result.AllowWebClient = settings.AllowWebClient;
110  result.Running = dd.Running;
111  result.SecondaryPort = settings.SecondaryPort;
112  result.SecurityLevel = settings.SecurityLevel;
113  result.SoftRestart = rstate == RebootState.Restart;
114  result.SoftShutdown = rstate == RebootState.Shutdown;
115  result.StartupTimeout = settings.StartupTimeout;
116  };
117 
118  if (revision)
119  {
120  var latestCompileJob = instance.LatestCompileJob();
121  result.ActiveCompileJob = (dd.ActiveCompileJob ?? latestCompileJob)?.ToApi();
122  if (latestCompileJob?.Id != result.ActiveCompileJob?.Id)
123  result.StagedCompileJob = latestCompileJob?.ToApi();
124  }
125 
126  return Json(result);
127  }
128 
130  [HttpDelete]
131  [TgsAuthorize(DreamDaemonRights.Shutdown)]
132  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
133  {
134  //alias for stopping DD
135  var instance = instanceManager.GetInstance(Instance);
136  await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false);
137  return Ok();
138  }
139 
141  [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout)]
142  public override async Task<IActionResult> Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
143  {
144  if (model == null)
145  throw new ArgumentNullException(nameof(model));
146 
147  if (model.PrimaryPort == 0)
148  return BadRequest(new ErrorMessage { Message = "Primary port cannot be 0!" });
149 
150  if (model.SecurityLevel == DreamDaemonSecurity.Ultrasafe)
151  return BadRequest(new ErrorMessage { Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!" });
152 
153  //alias for changing DD settings
154  var current = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
155 
156  if (current == default)
157  return StatusCode((int)HttpStatusCode.Gone);
158 
159  var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon);
160 
161  bool CheckModified<T>(Expression<Func<Api.Models.Internal.DreamDaemonSettings, T>> expression, DreamDaemonRights requiredRight)
162  {
163  var memberSelectorExpression = (MemberExpression)expression.Body;
164  var property = (PropertyInfo)memberSelectorExpression.Member;
165 
166  var newVal = property.GetValue(model);
167  if (newVal == null)
168  return false;
169  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
170  return true;
171 
172  property.SetValue(current, newVal);
173  return false;
174  };
175 
176  var oldSoftRestart = current.SoftRestart;
177  var oldSoftShutdown = current.SoftShutdown;
178 
179  if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient)
180  || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart)
181  || CheckModified(x => x.PrimaryPort, DreamDaemonRights.SetPorts)
182  || CheckModified(x => x.SecondaryPort, DreamDaemonRights.SetPorts)
183  || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity)
184  || CheckModified(x => x.SoftRestart, DreamDaemonRights.SoftRestart)
185  || CheckModified(x => x.SoftShutdown, DreamDaemonRights.SoftShutdown)
186  || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout))
187  return Forbid();
188 
189  if (current.PrimaryPort == current.SecondaryPort)
190  return BadRequest(new ErrorMessage { Message = "Primary port and secondary port cannot be the same!" });
191 
192  var wd = instanceManager.GetInstance(Instance).Watchdog;
193 
194  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
195  //run this second because current may be modified by it
196  await wd.ChangeSettings(current, cancellationToken).ConfigureAwait(false);
197 
198  if (!oldSoftRestart.Value && current.SoftRestart.Value)
199  await wd.Restart(true, cancellationToken).ConfigureAwait(false);
200  else if (!oldSoftShutdown.Value && current.SoftShutdown.Value)
201  await wd.Terminate(true, cancellationToken).ConfigureAwait(false);
202  else if ((oldSoftRestart.Value && !current.SoftRestart.Value) || (oldSoftShutdown.Value && !current.SoftShutdown.Value))
203  await wd.ResetRebootState(cancellationToken).ConfigureAwait(false);
204 
205  return await ReadImpl(current, cancellationToken).ConfigureAwait(false);
206  }
207 
213  [HttpPatch]
214  [TgsAuthorize(DreamDaemonRights.Restart)]
215  public async Task<IActionResult> Restart(CancellationToken cancellationToken)
216  {
217  var job = new Models.Job
218  {
219  Instance = Instance,
220  CancelRightsType = RightsType.DreamDaemon,
221  CancelRight = (ulong)DreamDaemonRights.Shutdown,
222  StartedBy = AuthenticationContext.User,
223  Description = "Restart Watchdog"
224  };
225 
226  var watchdog = instanceManager.GetInstance(Instance).Watchdog;
227 
228  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false);
229  return Accepted(job.ToApi());
230  }
231  }
232 }
async Task< IActionResult > Delete(CancellationToken cancellationToken)
+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;
19 
20 namespace Tgstation.Server.Host.Controllers
21 {
25  [Route(Routes.DreamDaemon)]
26  public sealed class DreamDaemonController : ModelController<DreamDaemon>
27  {
32 
37 
46  public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamDaemonController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
47  {
48  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
49  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
50  }
51 
53  [TgsAuthorize(DreamDaemonRights.Start)]
54  public override async Task<IActionResult> Create([FromBody] DreamDaemon model, CancellationToken cancellationToken)
55  {
56  // alias for launching DD
57  var instance = instanceManager.GetInstance(Instance);
58 
59  if (instance.Watchdog.Running)
60  return StatusCode((int)HttpStatusCode.Gone);
61 
62  var job = new Models.Job
63  {
64  Description = "Launch DreamDaemon",
65  CancelRight = (ulong)DreamDaemonRights.Shutdown,
66  CancelRightsType = RightsType.DreamDaemon,
68  StartedBy = AuthenticationContext.User
69  };
70  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false);
71  return Accepted(job.ToApi());
72  }
73 
75  [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)]
76  public override Task<IActionResult> Read(CancellationToken cancellationToken) => ReadImpl(null, cancellationToken);
77 
84  async Task<IActionResult> ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
85  {
86  var instance = instanceManager.GetInstance(Instance);
87  var dd = instance.Watchdog;
88 
89  var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadMetadata) != 0;
90  var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (ulong)DreamDaemonRights.ReadRevision) != 0;
91 
92  if (settings == null)
93  {
94  settings = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
95  if (settings == default)
96  return StatusCode((int)HttpStatusCode.Gone);
97  }
98 
99  var result = new DreamDaemon();
100  if (metadata)
101  {
102  var alphaActive = dd.AlphaIsActive;
103  var llp = dd.LastLaunchParameters;
104  var rstate = dd.RebootState;
105  result.AutoStart = settings.AutoStart;
106  result.CurrentPort = alphaActive ? llp?.PrimaryPort : llp?.SecondaryPort;
107  result.CurrentSecurity = llp?.SecurityLevel;
108  result.CurrentAllowWebclient = llp?.AllowWebClient;
109  result.PrimaryPort = settings.PrimaryPort;
110  result.AllowWebClient = settings.AllowWebClient;
111  result.Running = dd.Running;
112  result.SecondaryPort = settings.SecondaryPort;
113  result.SecurityLevel = settings.SecurityLevel;
114  result.SoftRestart = rstate == RebootState.Restart;
115  result.SoftShutdown = rstate == RebootState.Shutdown;
116  result.StartupTimeout = settings.StartupTimeout;
117  }
118 
119  if (revision)
120  {
121  var latestCompileJob = instance.LatestCompileJob();
122  result.ActiveCompileJob = (dd.ActiveCompileJob ?? latestCompileJob)?.ToApi();
123  if (latestCompileJob?.Id != result.ActiveCompileJob?.Id)
124  result.StagedCompileJob = latestCompileJob?.ToApi();
125  }
126 
127  return Json(result);
128  }
129 
135  [HttpDelete]
136  [TgsAuthorize(DreamDaemonRights.Shutdown)]
137  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
138  {
139  var instance = instanceManager.GetInstance(Instance);
140  await instance.Watchdog.Terminate(false, cancellationToken).ConfigureAwait(false);
141  return Ok();
142  }
143 
145  #pragma warning disable CA1506 // TODO: Decomplexify
146  [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start | DreamDaemonRights.SetStartupTimeout)]
147  public override async Task<IActionResult> Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
148  {
149  if (model == null)
150  throw new ArgumentNullException(nameof(model));
151 
152  if (model.PrimaryPort == 0)
153  return BadRequest(new ErrorMessage { Message = "Primary port cannot be 0!" });
154 
155  if (model.SecurityLevel == DreamDaemonSecurity.Ultrasafe)
156  return BadRequest(new ErrorMessage { Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!" });
157 
158  // alias for changing DD settings
159  var current = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
160 
161  if (current == default)
162  return StatusCode((int)HttpStatusCode.Gone);
163 
164  var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon);
165 
166  bool CheckModified<T>(Expression<Func<Api.Models.Internal.DreamDaemonSettings, T>> expression, DreamDaemonRights requiredRight)
167  {
168  var memberSelectorExpression = (MemberExpression)expression.Body;
169  var property = (PropertyInfo)memberSelectorExpression.Member;
170 
171  var newVal = property.GetValue(model);
172  if (newVal == null)
173  return false;
174  if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal)
175  return true;
176 
177  property.SetValue(current, newVal);
178  return false;
179  }
180 
181  var oldSoftRestart = current.SoftRestart;
182  var oldSoftShutdown = current.SoftShutdown;
183 
184  if (CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient)
185  || CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart)
186  || CheckModified(x => x.PrimaryPort, DreamDaemonRights.SetPorts)
187  || CheckModified(x => x.SecondaryPort, DreamDaemonRights.SetPorts)
188  || CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity)
189  || CheckModified(x => x.SoftRestart, DreamDaemonRights.SoftRestart)
190  || CheckModified(x => x.SoftShutdown, DreamDaemonRights.SoftShutdown)
191  || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout))
192  return Forbid();
193 
194  if (current.PrimaryPort == current.SecondaryPort)
195  return BadRequest(new ErrorMessage { Message = "Primary port and secondary port cannot be the same!" });
196 
197  var wd = instanceManager.GetInstance(Instance).Watchdog;
198 
199  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
200 
201  // run this second because current may be modified by it
202  await wd.ChangeSettings(current, cancellationToken).ConfigureAwait(false);
203 
204  if (!oldSoftRestart.Value && current.SoftRestart.Value)
205  await wd.Restart(true, cancellationToken).ConfigureAwait(false);
206  else if (!oldSoftShutdown.Value && current.SoftShutdown.Value)
207  await wd.Terminate(true, cancellationToken).ConfigureAwait(false);
208  else if ((oldSoftRestart.Value && !current.SoftRestart.Value) || (oldSoftShutdown.Value && !current.SoftShutdown.Value))
209  await wd.ResetRebootState(cancellationToken).ConfigureAwait(false);
210 
211  return await ReadImpl(current, cancellationToken).ConfigureAwait(false);
212  }
213  #pragma warning restore CA1506
214 
220  [HttpPatch]
221  [TgsAuthorize(DreamDaemonRights.Restart)]
222  public async Task<IActionResult> Restart(CancellationToken cancellationToken)
223  {
224  var job = new Models.Job
225  {
226  Instance = Instance,
227  CancelRightsType = RightsType.DreamDaemon,
228  CancelRight = (ulong)DreamDaemonRights.Shutdown,
229  StartedBy = AuthenticationContext.User,
230  Description = "Restart Watchdog"
231  };
232 
233  var watchdog = instanceManager.GetInstance(Instance).Watchdog;
234 
235  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false);
236  return Accepted(job.ToApi());
237  }
238  }
239 }
async Task< IActionResult > Delete(CancellationToken cancellationToken)
Stops DreamDaemon if it&#39;s running
Manages the runtime of Jobs
Definition: IJobManager.cs:12
@@ -104,32 +104,32 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
DreamDaemonSecurity SecurityLevel
The DreamDaemonSecurity level of DreamDaemon
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
bool AutoStart
If DreamDaemon starts when it&#39;s Instance starts
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
-
async Task< IActionResult > ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
Implementation of Read(CancellationToken)
-
async Task< IActionResult > Restart(CancellationToken cancellationToken)
Handle a HTTP PATCH to the DreamDaemonController
+
async Task< IActionResult > ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
Implementation of Read(CancellationToken)
+
async Task< IActionResult > Restart(CancellationToken cancellationToken)
Handle a HTTP PATCH to the DreamDaemonController
Routes to a server actions
Definition: Routes.cs:9
DreamDaemonRights
Rights for Models.DreamDaemon
-
override async Task< IActionResult > Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
- -
DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger< DreamDaemonController > logger)
Construct a DreamDaemonController
+
override async Task< IActionResult > Update([FromBody] DreamDaemon model, CancellationToken cancellationToken)
+ +
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
ModelController<TModel> for managing the DreamDaemon
const string DreamDaemon
The Models.DreamDaemon controller
Definition: Routes.cs:44
-
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
+
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
An ApiController representing a TModel
readonly IJobManager jobManager
The IJobManager for the DreamMakerController
Metadata about a server instance
Definition: Instance.cs:9
-
override async Task< IActionResult > Create([FromBody] DreamDaemon model, CancellationToken cancellationToken)
+
override async Task< IActionResult > Create([FromBody] DreamDaemon model, CancellationToken cancellationToken)
DreamDaemonSecurity
DreamDaemon&#39;s security level
diff --git a/_dream_daemon_rights_8cs_source.html b/_dream_daemon_rights_8cs_source.html index e9c936794d..95cd532e80 100644 --- a/_dream_daemon_rights_8cs_source.html +++ b/_dream_daemon_rights_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonRights.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum DreamDaemonRights : ulong
10  {
14  None = 0,
18  ReadRevision = 1,
22  SetPorts = 2,
26  SetAutoStart = 4,
30  SetSecurity = 8,
34  ReadMetadata = 16,
38  SetWebClient = 32,
42  SoftRestart = 64,
46  SoftShutdown = 128,
50  Restart = 256,
54  Shutdown = 512,
58  Start = 1024,
62  SetStartupTimeout = 2048
63  }
64 }
User can enable Models.Internal.DreamDaemonSettings.SoftRestart
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum DreamDaemonRights : ulong
10  {
14  None = 0,
15 
19  ReadRevision = 1,
20 
24  SetPorts = 2,
25 
29  SetAutoStart = 4,
30 
34  SetSecurity = 8,
35 
39  ReadMetadata = 16,
40 
44  SetWebClient = 32,
45 
49  SoftRestart = 64,
50 
54  SoftShutdown = 128,
55 
59  Restart = 256,
60 
64  Shutdown = 512,
65 
69  Start = 1024,
70 
74  SetStartupTimeout = 2048
75  }
76 }
User can enable Models.Internal.DreamDaemonSettings.SoftRestart
User can change Models.Internal.DreamDaemonLaunchParameters.AllowWebClient
diff --git a/_dream_daemon_security_8cs_source.html b/_dream_daemon_security_8cs_source.html index 88aed34370..2eeb96faa8 100644 --- a/_dream_daemon_security_8cs_source.html +++ b/_dream_daemon_security_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonSecurity.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public enum DreamDaemonSecurity
7  {
11  Trusted,
15  Safe,
19  Ultrasafe
20  }
21 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public enum DreamDaemonSecurity
7  {
11  Trusted,
12 
16  Safe,
17 
21  Ultrasafe
22  }
23 }
Server will not be able to run shell commands or access files outside it&#39;s working directory ...
Server will not be able to run shell commands or access anything but temporary files. Currently unsupported!
Server is unrestricted in terms of file access and shell commands
diff --git a/_dream_daemon_status_8cs_source.html b/_dream_daemon_status_8cs_source.html index f5d7b8f733..c6d429248e 100644 --- a/_dream_daemon_status_8cs_source.html +++ b/_dream_daemon_status_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamDaemonStatus.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6 #pragma warning disable CA1717 // Only FlagsAttribute enums should have plural names
7  public enum DreamDaemonStatus
8 #pragma warning restore CA1717 // Only FlagsAttribute enums should have plural names
9  {
13  Offline,
21  Online,
22  }
23 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6 #pragma warning disable CA1717 // Only FlagsAttribute enums should have plural names
7  public enum DreamDaemonStatus
8 #pragma warning restore CA1717 // Only FlagsAttribute enums should have plural names
9  {
13  Offline,
14 
19 
23  Online,
24  }
25 }
DreamDaemonStatus
The status of DreamDaemon
diff --git a/_dream_maker_controller_8cs_source.html b/_dream_maker_controller_8cs_source.html index 7d8a11409d..87c7ac101b 100644 --- a/_dream_maker_controller_8cs_source.html +++ b/_dream_maker_controller_8cs_source.html @@ -92,21 +92,21 @@ 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.Linq;
6 using System.Net;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
16 
17 namespace Tgstation.Server.Host.Controllers
18 {
22  [Route(Routes.DreamMaker)]
23  public sealed class DreamMakerController : ModelController<Api.Models.DreamMaker>
24  {
33 
42  public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamMakerController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
43  {
44  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
45  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
46  }
47 
49  [TgsAuthorize(DreamMakerRights.Read)]
50  public override async Task<IActionResult> Read(CancellationToken cancellationToken)
51  {
52  var instance = instanceManager.GetInstance(Instance);
53  var dreamMakerSettings = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
54  return Json(dreamMakerSettings.ToApi());
55  }
56 
58  [TgsAuthorize(DreamMakerRights.CompileJobs)]
59  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
60  {
61  var compileJob = await DatabaseContext.CompileJobs
62  .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
63  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
64  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
65  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
66  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
67  if (compileJob == default)
68  return NotFound();
69  return Json(compileJob.ToApi());
70  }
71 
73  [TgsAuthorize(DreamMakerRights.CompileJobs)]
74  public override async Task<IActionResult> List(CancellationToken cancellationToken)
75  {
76  var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob
77  {
78  Id = x.Id
79  }).ToListAsync(cancellationToken).ConfigureAwait(false);
80  return Json(compileJobs);
81  }
82 
84  [TgsAuthorize(DreamMakerRights.Compile)]
85  public override async Task<IActionResult> Create([FromBody] DreamMaker model, CancellationToken cancellationToken)
86  {
87  var job = new Models.Job
88  {
89  Description = "Compile active repository code",
90  StartedBy = AuthenticationContext.User,
91  CancelRightsType = RightsType.DreamMaker,
92  CancelRight = (ulong)DreamMakerRights.CancelCompile,
94  };
95  await jobManager.RegisterOperation(job, instanceManager.GetInstance(Instance).CompileProcess, cancellationToken).ConfigureAwait(false);
96  return Accepted(job.ToApi());
97  }
98 
100  [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)]
101  public override async Task<IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
102  {
103  if (model.ApiValidationPort == 0)
104  return BadRequest(new ErrorMessage { Message = "API Validation port cannot be 0!" });
105 
106  if (model.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
107  return BadRequest(new ErrorMessage { Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!" });
108 
109  var hostModel = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
110  if (hostModel == null)
111  return StatusCode((int)HttpStatusCode.Gone);
112 
113  if (model.ProjectName != null)
114  {
116  return Forbid();
117  if (model.ProjectName.Length == 0)
118  hostModel.ProjectName = null;
119  else
120  hostModel.ProjectName = model.ProjectName;
121  }
122 
123  if (model.ApiValidationPort.HasValue)
124  {
125  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort))
126  return Forbid();
127  hostModel.ApiValidationPort = model.ApiValidationPort;
128  }
129 
130  if (model.ApiValidationSecurityLevel.HasValue)
131  {
132  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel))
133  return Forbid();
134  hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
135  }
136 
137  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
138 
139  if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0)
140  return Ok();
141 
142  return await Read(cancellationToken).ConfigureAwait(false);
143  }
144  }
145 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Net;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
16 
17 namespace Tgstation.Server.Host.Controllers
18 {
22  [Route(Routes.DreamMaker)]
23  #pragma warning disable CA1506 // TODO: Decomplexify
24  public sealed class DreamMakerController : ModelController<DreamMaker>
25  {
30 
35 
44  public DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger<DreamMakerController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
45  {
46  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
47  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
48  }
49 
51  [TgsAuthorize(DreamMakerRights.Read)]
52  public override async Task<IActionResult> Read(CancellationToken cancellationToken)
53  {
54  var instance = instanceManager.GetInstance(Instance);
55  var dreamMakerSettings = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
56  return Json(dreamMakerSettings.ToApi());
57  }
58 
60  [TgsAuthorize(DreamMakerRights.CompileJobs)]
61  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
62  {
63  var compileJob = await DatabaseContext.CompileJobs
64  .Where(x => x.Id == id && x.Job.Instance.Id == Instance.Id)
65  .Include(x => x.Job).ThenInclude(x => x.StartedBy)
66  .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
67  .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
68  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
69  if (compileJob == default)
70  return NotFound();
71  return Json(compileJob.ToApi());
72  }
73 
75  [TgsAuthorize(DreamMakerRights.CompileJobs)]
76  public override async Task<IActionResult> List(CancellationToken cancellationToken)
77  {
78  var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob
79  {
80  Id = x.Id
81  }).ToListAsync(cancellationToken).ConfigureAwait(false);
82  return Json(compileJobs);
83  }
84 
86  [TgsAuthorize(DreamMakerRights.Compile)]
87  public override async Task<IActionResult> Create([FromBody] DreamMaker model, CancellationToken cancellationToken)
88  {
89  var job = new Models.Job
90  {
91  Description = "Compile active repository code",
92  StartedBy = AuthenticationContext.User,
93  CancelRightsType = RightsType.DreamMaker,
94  CancelRight = (ulong)DreamMakerRights.CancelCompile,
96  };
97  await jobManager.RegisterOperation(job, instanceManager.GetInstance(Instance).CompileProcess, cancellationToken).ConfigureAwait(false);
98  return Accepted(job.ToApi());
99  }
100 
102  [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)]
103  public override async Task<IActionResult> Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
104  {
105  if (model.ApiValidationPort == 0)
106  return BadRequest(new ErrorMessage { Message = "API Validation port cannot be 0!" });
107 
108  if (model.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
109  return BadRequest(new ErrorMessage { Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!" });
110 
111  var hostModel = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
112  if (hostModel == null)
113  return StatusCode((int)HttpStatusCode.Gone);
114 
115  if (model.ProjectName != null)
116  {
118  return Forbid();
119  if (model.ProjectName.Length == 0)
120  hostModel.ProjectName = null;
121  else
122  hostModel.ProjectName = model.ProjectName;
123  }
124 
125  if (model.ApiValidationPort.HasValue)
126  {
127  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetApiValidationPort))
128  return Forbid();
129  hostModel.ApiValidationPort = model.ApiValidationPort;
130  }
131 
132  if (model.ApiValidationSecurityLevel.HasValue)
133  {
134  if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel))
135  return Forbid();
136  hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel;
137  }
138 
139  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
140 
141  if ((AuthenticationContext.GetRight(RightsType.DreamMaker) & (ulong)DreamMakerRights.Read) == 0)
142  return Ok();
143 
144  return await Read(cancellationToken).ConfigureAwait(false);
145  }
146  }
147 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
-
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
+
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
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
- -
DbSet< DreamMakerSettings > DreamMakerSettings
+ +
DbSet< DreamMakerSettings > DreamMakerSettings
-
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
+
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
-
override async Task< IActionResult > Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
+
override async Task< IActionResult > Update([FromBody] DreamMaker model, CancellationToken cancellationToken)
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
@@ -115,18 +115,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Routes to a server actions
Definition: Routes.cs:9
const string DreamMaker
The Models.DreamMaker controller
Definition: Routes.cs:74
DreamMakerRights
Rights for Models.DreamMaker
- -
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
-
DreamMakerController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, ILogger< DreamMakerController > logger)
Construct a DreamMakerController
-
readonly IJobManager jobManager
The IJobManager for the DreamMakerController
+ +
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
+
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
An ApiController representing a TModel
DreamMakerRights DreamMakerRights
The Rights.DreamMakerRights of the InstanceUser
Definition: InstanceUser.cs:39
-
override async Task< IActionResult > Create([FromBody] DreamMaker model, CancellationToken cancellationToken)
-
override async Task< IActionResult > Read(CancellationToken cancellationToken)
Attempt to read a TModel
+
override async Task< IActionResult > Create([FromBody] DreamMaker model, CancellationToken cancellationToken)
+
override async Task< IActionResult > Read(CancellationToken cancellationToken)
Attempt to read a TModel
Metadata about a server instance
Definition: Instance.cs:9
@@ -135,7 +135,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents an error message returned by the server
Definition: ErrorMessage.cs:8
- +
diff --git a/_dream_maker_rights_8cs_source.html b/_dream_maker_rights_8cs_source.html index 35dc8767a9..38c72b2d86 100644 --- a/_dream_maker_rights_8cs_source.html +++ b/_dream_maker_rights_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamMakerRights.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum DreamMakerRights : ulong
10  {
14  None = 0,
18  Read = 1,
22  Compile = 2,
26  CancelCompile = 4,
30  SetDme = 8,
38  CompileJobs = 32,
42  SetSecurityLevel = 64
43  }
44 }
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum DreamMakerRights : ulong
10  {
14  None = 0,
15 
19  Read = 1,
20 
24  Compile = 2,
25 
29  CancelCompile = 4,
30 
34  SetDme = 8,
35 
40 
44  CompileJobs = 32,
45 
49  SetSecurityLevel = 64
50  }
51 }
User may modify Models.DreamMaker.ApiValidationPort
diff --git a/_event_consumer_8cs_source.html b/_event_consumer_8cs_source.html index 2981f03b9a..fda9d4fb7b 100644 --- a/_event_consumer_8cs_source.html +++ b/_event_consumer_8cs_source.html @@ -92,12 +92,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
EventConsumer.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;
7 
8 namespace Tgstation.Server.Host.Components
9 {
12  {
17 
22 
27  public EventConsumer(IConfiguration configuration)
28  {
29  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
30  }
31 
33  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
34  {
35  if (watchdog == null)
36  throw new InvalidOperationException("EventConsumer used without watchdog set!");
37 
38  if (!await configuration.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false))
39  return false;
40  return await watchdog.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false);
41  }
42 
47  public void SetWatchdog(IWatchdog watchdog)
48  {
49  if (watchdog == null)
50  throw new ArgumentNullException(nameof(watchdog));
51  if (this.watchdog != null)
52  throw new InvalidOperationException("watchdog already set!");
53  this.watchdog = watchdog;
54  }
55  }
56 }
async Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Threading;
4 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Components
9 {
12  {
17 
22 
27  public EventConsumer(IConfiguration configuration)
28  {
29  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
30  }
31 
33  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
34  {
35  if (watchdog == null)
36  throw new InvalidOperationException("EventConsumer used without watchdog set!");
37 
38  if (!await configuration.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false))
39  return false;
40  return await watchdog.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false);
41  }
42 
47  public void SetWatchdog(IWatchdog watchdog)
48  {
49 #pragma warning disable IDE0016 // Use 'throw' expression
50  if (watchdog == null)
51  throw new ArgumentNullException(nameof(watchdog));
52 #pragma warning restore IDE0016 // Use 'throw' expression
53  if (this.watchdog != null)
54  throw new InvalidOperationException("watchdog already set!");
55  this.watchdog = watchdog;
56  }
57  }
58 }
async Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
Consumes EventTypes and takes the appropriate actions
-
IWatchdog watchdog
The IWatchdog for the
+
IWatchdog watchdog
The IWatchdog for the EventConsumer
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
readonly IConfiguration configuration
The IConfiguration for the EventConsumer
@@ -105,7 +105,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For managing the Configuration directory
- +
Task< bool > HandleEvent(EventType eventType, IEnumerable< string > parameters, CancellationToken cancellationToken)
Handle a given eventType
void SetWatchdog(IWatchdog watchdog)
Set the watchdog for the EventConsumer
diff --git a/_event_type_8cs_source.html b/_event_type_8cs_source.html index cf39154ba2..b425e8b5ce 100644 --- a/_event_type_8cs_source.html +++ b/_event_type_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
EventType.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components
2 {
6  public enum EventType
7  {
11  RepoResetOrigin = 0,
15  RepoCheckout = 1,
19  RepoFetch = 2,
28 
36  ByondInstallFail = 6,
44  CompileStart = 8,
48  CompileCancelled = 9,
52  CompileFailure = 10,
56  CompileComplete = 11,
57 
62 
66  RepoMergeConflict = 13,
67  }
68 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components
2 {
6  public enum EventType
7  {
11  RepoResetOrigin = 0,
12 
16  RepoCheckout = 1,
17 
21  RepoFetch = 2,
22 
27 
32 
37 
41  ByondInstallFail = 6,
42 
47 
51  CompileStart = 8,
52 
56  CompileCancelled = 9,
57 
61  CompileFailure = 10,
62 
66  CompileComplete = 11,
67 
72 
76  RepoMergeConflict = 13,
77  }
78 }
Parameters: Base sha, target sha, base reference, target reference
diff --git a/_file_logging_configuration_8cs_source.html b/_file_logging_configuration_8cs_source.html index dbd1937047..15d9249331 100644 --- a/_file_logging_configuration_8cs_source.html +++ b/_file_logging_configuration_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
FileLoggingConfiguration.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using Newtonsoft.Json.Converters;
4 
5 namespace Tgstation.Server.Host.Configuration
6 {
11  {
15  public const string Section = "FileLogging";
16 
20  const LogLevel DefaultLogLevel = LogLevel.Debug;
21 
25  const LogLevel DefaultMicrosoftLogLevel = LogLevel.Warning;
26 
30  public string Directory { get; set; }
31 
35  public bool Disable { get; set; }
36 
40  [JsonConverter(typeof(StringEnumConverter))]
41  public LogLevel LogLevel { get; set; } = DefaultLogLevel;
42 
43 
47  [JsonConverter(typeof(StringEnumConverter))]
48  public LogLevel MicrosoftLogLevel { get; set; } = DefaultMicrosoftLogLevel;
49  }
50 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using Newtonsoft.Json.Converters;
4 
5 namespace Tgstation.Server.Host.Configuration
6 {
11  {
15  public const string Section = "FileLogging";
16 
20  const LogLevel DefaultLogLevel = LogLevel.Debug;
21 
25  const LogLevel DefaultMicrosoftLogLevel = LogLevel.Warning;
26 
30  public string Directory { get; set; }
31 
35  public bool Disable { get; set; }
36 
40  [JsonConverter(typeof(StringEnumConverter))]
41  public LogLevel LogLevel { get; set; } = DefaultLogLevel;
42 
46  [JsonConverter(typeof(StringEnumConverter))]
47  public LogLevel MicrosoftLogLevel { get; set; } = DefaultMicrosoftLogLevel;
48  }
49 }
diff --git a/_global_suppressions_8cs_source.html b/_global_suppressions_8cs_source.html index 6e01bf0a86..e1a32e9343 100644 --- a/_global_suppressions_8cs_source.html +++ b/_global_suppressions_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
GlobalSuppressions.cs
-Go to the documentation of this file.
1 [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames")]
2 [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Windows.Forms.MessageBox.Show(System.String,System.String,System.Windows.Forms.MessageBoxButtons)", Scope = "member", Target = "Tgstation.Server.Host.Service.Program.#OnExecute()")]
3 [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1300:SpecifyMessageBoxOptions", Scope = "member", Target = "Tgstation.Server.Host.Service.Program.#OnExecute()")]
4 [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2232:MarkWindowsFormsEntryPointsWithStaThread")]
5 // This file is used by Code Analysis to maintain SuppressMessage
6 // attributes that are applied to this project.
7 // Project-level suppressions either have no target or are given
8 // a specific target and scoped to a namespace, type, member, etc.
9 //
10 // To add a suppression to this file, right-click the message in the
11 // Code Analysis results, point to "Suppress Message", and click
12 // "In Suppression File".
13 // You do not need to add suppressions to this file manually.
14 
+Go to the documentation of this file.
1 [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2232:MarkWindowsFormsEntryPointsWithStaThread", Justification = "No don't")]
2 
3 // This file is used by Code Analysis to maintain SuppressMessage
4 // attributes that are applied to this project.
5 // Project-level suppressions either have no target or are given
6 // a specific target and scoped to a namespace, type, member, etc.
7 //
8 // To add a suppression to this file, right-click the message in the
9 // Code Analysis results, point to "Suppress Message", and click
10 // "In Suppression File".
11 // You do not need to add suppressions to this file manually.
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authorization;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.EntityFrameworkCore;
4 using Microsoft.Extensions.Logging;
5 using Microsoft.Extensions.Options;
6 using System;
7 using System.Linq;
8 using System.Net.Mime;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
16 using Wangkanai.Detection;
17 
18 namespace Tgstation.Server.Host.Controllers
19 {
23  [Route(Routes.Root)]
24  public sealed class HomeController : ApiController
25  {
46 
50  readonly IBrowserResolver browserResolver;
51 
56 
70  public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, IBrowserResolver browserResolver, IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions, ILogger<HomeController> logger) : base(databaseContext, authenticationContextFactory, logger, false, false)
71  {
72  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
73  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
74  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
75  this.application = application ?? throw new ArgumentNullException(nameof(application));
76  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
77  this.browserResolver = browserResolver ?? throw new ArgumentNullException(nameof(browserResolver));
78  controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
79  }
80 
85  [TgsAuthorize]
86  [AllowAnonymous]
87  [HttpGet]
88  public IActionResult Home()
89  {
90  if (AuthenticationContext != null)
91  return Json(new Api.Models.ServerInformation
92  {
93  Version = application.Version,
94  ApiVersion = ApiHeaders.Version
95  });
96 
97  //if we are using a browser and the control panel, soft redirect to the app page
98  if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic)
99  {
100  Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), loading control panel...", browserResolver.UserAgent);
101  return File("~/index.html", MediaTypeNames.Text.Html);
102  }
103 
104  return Unauthorized();
105  }
106 
112  [HttpPost]
113  public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
114  {
115  if (ApiHeaders == null)
116  return BadRequest(new Api.Models.ErrorMessage { Message = "Missing API headers!" });
117 
119  return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
120 
121  ISystemIdentity identity;
122  try
123  {
124  //trust the system over the database because a user's name can change while still having the same SID
125  identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
126  }
127  catch (NotImplementedException)
128  {
129  identity = null;
130  }
131  using (identity)
132  {
133  IQueryable<User> query;
134  if (identity == null)
135  query = DatabaseContext.Users.Where(x => x.CanonicalName == ApiHeaders.Username.ToUpperInvariant());
136  else
137  query = DatabaseContext.Users.Where(x => x.SystemIdentifier == identity.Uid);
138  var user = await query.Select(x => new User
139  {
140  Id = x.Id,
141  PasswordHash = x.PasswordHash,
142  Enabled = x.Enabled,
143  Name = x.Name
144  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
145 
146  if (user == null)
147  return Unauthorized();
148 
149  if (identity == null)
150  {
151  var originalHash = user.PasswordHash;
152  if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
153  return Unauthorized();
154  if (user.PasswordHash != originalHash)
155  {
156  var updatedUser = new User
157  {
158  Id = user.Id
159  };
160  DatabaseContext.Users.Attach(updatedUser);
161  updatedUser.PasswordHash = user.PasswordHash;
162  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
163  }
164  }
165  //check if the name changed and updoot accordingly
166  else if (identity.Username != user.Name)
167  {
168  DatabaseContext.Users.Attach(user);
169  user.Name = identity.Username;
170  user.CanonicalName = user.Name.ToUpperInvariant();
171  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
172  }
173 
174  if (!user.Enabled.Value)
175  return Forbid();
176 
177  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
178  if (identity != null)
179  {
180  //expire the identity slightly after the auth token in case of lag
181  var identExpiry = token.ExpiresAt.Value;
182  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
183  identExpiry += TimeSpan.FromSeconds(15);
184  identityCache.CacheSystemIdentity(user, identity, identExpiry);
185  }
186 
187  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
188 
189  return Json(token);
190  }
191  }
192  }
193 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Authorization;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.EntityFrameworkCore;
4 using Microsoft.Extensions.Logging;
5 using Microsoft.Extensions.Options;
6 using System;
7 using System.Linq;
8 using System.Net.Mime;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
16 using Wangkanai.Detection;
17 
18 namespace Tgstation.Server.Host.Controllers
19 {
23  [Route(Routes.Root)]
24  public sealed class HomeController : ApiController
25  {
30 
35 
40 
45 
50 
54  readonly IBrowserResolver browserResolver;
55 
60 
74  public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, IBrowserResolver browserResolver, IOptions<ControlPanelConfiguration> controlPanelConfigurationOptions, ILogger<HomeController> logger) : base(databaseContext, authenticationContextFactory, logger, false, false)
75  {
76  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
77  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
78  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
79  this.application = application ?? throw new ArgumentNullException(nameof(application));
80  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
81  this.browserResolver = browserResolver ?? throw new ArgumentNullException(nameof(browserResolver));
82  controlPanelConfiguration = controlPanelConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(controlPanelConfigurationOptions));
83  }
84 
89  [TgsAuthorize]
90  [AllowAnonymous]
91  [HttpGet]
92  public IActionResult Home()
93  {
94  if (AuthenticationContext != null)
95  return Json(new Api.Models.ServerInformation
96  {
97  Version = application.Version,
98  ApiVersion = ApiHeaders.Version
99  });
100 
101  // if we are using a browser and the control panel, soft redirect to the app page
102  if (controlPanelConfiguration.Enable && browserResolver.Browser.Type != BrowserType.Generic)
103  {
104  Logger.LogDebug("Unauthorized browser request (User-Agent: \"{0}\"), loading control panel...", browserResolver.UserAgent);
105  return File("~/index.html", MediaTypeNames.Text.Html);
106  }
107 
108  return Unauthorized();
109  }
110 
116  [HttpPost]
117  #pragma warning disable CA1506 // TODO: Decomplexify
118  public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
119  {
120  if (ApiHeaders == null)
121  return BadRequest(new Api.Models.ErrorMessage { Message = "Missing API headers!" });
122 
124  return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
125 
126  ISystemIdentity identity;
127  try
128  {
129  // trust the system over the database because a user's name can change while still having the same SID
130  identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
131  }
132  catch (NotImplementedException)
133  {
134  identity = null;
135  }
136 
137  using (identity)
138  {
139  IQueryable<User> query;
140  if (identity == null)
141  query = DatabaseContext.Users.Where(x => x.CanonicalName == ApiHeaders.Username.ToUpperInvariant());
142  else
143  query = DatabaseContext.Users.Where(x => x.SystemIdentifier == identity.Uid);
144  var user = await query.Select(x => new User
145  {
146  Id = x.Id,
147  PasswordHash = x.PasswordHash,
148  Enabled = x.Enabled,
149  Name = x.Name
150  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
151 
152  if (user == null)
153  return Unauthorized();
154 
155  if (identity == null)
156  {
157  var originalHash = user.PasswordHash;
158  if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
159  return Unauthorized();
160  if (user.PasswordHash != originalHash)
161  {
162  var updatedUser = new User
163  {
164  Id = user.Id
165  };
166  DatabaseContext.Users.Attach(updatedUser);
167  updatedUser.PasswordHash = user.PasswordHash;
168  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
169  }
170  }
171 
172  // check if the name changed and updoot accordingly
173  else if (identity.Username != user.Name)
174  {
175  DatabaseContext.Users.Attach(user);
176  user.Name = identity.Username;
177  user.CanonicalName = user.Name.ToUpperInvariant();
178  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
179  }
180 
181  if (!user.Enabled.Value)
182  return Forbid();
183 
184  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
185  if (identity != null)
186  {
187  // expire the identity slightly after the auth token in case of lag
188  var identExpiry = token.ExpiresAt.Value;
189  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
190  identExpiry += TimeSpan.FromSeconds(15);
191  identityCache.CacheSystemIdentity(user, identity, identExpiry);
192  }
193 
194  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
195 
196  return Json(token);
197  }
198  }
199  #pragma warning restore CA1506
200  }
201 }
-
async Task< IActionResult > CreateToken(CancellationToken cancellationToken)
Attempt to authenticate a User using ApiController.ApiHeaders
+
async Task< IActionResult > CreateToken(CancellationToken cancellationToken)
Attempt to authenticate a User using ApiController.ApiHeaders
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
- +
Represents a user on the current System.Runtime.InteropServices.OSPlatform
-
IActionResult Home()
Main page of the Application
+
IActionResult Home()
Main page of the Application
string Username
The client&#39;s username
Definition: ApiHeaders.cs:81
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
-
HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, IBrowserResolver browserResolver, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< HomeController > logger)
Construct a HomeController
+
HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, IBrowserResolver browserResolver, IOptions< ControlPanelConfiguration > controlPanelConfigurationOptions, ILogger< HomeController > logger)
Construct a HomeController
Contains various cryptographic functions
-
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the HomeController
+
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the HomeController
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:16
-
readonly ControlPanelConfiguration controlPanelConfiguration
The ControlPanelConfiguration for the HomeController
+
readonly ControlPanelConfiguration controlPanelConfiguration
The ControlPanelConfiguration for the HomeController
string Password
The client&#39;s password
Definition: ApiHeaders.cs:86
-
readonly IApplication application
The IApplication for the HomeController
+
readonly IApplication application
The IApplication for the HomeController
Routes to a server actions
Definition: Routes.cs:9
Main ApiController for the Application
-
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController
+
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController
bool IsTokenAuthentication
If the header uses password or JWT authentication
Definition: ApiHeaders.cs:91
- +
const string Root
The root controller
Definition: Routes.cs:14
long Id
The ID of the User
Definition: User.cs:15
-
readonly IBrowserResolver browserResolver
The IBrowserResolver for the HomeController
-
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the HomeController
+
readonly IBrowserResolver browserResolver
The IBrowserResolver for the HomeController
+
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the HomeController
readonly ITokenFactory tokenFactory
The ITokenFactory for the HomeController
string Uid
A unique identifier for the user
diff --git a/_i_administration_client_8cs_source.html b/_i_administration_client_8cs_source.html index 11371786a6..fe16f8f993 100644 --- a/_i_administration_client_8cs_source.html +++ b/_i_administration_client_8cs_source.html @@ -97,7 +97,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
Represents administrative server information
diff --git a/_i_api_client_8cs_source.html b/_i_api_client_8cs_source.html index 68b2818eea..198b5344c7 100644 --- a/_i_api_client_8cs_source.html +++ b/_i_api_client_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IApiClient.cs
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 using Tgstation.Server.Api;
5 
6 namespace Tgstation.Server.Client
7 {
11  interface IApiClient : IDisposable
12  {
13  ApiHeaders Headers { get; set; }
14 
15  Uri Url { get; }
16 
17  TimeSpan Timeout { get; set; }
18 
23  void AddRequestLogger(IRequestLogger requestLogger);
24 
25  Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken);
26  Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken);
27  Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken);
28  Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken);
29  Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken);
30  Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken);
31  Task Delete(string route, CancellationToken cancellationToken);
32 
33  Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
34  Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken);
35  Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken);
36  Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken);
37  Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
38  Task Delete(string route, long instanceId, CancellationToken cancellationToken);
39  Task Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
40  Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken);
41  }
42 }
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 using Tgstation.Server.Api;
5 
6 namespace Tgstation.Server.Client
7 {
11  interface IApiClient : IDisposable
12  {
16  ApiHeaders Headers { get; set; }
17 
21  Uri Url { get; }
22 
26  TimeSpan Timeout { get; set; }
27 
32  void AddRequestLogger(IRequestLogger requestLogger);
33 
43  Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken);
44 
52  Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken);
53 
61  Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken);
62 
72  Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken);
73 
81  Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken);
82 
91  Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken);
92 
99  Task Delete(string route, CancellationToken cancellationToken);
100 
111  Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
112 
121  Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken);
122 
131  Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken);
132 
141  Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken);
142 
153  Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
154 
162  Task Delete(string route, long instanceId, CancellationToken cancellationToken);
163 
173  Task Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
174 
183  Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken);
184  }
185 }
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:16
diff --git a/_i_byond_manager_8cs_source.html b/_i_byond_manager_8cs_source.html index e800377ebb..3fb9826ae8 100644 --- a/_i_byond_manager_8cs_source.html +++ b/_i_byond_manager_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IByondManager.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Collections.Generic;
4 using System.Threading;
5 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Components.Byond
8 {
13  {
17  Version ActiveVersion { get; }
18 
22  IReadOnlyList<Version> InstalledVersions { get; }
23 
29  Task ChangeVersion(Version version, CancellationToken cancellationToken);
30 
37  Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken);
38  }
39 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Collections.Generic;
4 using System.Threading;
5 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Components.Byond
8 {
13  {
17  Version ActiveVersion { get; }
18 
22  IReadOnlyList<Version> InstalledVersions { get; }
23 
30  Task ChangeVersion(Version version, CancellationToken cancellationToken);
31 
38  Task<IByondExecutableLock> UseExecutables(Version requiredVersion, CancellationToken cancellationToken);
39  }
40 }
For managing the BYOND installation
diff --git a/_i_database_context_8cs_source.html b/_i_database_context_8cs_source.html index 01b1bdfc47..4cd144979a 100644 --- a/_i_database_context_8cs_source.html +++ b/_i_database_context_8cs_source.html @@ -97,7 +97,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
diff --git a/_i_dmb_factory_8cs_source.html b/_i_dmb_factory_8cs_source.html index 3d2062aca2..f22edce58d 100644 --- a/_i_dmb_factory_8cs_source.html +++ b/_i_dmb_factory_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IDmbFactory.cs
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Components.Compiler
7 {
11  public interface IDmbFactory
12  {
17  Task OnNewerDmb { get; }
18 
22  bool DmbAvailable { get; }
23 
29  IDmbProvider LockNextDmb(int lockCount);
30 
37  Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken);
38 
45  Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken);
46 
47  }
48 }
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Components.Compiler
7 {
11  public interface IDmbFactory
12  {
17  Task OnNewerDmb { get; }
18 
22  bool DmbAvailable { get; }
23 
29  IDmbProvider LockNextDmb(int lockCount);
30 
37  Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken);
38 
45  Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken);
46  }
47 }
diff --git a/_i_instance_8cs_source.html b/_i_instance_8cs_source.html index d6d5ea30b9..359e548e0d 100644 --- a/_i_instance_8cs_source.html +++ b/_i_instance_8cs_source.html @@ -92,10 +92,10 @@ 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;
4 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Components
13 {
17  public interface IInstance : IHostedService, IDisposable
18  {
23 
28 
33 
37  IChat Chat { get; }
38 
43 
48  CompileJob LatestCompileJob();
49 
54  void Rename(string newName);
55 
61  Task SetAutoUpdateInterval(uint newInterval);
62 
71  Task CompileProcess(Job job, IDatabaseContext databaseContext, Action<int> progressReporter, CancellationToken cancellationToken);
72  }
73 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Threading;
4 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Components
13 {
17  public interface IInstance : IHostedService, IDisposable
18  {
23 
28 
33 
37  IChat Chat { get; }
38 
43 
48  CompileJob LatestCompileJob();
49 
54  void Rename(string newName);
55 
61  Task SetAutoUpdateInterval(uint newInterval);
62 
71  Task CompileProcess(Job job, IDatabaseContext databaseContext, Action<int> progressReporter, CancellationToken cancellationToken);
72  }
73 }
- - + + @@ -106,7 +106,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For interacting with the instance services
Definition: IInstance.cs:17
For managing the BYOND installation
For managing the Configuration directory
- + @@ -115,7 +115,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - + diff --git a/_i_job_manager_8cs_source.html b/_i_job_manager_8cs_source.html index 0e448f7036..ff764f553c 100644 --- a/_i_job_manager_8cs_source.html +++ b/_i_job_manager_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IJobManager.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;
6 
7 namespace Tgstation.Server.Host.Core
8 {
12  public interface IJobManager : IHostedService
13  {
17  int? JobProgress(Job job);
18 
26  Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken);
27 
36 #pragma warning disable CA1068 // CancellationToken parameters must come last https://github.com/dotnet/roslyn-analyzers/issues/1816
37  Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken);
38 #pragma warning restore CA1068 // CancellationToken parameters must come last
39 
48  Task<bool> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken);
49  }
50 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Threading;
4 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Core
8 {
12  public interface IJobManager : IHostedService
13  {
19  int? JobProgress(Job job);
20 
28  Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken);
29 
38 #pragma warning disable CA1068 // CancellationToken parameters must come last https://github.com/dotnet/roslyn-analyzers/issues/1816
39  Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken);
40 #pragma warning restore CA1068 // CancellationToken parameters must come last
41 
50  Task<bool> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken);
51  }
52 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
diff --git a/_i_post_write_handler_8cs.html b/_i_post_write_handler_8cs.html index dc91331be8..3e1d66207b 100644 --- a/_i_post_write_handler_8cs.html +++ b/_i_post_write_handler_8cs.html @@ -101,6 +101,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

Classes

interface  Tgstation.Server.Host.IO.IPostWriteHandler
 Handles changing file modes/permissions after writing More...
 
- +

diff --git a/_i_post_write_handler_8cs_source.html b/_i_post_write_handler_8cs_source.html index 8bc9a0b3be..6440305d77 100644 --- a/_i_post_write_handler_8cs_source.html +++ b/_i_post_write_handler_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IPostWriteHandler.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.IO
2 {
3  interface IPostWriteHandler
4  {
9  void HandleWrite(string filePath);
10  }
11 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Host.IO
2 {
6  interface IPostWriteHandler
7  {
12  void HandleWrite(string filePath);
13  }
14 }
Handles changing file modes/permissions after writing
diff --git a/_i_server_client_8cs_source.html b/_i_server_client_8cs_source.html index 9d28d78b1b..6fac88b51e 100644 --- a/_i_server_client_8cs_source.html +++ b/_i_server_client_8cs_source.html @@ -92,12 +92,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IServerClient.cs
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Client
7 {
11  public interface IServerClient : IDisposable
12  {
16  Uri Url { get; }
17 
21  Token Token { get; set; }
22 
26  TimeSpan Timeout { get; set; }
27 
31  IInstanceManagerClient Instances { get; }
32 
37 
41  IUsersClient Users { get; }
42 
46  Task<ServerInformation> Version(CancellationToken cancellationToken);
47 
52  void AddRequestLogger(IRequestLogger requestLogger);
53  }
54 }
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Client
7 {
11  public interface IServerClient : IDisposable
12  {
16  Uri Url { get; }
17 
21  Token Token { get; set; }
22 
26  TimeSpan Timeout { get; set; }
27 
31  IInstanceManagerClient Instances { get; }
32 
37 
41  IUsersClient Users { get; }
42 
48  Task<ServerInformation> Version(CancellationToken cancellationToken);
49 
54  void AddRequestLogger(IRequestLogger requestLogger);
55  }
56 }
- +
Represents administrative server information
Main client for communicating with a server
Represents a JWT returned by the API
Definition: Token.cs:8
diff --git a/_identity_cache_8cs_source.html b/_identity_cache_8cs_source.html index 8c476a4996..4484b93d09 100644 --- a/_identity_cache_8cs_source.html +++ b/_identity_cache_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IdentityCache.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
7 
8 namespace Tgstation.Server.Host.Security
9 {
12  {
17 
21  readonly ILogger<IdentityCache> logger;
22 
26  readonly Dictionary<long, IdentityCacheObject> cachedIdentities;
27 
33  public IdentityCache(IAsyncDelayer asyncDelayer, ILogger<IdentityCache> logger)
34  {
35  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
36  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
37 
38  cachedIdentities = new Dictionary<long, IdentityCacheObject>();
39  }
40 
42  public void Dispose()
43  {
44  logger.LogTrace("Disposing...");
45  foreach (var I in cachedIdentities.Select(x => x.Value).ToList())
46  I.Dispose();
47  }
48 
50  public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry)
51  {
52  if (user == null)
53  throw new ArgumentNullException(nameof(user));
54  if (systemIdentity == null)
55  throw new ArgumentNullException(nameof(systemIdentity));
56  lock (cachedIdentities)
57  {
58  var uid = systemIdentity.Uid;
59  logger.LogDebug("Caching system identity {0} of user {1}", uid, user.Id);
60 
61  if (cachedIdentities.TryGetValue(user.Id, out var identCache))
62  {
63  logger.LogTrace("Expiring previously cached identity...");
64  identCache.Dispose(); //also clears it out
65  }
66  identCache = new IdentityCacheObject(systemIdentity.Clone(), asyncDelayer, () =>
67  {
68  logger.LogDebug("Expiring system identity cache for user {1}", uid, user.Id);
69  lock (cachedIdentities)
70  cachedIdentities.Remove(user.Id);
71  }, expiry);
72  cachedIdentities.Add(user.Id, identCache);
73  }
74  }
75 
78  {
79  if (user == null)
80  throw new ArgumentNullException(nameof(user));
81  lock (cachedIdentities)
82  if (cachedIdentities.TryGetValue(user.Id, out var identity))
83  return identity.SystemIdentity.Clone();
84  throw new InvalidOperationException("Cached system identity has expired!");
85  }
86  }
87 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
7 
8 namespace Tgstation.Server.Host.Security
9 {
12  {
17 
21  readonly ILogger<IdentityCache> logger;
22 
26  readonly Dictionary<long, IdentityCacheObject> cachedIdentities;
27 
33  public IdentityCache(IAsyncDelayer asyncDelayer, ILogger<IdentityCache> logger)
34  {
35  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
36  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
37 
38  cachedIdentities = new Dictionary<long, IdentityCacheObject>();
39  }
40 
42  public void Dispose()
43  {
44  logger.LogTrace("Disposing...");
45  foreach (var I in cachedIdentities.Select(x => x.Value).ToList())
46  I.Dispose();
47  }
48 
50  public void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry)
51  {
52  if (user == null)
53  throw new ArgumentNullException(nameof(user));
54  if (systemIdentity == null)
55  throw new ArgumentNullException(nameof(systemIdentity));
56  lock (cachedIdentities)
57  {
58  var uid = systemIdentity.Uid;
59  logger.LogDebug("Caching system identity {0} of user {1}", uid, user.Id);
60 
61  if (cachedIdentities.TryGetValue(user.Id, out var identCache))
62  {
63  logger.LogTrace("Expiring previously cached identity...");
64  identCache.Dispose(); // also clears it out
65  }
66 
67  identCache = new IdentityCacheObject(systemIdentity.Clone(), asyncDelayer, () =>
68  {
69  logger.LogDebug("Expiring system identity cache for user {1}", uid, user.Id);
70  lock (cachedIdentities)
71  cachedIdentities.Remove(user.Id);
72  }, expiry);
73  cachedIdentities.Add(user.Id, identCache);
74  }
75  }
76 
79  {
80  if (user == null)
81  throw new ArgumentNullException(nameof(user));
82  lock (cachedIdentities)
83  if (cachedIdentities.TryGetValue(user.Id, out var identity))
84  return identity.SystemIdentity.Clone();
85  throw new InvalidOperationException("Cached system identity has expired!");
86  }
87  }
88 }
Represents a user on the current System.Runtime.InteropServices.OSPlatform
@@ -104,7 +104,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IdentityCache(IAsyncDelayer asyncDelayer, ILogger< IdentityCache > logger)
Construct an IdentityCache
void CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry)
Keep a user &#39;s systemIdentity alive until an expiry time
long Id
The ID of the User
Definition: User.cs:15
-
ISystemIdentity LoadCachedIdentity(User user)
Attempt to load a cached ISystemIdentity
+
ISystemIdentity LoadCachedIdentity(User user)
Attempt to load a cached ISystemIdentity
ISystemIdentity Clone()
Clone the ISystemIdentity creating another copy that must have IDisposable.Dispose called on it ...
readonly ILogger< IdentityCache > logger
The ILogger for the IdentityCache
diff --git a/_instance_controller_8cs_source.html b/_instance_controller_8cs_source.html index 3c52c1c10e..11630bb480 100644 --- a/_instance_controller_8cs_source.html +++ b/_instance_controller_8cs_source.html @@ -92,68 +92,68 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.IO;
8 using System.Linq;
9 using System.Linq.Expressions;
10 using System.Net;
11 using System.Reflection;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
19 using Tgstation.Server.Host.IO;
22 
23 namespace Tgstation.Server.Host.Controllers
24 {
28  [Route(Routes.InstanceManager)]
29  public sealed class InstanceController : ModelController<Api.Models.Instance>
30  {
34  const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
35 
36  const string MoveInstanceJobPrefix = "Move instance ID ";
37 
42 
47 
52 
57 
62 
74  public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger<InstanceController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
75  {
76  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
77  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
78  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
79  this.application = application ?? throw new ArgumentNullException(nameof(application));
80  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
81  }
82 
83  void NormalizeModelPath(Api.Models.Instance model, out string absolutePath)
84  {
85  if (model.Path == null)
86  {
87  absolutePath = null;
88  return;
89  }
90  absolutePath = ioManager.ResolvePath(model.Path);
91  if (platformIdentifier.IsWindows)
92  model.Path = absolutePath.ToUpperInvariant();
93  else
94  model.Path = absolutePath;
95  }
96 
97  Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
98  {
99  ByondRights = (ByondRights)~0U,
106  UserId = AuthenticationContext.User.Id
107  };
108 
110  [TgsAuthorize(InstanceManagerRights.Create)]
111  public override async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
112  {
113  if (model == null)
114  throw new ArgumentNullException(nameof(model));
115 
116  if (String.IsNullOrWhiteSpace(model.Name))
117  return BadRequest(new ErrorMessage { Message = "name must not be empty!" });
118 
119  if (model.Path == null)
120  return BadRequest(new ErrorMessage { Message = "path must not be empty!" });
121 
122  NormalizeModelPath(model, out var rawPath);
123 
124  var localPath = ioManager.ResolvePath(".");
125  NormalizeModelPath(new Api.Models.Instance
126  {
127  Path = localPath
128  }, out var normalizedLocalPath);
129 
130  if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal))
131  return Conflict("Instances cannot be created in the installation directory!");
132 
133 
134  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
135  bool attached = false;
136  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
137  if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
138  return Conflict(new ErrorMessage { Message = "Path not empty!" });
139  else
140  attached = true;
141 
142  var newInstance = new Models.Instance
143  {
144  ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
146  {
147  AllowWebClient = false,
148  AutoStart = false,
149  PrimaryPort = 1337,
150  SecondaryPort = 1338,
151  SecurityLevel = DreamDaemonSecurity.Safe,
152  SoftRestart = false,
153  SoftShutdown = false,
154  StartupTimeout = 20
155  },
157  {
158  ApiValidationPort = 1339,
159  ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
160  },
161  Name = model.Name,
162  Online = false,
163  Path = model.Path,
164  AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
166  {
167  CommitterEmail = "tgstation-server@users.noreply.github.com",
168  CommitterName = application.VersionPrefix,
169  PushTestMergeCommits = false,
170  ShowTestMergeCommitters = false,
171  AutoUpdatesKeepTestMerges = false,
172  AutoUpdatesSynchronize = false
173  },
174  //give this user full privileges on the instance
175  InstanceUsers = new List<Models.InstanceUser>
176  {
177  InstanceAdminUser()
178  }
179  };
180 
181  DatabaseContext.Instances.Add(newInstance);
182  try
183  {
184  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
185 
186  try
187  {
188  //actually reserve it now
189  await ioManager.CreateDirectory(rawPath, cancellationToken).ConfigureAwait(false);
190  await ioManager.DeleteFile(ioManager.ConcatPath(rawPath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
191  }
192  catch
193  {
194  //oh shit delete the model
195  DatabaseContext.Instances.Remove(newInstance);
196 
197  await DatabaseContext.Save(default).ConfigureAwait(false);
198  throw;
199  }
200  }
201  catch (IOException e)
202  {
203  return Conflict(new ErrorMessage { Message = e.Message });
204  }
205  catch (DbUpdateException e)
206  {
207  return Conflict(new ErrorMessage { Message = e.Message });
208  }
209 
210  Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
211 
212  var api = newInstance.ToApi();
213  return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
214  }
215 
217  [TgsAuthorize(InstanceManagerRights.Delete)]
218  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
219  {
220  var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id)
221  .Include(x => x.WatchdogReattachInformation)
222  .Include(x => x.WatchdogReattachInformation.Alpha)
223  .Include(x => x.WatchdogReattachInformation.Bravo)
224  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
225  if (originalModel == default)
226  return StatusCode((int)HttpStatusCode.Gone);
227  if (originalModel.Online.Value)
228  return Conflict(new ErrorMessage
229  {
230  Message = "Cannot detach an online instance!"
231  });
232 
233  if (originalModel.WatchdogReattachInformation != null)
234  {
235  DatabaseContext.WatchdogReattachInformations.Remove(originalModel.WatchdogReattachInformation);
236  if (originalModel.WatchdogReattachInformation.Alpha != null)
237  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Alpha);
238  if (originalModel.WatchdogReattachInformation.Bravo != null)
239  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Bravo);
240  }
241 
242  DatabaseContext.Instances.Remove(originalModel);
243 
244  var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
245  await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), default).ConfigureAwait(false);
246  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); //cascades everything
247  return Ok();
248  }
249 
251  [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)]
252  public override async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
253  {
254  var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id);
255 
256  var moveJob = await instanceQuery
257  .SelectMany(x => x.Jobs).
258 #pragma warning disable CA1307 // Specify StringComparison
259  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
260 #pragma warning restore CA1307 // Specify StringComparison
261  .Select(x => new Models.Job
262  {
263  Id = x.Id
264  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
265 
266  if (moveJob != default)
267  //cancel it now
268  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false);
269 
270  var usersInstanceUserTask = instanceQuery.SelectMany(x => x.InstanceUsers).Where(x => x.UserId == AuthenticationContext.User.Id).FirstOrDefaultAsync(cancellationToken);
271 
272  var originalModel = await instanceQuery
273  .Include(x => x.RepositorySettings)
274  .Include(x => x.ChatSettings)
275  .ThenInclude(x => x.Channels)
276  .Include(x => x.DreamDaemonSettings) //need these for onlining
277  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
278  if (originalModel == default(Models.Instance))
279  return StatusCode((int)HttpStatusCode.Gone);
280 
281  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
282  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
283  {
284  var memberSelectorExpression = (MemberExpression)expression.Body;
285  var property = (PropertyInfo)memberSelectorExpression.Member;
286 
287  var newVal = property.GetValue(model);
288  if (newVal == null)
289  return false;
290  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
291  return true;
292 
293  property.SetValue(originalModel, newVal);
294  return false;
295  };
296 
297  string originalModelPath = null;
298  string rawPath = null;
299  if (model.Path != null)
300  {
301  NormalizeModelPath(model, out rawPath);
302 
303  if (model.Path != originalModel.Path)
304  {
305  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
306  return Forbid();
307  if (originalModel.Online.Value && model.Online != true)
308  return Conflict(new ErrorMessage { Message = "Cannot relocate an online instance!" });
309 
310  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
311  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
312  return Conflict(new ErrorMessage { Message = "Path not empty!" });
313 
314  originalModelPath = originalModel.Path;
315  originalModel.Path = model.Path;
316  }
317  }
318 
319  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
320  var originalOnline = originalModel.Online.Value;
321  var renamed = model.Name != null && originalModel.Name != model.Name;
322 
323  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
324  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
325  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
326  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline))
327  return Forbid();
328 
329  //ensure the current user has write privilege on the instance
330  var usersInstanceUser = await usersInstanceUserTask.ConfigureAwait(false);
331  if (usersInstanceUser == default)
332  {
333  var instanceAdminUser = InstanceAdminUser();
334  instanceAdminUser.InstanceId = originalModel.Id;
335  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
336  }
337  else
338  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
339 
340  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
341 
342  if (renamed)
343  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
344 
345  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
346  try
347  {
348  if (originalOnline && model.Online == false)
349  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
350  else if (!originalOnline && model.Online == true)
351  {
352  //force autostart false here because we don't want any long running jobs right now
353  //remember to document this
354  originalModel.DreamDaemonSettings.AutoStart = false;
355  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
356  }
357  }
358  catch (Exception e)
359  {
360  if(!(e is OperationCanceledException))
361  Logger.LogError("Error changing instance online state! Exception: {0}", e);
362  originalModel.Online = originalOnline;
363  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
364  if (originalModelPath != null)
365  originalModel.Path = originalModelPath;
366  await DatabaseContext.Save(default).ConfigureAwait(false);
367  throw;
368  }
369 
370  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
371  {
372  Id = originalModel.Id
373  };
374  if (originalModelPath != null)
375  {
376  var job = new Models.Job
377  {
378  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
379  Instance = originalModel,
380  CancelRightsType = RightsType.InstanceManager,
381  CancelRight = (ulong)InstanceManagerRights.Relocate,
382  StartedBy = AuthenticationContext.User
383  };
384 
385  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
386  api.MoveJob = job.ToApi();
387  }
388 
389  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
390  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
391 
392  return Json(api);
393  }
394 
396  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
397  public override async Task<IActionResult> List(CancellationToken cancellationToken)
398  {
399  IQueryable<Models.Instance> query = DatabaseContext.Instances;
401  query = query.Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id)).Where(x => x.InstanceUsers.Any(y => y.AnyRights));
402 
403  var moveJobTasks = query
404  .SelectMany(x => x.Jobs)
405 #pragma warning disable CA1307 // Specify StringComparison
406  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
407 #pragma warning restore CA1307 // Specify StringComparison
408  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
409  .Include(x => x.Instance)
410  .ToListAsync(cancellationToken);
411  var instances = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
412  var apis = instances.Select(x => x.ToApi());
413  var moveJobs = await moveJobTasks.ConfigureAwait(false);
414  foreach(var I in moveJobs)
415  apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi(); //if this .First() fails i will personally murder kevinz000 because I just know he is somehow responsible
416  return Json(apis);
417  }
418 
420  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
421  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
422  {
423  var query = DatabaseContext.Instances.Where(x => x.Id == id);
424  var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
425 
426  if (cantList)
427  query = query.Include(x => x.InstanceUsers);
428 
429  var moveJobTask = query
430  .SelectMany(x => x.Jobs)
431 #pragma warning disable CA1307 // Specify StringComparison
432  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
433 #pragma warning restore CA1307 // Specify StringComparison
434  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
435  .FirstOrDefaultAsync(cancellationToken);
436  var instance = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
437 
438  if (instance == null)
439  return StatusCode((int)HttpStatusCode.Gone);
440 
441  if (cantList && !instance.InstanceUsers.Any(x => x.UserId == AuthenticationContext.User.Id && x.AnyRights))
442  return Forbid();
443 
444  var api = instance.ToApi();
445  api.MoveJob = (await moveJobTask.ConfigureAwait(false))?.ToApi();
446  return Json(api);
447  }
448  }
449 }
string Name
The name of the User
Definition: User.cs:38
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.IO;
8 using System.Linq;
9 using System.Linq.Expressions;
10 using System.Net;
11 using System.Reflection;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
19 using Tgstation.Server.Host.IO;
22 
23 namespace Tgstation.Server.Host.Controllers
24 {
28  [Route(Routes.InstanceManager)]
29  #pragma warning disable CA1506 // TODO: Decomplexify
30  public sealed class InstanceController : ModelController<Api.Models.Instance>
31  {
35  const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
36 
37  const string MoveInstanceJobPrefix = "Move instance ID ";
38 
43 
48 
53 
58 
63 
75  public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger<InstanceController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
76  {
77  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
78  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
79  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
80  this.application = application ?? throw new ArgumentNullException(nameof(application));
81  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
82  }
83 
84  void NormalizeModelPath(Api.Models.Instance model, out string absolutePath)
85  {
86  if (model.Path == null)
87  {
88  absolutePath = null;
89  return;
90  }
91 
92  absolutePath = ioManager.ResolvePath(model.Path);
93  if (platformIdentifier.IsWindows)
94  model.Path = absolutePath.ToUpperInvariant();
95  else
96  model.Path = absolutePath;
97  }
98 
99  Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
100  {
101  ByondRights = (ByondRights)~0U,
108  UserId = AuthenticationContext.User.Id
109  };
110 
112  [TgsAuthorize(InstanceManagerRights.Create)]
113  public override async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
114  {
115  if (model == null)
116  throw new ArgumentNullException(nameof(model));
117 
118  if (String.IsNullOrWhiteSpace(model.Name))
119  return BadRequest(new ErrorMessage { Message = "name must not be empty!" });
120 
121  if (model.Path == null)
122  return BadRequest(new ErrorMessage { Message = "path must not be empty!" });
123 
124  NormalizeModelPath(model, out var rawPath);
125 
126  var localPath = ioManager.ResolvePath(".");
127  NormalizeModelPath(new Api.Models.Instance
128  {
129  Path = localPath
130  }, out var normalizedLocalPath);
131 
132  if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal))
133  return Conflict("Instances cannot be created in the installation directory!");
134 
135  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
136  bool attached = false;
137  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
138  if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
139  return Conflict(new ErrorMessage { Message = "Path not empty!" });
140  else
141  attached = true;
142 
143  var newInstance = new Models.Instance
144  {
145  ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
147  {
148  AllowWebClient = false,
149  AutoStart = false,
150  PrimaryPort = 1337,
151  SecondaryPort = 1338,
152  SecurityLevel = DreamDaemonSecurity.Safe,
153  SoftRestart = false,
154  SoftShutdown = false,
155  StartupTimeout = 20
156  },
158  {
159  ApiValidationPort = 1339,
160  ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
161  },
162  Name = model.Name,
163  Online = false,
164  Path = model.Path,
165  AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
167  {
168  CommitterEmail = "tgstation-server@users.noreply.github.com",
169  CommitterName = application.VersionPrefix,
170  PushTestMergeCommits = false,
171  ShowTestMergeCommitters = false,
172  AutoUpdatesKeepTestMerges = false,
173  AutoUpdatesSynchronize = false
174  },
175  InstanceUsers = new List<Models.InstanceUser> // give this user full privileges on the instance
176  {
177  InstanceAdminUser()
178  }
179  };
180 
181  DatabaseContext.Instances.Add(newInstance);
182  try
183  {
184  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
185 
186  try
187  {
188  // actually reserve it now
189  await ioManager.CreateDirectory(rawPath, cancellationToken).ConfigureAwait(false);
190  await ioManager.DeleteFile(ioManager.ConcatPath(rawPath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
191  }
192  catch
193  {
194  // oh shit delete the model
195  DatabaseContext.Instances.Remove(newInstance);
196 
197  await DatabaseContext.Save(default).ConfigureAwait(false);
198  throw;
199  }
200  }
201  catch (IOException e)
202  {
203  return Conflict(new ErrorMessage { Message = e.Message });
204  }
205  catch (DbUpdateException e)
206  {
207  return Conflict(new ErrorMessage { Message = e.Message });
208  }
209 
210  Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
211 
212  var api = newInstance.ToApi();
213  return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
214  }
215 
217  [TgsAuthorize(InstanceManagerRights.Delete)]
218  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
219  {
220  var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id)
221  .Include(x => x.WatchdogReattachInformation)
222  .Include(x => x.WatchdogReattachInformation.Alpha)
223  .Include(x => x.WatchdogReattachInformation.Bravo)
224  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
225  if (originalModel == default)
226  return StatusCode((int)HttpStatusCode.Gone);
227  if (originalModel.Online.Value)
228  return Conflict(new ErrorMessage
229  {
230  Message = "Cannot detach an online instance!"
231  });
232 
233  if (originalModel.WatchdogReattachInformation != null)
234  {
235  DatabaseContext.WatchdogReattachInformations.Remove(originalModel.WatchdogReattachInformation);
236  if (originalModel.WatchdogReattachInformation.Alpha != null)
237  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Alpha);
238  if (originalModel.WatchdogReattachInformation.Bravo != null)
239  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Bravo);
240  }
241 
242  DatabaseContext.Instances.Remove(originalModel);
243 
244  var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
245  await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), default).ConfigureAwait(false);
246  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); // cascades everything
247  return Ok();
248  }
249 
251  [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)]
252  #pragma warning disable CA1502 // TODO: Decomplexify
253  public override async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
254  {
255  var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id);
256 
257  var moveJob = await instanceQuery
258  .SelectMany(x => x.Jobs).
259 #pragma warning disable CA1307 // Specify StringComparison
260  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
261 #pragma warning restore CA1307 // Specify StringComparison
262  .Select(x => new Models.Job
263  {
264  Id = x.Id
265  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
266 
267  if (moveJob != default)
268  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false); // cancel it now
269 
270  var usersInstanceUserTask = instanceQuery.SelectMany(x => x.InstanceUsers).Where(x => x.UserId == AuthenticationContext.User.Id).FirstOrDefaultAsync(cancellationToken);
271 
272  var originalModel = await instanceQuery
273  .Include(x => x.RepositorySettings)
274  .Include(x => x.ChatSettings)
275  .ThenInclude(x => x.Channels)
276  .Include(x => x.DreamDaemonSettings) // need these for onlining
277  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
278  if (originalModel == default(Models.Instance))
279  return StatusCode((int)HttpStatusCode.Gone);
280 
281  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
282  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
283  {
284  var memberSelectorExpression = (MemberExpression)expression.Body;
285  var property = (PropertyInfo)memberSelectorExpression.Member;
286 
287  var newVal = property.GetValue(model);
288  if (newVal == null)
289  return false;
290  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
291  return true;
292 
293  property.SetValue(originalModel, newVal);
294  return false;
295  }
296 
297  string originalModelPath = null;
298  string rawPath = null;
299  if (model.Path != null)
300  {
301  NormalizeModelPath(model, out rawPath);
302 
303  if (model.Path != originalModel.Path)
304  {
305  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
306  return Forbid();
307  if (originalModel.Online.Value && model.Online != true)
308  return Conflict(new ErrorMessage { Message = "Cannot relocate an online instance!" });
309 
310  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
311  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
312  return Conflict(new ErrorMessage { Message = "Path not empty!" });
313 
314  originalModelPath = originalModel.Path;
315  originalModel.Path = model.Path;
316  }
317  }
318 
319  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
320  var originalOnline = originalModel.Online.Value;
321  var renamed = model.Name != null && originalModel.Name != model.Name;
322 
323  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
324  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
325  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
326  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline))
327  return Forbid();
328 
329  // ensure the current user has write privilege on the instance
330  var usersInstanceUser = await usersInstanceUserTask.ConfigureAwait(false);
331  if (usersInstanceUser == default)
332  {
333  var instanceAdminUser = InstanceAdminUser();
334  instanceAdminUser.InstanceId = originalModel.Id;
335  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
336  }
337  else
338  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
339 
340  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
341 
342  if (renamed)
343  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
344 
345  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
346  try
347  {
348  if (originalOnline && model.Online == false)
349  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
350  else if (!originalOnline && model.Online == true)
351  {
352  // force autostart false here because we don't want any long running jobs right now
353  // remember to document this
354  originalModel.DreamDaemonSettings.AutoStart = false;
355  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
356  }
357  }
358  catch (Exception e)
359  {
360  if(!(e is OperationCanceledException))
361  Logger.LogError("Error changing instance online state! Exception: {0}", e);
362  originalModel.Online = originalOnline;
363  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
364  if (originalModelPath != null)
365  originalModel.Path = originalModelPath;
366  await DatabaseContext.Save(default).ConfigureAwait(false);
367  throw;
368  }
369 
370  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
371  {
372  Id = originalModel.Id
373  };
374  if (originalModelPath != null)
375  {
376  var job = new Models.Job
377  {
378  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
379  Instance = originalModel,
380  CancelRightsType = RightsType.InstanceManager,
381  CancelRight = (ulong)InstanceManagerRights.Relocate,
382  StartedBy = AuthenticationContext.User
383  };
384 
385  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
386  api.MoveJob = job.ToApi();
387  }
388 
389  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
390  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
391 
392  return Json(api);
393  }
394  #pragma warning restore CA1502
395 
397  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
398  public override async Task<IActionResult> List(CancellationToken cancellationToken)
399  {
400  IQueryable<Models.Instance> query = DatabaseContext.Instances;
402  query = query.Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id)).Where(x => x.InstanceUsers.Any(y => y.AnyRights));
403 
404  var moveJobTasks = query
405  .SelectMany(x => x.Jobs)
406 #pragma warning disable CA1307 // Specify StringComparison
407  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
408 #pragma warning restore CA1307 // Specify StringComparison
409  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
410  .Include(x => x.Instance)
411  .ToListAsync(cancellationToken);
412  var instances = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
413  var apis = instances.Select(x => x.ToApi());
414  var moveJobs = await moveJobTasks.ConfigureAwait(false);
415  foreach(var I in moveJobs)
416  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
417  return Json(apis);
418  }
419 
421  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
422  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
423  {
424  var query = DatabaseContext.Instances.Where(x => x.Id == id);
425  var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
426 
427  if (cantList)
428  query = query.Include(x => x.InstanceUsers);
429 
430  var moveJobTask = query
431  .SelectMany(x => x.Jobs)
432 #pragma warning disable CA1307 // Specify StringComparison
433  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
434 #pragma warning restore CA1307 // Specify StringComparison
435  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
436  .FirstOrDefaultAsync(cancellationToken);
437  var instance = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
438 
439  if (instance == null)
440  return StatusCode((int)HttpStatusCode.Gone);
441 
442  if (cantList && !instance.InstanceUsers.Any(x => x.UserId == AuthenticationContext.User.Id && x.AnyRights))
443  return Forbid();
444 
445  var api = instance.ToApi();
446  api.MoveJob = (await moveJobTask.ConfigureAwait(false))?.ToApi();
447  return Json(api);
448  }
449  }
450 }
string Name
The name of the User
Definition: User.cs:38
Manages the runtime of Jobs
Definition: IJobManager.cs:12
ConfigurationRights
Rights for Models.ConfigurationFile
-
InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger< InstanceController > logger)
Construct a InstanceController
+
InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger< InstanceController > logger)
Construct a InstanceController
User can enable Models.Internal.DreamDaemonSettings.SoftRestart
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
-
DbSet< ReattachInformation > ReattachInformations
+
DbSet< ReattachInformation > ReattachInformations
InstanceUserRights
Rights for an Models.Instance
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
InstanceManagerRights InstanceManagerRights
The Rights.InstanceManagerRights for the User
Definition: User.cs:50
-
readonly IInstanceManager instanceManager
The IInstanceManager for the InstanceController
-
readonly IApplication application
The IApplication for the InstanceController
+
readonly IInstanceManager instanceManager
The IInstanceManager for the InstanceController
+
readonly IApplication application
The IApplication for the InstanceController
- - + +
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ConfigurationType
The type of configuration allowed on an Instance
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
-
Controller for managing Components.Instances
+
Controller for managing Components.Instances
-
readonly IIOManager ioManager
The IIOManager for the InstanceController
-
readonly IJobManager jobManager
The IJobManager for the InstanceController
+
readonly IIOManager ioManager
The IIOManager for the InstanceController
+
readonly IJobManager jobManager
The IJobManager for the InstanceController
Routes to a server actions
Definition: Routes.cs:9
DreamDaemonRights
Rights for Models.DreamDaemon
InstanceManagerRights
Rights for managing Models.Instances
string Message
A human readable description of the error
Definition: ErrorMessage.cs:13
DreamMakerRights
Rights for Models.DreamMaker
- - -
override async Task< IActionResult > Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
+ + +
override async Task< IActionResult > Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
RepositoryRights
Rights for a Models.Repository
An ApiController representing a TModel
-
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
+
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
const string InstanceManager
The Models.Instance controller
Definition: Routes.cs:29
-
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the InstanceController
+
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the InstanceController
long Id
The ID of the User
Definition: User.cs:15
Metadata about a server instance
Definition: Instance.cs:9
override async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Attempt to delete a model with a particular id
-
DbSet< WatchdogReattachInformation > WatchdogReattachInformations
+
DbSet< WatchdogReattachInformation > WatchdogReattachInformations
DreamDaemonSecurity
DreamDaemon&#39;s security level
Interface for using filesystems
Definition: IIOManager.cs:11
-
override async Task< IActionResult > Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
-
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
+
override async Task< IActionResult > Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
+
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
For identifying the current platform
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
-
void NormalizeModelPath(Api.Models.Instance model, out string absolutePath)
+
void NormalizeModelPath(Api.Models.Instance model, out string absolutePath)
Represents an error message returned by the server
Definition: ErrorMessage.cs:8
User can enable Models.Internal.DreamDaemonSettings.SoftShutdown
diff --git a/_instance_factory_8cs_source.html b/_instance_factory_8cs_source.html index 6c30cdbf97..d7c3d79b62 100644 --- a/_instance_factory_8cs_source.html +++ b/_instance_factory_8cs_source.html @@ -92,14 +92,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceFactory.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Threading;
5 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
15 
16 namespace Tgstation.Server.Host.Components
17 {
20  {
25 
30 
35 
39  readonly ILoggerFactory loggerFactory;
40 
44  readonly IByondTopicSender byondTopicSender;
45 
50 
55 
60 
65 
70 
75 
80 
85 
90 
95 
100 
105 
110 
132  public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IChatFactory chatFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory, IJobManager jobManager, ICredentialsProvider credentialsProvider, INetworkPromptReaper networkPromptReaper, IGitHubClientFactory gitHubClientFactory, IPlatformIdentifier platformIdentifier)
133  {
134  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
135  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
136  this.application = application ?? throw new ArgumentNullException(nameof(application));
137  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
138  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
139  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
140  this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
141  this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
142  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
143  this.chatFactory = chatFactory ?? throw new ArgumentNullException(nameof(chatFactory));
144  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
145  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
146  this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory));
147  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
148  this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
149  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
150  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
151  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
152  }
153 
155  public IInstance CreateInstance(Models.Instance metadata)
156  {
157  //Create the ioManager for the instance
158  var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path);
159 
160  //various other ioManagers
161  var repoIoManager = new ResolvingIOManager(instanceIoManager, "Repository");
162  var byondIOManager = new ResolvingIOManager(instanceIoManager, "Byond");
163  var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game");
164  var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");
165 
166  var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, platformIdentifier, loggerFactory.CreateLogger<StaticFiles.Configuration>());
167  var eventConsumer = new EventConsumer(configuration);
168 
169  var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger<DmbFactory>(), metadata.CloneMetadata());
170  try
171  {
172  var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer, credentialsProvider, loggerFactory.CreateLogger<Repository.Repository>(), loggerFactory.CreateLogger<RepositoryManager>());
173  try
174  {
175  var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger<ByondManager>());
176 
177  var commandFactory = new CommandFactory(application, byond, repoManager, databaseContextFactory, metadata);
178 
179  var chat = chatFactory.CreateChat(instanceIoManager, commandFactory, metadata.ChatSettings);
180  try
181  {
182  var sessionControllerFactory = new SessionControllerFactory(processExecutor, byond, byondTopicSender, cryptographySuite, application, gameIoManager, chat, networkPromptReaper, platformIdentifier, loggerFactory, metadata.CloneMetadata());
183  var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, loggerFactory.CreateLogger<ReattachInfoHandler>(), metadata.CloneMetadata());
184  var watchdog = watchdogFactory.CreateWatchdog(chat, dmbFactory, reattachInfoHandler, configuration, sessionControllerFactory, metadata.CloneMetadata(), metadata.DreamDaemonSettings);
185  eventConsumer.SetWatchdog(watchdog);
186  commandFactory.SetWatchdog(watchdog);
187  try
188  {
189  var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, chat, processExecutor, watchdog, loggerFactory.CreateLogger<DreamMaker>());
190 
191  return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, jobManager, eventConsumer, gitHubClientFactory, loggerFactory.CreateLogger<Instance>());
192  }
193  catch
194  {
195  watchdog.Dispose();
196  throw;
197  }
198  }
199  catch
200  {
201  chat.Dispose();
202  throw;
203  }
204  }
205  catch
206  {
207  repoManager.Dispose();
208  throw;
209  }
210  }
211  catch
212  {
213  dmbFactory.Dispose();
214  throw;
215  }
216  }
217 
219  public Task StartAsync(CancellationToken cancellationToken) => byondInstaller.CleanCache(cancellationToken);
220 
222  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
223  }
224 }
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Threading;
5 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
15 
16 namespace Tgstation.Server.Host.Components
17 {
20  {
25 
30 
35 
39  readonly ILoggerFactory loggerFactory;
40 
44  readonly IByondTopicSender byondTopicSender;
45 
50 
55 
60 
65 
70 
75 
80 
85 
90 
95 
100 
105 
110 
132  public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IChatFactory chatFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory, IJobManager jobManager, ICredentialsProvider credentialsProvider, INetworkPromptReaper networkPromptReaper, IGitHubClientFactory gitHubClientFactory, IPlatformIdentifier platformIdentifier)
133  {
134  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
135  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
136  this.application = application ?? throw new ArgumentNullException(nameof(application));
137  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
138  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
139  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
140  this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
141  this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
142  this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
143  this.chatFactory = chatFactory ?? throw new ArgumentNullException(nameof(chatFactory));
144  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
145  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
146  this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory));
147  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
148  this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
149  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
150  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
151  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
152  }
153 
155  #pragma warning disable CA1506 // TODO: Decomplexify
156  public IInstance CreateInstance(Models.Instance metadata)
157  {
158  // Create the ioManager for the instance
159  var instanceIoManager = new ResolvingIOManager(ioManager, metadata.Path);
160 
161  // various other ioManagers
162  var repoIoManager = new ResolvingIOManager(instanceIoManager, "Repository");
163  var byondIOManager = new ResolvingIOManager(instanceIoManager, "Byond");
164  var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game");
165  var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");
166 
167  var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, platformIdentifier, loggerFactory.CreateLogger<StaticFiles.Configuration>());
168  var eventConsumer = new EventConsumer(configuration);
169 
170  var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger<DmbFactory>(), metadata.CloneMetadata());
171  try
172  {
173  var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer, credentialsProvider, loggerFactory.CreateLogger<Repository.Repository>(), loggerFactory.CreateLogger<RepositoryManager>());
174  try
175  {
176  var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger<ByondManager>());
177 
178  var commandFactory = new CommandFactory(application, byond, repoManager, databaseContextFactory, metadata);
179 
180  var chat = chatFactory.CreateChat(instanceIoManager, commandFactory, metadata.ChatSettings);
181  try
182  {
183  var sessionControllerFactory = new SessionControllerFactory(processExecutor, byond, byondTopicSender, cryptographySuite, application, gameIoManager, chat, networkPromptReaper, platformIdentifier, loggerFactory, metadata.CloneMetadata());
184  var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, loggerFactory.CreateLogger<ReattachInfoHandler>(), metadata.CloneMetadata());
185  var watchdog = watchdogFactory.CreateWatchdog(chat, dmbFactory, reattachInfoHandler, configuration, sessionControllerFactory, metadata.CloneMetadata(), metadata.DreamDaemonSettings);
186  eventConsumer.SetWatchdog(watchdog);
187  commandFactory.SetWatchdog(watchdog);
188  try
189  {
190  var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, chat, processExecutor, watchdog, loggerFactory.CreateLogger<DreamMaker>());
191 
192  return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, jobManager, eventConsumer, gitHubClientFactory, loggerFactory.CreateLogger<Instance>());
193  }
194  catch
195  {
196  watchdog.Dispose();
197  throw;
198  }
199  }
200  catch
201  {
202  chat.Dispose();
203  throw;
204  }
205  }
206  catch
207  {
208  repoManager.Dispose();
209  throw;
210  }
211  }
212  catch
213  {
214  dmbFactory.Dispose();
215  throw;
216  }
217  }
218  #pragma warning restore CA1506
219 
221  public Task StartAsync(CancellationToken cancellationToken) => byondInstaller.CleanCache(cancellationToken);
222 
224  public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
225  }
226 }
readonly ISynchronousIOManager synchronousIOManager
The ISynchronousIOManager for the InstanceFactory
Manages the runtime of Jobs
Definition: IJobManager.cs:12
readonly IByondInstaller byondInstaller
The IByondInstaller for the InstanceFactory
-
+
Handles changing file modes/permissions after writing
An IIOManager that resolve relative paths from another IIOManager to a subdirectory of that ...
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
@@ -117,7 +117,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IApplication application
The IApplication for the InstanceFactory
-
Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger< Repository > logger, Action onDispose)
Construct a Repository
Definition: Repository.cs:118
+
Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger< Repository > logger, Action onDispose)
Construct a Repository
Definition: Repository.cs:103
Contains various cryptographic functions
readonly IProcessExecutor processExecutor
The IProcessExecutor for the InstanceFactory
@@ -148,15 +148,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
IInstance CreateInstance(Models.Instance metadata)
Create an IInstance
+
IInstance CreateInstance(Models.Instance metadata)
Create an IInstance
Interface for using filesystems
Definition: IIOManager.cs:11
On Windows, DreamDaemon will show an unskippable prompt when using /world/proc/OpenPort(). This looks out for those prompts and immediately clicks "Yes" if the owning process has registered for it
For identifying the current platform
- + - +
diff --git a/_instance_manager_8cs_source.html b/_instance_manager_8cs_source.html index 47b8a5161f..71161acc59 100644 --- a/_instance_manager_8cs_source.html +++ b/_instance_manager_8cs_source.html @@ -92,17 +92,17 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManager.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components
13 {
16  {
21 
26 
31 
36 
41 
46 
50  readonly ILogger<InstanceManager> logger;
51 
55  readonly Dictionary<long, IInstance> instances;
56 
61 
65  bool disposed;
66 
77  public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger<InstanceManager> logger)
78  {
79  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
80  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
81  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
82  this.application = application ?? throw new ArgumentNullException(nameof(application));
83  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
84  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
85  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
86 
87  serverControl.RegisterForRestart(this);
88 
89  instances = new Dictionary<long, IInstance>();
90  }
91 
93  public void Dispose()
94  {
95  lock (this)
96  {
97  if (disposed)
98  return;
99  disposed = true;
100  }
101  foreach (var I in instances)
102  I.Value.Dispose();
103  }
104 
106  public IInstance GetInstance(Models.Instance metadata)
107  {
108  if (metadata == null)
109  throw new ArgumentNullException(nameof(metadata));
110  lock (this)
111  {
112  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
113  throw new InvalidOperationException("Instance not online!");
114  return instance;
115  }
116  }
117 
119  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
120  {
121  if (newPath == null)
122  throw new ArgumentNullException(nameof(newPath));
123  if (instance.Online.Value)
124  throw new InvalidOperationException("Cannot move an online instance!");
125  var oldPath = instance.Path;
126  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
127  await databaseContextFactory.UseContext(db =>
128  {
129  var targetInstance = new Models.Instance
130  {
131  Id = instance.Id
132  };
133  db.Instances.Attach(targetInstance);
134  targetInstance.Path = newPath;
135  return db.Save(cancellationToken);
136  }).ConfigureAwait(false);
137  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
138  }
139 
141  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
142  {
143  if (metadata == null)
144  throw new ArgumentNullException(nameof(metadata));
145  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
146  IInstance instance;
147  lock (this)
148  {
149  if (!instances.TryGetValue(metadata.Id, out instance))
150  throw new InvalidOperationException("Instance not online!");
151  instances.Remove(metadata.Id);
152  }
153  try
154  {
155  //we are the one responsible for cancelling his jobs
156  var tasks = new List<Task>();
157  await databaseContextFactory.UseContext(async db =>
158  {
159  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
160  {
161  Id = x.Id
162  }).ToAsyncEnumerable();
163  await jobs.ForEachAsync(job =>
164  {
165  lock (tasks)
166  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
167  }, cancellationToken).ConfigureAwait(false);
168  }).ConfigureAwait(false);
169 
170  await Task.WhenAll(tasks).ConfigureAwait(false);
171 
172  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
173  }
174  finally
175  {
176  instance.Dispose();
177  }
178  }
179 
181  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
182  {
183  if (metadata == null)
184  throw new ArgumentNullException(nameof(metadata));
185  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
186  var instance = instanceFactory.CreateInstance(metadata);
187  try
188  {
189  lock (this)
190  {
191  if (instances.ContainsKey(metadata.Id))
192  throw new InvalidOperationException("Instance already online!");
193  instances.Add(metadata.Id, instance);
194  }
195  }
196  catch
197  {
198  instance.Dispose();
199  throw;
200  }
201  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
202  }
203 
205  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
206  {
207  try
208  {
209  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
210  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
211  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
212  var dbInstances = databaseContext.Instances.Where(x => x.Online.Value)
213  .Include(x => x.RepositorySettings)
214  .Include(x => x.ChatSettings)
215  .ThenInclude(x => x.Channels)
216  .Include(x => x.DreamDaemonSettings)
217  .ToAsyncEnumerable();
218  var tasks = new List<Task>();
219  await factoryStartup.ConfigureAwait(false);
220  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
221  await Task.WhenAll(tasks).ConfigureAwait(false);
222  logger.LogInformation("Server ready!");
223  application.Ready(null);
224  }
225  catch (OperationCanceledException)
226  {
227  logger.LogInformation("Cancelled instance manager initialization!");
228  }
229  catch (Exception e)
230  {
231  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
232  application.Ready(e);
233  try
234  {
235  await serverControl.Die(e).ConfigureAwait(false);
236  }
237  catch (Exception e2)
238  {
239  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
240  }
241  }
242  });
243 
245  public async Task StopAsync(CancellationToken cancellationToken)
246  {
247  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
248  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
249  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
250 
251  //downgrade the db if necessary
252  if (downgradeVersion != null)
253  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
254  }
255 
257  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
258  {
259  downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
260  return Task.CompletedTask;
261  }
262  }
263 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components
13 {
16  {
21 
26 
31 
36 
41 
46 
50  readonly ILogger<InstanceManager> logger;
51 
55  readonly Dictionary<long, IInstance> instances;
56 
61 
65  bool disposed;
66 
77  public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger<InstanceManager> logger)
78  {
79  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
80  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
81  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
82  this.application = application ?? throw new ArgumentNullException(nameof(application));
83  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
84  this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
85  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
86 
87  serverControl.RegisterForRestart(this);
88 
89  instances = new Dictionary<long, IInstance>();
90  }
91 
93  public void Dispose()
94  {
95  lock (this)
96  {
97  if (disposed)
98  return;
99  disposed = true;
100  }
101 
102  foreach (var I in instances)
103  I.Value.Dispose();
104  }
105 
107  public IInstance GetInstance(Models.Instance metadata)
108  {
109  if (metadata == null)
110  throw new ArgumentNullException(nameof(metadata));
111  lock (this)
112  {
113  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
114  throw new InvalidOperationException("Instance not online!");
115  return instance;
116  }
117  }
118 
120  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
121  {
122  if (newPath == null)
123  throw new ArgumentNullException(nameof(newPath));
124  if (instance.Online.Value)
125  throw new InvalidOperationException("Cannot move an online instance!");
126  var oldPath = instance.Path;
127  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
128  await databaseContextFactory.UseContext(db =>
129  {
130  var targetInstance = new Models.Instance
131  {
132  Id = instance.Id
133  };
134  db.Instances.Attach(targetInstance);
135  targetInstance.Path = newPath;
136  return db.Save(cancellationToken);
137  }).ConfigureAwait(false);
138  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
139  }
140 
142  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
143  {
144  if (metadata == null)
145  throw new ArgumentNullException(nameof(metadata));
146  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
147  IInstance instance;
148  lock (this)
149  {
150  if (!instances.TryGetValue(metadata.Id, out instance))
151  throw new InvalidOperationException("Instance not online!");
152  instances.Remove(metadata.Id);
153  }
154 
155  try
156  {
157  // we are the one responsible for cancelling his jobs
158  var tasks = new List<Task>();
159  await databaseContextFactory.UseContext(async db =>
160  {
161  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
162  {
163  Id = x.Id
164  }).ToAsyncEnumerable();
165  await jobs.ForEachAsync(job =>
166  {
167  lock (tasks)
168  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
169  }, cancellationToken).ConfigureAwait(false);
170  }).ConfigureAwait(false);
171 
172  await Task.WhenAll(tasks).ConfigureAwait(false);
173 
174  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
175  }
176  finally
177  {
178  instance.Dispose();
179  }
180  }
181 
183  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
184  {
185  if (metadata == null)
186  throw new ArgumentNullException(nameof(metadata));
187  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
188  var instance = instanceFactory.CreateInstance(metadata);
189  try
190  {
191  lock (this)
192  {
193  if (instances.ContainsKey(metadata.Id))
194  throw new InvalidOperationException("Instance already online!");
195  instances.Add(metadata.Id, instance);
196  }
197  }
198  catch
199  {
200  instance.Dispose();
201  throw;
202  }
203 
204  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
205  }
206 
208  #pragma warning disable CA1506 // TODO: Decomplexify
209  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
210  {
211  try
212  {
213  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
214  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
215  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
216  var dbInstances = databaseContext.Instances.Where(x => x.Online.Value)
217  .Include(x => x.RepositorySettings)
218  .Include(x => x.ChatSettings)
219  .ThenInclude(x => x.Channels)
220  .Include(x => x.DreamDaemonSettings)
221  .ToAsyncEnumerable();
222  var tasks = new List<Task>();
223  await factoryStartup.ConfigureAwait(false);
224  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
225  await Task.WhenAll(tasks).ConfigureAwait(false);
226  logger.LogInformation("Server ready!");
227  application.Ready(null);
228  }
229  catch (OperationCanceledException)
230  {
231  logger.LogInformation("Cancelled instance manager initialization!");
232  }
233  catch (Exception e)
234  {
235  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
236  application.Ready(e);
237  try
238  {
239  await serverControl.Die(e).ConfigureAwait(false);
240  }
241  catch (Exception e2)
242  {
243  logger.LogCritical("Failed to kill server! Exception: {0}", e2);
244  }
245  }
246  });
247  #pragma warning restore CA1506 // TODO: Decomplexify
248 
250  public async Task StopAsync(CancellationToken cancellationToken)
251  {
252  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
253  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
254  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
255 
256  // downgrade the db if necessary
257  if (downgradeVersion != null)
258  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
259  }
260 
262  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
263  {
264  downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
265  return Task.CompletedTask;
266  }
267  }
268 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
readonly IServerControl serverControl
The IServerControl for the InstanceManager
-
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
+
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
-
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
+
async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
Online an IInstance
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
bool disposed
If the InstanceManager has been Disposed
-
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
-
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
+
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
+
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
@@ -111,7 +111,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
For interacting with the instance services
Definition: IInstance.cs:17
-
async Task StopAsync(CancellationToken cancellationToken)
+
async Task StopAsync(CancellationToken cancellationToken)
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
@@ -123,7 +123,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IIOManager ioManager
The IIOManager for the InstanceManager
Interface for using filesystems
Definition: IIOManager.cs:11
-
async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
Move an IInstance
+
async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
Move an IInstance
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
diff --git a/_instance_manager_client_8cs_source.html b/_instance_manager_client_8cs_source.html index db05b4e684..7177db3bc2 100644 --- a/_instance_manager_client_8cs_source.html +++ b/_instance_manager_client_8cs_source.html @@ -92,7 +92,7 @@ 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.List(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  return client;
59  }
60  }
61 }
+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.List(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 }
diff --git a/_instance_manager_rights_8cs_source.html b/_instance_manager_rights_8cs_source.html index 2aa9dd2497..4266fc00a6 100644 --- a/_instance_manager_rights_8cs_source.html +++ b/_instance_manager_rights_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManagerRights.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum InstanceManagerRights : ulong
10  {
14  None = 0,
18  Read = 1,
22  Create = 2,
26  Rename = 4,
30  Relocate = 8,
34  SetOnline = 16,
38  Delete = 32,
42  List = 64,
46  SetConfiguration = 128,
50  SetAutoUpdate = 256
51  }
52 }
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum InstanceManagerRights : ulong
10  {
14  None = 0,
15 
19  Read = 1,
20 
24  Create = 2,
25 
29  Rename = 4,
30 
34  Relocate = 8,
35 
39  SetOnline = 16,
40 
44  Delete = 32,
45 
49  List = 64,
50 
54  SetConfiguration = 128,
55 
59  SetAutoUpdate = 256
60  }
61 }
User can change Models.Instance.ConfigurationType
diff --git a/_instance_manager_test_8cs_source.html b/_instance_manager_test_8cs_source.html index 40ecfc2107..9e8fa79aa7 100644 --- a/_instance_manager_test_8cs_source.html +++ b/_instance_manager_test_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManagerTest.cs
-Go to the documentation of this file.
1 using Microsoft.VisualStudio.TestTools.UnitTesting;
2 using System;
3 using System.IO;
4 using System.Reflection;
5 using System.Threading;
6 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Tests
13 {
14  sealed class InstanceManagerTest
15  {
17  readonly string testRootPath;
18 
19  long counter;
20 
21  public InstanceManagerTest(IInstanceManagerClient instanceManagerClient, string testRootPath)
22  {
23  this.instanceManagerClient = instanceManagerClient ?? throw new ArgumentNullException(nameof(instanceManagerClient));
24  this.testRootPath = testRootPath ?? throw new ArgumentNullException(nameof(testRootPath));
25 
26  counter = 0;
27  }
28 
29  Task<Api.Models.Instance> CreateTestInstance(CancellationToken cancellationToken) => instanceManagerClient.CreateOrAttach(new Api.Models.Instance
30  {
31  Name = "TestInstance-" + ++counter,
32  Path = Path.Combine(testRootPath, Guid.NewGuid().ToString()),
33  Online = true
34  }, cancellationToken);
35 
36  public async Task Run(CancellationToken cancellationToken)
37  {
38  var firstTest = await CreateTestInstance(cancellationToken).ConfigureAwait(false);
39  //instances always start offline
40  Assert.AreEqual(false, firstTest.Online);
41  //check it exists
42  Assert.IsTrue(Directory.Exists(firstTest.Path));
43 
44  //cant create instances in existent directories
45  var testNonEmpty = Path.Combine(testRootPath, Guid.NewGuid().ToString());
46  Directory.CreateDirectory(testNonEmpty);
47  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(new Api.Models.Instance
48  {
49  Path = testNonEmpty,
50  Name = "NonEmptyTest"
51  }, cancellationToken)).ConfigureAwait(false);
52  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(firstTest, cancellationToken)).ConfigureAwait(false);
53 
54  //can't create instances in installation directory
55  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(new Api.Models.Instance
56  {
57  Path = "./A/Local/Path",
58  Name = "NoInstallDirTest"
59  }, cancellationToken)).ConfigureAwait(false);
60 
61  //can't move to existent directories
62  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.Update(new Api.Models.Instance
63  {
64  Id = firstTest.Id,
65  Path = testNonEmpty
66  }, cancellationToken)).ConfigureAwait(false);
67 
68  //test basic move
69  Directory.Delete(testNonEmpty);
70  var initialPath = firstTest.Path;
71  firstTest = await instanceManagerClient.Update(new Api.Models.Instance
72  {
73  Id = firstTest.Id,
74  Path = testNonEmpty
75  }, cancellationToken).ConfigureAwait(false);
76 
77  Assert.IsNotNull(firstTest.MoveJob);
78 
79  do
80  {
81  firstTest = await instanceManagerClient.GetId(firstTest, cancellationToken).ConfigureAwait(false);
82  await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
83  } while (firstTest.MoveJob != null);
84 
85 
86  //online it for real for component tests
87  firstTest.Online = true;
88  firstTest.ConfigurationType = ConfigurationType.HostWrite;
89  firstTest = await instanceManagerClient.Update(firstTest, cancellationToken).ConfigureAwait(false);
90  Assert.AreEqual(true, firstTest.Online);
91  Assert.AreEqual(ConfigurationType.HostWrite, firstTest.ConfigurationType);
92 
93  //can't move online instance
94  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.Update(new Api.Models.Instance
95  {
96  Id = firstTest.Id,
97  Path = initialPath
98  }, cancellationToken)).ConfigureAwait(false);
99 
100  var testSuite1 = new InstanceTest(instanceManagerClient.CreateClient(firstTest));
101  await testSuite1.RunTests(cancellationToken).ConfigureAwait(false);
102 
103  //can regain permissions on instance without instance user
104  var instanceClient = instanceManagerClient.CreateClient(firstTest);
105  var ourInstanceUser = await instanceClient.Users.Read(cancellationToken).ConfigureAwait(false);
106  await instanceClient.Users.Delete(ourInstanceUser, cancellationToken).ConfigureAwait(false);
107 
108  await Assert.ThrowsExceptionAsync<InsufficientPermissionsException>(() => instanceClient.Users.Read(cancellationToken)).ConfigureAwait(false);
109 
110  await instanceManagerClient.Update(new Api.Models.Instance
111  {
112  Id = firstTest.Id
113  }, cancellationToken).ConfigureAwait(false);
114  ourInstanceUser = await instanceClient.Users.Read(cancellationToken).ConfigureAwait(false);
115 
116  //can't detach online instance
117  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.Detach(firstTest, cancellationToken)).ConfigureAwait(false);
118 
119  firstTest.Online = false;
120  firstTest = await instanceManagerClient.Update(firstTest, cancellationToken).ConfigureAwait(false);
121  await instanceManagerClient.Detach(firstTest, cancellationToken).ConfigureAwait(false);
122 
123  var instanceAttachFileName = (string)typeof(InstanceController).GetField("InstanceAttachFileName", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
124  var attachPath = Path.Combine(firstTest.Path, instanceAttachFileName);
125  Assert.IsTrue(File.Exists(attachPath));
126 
127  //can recreate detached instance
128  firstTest = await instanceManagerClient.CreateOrAttach(firstTest, cancellationToken).ConfigureAwait(false);
129 
130  //but only if the attach file exists
131  await instanceManagerClient.Detach(firstTest, cancellationToken).ConfigureAwait(false);
132  File.Delete(attachPath);
133  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(firstTest, cancellationToken)).ConfigureAwait(false);
134  }
135  }
136 }
+Go to the documentation of this file.
1 using Microsoft.VisualStudio.TestTools.UnitTesting;
2 using System;
3 using System.IO;
4 using System.Reflection;
5 using System.Threading;
6 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Tests
13 {
14  sealed class InstanceManagerTest
15  {
17  readonly string testRootPath;
18 
19  long counter;
20 
21  public InstanceManagerTest(IInstanceManagerClient instanceManagerClient, string testRootPath)
22  {
23  this.instanceManagerClient = instanceManagerClient ?? throw new ArgumentNullException(nameof(instanceManagerClient));
24  this.testRootPath = testRootPath ?? throw new ArgumentNullException(nameof(testRootPath));
25 
26  counter = 0;
27  }
28 
29  Task<Api.Models.Instance> CreateTestInstance(CancellationToken cancellationToken) => instanceManagerClient.CreateOrAttach(new Api.Models.Instance
30  {
31  Name = "TestInstance-" + ++counter,
32  Path = Path.Combine(testRootPath, Guid.NewGuid().ToString()),
33  Online = true
34  }, cancellationToken);
35 
36  public async Task Run(CancellationToken cancellationToken)
37  {
38  var firstTest = await CreateTestInstance(cancellationToken).ConfigureAwait(false);
39  //instances always start offline
40  Assert.AreEqual(false, firstTest.Online);
41  //check it exists
42  Assert.IsTrue(Directory.Exists(firstTest.Path));
43 
44  //cant create instances in existent directories
45  var testNonEmpty = Path.Combine(testRootPath, Guid.NewGuid().ToString());
46  Directory.CreateDirectory(testNonEmpty);
47  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(new Api.Models.Instance
48  {
49  Path = testNonEmpty,
50  Name = "NonEmptyTest"
51  }, cancellationToken)).ConfigureAwait(false);
52  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(firstTest, cancellationToken)).ConfigureAwait(false);
53 
54  //can't create instances in installation directory
55  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(new Api.Models.Instance
56  {
57  Path = "./A/Local/Path",
58  Name = "NoInstallDirTest"
59  }, cancellationToken)).ConfigureAwait(false);
60 
61  //can't move to existent directories
62  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.Update(new Api.Models.Instance
63  {
64  Id = firstTest.Id,
65  Path = testNonEmpty
66  }, cancellationToken)).ConfigureAwait(false);
67 
68  //test basic move
69  Directory.Delete(testNonEmpty);
70  var initialPath = firstTest.Path;
71  firstTest = await instanceManagerClient.Update(new Api.Models.Instance
72  {
73  Id = firstTest.Id,
74  Path = testNonEmpty
75  }, cancellationToken).ConfigureAwait(false);
76 
77  Assert.IsNotNull(firstTest.MoveJob);
78 
79  do
80  {
81  firstTest = await instanceManagerClient.GetId(firstTest, cancellationToken).ConfigureAwait(false);
82  await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
83  } while (firstTest.MoveJob != null);
84 
85 
86  //online it for real for component tests
87  firstTest.Online = true;
88  firstTest.ConfigurationType = ConfigurationType.HostWrite;
89  firstTest = await instanceManagerClient.Update(firstTest, cancellationToken).ConfigureAwait(false);
90  Assert.AreEqual(true, firstTest.Online);
91  Assert.AreEqual(ConfigurationType.HostWrite, firstTest.ConfigurationType);
92 
93  //can't move online instance
94  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.Update(new Api.Models.Instance
95  {
96  Id = firstTest.Id,
97  Path = initialPath
98  }, cancellationToken)).ConfigureAwait(false);
99 
100  var testSuite1 = new InstanceTest(instanceManagerClient.CreateClient(firstTest));
101  await testSuite1.RunTests(cancellationToken).ConfigureAwait(false);
102 
103  //can regain permissions on instance without instance user
104  var instanceClient = instanceManagerClient.CreateClient(firstTest);
105  var ourInstanceUser = await instanceClient.Users.Read(cancellationToken).ConfigureAwait(false);
106  await instanceClient.Users.Delete(ourInstanceUser, cancellationToken).ConfigureAwait(false);
107 
108  await Assert.ThrowsExceptionAsync<InsufficientPermissionsException>(() => instanceClient.Users.Read(cancellationToken)).ConfigureAwait(false);
109 
110  await instanceManagerClient.Update(new Api.Models.Instance
111  {
112  Id = firstTest.Id
113  }, cancellationToken).ConfigureAwait(false);
114  ourInstanceUser = await instanceClient.Users.Read(cancellationToken).ConfigureAwait(false);
115 
116  //can't detach online instance
117  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.Detach(firstTest, cancellationToken)).ConfigureAwait(false);
118 
119  firstTest.Online = false;
120  firstTest = await instanceManagerClient.Update(firstTest, cancellationToken).ConfigureAwait(false);
121  await instanceManagerClient.Detach(firstTest, cancellationToken).ConfigureAwait(false);
122 
123  var instanceAttachFileName = (string)typeof(InstanceController).GetField("InstanceAttachFileName", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
124  var attachPath = Path.Combine(firstTest.Path, instanceAttachFileName);
125  Assert.IsTrue(File.Exists(attachPath));
126 
127  //can recreate detached instance
128  firstTest = await instanceManagerClient.CreateOrAttach(firstTest, cancellationToken).ConfigureAwait(false);
129 
130  //but only if the attach file exists
131  await instanceManagerClient.Detach(firstTest, cancellationToken).ConfigureAwait(false);
132  File.Delete(attachPath);
133  await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(firstTest, cancellationToken)).ConfigureAwait(false);
134  }
135  }
136 }
async Task RunTests(CancellationToken cancellationToken)
Definition: InstanceTest.cs:17
@@ -102,7 +102,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ConfigurationType
The type of configuration allowed on an Instance
-
Controller for managing Components.Instances
+
Controller for managing Components.Instances
async Task Run(CancellationToken cancellationToken)
diff --git a/_instance_user_client_8cs_source.html b/_instance_user_client_8cs_source.html index e88b5abec3..9f430d0348 100644 --- a/_instance_user_client_8cs_source.html +++ b/_instance_user_client_8cs_source.html @@ -92,13 +92,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceUserClient.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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
12  {
20  readonly Instance instance;
21 
27  public InstanceUserClient(IApiClient apiClient, Instance instance)
28  {
29  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
30  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
31  }
32 
34  public Task<InstanceUser> Create(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Create<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
35 
36  public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceUser, instanceUser.UserId.Value), instance.Id, cancellationToken);
37 
39  public Task<InstanceUser> Read(CancellationToken cancellationToken) => apiClient.Read<InstanceUser>(Routes.InstanceUser, instance.Id, cancellationToken);
40 
42  public Task<InstanceUser> Update(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Update<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
43 
45  public Task<IReadOnlyList<InstanceUser>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<InstanceUser>>(Routes.List(Routes.InstanceUser), instance.Id, cancellationToken);
46 
48  public Task<InstanceUser> GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Read<InstanceUser>(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken);
49  }
50 }
readonly IApiClient apiClient
The IApiClient for the InstanceUserClient
+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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
12  {
17 
21  readonly Instance instance;
22 
28  public InstanceUserClient(IApiClient apiClient, Instance instance)
29  {
30  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
31  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
32  }
33 
35  public Task<InstanceUser> Create(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Create<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
36 
38  public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceUser, instanceUser.UserId.Value), instance.Id, cancellationToken);
39 
41  public Task<InstanceUser> Read(CancellationToken cancellationToken) => apiClient.Read<InstanceUser>(Routes.InstanceUser, instance.Id, cancellationToken);
42 
44  public Task<InstanceUser> Update(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Update<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
45 
47  public Task<IReadOnlyList<InstanceUser>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<InstanceUser>>(Routes.List(Routes.InstanceUser), instance.Id, cancellationToken);
48 
50  public Task<InstanceUser> GetId(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Read<InstanceUser>(Routes.SetID(Routes.InstanceUser, instanceUser?.UserId ?? throw new ArgumentNullException(nameof(instanceUser))), instance.Id, cancellationToken);
51  }
52 }
readonly IApiClient apiClient
The IApiClient for the InstanceUserClient
-
readonly Instance instance
The Instance for the InstanceUserClient
+
readonly Instance instance
The Instance for the InstanceUserClient
const string InstanceUser
The Models.InstanceUser controller
Definition: Routes.cs:64
-
InstanceUserClient(IApiClient apiClient, Instance instance)
Construct an InstanceUserClient
+
InstanceUserClient(IApiClient apiClient, Instance instance)
Construct an InstanceUserClient
Routes to a server actions
Definition: Routes.cs:9
diff --git a/_instance_user_controller_8cs_source.html b/_instance_user_controller_8cs_source.html index c7d4c32db9..2c7ad7f1e5 100644 --- a/_instance_user_controller_8cs_source.html +++ b/_instance_user_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceUserController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Net;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
14 using Z.EntityFramework.Plus;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.InstanceUser)]
22  public sealed class InstanceUserController : ModelController<Api.Models.InstanceUser>
23  {
30  public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, logger, true) //false instance requirement, we handle this ourself
31  { }
32 
38  BadRequestObjectResult StandardModelChecks(Api.Models.InstanceUser model)
39  {
40  if (model == null)
41  throw new ArgumentNullException(nameof(model));
42 
43  if (!model.UserId.HasValue)
44  return BadRequest(new ErrorMessage { Message = "Missing UserId!" });
45 
46  return null;
47  }
48 
50  [TgsAuthorize(InstanceUserRights.CreateUsers)]
51  public override async Task<IActionResult> Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
52  {
53  var test = StandardModelChecks(model);
54  if (test != null)
55  return test;
56 
57  var dbUser = new Models.InstanceUser
58  {
59  ByondRights = model.ByondRights ?? ByondRights.None,
60  ChatBotRights = model.ChatBotRights ?? ChatBotRights.None,
61  ConfigurationRights = model.ConfigurationRights ?? ConfigurationRights.None,
62  DreamDaemonRights = model.DreamDaemonRights ?? DreamDaemonRights.None,
63  DreamMakerRights = model.DreamMakerRights ?? DreamMakerRights.None,
64  RepositoryRights = model.RepositoryRights ?? RepositoryRights.None,
65  InstanceUserRights = model.InstanceUserRights ?? InstanceUserRights.None,
66  UserId = model.UserId,
67  InstanceId = Instance.Id
68  };
69 
70  DatabaseContext.InstanceUsers.Add(dbUser);
71 
72  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
73  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
74  }
75 
77  [TgsAuthorize(InstanceUserRights.WriteUsers)]
78  public override async Task<IActionResult> Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
79  {
80  var test = StandardModelChecks(model);
81  if (test != null)
82  return test;
83 
84  var originalUser = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == model.UserId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
85  if (originalUser == null)
86  return StatusCode((int)HttpStatusCode.Gone);
87 
88  originalUser.ByondRights = model.ByondRights ?? originalUser.ByondRights;
89  originalUser.RepositoryRights = model.RepositoryRights ?? originalUser.RepositoryRights;
90  originalUser.InstanceUserRights = model.InstanceUserRights ?? originalUser.InstanceUserRights;
91  originalUser.ChatBotRights = model.ChatBotRights ?? originalUser.ChatBotRights;
92  originalUser.ConfigurationRights = model.ConfigurationRights ?? originalUser.ConfigurationRights;
93  originalUser.DreamDaemonRights = model.DreamDaemonRights ?? originalUser.DreamDaemonRights;
94  originalUser.DreamMakerRights = model.DreamMakerRights ?? originalUser.DreamMakerRights;
95 
96  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
97  return Json(originalUser.UserId == AuthenticationContext.User.Id || (AuthenticationContext.GetRight(RightsType.InstanceUser) & (ulong)InstanceUserRights.ReadUsers) != 0 ? originalUser.ToApi() : new Api.Models.InstanceUser
98  {
99  UserId = originalUser.UserId
100  });
101  }
102 
104  [TgsAuthorize]
105  public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult(AuthenticationContext.InstanceUser != null ? (IActionResult)Json(AuthenticationContext.InstanceUser.ToApi()) : NotFound());
106 
108  [TgsAuthorize(InstanceUserRights.ReadUsers)]
109  public override async Task<IActionResult> List(CancellationToken cancellationToken)
110  {
111  var users = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).ToListAsync(cancellationToken).ConfigureAwait(false);
112  return Json(users.Select(x => x.ToApi()));
113  }
114 
116  [TgsAuthorize(InstanceUserRights.ReadUsers)]
117  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
118  {
119  //this functions as userId
120  var user = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
121  if (user == default)
122  return StatusCode((int)HttpStatusCode.Gone);
123  return Json(user.ToApi());
124  }
125 
127  [TgsAuthorize(InstanceUserRights.WriteUsers)]
128  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
129  {
130  await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).DeleteAsync(cancellationToken).ConfigureAwait(false);
131  return Ok();
132  }
133  }
134 }
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.Linq;
6 using System.Net;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
14 using Z.EntityFramework.Plus;
15 
16 namespace Tgstation.Server.Host.Controllers
17 {
21  [Route(Routes.InstanceUser)]
22  public sealed class InstanceUserController : ModelController<Api.Models.InstanceUser>
23  {
30  public InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InstanceUserController> logger) : base(databaseContext, authenticationContextFactory, logger, true) // false instance requirement, we handle this ourself
31  { }
32 
38  BadRequestObjectResult StandardModelChecks(Api.Models.InstanceUser model)
39  {
40  if (model == null)
41  throw new ArgumentNullException(nameof(model));
42 
43  if (!model.UserId.HasValue)
44  return BadRequest(new ErrorMessage { Message = "Missing UserId!" });
45 
46  return null;
47  }
48 
50  [TgsAuthorize(InstanceUserRights.CreateUsers)]
51  public override async Task<IActionResult> Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
52  {
53  var test = StandardModelChecks(model);
54  if (test != null)
55  return test;
56 
57  var dbUser = new Models.InstanceUser
58  {
59  ByondRights = model.ByondRights ?? ByondRights.None,
60  ChatBotRights = model.ChatBotRights ?? ChatBotRights.None,
61  ConfigurationRights = model.ConfigurationRights ?? ConfigurationRights.None,
62  DreamDaemonRights = model.DreamDaemonRights ?? DreamDaemonRights.None,
63  DreamMakerRights = model.DreamMakerRights ?? DreamMakerRights.None,
64  RepositoryRights = model.RepositoryRights ?? RepositoryRights.None,
65  InstanceUserRights = model.InstanceUserRights ?? InstanceUserRights.None,
66  UserId = model.UserId,
67  InstanceId = Instance.Id
68  };
69 
70  DatabaseContext.InstanceUsers.Add(dbUser);
71 
72  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
73  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi());
74  }
75 
77  [TgsAuthorize(InstanceUserRights.WriteUsers)]
78  #pragma warning disable CA1506 // TODO: Decomplexify
79  public override async Task<IActionResult> Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
80  {
81  var test = StandardModelChecks(model);
82  if (test != null)
83  return test;
84 
85  var originalUser = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == model.UserId).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
86  if (originalUser == null)
87  return StatusCode((int)HttpStatusCode.Gone);
88 
89  originalUser.ByondRights = model.ByondRights ?? originalUser.ByondRights;
90  originalUser.RepositoryRights = model.RepositoryRights ?? originalUser.RepositoryRights;
91  originalUser.InstanceUserRights = model.InstanceUserRights ?? originalUser.InstanceUserRights;
92  originalUser.ChatBotRights = model.ChatBotRights ?? originalUser.ChatBotRights;
93  originalUser.ConfigurationRights = model.ConfigurationRights ?? originalUser.ConfigurationRights;
94  originalUser.DreamDaemonRights = model.DreamDaemonRights ?? originalUser.DreamDaemonRights;
95  originalUser.DreamMakerRights = model.DreamMakerRights ?? originalUser.DreamMakerRights;
96 
97  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
98  return Json(originalUser.UserId == AuthenticationContext.User.Id || (AuthenticationContext.GetRight(RightsType.InstanceUser) & (ulong)InstanceUserRights.ReadUsers) != 0 ? originalUser.ToApi() : new Api.Models.InstanceUser
99  {
100  UserId = originalUser.UserId
101  });
102  }
103  #pragma warning restore CA1506
104 
106  [TgsAuthorize]
107  public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult(AuthenticationContext.InstanceUser != null ? (IActionResult)Json(AuthenticationContext.InstanceUser.ToApi()) : NotFound());
108 
110  [TgsAuthorize(InstanceUserRights.ReadUsers)]
111  public override async Task<IActionResult> List(CancellationToken cancellationToken)
112  {
113  var users = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).ToListAsync(cancellationToken).ConfigureAwait(false);
114  return Json(users.Select(x => x.ToApi()));
115  }
116 
118  [TgsAuthorize(InstanceUserRights.ReadUsers)]
119  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
120  {
121  // this functions as userId
122  var user = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
123  if (user == default)
124  return StatusCode((int)HttpStatusCode.Gone);
125  return Json(user.ToApi());
126  }
127 
129  [TgsAuthorize(InstanceUserRights.WriteUsers)]
130  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
131  {
132  await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).SelectMany(x => x.InstanceUsers).Where(x => x.UserId == id).DeleteAsync(cancellationToken).ConfigureAwait(false);
133  return Ok();
134  }
135  }
136 }
InstanceUserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger< InstanceUserController > logger)
Construct a UserController
ConfigurationRights
Rights for Models.ConfigurationFile
@@ -105,11 +105,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
const string InstanceUser
The Models.InstanceUser controller
Definition: Routes.cs:64
- -
override async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Attempt to delete a model with a particular id
+ +
override async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Attempt to delete a model with a particular id
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
override async Task< IActionResult > Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
-
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
+
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
@@ -118,17 +118,17 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
BadRequestObjectResult StandardModelChecks(Api.Models.InstanceUser model)
Checks a model for errors
DreamMakerRights
Rights for Models.DreamMaker
- - + +
RepositoryRights
Rights for a Models.Repository
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
-
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
+
override async Task< IActionResult > GetId(long id, CancellationToken cancellationToken)
Attempt to get a specific a TModel
An ApiController representing a TModel
long Id
The ID of the User
Definition: User.cs:15
Metadata about a server instance
Definition: Instance.cs:9
-
override async Task< IActionResult > Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
+
override async Task< IActionResult > Update([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
User can read all chat settings except Models.Internal.ChatBot.ConnectionString
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
diff --git a/_instance_user_rights_8cs_source.html b/_instance_user_rights_8cs_source.html index 1e820a43f9..d8e55bfa17 100644 --- a/_instance_user_rights_8cs_source.html +++ b/_instance_user_rights_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceUserRights.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum InstanceUserRights : ulong
10  {
14  None = 0,
18  ReadUsers = 1,
22  WriteUsers = 2,
26  CreateUsers = 4
27  }
28 }
User can edit themself and other Models.Users and also create others
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum InstanceUserRights : ulong
10  {
14  None = 0,
15 
19  ReadUsers = 1,
20 
24  WriteUsers = 2,
25 
29  CreateUsers = 4
30  }
31 }
User can edit themself and other Models.Users and also create others
InstanceUserRights
Rights for an Models.Instance
diff --git a/_integration_test_8cs_source.html b/_integration_test_8cs_source.html index b0106c2c50..1dc7c5060a 100644 --- a/_integration_test_8cs_source.html +++ b/_integration_test_8cs_source.html @@ -105,7 +105,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task Run(CancellationToken cancellationToken)
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:16
- +
Represents administrative server information
Occurs when the client makes a request while the server is starting or stopping
Token Token
The Token used to access the server
Main client for communicating with a server
diff --git a/_irc_connection_string_builder_8cs_source.html b/_irc_connection_string_builder_8cs_source.html index d47fd5c8b5..41f9f026f4 100644 --- a/_irc_connection_string_builder_8cs_source.html +++ b/_irc_connection_string_builder_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IrcConnectionStringBuilder.cs
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
5 
6 namespace Tgstation.Server.Api.Models
7 {
12  {
14  public override bool Valid => Address != null && Port.HasValue && Port != 0 && UseSsl.HasValue && (PasswordType.HasValue ^ Password == null);
15 
19  public string Address { get; set; }
20 
24  public ushort? Port { get; set; }
25 
29  public string Nickname { get; set; }
30 
34  public bool? UseSsl { get; set; }
35 
39  public IrcPasswordType? PasswordType { get; set; }
40 
44  public string Password { get; set; }
45 
50 
55  public IrcConnectionStringBuilder(string connectionString)
56  {
57  if (connectionString == null)
58  throw new ArgumentNullException(nameof(connectionString));
59  var splits = connectionString.Split(';');
60 
61  Address = splits[0];
62 
63  if (splits.Length < 2)
64  return;
65 
66  if (UInt16.TryParse(splits[1], out var port))
67  Port = port;
68 
69  if (splits.Length < 3)
70  return;
71 
72  Nickname = splits[2];
73 
74  if (splits.Length < 4)
75  return;
76 
77  if (Int32.TryParse(splits[3], out var intSsl))
78  UseSsl = Convert.ToBoolean(intSsl);
79 
80  if (splits.Length < 5)
81  return;
82  if (Enum.TryParse<IrcPasswordType>(splits[4], out var passwordType))
83  switch (passwordType)
84  {
85  case IrcPasswordType.NickServ:
86  case IrcPasswordType.Sasl:
87  case IrcPasswordType.Server:
88  PasswordType = passwordType;
89  break;
90  }
91 
92  if (splits.Length < 6)
93  return;
94 
95  var rest = new List<string>(splits);
96  rest.RemoveRange(0, 5);
97  Password = String.Join(";", rest);
98  }
99 
101  public override string ToString()
102  {
103  var sb = new StringBuilder();
104  sb.Append(Address);
105  sb.Append(';');
106  sb.Append(Port);
107  sb.Append(';');
108  sb.Append(Nickname);
109  sb.Append(';');
110  if(UseSsl.HasValue)
111  sb.Append(Convert.ToInt32(UseSsl.Value));
112  if (PasswordType.HasValue)
113  {
114  sb.Append(';');
115  sb.Append((int)PasswordType);
116  sb.Append(';');
117  sb.Append(Password);
118  }
119  return sb.ToString();
120  }
121  }
122 }
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
5 
6 namespace Tgstation.Server.Api.Models
7 {
12  {
14  public override bool Valid => Address != null && Port.HasValue && Port != 0 && UseSsl.HasValue && (PasswordType.HasValue ^ Password == null);
15 
19  public string Address { get; set; }
20 
24  public ushort? Port { get; set; }
25 
29  public string Nickname { get; set; }
30 
34  public bool? UseSsl { get; set; }
35 
39  public IrcPasswordType? PasswordType { get; set; }
40 
44  public string Password { get; set; }
45 
50 
55  public IrcConnectionStringBuilder(string connectionString)
56  {
57  if (connectionString == null)
58  throw new ArgumentNullException(nameof(connectionString));
59  var splits = connectionString.Split(';');
60 
61  Address = splits[0];
62 
63  if (splits.Length < 2)
64  return;
65 
66  if (UInt16.TryParse(splits[1], out var port))
67  Port = port;
68 
69  if (splits.Length < 3)
70  return;
71 
72  Nickname = splits[2];
73 
74  if (splits.Length < 4)
75  return;
76 
77  if (Int32.TryParse(splits[3], out var intSsl))
78  UseSsl = Convert.ToBoolean(intSsl);
79 
80  if (splits.Length < 5)
81  return;
82  if (Enum.TryParse<IrcPasswordType>(splits[4], out var passwordType))
83  switch (passwordType)
84  {
85  case IrcPasswordType.NickServ:
86  case IrcPasswordType.Sasl:
87  case IrcPasswordType.Server:
88  PasswordType = passwordType;
89  break;
90  default:
91  break;
92  }
93 
94  if (splits.Length < 6)
95  return;
96 
97  var rest = new List<string>(splits);
98  rest.RemoveRange(0, 5);
99  Password = String.Join(";", rest);
100  }
101 
103  public override string ToString()
104  {
105  var sb = new StringBuilder();
106  sb.Append(Address);
107  sb.Append(';');
108  sb.Append(Port);
109  sb.Append(';');
110  sb.Append(Nickname);
111  sb.Append(';');
112  if(UseSsl.HasValue)
113  sb.Append(Convert.ToInt32(UseSsl.Value));
114  if (PasswordType.HasValue)
115  {
116  sb.Append(';');
117  sb.Append((int)PasswordType);
118  sb.Append(';');
119  sb.Append(Password);
120  }
121 
122  return sb.ToString();
123  }
124  }
125 }
@@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IrcPasswordType
Represents the type of a password for a ChatProvider.Irc
ChatConnectionStringBuilder for ChatProvider.Irc
-
override string ToString()
Gets the ChatBot.ConnectionString associated with the ChatConnectionStringBuilder ...
+
override string ToString()
Gets the ChatBot.ConnectionString associated with the ChatConnectionStringBuilder ...
diff --git a/_irc_password_type_8cs_source.html b/_irc_password_type_8cs_source.html index 39a7b51a98..01fa88718f 100644 --- a/_irc_password_type_8cs_source.html +++ b/_irc_password_type_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IrcPasswordType.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public enum IrcPasswordType
7  {
11  Server,
15  Sasl,
19  NickServ
20  }
21 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Models
2 {
6  public enum IrcPasswordType
7  {
11  Server,
12 
16  Sasl,
17 
21  NickServ
22  }
23 }
IrcPasswordType
Represents the type of a password for a ChatProvider.Irc
diff --git a/_irc_provider_8cs_source.html b/_irc_provider_8cs_source.html index 217b241e07..edfac8c1ad 100644 --- a/_irc_provider_8cs_source.html +++ b/_irc_provider_8cs_source.html @@ -92,40 +92,40 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IrcProvider.cs
-Go to the documentation of this file.
1 using Meebey.SmartIrc4net;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Components.Chat.Providers
14 {
18  sealed class IrcProvider : Provider
19  {
20  const int TimeoutSeconds = 5;
21 
23  public override bool Connected => client.IsConnected;
24 
26  public override string BotMention => client.Nickname;
27 
32 
36  readonly ILogger<IrcProvider> logger;
37 
41  readonly IrcFeatures client;
42 
46  readonly string address;
50  readonly ushort port;
54  readonly string nickname;
58  readonly string password;
63 
67  readonly Dictionary<ulong, string> channelIdMap;
68 
72  readonly Dictionary<ulong, string> queryChannelIdMap;
73 
78 
82  Task listenTask;
83 
88 
101  public IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger<IrcProvider> logger, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl)
102  {
103  if (application == null)
104  throw new ArgumentNullException(nameof(application));
105  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
106  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
107 
108  this.address = address ?? throw new ArgumentNullException(nameof(address));
109  this.port = port;
110  this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
111 
112  if (passwordType.HasValue && password == null)
113  throw new ArgumentNullException(nameof(password));
114 
115  if (password != null && !passwordType.HasValue)
116  throw new ArgumentNullException(nameof(passwordType));
117 
118  this.password = password;
119  this.passwordType = passwordType;
120 
121  client = new IrcFeatures
122  {
123  SupportNonRfc = true,
124  CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
125  AutoRejoin = true,
126  AutoRejoinOnKick = true,
127  AutoRelogin = true,
128  AutoRetry = true,
129  AutoRetryLimit = TimeoutSeconds,
130  AutoRetryDelay = TimeoutSeconds,
131  ActiveChannelSyncing = true,
132  AutoNickHandling = true,
133  CtcpVersion = application.VersionString,
134  UseSsl = useSsl
135  };
136  if (useSsl)
137  client.ValidateServerCertificate = true; //dunno if it defaults to that or what
138 
139  client.OnChannelMessage += Client_OnChannelMessage;
140  client.OnQueryMessage += Client_OnQueryMessage;
141 
142  channelIdMap = new Dictionary<ulong, string>();
143  queryChannelIdMap = new Dictionary<ulong, string>();
144  channelIdCounter = 1;
145  disconnecting = false;
146  }
147 
149  public override void Dispose()
150  {
151  if (Connected)
152  {
153  disconnecting = true;
154  client.Disconnect(); //just closes the socket
155  }
156  }
157 
163  void HandleMessage(IrcEventArgs e, bool isPrivate)
164  {
165  if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
166  return;
167 
168  var username = e.Data.Nick;
169  var channelName = isPrivate ? username : e.Data.Channel;
170 
171  ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
172  {
173  ulong? resultId = null;
174  if (!dicToCheck.Any(x =>
175  {
176  if (x.Value != channelName)
177  return false;
178  resultId = x.Key;
179  return true;
180  }))
181  {
182  resultId = channelIdCounter++;
183  dicToCheck.Add(resultId.Value, channelName);
184  if (dicToCheck == queryChannelIdMap)
185  channelIdMap.Add(resultId.Value, null);
186  }
187  return resultId.Value;
188  };
189 
190  ulong userId, channelId;
191  lock (this)
192  {
193  userId = MapAndGetChannelId(queryChannelIdMap);
194  channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
195  }
196 
197  var message = new Message
198  {
199  Content = e.Data.Message,
200  User = new User
201  {
202  Channel = new Channel
203  {
204  ConnectionName = address,
205  FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
206  RealId = channelId,
207  IsPrivateChannel = isPrivate
208  //isAdmin and Tag populated by manager
209  },
210  FriendlyName = username,
211  RealId = userId,
212  Mention = username
213  }
214  };
215 
216  EnqueueMessage(message);
217  }
218 
224  void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
225 
231  void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
232 
234  public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
235  {
236  disconnecting = false;
237  lock (this)
238  try
239  {
240  client.Connect(address, port);
241 
242  cancellationToken.ThrowIfCancellationRequested();
243 
244  if (passwordType == IrcPasswordType.Server)
245  client.Login(nickname, nickname, 0, nickname, password);
246  else
247  {
248  if (passwordType == IrcPasswordType.Sasl)
249  {
250  client.WriteLine("CAP REQ :sasl", Priority.Critical); //needs to be put in the buffer before anything else
251  cancellationToken.ThrowIfCancellationRequested();
252  }
253  client.Login(nickname, nickname, 0, nickname);
254  }
255 
256  if (passwordType == IrcPasswordType.NickServ)
257  {
258  cancellationToken.ThrowIfCancellationRequested();
259  client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
260  }
261  else if (passwordType == IrcPasswordType.Sasl)
262  {
263  //wait for the sasl ack or timeout
264  var recievedAck = false;
265  var recievedPlus = false;
266  client.OnReadLine += (sender, e) =>
267  {
268  if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal))
269  recievedAck = true;
270  else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal))
271  recievedPlus = true;
272  };
273 
274  var startTime = DateTimeOffset.Now;
275  var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
276  cancellationToken.ThrowIfCancellationRequested();
277 
278  var listenTimeSpan = TimeSpan.FromMilliseconds(10);
279  for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
280  client.Listen(false);
281 
282  client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
283  cancellationToken.ThrowIfCancellationRequested();
284 
285  for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
286  client.Listen(false);
287 
288  //Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
289  var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password);
290  var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
291  var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64);
292  var chars = authLine.ToCharArray();
293  client.WriteLine(authLine, Priority.Critical);
294 
295  cancellationToken.ThrowIfCancellationRequested();
296  client.WriteLine("CAP END", Priority.Critical);
297  }
298 
299  client.Listen(false);
300 
301  listenTask = Task.Factory.StartNew(() =>
302  {
303  while (!disconnecting && client.IsConnected && client.Nickname != nickname)
304  {
305  client.ListenOnce(true);
306  if (disconnecting || !client.IsConnected)
307  break;
308  client.Listen(false);
309  //ensure we have the correct nick
310  if (client.GetIrcUser(nickname) == null)
311  client.RfcNick(nickname);
312  }
313  client.Listen();
314  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
315  }
316  catch (OperationCanceledException)
317  {
318  throw;
319  }
320  catch (Exception e)
321  {
322  logger.LogWarning("Unable to connect to IRC: {0}", e);
323  return false;
324  }
325  return true;
326  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
327 
329  public override async Task Disconnect(CancellationToken cancellationToken)
330  {
331  if (!Connected)
332  return;
333  try
334  {
335  await Task.Factory.StartNew(() =>
336  {
337  try
338  {
339  client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise it wont go through
340  }
341  catch (Exception e)
342  {
343  logger.LogWarning("Error quitting IRC: {0}", e);
344  }
345  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
346  Dispose();
347  await listenTask.ConfigureAwait(false);
348  }
349  catch (OperationCanceledException)
350  {
351  throw;
352  }
353  catch (Exception e)
354  {
355  logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
356  }
357  }
358 
360  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
361  {
362  if (channels.Any(x => x.IrcChannel == null))
363  throw new InvalidOperationException("ChatChannel missing IrcChannel!");
364  lock (this)
365  {
366  var hs = new HashSet<string>(); //for unique inserts
367  foreach (var I in channels)
368  hs.Add(I.IrcChannel);
369  var toPart = new List<string>();
370  foreach (var I in client.JoinedChannels)
371  if (!hs.Remove(I))
372  toPart.Add(I);
373 
374  foreach (var I in toPart)
375  client.RfcPart(I, "Pretty nice abscond!");
376  foreach (var I in hs)
377  client.RfcJoin(I);
378 
379  return (IReadOnlyList<Channel>)channels.Select(x =>
380  {
381  ulong? id = null;
382  if (!channelIdMap.Any(y =>
383  {
384  if (y.Value != x.IrcChannel)
385  return false;
386  id = y.Key;
387  return true;
388  }))
389  {
390  id = channelIdCounter++;
391  channelIdMap.Add(id.Value, x.IrcChannel);
392  }
393  return new Channel
394  {
395  RealId = id.Value,
396  IsAdminChannel = x.IsAdminChannel == true,
397  ConnectionName = address,
398  FriendlyName = channelIdMap[id.Value],
399  IsPrivateChannel = false,
400  Tag = x.Tag
401  };
402  }).ToList();
403  }
404  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
405 
407  public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
408  {
409  var channelName = channelIdMap[channelId];
410  SendType sendType;
411  if (channelName == null)
412  {
413  channelName = queryChannelIdMap[channelId];
414  sendType = SendType.Notice;
415  }
416  else
417  sendType = SendType.Message;
418  try
419  {
420  client.SendMessage(sendType, channelName, message);
421  }
422  catch (Exception e)
423  {
424  logger.LogWarning("Unable to send to channel: {0}", e);
425  }
426  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
427  }
428 }
ulong channelIdCounter
Id counter for channelIdMap
Definition: IrcProvider.cs:77
-
readonly Dictionary< ulong, string > queryChannelIdMap
Map of Channel.RealIds to query users
Definition: IrcProvider.cs:72
+Go to the documentation of this file.
1 using Meebey.SmartIrc4net;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Components.Chat.Providers
14 {
18  sealed class IrcProvider : Provider
19  {
20  const int TimeoutSeconds = 5;
21 
23  public override bool Connected => client.IsConnected;
24 
26  public override string BotMention => client.Nickname;
27 
32 
36  readonly ILogger<IrcProvider> logger;
37 
41  readonly IrcFeatures client;
42 
46  readonly string address;
47 
51  readonly ushort port;
52 
56  readonly string nickname;
57 
61  readonly string password;
62 
67 
71  readonly Dictionary<ulong, string> channelIdMap;
72 
76  readonly Dictionary<ulong, string> queryChannelIdMap;
77 
82 
86  Task listenTask;
87 
92 
105  public IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger<IrcProvider> logger, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl)
106  {
107  if (application == null)
108  throw new ArgumentNullException(nameof(application));
109  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
110  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
111 
112  this.address = address ?? throw new ArgumentNullException(nameof(address));
113  this.port = port;
114  this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
115 
116  if (passwordType.HasValue && password == null)
117  throw new ArgumentNullException(nameof(password));
118 
119  if (password != null && !passwordType.HasValue)
120  throw new ArgumentNullException(nameof(passwordType));
121 
122  this.password = password;
123  this.passwordType = passwordType;
124 
125  client = new IrcFeatures
126  {
127  SupportNonRfc = true,
128  CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
129  AutoRejoin = true,
130  AutoRejoinOnKick = true,
131  AutoRelogin = true,
132  AutoRetry = true,
133  AutoRetryLimit = TimeoutSeconds,
134  AutoRetryDelay = TimeoutSeconds,
135  ActiveChannelSyncing = true,
136  AutoNickHandling = true,
137  CtcpVersion = application.VersionString,
138  UseSsl = useSsl
139  };
140  if (useSsl)
141  client.ValidateServerCertificate = true; // dunno if it defaults to that or what
142 
143  client.OnChannelMessage += Client_OnChannelMessage;
144  client.OnQueryMessage += Client_OnQueryMessage;
145 
146  channelIdMap = new Dictionary<ulong, string>();
147  queryChannelIdMap = new Dictionary<ulong, string>();
148  channelIdCounter = 1;
149  disconnecting = false;
150  }
151 
153  public override void Dispose()
154  {
155  if (Connected)
156  {
157  disconnecting = true;
158  client.Disconnect(); // just closes the socket
159  }
160  }
161 
167  void HandleMessage(IrcEventArgs e, bool isPrivate)
168  {
169  if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
170  return;
171 
172  var username = e.Data.Nick;
173  var channelName = isPrivate ? username : e.Data.Channel;
174 
175  ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
176  {
177  ulong? resultId = null;
178  if (!dicToCheck.Any(x =>
179  {
180  if (x.Value != channelName)
181  return false;
182  resultId = x.Key;
183  return true;
184  }))
185  {
186  resultId = channelIdCounter++;
187  dicToCheck.Add(resultId.Value, channelName);
188  if (dicToCheck == queryChannelIdMap)
189  channelIdMap.Add(resultId.Value, null);
190  }
191 
192  return resultId.Value;
193  }
194 
195  ulong userId, channelId;
196  lock (this)
197  {
198  userId = MapAndGetChannelId(queryChannelIdMap);
199  channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
200  }
201 
202  var message = new Message
203  {
204  Content = e.Data.Message,
205  User = new User
206  {
207  Channel = new Channel
208  {
209  ConnectionName = address,
210  FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
211  RealId = channelId,
212  IsPrivateChannel = isPrivate
213 
214  // isAdmin and Tag populated by manager
215  },
216  FriendlyName = username,
217  RealId = userId,
218  Mention = username
219  }
220  };
221 
222  EnqueueMessage(message);
223  }
224 
230  void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
231 
237  void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
238 
240  public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
241  {
242  disconnecting = false;
243  lock (this)
244  try
245  {
246  client.Connect(address, port);
247 
248  cancellationToken.ThrowIfCancellationRequested();
249 
250  if (passwordType == IrcPasswordType.Server)
251  client.Login(nickname, nickname, 0, nickname, password);
252  else
253  {
254  if (passwordType == IrcPasswordType.Sasl)
255  {
256  client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else
257  cancellationToken.ThrowIfCancellationRequested();
258  }
259 
260  client.Login(nickname, nickname, 0, nickname);
261  }
262 
263  if (passwordType == IrcPasswordType.NickServ)
264  {
265  cancellationToken.ThrowIfCancellationRequested();
266  client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
267  }
268  else if (passwordType == IrcPasswordType.Sasl)
269  {
270  // wait for the sasl ack or timeout
271  var recievedAck = false;
272  var recievedPlus = false;
273  client.OnReadLine += (sender, e) =>
274  {
275  if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal))
276  recievedAck = true;
277  else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal))
278  recievedPlus = true;
279  };
280 
281  var startTime = DateTimeOffset.Now;
282  var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
283  cancellationToken.ThrowIfCancellationRequested();
284 
285  var listenTimeSpan = TimeSpan.FromMilliseconds(10);
286  for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
287  client.Listen(false);
288 
289  client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
290  cancellationToken.ThrowIfCancellationRequested();
291 
292  for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
293  client.Listen(false);
294 
295  // Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
296  var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password);
297  var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
298  var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64);
299  var chars = authLine.ToCharArray();
300  client.WriteLine(authLine, Priority.Critical);
301 
302  cancellationToken.ThrowIfCancellationRequested();
303  client.WriteLine("CAP END", Priority.Critical);
304  }
305 
306  client.Listen(false);
307 
308  listenTask = Task.Factory.StartNew(() =>
309  {
310  while (!disconnecting && client.IsConnected && client.Nickname != nickname)
311  {
312  client.ListenOnce(true);
313  if (disconnecting || !client.IsConnected)
314  break;
315  client.Listen(false);
316 
317  // ensure we have the correct nick
318  if (client.GetIrcUser(nickname) == null)
319  client.RfcNick(nickname);
320  }
321 
322  client.Listen();
323  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
324  }
325  catch (OperationCanceledException)
326  {
327  throw;
328  }
329  catch (Exception e)
330  {
331  logger.LogWarning("Unable to connect to IRC: {0}", e);
332  return false;
333  }
334 
335  return true;
336  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
337 
339  public override async Task Disconnect(CancellationToken cancellationToken)
340  {
341  if (!Connected)
342  return;
343  try
344  {
345  await Task.Factory.StartNew(() =>
346  {
347  try
348  {
349  client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through
350  }
351  catch (Exception e)
352  {
353  logger.LogWarning("Error quitting IRC: {0}", e);
354  }
355  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
356  Dispose();
357  await listenTask.ConfigureAwait(false);
358  }
359  catch (OperationCanceledException)
360  {
361  throw;
362  }
363  catch (Exception e)
364  {
365  logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
366  }
367  }
368 
370  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
371  {
372  if (channels.Any(x => x.IrcChannel == null))
373  throw new InvalidOperationException("ChatChannel missing IrcChannel!");
374  lock (this)
375  {
376  var hs = new HashSet<string>(); // for unique inserts
377  foreach (var I in channels)
378  hs.Add(I.IrcChannel);
379  var toPart = new List<string>();
380  foreach (var I in client.JoinedChannels)
381  if (!hs.Remove(I))
382  toPart.Add(I);
383 
384  foreach (var I in toPart)
385  client.RfcPart(I, "Pretty nice abscond!");
386  foreach (var I in hs)
387  client.RfcJoin(I);
388 
389  return (IReadOnlyList<Channel>)channels.Select(x =>
390  {
391  ulong? id = null;
392  if (!channelIdMap.Any(y =>
393  {
394  if (y.Value != x.IrcChannel)
395  return false;
396  id = y.Key;
397  return true;
398  }))
399  {
400  id = channelIdCounter++;
401  channelIdMap.Add(id.Value, x.IrcChannel);
402  }
403 
404  return new Channel
405  {
406  RealId = id.Value,
407  IsAdminChannel = x.IsAdminChannel == true,
408  ConnectionName = address,
409  FriendlyName = channelIdMap[id.Value],
410  IsPrivateChannel = false,
411  Tag = x.Tag
412  };
413  }).ToList();
414  }
415  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
416 
418  public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
419  {
420  var channelName = channelIdMap[channelId];
421  SendType sendType;
422  if (channelName == null)
423  {
424  channelName = queryChannelIdMap[channelId];
425  sendType = SendType.Notice;
426  }
427  else
428  sendType = SendType.Message;
429  try
430  {
431  client.SendMessage(sendType, channelName, message);
432  }
433  catch (Exception e)
434  {
435  logger.LogWarning("Unable to send to channel: {0}", e);
436  }
437  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
438  }
439 }
ulong channelIdCounter
Id counter for channelIdMap
Definition: IrcProvider.cs:81
+
readonly Dictionary< ulong, string > queryChannelIdMap
Map of Channel.RealIds to query users
Definition: IrcProvider.cs:76
readonly IrcFeatures client
The IrcFeatures client
Definition: IrcProvider.cs:41
-
readonly ushort port
Port of the server to connect to
Definition: IrcProvider.cs:50
+
readonly ushort port
Port of the server to connect to
Definition: IrcProvider.cs:51
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
readonly ILogger< IrcProvider > logger
The ILogger for the IrcProvider
Definition: IrcProvider.cs:36
- +
readonly string address
Address of the server to connect to
Definition: IrcProvider.cs:46
-
override async Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
Definition: IrcProvider.cs:329
-
readonly Dictionary< ulong, string > channelIdMap
Map of Channel.RealIds to channel names
Definition: IrcProvider.cs:67
+
override async Task Disconnect(CancellationToken cancellationToken)
Gracefully disconnects the provider. Implies a call to IDisposable.Dispose
Definition: IrcProvider.cs:339
+
readonly Dictionary< ulong, string > channelIdMap
Map of Channel.RealIds to channel names
Definition: IrcProvider.cs:71
Represents a message recieved by a IProvider
Definition: Message.cs:6
-
void HandleMessage(IrcEventArgs e, bool isPrivate)
Handle an IRC message
Definition: IrcProvider.cs:163
- +
void HandleMessage(IrcEventArgs e, bool isPrivate)
Handle an IRC message
Definition: IrcProvider.cs:167
+
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the IrcProvider
Definition: IrcProvider.cs:31
-
Task listenTask
The Task used for IrcConnection.Listen(bool)
Definition: IrcProvider.cs:82
+
Task listenTask
The Task used for IrcConnection.Listen(bool)
Definition: IrcProvider.cs:86
Represents a Providers.IProvider channel
Definition: Channel.cs:10
-
readonly IrcPasswordType passwordType
The IrcPasswordType of password
Definition: IrcProvider.cs:62
+
readonly IrcPasswordType passwordType
The IrcPasswordType of password
Definition: IrcProvider.cs:66
Represents a tgs_chat_user datum
Definition: User.cs:10
IrcPasswordType
Represents the type of a password for a ChatProvider.Irc
string VersionString
A more verbose version of Version
Definition: IApplication.cs:18
bool IsAdminChannel
If this is considered a channel for admin commands
Definition: Channel.cs:41
-
readonly string password
Password which will used for authentication
Definition: IrcProvider.cs:58
-
IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger< IrcProvider > logger, string address, ushort port, string nickname, string password, IrcPasswordType?passwordType, bool useSsl)
Construct an IrcProvider
Definition: IrcProvider.cs:101
- +
readonly string password
Password which will used for authentication
Definition: IrcProvider.cs:61
+
IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger< IrcProvider > logger, string address, ushort port, string nickname, string password, IrcPasswordType?passwordType, bool useSsl)
Construct an IrcProvider
Definition: IrcProvider.cs:105
+
diff --git a/_job_controller_8cs_source.html b/_job_controller_8cs_source.html index 0448561b76..6694da5e29 100644 --- a/_job_controller_8cs_source.html +++ b/_job_controller_8cs_source.html @@ -92,7 +92,7 @@ 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.Linq;
6 using System.Net;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Controllers
15 {
19  [Route(Routes.Jobs)]
20  public sealed class JobController : ModelController<Api.Models.Job>
21  {
26 
34  public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger<JobController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
35  {
36  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
37  }
38 
40  [TgsAuthorize]
41  public override async Task<IActionResult> Read(CancellationToken cancellationToken)
42  {
43  var result = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue).OrderByDescending(x => x.StartedAt).ToListAsync(cancellationToken).ConfigureAwait(false);
44  return Json(result.Select(x => x.ToApi()));
45  }
46 
48  [TgsAuthorize]
49  public override async Task<IActionResult> List(CancellationToken cancellationToken)
50  {
51  //you KNOW this will need pagination eventually right?
52  var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.Job
53  {
54  Id = x.Id
55  }).ToListAsync(cancellationToken).ConfigureAwait(false);
56  return Json(jobs);
57  }
58 
60  [TgsAuthorize]
61  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
62  {
63  //don't care if an instance post or not at this point
64  var job = await DatabaseContext.Jobs.Where(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
65  if (job == default(Job))
66  return NotFound();
67 
68  if (job.StoppedAt != null)
69  return StatusCode((int)HttpStatusCode.Gone);
70 
71  if (job.CancelRight.HasValue && job.CancelRightsType.HasValue && (AuthenticationContext.GetRight(job.CancelRightsType.Value) & job.CancelRight.Value) == 0)
72  return Forbid();
73 
74  var cancelled = await jobManager.CancelJob(job, AuthenticationContext.User, false, cancellationToken).ConfigureAwait(false);
75  return cancelled ? (IActionResult)Accepted() : StatusCode((int)HttpStatusCode.Gone);
76  }
77 
79  [TgsAuthorize]
80  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
81  {
82  var job = await DatabaseContext.Jobs.Where(x => x.Id == id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
83  if (job == default(Job))
84  return NotFound();
85  var api = job.ToApi();
86  api.Progress = jobManager.JobProgress(job);
87  return Json(api);
88  }
89  }
90 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Net;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Controllers
15 {
19  [Route(Routes.Jobs)]
20  public sealed class JobController : ModelController<Api.Models.Job>
21  {
26 
34  public JobController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, ILogger<JobController> logger) : base(databaseContext, authenticationContextFactory, logger, true)
35  {
36  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
37  }
38 
40  [TgsAuthorize]
41  public override async Task<IActionResult> Read(CancellationToken cancellationToken)
42  {
43  var result = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id && !x.StoppedAt.HasValue).OrderByDescending(x => x.StartedAt).ToListAsync(cancellationToken).ConfigureAwait(false);
44  return Json(result.Select(x => x.ToApi()));
45  }
46 
48  [TgsAuthorize]
49  public override async Task<IActionResult> List(CancellationToken cancellationToken)
50  {
51  // you KNOW this will need pagination eventually right?
52  var jobs = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).Select(x => new Api.Models.Job
53  {
54  Id = x.Id
55  }).ToListAsync(cancellationToken).ConfigureAwait(false);
56  return Json(jobs);
57  }
58 
60  [TgsAuthorize]
61  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
62  {
63  // don't care if an instance post or not at this point
64  var job = await DatabaseContext.Jobs.Where(x => x.Id == id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
65  if (job == default(Job))
66  return NotFound();
67 
68  if (job.StoppedAt != null)
69  return StatusCode((int)HttpStatusCode.Gone);
70 
71  if (job.CancelRight.HasValue && job.CancelRightsType.HasValue && (AuthenticationContext.GetRight(job.CancelRightsType.Value) & job.CancelRight.Value) == 0)
72  return Forbid();
73 
74  var cancelled = await jobManager.CancelJob(job, AuthenticationContext.User, false, cancellationToken).ConfigureAwait(false);
75  return cancelled ? (IActionResult)Accepted() : StatusCode((int)HttpStatusCode.Gone);
76  }
77 
79  [TgsAuthorize]
80  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
81  {
82  var job = await DatabaseContext.Jobs.Where(x => x.Id == id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
83  if (job == default(Job))
84  return NotFound();
85  var api = job.ToApi();
86  api.Progress = jobManager.JobProgress(job);
87  return Json(api);
88  }
89  }
90 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
@@ -108,9 +108,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
override async Task< IActionResult > Delete(long id, CancellationToken cancellationToken)
Attempt to delete a model with a particular id
Routes to a server actions
Definition: Routes.cs:9
- +
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
- +
const string Jobs
The Models.Job controller
Definition: Routes.cs:79
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
An ApiController representing a TModel
diff --git a/_job_handler_8cs_source.html b/_job_handler_8cs_source.html index 58713c43b3..4447a5b4b1 100644 --- a/_job_handler_8cs_source.html +++ b/_job_handler_8cs_source.html @@ -92,12 +92,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JobHandler.cs
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 
5 namespace Tgstation.Server.Host.Core
6 {
10  sealed class JobHandler : IDisposable
11  {
15  readonly CancellationTokenSource cancellationTokenSource;
19  readonly Task task;
20 
25  public JobHandler(Func<CancellationToken, Task> job)
26  {
27  if (job == null)
28  throw new ArgumentNullException(nameof(job));
29  cancellationTokenSource = new CancellationTokenSource();
30  task = job(cancellationTokenSource.Token);
31  }
32 
34  public void Dispose() => cancellationTokenSource.Dispose();
35 
39  public int? Progress { get; set; }
40 
46  public async Task Wait(CancellationToken cancellationToken)
47  {
48  TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
49  using (cancellationToken.Register(() => tcs.SetCanceled()))
50  await Task.WhenAny(tcs.Task, task).ConfigureAwait(false);
51  cancellationToken.ThrowIfCancellationRequested();
52  }
53 
57  public void Cancel() => cancellationTokenSource.Cancel();
58  }
59 }
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 
5 namespace Tgstation.Server.Host.Core
6 {
10  sealed class JobHandler : IDisposable
11  {
15  readonly CancellationTokenSource cancellationTokenSource;
16 
20  readonly Task task;
21 
26  public JobHandler(Func<CancellationToken, Task> job)
27  {
28  if (job == null)
29  throw new ArgumentNullException(nameof(job));
30  cancellationTokenSource = new CancellationTokenSource();
31  task = job(cancellationTokenSource.Token);
32  }
33 
35  public void Dispose() => cancellationTokenSource.Dispose();
36 
40  public int? Progress { get; set; }
41 
47  public async Task Wait(CancellationToken cancellationToken)
48  {
49  TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
50  using (cancellationToken.Register(() => tcs.SetCanceled()))
51  await Task.WhenAny(tcs.Task, task).ConfigureAwait(false);
52  cancellationToken.ThrowIfCancellationRequested();
53  }
54 
58  public void Cancel() => cancellationTokenSource.Cancel();
59  }
60 }
-
async Task Wait(CancellationToken cancellationToken)
Wait for task to complete
Definition: JobHandler.cs:46
-
JobHandler(Func< CancellationToken, Task > job)
Construct a JobHandler
Definition: JobHandler.cs:25
+
async Task Wait(CancellationToken cancellationToken)
Wait for task to complete
Definition: JobHandler.cs:47
+
JobHandler(Func< CancellationToken, Task > job)
Construct a JobHandler
Definition: JobHandler.cs:26
readonly CancellationTokenSource cancellationTokenSource
The CancellationTokenSource for task
Definition: JobHandler.cs:15
-
readonly Task task
The Task being run
Definition: JobHandler.cs:19
+
readonly Task task
The Task being run
Definition: JobHandler.cs:20
Class for pairing Tasks with CancellationTokenSources
Definition: JobHandler.cs:10
diff --git a/_job_manager_8cs_source.html b/_job_manager_8cs_source.html index 77c862a904..5551f3b0e8 100644 --- a/_job_manager_8cs_source.html +++ b/_job_manager_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JobManager.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Core
11 {
14  {
19 
23  readonly ILogger<JobManager> logger;
24 
28  readonly Dictionary<long, JobHandler> jobs;
29 
35  public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger<JobManager> logger)
36  {
37  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
38  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
39  jobs = new Dictionary<long, JobHandler>();
40  }
41 
43  public void Dispose()
44  {
45  foreach (var job in jobs)
46  job.Value.Dispose();
47  }
48 
55  {
56  lock (this)
57  {
58  if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler))
59  throw new InvalidOperationException("Job not running!");
60  return jobHandler;
61  }
62  }
63 
71  async Task RunJob(Job job, Func<Job, IDatabaseContext, CancellationToken, Task> operation, CancellationToken cancellationToken)
72  {
73  try
74  {
75  await databaseContextFactory.UseContext(async databaseContext =>
76  {
77  async Task HandleExceptions(Task task)
78  {
79  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
80  try
81  {
82  await task.ConfigureAwait(false);
83  }
84  catch (OperationCanceledException)
85  {
86  logger.LogDebug("Job {0} cancelled!", job.Id);
87  job.Cancelled = true;
88  }
89  catch (JobException e)
90  {
91  job.ExceptionDetails = e.Message;
92  LogRegularException();
93  if (e.InnerException != null)
94  logger.LogDebug("Inner exception for job {0}: {1}", job.Id, e.InnerException);
95  }
96  catch (Exception e)
97  {
98  job.ExceptionDetails = e.ToString();
99  LogRegularException();
100  }
101  finally
102  {
103  job.StoppedAt = DateTimeOffset.Now;
104  }
105  }
106 
107  async Task RunJobInternal()
108  {
109  var oldJob = job;
110  job = new Job { Id = oldJob.Id };
111  databaseContext.Jobs.Attach(job);
112 
113  await operation(job, databaseContext, cancellationToken).ConfigureAwait(false);
114 
115  logger.LogDebug("Job {0} completed!", job.Id);
116  };
117 
118  await HandleExceptions(RunJobInternal()).ConfigureAwait(false);
119 
120  await databaseContext.Save(default).ConfigureAwait(false);
121 
122  bool JobErroredOrCancelled() => job.ExceptionDetails != null || job.Cancelled == true;
123 
124  //ok so, now it's time for the post commit step if it exists
125  if (!JobErroredOrCancelled() && job.PostComplete != null)
126  {
127  await HandleExceptions(job.PostComplete(cancellationToken)).ConfigureAwait(false);
128  if (JobErroredOrCancelled())
129  await databaseContext.Save(default).ConfigureAwait(false);
130  }
131  }).ConfigureAwait(false);
132  }
133  finally
134  {
135  lock (this)
136  {
137  var handler = jobs[job.Id];
138  jobs.Remove(job.Id);
139  handler.Dispose();
140  }
141  }
142  }
143 
145  public Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
146  {
147  if (job == null)
148  throw new ArgumentNullException(nameof(job));
149  if (operation == null)
150  throw new ArgumentNullException(nameof(operation));
151 
152  job.StartedAt = DateTimeOffset.Now;
153  job.Cancelled = false;
154 
155  job.Instance = new Instance
156  {
157  Id = job.Instance.Id
158  };
159  databaseContext.Instances.Attach(job.Instance);
160 
161  job.StartedBy = new User
162  {
163  Id = job.StartedBy.Id
164  };
165  databaseContext.Users.Attach(job.StartedBy);
166 
167  databaseContext.Jobs.Add(job);
168 
169  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
170  logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
171  var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
172  operation(jobParam, serviceProvider, y =>
173  {
174  lock (this)
175  if (jobs.TryGetValue(job.Id, out var handler))
176  handler.Progress = y;
177  }, ct),
178  x));
179  lock (this)
180  jobs.Add(job.Id, jobHandler);
181  });
182 
184  public async Task StartAsync(CancellationToken cancellationToken)
185  {
186  logger.LogTrace("Starting job manager...");
187  await databaseContextFactory.UseContext(async databaseContext =>
188  {
189  //mark all jobs as cancelled
190  var badJobs = await databaseContext.Jobs.Where(y => !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false);
191  if (badJobs.Count > 0)
192  {
193  logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
194  foreach (var I in badJobs)
195  {
196  var job = new Job { Id = I };
197  databaseContext.Jobs.Attach(job);
198  job.Cancelled = true;
199  job.StoppedAt = DateTimeOffset.Now;
200  }
201  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
202  }
203  }).ConfigureAwait(false);
204  logger.LogDebug("Job manager started!");
205  }
206 
208  public async Task StopAsync(CancellationToken cancellationToken)
209  {
210  var joinTasks = jobs.Select(x =>
211  {
212  x.Value.Cancel();
213  return x.Value.Wait(cancellationToken);
214  });
215  await Task.WhenAll(joinTasks).ConfigureAwait(false);
216  }
217 
219  public async Task<bool> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
220  {
221  if (job == null)
222  throw new ArgumentNullException(nameof(job));
223  if (user == null)
224  throw new ArgumentNullException(nameof(user));
225  JobHandler handler;
226  try
227  {
228  handler = CheckGetJob(job);
229  }
230  catch (InvalidOperationException)
231  {
232  //this is fine
233  return false;
234  }
235  handler.Cancel(); //this will ensure the db update is only done once
236  await databaseContextFactory.UseContext(async databaseContext =>
237  {
238  job = new Job { Id = job.Id };
239  databaseContext.Jobs.Attach(job);
240  user = new User { Id = user.Id };
241  databaseContext.Users.Attach(user);
242  job.CancelledBy = user;
243  //let either startup or cancellation set job.cancelled
244  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
245  }).ConfigureAwait(false);
246  if (blocking)
247  await handler.Wait(cancellationToken).ConfigureAwait(false);
248  return true;
249  }
250 
252  public int? JobProgress(Job job)
253  {
254  if (job == null)
255  throw new ArgumentNullException(nameof(job));
256  lock (this)
257  {
258  if (!jobs.TryGetValue(job.Id, out var handler))
259  return null;
260  return handler.Progress;
261  }
262  }
263 
265  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
266  {
267  if (job == null)
268  throw new ArgumentNullException(nameof(job));
269  if (canceller == null)
270  throw new ArgumentNullException(nameof(canceller));
271  JobHandler handler;
272  lock (this)
273  {
274  if (!jobs.TryGetValue(job.Id, out handler))
275  return;
276  }
277  Task cancelTask = null;
278  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
279  await handler.Wait(cancellationToken).ConfigureAwait(false);
280 
281  if (cancelTask != null)
282  await cancelTask.ConfigureAwait(false);
283  }
284  }
285 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Core
11 {
14  {
19 
23  readonly ILogger<JobManager> logger;
24 
28  readonly Dictionary<long, JobHandler> jobs;
29 
35  public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger<JobManager> logger)
36  {
37  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
38  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
39  jobs = new Dictionary<long, JobHandler>();
40  }
41 
43  public void Dispose()
44  {
45  foreach (var job in jobs)
46  job.Value.Dispose();
47  }
48 
55  {
56  lock (this)
57  {
58  if (!jobs.TryGetValue(job.Id, out JobHandler jobHandler))
59  throw new InvalidOperationException("Job not running!");
60  return jobHandler;
61  }
62  }
63 
71  async Task RunJob(Job job, Func<Job, IDatabaseContext, CancellationToken, Task> operation, CancellationToken cancellationToken)
72  {
73  try
74  {
75  await databaseContextFactory.UseContext(async databaseContext =>
76  {
77  async Task HandleExceptions(Task task)
78  {
79  void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
80  try
81  {
82  await task.ConfigureAwait(false);
83  }
84  catch (OperationCanceledException)
85  {
86  logger.LogDebug("Job {0} cancelled!", job.Id);
87  job.Cancelled = true;
88  }
89  catch (JobException e)
90  {
91  job.ExceptionDetails = e.Message;
92  LogRegularException();
93  if (e.InnerException != null)
94  logger.LogDebug("Inner exception for job {0}: {1}", job.Id, e.InnerException);
95  }
96  catch (Exception e)
97  {
98  job.ExceptionDetails = e.ToString();
99  LogRegularException();
100  }
101  finally
102  {
103  job.StoppedAt = DateTimeOffset.Now;
104  }
105  }
106 
107  async Task RunJobInternal()
108  {
109  var oldJob = job;
110  job = new Job { Id = oldJob.Id };
111  databaseContext.Jobs.Attach(job);
112 
113  await operation(job, databaseContext, cancellationToken).ConfigureAwait(false);
114 
115  logger.LogDebug("Job {0} completed!", job.Id);
116  }
117 
118  await HandleExceptions(RunJobInternal()).ConfigureAwait(false);
119 
120  await databaseContext.Save(default).ConfigureAwait(false);
121 
122  bool JobErroredOrCancelled() => job.ExceptionDetails != null || job.Cancelled == true;
123 
124  // ok so, now it's time for the post commit step if it exists
125  if (!JobErroredOrCancelled() && job.PostComplete != null)
126  {
127  await HandleExceptions(job.PostComplete(cancellationToken)).ConfigureAwait(false);
128  if (JobErroredOrCancelled())
129  await databaseContext.Save(default).ConfigureAwait(false);
130  }
131  }).ConfigureAwait(false);
132  }
133  finally
134  {
135  lock (this)
136  {
137  var handler = jobs[job.Id];
138  jobs.Remove(job.Id);
139  handler.Dispose();
140  }
141  }
142  }
143 
145  public Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
146  {
147  if (job == null)
148  throw new ArgumentNullException(nameof(job));
149  if (operation == null)
150  throw new ArgumentNullException(nameof(operation));
151 
152  job.StartedAt = DateTimeOffset.Now;
153  job.Cancelled = false;
154 
155  job.Instance = new Instance
156  {
157  Id = job.Instance.Id
158  };
159  databaseContext.Instances.Attach(job.Instance);
160 
161  job.StartedBy = new User
162  {
163  Id = job.StartedBy.Id
164  };
165  databaseContext.Users.Attach(job.StartedBy);
166 
167  databaseContext.Jobs.Add(job);
168 
169  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
170  logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
171  var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
172  operation(jobParam, serviceProvider, y =>
173  {
174  lock (this)
175  if (jobs.TryGetValue(job.Id, out var handler))
176  handler.Progress = y;
177  }, ct),
178  x));
179  lock (this)
180  jobs.Add(job.Id, jobHandler);
181  });
182 
184  public async Task StartAsync(CancellationToken cancellationToken)
185  {
186  logger.LogTrace("Starting job manager...");
187  await databaseContextFactory.UseContext(async databaseContext =>
188  {
189  // mark all jobs as cancelled
190  var badJobs = await databaseContext.Jobs.Where(y => !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false);
191  if (badJobs.Count > 0)
192  {
193  logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
194  foreach (var I in badJobs)
195  {
196  var job = new Job { Id = I };
197  databaseContext.Jobs.Attach(job);
198  job.Cancelled = true;
199  job.StoppedAt = DateTimeOffset.Now;
200  }
201 
202  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
203  }
204  }).ConfigureAwait(false);
205  logger.LogDebug("Job manager started!");
206  }
207 
209  public async Task StopAsync(CancellationToken cancellationToken)
210  {
211  var joinTasks = jobs.Select(x =>
212  {
213  x.Value.Cancel();
214  return x.Value.Wait(cancellationToken);
215  });
216  await Task.WhenAll(joinTasks).ConfigureAwait(false);
217  }
218 
220  public async Task<bool> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
221  {
222  if (job == null)
223  throw new ArgumentNullException(nameof(job));
224  if (user == null)
225  throw new ArgumentNullException(nameof(user));
226  JobHandler handler;
227  try
228  {
229  handler = CheckGetJob(job);
230  }
231  catch (InvalidOperationException)
232  {
233  // this is fine
234  return false;
235  }
236 
237  handler.Cancel(); // this will ensure the db update is only done once
238  await databaseContextFactory.UseContext(async databaseContext =>
239  {
240  job = new Job { Id = job.Id };
241  databaseContext.Jobs.Attach(job);
242  user = new User { Id = user.Id };
243  databaseContext.Users.Attach(user);
244  job.CancelledBy = user;
245 
246  // let either startup or cancellation set job.cancelled
247  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
248  }).ConfigureAwait(false);
249  if (blocking)
250  await handler.Wait(cancellationToken).ConfigureAwait(false);
251  return true;
252  }
253 
255  public int? JobProgress(Job job)
256  {
257  if (job == null)
258  throw new ArgumentNullException(nameof(job));
259  lock (this)
260  {
261  if (!jobs.TryGetValue(job.Id, out var handler))
262  return null;
263  return handler.Progress;
264  }
265  }
266 
268  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
269  {
270  if (job == null)
271  throw new ArgumentNullException(nameof(job));
272  if (canceller == null)
273  throw new ArgumentNullException(nameof(canceller));
274  JobHandler handler;
275  lock (this)
276  {
277  if (!jobs.TryGetValue(job.Id, out handler))
278  return;
279  }
280 
281  Task cancelTask = null;
282  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
283  await handler.Wait(cancellationToken).ConfigureAwait(false);
284 
285  if (cancelTask != null)
286  await cancelTask.ConfigureAwait(false);
287  }
288  }
289 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
JobHandler CheckGetJob(Job job)
Gets the JobHandler for a given job if it exists
Definition: JobManager.cs:54
@@ -104,9 +104,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
DateTimeOffset StoppedAt
When the Job stopped
Definition: Job.cs:37
-
async Task< bool > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job
Definition: JobManager.cs:219
-
async Task Wait(CancellationToken cancellationToken)
Wait for task to complete
Definition: JobHandler.cs:46
-
async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete
Definition: JobManager.cs:265
+
async Task< bool > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job
Definition: JobManager.cs:220
+
async Task Wait(CancellationToken cancellationToken)
Wait for task to complete
Definition: JobHandler.cs:47
+
async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete
Definition: JobManager.cs:268
DateTimeOffset StartedAt
When the Job was started
Definition: Job.cs:32
string Description
English description of the Job
Definition: Job.cs:21
string ExceptionDetails
Details of any exceptions caught during the Job
Definition: Job.cs:26
@@ -114,14 +114,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Instance Instance
The Models.Instance the job belongs to if any
Definition: Job.cs:27
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:208
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: JobManager.cs:209
JobManager(IDatabaseContextFactory databaseContextFactory, ILogger< JobManager > logger)
Construct a JobManager
Definition: JobManager.cs:35
Func< CancellationToken, Task > PostComplete
A Task to run after the job completes. This will not affect the Api.Models.Internal.Job.StoppedAt time, unless it is cancelled or errors
Definition: Job.cs:34
readonly ILogger< JobManager > logger
The ILogger for the JobManager
Definition: JobManager.cs:23
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
-
int JobProgress(Job job)
Get the Api.Models.Job.Progress for a job
Definition: JobManager.cs:252
+
int JobProgress(Job job)
Get the Api.Models.Job.Progress for a job
Definition: JobManager.cs:255
long Id
The ID of the User
Definition: User.cs:15
readonly IDatabaseContextFactory databaseContextFactory
The IServiceProvider for the JobManager
Definition: JobManager.cs:18
diff --git a/_jobs_client_8cs_source.html b/_jobs_client_8cs_source.html index c8b0dd7fd3..ebd3530515 100644 --- a/_jobs_client_8cs_source.html +++ b/_jobs_client_8cs_source.html @@ -92,9 +92,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JobsClient.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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
11  sealed class JobsClient : IJobsClient
12  {
20  readonly Instance instance;
21 
27  public JobsClient(IApiClient apiClient, Instance instance)
28  {
29  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
30  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
31  }
32 
34  public Task Cancel(Job job, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
35 
37  public Task<IReadOnlyList<Job>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Job>>(Routes.List(Routes.Jobs), instance.Id, cancellationToken);
38 
40  public Task<IReadOnlyList<Job>> ListActive(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Job>>(Routes.Jobs, instance.Id, cancellationToken);
41 
43  public Task<Job> GetId(Job job, CancellationToken cancellationToken) => apiClient.Read<Job>(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
44  }
45 }
+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;
7 
8 namespace Tgstation.Server.Client.Components
9 {
11  sealed class JobsClient : IJobsClient
12  {
17 
21  readonly Instance instance;
22 
28  public JobsClient(IApiClient apiClient, Instance instance)
29  {
30  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
31  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
32  }
33 
35  public Task Cancel(Job job, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
36 
38  public Task<IReadOnlyList<Job>> List(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Job>>(Routes.List(Routes.Jobs), instance.Id, cancellationToken);
39 
41  public Task<IReadOnlyList<Job>> ListActive(CancellationToken cancellationToken) => apiClient.Read<IReadOnlyList<Job>>(Routes.Jobs, instance.Id, cancellationToken);
42 
44  public Task<Job> GetId(Job job, CancellationToken cancellationToken) => apiClient.Read<Job>(Routes.SetID(Routes.Jobs, job?.Id ?? throw new ArgumentNullException(nameof(job))), instance.Id, cancellationToken);
45  }
46 }
-
JobsClient(IApiClient apiClient, Instance instance)
Construct a JobsClient
Definition: JobsClient.cs:27
+
JobsClient(IApiClient apiClient, Instance instance)
Construct a JobsClient
Definition: JobsClient.cs:28
Routes to a server actions
Definition: Routes.cs:9
@@ -108,7 +108,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
static string List(string route)
Get the /List postfix for a route
Web interface for the API
Definition: IApiClient.cs:11
-
readonly Instance instance
The Instance for the JobsClient
Definition: JobsClient.cs:20
+
readonly Instance instance
The Instance for the JobsClient
Definition: JobsClient.cs:21
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/_json_tracking_context_8cs_source.html b/_json_tracking_context_8cs_source.html index 41de001daf..8c592d38b1 100644 --- a/_json_tracking_context_8cs_source.html +++ b/_json_tracking_context_8cs_source.html @@ -92,16 +92,16 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
JsonTrackingContext.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using Newtonsoft.Json.Serialization;
4 using System;
5 using System.Collections.Generic;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Chat
14 {
17  {
19  public bool Active
20  {
21  get => active;
22  set
23  {
24  active = true;
25  logger.LogDebug("Tracking {0}activated", !active ? "de" : String.Empty);
26  }
27  }
28 
31  readonly ILogger<JsonTrackingContext> logger;
32  readonly Action onDispose;
33 
34  readonly string commandsPath;
35  readonly string channelsPath;
36 
37  readonly SemaphoreSlim channelsSemaphore;
38 
39  bool active;
40 
41  public JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, ILogger<JsonTrackingContext> logger, Action onDispose, string commandsPath, string channelsPath)
42  {
43  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
44  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
45  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
46  this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
47  this.commandsPath = commandsPath ?? throw new ArgumentNullException(nameof(commandsPath));
48  this.channelsPath = channelsPath ?? throw new ArgumentNullException(nameof(channelsPath));
49 
50  channelsSemaphore = new SemaphoreSlim(1);
51  active = false;
52 
53  logger.LogTrace("Created tracking context for {0} and {1}", commandsPath, channelsPath);
54  }
55 
57  public void Dispose()
58  {
59  logger.LogTrace("Disposing...");
60  onDispose();
61  }
62 
64  public async Task<IReadOnlyList<CustomCommand>> GetCustomCommands(CancellationToken cancellationToken)
65  {
66  try
67  {
68  if (Active && await ioManager.FileExists(commandsPath, cancellationToken).ConfigureAwait(false))
69  {
70  var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false);
71  var resultJson = Encoding.UTF8.GetString(resultBytes);
72  logger.LogTrace("Read commands JSON: {0}", resultJson);
73  var result = JsonConvert.DeserializeObject<List<CustomCommand>>(resultJson, new JsonSerializerSettings
74  {
75  ContractResolver = new DefaultContractResolver
76  {
77  NamingStrategy = new SnakeCaseNamingStrategy()
78  }
79  });
80  foreach (var I in result)
81  I.SetHandler(customCommandHandler);
82  return result;
83  }
84  }
85  catch (OperationCanceledException)
86  {
87  throw;
88  }
89  catch (Exception e)
90  {
91  logger.LogWarning("Error retrieving custom commands! Exception: {0}", e);
92  }
93  return new List<CustomCommand>();
94  }
95 
97  public async Task SetChannels(IEnumerable<Channel> channels, CancellationToken cancellationToken)
98  {
99  using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false))
100  {
101  var json = JsonConvert.SerializeObject(channels, new JsonSerializerSettings
102  {
103  ContractResolver = new CamelCasePropertyNamesContractResolver()
104  });
105  logger.LogTrace("Writing channels JSON: {0}", json);
106  await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false);
107  }
108  }
109  }
110 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using Newtonsoft.Json.Serialization;
4 using System;
5 using System.Collections.Generic;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host.Components.Chat
14 {
17  {
19  public bool Active
20  {
21  get => active;
22  set
23  {
24  active = true;
25  logger.LogDebug("Tracking {0}activated", !active ? "de" : String.Empty);
26  }
27  }
28 
31  readonly ILogger<JsonTrackingContext> logger;
32  readonly Action onDispose;
33 
34  readonly string commandsPath;
35  readonly string channelsPath;
36 
37  readonly SemaphoreSlim channelsSemaphore;
38 
39  bool active;
40 
50  public JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, ILogger<JsonTrackingContext> logger, Action onDispose, string commandsPath, string channelsPath)
51  {
52  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
53  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
54  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
55  this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
56  this.commandsPath = commandsPath ?? throw new ArgumentNullException(nameof(commandsPath));
57  this.channelsPath = channelsPath ?? throw new ArgumentNullException(nameof(channelsPath));
58 
59  channelsSemaphore = new SemaphoreSlim(1);
60  active = false;
61 
62  logger.LogTrace("Created tracking context for {0} and {1}", commandsPath, channelsPath);
63  }
64 
66  public void Dispose()
67  {
68  logger.LogTrace("Disposing...");
69  onDispose();
70  }
71 
73  public async Task<IReadOnlyList<CustomCommand>> GetCustomCommands(CancellationToken cancellationToken)
74  {
75  try
76  {
77  if (Active && await ioManager.FileExists(commandsPath, cancellationToken).ConfigureAwait(false))
78  {
79  var resultBytes = await ioManager.ReadAllBytes(commandsPath, cancellationToken).ConfigureAwait(false);
80  var resultJson = Encoding.UTF8.GetString(resultBytes);
81  logger.LogTrace("Read commands JSON: {0}", resultJson);
82  var result = JsonConvert.DeserializeObject<List<CustomCommand>>(resultJson, new JsonSerializerSettings
83  {
84  ContractResolver = new DefaultContractResolver
85  {
86  NamingStrategy = new SnakeCaseNamingStrategy()
87  }
88  });
89  foreach (var I in result)
90  I.SetHandler(customCommandHandler);
91  return result;
92  }
93  }
94  catch (OperationCanceledException)
95  {
96  throw;
97  }
98  catch (Exception e)
99  {
100  logger.LogWarning("Error retrieving custom commands! Exception: {0}", e);
101  }
102 
103  return new List<CustomCommand>();
104  }
105 
107  public async Task SetChannels(IEnumerable<Channel> channels, CancellationToken cancellationToken)
108  {
109  using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false))
110  {
111  var json = JsonConvert.SerializeObject(channels, new JsonSerializerSettings
112  {
113  ContractResolver = new CamelCasePropertyNamesContractResolver()
114  });
115  logger.LogTrace("Writing channels JSON: {0}", json);
116  await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false);
117  }
118  }
119  }
120 }
-
async Task SetChannels(IEnumerable< Channel > channels, CancellationToken cancellationToken)
Writes information about connected channels to the IJsonTrackingContext
+
async Task SetChannels(IEnumerable< Channel > channels, CancellationToken cancellationToken)
Writes information about connected channels to the IJsonTrackingContext
- + -
JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, ILogger< JsonTrackingContext > logger, Action onDispose, string commandsPath, string channelsPath)
+
JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, ILogger< JsonTrackingContext > logger, Action onDispose, string commandsPath, string channelsPath)
Construct a JsonTrackingContext
@@ -119,7 +119,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); -
async Task< IReadOnlyList< CustomCommand > > GetCustomCommands(CancellationToken cancellationToken)
Read CustomCommands from the IJsonTrackingContext
+
async Task< IReadOnlyList< CustomCommand > > GetCustomCommands(CancellationToken cancellationToken)
Read CustomCommands from the IJsonTrackingContext
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Chat.Providers
2 {
6  sealed class Message
7  {
11  public string Content { get; set; }
12 
16  public User User { get; set; }
17  }
18 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Chat.Providers
2 {
6  sealed class Message
7  {
11  public string Content { get; set; }
12 
16  public User User { get; set; }
17  }
18 }
Represents a message recieved by a IProvider
Definition: Message.cs:6
Represents a tgs_chat_user datum
Definition: User.cs:10
diff --git a/_monitor_action_8cs_source.html b/_monitor_action_8cs_source.html index dbc33c6c20..e0c3aaf88f 100644 --- a/_monitor_action_8cs_source.html +++ b/_monitor_action_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
MonitorAction.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Watchdog
2 {
7  {
11  Continue,
15  Skip,
19  Restart,
23  Break,
27  Exit
28  }
29 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Watchdog
2 {
7  {
11  Continue,
12 
16  Skip,
17 
21  Restart,
22 
26  Break,
27 
31  Exit
32  }
33 }
Skips the next call to HandleMonitorWakeup action
The monitor should stop checking actions for this iteration and continue its loop ...
diff --git a/_monitor_activation_reason_8cs_source.html b/_monitor_activation_reason_8cs_source.html index 582fd996bb..c65689af92 100644 --- a/_monitor_activation_reason_8cs_source.html +++ b/_monitor_activation_reason_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
MonitorActivationReason.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Watchdog
2 {
7  {
36  }
37 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Watchdog
2 {
7  {
12 
17 
22 
27 
32 
37 
42  }
43 }
diff --git a/_my_sql_database_context_8cs_source.html b/_my_sql_database_context_8cs_source.html index d83d752bdc..f13f83c8a2 100644 --- a/_my_sql_database_context_8cs_source.html +++ b/_my_sql_database_context_8cs_source.html @@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
For initially seeding a database
DatabaseType DatabaseType
The Configuration.DatabaseType to create
- +
string MySqlServerVersion
The string form of the System.Version of a target MySQL/MariaDB server
Configuration options for the Models.DatabaseContext<TParentContext>
diff --git a/_my_sql_design_time_db_context_factory_8cs_source.html b/_my_sql_design_time_db_context_factory_8cs_source.html index 11654e5315..c91a91f440 100644 --- a/_my_sql_design_time_db_context_factory_8cs_source.html +++ b/_my_sql_design_time_db_context_factory_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
MySqlDesignTimeDbContextFactory.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
6 
7 namespace Tgstation.Server.Host.Models.Migrations
8 {
10  sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MySqlDatabaseContext>
11  {
13  public MySqlDatabaseContext CreateDbContext(string[] args) => new MySqlDatabaseContext(new DbContextOptions<MySqlDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())), new LoggerFactory().CreateLogger<MySqlDatabaseContext>());
14  }
15 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
6 
7 namespace Tgstation.Server.Host.Models.Migrations
8 {
10  sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MySqlDatabaseContext>
11  {
13  public MySqlDatabaseContext CreateDbContext(string[] args) => new MySqlDatabaseContext(new DbContextOptions<MySqlDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())), new LoggerFactory().CreateLogger<MySqlDatabaseContext>());
14  }
15 }
static IOptions< DatabaseConfiguration > GetDbContextOptions()
Get the IOptions<TOptions> for the DatabaseConfiguration
Contains helpers for creating design time Models.DatabaseContext<TParentContext>s ...
@@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseContext<TParentContext> for MySQL
- +
-Go to the documentation of this file.
1 using System;
2 using System.Runtime.InteropServices;
3 using System.Text;
4 
5 namespace Tgstation.Server.Host
6 {
10  static class NativeMethods
11  {
15  [Flags]
16  public enum CreateSymbolicLinkFlags : int
17  {
18  None = 0,
19  Directory = 1,
20  AllowUnprivilegedCreate = 2
21  }
22 
26  [DllImport("user32.dll")]
27  public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
28 
32  [DllImport("user32.dll", CharSet = CharSet.Unicode)]
33  public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
34 
38  [DllImport("user32.dll")]
39  public static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);
40 
44  public delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam);
45 
49  [DllImport("user32.dll")]
50  public static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam);
51 
55  [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
56  public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
57 
61  [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
62  public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken);
63 
67  [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
68  public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, CreateSymbolicLinkFlags dwFlags);
69  }
70 }
+Go to the documentation of this file.
1 using System;
2 using System.Runtime.InteropServices;
3 using System.Text;
4 
5 namespace Tgstation.Server.Host
6 {
10  #pragma warning disable SA1600
11  #pragma warning disable SA1602
12  #pragma warning disable SA1611
13  #pragma warning disable SA1615
14  static class NativeMethods
15  {
19  [Flags]
20  public enum CreateSymbolicLinkFlags : int
21  {
22  None = 0,
23  Directory = 1,
24  AllowUnprivilegedCreate = 2
25  }
26 
30  [DllImport("user32.dll")]
31  public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
32 
36  [DllImport("user32.dll", CharSet = CharSet.Unicode)]
37  public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
38 
42  [DllImport("user32.dll")]
43  public static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);
44 
48  public delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam);
49 
53  [DllImport("user32.dll")]
54  public static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam);
55 
59  [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
60  public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
61 
65  [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
66  public static extern bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken);
67 
71  [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
72  public static extern bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, CreateSymbolicLinkFlags dwFlags);
73  }
74 }
-
CreateSymbolicLinkFlags
See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createsymboliclinka#param...
-
Native methods used by the code
+
CreateSymbolicLinkFlags
See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createsymboliclinka#param...
+
Native methods used by the code
diff --git a/_posix_byond_installer_8cs_source.html b/_posix_byond_installer_8cs_source.html index 1f853c918c..c25ccc78fb 100644 --- a/_posix_byond_installer_8cs_source.html +++ b/_posix_byond_installer_8cs_source.html @@ -92,20 +92,19 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
PosixByondInstaller.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
4 using System.Net;
5 using System.Text;
6 using System.Threading;
7 using System.Threading.Tasks;
9 using Tgstation.Server.Host.IO;
10 
11 namespace Tgstation.Server.Host.Components.Byond
12 {
17  {
21  const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
25  const string ByondCachePath = "~/.byond/cache";
26 
27  const string DreamDaemonExecutableName = "DreamDaemon";
28  const string DreamMakerExecutableName = "DreamMaker";
29  const string ShellScriptExtension = ".sh";
30 
32  public string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension;
33 
35  public string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
36 
41 
46 
50  readonly ILogger<PosixByondInstaller> logger;
51 
58  public PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger<PosixByondInstaller> logger)
59  {
60  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
61  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
62  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
63  }
64 
66  public async Task CleanCache(CancellationToken cancellationToken)
67  {
68  try
69  {
70  await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
71  }
72  catch (OperationCanceledException)
73  {
74  throw;
75  }
76  catch (Exception e)
77  {
78  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
79  }
80  }
81 
83  public async Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
84  {
85  if (version == null)
86  throw new ArgumentNullException(nameof(version));
87 
88  var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor);
89 
90  return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false);
91  }
92 
94  public Task InstallByond(string path, Version version, CancellationToken cancellationToken)
95  {
96  if (path == null)
97  throw new ArgumentNullException(nameof(path));
98  if (version == null)
99  throw new ArgumentNullException(nameof(version));
100 
101  //write the scripts for running the ting
102  //need to add $ORIGIN to LD_LIBRARY_PATH
103  const string StandardScript = "#!/bin/sh\nexport LD_LIBRARY_PATH=\"\\$ORIGIN:$LD_LIBRARY_PATH\"\nBASEDIR=$(dirname \"$0\")\nexec \"$BASEDIR/{0}\" \"$@\"\n";
104 
105  var dreamDaemonScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamDaemonExecutableName);
106  var dreamMakerScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamMakerExecutableName);
107 
108  async Task WriteAndMakeExecutable(string fullPath, string script)
109  {
110  await ioManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false);
111  postWriteHandler.HandleWrite(fullPath);
112  }
113 
114  var basePath = ioManager.ConcatPath(path, ByondManager.BinPath);
115 
116  var task = Task.WhenAll(WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript));
117 
118  postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamDaemonExecutableName));
119  postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamMakerExecutableName));
120 
121  return task;
122  }
123  }
124 }
PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger< PosixByondInstaller > logger)
Construct a WindowsByondInstaller
-
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
4 using System.Text;
5 using System.Threading;
6 using System.Threading.Tasks;
7 using Tgstation.Server.Host.IO;
8 
9 namespace Tgstation.Server.Host.Components.Byond
10 {
15  {
19  const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
20 
24  const string ByondCachePath = "~/.byond/cache";
25 
26  const string DreamDaemonExecutableName = "DreamDaemon";
27  const string DreamMakerExecutableName = "DreamMaker";
28  const string ShellScriptExtension = ".sh";
29 
31  public string DreamDaemonName => DreamDaemonExecutableName + ShellScriptExtension;
32 
34  public string DreamMakerName => DreamMakerExecutableName + ShellScriptExtension;
35 
40 
45 
49  readonly ILogger<PosixByondInstaller> logger;
50 
57  public PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger<PosixByondInstaller> logger)
58  {
59  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
60  this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
61  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
62  }
63 
65  public async Task CleanCache(CancellationToken cancellationToken)
66  {
67  try
68  {
69  await ioManager.DeleteDirectory(ByondCachePath, cancellationToken).ConfigureAwait(false);
70  }
71  catch (OperationCanceledException)
72  {
73  throw;
74  }
75  catch (Exception e)
76  {
77  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
78  }
79  }
80 
82  public async Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
83  {
84  if (version == null)
85  throw new ArgumentNullException(nameof(version));
86 
87  var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
88 
89  return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false);
90  }
91 
93  public Task InstallByond(string path, Version version, CancellationToken cancellationToken)
94  {
95  if (path == null)
96  throw new ArgumentNullException(nameof(path));
97  if (version == null)
98  throw new ArgumentNullException(nameof(version));
99 
100  // write the scripts for running the ting
101  // need to add $ORIGIN to LD_LIBRARY_PATH
102  const string StandardScript = "#!/bin/sh\nexport LD_LIBRARY_PATH=\"\\$ORIGIN:$LD_LIBRARY_PATH\"\nBASEDIR=$(dirname \"$0\")\nexec \"$BASEDIR/{0}\" \"$@\"\n";
103 
104  var dreamDaemonScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamDaemonExecutableName);
105  var dreamMakerScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamMakerExecutableName);
106 
107  async Task WriteAndMakeExecutable(string fullPath, string script)
108  {
109  await ioManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false);
110  postWriteHandler.HandleWrite(fullPath);
111  }
112 
113  var basePath = ioManager.ConcatPath(path, ByondManager.BinPath);
114 
115  var task = Task.WhenAll(WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript));
116 
117  postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamDaemonExecutableName));
118  postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamMakerExecutableName));
119 
120  return task;
121  }
122  }
123 }
PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger< PosixByondInstaller > logger)
Construct a WindowsByondInstaller
+
Handles changing file modes/permissions after writing
-
async Task< byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
Download a given BYOND version
+
async Task< byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
Download a given BYOND version
-
async Task CleanCache(CancellationToken cancellationToken)
Attempts to cleans the BYOND cache folder for the system
- +
async Task CleanCache(CancellationToken cancellationToken)
Attempts to cleans the BYOND cache folder for the system
For downloading and installing BYOND extractions for a given system
- -
readonly ILogger< PosixByondInstaller > logger
The ILogger for the PosixByondInstaller
-
readonly IIOManager ioManager
The IIOManager for the PosixByondInstaller
-
Task InstallByond(string path, Version version, CancellationToken cancellationToken)
Does actions necessary to get an extracted BYOND installation working
-
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for the PosixByondInstaller
+ +
readonly ILogger< PosixByondInstaller > logger
The ILogger for the PosixByondInstaller
+
readonly IIOManager ioManager
The IIOManager for the PosixByondInstaller
+
Task InstallByond(string path, Version version, CancellationToken cancellationToken)
Does actions necessary to get an extracted BYOND installation working
+
readonly IPostWriteHandler postWriteHandler
The IPostWriteHandler for the PosixByondInstaller
Interface for using filesystems
Definition: IIOManager.cs:11
diff --git a/_posix_network_prompt_reaper_8cs_source.html b/_posix_network_prompt_reaper_8cs_source.html index 598abf63a0..9cd3a6d3ec 100644 --- a/_posix_network_prompt_reaper_8cs_source.html +++ b/_posix_network_prompt_reaper_8cs_source.html @@ -92,9 +92,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
PosixNetworkPromptReaper.cs
-Go to the documentation of this file.
2 
3 namespace Tgstation.Server.Host.Components.Watchdog
4 {
5  //POSIX BYOND doesn't prompt you when you change the port
8  {
10  public void RegisterProcess(IProcess process) { }
11  }
12 }
-
void RegisterProcess(IProcess process)
Register a given process for network prompt reaping
- +Go to the documentation of this file.
2 
3 namespace Tgstation.Server.Host.Components.Watchdog
4 {
7  {
9  public void RegisterProcess(IProcess process)
10  {
11  // POSIX BYOND doesn't prompt you when you change the port
12  }
13  }
14 }
+
void RegisterProcess(IProcess process)
Register a given process for network prompt reaping
+
Abstraction over a System.Diagnostics.Process
Definition: IProcess.cs:9
diff --git a/_posix_post_write_handler_8cs.html b/_posix_post_write_handler_8cs.html index 8a498a286c..9aaaaadfe0 100644 --- a/_posix_post_write_handler_8cs.html +++ b/_posix_post_write_handler_8cs.html @@ -101,7 +101,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

Classes

class  Tgstation.Server.Host.IO.PosixPostWriteHandler
 IPostWriteHandler for POSIX systems More...
 IPostWriteHandler for POSIX systems More...
 
- +

diff --git a/_posix_post_write_handler_8cs_source.html b/_posix_post_write_handler_8cs_source.html index 8e47bc04ee..82549d6f27 100644 --- a/_posix_post_write_handler_8cs_source.html +++ b/_posix_post_write_handler_8cs_source.html @@ -92,9 +92,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
PosixPostWriteHandler.cs
-Go to the documentation of this file.
1 using Mono.Unix;
2 using Mono.Unix.Native;
3 using System;
4 
5 namespace Tgstation.Server.Host.IO
6 {
11  {
13  public void HandleWrite(string filePath)
14  {
15  if (filePath == null)
16  throw new ArgumentNullException(nameof(filePath));
17 
18  //set executable bit every time, don't want people calling me when their uploaded "sl" binary doesn't work
19  if (Syscall.stat(filePath, out var stat) != 0)
20  throw new UnixIOException(Stdlib.GetLastError());
21 
22  if (stat.st_mode.HasFlag(FilePermissions.S_IXUSR))
23  return;
24 
25  if (Syscall.chmod(filePath, stat.st_mode | FilePermissions.S_IXUSR) != 0)
26  throw new UnixIOException(Stdlib.GetLastError());
27  }
28  }
29 }
void HandleWrite(string filePath)
For handling system specific necessities after a write
+Go to the documentation of this file.
1 using Mono.Unix;
2 using Mono.Unix.Native;
3 using System;
4 
5 namespace Tgstation.Server.Host.IO
6 {
11  {
13  public void HandleWrite(string filePath)
14  {
15  if (filePath == null)
16  throw new ArgumentNullException(nameof(filePath));
17 
18  // set executable bit every time, don't want people calling me when their uploaded "sl" binary doesn't work
19  if (Syscall.stat(filePath, out var stat) != 0)
20  throw new UnixIOException(Stdlib.GetLastError());
21 
22  if (stat.st_mode.HasFlag(FilePermissions.S_IXUSR))
23  return;
24 
25  if (Syscall.chmod(filePath, stat.st_mode | FilePermissions.S_IXUSR) != 0)
26  throw new UnixIOException(Stdlib.GetLastError());
27  }
28  }
29 }
void HandleWrite(string filePath)
For handling system specific necessities after a write
IPostWriteHandler for POSIX systems
-
+
Handles changing file modes/permissions after writing
diff --git a/_process_8cs_source.html b/_process_8cs_source.html index 5338cd3277..dda59e64b5 100644 --- a/_process_8cs_source.html +++ b/_process_8cs_source.html @@ -92,21 +92,21 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Process.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Text;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
9  sealed class Process : IProcess
10  {
12  public int Id { get; }
13 
15  public Task Startup { get; }
16 
18  public Task<int> Lifetime { get; }
19 
20  readonly System.Diagnostics.Process handle;
21 
22  readonly StringBuilder outputStringBuilder;
23  readonly StringBuilder errorStringBuilder;
24  readonly StringBuilder combinedStringBuilder;
25 
29  readonly ILogger<Process> logger;
30 
31  public Process(System.Diagnostics.Process handle, Task<int> lifetime, StringBuilder outputStringBuilder, StringBuilder errorStringBuilder, StringBuilder combinedStringBuilder, ILogger<Process> logger, bool preExisting)
32  {
33  this.handle = handle ?? throw new ArgumentNullException(nameof(handle));
34  Lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
35 
36  this.outputStringBuilder = outputStringBuilder;
37  this.errorStringBuilder = errorStringBuilder;
38  this.combinedStringBuilder = combinedStringBuilder;
39 
40  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
41 
42  Id = handle.Id;
43 
44  if (preExisting)
45  {
46  Startup = Task.CompletedTask;
47  return;
48  }
49 
50  Startup = Task.Factory.StartNew(() =>
51  {
52  try
53  {
54  handle.WaitForInputIdle();
55  }
56  catch (InvalidOperationException) { }
57  }, default, TaskCreationOptions.LongRunning, TaskScheduler.Current);
58 
59  logger.LogTrace("Created process ID: {0}", Id);
60  }
61 
63  public void Dispose() => handle.Dispose();
64 
66  public string GetCombinedOutput()
67  {
68  if (combinedStringBuilder == null)
69  throw new InvalidOperationException("Output/Error reading was not enabled!");
70  return combinedStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
71  }
72 
74  public string GetErrorOutput()
75  {
76  if (errorStringBuilder == null)
77  throw new InvalidOperationException("Error reading was not enabled!");
78  return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
79  }
80 
82  public string GetStandardOutput()
83  {
84  if (outputStringBuilder == null)
85  throw new InvalidOperationException("Output reading was not enabled!");
86  return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
87  }
88 
90  public void Terminate()
91  {
92  if (handle.HasExited)
93  return;
94  try
95  {
96  logger.LogTrace("Terminating process...");
97  handle.Kill();
98  handle.WaitForExit();
99  }
100  catch (Exception e)
101  {
102  logger.LogDebug("Process termination exception: {0}", e);
103  }
104  }
105 
106  public void SetHighPriority()
107  {
108  try
109  {
110  handle.PriorityClass = System.Diagnostics.ProcessPriorityClass.AboveNormal;
111  logger.LogTrace("Set to above normal priority", handle.Id);
112  }
113  catch (Exception e)
114  {
115  logger.LogWarning("Unable to raise process priority! Exception: {0}", e);
116  }
117  }
118  }
119 }
readonly StringBuilder errorStringBuilder
Definition: Process.cs:23
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Text;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
9  sealed class Process : IProcess
10  {
12  public int Id { get; }
13 
15  public Task Startup { get; }
16 
18  public Task<int> Lifetime { get; }
19 
20  readonly System.Diagnostics.Process handle;
21 
22  readonly StringBuilder outputStringBuilder;
23  readonly StringBuilder errorStringBuilder;
24  readonly StringBuilder combinedStringBuilder;
25 
29  readonly ILogger<Process> logger;
30 
41  public Process(System.Diagnostics.Process handle, Task<int> lifetime, StringBuilder outputStringBuilder, StringBuilder errorStringBuilder, StringBuilder combinedStringBuilder, ILogger<Process> logger, bool preExisting)
42  {
43  this.handle = handle ?? throw new ArgumentNullException(nameof(handle));
44  Lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
45 
46  this.outputStringBuilder = outputStringBuilder;
47  this.errorStringBuilder = errorStringBuilder;
48  this.combinedStringBuilder = combinedStringBuilder;
49 
50  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
51 
52  Id = handle.Id;
53 
54  if (preExisting)
55  {
56  Startup = Task.CompletedTask;
57  return;
58  }
59 
60  Startup = Task.Factory.StartNew(() =>
61  {
62  try
63  {
64  handle.WaitForInputIdle();
65  }
66  catch (InvalidOperationException) { }
67  }, default, TaskCreationOptions.LongRunning, TaskScheduler.Current);
68 
69  logger.LogTrace("Created process ID: {0}", Id);
70  }
71 
73  public void Dispose() => handle.Dispose();
74 
76  public string GetCombinedOutput()
77  {
78  if (combinedStringBuilder == null)
79  throw new InvalidOperationException("Output/Error reading was not enabled!");
80  return combinedStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
81  }
82 
84  public string GetErrorOutput()
85  {
86  if (errorStringBuilder == null)
87  throw new InvalidOperationException("Error reading was not enabled!");
88  return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
89  }
90 
92  public string GetStandardOutput()
93  {
94  if (outputStringBuilder == null)
95  throw new InvalidOperationException("Output reading was not enabled!");
96  return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray());
97  }
98 
100  public void Terminate()
101  {
102  if (handle.HasExited)
103  return;
104  try
105  {
106  logger.LogTrace("Terminating process...");
107  handle.Kill();
108  handle.WaitForExit();
109  }
110  catch (Exception e)
111  {
112  logger.LogDebug("Process termination exception: {0}", e);
113  }
114  }
115 
117  public void SetHighPriority()
118  {
119  try
120  {
121  handle.PriorityClass = System.Diagnostics.ProcessPriorityClass.AboveNormal;
122  logger.LogTrace("Set to above normal priority", handle.Id);
123  }
124  catch (Exception e)
125  {
126  logger.LogWarning("Unable to raise process priority! Exception: {0}", e);
127  }
128  }
129  }
130 }
readonly StringBuilder errorStringBuilder
Definition: Process.cs:23
-
string GetErrorOutput()
Get the stderr output of the IProcess
Definition: Process.cs:74
+
string GetErrorOutput()
Get the stderr output of the IProcess
Definition: Process.cs:84
-
Process(System.Diagnostics.Process handle, Task< int > lifetime, StringBuilder outputStringBuilder, StringBuilder errorStringBuilder, StringBuilder combinedStringBuilder, ILogger< Process > logger, bool preExisting)
Definition: Process.cs:31
+
Process(System.Diagnostics.Process handle, Task< int > lifetime, StringBuilder outputStringBuilder, StringBuilder errorStringBuilder, StringBuilder combinedStringBuilder, ILogger< Process > logger, bool preExisting)
Construct a Process
Definition: Process.cs:41
readonly StringBuilder combinedStringBuilder
Definition: Process.cs:24
-
string GetStandardOutput()
Get the stdout output of the IProcess
Definition: Process.cs:82
-
string GetCombinedOutput()
Get the stderr and stdout output of the IProcess
Definition: Process.cs:66
+
string GetStandardOutput()
Get the stdout output of the IProcess
Definition: Process.cs:92
+
string GetCombinedOutput()
Get the stderr and stdout output of the IProcess
Definition: Process.cs:76
Abstraction over a System.Diagnostics.Process
Definition: IProcess.cs:9
-
void SetHighPriority()
Set&#39;s the owned System.Diagnostics.Process.PriorityClass to System.Diagnostics.ProcessPriorityClass.AboveNormal
Definition: Process.cs:106
+
void SetHighPriority()
Set&#39;s the owned System.Diagnostics.Process.PriorityClass to System.Diagnostics.ProcessPriorityClass.AboveNormal
Definition: Process.cs:117
readonly ILogger< Process > logger
The ILogger for the Process
Definition: Process.cs:29
readonly System.Diagnostics.Process handle
Definition: Process.cs:20
readonly StringBuilder outputStringBuilder
Definition: Process.cs:22
-
void Terminate()
Terminates the process
Definition: Process.cs:90
+
void Terminate()
Terminates the process
Definition: Process.cs:100
diff --git a/_process_executor_8cs_source.html b/_process_executor_8cs_source.html index 2403eea00a..92d0029ad8 100644 --- a/_process_executor_8cs_source.html +++ b/_process_executor_8cs_source.html @@ -92,17 +92,17 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ProcessExecutor.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Text;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
10  {
14  readonly ILogger<ProcessExecutor> logger;
15 
19  readonly ILoggerFactory loggerFactory;
20 
26  static Task<int> AttachExitHandler(System.Diagnostics.Process handle)
27  {
28  handle.EnableRaisingEvents = true;
29  var tcs = new TaskCompletionSource<int>();
30  handle.Exited += (a, b) =>
31  {
32  int exitCode;
33  try
34  {
35  exitCode = handle.ExitCode;
36  }
37  catch (InvalidOperationException)
38  {
39  return;
40  }
41  tcs.SetResult(exitCode);
42  };
43  return tcs.Task;
44  }
45 
51  public ProcessExecutor(ILogger<ProcessExecutor> logger, ILoggerFactory loggerFactory)
52  {
53  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
54  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
55  }
56 
58  public IProcess GetProcess(int id)
59  {
60  logger.LogDebug("Attaching to process {0}...", id);
61  System.Diagnostics.Process handle;
62  try
63  {
64  handle = System.Diagnostics.Process.GetProcessById(id);
65  }
66  catch(Exception e)
67  {
68  logger.LogDebug("Unable to get process {0}! Exception: {1}", id, e);
69  return null;
70  }
71  try
72  {
73  return new Process(handle, AttachExitHandler(handle), null, null, null, loggerFactory.CreateLogger<Process>(), true);
74  }
75  catch
76  {
77  handle.Dispose();
78  throw;
79  }
80  }
81 
83  public IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute)
84  {
85  logger.LogDebug("Launching process in {0}: {1} {2}", workingDirectory, fileName, arguments);
86  var handle = new System.Diagnostics.Process();
87  try
88  {
89  handle.StartInfo.FileName = fileName;
90  handle.StartInfo.Arguments = arguments;
91  handle.StartInfo.WorkingDirectory = workingDirectory;
92 
93  handle.StartInfo.UseShellExecute = !(noShellExecute || readOutput || readError);
94 
95  StringBuilder outputStringBuilder = null, errorStringBuilder = null, combinedStringBuilder = null;
96  if (readOutput || readError)
97  {
98  combinedStringBuilder = new StringBuilder();
99  if (readOutput)
100  {
101  outputStringBuilder = new StringBuilder();
102  handle.StartInfo.RedirectStandardOutput = true;
103  handle.OutputDataReceived += (sender, e) =>
104  {
105  combinedStringBuilder.Append(Environment.NewLine);
106  combinedStringBuilder.Append(e.Data);
107  outputStringBuilder.Append(Environment.NewLine);
108  outputStringBuilder.Append(e.Data);
109  };
110  }
111  if (readError)
112  {
113  errorStringBuilder = new StringBuilder();
114  handle.StartInfo.RedirectStandardError = true;
115  handle.ErrorDataReceived += (sender, e) =>
116  {
117  combinedStringBuilder.Append(Environment.NewLine);
118  combinedStringBuilder.Append(e.Data);
119  errorStringBuilder.Append(Environment.NewLine);
120  errorStringBuilder.Append(e.Data);
121  };
122  }
123  }
124 
125  var lifetimeTask = AttachExitHandler(handle);
126 
127  handle.Start();
128  try
129  {
130  if (readOutput)
131  handle.BeginOutputReadLine();
132  }
133  catch (InvalidOperationException) { }
134  try
135  {
136  if (readError)
137  handle.BeginErrorReadLine();
138  }
139  catch (InvalidOperationException) { }
140 
141  return new Process(handle, lifetimeTask, outputStringBuilder, errorStringBuilder, combinedStringBuilder, loggerFactory.CreateLogger<Process>(), false);
142  }
143  catch
144  {
145  handle.Dispose();
146  throw;
147  }
148  }
149  }
150 }
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProcessExecutor
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Text;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
10  {
14  readonly ILogger<ProcessExecutor> logger;
15 
19  readonly ILoggerFactory loggerFactory;
20 
26  static Task<int> AttachExitHandler(System.Diagnostics.Process handle)
27  {
28  handle.EnableRaisingEvents = true;
29  var tcs = new TaskCompletionSource<int>();
30  handle.Exited += (a, b) =>
31  {
32  int exitCode;
33  try
34  {
35  exitCode = handle.ExitCode;
36  }
37  catch (InvalidOperationException)
38  {
39  return;
40  }
41 
42  tcs.SetResult(exitCode);
43  };
44  return tcs.Task;
45  }
46 
52  public ProcessExecutor(ILogger<ProcessExecutor> logger, ILoggerFactory loggerFactory)
53  {
54  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
55  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
56  }
57 
59  public IProcess GetProcess(int id)
60  {
61  logger.LogDebug("Attaching to process {0}...", id);
62  System.Diagnostics.Process handle;
63  try
64  {
65  handle = System.Diagnostics.Process.GetProcessById(id);
66  }
67  catch(Exception e)
68  {
69  logger.LogDebug("Unable to get process {0}! Exception: {1}", id, e);
70  return null;
71  }
72 
73  try
74  {
75  return new Process(handle, AttachExitHandler(handle), null, null, null, loggerFactory.CreateLogger<Process>(), true);
76  }
77  catch
78  {
79  handle.Dispose();
80  throw;
81  }
82  }
83 
85  public IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute)
86  {
87  logger.LogDebug("Launching process in {0}: {1} {2}", workingDirectory, fileName, arguments);
88  var handle = new System.Diagnostics.Process();
89  try
90  {
91  handle.StartInfo.FileName = fileName;
92  handle.StartInfo.Arguments = arguments;
93  handle.StartInfo.WorkingDirectory = workingDirectory;
94 
95  handle.StartInfo.UseShellExecute = !(noShellExecute || readOutput || readError);
96 
97  StringBuilder outputStringBuilder = null, errorStringBuilder = null, combinedStringBuilder = null;
98  if (readOutput || readError)
99  {
100  combinedStringBuilder = new StringBuilder();
101  if (readOutput)
102  {
103  outputStringBuilder = new StringBuilder();
104  handle.StartInfo.RedirectStandardOutput = true;
105  handle.OutputDataReceived += (sender, e) =>
106  {
107  combinedStringBuilder.Append(Environment.NewLine);
108  combinedStringBuilder.Append(e.Data);
109  outputStringBuilder.Append(Environment.NewLine);
110  outputStringBuilder.Append(e.Data);
111  };
112  }
113 
114  if (readError)
115  {
116  errorStringBuilder = new StringBuilder();
117  handle.StartInfo.RedirectStandardError = true;
118  handle.ErrorDataReceived += (sender, e) =>
119  {
120  combinedStringBuilder.Append(Environment.NewLine);
121  combinedStringBuilder.Append(e.Data);
122  errorStringBuilder.Append(Environment.NewLine);
123  errorStringBuilder.Append(e.Data);
124  };
125  }
126  }
127 
128  var lifetimeTask = AttachExitHandler(handle);
129 
130  handle.Start();
131  try
132  {
133  if (readOutput)
134  handle.BeginOutputReadLine();
135  }
136  catch (InvalidOperationException) { }
137  try
138  {
139  if (readError)
140  handle.BeginErrorReadLine();
141  }
142  catch (InvalidOperationException) { }
143 
144  return new Process(handle, lifetimeTask, outputStringBuilder, errorStringBuilder, combinedStringBuilder, loggerFactory.CreateLogger<Process>(), false);
145  }
146  catch
147  {
148  handle.Dispose();
149  throw;
150  }
151  }
152  }
153 }
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the ProcessExecutor
-
IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute)
Launch a IProcess
+
IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute)
Launch a IProcess
-
IProcess GetProcess(int id)
Get a IProcess by id
+
IProcess GetProcess(int id)
Get a IProcess by id
static Task< int > AttachExitHandler(System.Diagnostics.Process handle)
Create a Task<TResult> resulting in the exit code of a given handle
Abstraction over a System.Diagnostics.Process
Definition: IProcess.cs:9
-
ProcessExecutor(ILogger< ProcessExecutor > logger, ILoggerFactory loggerFactory)
Construct a ProcessExecutor
+
ProcessExecutor(ILogger< ProcessExecutor > logger, ILoggerFactory loggerFactory)
Construct a ProcessExecutor
readonly ILogger< ProcessExecutor > logger
The ILogger for the ProcessExecutor
diff --git a/_pull_requests_command_8cs_source.html b/_pull_requests_command_8cs_source.html index 8b9817a2f5..352e2c6853 100644 --- a/_pull_requests_command_8cs_source.html +++ b/_pull_requests_command_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
PullRequestsCommand.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Components.Chat.Commands
13 {
18  {
20  public string Name => "prs";
21 
23  public string HelpText => "Display live test merge pull request numbers. Add --repo to view repository test merges";
24 
26  public bool AdminOnly => false;
27 
31  readonly IWatchdog watchdog;
32 
37 
42 
46  readonly Models.Instance instance;
47 
55  public PullRequestsCommand(IWatchdog watchdog, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
56  {
57  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
58  this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
59  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
60  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
61  }
62 
64  public async Task<string> Invoke(string arguments, User user, CancellationToken cancellationToken)
65  {
66  IEnumerable<Models.TestMerge> results = null;
67  if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--REPO"))
68  {
69  string head;
70  using (var repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
71  {
72  if (repo == null)
73  return "Repository unavailable!";
74  head = repo.Head;
75  }
76  await databaseContextFactory.UseContext(async db => results = await db.RevisionInformations.Where(x => x.Instance.Id == instance.Id && x.CommitSha == head)
77  .SelectMany(x => x.ActiveTestMerges)
78  .Select(x => x.TestMerge)
79  .Select(x => new Models.TestMerge
80  {
81  Number = x.Number,
82  PullRequestRevision = x.PullRequestRevision
83  }).ToListAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
84  }
85  else
86  {
87  if (!watchdog.Running)
88  return "Server offline!";
89  results = watchdog.ActiveCompileJob?.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList() ?? new List<Models.TestMerge>();
90  }
91 
92  if (!results.Any())
93  return "None!";
94 
95  return String.Join(", ", results.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7))));
96  }
97  }
98 }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the PullRequestsCommand
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Components.Chat.Commands
13 {
18  {
20  public string Name => "prs";
21 
23  public string HelpText => "Display live test merge pull request numbers. Add --repo to view repository test merges";
24 
26  public bool AdminOnly => false;
27 
31  readonly IWatchdog watchdog;
32 
37 
42 
46  readonly Models.Instance instance;
47 
55  public PullRequestsCommand(IWatchdog watchdog, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
56  {
57  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
58  this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
59  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
60  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
61  }
62 
64  // TODO: Decomplexify
65  #pragma warning disable CA1506
66  public async Task<string> Invoke(string arguments, User user, CancellationToken cancellationToken)
67  {
68  IEnumerable<Models.TestMerge> results = null;
69  if (arguments.Split(' ').Any(x => x.ToUpperInvariant() == "--REPO"))
70  {
71  string head;
72  using (var repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
73  {
74  if (repo == null)
75  return "Repository unavailable!";
76  head = repo.Head;
77  }
78 
79  await databaseContextFactory.UseContext(async db => results = await db.RevisionInformations.Where(x => x.Instance.Id == instance.Id && x.CommitSha == head)
80  .SelectMany(x => x.ActiveTestMerges)
81  .Select(x => x.TestMerge)
82  .Select(x => new Models.TestMerge
83  {
84  Number = x.Number,
85  PullRequestRevision = x.PullRequestRevision
86  }).ToListAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
87  }
88  else
89  {
90  if (!watchdog.Running)
91  return "Server offline!";
92  results = watchdog.ActiveCompileJob?.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList() ?? new List<Models.TestMerge>();
93  }
94 
95  return !results.Any() ? "None!" : String.Join(", ", results.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7))));
96  }
97  #pragma warning restore CA1506
98  }
99 }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the PullRequestsCommand
readonly IRepositoryManager repositoryManager
The IRepositoryManager for the PullRequestsCommand
@@ -110,7 +110,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
PullRequestsCommand(IWatchdog watchdog, IRepositoryManager repositoryManager, IDatabaseContextFactory databaseContextFactory, Models.Instance instance)
Construct a PullRequestsCommand
Represents a command that can be invoked by talking to chat bots
Definition: ICommand.cs:9
-
async Task< string > Invoke(string arguments, User user, CancellationToken cancellationToken)
Invoke the ICommand
+
async Task< string > Invoke(string arguments, User user, CancellationToken cancellationToken)
Invoke the ICommand
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:12
diff --git a/_reboot_state_8cs_source.html b/_reboot_state_8cs_source.html index 210b910d69..2a276e22e4 100644 --- a/_reboot_state_8cs_source.html +++ b/_reboot_state_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RebootState.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Watchdog
2 {
6  public enum RebootState : int
7  {
11  Normal = 0,
15  Shutdown = 1,
19  Restart = 2
20  }
21 }
Run DreamDaemon&#39;s normal reboot process
+Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Components.Watchdog
2 {
6  public enum RebootState : int
7  {
11  Normal = 0,
12 
16  Shutdown = 1,
17 
21  Restart = 2
22  }
23 }
Run DreamDaemon&#39;s normal reboot process
RebootState
Represents the action to take when /world/Reboot() is called
Definition: RebootState.cs:6
diff --git a/_release_notes_8_assembly_info_8cs.html b/_release_notes_8_assembly_info_8cs.html index 638a7353fc..85ccdc6409 100644 --- a/_release_notes_8_assembly_info_8cs.html +++ b/_release_notes_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: tools/ReleaseNotes/obj/Release/netcoreapp2.1/ReleaseNotes.AssemblyInfo.cs File Reference +tgstation-server: tools/ReleaseNotes/obj/Debug/netcoreapp2.1/ReleaseNotes.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_release_notes_8_assembly_info_8cs_source.html b/_release_notes_8_assembly_info_8cs_source.html index 2af530f94b..9a5ab05d0e 100644 --- a/_release_notes_8_assembly_info_8cs_source.html +++ b/_release_notes_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tools/ReleaseNotes/obj/Release/netcoreapp2.1/ReleaseNotes.AssemblyInfo.cs Source File +tgstation-server: tools/ReleaseNotes/obj/Debug/netcoreapp2.1/ReleaseNotes.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ReleaseNotes.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("ReleaseNotes")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("ReleaseNotes")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("ReleaseNotes")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("ReleaseNotes")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("ReleaseNotes")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("ReleaseNotes")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 using Tgstation.Server.Api;
6 
7 namespace Tgstation.Server.Client.Components
8 {
11  {
19  readonly Instance instance;
20 
26  public RepositoryClient(IApiClient apiClient, Instance instance)
27  {
28  this.apiClient = apiClient;
29  this.instance = instance;
30  }
31 
33  public Task<Repository> Clone(Repository repository, CancellationToken cancellationToken) => apiClient.Create<Repository, Repository>(Routes.Repository, repository, instance.Id, cancellationToken);
34 
36  public Task<Repository> Delete(CancellationToken cancellationToken) => apiClient.Delete<Repository>(Routes.Repository, instance.Id, cancellationToken);
37 
39  public Task<Repository> Read(CancellationToken cancellationToken) => apiClient.Read<Repository>(Routes.Repository, instance.Id, cancellationToken);
40 
42  public Task<Repository> Update(Repository repository, CancellationToken cancellationToken) => apiClient.Update<Repository, Repository>(Routes.Repository, repository ?? throw new ArgumentNullException(nameof(repository)), instance.Id, cancellationToken);
43  }
44 }
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 using Tgstation.Server.Api;
6 
7 namespace Tgstation.Server.Client.Components
8 {
11  {
16 
20  readonly Instance instance;
21 
27  public RepositoryClient(IApiClient apiClient, Instance instance)
28  {
29  this.apiClient = apiClient;
30  this.instance = instance;
31  }
32 
34  public Task<Repository> Clone(Repository repository, CancellationToken cancellationToken) => apiClient.Create<Repository, Repository>(Routes.Repository, repository, instance.Id, cancellationToken);
35 
37  public Task<Repository> Delete(CancellationToken cancellationToken) => apiClient.Delete<Repository>(Routes.Repository, instance.Id, cancellationToken);
38 
40  public Task<Repository> Read(CancellationToken cancellationToken) => apiClient.Read<Repository>(Routes.Repository, instance.Id, cancellationToken);
41 
43  public Task<Repository> Update(Repository repository, CancellationToken cancellationToken) => apiClient.Update<Repository, Repository>(Routes.Repository, repository ?? throw new ArgumentNullException(nameof(repository)), instance.Id, cancellationToken);
44  }
45 }
-
readonly Instance instance
The Instance for the RepositoryClient
+
readonly Instance instance
The Instance for the RepositoryClient
Routes to a server actions
Definition: Routes.cs:9
@@ -105,7 +105,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Metadata about a server instance
Definition: Instance.cs:9
readonly IApiClient apiClient
The IApiClient for the RepositoryClient
-
RepositoryClient(IApiClient apiClient, Instance instance)
Construct a RepositoryClient
+
RepositoryClient(IApiClient apiClient, Instance instance)
Construct a RepositoryClient
Web interface for the API
Definition: IApiClient.cs:11
diff --git a/_repository_controller_8cs_source.html b/_repository_controller_8cs_source.html index e1198fb312..1b96c1c71c 100644 --- a/_repository_controller_8cs_source.html +++ b/_repository_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RepositoryController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Linq.Expressions;
10 using System.Net;
11 using System.Reflection;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
22 
23 namespace Tgstation.Server.Host.Controllers
24 {
28  [Route(Routes.Repository)]
29  public sealed class RepositoryController : ModelController<Repository>
30  {
35 
40 
45 
50 
61  public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger<RepositoryController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true)
62  {
63  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
64  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
65  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
66  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
67  }
68 
69  async Task<bool> LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action<Models.RevisionInformation> revInfoSink, CancellationToken cancellationToken)
70  {
71  var repoSha = repository.Head;
72 
73  IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> query) => query
74  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
75  .Include(x => x.CompileJobs)
76  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy);
77 
78  var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
79 
80  if (revisionInfo == default)
81  revisionInfo = databaseContext.RevisionInformations.Local.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id).FirstOrDefault();
82 
83  var needsDbUpdate = revisionInfo == default;
84  if (needsDbUpdate)
85  {
86  //needs insertion
87  revisionInfo = new Models.RevisionInformation
88  {
89  Instance = instance,
90  CommitSha = repoSha,
91  CompileJobs = new List<Models.CompileJob>(),
92  ActiveTestMerges = new List<RevInfoTestMerge>() //non null vals for api returns
93  };
94 
95  lock (databaseContext) //cleaner this way
96  databaseContext.RevisionInformations.Add(revisionInfo);
97  }
98  revisionInfo.OriginCommitSha = revisionInfo.OriginCommitSha ?? lastOriginCommitSha;
99  if (revisionInfo.OriginCommitSha == null)
100  {
101  revisionInfo.OriginCommitSha = repoSha;
102  Logger.LogWarning(Components.Repository.Repository.OriginTrackingErrorTemplate, repoSha);
103  }
104  revInfoSink?.Invoke(revisionInfo);
105  return needsDbUpdate;
106  }
107 
108  async Task<bool> PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
109  {
110  if (repository.IsGitHubRepository)
111  {
112  model.GitHubOwner = repository.GitHubOwner;
113  model.GitHubName = repository.GitHubRepoName;
114  }
115  model.Origin = repository.Origin;
116  model.Reference = repository.Reference;
117 
118  //rev info stuff
119  Models.RevisionInformation revisionInfo = null;
120  var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, null, x => revisionInfo = x, cancellationToken).ConfigureAwait(false);
121  model.RevisionInformation = revisionInfo.ToApi();
122  return needsDbUpdate;
123  }
124 
126  [TgsAuthorize(RepositoryRights.SetOrigin)]
127  public override async Task<IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken)
128  {
129  if (model == null)
130  throw new ArgumentNullException(nameof(model));
131 
132  if (model.Origin == null)
133  return BadRequest(new ErrorMessage { Message = "Missing repo origin!" });
134 
135  if (model.AccessUser == null ^ model.AccessToken == null)
136  return BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" });
137 
138  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
139 
140  if (currentModel == default)
141  return StatusCode((int)HttpStatusCode.Gone);
142 
143  //normalize github urls
144  const string BadGitHubUrl = "://www.github.com/";
145  var uiOrigin = model.Origin.ToUpperInvariant();
146  var uiBad = BadGitHubUrl.ToUpperInvariant();
147  var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant();
148  if (uiOrigin.Contains(uiBad, StringComparison.Ordinal))
149  model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal);
150 
151  currentModel.AccessToken = model.AccessToken;
152  currentModel.AccessUser = model.AccessUser; //intentionally only these fields, user not allowed to change anything else atm
153  var cloneBranch = model.Reference;
154  var origin = model.Origin;
155 
156  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
157 
158  if (repoManager.CloneInProgress)
159  return Conflict(new ErrorMessage
160  {
161  Message = "A clone operation is in progress!"
162  });
163 
164  if(repoManager.InUse)
165  return Conflict(new ErrorMessage
166  {
167  Message = "The repo is busy!"
168  });
169 
170  using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false))
171  {
172  if (repo != null)
173  //clone conflict
174  return Conflict(new ErrorMessage
175  {
176  Message = "The repository already exists!"
177  });
178 
179  var job = new Models.Job
180  {
181  Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"),
182  StartedBy = AuthenticationContext.User,
183  CancelRightsType = RightsType.Repository,
184  CancelRight = (ulong)RepositoryRights.CancelClone,
186  };
187  var api = currentModel.ToApi();
188  await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) =>
189  {
190  using (var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false))
191  {
192  if (repos == null)
193  throw new JobException("Filesystem conflict while cloning repository!");
194  var instance = new Models.Instance
195  {
196  Id = Instance.Id
197  };
198  databaseContext.Instances.Attach(instance);
199  if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false))
200  await databaseContext.Save(ct).ConfigureAwait(false);
201  }
202  }, cancellationToken).ConfigureAwait(false);
203 
204  api.Origin = model.Origin;
205  api.Reference = model.Reference;
206  api.ActiveJob = job.ToApi();
207 
208  return StatusCode((int)HttpStatusCode.Created, api);
209  }
210  }
211 
217  [TgsAuthorize(RepositoryRights.Delete)]
218  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
219  {
220  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
221 
222  if (currentModel == default)
223  return StatusCode((int)HttpStatusCode.Gone);
224 
225  currentModel.AccessToken = null;
226  currentModel.AccessUser = null;
227 
228  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
229 
230  Logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id);
231 
232  var job = new Models.Job
233  {
234  Description = "Delete repository",
235  StartedBy = AuthenticationContext.User,
237  };
238  var api = currentModel.ToApi();
239  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
240  api.ActiveJob = job.ToApi();
241  return Accepted(api);
242  }
243 
245  [TgsAuthorize(RepositoryRights.Read)]
246  public override async Task<IActionResult> Read(CancellationToken cancellationToken)
247  {
248  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
249 
250  if (currentModel == default)
251  return StatusCode((int)HttpStatusCode.Gone);
252 
253  var api = currentModel.ToApi();
254  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
255 
256  if (repoManager.CloneInProgress)
257  return Conflict(new ErrorMessage
258  {
259  Message = "A clone operation is in progress!"
260  });
261 
262  if (repoManager.InUse)
263  return Conflict(new ErrorMessage
264  {
265  Message = "The repo is busy!"
266  });
267 
268  using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false))
269  {
270  if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false))
271  {
272  //user may have fucked with the repo without telling us, do what we can
273  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
274  return StatusCode((int)HttpStatusCode.Created, api);
275  }
276  return Json(api);
277  }
278  }
279 
281  [TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)]
282  public override async Task<IActionResult> Update([FromBody]Repository model, CancellationToken cancellationToken)
283  {
284  if (model == null)
285  throw new ArgumentNullException(nameof(model));
286 
287  if (model.AccessUser == null ^ model.AccessToken == null)
288  return BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" });
289 
290  if (model.CheckoutSha != null && model.Reference != null)
291  return BadRequest(new ErrorMessage { Message = "Only one of sha or reference may be specified!" });
292 
293  if (model.CheckoutSha != null && model.UpdateFromOrigin == true)
294  return BadRequest(new ErrorMessage { Message = "Cannot update a reference when checking out a sha!" });
295 
296  if (model.Origin != null)
297  return BadRequest(new ErrorMessage { Message = "origin cannot be modified without deleting the repository!" });
298 
299  if (model.NewTestMerges?.Any(x => !x.Number.HasValue) == true)
300  return BadRequest(new ErrorMessage { Message = "All new test merges must provide a number!" });
301 
302  if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true)
303  return BadRequest(new ErrorMessage { Message = "Cannot test merge the same PR twice in one job!" });
304 
305  if (model.CommitterName?.Length == 0)
306  return BadRequest(new ErrorMessage { Message = "Cannot set empty committer name!" });
307 
308  if (model.CommitterEmail?.Length == 0)
309  return BadRequest(new ErrorMessage { Message = "Cannot set empty committer e=mail!" });
310 
311  var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0;
312  var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository);
313  if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest))
314  return Forbid();
315 
316  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
317 
318  if (currentModel == default)
319  return StatusCode((int)HttpStatusCode.Gone);
320 
321  bool CheckModified<T>(Expression<Func<Api.Models.Internal.RepositorySettings, T>> expression, RepositoryRights requiredRight)
322  {
323  var memberSelectorExpression = (MemberExpression)expression.Body;
324  var property = (PropertyInfo)memberSelectorExpression.Member;
325 
326  var newVal = property.GetValue(model);
327  if (newVal == null)
328  return false;
329  if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal)
330  return true;
331 
332  property.SetValue(currentModel, newVal);
333  return false;
334  };
335 
336  if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials)
337  || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials)
338  || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings)
339  || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings)
340  || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter)
341  || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter)
342  || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits)
343  || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits)
344  || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch)))
345  return Forbid();
346 
347  if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0)
348  {
349  //setting an empty string clears everything
350  currentModel.AccessUser = null;
351  currentModel.AccessToken = null;
352  }
353 
354  var canRead = userRights.HasFlag(RepositoryRights.Read);
355 
356  var api = canRead ? currentModel.ToApi() : new Repository();
357  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
358 
359  if (canRead)
360  {
361  if (repoManager.CloneInProgress)
362  return Conflict(new ErrorMessage
363  {
364  Message = "A clone operation is in progress!"
365  });
366 
367  if (repoManager.InUse)
368  return Conflict(new ErrorMessage
369  {
370  Message = "The repo is busy!"
371  });
372 
373  using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false))
374  {
375  if (repo == null)
376  return Conflict(new ErrorMessage
377  {
378  Message = "Repository could not be loaded!"
379  });
380  await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false);
381  }
382  }
383 
384  //this is just db stuf so stow it away
385  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
386 
387  //format the job description
388  string description = null;
389  if (model.UpdateFromOrigin == true)
390  if (model.Reference != null)
391  description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference);
392  else if (model.CheckoutSha != null)
393  description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha);
394  else
395  description = "Pull current repository reference";
396  else if (model.Reference != null || model.CheckoutSha != null)
397  description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha);
398 
399  if (newTestMerges)
400  description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}",
401  description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T",
402  String.Join(", ", model.NewTestMerges.Select(x =>
403  String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number,
404  x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))),
405  description != null ? String.Empty : " in repository");
406 
407  if (description == null)
408  //no git changes
409  return Json(api);
410 
411  var job = new Models.Job
412  {
413  Description = description,
414  StartedBy = AuthenticationContext.User,
415  Instance = Instance,
416  CancelRightsType = RightsType.Repository,
417  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
418  };
419 
420  await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) =>
421  {
422  using (var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false))
423  {
424  if (repo == null)
425  throw new JobException("Repository could not be loaded!");
426 
427  var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null;
428 
429  var startReference = repo.Reference;
430  var startSha = repo.Head;
431  string postUpdateSha = null;
432 
433  if (newTestMerges && !repo.IsGitHubRepository)
434  throw new JobException("Cannot test merge on a non GitHub based repository!");
435 
436  var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name : currentModel.CommitterName;
437 
438  var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null;
439 
440  var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
441  var doneSteps = 0;
442 
443  Action<int> NextProgressReporter()
444  {
445  var tmpDoneSteps = doneSteps;
446  ++doneSteps;
447  return progress => progressReporter((progress + 100 * tmpDoneSteps) / numSteps);
448  };
449 
450  progressReporter(0);
451 
452  //get a base line for where we are
453  Models.RevisionInformation lastRevisionInfo = null;
454 
455  var attachedInstance = new Models.Instance
456  {
457  Id = Instance.Id
458  };
459  databaseContext.Instances.Attach(attachedInstance);
460 
461  await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
462 
463  //apply new rev info, tracking applied test merges
464  async Task UpdateRevInfo()
465  {
466  var last = lastRevisionInfo;
467  await LoadRevisionInformation(repo, databaseContext, attachedInstance, last.OriginCommitSha, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
468  lastRevisionInfo.ActiveTestMerges.AddRange(last.ActiveTestMerges);
469  };
470 
471  try
472  {
473  //fetch/pull
474  if (model.UpdateFromOrigin == true)
475  {
476  if (!repo.Tracking)
477  throw new JobException("Not on an updatable reference!");
478  await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false);
479  doneSteps = 1;
480  if (!modelHasShaOrReference)
481  {
482  var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false);
483  if (!fastForward.HasValue)
484  throw new JobException("Merge conflict occurred during origin update!");
485  await UpdateRevInfo().ConfigureAwait(false);
486  if (fastForward.Value)
487  {
488  lastRevisionInfo.OriginCommitSha = repo.Head;
489  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
490  postUpdateSha = repo.Head;
491  }
492  else
493  NextProgressReporter()(100);
494  }
495  }
496 
497  //checkout/hard reset
498  if (modelHasShaOrReference)
499  {
500  if ((model.CheckoutSha != null && repo.Head.ToUpperInvariant().StartsWith(model.CheckoutSha.ToUpperInvariant(), StringComparison.Ordinal))
501  || (model.Reference != null && repo.Reference.ToUpperInvariant() != model.Reference.ToUpperInvariant()))
502  {
503  var committish = model.CheckoutSha ?? model.Reference;
504  var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false);
505 
506  if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
507  throw new JobException("Attempted to checkout a SHA or reference that was actually the opposite!");
508 
509  await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false);
510  await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); //we've either seen origin before or what we're checking out is on origin
511  }
512  else
513  NextProgressReporter()(100);
514 
515  if (hardResettingToOriginReference)
516  {
517  if (!repo.Tracking)
518  throw new JobException("Checked out reference does not track a remote object!");
519  await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false);
520  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
521  await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
522  //repo head is on origin so force this
523  //will update the db if necessary
524  lastRevisionInfo.OriginCommitSha = repo.Head;
525  }
526  }
527 
528  Dictionary<int, Octokit.PullRequest> prMap = null;
529  //test merging
530  if (newTestMerges)
531  {
532  //bit of sanitization
533  foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision)))
534  I.PullRequestRevision = null;
535 
536  var gitHubClient = currentModel.AccessToken != null
537  ? gitHubClientFactory.CreateClient(currentModel.AccessToken)
538  : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken)
539  ? gitHubClientFactory.CreateClient()
540  : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken));
541 
542  var repoOwner = repo.GitHubOwner;
543  var repoName = repo.GitHubRepoName;
544 
545  Models.RevisionInformation revInfoWereLookingFor = null;
546  bool needToApplyRemainingPrs = true;
547  //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
548  if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
549  {
550  //In order for this to work though we need the shas of all the commits
551  if (model.NewTestMerges.Any(x => x.PullRequestRevision == null))
552  prMap = new Dictionary<int, Octokit.PullRequest>();
553 
554  bool cantSearch = false;
555  foreach (var I in model.NewTestMerges)
556  {
557  if (I.PullRequestRevision != null)
558  //normalize the shas to lowercase ala libgit2
559 #pragma warning disable CA1308 // Normalize strings to uppercase
560  I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant();
561 #pragma warning restore CA1308 // Normalize strings to uppercase
562  else
563  //retrieve the latest sha
564  try
565  {
566  var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false);
567  prMap.Add(I.Number.Value, pr);
568  I.PullRequestRevision = pr.Head.Sha;
569  }
570  catch
571  {
572  cantSearch = true;
573  break;
574  }
575  }
576 
577  if (!cantSearch)
578  {
579  var dbPull = await databaseContext.RevisionInformations
580  .Where(x => x.Instance.Id == Instance.Id
581  && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
582  && x.ActiveTestMerges.Count <= model.NewTestMerges.Count
583  && x.ActiveTestMerges.Count > 0)
584  .Include(x => x.ActiveTestMerges)
585  .ThenInclude(x => x.TestMerge)
586  .ToListAsync(cancellationToken).ConfigureAwait(false);
587 
588  //split here cause this bit has to be done locally
589  revInfoWereLookingFor = dbPull
590  .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count
591  && x.ActiveTestMerges.Select(y => y.TestMerge)
592  .All(y => model.NewTestMerges.Any(z =>
593  y.Number == z.Number
594  && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
595  && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
596  .FirstOrDefault();
597 
598  if (revInfoWereLookingFor == null && model.NewTestMerges.Count > 1)
599  {
600  //okay try to add at least SOME prs we've seen before
601  var search = model.NewTestMerges.ToList();
602 
603  var appliedTestMergeIds = new List<long>();
604 
605  Models.RevisionInformation lastGoodRevInfo = null;
606  do
607  {
608  foreach (var I in search)
609  {
610  revInfoWereLookingFor = dbPull
611  .Where(x => model.NewTestMerges.Any(z =>
612  x.PrimaryTestMerge.Number == z.Number
613  && x.PrimaryTestMerge.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
614  && (x.PrimaryTestMerge.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))
615  && x.ActiveTestMerges.Select(y => y.TestMerge).All(y => appliedTestMergeIds.Contains(y.Id)))
616  .FirstOrDefault();
617 
618  if (revInfoWereLookingFor != null)
619  {
620  lastGoodRevInfo = revInfoWereLookingFor;
621  appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id);
622  search.Remove(I);
623  break;
624  }
625  }
626  } while (revInfoWereLookingFor != null && search.Count > 0);
627 
628  revInfoWereLookingFor = lastGoodRevInfo;
629  needToApplyRemainingPrs = search.Count != 0;
630  if (needToApplyRemainingPrs)
631  model.NewTestMerges = search;
632  }
633  else if (revInfoWereLookingFor != null)
634  needToApplyRemainingPrs = false;
635  }
636  }
637 
638  if (revInfoWereLookingFor != null)
639  {
640  //goteem
641  await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false);
642  lastRevisionInfo = revInfoWereLookingFor;
643  }
644 
645  if (needToApplyRemainingPrs)
646  {
647  //an invocation of LoadRevisionInformation could have already loaded this user
648  var contextUser = databaseContext.Users.Local.Where(x => x.Id == AuthenticationContext.User.Id).FirstOrDefault();
649  if (contextUser == default)
650  {
651  contextUser = new Models.User
652  {
654  };
655  databaseContext.Users.Attach(contextUser);
656  }
657  else
658  Logger.LogTrace("Skipping attaching the user to the database context as it is already loaded!");
659 
660  foreach (var I in model.NewTestMerges)
661  {
662  Octokit.PullRequest pr = null;
663  string errorMessage = null;
664 
665  if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number.Value))
666  throw new JobException("Cannot test merge the same PR twice in one HEAD!");
667 
668  try
669  {
670  //load from cache if possible
671  if (prMap == null || !prMap.TryGetValue(I.Number.Value, out pr))
672  pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false);
673  }
674  catch (Octokit.RateLimitExceededException)
675  {
676  //you look at your anonymous access and sigh
677  errorMessage = "P.R.E. RATE LIMITED";
678  }
679  catch (Octokit.AuthorizationException)
680  {
681  errorMessage = "P.R.E. BAD CREDENTIALS";
682  }
683  catch (Octokit.NotFoundException)
684  {
685  //you look at your shithub and sigh
686  errorMessage = "P.R.E. NOT FOUND";
687  }
688 
689  //we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it
690  if (I.PullRequestRevision == null && pr != null)
691  I.PullRequestRevision = pr.Head.Sha;
692 
693  var mergeResult = await repo.AddTestMerge(I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false);
694 
695  if (!mergeResult.HasValue)
696  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Merge of PR #{0} at {1} conflicted!", I.Number, I.PullRequestRevision.Substring(0, 7)));
697 
698  ++doneSteps;
699 
700  var revInfoUpdateTask = UpdateRevInfo();
701 
702  var tm = new Models.TestMerge
703  {
704  Author = pr?.User.Login ?? errorMessage,
705  BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty,
706  MergedAt = DateTimeOffset.Now,
707  TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty,
708  Comment = I.Comment,
709  Number = I.Number,
710  MergedBy = contextUser,
711  PullRequestRevision = I.PullRequestRevision,
712  Url = pr?.HtmlUrl ?? errorMessage
713  };
714 
715  await revInfoUpdateTask.ConfigureAwait(false);
716 
717  lastRevisionInfo.PrimaryTestMerge = tm;
718  lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
719  {
720  TestMerge = tm
721  });
722  }
723  }
724  }
725 
726  var currentHead = repo.Head;
727  if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))
728  {
729  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false);
730  await UpdateRevInfo().ConfigureAwait(false);
731  }
732  await databaseContext.Save(ct).ConfigureAwait(false);
733  }
734  catch
735  {
736  doneSteps = 0;
737  numSteps = 2;
738  //the stuff didn't make it into the db, forget what we've done and abort
739  await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false);
740  if (startReference != null && repo.Head != startSha)
741  await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false);
742  else
743  progressReporter(100);
744  throw;
745  }
746  }
747  }, cancellationToken).ConfigureAwait(false);
748 
749  api.ActiveJob = job.ToApi();
750  return Accepted(api);
751  }
752  }
753 }
string Name
The name of the User
Definition: User.cs:38
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using 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;
22 
23 namespace Tgstation.Server.Host.Controllers
24 {
28  [Route(Routes.Repository)]
29  #pragma warning disable CA1506 // TODO: Decomplexify
30  public sealed class RepositoryController : ModelController<Repository>
31  {
36 
41 
46 
51 
62  public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger<RepositoryController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true)
63  {
64  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
65  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
66  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
67  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
68  }
69 
70  async Task<bool> LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action<Models.RevisionInformation> revInfoSink, CancellationToken cancellationToken)
71  {
72  var repoSha = repository.Head;
73 
74  IQueryable<Models.RevisionInformation> ApplyQuery(IQueryable<Models.RevisionInformation> query) => query
75  .Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
76  .Include(x => x.CompileJobs)
77  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy);
78 
79  var revisionInfo = await ApplyQuery(databaseContext.RevisionInformations).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
80 
81  if (revisionInfo == default)
82  revisionInfo = databaseContext.RevisionInformations.Local.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id).FirstOrDefault();
83 
84  var needsDbUpdate = revisionInfo == default;
85  if (needsDbUpdate)
86  {
87  // needs insertion
88  revisionInfo = new Models.RevisionInformation
89  {
90  Instance = instance,
91  CommitSha = repoSha,
92  CompileJobs = new List<Models.CompileJob>(),
93  ActiveTestMerges = new List<RevInfoTestMerge>() // non null vals for api returns
94  };
95 
96  lock (databaseContext) // cleaner this way
97  databaseContext.RevisionInformations.Add(revisionInfo);
98  }
99 
100  revisionInfo.OriginCommitSha = revisionInfo.OriginCommitSha ?? lastOriginCommitSha;
101  if (revisionInfo.OriginCommitSha == null)
102  {
103  revisionInfo.OriginCommitSha = repoSha;
104  Logger.LogWarning(Components.Repository.Repository.OriginTrackingErrorTemplate, repoSha);
105  }
106 
107  revInfoSink?.Invoke(revisionInfo);
108  return needsDbUpdate;
109  }
110 
111  async Task<bool> PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
112  {
113  if (repository.IsGitHubRepository)
114  {
115  model.GitHubOwner = repository.GitHubOwner;
116  model.GitHubName = repository.GitHubRepoName;
117  }
118 
119  model.Origin = repository.Origin;
120  model.Reference = repository.Reference;
121 
122  // rev info stuff
123  Models.RevisionInformation revisionInfo = null;
124  var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, null, x => revisionInfo = x, cancellationToken).ConfigureAwait(false);
125  model.RevisionInformation = revisionInfo.ToApi();
126  return needsDbUpdate;
127  }
128 
130  [TgsAuthorize(RepositoryRights.SetOrigin)]
131  public override async Task<IActionResult> Create([FromBody] Repository model, CancellationToken cancellationToken)
132  {
133  if (model == null)
134  throw new ArgumentNullException(nameof(model));
135 
136  if (model.Origin == null)
137  return BadRequest(new ErrorMessage { Message = "Missing repo origin!" });
138 
139  if (model.AccessUser == null ^ model.AccessToken == null)
140  return BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" });
141 
142  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
143 
144  if (currentModel == default)
145  return StatusCode((int)HttpStatusCode.Gone);
146 
147  // normalize github urls
148  const string BadGitHubUrl = "://www.github.com/";
149  var uiOrigin = model.Origin.ToUpperInvariant();
150  var uiBad = BadGitHubUrl.ToUpperInvariant();
151  var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant();
152  if (uiOrigin.Contains(uiBad, StringComparison.Ordinal))
153  model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal);
154 
155  currentModel.AccessToken = model.AccessToken;
156  currentModel.AccessUser = model.AccessUser; // intentionally only these fields, user not allowed to change anything else atm
157  var cloneBranch = model.Reference;
158  var origin = model.Origin;
159 
160  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
161 
162  if (repoManager.CloneInProgress)
163  return Conflict(new ErrorMessage
164  {
165  Message = "A clone operation is in progress!"
166  });
167 
168  if(repoManager.InUse)
169  return Conflict(new ErrorMessage
170  {
171  Message = "The repo is busy!"
172  });
173 
174  using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false))
175  {
176  // clone conflict
177  if (repo != null)
178  return Conflict(new ErrorMessage
179  {
180  Message = "The repository already exists!"
181  });
182 
183  var job = new Models.Job
184  {
185  Description = String.Format(CultureInfo.InvariantCulture, "Clone branch {1} of repository {0}", origin, cloneBranch ?? "master"),
186  StartedBy = AuthenticationContext.User,
187  CancelRightsType = RightsType.Repository,
188  CancelRight = (ulong)RepositoryRights.CancelClone,
190  };
191  var api = currentModel.ToApi();
192  await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) =>
193  {
194  using (var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false))
195  {
196  if (repos == null)
197  throw new JobException("Filesystem conflict while cloning repository!");
198  var instance = new Models.Instance
199  {
200  Id = Instance.Id
201  };
202  databaseContext.Instances.Attach(instance);
203  if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false))
204  await databaseContext.Save(ct).ConfigureAwait(false);
205  }
206  }, cancellationToken).ConfigureAwait(false);
207 
208  api.Origin = model.Origin;
209  api.Reference = model.Reference;
210  api.ActiveJob = job.ToApi();
211 
212  return StatusCode((int)HttpStatusCode.Created, api);
213  }
214  }
215 
221  [TgsAuthorize(RepositoryRights.Delete)]
222  public async Task<IActionResult> Delete(CancellationToken cancellationToken)
223  {
224  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
225 
226  if (currentModel == default)
227  return StatusCode((int)HttpStatusCode.Gone);
228 
229  currentModel.AccessToken = null;
230  currentModel.AccessUser = null;
231 
232  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
233 
234  Logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id);
235 
236  var job = new Models.Job
237  {
238  Description = "Delete repository",
239  StartedBy = AuthenticationContext.User,
241  };
242  var api = currentModel.ToApi();
243  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
244  api.ActiveJob = job.ToApi();
245  return Accepted(api);
246  }
247 
249  [TgsAuthorize(RepositoryRights.Read)]
250  public override async Task<IActionResult> Read(CancellationToken cancellationToken)
251  {
252  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
253 
254  if (currentModel == default)
255  return StatusCode((int)HttpStatusCode.Gone);
256 
257  var api = currentModel.ToApi();
258  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
259 
260  if (repoManager.CloneInProgress)
261  return Conflict(new ErrorMessage
262  {
263  Message = "A clone operation is in progress!"
264  });
265 
266  if (repoManager.InUse)
267  return Conflict(new ErrorMessage
268  {
269  Message = "The repo is busy!"
270  });
271 
272  using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false))
273  {
274  if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false))
275  {
276  // user may have fucked with the repo without telling us, do what we can
277  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
278  return StatusCode((int)HttpStatusCode.Created, api);
279  }
280 
281  return Json(api);
282  }
283  }
284 
286  [TgsAuthorize(RepositoryRights.ChangeAutoUpdateSettings | RepositoryRights.ChangeCommitter | RepositoryRights.ChangeCredentials | RepositoryRights.ChangeTestMergeCommits | RepositoryRights.MergePullRequest | RepositoryRights.SetReference | RepositoryRights.SetSha | RepositoryRights.UpdateBranch)]
287  #pragma warning disable CA1502 // TODO: Decomplexify
288  #pragma warning disable CA1505
289  public override async Task<IActionResult> Update([FromBody]Repository model, CancellationToken cancellationToken)
290  {
291  if (model == null)
292  throw new ArgumentNullException(nameof(model));
293 
294  if (model.AccessUser == null ^ model.AccessToken == null)
295  return BadRequest(new ErrorMessage { Message = "Either both accessToken and accessUser must be present or neither!" });
296 
297  if (model.CheckoutSha != null && model.Reference != null)
298  return BadRequest(new ErrorMessage { Message = "Only one of sha or reference may be specified!" });
299 
300  if (model.CheckoutSha != null && model.UpdateFromOrigin == true)
301  return BadRequest(new ErrorMessage { Message = "Cannot update a reference when checking out a sha!" });
302 
303  if (model.Origin != null)
304  return BadRequest(new ErrorMessage { Message = "origin cannot be modified without deleting the repository!" });
305 
306  if (model.NewTestMerges?.Any(x => !x.Number.HasValue) == true)
307  return BadRequest(new ErrorMessage { Message = "All new test merges must provide a number!" });
308 
309  if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true)
310  return BadRequest(new ErrorMessage { Message = "Cannot test merge the same PR twice in one job!" });
311 
312  if (model.CommitterName?.Length == 0)
313  return BadRequest(new ErrorMessage { Message = "Cannot set empty committer name!" });
314 
315  if (model.CommitterEmail?.Length == 0)
316  return BadRequest(new ErrorMessage { Message = "Cannot set empty committer e=mail!" });
317 
318  var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0;
319  var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository);
320  if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest))
321  return Forbid();
322 
323  var currentModel = await DatabaseContext.RepositorySettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
324 
325  if (currentModel == default)
326  return StatusCode((int)HttpStatusCode.Gone);
327 
328  bool CheckModified<T>(Expression<Func<Api.Models.Internal.RepositorySettings, T>> expression, RepositoryRights requiredRight)
329  {
330  var memberSelectorExpression = (MemberExpression)expression.Body;
331  var property = (PropertyInfo)memberSelectorExpression.Member;
332 
333  var newVal = property.GetValue(model);
334  if (newVal == null)
335  return false;
336  if (!userRights.HasFlag(requiredRight) && property.GetValue(currentModel) != newVal)
337  return true;
338 
339  property.SetValue(currentModel, newVal);
340  return false;
341  }
342 
343  if (CheckModified(x => x.AccessToken, RepositoryRights.ChangeCredentials)
344  || CheckModified(x => x.AccessUser, RepositoryRights.ChangeCredentials)
345  || CheckModified(x => x.AutoUpdatesKeepTestMerges, RepositoryRights.ChangeAutoUpdateSettings)
346  || CheckModified(x => x.AutoUpdatesSynchronize, RepositoryRights.ChangeAutoUpdateSettings)
347  || CheckModified(x => x.CommitterEmail, RepositoryRights.ChangeCommitter)
348  || CheckModified(x => x.CommitterName, RepositoryRights.ChangeCommitter)
349  || CheckModified(x => x.PushTestMergeCommits, RepositoryRights.ChangeTestMergeCommits)
350  || CheckModified(x => x.ShowTestMergeCommitters, RepositoryRights.ChangeTestMergeCommits)
351  || (model.UpdateFromOrigin == true && !userRights.HasFlag(RepositoryRights.UpdateBranch)))
352  return Forbid();
353 
354  if (currentModel.AccessToken?.Length == 0 && currentModel.AccessUser?.Length == 0)
355  {
356  // setting an empty string clears everything
357  currentModel.AccessUser = null;
358  currentModel.AccessToken = null;
359  }
360 
361  var canRead = userRights.HasFlag(RepositoryRights.Read);
362 
363  var api = canRead ? currentModel.ToApi() : new Repository();
364  var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
365 
366  if (canRead)
367  {
368  if (repoManager.CloneInProgress)
369  return Conflict(new ErrorMessage
370  {
371  Message = "A clone operation is in progress!"
372  });
373 
374  if (repoManager.InUse)
375  return Conflict(new ErrorMessage
376  {
377  Message = "The repo is busy!"
378  });
379 
380  using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false))
381  {
382  if (repo == null)
383  return Conflict(new ErrorMessage
384  {
385  Message = "Repository could not be loaded!"
386  });
387  await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false);
388  }
389  }
390 
391  // this is just db stuf so stow it away
392  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
393 
394  // format the job description
395  string description = null;
396  if (model.UpdateFromOrigin == true)
397  if (model.Reference != null)
398  description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference);
399  else if (model.CheckoutSha != null)
400  description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha);
401  else
402  description = "Pull current repository reference";
403  else if (model.Reference != null || model.CheckoutSha != null)
404  description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha);
405 
406  if (newTestMerges)
407  description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}",
408  description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T",
409  String.Join(", ", model.NewTestMerges.Select(x =>
410  String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number,
411  x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))),
412  description != null ? String.Empty : " in repository");
413 
414  if (description == null)
415  return Json(api); // no git changes
416 
417  var job = new Models.Job
418  {
419  Description = description,
420  StartedBy = AuthenticationContext.User,
421  Instance = Instance,
422  CancelRightsType = RightsType.Repository,
423  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
424  };
425 
426  await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) =>
427  {
428  using (var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false))
429  {
430  if (repo == null)
431  throw new JobException("Repository could not be loaded!");
432 
433  var modelHasShaOrReference = model.CheckoutSha != null || model.Reference != null;
434 
435  var startReference = repo.Reference;
436  var startSha = repo.Head;
437  string postUpdateSha = null;
438 
439  if (newTestMerges && !repo.IsGitHubRepository)
440  throw new JobException("Cannot test merge on a non GitHub based repository!");
441 
442  var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name : currentModel.CommitterName;
443 
444  var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null;
445 
446  var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1));
447  var doneSteps = 0;
448 
449  Action<int> NextProgressReporter()
450  {
451  var tmpDoneSteps = doneSteps;
452  ++doneSteps;
453  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / numSteps);
454  }
455 
456  progressReporter(0);
457 
458  // get a base line for where we are
459  Models.RevisionInformation lastRevisionInfo = null;
460 
461  var attachedInstance = new Models.Instance
462  {
463  Id = Instance.Id
464  };
465  databaseContext.Instances.Attach(attachedInstance);
466 
467  await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
468 
469  // apply new rev info, tracking applied test merges
470  async Task UpdateRevInfo()
471  {
472  var last = lastRevisionInfo;
473  await LoadRevisionInformation(repo, databaseContext, attachedInstance, last.OriginCommitSha, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
474  lastRevisionInfo.ActiveTestMerges.AddRange(last.ActiveTestMerges);
475  }
476 
477  try
478  {
479  // fetch/pull
480  if (model.UpdateFromOrigin == true)
481  {
482  if (!repo.Tracking)
483  throw new JobException("Not on an updatable reference!");
484  await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false);
485  doneSteps = 1;
486  if (!modelHasShaOrReference)
487  {
488  var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false);
489  if (!fastForward.HasValue)
490  throw new JobException("Merge conflict occurred during origin update!");
491  await UpdateRevInfo().ConfigureAwait(false);
492  if (fastForward.Value)
493  {
494  lastRevisionInfo.OriginCommitSha = repo.Head;
495  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
496  postUpdateSha = repo.Head;
497  }
498  else
499  NextProgressReporter()(100);
500  }
501  }
502 
503  // checkout/hard reset
504  if (modelHasShaOrReference)
505  {
506  if ((model.CheckoutSha != null && repo.Head.ToUpperInvariant().StartsWith(model.CheckoutSha.ToUpperInvariant(), StringComparison.Ordinal))
507  || (model.Reference != null && repo.Reference.ToUpperInvariant() != model.Reference.ToUpperInvariant()))
508  {
509  var committish = model.CheckoutSha ?? model.Reference;
510  var isSha = await repo.IsSha(committish, cancellationToken).ConfigureAwait(false);
511 
512  if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null))
513  throw new JobException("Attempted to checkout a SHA or reference that was actually the opposite!");
514 
515  await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false);
516  await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin
517  }
518  else
519  NextProgressReporter()(100);
520 
521  if (hardResettingToOriginReference)
522  {
523  if (!repo.Tracking)
524  throw new JobException("Checked out reference does not track a remote object!");
525  await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false);
526  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
527  await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
528 
529  // repo head is on origin so force this
530  // will update the db if necessary
531  lastRevisionInfo.OriginCommitSha = repo.Head;
532  }
533  }
534 
535  // test merging
536  Dictionary<int, Octokit.PullRequest> prMap = null;
537  if (newTestMerges)
538  {
539  // bit of sanitization
540  foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision)))
541  I.PullRequestRevision = null;
542 
543  var gitHubClient = currentModel.AccessToken != null
544  ? gitHubClientFactory.CreateClient(currentModel.AccessToken)
545  : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken)
546  ? gitHubClientFactory.CreateClient()
547  : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken));
548 
549  var repoOwner = repo.GitHubOwner;
550  var repoName = repo.GitHubRepoName;
551 
552  // 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
553  Models.RevisionInformation revInfoWereLookingFor = null;
554  bool needToApplyRemainingPrs = true;
555  if (lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
556  {
557  // In order for this to work though we need the shas of all the commits
558  if (model.NewTestMerges.Any(x => x.PullRequestRevision == null))
559  prMap = new Dictionary<int, Octokit.PullRequest>();
560 
561  bool cantSearch = false;
562  foreach (var I in model.NewTestMerges)
563  {
564  if (I.PullRequestRevision != null)
565 #pragma warning disable CA1308 // Normalize strings to uppercase
566  I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant(); // ala libgit2
567 #pragma warning restore CA1308 // Normalize strings to uppercase
568  else
569  try
570  {
571  // retrieve the latest sha
572  var pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false);
573  prMap.Add(I.Number.Value, pr);
574  I.PullRequestRevision = pr.Head.Sha;
575  }
576  catch
577  {
578  cantSearch = true;
579  break;
580  }
581  }
582 
583  if (!cantSearch)
584  {
585  var dbPull = await databaseContext.RevisionInformations
586  .Where(x => x.Instance.Id == Instance.Id
587  && x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
588  && x.ActiveTestMerges.Count <= model.NewTestMerges.Count
589  && x.ActiveTestMerges.Count > 0)
590  .Include(x => x.ActiveTestMerges)
591  .ThenInclude(x => x.TestMerge)
592  .ToListAsync(cancellationToken).ConfigureAwait(false);
593 
594  // split here cause this bit has to be done locally
595  revInfoWereLookingFor = dbPull
596  .Where(x => x.ActiveTestMerges.Count == model.NewTestMerges.Count
597  && x.ActiveTestMerges.Select(y => y.TestMerge)
598  .All(y => model.NewTestMerges.Any(z =>
599  y.Number == z.Number
600  && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
601  && (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
602  .FirstOrDefault();
603 
604  if (revInfoWereLookingFor == null && model.NewTestMerges.Count > 1)
605  {
606  // okay try to add at least SOME prs we've seen before
607  var search = model.NewTestMerges.ToList();
608 
609  var appliedTestMergeIds = new List<long>();
610 
611  Models.RevisionInformation lastGoodRevInfo = null;
612  do
613  {
614  foreach (var I in search)
615  {
616  revInfoWereLookingFor = dbPull
617  .Where(x => model.NewTestMerges.Any(z =>
618  x.PrimaryTestMerge.Number == z.Number
619  && x.PrimaryTestMerge.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)
620  && (x.PrimaryTestMerge.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))
621  && x.ActiveTestMerges.Select(y => y.TestMerge).All(y => appliedTestMergeIds.Contains(y.Id)))
622  .FirstOrDefault();
623 
624  if (revInfoWereLookingFor != null)
625  {
626  lastGoodRevInfo = revInfoWereLookingFor;
627  appliedTestMergeIds.Add(revInfoWereLookingFor.PrimaryTestMerge.Id);
628  search.Remove(I);
629  break;
630  }
631  }
632  }
633  while (revInfoWereLookingFor != null && search.Count > 0);
634 
635  revInfoWereLookingFor = lastGoodRevInfo;
636  needToApplyRemainingPrs = search.Count != 0;
637  if (needToApplyRemainingPrs)
638  model.NewTestMerges = search;
639  }
640  else if (revInfoWereLookingFor != null)
641  needToApplyRemainingPrs = false;
642  }
643  }
644 
645  if (revInfoWereLookingFor != null)
646  {
647  // goteem
648  await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false);
649  lastRevisionInfo = revInfoWereLookingFor;
650  }
651 
652  if (needToApplyRemainingPrs)
653  {
654  // an invocation of LoadRevisionInformation could have already loaded this user
655  var contextUser = databaseContext.Users.Local.Where(x => x.Id == AuthenticationContext.User.Id).FirstOrDefault();
656  if (contextUser == default)
657  {
658  contextUser = new Models.User
659  {
661  };
662  databaseContext.Users.Attach(contextUser);
663  }
664  else
665  Logger.LogTrace("Skipping attaching the user to the database context as it is already loaded!");
666 
667  foreach (var I in model.NewTestMerges)
668  {
669  Octokit.PullRequest pr = null;
670  string errorMessage = null;
671 
672  if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number.Value))
673  throw new JobException("Cannot test merge the same PR twice in one HEAD!");
674 
675  try
676  {
677  // load from cache if possible
678  if (prMap == null || !prMap.TryGetValue(I.Number.Value, out pr))
679  pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number.Value).ConfigureAwait(false);
680  }
681  catch (Octokit.RateLimitExceededException)
682  {
683  // you look at your anonymous access and sigh
684  errorMessage = "P.R.E. RATE LIMITED";
685  }
686  catch (Octokit.AuthorizationException)
687  {
688  errorMessage = "P.R.E. BAD CREDENTIALS";
689  }
690  catch (Octokit.NotFoundException)
691  {
692  // you look at your shithub and sigh
693  errorMessage = "P.R.E. NOT FOUND";
694  }
695 
696  // we want to take the earliest truth possible to prevent RCEs, if this fails AddTestMerge will set it
697  if (I.PullRequestRevision == null && pr != null)
698  I.PullRequestRevision = pr.Head.Sha;
699 
700  var mergeResult = await repo.AddTestMerge(I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false);
701 
702  if (!mergeResult.HasValue)
703  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Merge of PR #{0} at {1} conflicted!", I.Number, I.PullRequestRevision.Substring(0, 7)));
704 
705  ++doneSteps;
706 
707  var revInfoUpdateTask = UpdateRevInfo();
708 
709  var tm = new Models.TestMerge
710  {
711  Author = pr?.User.Login ?? errorMessage,
712  BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty,
713  MergedAt = DateTimeOffset.Now,
714  TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty,
715  Comment = I.Comment,
716  Number = I.Number,
717  MergedBy = contextUser,
718  PullRequestRevision = I.PullRequestRevision,
719  Url = pr?.HtmlUrl ?? errorMessage
720  };
721 
722  await revInfoUpdateTask.ConfigureAwait(false);
723 
724  lastRevisionInfo.PrimaryTestMerge = tm;
725  lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
726  {
727  TestMerge = tm
728  });
729  }
730  }
731  }
732 
733  var currentHead = repo.Head;
734  if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead))
735  {
736  await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false);
737  await UpdateRevInfo().ConfigureAwait(false);
738  }
739 
740  await databaseContext.Save(ct).ConfigureAwait(false);
741  }
742  catch
743  {
744  doneSteps = 0;
745  numSteps = 2;
746 
747  // the stuff didn't make it into the db, forget what we've done and abort
748  await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false);
749  if (startReference != null && repo.Head != startSha)
750  await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false);
751  else
752  progressReporter(100);
753  throw;
754  }
755  }
756  }, cancellationToken).ConfigureAwait(false);
757 
758  api.ActiveJob = job.ToApi();
759  return Accepted(api);
760  }
761  #pragma warning restore CA1502
762  #pragma warning restore CA1505
763  }
764 }
string Name
The name of the User
Definition: User.cs:38
Manages the runtime of Jobs
Definition: IJobManager.cs:12
@@ -109,47 +109,47 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Many to many relationship for Models.RevisionInformation and Models.TestMerge
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
-
async Task< bool > PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
+
async Task< bool > PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
User may list and read all Models.CompileJobs
-
readonly IInstanceManager instanceManager
The IInstanceManager for the RepositoryController
+
readonly IInstanceManager instanceManager
The IInstanceManager for the RepositoryController
Routes to a server actions
Definition: Routes.cs:9
-
async Task< bool > LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action< Models.RevisionInformation > revInfoSink, CancellationToken cancellationToken)
- +
async Task< bool > LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action< Models.RevisionInformation > revInfoSink, CancellationToken cancellationToken)
+
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
- -
DbSet< RepositorySettings > RepositorySettings
+ +
DbSet< RepositorySettings > RepositorySettings
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
+
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for the RepositoryController
string Reference
The branch or tag HEAD points to
Definition: Repository.cs:48
An ApiController representing a TModel
-
override async Task< IActionResult > Update([FromBody]Repository model, CancellationToken cancellationToken)
-
RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger< RepositoryController > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct a RepositoryController
+
override async Task< IActionResult > Update([FromBody]Repository model, CancellationToken cancellationToken)
+
RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger< RepositoryController > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct a RepositoryController
long Id
The ID of the User
Definition: User.cs:15
Metadata about a server instance
Definition: Instance.cs:9
DbSet< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
-
override async Task< IActionResult > Create([FromBody] Repository model, CancellationToken cancellationToken)
+
override async Task< IActionResult > Create([FromBody] Repository model, CancellationToken cancellationToken)
string Origin
The origin URL. If , the Repository does not exist
Definition: Repository.cs:13
string GitHubName
If the repository was cloned from GitHub.com this will be set with the name of the repository ...
Definition: Repository.cs:33
-
override async Task< IActionResult > Read(CancellationToken cancellationToken)
Attempt to read a TModel
-
readonly IJobManager jobManager
The IJobManager for the RepositoryController
+
override async Task< IActionResult > Read(CancellationToken cancellationToken)
Attempt to read a TModel
+
readonly IJobManager jobManager
The IJobManager for the RepositoryController
Represents an error message returned by the server
Definition: ErrorMessage.cs:8
-
async Task< IActionResult > Delete(CancellationToken cancellationToken)
Delete the Repository
-
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the RepositoryController
+
async Task< IActionResult > Delete(CancellationToken cancellationToken)
Delete the Repository
+
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the RepositoryController
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:8
diff --git a/_repository_manager_8cs_source.html b/_repository_manager_8cs_source.html index 7207aa1459..b3a9dcbd7c 100644 --- a/_repository_manager_8cs_source.html +++ b/_repository_manager_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RepositoryManager.cs
-Go to the documentation of this file.
1 using LibGit2Sharp;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Threading;
5 using System.Threading.Tasks;
8 using Tgstation.Server.Host.IO;
9 
10 namespace Tgstation.Server.Host.Components.Repository
11 {
14  {
16  public bool InUse => semaphore.CurrentCount == 0;
17 
19  public bool CloneInProgress { get; private set; }
20 
25 
30 
35 
39  readonly ILogger<Repository> repositoryLogger;
40 
44  readonly ILogger<RepositoryManager> logger;
45 
50 
54  readonly SemaphoreSlim semaphore;
55 
65  public RepositoryManager(RepositorySettings repositorySettings, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger<Repository> repositoryLogger, ILogger<RepositoryManager> logger)
66  {
67  this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings));
68  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
69  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
70  this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
71  this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger));
72  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  semaphore = new SemaphoreSlim(1);
74  }
75 
77  public void Dispose()
78  {
79  logger.LogTrace("Disposing...");
80  semaphore.Dispose();
81  }
82 
84  public async Task<IRepository> CloneRepository(Uri url, string initialBranch, string username, string password, Action<int> progressReporter, CancellationToken cancellationToken)
85  {
86  if (url == null)
87  throw new ArgumentNullException(nameof(url));
88  if (progressReporter == null)
89  throw new ArgumentNullException(nameof(progressReporter));
90 
91  logger.LogInformation("Begin clone {0} (Branch: {1})", url, initialBranch);
92  lock (this)
93  {
94  if (CloneInProgress)
95  throw new InvalidOperationException("The repository is already being cloned!");
96  CloneInProgress = true;
97  }
98  try
99  {
100  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
101  {
102  logger.LogTrace("Semaphore acquired");
103  if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
104  try
105  {
106  await Task.Factory.StartNew(() =>
107  {
108  string path = null;
109  try
110  {
111  path = LibGit2Sharp.Repository.Clone(url.ToString(), ioManager.ResolvePath("."), new CloneOptions
112  {
113  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
114  OnTransferProgress = (a) =>
115  {
116  var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
117  progressReporter((int)percentage);
118  return !cancellationToken.IsCancellationRequested;
119  },
120  RecurseSubmodules = true,
121  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
122  RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
123  BranchName = initialBranch,
124  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
125  });
126  }
127  catch (UserCancelledException) { }
128  cancellationToken.ThrowIfCancellationRequested();
129  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
130  }
131  catch
132  {
133  try
134  {
135  logger.LogTrace("Deleting partially cloned repository...");
136  await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
137  }
138  catch (Exception e)
139  {
140  logger.LogDebug("Error deleting partially cloned repository! Exception: {0}", e);
141  }
142  throw;
143  }
144  else
145  {
146  logger.LogDebug("Repository exists, clone aborted!");
147  return null;
148  }
149  }
150  logger.LogInformation("Clone complete!");
151  }
152  finally
153  {
154  CloneInProgress = false;
155  }
156  return await LoadRepository(cancellationToken).ConfigureAwait(false);
157  }
158 
160  public async Task<IRepository> LoadRepository(CancellationToken cancellationToken)
161  {
162  logger.LogTrace("Begin LoadRepository...");
163  lock (this)
164  if (CloneInProgress)
165  throw new InvalidOperationException("The repository is being cloned!");
166  await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
167  LibGit2Sharp.Repository repo = null;
168  await Task.Factory.StartNew(() =>
169  {
170  try
171  {
172  logger.LogTrace("Creating LibGit2Sharp.Repository...");
173  repo = new LibGit2Sharp.Repository(ioManager.ResolvePath("."));
174  }
175  catch (RepositoryNotFoundException e)
176  {
177  logger.LogDebug("Repository not found!");
178  logger.LogTrace("Exception: {0}", e);
179  }
180  catch
181  {
182  semaphore.Release();
183  throw;
184  }
185  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
186  if (repo == null)
187  {
188  semaphore.Release();
189  return null;
190  }
191  return new Repository(repo, ioManager, eventConsumer, credentialsProvider, repositoryLogger, () =>
192  {
193  logger.LogTrace("Releasing semaphore due to Repository disposal...");
194  semaphore.Release();
195  });
196  }
197 
199  public async Task DeleteRepository(CancellationToken cancellationToken)
200  {
201  logger.LogInformation("Deleting repository...");
202  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
203  {
204  logger.LogTrace("Semaphore acquired, deleting Repository directory...");
205  await ioManager.DeleteDirectory(".", cancellationToken).ConfigureAwait(false);
206  }
207  }
208  }
209 }
Represents configurable settings for a Repository
+Go to the documentation of this file.
1 using LibGit2Sharp;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Threading;
5 using System.Threading.Tasks;
8 using Tgstation.Server.Host.IO;
9 
10 namespace Tgstation.Server.Host.Components.Repository
11 {
14  {
16  public bool InUse => semaphore.CurrentCount == 0;
17 
19  public bool CloneInProgress { get; private set; }
20 
25 
30 
35 
39  readonly ILogger<Repository> repositoryLogger;
40 
44  readonly ILogger<RepositoryManager> logger;
45 
50 
54  readonly SemaphoreSlim semaphore;
55 
65  public RepositoryManager(RepositorySettings repositorySettings, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger<Repository> repositoryLogger, ILogger<RepositoryManager> logger)
66  {
67  this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings));
68  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
69  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
70  this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
71  this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger));
72  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
73  semaphore = new SemaphoreSlim(1);
74  }
75 
77  public void Dispose()
78  {
79  logger.LogTrace("Disposing...");
80  semaphore.Dispose();
81  }
82 
84  public async Task<IRepository> CloneRepository(Uri url, string initialBranch, string username, string password, Action<int> progressReporter, CancellationToken cancellationToken)
85  {
86  if (url == null)
87  throw new ArgumentNullException(nameof(url));
88  if (progressReporter == null)
89  throw new ArgumentNullException(nameof(progressReporter));
90 
91  logger.LogInformation("Begin clone {0} (Branch: {1})", url, initialBranch);
92  lock (this)
93  {
94  if (CloneInProgress)
95  throw new InvalidOperationException("The repository is already being cloned!");
96  CloneInProgress = true;
97  }
98 
99  try
100  {
101  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
102  {
103  logger.LogTrace("Semaphore acquired");
104  if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
105  try
106  {
107  await Task.Factory.StartNew(() =>
108  {
109  string path = null;
110  try
111  {
112  path = LibGit2Sharp.Repository.Clone(url.ToString(), ioManager.ResolvePath("."), new CloneOptions
113  {
114  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
115  OnTransferProgress = (a) =>
116  {
117  var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
118  progressReporter((int)percentage);
119  return !cancellationToken.IsCancellationRequested;
120  },
121  RecurseSubmodules = true,
122  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
123  RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
124  BranchName = initialBranch,
125  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
126  });
127  }
128  catch (UserCancelledException) { }
129  cancellationToken.ThrowIfCancellationRequested();
130  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
131  }
132  catch
133  {
134  try
135  {
136  logger.LogTrace("Deleting partially cloned repository...");
137  await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
138  }
139  catch (Exception e)
140  {
141  logger.LogDebug("Error deleting partially cloned repository! Exception: {0}", e);
142  }
143 
144  throw;
145  }
146  else
147  {
148  logger.LogDebug("Repository exists, clone aborted!");
149  return null;
150  }
151  }
152 
153  logger.LogInformation("Clone complete!");
154  }
155  finally
156  {
157  CloneInProgress = false;
158  }
159 
160  return await LoadRepository(cancellationToken).ConfigureAwait(false);
161  }
162 
164  public async Task<IRepository> LoadRepository(CancellationToken cancellationToken)
165  {
166  logger.LogTrace("Begin LoadRepository...");
167  lock (this)
168  if (CloneInProgress)
169  throw new InvalidOperationException("The repository is being cloned!");
170  await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
171  LibGit2Sharp.Repository repo = null;
172  await Task.Factory.StartNew(() =>
173  {
174  try
175  {
176  logger.LogTrace("Creating LibGit2Sharp.Repository...");
177  repo = new LibGit2Sharp.Repository(ioManager.ResolvePath("."));
178  }
179  catch (RepositoryNotFoundException e)
180  {
181  logger.LogDebug("Repository not found!");
182  logger.LogTrace("Exception: {0}", e);
183  }
184  catch
185  {
186  semaphore.Release();
187  throw;
188  }
189  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
190  if (repo == null)
191  {
192  semaphore.Release();
193  return null;
194  }
195 
196  return new Repository(repo, ioManager, eventConsumer, credentialsProvider, repositoryLogger, () =>
197  {
198  logger.LogTrace("Releasing semaphore due to Repository disposal...");
199  semaphore.Release();
200  });
201  }
202 
204  public async Task DeleteRepository(CancellationToken cancellationToken)
205  {
206  logger.LogInformation("Deleting repository...");
207  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
208  {
209  logger.LogTrace("Semaphore acquired, deleting Repository directory...");
210  await ioManager.DeleteDirectory(".", cancellationToken).ConfigureAwait(false);
211  }
212  }
213  }
214 }
Represents configurable settings for a Repository
Consumes EventTypes and takes the appropriate actions
@@ -117,9 +117,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IEventConsumer eventConsumer
The IEventConsumer for the RepositoryManager
async Task< IRepository > CloneRepository(Uri url, string initialBranch, string username, string password, Action< int > progressReporter, CancellationToken cancellationToken)
Clone the repository at url
Interface for using filesystems
Definition: IIOManager.cs:11
-
async Task< IRepository > LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location
+
async Task< IRepository > LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location
-
async Task DeleteRepository(CancellationToken cancellationToken)
Delete the current repository
+
async Task DeleteRepository(CancellationToken cancellationToken)
Delete the current repository
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
readonly RepositorySettings repositorySettings
The RepositorySettings for the RepositoryManager
diff --git a/_repository_rights_8cs_source.html b/_repository_rights_8cs_source.html index 4f0224ee5e..e401289d14 100644 --- a/_repository_rights_8cs_source.html +++ b/_repository_rights_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RepositoryRights.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum RepositoryRights : ulong
10  {
14  None = 0,
22  SetOrigin = 2,
26  SetSha = 4,
30  MergePullRequest = 8,
34  UpdateBranch = 16,
38  ChangeCommitter = 32,
46  ChangeCredentials = 128,
50  SetReference = 256,
54  Read = 512,
62  Delete = 2048,
66  CancelClone = 4096
67  }
68 }
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Api.Rights
4 {
8  [Flags]
9  public enum RepositoryRights : ulong
10  {
14  None = 0,
15 
20 
24  SetOrigin = 2,
25 
29  SetSha = 4,
30 
34  MergePullRequest = 8,
35 
39  UpdateBranch = 16,
40 
44  ChangeCommitter = 32,
45 
50 
54  ChangeCredentials = 128,
55 
59  SetReference = 256,
60 
64  Read = 512,
65 
70 
74  Delete = 2048,
75 
79  CancelClone = 4096
80  }
81 }
User may change Models.Internal.RepositorySettings.AutoUpdatesKeepTestMerges and Models.Internal.RepositorySettings.AutoUpdatesSynchronize
diff --git a/_resolving_i_o_manager_8cs_source.html b/_resolving_i_o_manager_8cs_source.html index 72fc51252e..f585a7a1d3 100644 --- a/_resolving_i_o_manager_8cs_source.html +++ b/_resolving_i_o_manager_8cs_source.html @@ -92,15 +92,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ResolvingIOManager.cs
-Go to the documentation of this file.
1 using System;
2 using System.IO;
3 
4 namespace Tgstation.Server.Host.IO
5 {
10  {
14  readonly string subdirectory;
15 
21  public ResolvingIOManager(IIOManager parent, string _subdirectory)
22  {
23  if (parent == null)
24  throw new ArgumentNullException(nameof(parent));
25  if (_subdirectory == null)
26  throw new ArgumentNullException(nameof(_subdirectory));
27 
28  subdirectory = ConcatPath(parent.ResolvePath("."), _subdirectory);
29  }
30 
32  public override string ResolvePath(string path)
33  {
34  if (!Path.IsPathRooted(path))
35  return base.ResolvePath(ConcatPath(subdirectory, path));
36  return path;
37  }
38  }
39 }
An IIOManager that resolve relative paths from another IIOManager to a subdirectory of that ...
+Go to the documentation of this file.
1 using System;
2 using System.IO;
3 
4 namespace Tgstation.Server.Host.IO
5 {
10  {
14  readonly string subdirectory;
15 
21  public ResolvingIOManager(IIOManager parent, string subdirectory)
22  {
23  if (parent == null)
24  throw new ArgumentNullException(nameof(parent));
25  if (subdirectory == null)
26  throw new ArgumentNullException(nameof(subdirectory));
27 
28  this.subdirectory = ConcatPath(parent.ResolvePath("."), subdirectory);
29  }
30 
32  public override string ResolvePath(string path)
33  {
34  if (!Path.IsPathRooted(path))
35  return base.ResolvePath(ConcatPath(subdirectory, path));
36  return path;
37  }
38  }
39 }
An IIOManager that resolve relative paths from another IIOManager to a subdirectory of that ...
string ResolvePath(string path)
Retrieve the full path of some path given a relative path. Must be used before passing relative path...
+
ResolvingIOManager(IIOManager parent, string subdirectory)
Construct a ResolvingIOManager
override string ResolvePath(string path)
Retrieve the full path of some path given a relative path. Must be used before passing relative path...
-
IIOManager that resolves paths to Environment.CurrentDirectory
+
IIOManager that resolves paths to Environment.CurrentDirectory
Interface for using filesystems
Definition: IIOManager.cs:11
readonly string subdirectory
Path to the subdirectory attached to path resolutions
-
ResolvingIOManager(IIOManager parent, string _subdirectory)
Construct a ResolvingIOManager
diff --git a/_rights_helper_8cs_source.html b/_rights_helper_8cs_source.html index cb65635b7d..c41c17bfc2 100644 --- a/_rights_helper_8cs_source.html +++ b/_rights_helper_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RightsHelper.cs
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Globalization;
4 
5 namespace Tgstation.Server.Api.Rights
6 {
10  public static class RightsHelper
11  {
15  static readonly IReadOnlyDictionary<RightsType, Type> typeMap = new Dictionary<RightsType, Type>
16  {
17  { RightsType.Administration, typeof(AdministrationRights) },
18  { RightsType.InstanceManager, typeof(InstanceManagerRights) },
19 
20  { RightsType.Repository, typeof(RepositoryRights) },
21  { RightsType.Byond, typeof(ByondRights) },
22  { RightsType.DreamMaker, typeof(DreamMakerRights) },
23  { RightsType.DreamDaemon, typeof(DreamDaemonRights) },
24  { RightsType.ChatBots, typeof(ChatBotRights) },
25  { RightsType.Configuration, typeof(ConfigurationRights) },
26  { RightsType.InstanceUser, typeof(InstanceUserRights) }
27  };
28 
34  public static Type RightToType(RightsType rightsType) => typeMap[rightsType];
35 
42  public static string RoleNames<TRight>(TRight right) where TRight : Enum
43  {
44  var flags = new List<string>();
45  IEnumerable<string> GetRoleNames()
46  {
47  foreach (Enum J in Enum.GetValues(right.GetType()))
48  if (Convert.ToInt32(J, CultureInfo.InvariantCulture) != 0 && right.HasFlag(J))
49  yield return String.Concat(typeof(TRight).Name, '.', J.ToString());
50  };
51  var names = GetRoleNames();
52  return String.Join(",", names);
53  }
54 
61  public static string RoleName(RightsType rightsType, Enum right)
62  {
63  var enumType = typeMap[rightsType];
64  return String.Concat(enumType.Name, '.', Enum.GetName(enumType, right));
65  }
66 
72  public static bool IsInstanceRight(RightsType rightsType) => !(rightsType == RightsType.Administration || rightsType == RightsType.InstanceManager);
73  }
74 }
ConfigurationRights
Rights for Models.ConfigurationFile
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Globalization;
4 
5 namespace Tgstation.Server.Api.Rights
6 {
10  public static class RightsHelper
11  {
15  static readonly IReadOnlyDictionary<RightsType, Type> TypeMap = new Dictionary<RightsType, Type>
16  {
17  { RightsType.Administration, typeof(AdministrationRights) },
18  { RightsType.InstanceManager, typeof(InstanceManagerRights) },
19  { RightsType.Repository, typeof(RepositoryRights) },
20  { RightsType.Byond, typeof(ByondRights) },
21  { RightsType.DreamMaker, typeof(DreamMakerRights) },
22  { RightsType.DreamDaemon, typeof(DreamDaemonRights) },
23  { RightsType.ChatBots, typeof(ChatBotRights) },
24  { RightsType.Configuration, typeof(ConfigurationRights) },
25  { RightsType.InstanceUser, typeof(InstanceUserRights) }
26  };
27 
33  public static Type RightToType(RightsType rightsType) => TypeMap[rightsType];
34 
41  public static string RoleNames<TRight>(TRight right) where TRight : Enum
42  {
43  var flags = new List<string>();
44  IEnumerable<string> GetRoleNames()
45  {
46  foreach (Enum J in Enum.GetValues(right.GetType()))
47  if (Convert.ToInt32(J, CultureInfo.InvariantCulture) != 0 && right.HasFlag(J))
48  yield return String.Concat(typeof(TRight).Name, '.', J.ToString());
49  }
50 
51  var names = GetRoleNames();
52  return String.Join(",", names);
53  }
54 
61  public static string RoleName(RightsType rightsType, Enum right)
62  {
63  var enumType = TypeMap[rightsType];
64  return String.Concat(enumType.Name, '.', Enum.GetName(enumType, right));
65  }
66 
72  public static bool IsInstanceRight(RightsType rightsType) => !(rightsType == RightsType.Administration || rightsType == RightsType.InstanceManager);
73  }
74 }
ConfigurationRights
Rights for Models.ConfigurationFile
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
diff --git a/_rights_type_8cs_source.html b/_rights_type_8cs_source.html index 197cfb78ef..f4aa042ece 100644 --- a/_rights_type_8cs_source.html +++ b/_rights_type_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RightsType.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Rights
2 {
6  public enum RightsType : ulong
7  {
19  Repository,
23  Byond,
27  DreamMaker,
35  ChatBots,
44  }
45 }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
+Go to the documentation of this file.
1 namespace Tgstation.Server.Api.Rights
2 {
6  public enum RightsType : ulong
7  {
12 
17 
21  Repository,
22 
26  Byond,
27 
31  DreamMaker,
32 
37 
41  ChatBots,
42 
47 
52  }
53 }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
diff --git a/_semaphore_slim_context_8cs_source.html b/_semaphore_slim_context_8cs_source.html index 3d5ad71763..1f8721bf74 100644 --- a/_semaphore_slim_context_8cs_source.html +++ b/_semaphore_slim_context_8cs_source.html @@ -92,11 +92,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SemaphoreSlimContext.cs
-Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 
5 namespace Tgstation.Server.Host.Core
6 {
10  public sealed class SemaphoreSlimContext : IDisposable
11  {
18  public static async Task<SemaphoreSlimContext> Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
19  {
20  if (semaphore == null)
21  throw new ArgumentNullException(nameof(semaphore));
22  await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
23  cancellationToken.ThrowIfCancellationRequested();
24  return new SemaphoreSlimContext(semaphore);
25  }
26 
30  readonly SemaphoreSlim lockedSemaphore;
31 
35  bool disposed;
36 
41  SemaphoreSlimContext(SemaphoreSlim lockedSemaphore) => this.lockedSemaphore = lockedSemaphore;
42 
46 #pragma warning disable CA1821 // Remove empty Finalizers //TODO remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
47  ~SemaphoreSlimContext() => Dispose();
48 #pragma warning restore CA1821 // Remove empty Finalizers
49 
53  public void Dispose()
54  {
55  lock (this)
56  {
57  if (disposed)
58  return;
59  disposed = true;
60  }
61  GC.SuppressFinalize(this);
62  lockedSemaphore.Release();
63  }
64  }
65 }
+Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 
5 namespace Tgstation.Server.Host.Core
6 {
10  public sealed class SemaphoreSlimContext : IDisposable
11  {
18  public static async Task<SemaphoreSlimContext> Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
19  {
20  if (semaphore == null)
21  throw new ArgumentNullException(nameof(semaphore));
22  await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
23  cancellationToken.ThrowIfCancellationRequested();
24  return new SemaphoreSlimContext(semaphore);
25  }
26 
30  readonly SemaphoreSlim lockedSemaphore;
31 
35  bool disposed;
36 
41  SemaphoreSlimContext(SemaphoreSlim lockedSemaphore)
42  {
43  this.lockedSemaphore = lockedSemaphore;
44  }
45 
49 #pragma warning disable CA1821 // Remove empty Finalizers //TODO remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
50  ~SemaphoreSlimContext() => Dispose();
51 #pragma warning restore CA1821 // Remove empty Finalizers
52 
56  public void Dispose()
57  {
58  lock (this)
59  {
60  if (disposed)
61  return;
62  disposed = true;
63  }
64 
65  GC.SuppressFinalize(this);
66  lockedSemaphore.Release();
67  }
68  }
69 }
readonly SemaphoreSlim lockedSemaphore
The locked SemaphoreSlim
+
SemaphoreSlimContext(SemaphoreSlim lockedSemaphore)
Construct a SemaphoreSlimContext
-
void Dispose()
Release the lock on lockedSemaphore
+
void Dispose()
Release the lock on lockedSemaphore
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
diff --git a/_server_8cs_source.html b/_server_8cs_source.html index c8b33b1f8e..3552858239 100644 --- a/_server_8cs_source.html +++ b/_server_8cs_source.html @@ -92,15 +92,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Server.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.DependencyInjection;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.IO;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host
14 {
16 #pragma warning disable CA1001 // Types that own disposable fields should be disposable
17  sealed class Server : IServer, IServerControl
18 #pragma warning restore CA1001 // Types that own disposable fields should be disposable
19  {
21  public bool RestartRequested { get; private set; }
22 
24  public bool WatchdogPresent => updatePath != null;
25 
29  readonly IWebHostBuilder webHostBuilder;
30 
34  readonly List<IRestartHandler> restartHandlers;
35 
39  readonly string updatePath;
40 
44  ILogger<Server> logger;
45 
49  CancellationTokenSource cancellationTokenSource;
50 
55 
59  bool updating;
60 
66  public Server(IWebHostBuilder webHostBuilder, string updatePath)
67  {
68  this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder));
69  this.updatePath = updatePath;
70 
71  webHostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
72 
73  restartHandlers = new List<IRestartHandler>();
74  }
75 
80  void CheckSanity(bool checkWatchdog)
81  {
82  if (checkWatchdog && !WatchdogPresent && propagatedException == null)
83  throw new InvalidOperationException("Server restarts are not supported");
84 
85  if (cancellationTokenSource == null || logger == null)
86  throw new InvalidOperationException("Tried to control a non-running Server!");
87  }
88 
93  {
94  if (propagatedException != null)
95  throw propagatedException;
96  }
97 
99  public async Task RunAsync(CancellationToken cancellationToken)
100  {
101  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
102  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
103  {
104  if (fsWatcher != null)
105  {
106  fsWatcher.Created += (a, b) =>
107  {
108  if (b.FullPath == updatePath && File.Exists(b.FullPath))
109  cancellationTokenSource.Cancel();
110  };
111  fsWatcher.EnableRaisingEvents = true;
112  }
113 
114  using (var webHost = webHostBuilder.Build())
115  try
116  {
117  logger = webHost.Services.GetRequiredService<ILogger<Server>>();
118  await webHost.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
119  }
120  catch (OperationCanceledException)
121  {
122  CheckExceptionPropagation();
123  throw;
124  }
125  }
126  CheckExceptionPropagation();
127  }
128 
130  public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
131  {
132  if (version == null)
133  throw new ArgumentNullException(nameof(version));
134  if (updateZipUrl == null)
135  throw new ArgumentNullException(nameof(updateZipUrl));
136  if (ioManager == null)
137  throw new ArgumentNullException(nameof(ioManager));
138 
139  CheckSanity(true);
140 
141  logger.LogTrace("Begin ApplyUpdate...");
142 
143  lock (this)
144  {
145  if (updating || RestartRequested)
146  {
147  logger.LogTrace("Aborted due to concurrency conflict!");
148  return false;
149  }
150  updating = true;
151  }
152 
153  async void RunUpdate()
154  {
155  try
156  {
157  logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl);
158 
159  if (cancellationTokenSource == null)
160  throw new InvalidOperationException("Tried to update a non-running Server!");
161  var cancellationToken = cancellationTokenSource.Token;
162 
163  logger.LogTrace("Downloading zip package...");
164  var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false);
165 
166  try
167  {
168  logger.LogTrace("Exctracting zip package to {0}...", updatePath);
169  await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false);
170  }
171  catch (Exception e)
172  {
173  updating = false;
174  try
175  {
176  //important to not leave this directory around if possible
177  await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false);
178  }
179  catch (Exception e2)
180  {
181  throw new AggregateException(e, e2);
182  }
183  throw;
184  }
185 
186  await Restart(version, null).ConfigureAwait(false);
187  }
188  catch (OperationCanceledException)
189  {
190  logger.LogInformation("Server update cancelled!");
191  }
192  catch (Exception e)
193  {
194  logger.LogError("Error updating server! Exception: {0}", e);
195  }
196  finally
197  {
198  updating = false;
199  }
200  }
201 
202  RunUpdate();
203  return true;
204  }
205 
208  {
209  if (handler == null)
210  throw new ArgumentNullException(nameof(handler));
211 
212  CheckSanity(false);
213 
214  lock (this)
215  if (!RestartRequested)
216  {
217  logger.LogTrace("Registering restart handler {0}...", handler);
218  restartHandlers.Add(handler);
219  return new RestartRegistration(() =>
220  {
221  lock (this)
222  if (!RestartRequested)
223  restartHandlers.Remove(handler);
224  });
225  }
226  return new RestartRegistration(() => { });
227  }
228 
230  public Task Restart() => Restart(null, null);
231 
238  async Task Restart(Version newVersion, Exception exception)
239  {
240  CheckSanity(true);
241 
242  logger.LogTrace("Begin Restart...");
243 
244  lock (this)
245  {
246  if ((updating && newVersion == null) || RestartRequested)
247  {
248  logger.LogTrace("Aborted due to concurrency conflict!");
249  return;
250  }
251  RestartRequested = true;
252  propagatedException = exception;
253  }
254 
255  if (exception == null)
256  using (var cts = new CancellationTokenSource())
257  {
258  logger.LogInformation("Restarting server...");
259  var cancellationToken = cts.Token;
260  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
261  //YA GOT 10 SECONDS
262  var expiryTask = Task.Delay(TimeSpan.FromSeconds(10));
263  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
264  logger.LogTrace("Joining restart handlers...");
265  cts.Cancel();
266  try
267  {
268  await eventsTask.ConfigureAwait(false);
269  }
270  catch (OperationCanceledException) { }
271  catch (Exception e)
272  {
273  logger.LogError("Restart handlers error! Exception: {0}", e);
274  }
275  }
276 
277  logger.LogTrace("Stopping host...");
278  cancellationTokenSource.Cancel();
279  }
280 
282  public Task Die(Exception exception) => Restart(null, exception);
283  }
284 }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.DependencyInjection;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.IO;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
11 using Tgstation.Server.Host.IO;
12 
13 namespace Tgstation.Server.Host
14 {
16 #pragma warning disable CA1001 // Types that own disposable fields should be disposable
17  sealed class Server : IServer, IServerControl
18 #pragma warning restore CA1001 // Types that own disposable fields should be disposable
19  {
21  public bool RestartRequested { get; private set; }
22 
24  public bool WatchdogPresent => updatePath != null;
25 
29  readonly IWebHostBuilder webHostBuilder;
30 
34  readonly List<IRestartHandler> restartHandlers;
35 
39  readonly string updatePath;
40 
44  ILogger<Server> logger;
45 
49  CancellationTokenSource cancellationTokenSource;
50 
55 
59  bool updating;
60 
66  public Server(IWebHostBuilder webHostBuilder, string updatePath)
67  {
68  this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder));
69  this.updatePath = updatePath;
70 
71  webHostBuilder.ConfigureServices(serviceCollection => serviceCollection.AddSingleton<IServerControl>(this));
72 
73  restartHandlers = new List<IRestartHandler>();
74  }
75 
80  void CheckSanity(bool checkWatchdog)
81  {
82  if (checkWatchdog && !WatchdogPresent && propagatedException == null)
83  throw new InvalidOperationException("Server restarts are not supported");
84 
85  if (cancellationTokenSource == null || logger == null)
86  throw new InvalidOperationException("Tried to control a non-running Server!");
87  }
88 
93  {
94  if (propagatedException != null)
95  throw propagatedException;
96  }
97 
99  public async Task RunAsync(CancellationToken cancellationToken)
100  {
101  using (cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
102  using (var fsWatcher = updatePath != null ? new FileSystemWatcher(Path.GetDirectoryName(updatePath)) : null)
103  {
104  if (fsWatcher != null)
105  {
106  fsWatcher.Created += (a, b) =>
107  {
108  if (b.FullPath == updatePath && File.Exists(b.FullPath))
109  cancellationTokenSource.Cancel();
110  };
111  fsWatcher.EnableRaisingEvents = true;
112  }
113 
114  using (var webHost = webHostBuilder.Build())
115  try
116  {
117  logger = webHost.Services.GetRequiredService<ILogger<Server>>();
118  await webHost.RunAsync(cancellationTokenSource.Token).ConfigureAwait(false);
119  }
120  catch (OperationCanceledException)
121  {
122  CheckExceptionPropagation();
123  throw;
124  }
125  }
126 
127  CheckExceptionPropagation();
128  }
129 
131  public bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
132  {
133  if (version == null)
134  throw new ArgumentNullException(nameof(version));
135  if (updateZipUrl == null)
136  throw new ArgumentNullException(nameof(updateZipUrl));
137  if (ioManager == null)
138  throw new ArgumentNullException(nameof(ioManager));
139 
140  CheckSanity(true);
141 
142  logger.LogTrace("Begin ApplyUpdate...");
143 
144  lock (this)
145  {
146  if (updating || RestartRequested)
147  {
148  logger.LogTrace("Aborted due to concurrency conflict!");
149  return false;
150  }
151 
152  updating = true;
153  }
154 
155  async void RunUpdate()
156  {
157  try
158  {
159  logger.LogInformation("Updating server to version {0} ({1})...", version, updateZipUrl);
160 
161  if (cancellationTokenSource == null)
162  throw new InvalidOperationException("Tried to update a non-running Server!");
163  var cancellationToken = cancellationTokenSource.Token;
164 
165  logger.LogTrace("Downloading zip package...");
166  var updateZipData = await ioManager.DownloadFile(updateZipUrl, cancellationToken).ConfigureAwait(false);
167 
168  try
169  {
170  logger.LogTrace("Exctracting zip package to {0}...", updatePath);
171  await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false);
172  }
173  catch (Exception e)
174  {
175  updating = false;
176  try
177  {
178  // important to not leave this directory around if possible
179  await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false);
180  }
181  catch (Exception e2)
182  {
183  throw new AggregateException(e, e2);
184  }
185 
186  throw;
187  }
188 
189  await Restart(version, null).ConfigureAwait(false);
190  }
191  catch (OperationCanceledException)
192  {
193  logger.LogInformation("Server update cancelled!");
194  }
195  catch (Exception e)
196  {
197  logger.LogError("Error updating server! Exception: {0}", e);
198  }
199  finally
200  {
201  updating = false;
202  }
203  }
204 
205  RunUpdate();
206  return true;
207  }
208 
211  {
212  if (handler == null)
213  throw new ArgumentNullException(nameof(handler));
214 
215  CheckSanity(false);
216 
217  lock (this)
218  if (!RestartRequested)
219  {
220  logger.LogTrace("Registering restart handler {0}...", handler);
221  restartHandlers.Add(handler);
222  return new RestartRegistration(() =>
223  {
224  lock (this)
225  if (!RestartRequested)
226  restartHandlers.Remove(handler);
227  });
228  }
229 
230  return new RestartRegistration(() => { });
231  }
232 
234  public Task Restart() => Restart(null, null);
235 
242  async Task Restart(Version newVersion, Exception exception)
243  {
244  CheckSanity(true);
245 
246  logger.LogTrace("Begin Restart...");
247 
248  lock (this)
249  {
250  if ((updating && newVersion == null) || RestartRequested)
251  {
252  logger.LogTrace("Aborted due to concurrency conflict!");
253  return;
254  }
255 
256  RestartRequested = true;
257  propagatedException = exception;
258  }
259 
260  if (exception == null)
261  using (var cts = new CancellationTokenSource())
262  {
263  logger.LogInformation("Restarting server...");
264  var cancellationToken = cts.Token;
265  var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken)).ToList());
266 
267  // YA GOT 10 SECONDS
268  var expiryTask = Task.Delay(TimeSpan.FromSeconds(10));
269  await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false);
270  logger.LogTrace("Joining restart handlers...");
271  cts.Cancel();
272  try
273  {
274  await eventsTask.ConfigureAwait(false);
275  }
276  catch (OperationCanceledException) { }
277  catch (Exception e)
278  {
279  logger.LogError("Restart handlers error! Exception: {0}", e);
280  }
281  }
282 
283  logger.LogTrace("Stopping host...");
284  cancellationTokenSource.Cancel();
285  }
286 
288  public Task Die(Exception exception) => Restart(null, exception);
289  }
290 }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
ILogger< Server > logger
The ILogger for the Server
Definition: Server.cs:44
async Task RunAsync(CancellationToken cancellationToken)
Runs the IServer
Definition: Server.cs:99
-
bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
Run a new Host assembly and stop the current one. This will likely trigger all active CancellationTok...
Definition: Server.cs:130
+
bool ApplyUpdate(Version version, Uri updateZipUrl, IIOManager ioManager)
Run a new Host assembly and stop the current one. This will likely trigger all active CancellationTok...
Definition: Server.cs:131
readonly List< IRestartHandler > restartHandlers
The IRestartHandlers to run when the Server restarts
Definition: Server.cs:34
-
async Task Restart(Version newVersion, Exception exception)
Implements Restart()
Definition: Server.cs:238
+
async Task Restart(Version newVersion, Exception exception)
Implements Restart()
Definition: Server.cs:242
Task< byte[]> DownloadFile(Uri url, CancellationToken cancellationToken)
Downloads a file from url
-
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
Definition: Server.cs:207
+
IRestartRegistration RegisterForRestart(IRestartHandler handler)
Register a given handler to run before stopping the server for a restart
Definition: Server.cs:210
Task ZipToDirectory(string path, byte[] zipFileBytes, CancellationToken cancellationToken)
Extract a set of zipFileBytes to a given path
diff --git a/_server_client_8cs_source.html b/_server_client_8cs_source.html index acd64c464e..7fcce766d9 100644 --- a/_server_client_8cs_source.html +++ b/_server_client_8cs_source.html @@ -93,7 +93,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Go to the documentation of this file.
1 using System;
2 using System.Threading;
3 using System.Threading.Tasks;
4 using Tgstation.Server.Api;
6 
7 namespace Tgstation.Server.Client
8 {
10  sealed class ServerClient : IServerClient
11  {
13  public Uri Url => apiClient.Url;
14 
16  public Token Token
17  {
18  get => token;
19  set
20  {
21  token = value ?? throw new InvalidOperationException("Cannot set a null Token!");
22  apiClient.Headers = new ApiHeaders(apiClient.Headers.UserAgent, token.Bearer);
23  }
24  }
25 
27  public TimeSpan Timeout
28  {
29  get => apiClient.Timeout;
30  set => apiClient.Timeout = value;
31  }
32 
34  public IInstanceManagerClient Instances { get; }
35 
38 
40  public IUsersClient Users { get; }
41 
46 
51 
57  public ServerClient(IApiClient apiClient, Token token)
58  {
59  this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
60  this.token = token ?? throw new ArgumentNullException(nameof(token));
61 
62  if (Token.Bearer != apiClient.Headers.Token)
63  throw new ArgumentOutOfRangeException(nameof(token), token, "Provided token does not match apiClient headers!");
64 
65  Instances = new InstanceManagerClient(apiClient);
66  Users = new UsersClient(apiClient);
67  Administration = new AdministrationClient(apiClient);
68  }
69 
71  public void Dispose() => apiClient.Dispose();
72 
74  public Task<ServerInformation> Version(CancellationToken cancellationToken) => apiClient.Read<ServerInformation>(Routes.Root, cancellationToken);
75 
77  public void AddRequestLogger(IRequestLogger requestLogger) => apiClient.AddRequestLogger(requestLogger);
78  }
79 }
ServerClient(IApiClient apiClient, Token token)
Construct a ServerClient
Definition: ServerClient.cs:57
- +
ApiHeaders Headers
The ApiHeaders the IApiClient uses
Definition: IApiClient.cs:16
Token token
Backing field for Token
Definition: ServerClient.cs:50
@@ -105,7 +105,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents basic server information
Represents the header that must be present for every server request
Definition: ApiHeaders.cs:16
Uri Url
The connected server Uri
- +
Represents administrative server information
Routes to a server actions
Definition: Routes.cs:9
Main client for communicating with a server
diff --git a/_server_client_factory_8cs_source.html b/_server_client_factory_8cs_source.html index 64178434de..788cac46ec 100644 --- a/_server_client_factory_8cs_source.html +++ b/_server_client_factory_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServerClientFactory.cs
-Go to the documentation of this file.
1 using System;
2 using System.Net.Http.Headers;
3 using System.Threading;
4 using System.Threading.Tasks;
5 using Tgstation.Server.Api;
7 
8 namespace Tgstation.Server.Client
9 {
12  {
16  static readonly IApiClientFactory apiClientFactory = new ApiClientFactory();
17 
21  readonly ProductHeaderValue productHeaderValue;
22 
27  public ServerClientFactory(ProductHeaderValue productHeaderValue)
28  {
29  this.productHeaderValue = productHeaderValue ?? throw new ArgumentNullException(nameof(productHeaderValue));
30  }
31 
33  public async Task<IServerClient> CreateServerClient(Uri host, string username, string password, TimeSpan timeout, CancellationToken cancellationToken)
34  {
35  if (host == null)
36  throw new ArgumentNullException(nameof(host));
37  if (username == null)
38  throw new ArgumentNullException(nameof(username));
39  if (password == null)
40  throw new ArgumentNullException(nameof(password));
41 
42  Token token;
43  using (var api = apiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, username, password)))
44  {
45  if (timeout != default)
46  api.Timeout = timeout;
47  token = await api.Update<Token>(Routes.Root, cancellationToken).ConfigureAwait(false);
48  }
49  return CreateServerClient(host, token, timeout);
50  }
51 
53  public IServerClient CreateServerClient(Uri host, Token token, TimeSpan timeout)
54  {
55  if (host == null)
56  throw new ArgumentNullException(nameof(host));
57  if (token == null)
58  throw new ArgumentNullException(nameof(token));
59  var result = new ServerClient(apiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, token.Bearer)), token);
60  if (timeout != default)
61  result.Timeout = timeout;
62  return result;
63  }
64  }
65 }
+Go to the documentation of this file.
1 using System;
2 using System.Net.Http.Headers;
3 using System.Threading;
4 using System.Threading.Tasks;
5 using Tgstation.Server.Api;
7 
8 namespace Tgstation.Server.Client
9 {
12  {
17 
21  readonly ProductHeaderValue productHeaderValue;
22 
27  public ServerClientFactory(ProductHeaderValue productHeaderValue)
28  {
29  this.productHeaderValue = productHeaderValue ?? throw new ArgumentNullException(nameof(productHeaderValue));
30  }
31 
33  public async Task<IServerClient> CreateServerClient(Uri host, string username, string password, TimeSpan timeout, CancellationToken cancellationToken)
34  {
35  if (host == null)
36  throw new ArgumentNullException(nameof(host));
37  if (username == null)
38  throw new ArgumentNullException(nameof(username));
39  if (password == null)
40  throw new ArgumentNullException(nameof(password));
41 
42  Token token;
43  using (var api = ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, username, password)))
44  {
45  if (timeout != default)
46  api.Timeout = timeout;
47  token = await api.Update<Token>(Routes.Root, cancellationToken).ConfigureAwait(false);
48  }
49 
50  return CreateServerClient(host, token, timeout);
51  }
52 
54  public IServerClient CreateServerClient(Uri host, Token token, TimeSpan timeout)
55  {
56  if (host == null)
57  throw new ArgumentNullException(nameof(host));
58  if (token == null)
59  throw new ArgumentNullException(nameof(token));
60  var result = new ServerClient(ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, token.Bearer)), token);
61  if (timeout != default)
62  result.Timeout = timeout;
63  return result;
64  }
65  }
66 }
@@ -107,9 +107,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly ProductHeaderValue productHeaderValue
The ProductHeaderValue for the ServerClientFactory
+
static readonly IApiClientFactory ApiClientFactory
The IApiClientFactory for the ServerClientFactory
async Task< IServerClient > CreateServerClient(Uri host, string username, string password, TimeSpan timeout, CancellationToken cancellationToken)
Create a IServerClient
-
IServerClient CreateServerClient(Uri host, Token token, TimeSpan timeout)
Create a IServerClient
+
IServerClient CreateServerClient(Uri host, Token token, TimeSpan timeout)
Create a IServerClient
string Bearer
The value of the JWT
Definition: Token.cs:13
ServerClientFactory(ProductHeaderValue productHeaderValue)
Construct a ServerClientFactory
diff --git a/_server_factory_8cs_source.html b/_server_factory_8cs_source.html index 68962d13be..9e625f5015 100644 --- a/_server_factory_8cs_source.html +++ b/_server_factory_8cs_source.html @@ -92,10 +92,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServerFactory.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore;
2 using Microsoft.AspNetCore.Hosting;
3 using Microsoft.Extensions.Configuration;
4 using System;
5 using System.IO;
6 using System.Reflection;
8 
9 namespace Tgstation.Server.Host
10 {
12  public sealed class ServerFactory : IServerFactory
13  {
15  public IServer CreateServer(string[] args, string updatePath) {
16 
17  var webHost = WebHost.CreateDefaultBuilder(args ?? throw new ArgumentNullException(nameof(args)))
18  .ConfigureAppConfiguration((context, configurationBuilder) => configurationBuilder.SetBasePath(Directory.GetCurrentDirectory()))
19  .UseStartup<Application>()
20  .SuppressStatusMessages(true)
21  .UseShutdownTimeout(TimeSpan.FromMinutes(1));
22 
23  if(updatePath != null)
24  webHost.UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
25 
26  return new Server(webHost, updatePath);
27  }
28  }
29 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore;
2 using Microsoft.AspNetCore.Hosting;
3 using Microsoft.Extensions.Configuration;
4 using System;
5 using System.IO;
6 using System.Reflection;
8 
9 namespace Tgstation.Server.Host
10 {
12  public sealed class ServerFactory : IServerFactory
13  {
15  public IServer CreateServer(string[] args, string updatePath)
16  {
17  var webHost = WebHost.CreateDefaultBuilder(args ?? throw new ArgumentNullException(nameof(args)))
18  .ConfigureAppConfiguration((context, configurationBuilder) => configurationBuilder.SetBasePath(Directory.GetCurrentDirectory()))
19  .UseStartup<Application>()
20  .SuppressStatusMessages(true)
21  .UseShutdownTimeout(TimeSpan.FromMinutes(1));
22 
23  if(updatePath != null)
24  webHost.UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
25 
26  return new Server(webHost, updatePath);
27  }
28  }
29 }
- +
Represents the host
Definition: IServer.cs:9
diff --git a/_server_service_8cs_source.html b/_server_service_8cs_source.html index 045897ffdd..439aed0c5c 100644 --- a/_server_service_8cs_source.html +++ b/_server_service_8cs_source.html @@ -92,13 +92,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ServerService.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.Extensions.Logging.EventLog;
3 using Microsoft.Extensions.Logging.EventLog.Internal;
4 using System;
5 using System.Diagnostics;
6 using System.Diagnostics.CodeAnalysis;
7 using System.Globalization;
8 using System.ServiceProcess;
9 using System.Threading;
10 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Service
14 {
19  {
23  public const string Name = "tgstation-server-4";
24 
28  readonly IWatchdog watchdog;
29 
34 
38  CancellationTokenSource cancellationTokenSource;
39 
46  public ServerService(IWatchdogFactory watchdogFactory, ILoggerFactory loggerFactory, LogLevel minumumLogLevel)
47  {
48  if (watchdogFactory == null)
49  throw new ArgumentNullException(nameof(watchdogFactory));
50  if (loggerFactory == null)
51  throw new ArgumentNullException(nameof(loggerFactory));
52 
53  loggerFactory.AddEventLog(new EventLogSettings
54  {
55  EventLog = this,
56  Filter = (message, logLevel) => logLevel >= minumumLogLevel
57  });
58 
59  ServiceName = Name;
60  watchdog = watchdogFactory.CreateWatchdog(loggerFactory);
61  }
62 
64  public int MaxMessageSize => (int)EventLog.MaximumKilobytes * 1024;
65 
67  public void WriteEntry(string message, EventLogEntryType type, int eventID, short category) => EventLog.WriteEntry(message, type, eventID, category);
68 
75  async Task RunWatchdog(string[] args, CancellationToken cancellationToken)
76  {
77  await watchdog.RunAsync(false, args, cancellationToken).ConfigureAwait(false);
78 
79  void StopServiceAsync()
80  {
81  try
82  {
83  Task.Run(Stop, cancellationToken);
84  }
85  catch (OperationCanceledException) { }
86  catch (Exception e)
87  {
88  EventLog.WriteEntry(String.Format(CultureInfo.InvariantCulture, "Error stopping service! Exception: {0}", e));
89  }
90  }
91  StopServiceAsync();
92  }
93 
95  [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "cancellationTokenSource")]
96  protected override void Dispose(bool disposing)
97  {
98  cancellationTokenSource?.Dispose();
99  base.Dispose(disposing);
100  }
101 
103  protected override void OnStart(string[] args)
104  {
105  cancellationTokenSource?.Dispose();
106  cancellationTokenSource = new CancellationTokenSource();
107  watchdogTask = RunWatchdog(args, cancellationTokenSource.Token);
108  }
109 
111  protected override void OnStop()
112  {
113  cancellationTokenSource.Cancel();
114  watchdogTask.GetAwaiter().GetResult();
115  }
116  }
117 }
async Task RunWatchdog(string[] args, CancellationToken cancellationToken)
Executes the watchdog, stopping the service if it exits
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.Extensions.Logging.EventLog;
3 using Microsoft.Extensions.Logging.EventLog.Internal;
4 using System;
5 using System.Diagnostics;
6 using System.Diagnostics.CodeAnalysis;
7 using System.Globalization;
8 using System.ServiceProcess;
9 using System.Threading;
10 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Service
14 {
19  {
23  public const string Name = "tgstation-server-4";
24 
28  readonly IWatchdog watchdog;
29 
34 
38  CancellationTokenSource cancellationTokenSource;
39 
46  public ServerService(IWatchdogFactory watchdogFactory, ILoggerFactory loggerFactory, LogLevel minumumLogLevel)
47  {
48  if (watchdogFactory == null)
49  throw new ArgumentNullException(nameof(watchdogFactory));
50  if (loggerFactory == null)
51  throw new ArgumentNullException(nameof(loggerFactory));
52 
53  loggerFactory.AddEventLog(new EventLogSettings
54  {
55  EventLog = this,
56  Filter = (message, logLevel) => logLevel >= minumumLogLevel
57  });
58 
59  ServiceName = Name;
60  watchdog = watchdogFactory.CreateWatchdog(loggerFactory);
61  }
62 
64  public int MaxMessageSize => (int)EventLog.MaximumKilobytes * 1024;
65 
67  public void WriteEntry(string message, EventLogEntryType type, int eventID, short category) => EventLog.WriteEntry(message, type, eventID, category);
68 
75  async Task RunWatchdog(string[] args, CancellationToken cancellationToken)
76  {
77  await watchdog.RunAsync(false, args, cancellationToken).ConfigureAwait(false);
78 
79  void StopServiceAsync()
80  {
81  try
82  {
83  Task.Run(Stop, cancellationToken);
84  }
85  catch (OperationCanceledException) { }
86  catch (Exception e)
87  {
88  EventLog.WriteEntry(String.Format(CultureInfo.InvariantCulture, "Error stopping service! Exception: {0}", e));
89  }
90  }
91 
92  StopServiceAsync();
93  }
94 
96  [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "cancellationTokenSource", Justification = "IT'S DISPOSED RIGHT THERE YOU FUCCBOI!")]
97  protected override void Dispose(bool disposing)
98  {
99  cancellationTokenSource?.Dispose();
100  base.Dispose(disposing);
101  }
102 
104  protected override void OnStart(string[] args)
105  {
106  cancellationTokenSource?.Dispose();
107  cancellationTokenSource = new CancellationTokenSource();
108  watchdogTask = RunWatchdog(args, cancellationTokenSource.Token);
109  }
110 
112  protected override void OnStop()
113  {
114  cancellationTokenSource.Cancel();
115  watchdogTask.GetAwaiter().GetResult();
116  }
117  }
118 }
async Task RunWatchdog(string[] args, CancellationToken cancellationToken)
Executes the watchdog, stopping the service if it exits
IWatchdog CreateWatchdog(ILoggerFactory loggerFactory)
Create a IWatchdog
-
override void OnStart(string[] args)
+
override void OnStart(string[] args)
Task watchdogTask
The Task recieved from IWatchdog.RunAsync(bool, string[], CancellationToken) of watchdog ...
-
override void Dispose(bool disposing)
+
override void Dispose(bool disposing)
ServerService(IWatchdogFactory watchdogFactory, ILoggerFactory loggerFactory, LogLevel minumumLogLevel)
Construct a ServerService
@@ -107,7 +107,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IWatchdog watchdog
The IWatchdog for the ServerService
Represents a IWatchdog as a ServiceBase
- + diff --git a/_service_collection_extensions_8cs_source.html b/_service_collection_extensions_8cs_source.html index a31815b0c6..eba7d6bd3c 100644 --- a/_service_collection_extensions_8cs_source.html +++ b/_service_collection_extensions_8cs_source.html @@ -92,7 +92,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 System;
4 using System.Globalization;
6 
7 namespace Tgstation.Server.Host.Core
8 {
13  {
21  public static IServiceCollection UseStandardConfig<TConfig>(this IServiceCollection serviceCollection, IConfiguration configuration) where TConfig: class
22  {
23  if (serviceCollection == null)
24  throw new ArgumentNullException(nameof(serviceCollection));
25  if (configuration == null)
26  throw new ArgumentNullException(nameof(configuration));
27 
28  const string SectionFieldName = nameof(GeneralConfiguration.Section);
29 
30  var configType = typeof(TConfig);
31  var sectionField = configType.GetField(SectionFieldName);
32  if (sectionField == null)
33  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has no {1} field!", configType, SectionFieldName));
34 
35  var stringType = typeof(string);
36  if (sectionField.FieldType != stringType)
37  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has invalid {1} field type, must be {2}!", configType, SectionFieldName, stringType));
38 
39  var sectionName = (string)sectionField.GetValue(null);
40 
41  return serviceCollection.Configure<TConfig>(configuration.GetSection(sectionName));
42  }
43  }
44 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Configuration;
2 using Microsoft.Extensions.DependencyInjection;
3 using System;
4 using System.Globalization;
6 
7 namespace Tgstation.Server.Host.Core
8 {
13  {
21  public static IServiceCollection UseStandardConfig<TConfig>(this IServiceCollection serviceCollection, IConfiguration configuration) where TConfig : class
22  {
23  if (serviceCollection == null)
24  throw new ArgumentNullException(nameof(serviceCollection));
25  if (configuration == null)
26  throw new ArgumentNullException(nameof(configuration));
27 
28  const string SectionFieldName = nameof(GeneralConfiguration.Section);
29 
30  var configType = typeof(TConfig);
31  var sectionField = configType.GetField(SectionFieldName);
32  if (sectionField == null)
33  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has no {1} field!", configType, SectionFieldName));
34 
35  var stringType = typeof(string);
36  if (sectionField.FieldType != stringType)
37  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "{0} has invalid {1} field type, must be {2}!", configType, SectionFieldName, stringType));
38 
39  var sectionName = (string)sectionField.GetValue(null);
40 
41  return serviceCollection.Configure<TConfig>(configuration.GetSection(sectionName));
42  }
43  }
44 }
const string Section
The key for the Microsoft.Extensions.Configuration.IConfigurationSection the GeneralConfiguration res...
diff --git a/_session_controller_8cs_source.html b/_session_controller_8cs_source.html index 11b4ff52f6..c671d91f1e 100644 --- a/_session_controller_8cs_source.html +++ b/_session_controller_8cs_source.html @@ -92,73 +92,73 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SessionController.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Newtonsoft.Json.Serialization;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Net;
9 using System.Threading;
10 using System.Threading.Tasks;
16 
17 namespace Tgstation.Server.Host.Components.Watchdog
18 {
21  {
23  public bool IsPrimary
24  {
25  get
26  {
27  CheckDisposed();
28  return reattachInformation.IsPrimary;
29  }
30  }
31 
34  {
35  get
36  {
37  if (!Lifetime.IsCompleted)
38  throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
39  return apiValidationStatus;
40  }
41  }
42 
44  public IDmbProvider Dmb
45  {
46  get
47  {
48  CheckDisposed();
49  return reattachInformation.Dmb;
50  }
51  }
52 
54  public ushort? Port
55  {
56  get
57  {
58  CheckDisposed();
59  if (portClosedForReboot)
60  return null;
61  return reattachInformation.Port;
62  }
63  }
64 
67  {
68  get
69  {
70  CheckDisposed();
71  return reattachInformation.RebootState;
72  }
73  }
74 
76  public bool ClosePortOnReboot { get; set; }
77 
79  public bool TerminationWasRequested { get; private set; }
80 
82  public Task<LaunchResult> LaunchResult { get; }
83 
85  public Task<int> Lifetime => process.Lifetime;
86 
88  public Task OnReboot => rebootTcs.Task;
89 
94 
98  readonly IByondTopicSender byondTopicSender;
99 
104 
108  readonly IProcess process;
109 
114 
119 
123  readonly IChat chat;
124 
128  readonly ILogger<SessionController> logger;
129 
134 
138  TaskCompletionSource<bool> portAssignmentTcs;
142  ushort? nextPort;
143 
147  TaskCompletionSource<object> rebootTcs;
148 
153 
157  bool disposed;
158 
163 
167  bool released;
168 
182  public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger<SessionController> logger, DreamDaemonSecurity? launchSecurityLevel, uint? startupTimeout)
183  {
184  this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid
185  this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
186  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
187  this.process = process ?? throw new ArgumentNullException(nameof(process));
188  this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
189  this.interopContext = interopContext ?? throw new ArgumentNullException(nameof(interopContext));
190  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
191  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
192 
193  this.launchSecurityLevel = launchSecurityLevel;
194 
195  interopContext.RegisterHandler(this);
196 
197  portClosedForReboot = false;
198  disposed = false;
199  apiValidationStatus = ApiValidationStatus.NeverValidated;
200  released = false;
201 
202  rebootTcs = new TaskCompletionSource<object>();
203 
204  process.Lifetime.ContinueWith(x => chatJsonTrackingContext.Active = false, TaskScheduler.Current);
205 
206  async Task<LaunchResult> GetLaunchResult()
207  {
208  var startTime = DateTimeOffset.Now;
209  Task toAwait = process.Startup;
210 
211  if (startupTimeout.HasValue)
212  toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
213 
214  await toAwait.ConfigureAwait(false);
215 
216  var result = new LaunchResult
217  {
218  ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
219  StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
220  };
221  return result;
222  };
223  LaunchResult = GetLaunchResult();
224 
225  logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port);
226  }
227 
232 #pragma warning disable CA1821 // Remove empty Finalizers //TODO remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
233  ~SessionController() => Dispose(false);
234 #pragma warning restore CA1821 // Remove empty Finalizers
235 
237  public void Dispose()
238  {
239  Dispose(true);
240  GC.SuppressFinalize(this);
241  }
242 
244  void Dispose(bool disposing)
245  {
246  lock (this)
247  {
248  if (disposed)
249  return;
250  if (disposing)
251  {
252  if (!released)
253  {
254  process.Terminate();
255  byondLock.Dispose();
256  }
257  process.Dispose();
258  interopContext.Dispose();
259  Dmb?.Dispose(); //will be null when released
260  chatJsonTrackingContext.Dispose();
261  disposed = true;
262  }
263  else
264  {
265  if (logger != null)
266  logger.LogError("Being disposed via finalizer!");
267  if (!released)
268  if (process != null)
269  process.Terminate();
270  else if (logger != null)
271  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
272  }
273  }
274  }
275 
277  public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
278  {
279  if (command == null)
280  throw new ArgumentNullException(nameof(command));
281 
282  var query = command.Parameters;
283 
284  object content;
285  Action postRespond = null;
286  ushort? overrideResponsePort = null;
287  if (query.TryGetValue(Constants.DMParameterCommand, out var method))
288  {
289  content = new object();
290  switch (method)
291  {
293  try
294  {
295  var message = JsonConvert.DeserializeObject<Response>(command.RawJson, new JsonSerializerSettings
296  {
297  ContractResolver = new CamelCasePropertyNamesContractResolver()
298  });
299  if (message.ChannelIds == null)
300  throw new InvalidOperationException("Missing ChannelIds field!");
301  if (message.Message == null)
302  throw new InvalidOperationException("Missing Message field!");
303  await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false);
304  }
305  catch (Exception e)
306  {
307  logger.LogDebug("Exception while decoding chat message! Exception: {0}", e);
308  goto default;
309  }
310  break;
312  //currently unused, maybe in the future
313  break;
315  TerminationWasRequested = true;
316  process.Terminate();
317  return;
319  lock (this)
320  {
321  if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort))
322  {
324  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
325  content = new ErrorMessage { Message = "Missing stringified port as data parameter!" };
326  break;
327  }
328 
329  if (!nextPort.HasValue)
330  //not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
331  reattachInformation.Port = currentPort;
332  else
333  {
334  //nextPort is ready, tell DD to switch to that
335  //if it fails it'll kill itself
336  content = new Dictionary<string, ushort> { { Constants.DMParameterData, nextPort.Value } };
337  reattachInformation.Port = nextPort.Value;
338  overrideResponsePort = currentPort;
339  nextPort = null;
340 
341  //we'll also get here from SetPort so complete that task
342  var tmpTcs = portAssignmentTcs;
343  portAssignmentTcs = null;
344  if (tmpTcs != null)
345  postRespond = () => tmpTcs.SetResult(true);
346  }
347 
348  portClosedForReboot = false;
349  }
350  break;
352  if (!launchSecurityLevel.HasValue)
353  {
354  logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!");
355  apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest;
356  content = new ErrorMessage { Message = "Invalid API validation request!" };
357  break;
358  }
359  if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse<DreamDaemonSecurity>(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel))
360  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
361  else
362  switch (minimumSecurityLevel)
363  {
364  case DreamDaemonSecurity.Safe:
365  apiValidationStatus = ApiValidationStatus.RequiresSafe;
366  break;
367  case DreamDaemonSecurity.Ultrasafe:
368  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
369  break;
370  case DreamDaemonSecurity.Trusted:
371  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
372  break;
373  default:
374  throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!");
375  }
376  break;
378  if (ClosePortOnReboot)
379  {
380  chatJsonTrackingContext.Active = false;
381  content = new Dictionary<string, int> { { Constants.DMParameterData, 0 } };
382  portClosedForReboot = true;
383  }
384  var oldTcs = rebootTcs;
385  rebootTcs = new TaskCompletionSource<object>();
386  postRespond = () => oldTcs.SetResult(null);
387  break;
388  default:
389  content = new ErrorMessage { Message = "Requested command not supported!" };
390  break;
391  }
392  }
393  else
394  content = new ErrorMessage { Message = "Missing command parameter!" };
395 
396  var json = JsonConvert.SerializeObject(content);
397  var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false);
398 
399  if (response != Constants.DMResponseSuccess)
400  logger.LogWarning("Received error response while responding to interop: {0}", response);
401 
402  postRespond?.Invoke();
403  }
404 
409  {
410  if (disposed)
411  throw new ObjectDisposedException(nameof(SessionController));
412  }
413 
415  public void EnableCustomChatCommands() => chatJsonTrackingContext.Active = true;
416 
419  {
420  CheckDisposed();
421  //we still don't want to dispose the dmb yet, even though we're keeping it alive
422  var tmpProvider = reattachInformation.Dmb;
423  reattachInformation.Dmb = null;
424  released = true;
425  Dispose();
426  byondLock.DoNotDeleteThisSession();
427  tmpProvider.KeepAlive();
428  reattachInformation.Dmb = tmpProvider;
429  return reattachInformation;
430  }
431 
433  public Task<string> SendCommand(string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken);
434 
435  async Task<string> SendCommand(string command, ushort? overridePort, CancellationToken cancellationToken)
436  {
437  try
438  {
439  var commandString = String.Format(CultureInfo.InvariantCulture,
440  "?{0}={1}&{2}={3}",
441  byondTopicSender.SanitizeString(Constants.DMInteropAccessIdentifier),
442  byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier),
443  byondTopicSender.SanitizeString(Constants.DMParameterCommand),
444  //intentionally don't sanitize command, that's up to the caller
445  command);
446 
447  var targetPort = overridePort ?? reattachInformation.Port;
448  logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString);
449 
450  return await byondTopicSender.SendTopic(
451  new IPEndPoint(IPAddress.Loopback, targetPort),
452  commandString,
453  cancellationToken).ConfigureAwait(false);
454  }
455  catch (OperationCanceledException)
456  {
457  throw;
458  }
459  catch (Exception e)
460  {
461  logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message);
462  return null;
463  }
464  }
465 
467  public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
468  {
469  CheckDisposed();
470 
471  if (port == 0)
472  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
473 
474  async Task<bool> ImmediateTopicPortChange()
475  {
476  var commandResult = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangePort), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(false);
477 
478  if (commandResult != Constants.DMResponseSuccess)
479  {
480  logger.LogWarning("Failed port change! DD says: {0}", commandResult);
481  return false;
482  }
483 
484  reattachInformation.Port = port;
485  return true;
486  }
487 
488  lock (this)
489  if (portClosedForReboot)
490  {
491  if (portAssignmentTcs != null)
492  throw new InvalidOperationException("A port change operation is already in progress!");
493  nextPort = port;
494  portAssignmentTcs = new TaskCompletionSource<bool>();
495  return portAssignmentTcs.Task;
496  }
497  else
498  return ImmediateTopicPortChange();
499  }
500 
502  public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
503  {
504  if (RebootState == newRebootState)
505  return true;
506  reattachInformation.RebootState = newRebootState;
507  return await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangeReboot), byondTopicSender.SanitizeString(Constants.DMParameterData), (int)newRebootState), cancellationToken).ConfigureAwait(false) == Constants.DMResponseSuccess;
508  }
509 
511  public void ResetRebootState()
512  {
513  CheckDisposed();
514  reattachInformation.RebootState = RebootState.Normal;
515  }
516 
518  public void SetHighPriority() => process.SetHighPriority();
519  }
520 }
bool disposed
If the SessionController has been disposed
- +Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Newtonsoft.Json.Serialization;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Net;
9 using System.Threading;
10 using System.Threading.Tasks;
16 
17 namespace Tgstation.Server.Host.Components.Watchdog
18 {
21  {
23  public bool IsPrimary
24  {
25  get
26  {
27  CheckDisposed();
28  return reattachInformation.IsPrimary;
29  }
30  }
31 
34  {
35  get
36  {
37  if (!Lifetime.IsCompleted)
38  throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
39  return apiValidationStatus;
40  }
41  }
42 
44  public IDmbProvider Dmb
45  {
46  get
47  {
48  CheckDisposed();
49  return reattachInformation.Dmb;
50  }
51  }
52 
54  public ushort? Port
55  {
56  get
57  {
58  CheckDisposed();
59  if (portClosedForReboot)
60  return null;
61  return reattachInformation.Port;
62  }
63  }
64 
67  {
68  get
69  {
70  CheckDisposed();
71  return reattachInformation.RebootState;
72  }
73  }
74 
76  public bool ClosePortOnReboot { get; set; }
77 
79  public bool TerminationWasRequested { get; private set; }
80 
82  public Task<LaunchResult> LaunchResult { get; }
83 
85  public Task<int> Lifetime => process.Lifetime;
86 
88  public Task OnReboot => rebootTcs.Task;
89 
94 
98  readonly IByondTopicSender byondTopicSender;
99 
104 
108  readonly IProcess process;
109 
114 
119 
123  readonly IChat chat;
124 
128  readonly ILogger<SessionController> logger;
129 
134 
138  TaskCompletionSource<bool> portAssignmentTcs;
139 
143  ushort? nextPort;
144 
148  TaskCompletionSource<object> rebootTcs;
149 
154 
158  bool disposed;
159 
164 
168  bool released;
169 
183  public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger<SessionController> logger, DreamDaemonSecurity? launchSecurityLevel, uint? startupTimeout)
184  {
185  this.chatJsonTrackingContext = chatJsonTrackingContext; // null valid
186  this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
187  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
188  this.process = process ?? throw new ArgumentNullException(nameof(process));
189  this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
190  this.interopContext = interopContext ?? throw new ArgumentNullException(nameof(interopContext));
191  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
192  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
193 
194  this.launchSecurityLevel = launchSecurityLevel;
195 
196  interopContext.RegisterHandler(this);
197 
198  portClosedForReboot = false;
199  disposed = false;
200  apiValidationStatus = ApiValidationStatus.NeverValidated;
201  released = false;
202 
203  rebootTcs = new TaskCompletionSource<object>();
204 
205  process.Lifetime.ContinueWith(x => chatJsonTrackingContext.Active = false, TaskScheduler.Current);
206 
207  async Task<LaunchResult> GetLaunchResult()
208  {
209  var startTime = DateTimeOffset.Now;
210  Task toAwait = process.Startup;
211 
212  if (startupTimeout.HasValue)
213  toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
214 
215  await toAwait.ConfigureAwait(false);
216 
217  var result = new LaunchResult
218  {
219  ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
220  StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
221  };
222  return result;
223  }
224 
225  LaunchResult = GetLaunchResult();
226 
227  logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port);
228  }
229 
234 #pragma warning disable CA1821 // Remove empty Finalizers TODO: remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
235  ~SessionController() => Dispose(false);
236 #pragma warning restore CA1821 // Remove empty Finalizers
237 
239  public void Dispose()
240  {
241  Dispose(true);
242  GC.SuppressFinalize(this);
243  }
244 
249  void Dispose(bool disposing)
250  {
251  lock (this)
252  {
253  if (disposed)
254  return;
255  if (disposing)
256  {
257  if (!released)
258  {
259  process.Terminate();
260  byondLock.Dispose();
261  }
262 
263  process.Dispose();
264  interopContext.Dispose();
265  Dmb?.Dispose(); // will be null when released
266  chatJsonTrackingContext.Dispose();
267  disposed = true;
268  }
269  else
270  {
271  if (logger != null)
272  logger.LogError("Being disposed via finalizer!");
273  if (!released)
274  if (process != null)
275  process.Terminate();
276  else if (logger != null)
277  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
278  }
279  }
280  }
281 
283  #pragma warning disable CA1502 // TODO: Decomplexify
284  public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
285  {
286  if (command == null)
287  throw new ArgumentNullException(nameof(command));
288 
289  var query = command.Parameters;
290 
291  object content;
292  Action postRespond = null;
293  ushort? overrideResponsePort = null;
294  if (query.TryGetValue(Constants.DMParameterCommand, out var method))
295  {
296  content = new object();
297  switch (method)
298  {
300  try
301  {
302  var message = JsonConvert.DeserializeObject<Response>(command.RawJson, new JsonSerializerSettings
303  {
304  ContractResolver = new CamelCasePropertyNamesContractResolver()
305  });
306  if (message.ChannelIds == null)
307  throw new InvalidOperationException("Missing ChannelIds field!");
308  if (message.Message == null)
309  throw new InvalidOperationException("Missing Message field!");
310  await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false);
311  }
312  catch (Exception e)
313  {
314  logger.LogDebug("Exception while decoding chat message! Exception: {0}", e);
315  goto default;
316  }
317 
318  break;
320  // currently unused, maybe in the future
321  break;
323  TerminationWasRequested = true;
324  process.Terminate();
325  return;
327  lock (this)
328  {
329  if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort))
330  {
332  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
333  content = new ErrorMessage { Message = "Missing stringified port as data parameter!" };
334  break;
335  }
336 
337  if (!nextPort.HasValue)
338  reattachInformation.Port = currentPort; // 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
339  else
340  {
341  // nextPort is ready, tell DD to switch to that
342  // if it fails it'll kill itself
343  content = new Dictionary<string, ushort> { { Constants.DMParameterData, nextPort.Value } };
344  reattachInformation.Port = nextPort.Value;
345  overrideResponsePort = currentPort;
346  nextPort = null;
347 
348  // we'll also get here from SetPort so complete that task
349  var tmpTcs = portAssignmentTcs;
350  portAssignmentTcs = null;
351  if (tmpTcs != null)
352  postRespond = () => tmpTcs.SetResult(true);
353  }
354 
355  portClosedForReboot = false;
356  }
357 
358  break;
360  if (!launchSecurityLevel.HasValue)
361  {
362  logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!");
363  apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest;
364  content = new ErrorMessage { Message = "Invalid API validation request!" };
365  break;
366  }
367 
368  if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse<DreamDaemonSecurity>(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel))
369  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
370  else
371  switch (minimumSecurityLevel)
372  {
373  case DreamDaemonSecurity.Safe:
374  apiValidationStatus = ApiValidationStatus.RequiresSafe;
375  break;
376  case DreamDaemonSecurity.Ultrasafe:
377  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
378  break;
379  case DreamDaemonSecurity.Trusted:
380  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
381  break;
382  default:
383  throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!");
384  }
385 
386  break;
388  if (ClosePortOnReboot)
389  {
390  chatJsonTrackingContext.Active = false;
391  content = new Dictionary<string, int> { { Constants.DMParameterData, 0 } };
392  portClosedForReboot = true;
393  }
394 
395  var oldTcs = rebootTcs;
396  rebootTcs = new TaskCompletionSource<object>();
397  postRespond = () => oldTcs.SetResult(null);
398  break;
399  default:
400  content = new ErrorMessage { Message = "Requested command not supported!" };
401  break;
402  }
403  }
404  else
405  content = new ErrorMessage { Message = "Missing command parameter!" };
406 
407  var json = JsonConvert.SerializeObject(content);
408  var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false);
409 
410  if (response != Constants.DMResponseSuccess)
411  logger.LogWarning("Received error response while responding to interop: {0}", response);
412 
413  postRespond?.Invoke();
414  }
415  #pragma warning restore CA1502
416 
421  {
422  if (disposed)
423  throw new ObjectDisposedException(nameof(SessionController));
424  }
425 
427  public void EnableCustomChatCommands() => chatJsonTrackingContext.Active = true;
428 
431  {
432  CheckDisposed();
433 
434  // we still don't want to dispose the dmb yet, even though we're keeping it alive
435  var tmpProvider = reattachInformation.Dmb;
436  reattachInformation.Dmb = null;
437  released = true;
438  Dispose();
439  byondLock.DoNotDeleteThisSession();
440  tmpProvider.KeepAlive();
441  reattachInformation.Dmb = tmpProvider;
442  return reattachInformation;
443  }
444 
446  public Task<string> SendCommand(string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken);
447 
448  async Task<string> SendCommand(string command, ushort? overridePort, CancellationToken cancellationToken)
449  {
450  try
451  {
452  var commandString = String.Format(CultureInfo.InvariantCulture,
453  "?{0}={1}&{2}={3}",
454  byondTopicSender.SanitizeString(Constants.DMInteropAccessIdentifier),
455  byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier),
456  byondTopicSender.SanitizeString(Constants.DMParameterCommand),
457  command); // intentionally don't sanitize command, that's up to the caller
458 
459  var targetPort = overridePort ?? reattachInformation.Port;
460  logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString);
461 
462  return await byondTopicSender.SendTopic(
463  new IPEndPoint(IPAddress.Loopback, targetPort),
464  commandString,
465  cancellationToken).ConfigureAwait(false);
466  }
467  catch (OperationCanceledException)
468  {
469  throw;
470  }
471  catch (Exception e)
472  {
473  logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message);
474  return null;
475  }
476  }
477 
479  public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
480  {
481  CheckDisposed();
482 
483  if (port == 0)
484  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
485 
486  async Task<bool> ImmediateTopicPortChange()
487  {
488  var commandResult = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangePort), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(false);
489 
490  if (commandResult != Constants.DMResponseSuccess)
491  {
492  logger.LogWarning("Failed port change! DD says: {0}", commandResult);
493  return false;
494  }
495 
496  reattachInformation.Port = port;
497  return true;
498  }
499 
500  lock (this)
501  if (portClosedForReboot)
502  {
503  if (portAssignmentTcs != null)
504  throw new InvalidOperationException("A port change operation is already in progress!");
505  nextPort = port;
506  portAssignmentTcs = new TaskCompletionSource<bool>();
507  return portAssignmentTcs.Task;
508  }
509  else
510  return ImmediateTopicPortChange();
511  }
512 
514  public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
515  {
516  if (RebootState == newRebootState)
517  return true;
518  reattachInformation.RebootState = newRebootState;
519  return await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangeReboot), byondTopicSender.SanitizeString(Constants.DMParameterData), (int)newRebootState), cancellationToken).ConfigureAwait(false) == Constants.DMResponseSuccess;
520  }
521 
523  public void ResetRebootState()
524  {
525  CheckDisposed();
526  reattachInformation.RebootState = RebootState.Normal;
527  }
528 
530  public void SetHighPriority() => process.SetHighPriority();
531  }
532 }
bool disposed
If the SessionController has been disposed
+
const string DMCommandWorldReboot
World reboot command
Definition: Constants.cs:71
readonly ICommContext interopContext
The ICommContext for the SessionController
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
IReadOnlyDictionary< string, object > Parameters
The dictionary of the CommCommand
Definition: CommCommand.cs:13
Represents the result of trying to start a DD process
Definition: LaunchResult.cs:9
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
-
Task< bool > SetPort(ushort port, CancellationToken cancellationToken)
Causes the world to start listening on a newPort
+
Task< bool > SetPort(ushort port, CancellationToken cancellationToken)
Causes the world to start listening on a newPort
readonly IProcess process
The IProcess for the SessionController
-
ReattachInformation Release()
Releases the IProcess without terminating it. Also calls System.IDisposable.Dispose ...
+
ReattachInformation Release()
Releases the IProcess without terminating it. Also calls System.IDisposable.Dispose ...
Represents a registration of an interop session
Definition: ICommContext.cs:8
readonly IJsonTrackingContext chatJsonTrackingContext
The IJsonTrackingContext for the SessionController
-
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
bool Active
If the IJsonTrackingContext should be used for GetCustomCommands(CancellationToken) ...
- +
readonly IByondExecutableLock byondLock
The IByondExecutableLock for the SessionController
- +
void Dispose(bool disposing)
Implements the IDisposable pattern
readonly IChat chat
The IChat for the SessionController
Task Startup
The Task representing the time until the IProcess becomes "idle"
Definition: IProcess.cs:19
For managing connected chat services
Definition: IChat.cs:13
void RegisterHandler(ICommHandler handler)
Register a handler with the ICommContext
-
SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger< SessionController > logger, DreamDaemonSecurity?launchSecurityLevel, uint?startupTimeout)
Construct a SessionController
+
SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger< SessionController > logger, DreamDaemonSecurity?launchSecurityLevel, uint?startupTimeout)
Construct a SessionController
Handles communication with a DreamDaemon IProcess
string RawJson
The raw JSON of the CommCommand
Definition: CommCommand.cs:18
readonly DreamDaemonSecurity launchSecurityLevel
The DreamDaemonSecurity level the process was launched with
TaskCompletionSource< bool > portAssignmentTcs
The TaskCompletionSource<TResult> SetPort(ushort, CancellationToken) waits on when DreamDaemon curren...
- -
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
- -
async Task< string > SendCommand(string command, ushort?overridePort, CancellationToken cancellationToken)
- - -
ushort nextPort
The port to assign DreamDaemon when it queries for it
+
const string DMCommandEndProcess
Terminate process command
Definition: Constants.cs:76
+
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
+
const string DMCommandApiValidate
API validation command
Definition: Constants.cs:61
+
async Task< string > SendCommand(string command, ushort?overridePort, CancellationToken cancellationToken)
+ +
const string DMTopicInteropResponse
Response to an interop export from DM
Definition: Constants.cs:51
+
ushort nextPort
The port to assign DreamDaemon when it queries for it
readonly ILogger< SessionController > logger
The ILogger for the SessionController
-
TaskCompletionSource< object > rebootTcs
The TaskCompletionSource<TResult> that completes when DD tells us about a reboot
- +
TaskCompletionSource< object > rebootTcs
The TaskCompletionSource<TResult> that completes when DD tells us about a reboot
+
const string DMCommandServerPrimed
Server primed command
Definition: Constants.cs:66
Abstraction over a System.Diagnostics.Process
Definition: IProcess.cs:9
Represents a tracking of dynamic chat json files
- - + +
Constants used for communication with the DMAPI
Definition: Constants.cs:6
- +
const string DMResponseSuccess
Generic OK response
Definition: Constants.cs:26
Represents usage of the two primary BYOND server executables
- -
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController
- +
const string DMParameterCommand
Topic command parameter
Definition: Constants.cs:86
+
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController
+
const string DMCommandNewPort
Set port command
Definition: Constants.cs:56
-
void ResetRebootState()
Changes RebootState to Components.Watchdog.RebootState.Normal without telling the DMAPI ...
+
void ResetRebootState()
Changes RebootState to Components.Watchdog.RebootState.Normal without telling the DMAPI ...
Represents a BYOND installation
Definition: Byond.cs:8
- +
const string DMInteropAccessIdentifier
The JsonFile.AccessIdentifier
Definition: Constants.cs:21
DreamDaemonSecurity
DreamDaemon&#39;s security level
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
-
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 DMTopicChangeReboot
Change reboot mode
Definition: Constants.cs:36
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the SessionController
- -
async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
Handle a command
+
const string DMCommandChat
Chat send command
Definition: Constants.cs:81
+
async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
Handle a command
Represents an error message returned by the server
Definition: ErrorMessage.cs:8
@@ -167,7 +167,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiValidationStatus
Status of DMAPI validation
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance
-
bool released
If process should be kept alive instead
+
bool released
If process should be kept alive instead
Represents a chat message requested by DD
Definition: Response.cs:8
diff --git a/_session_controller_factory_8cs_source.html b/_session_controller_factory_8cs_source.html index 53b974a8ad..844ff7f54a 100644 --- a/_session_controller_factory_8cs_source.html +++ b/_session_controller_factory_8cs_source.html @@ -92,18 +92,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SessionControllerFactory.cs
-Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Newtonsoft.Json.Serialization;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Runtime.InteropServices;
9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
18 using Tgstation.Server.Host.IO;
20 
21 namespace Tgstation.Server.Host.Components.Watchdog
22 {
25  {
30 
34  readonly IByondManager byond;
35 
39  readonly IByondTopicSender byondTopicSender;
40 
45 
50 
55 
59  readonly IChat chat;
60 
65 
70 
74  readonly ILoggerFactory loggerFactory;
75 
79  readonly Api.Models.Instance instance;
80 
86  static string SecurityWord(DreamDaemonSecurity securityLevel)
87  {
88  switch (securityLevel)
89  {
90  case DreamDaemonSecurity.Safe:
91  return "safe";
92  case DreamDaemonSecurity.Trusted:
93  return "trusted";
94  case DreamDaemonSecurity.Ultrasafe:
95  return "ultrasafe";
96  default:
97  throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel));
98  }
99  }
100 
115  public SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, INetworkPromptReaper networkPromptReaper, IPlatformIdentifier platformIdentifier, ILoggerFactory loggerFactory, Api.Models.Instance instance)
116  {
117  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
118  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
119  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
120  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
121  this.application = application ?? throw new ArgumentNullException(nameof(application));
122  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
123  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
124  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
125  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
126  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
127  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
128  }
129 
131  public async Task<ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
132  {
133  var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort;
134  if (!portToUse.HasValue)
135  throw new InvalidOperationException("Given port is null!");
136  var accessIdentifier = cryptographySuite.GetSecureString();
137 
138  const string JsonPostfix = "tgs.json";
139 
140  var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory;
141  //delete all previous tgs json files
142  var files = await ioManager.GetFilesWithExtension(basePath, JsonPostfix, cancellationToken).ConfigureAwait(false);
143 
144  await Task.WhenAll(files.Select(x => ioManager.DeleteFile(x, cancellationToken))).ConfigureAwait(false);
145 
146  //i changed this back from guids, hopefully i don't regret that
147  string JsonFile(string name) => String.Format(CultureInfo.InvariantCulture, "{0}.{1}", name, JsonPostfix);
148 
149  var securityLevelToUse = launchParameters.SecurityLevel.Value;
150  switch (dmbProvider.CompileJob.MinimumSecurityLevel)
151  {
152  case DreamDaemonSecurity.Ultrasafe:
153  break;
154  case DreamDaemonSecurity.Safe:
155  if (securityLevelToUse == DreamDaemonSecurity.Ultrasafe)
156  securityLevelToUse = DreamDaemonSecurity.Safe;
157  break;
158  case DreamDaemonSecurity.Trusted:
159  securityLevelToUse = DreamDaemonSecurity.Trusted;
160  break;
161  default:
162  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel));
163  }
164 
165  //setup interop files
166  var interopInfo = new JsonFile
167  {
168  AccessIdentifier = accessIdentifier,
169  ApiValidateOnly = apiValidate,
170  ChatChannelsJson = JsonFile("chat_channels"),
171  ChatCommandsJson = JsonFile("chat_commands"),
172  ServerCommandsJson = JsonFile("server_commands"),
173  InstanceName = instance.Name,
174  SecurityLevel = securityLevelToUse,
175  Revision = new Api.Models.Internal.RevisionInformation
176  {
177  CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha,
178  OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha
179  }
180  };
181 
182  interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new Interop.TestMerge(x, interopInfo.Revision)));
183 
184  var interopJsonFile = JsonFile("interop");
185 
186  var interopJson = JsonConvert.SerializeObject(interopInfo, new JsonSerializerSettings
187  {
188  ContractResolver = new CamelCasePropertyNamesContractResolver(),
189  ReferenceLoopHandling = ReferenceLoopHandling.Ignore
190  });
191 
192  var localIoManager = new ResolvingIOManager(ioManager, basePath);
193 
194  var chatJsonTrackingTask = chat.TrackJsons(basePath, interopInfo.ChatChannelsJson, interopInfo.ChatCommandsJson, cancellationToken);
195 
196  await localIoManager.WriteAllBytes(interopJsonFile, Encoding.UTF8.GetBytes(interopJson), cancellationToken).ConfigureAwait(false);
197  var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false);
198  try
199  {
200  //get the byond lock
201  var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
202  try
203  {
204  //create interop context
205  var context = new CommContext(ioManager, loggerFactory.CreateLogger<CommContext>(), basePath, interopInfo.ServerCommandsJson);
206  try
207  {
208  //set command line options
209  //more sanitization here cause it uses the same scheme
210  var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", byondTopicSender.SanitizeString(application.Version.ToString()), byondTopicSender.SanitizeString(interopJsonFile), byondTopicSender.SanitizeString(Constants.DMParamHostVersion), byondTopicSender.SanitizeString(Constants.DMParamInfoJson));
211 
212  //important to run on all ports to allow port changing
213  var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -verbose -public -params \"{4}\"",
214  dmbProvider.DmbName,
215  primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort,
216  launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
217  SecurityWord(securityLevelToUse),
218  parameters);
219 
220  //See #719
221  var noShellExecute = !platformIdentifier.IsWindows;
222  //launch dd
223  var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute);
224  try
225  {
226  networkPromptReaper.RegisterProcess(process);
227 
228  //return the session controller for it
229  var result = new SessionController(new ReattachInformation
230  {
231  AccessIdentifier = accessIdentifier,
232  Dmb = dmbProvider,
233  IsPrimary = primaryDirectory,
234  Port = portToUse.Value,
235  ProcessId = process.Id,
236  ChatChannelsJson = interopInfo.ChatChannelsJson,
237  ChatCommandsJson = interopInfo.ChatCommandsJson,
238  ServerCommandsJson = interopInfo.ServerCommandsJson,
239  }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), launchParameters.SecurityLevel, launchParameters.StartupTimeout);
240 
241  //writeback launch parameter's fixed security level
242  launchParameters.SecurityLevel = securityLevelToUse;
243 
244  return result;
245  }
246  catch
247  {
248  process.Dispose();
249  throw;
250  }
251  }
252  catch
253  {
254  context.Dispose();
255  throw;
256  }
257  }
258  catch
259  {
260  if (currentByondLock == null)
261  byondLock.Dispose();
262  throw;
263  }
264  }
265  catch
266  {
267  chatJsonTrackingContext.Dispose();
268  throw;
269  }
270  }
271 
273  public async Task<ISessionController> Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
274  {
275  if (reattachInformation == null)
276  throw new ArgumentNullException(nameof(reattachInformation));
277 
278  SessionController result = null;
279  var basePath = reattachInformation.IsPrimary ? reattachInformation.Dmb.PrimaryDirectory : reattachInformation.Dmb.SecondaryDirectory;
280  var chatJsonTrackingContext = await chat.TrackJsons(basePath, reattachInformation.ChatChannelsJson, reattachInformation.ChatCommandsJson, cancellationToken).ConfigureAwait(false);
281  try
282  {
283  var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
284  try
285  {
286  var context = new CommContext(ioManager, loggerFactory.CreateLogger<CommContext>(), basePath, reattachInformation.ServerCommandsJson);
287  try
288  {
289  var process = processExecutor.GetProcess(reattachInformation.ProcessId);
290 
291  if (process != null)
292  try
293  {
294  networkPromptReaper.RegisterProcess(process);
295  result = new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), null, null);
296  }
297  finally
298  {
299  if (result == null)
300  process.Dispose();
301  }
302  }
303  finally
304  {
305  if (result == null)
306  context.Dispose();
307  }
308  }
309  finally
310  {
311  if (result == null)
312  byondLock.Dispose();
313  }
314  }
315  finally
316  {
317  if (result == null)
318  chatJsonTrackingContext.Dispose();
319  }
320  return result;
321  }
322 
324  public ISessionController CreateDeadSession(IDmbProvider dmbProvider) => new DeadSessionController(dmbProvider);
325  }
326 }
string ServerCommandsJson
Path to the server commands json file
-
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the SessionControllerFactory
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Newtonsoft.Json.Serialization;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
17 using Tgstation.Server.Host.IO;
19 
20 namespace Tgstation.Server.Host.Components.Watchdog
21 {
24  {
29 
33  readonly IByondManager byond;
34 
38  readonly IByondTopicSender byondTopicSender;
39 
44 
49 
54 
58  readonly IChat chat;
59 
64 
69 
73  readonly ILoggerFactory loggerFactory;
74 
78  readonly Api.Models.Instance instance;
79 
85  static string SecurityWord(DreamDaemonSecurity securityLevel)
86  {
87  switch (securityLevel)
88  {
89  case DreamDaemonSecurity.Safe:
90  return "safe";
91  case DreamDaemonSecurity.Trusted:
92  return "trusted";
93  case DreamDaemonSecurity.Ultrasafe:
94  return "ultrasafe";
95  default:
96  throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel));
97  }
98  }
99 
114  public SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, INetworkPromptReaper networkPromptReaper, IPlatformIdentifier platformIdentifier, ILoggerFactory loggerFactory, Api.Models.Instance instance)
115  {
116  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
117  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
118  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
119  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
120  this.application = application ?? throw new ArgumentNullException(nameof(application));
121  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
122  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
123  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
124  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
125  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
126  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
127  }
128 
130  #pragma warning disable CA1506 // TODO: Decomplexify
131  public async Task<ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
132  {
133  var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort;
134  if (!portToUse.HasValue)
135  throw new InvalidOperationException("Given port is null!");
136  var accessIdentifier = cryptographySuite.GetSecureString();
137 
138  const string JsonPostfix = "tgs.json";
139 
140  var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory;
141 
142  // delete all previous tgs json files
143  var files = await ioManager.GetFilesWithExtension(basePath, JsonPostfix, cancellationToken).ConfigureAwait(false);
144  await Task.WhenAll(files.Select(x => ioManager.DeleteFile(x, cancellationToken))).ConfigureAwait(false);
145 
146  // i changed this back from guids, hopefully i don't regret that
147  string JsonFile(string name) => String.Format(CultureInfo.InvariantCulture, "{0}.{1}", name, JsonPostfix);
148 
149  var securityLevelToUse = launchParameters.SecurityLevel.Value;
150  switch (dmbProvider.CompileJob.MinimumSecurityLevel)
151  {
152  case DreamDaemonSecurity.Ultrasafe:
153  break;
154  case DreamDaemonSecurity.Safe:
155  if (securityLevelToUse == DreamDaemonSecurity.Ultrasafe)
156  securityLevelToUse = DreamDaemonSecurity.Safe;
157  break;
158  case DreamDaemonSecurity.Trusted:
159  securityLevelToUse = DreamDaemonSecurity.Trusted;
160  break;
161  default:
162  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel));
163  }
164 
165  // setup interop files
166  var interopInfo = new JsonFile
167  {
168  AccessIdentifier = accessIdentifier,
169  ApiValidateOnly = apiValidate,
170  ChatChannelsJson = JsonFile("chat_channels"),
171  ChatCommandsJson = JsonFile("chat_commands"),
172  ServerCommandsJson = JsonFile("server_commands"),
173  InstanceName = instance.Name,
174  SecurityLevel = securityLevelToUse,
175  Revision = new Api.Models.Internal.RevisionInformation
176  {
177  CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha,
178  OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha
179  }
180  };
181 
182  interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new Interop.TestMerge(x, interopInfo.Revision)));
183 
184  var interopJsonFile = JsonFile("interop");
185 
186  var interopJson = JsonConvert.SerializeObject(interopInfo, new JsonSerializerSettings
187  {
188  ContractResolver = new CamelCasePropertyNamesContractResolver(),
189  ReferenceLoopHandling = ReferenceLoopHandling.Ignore
190  });
191 
192  var localIoManager = new ResolvingIOManager(ioManager, basePath);
193 
194  var chatJsonTrackingTask = chat.TrackJsons(basePath, interopInfo.ChatChannelsJson, interopInfo.ChatCommandsJson, cancellationToken);
195 
196  await localIoManager.WriteAllBytes(interopJsonFile, Encoding.UTF8.GetBytes(interopJson), cancellationToken).ConfigureAwait(false);
197  var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false);
198  try
199  {
200  // get the byond lock
201  var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
202  try
203  {
204  // create interop context
205  var context = new CommContext(ioManager, loggerFactory.CreateLogger<CommContext>(), basePath, interopInfo.ServerCommandsJson);
206  try
207  {
208  // set command line options
209  // more sanitization here cause it uses the same scheme
210  var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", byondTopicSender.SanitizeString(application.Version.ToString()), byondTopicSender.SanitizeString(interopJsonFile), byondTopicSender.SanitizeString(Constants.DMParamHostVersion), byondTopicSender.SanitizeString(Constants.DMParamInfoJson));
211 
212  // important to run on all ports to allow port changing
213  var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -verbose -public -params \"{4}\"",
214  dmbProvider.DmbName,
215  primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort,
216  launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
217  SecurityWord(securityLevelToUse),
218  parameters);
219 
220  // See https://github.com/tgstation/tgstation-server/issues/719
221  var noShellExecute = !platformIdentifier.IsWindows;
222 
223  // launch dd
224  var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute);
225  try
226  {
227  networkPromptReaper.RegisterProcess(process);
228 
229  // return the session controller for it
230  var result = new SessionController(new ReattachInformation
231  {
232  AccessIdentifier = accessIdentifier,
233  Dmb = dmbProvider,
234  IsPrimary = primaryDirectory,
235  Port = portToUse.Value,
236  ProcessId = process.Id,
237  ChatChannelsJson = interopInfo.ChatChannelsJson,
238  ChatCommandsJson = interopInfo.ChatCommandsJson,
239  ServerCommandsJson = interopInfo.ServerCommandsJson,
240  }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), launchParameters.SecurityLevel, launchParameters.StartupTimeout);
241 
242  // writeback launch parameter's fixed security level
243  launchParameters.SecurityLevel = securityLevelToUse;
244 
245  return result;
246  }
247  catch
248  {
249  process.Dispose();
250  throw;
251  }
252  }
253  catch
254  {
255  context.Dispose();
256  throw;
257  }
258  }
259  catch
260  {
261  if (currentByondLock == null)
262  byondLock.Dispose();
263  throw;
264  }
265  }
266  catch
267  {
268  chatJsonTrackingContext.Dispose();
269  throw;
270  }
271  }
272  #pragma warning restore CA1506
273 
275  public async Task<ISessionController> Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
276  {
277  if (reattachInformation == null)
278  throw new ArgumentNullException(nameof(reattachInformation));
279 
280  SessionController result = null;
281  var basePath = reattachInformation.IsPrimary ? reattachInformation.Dmb.PrimaryDirectory : reattachInformation.Dmb.SecondaryDirectory;
282  var chatJsonTrackingContext = await chat.TrackJsons(basePath, reattachInformation.ChatChannelsJson, reattachInformation.ChatCommandsJson, cancellationToken).ConfigureAwait(false);
283  try
284  {
285  var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
286  try
287  {
288  var context = new CommContext(ioManager, loggerFactory.CreateLogger<CommContext>(), basePath, reattachInformation.ServerCommandsJson);
289  try
290  {
291  var process = processExecutor.GetProcess(reattachInformation.ProcessId);
292 
293  if (process != null)
294  try
295  {
296  networkPromptReaper.RegisterProcess(process);
297  result = new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), null, null);
298  }
299  finally
300  {
301  if (result == null)
302  process.Dispose();
303  }
304  }
305  finally
306  {
307  if (result == null)
308  context.Dispose();
309  }
310  }
311  finally
312  {
313  if (result == null)
314  byondLock.Dispose();
315  }
316  }
317  finally
318  {
319  if (result == null)
320  chatJsonTrackingContext.Dispose();
321  }
322 
323  return result;
324  }
325 
327  public ISessionController CreateDeadSession(IDmbProvider dmbProvider) => new DeadSessionController(dmbProvider);
328  }
329 }
string ServerCommandsJson
Path to the server commands json file
+
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the SessionControllerFactory
-
readonly IIOManager ioManager
The IIOManager for the SessionControllerFactory
- +
readonly IIOManager ioManager
The IIOManager for the SessionControllerFactory
+
const string DMParamInfoJson
Path to the JsonFile
Definition: Constants.cs:16
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
string DmbName
The file name of the .dmb
Definition: IDmbProvider.cs:14
-
readonly IProcessExecutor processExecutor
The IProcessExecutor for the SessionControllerFactory
+
readonly IProcessExecutor processExecutor
The IProcessExecutor for the SessionControllerFactory
An IIOManager that resolve relative paths from another IIOManager to a subdirectory of that ...
-
readonly Api.Models.Instance instance
The Api.Models.Instance for the SessionControllerFactory
-
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the SessionControllerFactory
+
readonly Api.Models.Instance instance
The Api.Models.Instance for the SessionControllerFactory
+
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the SessionControllerFactory
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
string SecondaryDirectory
The secondary game directory with a trailing directory separator
Definition: IDmbProvider.cs:24
@@ -112,7 +112,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
string ChatCommandsJson
Path to the chat commands json file
-
readonly INetworkPromptReaper networkPromptReaper
The INetworkPromptReaper for the SessionControllerFactory
+
readonly INetworkPromptReaper networkPromptReaper
The INetworkPromptReaper for the SessionControllerFactory
DreamDaemonSecurity SecurityLevel
The DreamDaemonSecurity level of DreamDaemon
@@ -123,47 +123,47 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Contains various cryptographic functions
Handles communication with a DreamDaemon IProcess
-
readonly IApplication application
The IApplication for the SessionControllerFactory
+
readonly IApplication application
The IApplication for the SessionControllerFactory
async Task< ISessionController > LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
Create a ISessionController from a freshly launch DreamDaemon instance
DreamDaemonSecurity MinimumSecurityLevel
The minimum DreamDaemonSecurity required to run the CompileJob&#39;s output
Definition: CompileJob.cs:38
Version ByondVersion
The Byond.Version the CompileJob was made with
Definition: CompileJob.cs:21
-
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the SessionControllerFactory
-
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SessionControllerFactory
-
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
+
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the SessionControllerFactory
+
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SessionControllerFactory
+
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon
For managing the BYOND installation
bool IsPrimary
If the Components.IDmbProvider.PrimaryDirectory of the associated dmb is being used ...
- +
const string DMParamHostVersion
Identifies a TGS execution. The server version
Definition: Constants.cs:11
string ChatChannelsJson
Path to the chat channels json file
- -
readonly IChat chat
The IChat for the SessionControllerFactory
+
Constants used for communication with the DMAPI
Definition: Constants.cs:6
+
readonly IChat chat
The IChat for the SessionControllerFactory
-
SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, INetworkPromptReaper networkPromptReaper, IPlatformIdentifier platformIdentifier, ILoggerFactory loggerFactory, Api.Models.Instance instance)
Construct a SessionControllerFactory
+
SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, INetworkPromptReaper networkPromptReaper, IPlatformIdentifier platformIdentifier, ILoggerFactory loggerFactory, Api.Models.Instance instance)
Construct a SessionControllerFactory
Represents usage of the two primary BYOND server executables
Represents a BYOND installation
Definition: Byond.cs:8
DreamDaemonSecurity
DreamDaemon&#39;s security level
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
- +
Interface for using filesystems
Definition: IIOManager.cs:11
-
readonly IByondManager byond
The IByondManager for the SessionControllerFactory
+
readonly IByondManager byond
The IByondManager for the SessionControllerFactory
On Windows, DreamDaemon will show an unskippable prompt when using /world/proc/OpenPort(). This looks out for those prompts and immediately clicks "Yes" if the owning process has registered for it
For identifying the current platform
-
static string SecurityWord(DreamDaemonSecurity securityLevel)
Change a given securityLevel into the appropriate DreamDaemon command line word
+
static string SecurityWord(DreamDaemonSecurity securityLevel)
Change a given securityLevel into the appropriate DreamDaemon command line word
-
string OriginCommitSha
The sha of the most recent remote commit
+
string OriginCommitSha
The sha of the most recent remote commit
ushort PrimaryPort
The first port DreamDaemon uses. This should be the publically advertised port
Parameters necessary for duplicating a ISessionController session
- + diff --git a/_setup_wizard_8cs_source.html b/_setup_wizard_8cs_source.html index 2c2e08ce32..1287eae142 100644 --- a/_setup_wizard_8cs_source.html +++ b/_setup_wizard_8cs_source.html @@ -92,9 +92,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SetupWizard.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using MySql.Data.MySqlClient;
5 using Newtonsoft.Json;
6 using System;
7 using System.Collections.Generic;
8 using System.Data.Common;
9 using System.Data.SqlClient;
10 using System.Globalization;
11 using System.Linq;
12 using System.Text;
13 using System.Threading;
14 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
17 
18 namespace Tgstation.Server.Host.Core
19 {
21  sealed class SetupWizard : ISetupWizard
22  {
27 
31  readonly IConsole console;
32 
36  readonly IHostingEnvironment hostingEnvironment;
37 
42 
47 
52 
57 
61  readonly ILogger<SetupWizard> logger;
62 
67 
80  public SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, IDBConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, ILogger<SetupWizard> logger, IOptions<GeneralConfiguration> generalConfigurationOptions)
81  {
82  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
83  this.console = console ?? throw new ArgumentNullException(nameof(console));
84  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
85  this.application = application ?? throw new ArgumentNullException(nameof(application));
86  this.dbConnectionFactory = dbConnectionFactory ?? throw new ArgumentNullException(nameof(dbConnectionFactory));
87  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
88  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
89  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
90  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
91  }
92 
99  async Task<bool> PromptYesNo(string question, CancellationToken cancellationToken)
100  {
101  do
102  {
103  await console.WriteAsync(question, false, cancellationToken).ConfigureAwait(false);
104  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
105  var upperResponse = responseString.ToUpperInvariant();
106  if (upperResponse == "Y" || upperResponse == "YES")
107  return true;
108  else if (upperResponse == "N" || upperResponse == "NO")
109  return false;
110  await console.WriteAsync("Invalid response!", true, cancellationToken).ConfigureAwait(false);
111  }
112  while (true);
113  }
114 
120  async Task<ushort?> PromptForHostingPort(CancellationToken cancellationToken)
121  {
122  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
123  await console.WriteAsync("What port would you like to connect to TGS on?", true, cancellationToken).ConfigureAwait(false);
124  await console.WriteAsync("Note: If this is a docker container with the default port already mapped, use the default.", true, cancellationToken).ConfigureAwait(false);
125 
126  do
127  {
128  await console.WriteAsync("API Port (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
129  var portString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
130  if (String.IsNullOrWhiteSpace(portString))
131  return null;
132  if (UInt16.TryParse(portString, out var port) && port != 0)
133  return port;
134  await console.WriteAsync("Invalid port! Please enter a value between 1 and 65535", true, cancellationToken).ConfigureAwait(false);
135  }
136  while (true);
137  }
138 
144  async Task<DatabaseConfiguration> ConfigureDatabase(CancellationToken cancellationToken)
145  {
146  do
147  {
148  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
149  await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
150 
151  var databaseConfiguration = new DatabaseConfiguration();
152  do
153  {
154  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Please enter one of {0}, {1}, or {2}: ", DatabaseType.MariaDB, DatabaseType.SqlServer, DatabaseType.MySql), false, cancellationToken).ConfigureAwait(false);
155  var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
156  if (Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
157  {
158  databaseConfiguration.DatabaseType = databaseType;
159  break;
160  }
161  await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
162  }
163  while (true);
164 
165  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
166  await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false);
167  var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
168  if (String.IsNullOrWhiteSpace(serverAddress))
169  serverAddress = null;
170 
171  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
172  await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
173  string databaseName;
174 
175  do
176  {
177  databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
178  if (!String.IsNullOrWhiteSpace(databaseName))
179  break;
180  await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
181  }
182  while (true);
183 
184  var dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false);
185 
186  bool useWinAuth;
187  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
188  useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
189  else
190  useWinAuth = false;
191 
192  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
193 
194  string username = null;
195  string password = null;
196  if (!useWinAuth)
197  {
198  await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
199  username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
200  await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
201  password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
202  }
203  else
204  {
205  await console.WriteAsync("IMPORTANT: If using the service runner, ensure this computer's LocalSystem account has CREATE DATABASE permissions on the target server!", true, cancellationToken).ConfigureAwait(false);
206  await console.WriteAsync("The account it uses in MSSQL is usually \"NT AUTHORITY\\SYSTEM\" and the role it needs is usually \"dbcreator\".", true, cancellationToken).ConfigureAwait(false);
207  await console.WriteAsync("We'll run a sanity test here, but it won't be indicative of the service's permissions if that is the case", true, cancellationToken).ConfigureAwait(false);
208  }
209  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
210 
211  DbConnection testConnection;
212  void CreateTestConnection(string connectionString)
213  {
214  testConnection = dbConnectionFactory.CreateConnection(connectionString, databaseConfiguration.DatabaseType);
215  }
216 
217  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
218  {
219  var csb = new SqlConnectionStringBuilder
220  {
221  ApplicationName = application.VersionPrefix,
222  DataSource = serverAddress ?? "(local)"
223  };
224  if (useWinAuth)
225  csb.IntegratedSecurity = true;
226  else
227  {
228  csb.UserID = username;
229  csb.Password = password;
230  }
231 
232  CreateTestConnection(csb.ConnectionString);
233  csb.InitialCatalog = databaseName;
234  databaseConfiguration.ConnectionString = csb.ConnectionString;
235  }
236  else
237  {
238  var csb = new MySqlConnectionStringBuilder
239  {
240  Server = serverAddress ?? "127.0.0.1",
241  UserID = username,
242  Password = password
243  };
244 
245  CreateTestConnection(csb.ConnectionString);
246  csb.Database = databaseName;
247  databaseConfiguration.ConnectionString = csb.ConnectionString;
248  }
249 
250  try
251  {
252  using (testConnection)
253  {
254  await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
255  await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
256  await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
257 
258  if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
259  {
260  await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
261  using (var command = testConnection.CreateCommand())
262  {
263  command.CommandText = "SELECT VERSION()";
264  var fullVersion = (string)(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
265  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
266  var splits = fullVersion.Split('-');
267  databaseConfiguration.MySqlServerVersion = splits[0];
268  }
269  }
270 
271  if (!dbExists)
272  {
273  await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
274  using (var command = testConnection.CreateCommand())
275  {
276  command.CommandText = String.Format(CultureInfo.InvariantCulture, "CREATE DATABASE {0}", databaseName);
277  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
278  }
279  await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
280  await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
281  using (var command = testConnection.CreateCommand())
282  {
283  command.CommandText = String.Format(CultureInfo.InvariantCulture, "DROP DATABASE {0}", databaseName);
284  try
285  {
286  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
287  }
288  catch (OperationCanceledException)
289  {
290  throw;
291  }
292  catch (Exception e)
293  {
294  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
295  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
296  await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
297  await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
298  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
299  }
300  }
301  }
302  }
303 
304  return databaseConfiguration;
305  }
306  catch (OperationCanceledException)
307  {
308  throw;
309  }
310  catch (Exception e)
311  {
312  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
313  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
314  await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
315  }
316  } while (true);
317  }
318 
324  async Task<GeneralConfiguration> ConfigureGeneral(CancellationToken cancellationToken)
325  {
326  var newGeneralConfiguration = new GeneralConfiguration
327  {
329  };
330 
331  do
332  {
333  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
334  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
335  var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
336  if (String.IsNullOrWhiteSpace(passwordLengthString))
337  break;
338  if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
339  {
340  newGeneralConfiguration.MinimumPasswordLength = passwordLength;
341  break;
342  }
343  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
344  }
345  while (true);
346 
347  do
348  {
349  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
350  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Timeout for sending and receiving BYOND topics (ms, 0 for infinite, leave blank for default of {0}): ", newGeneralConfiguration.ByondTopicTimeout), false, cancellationToken).ConfigureAwait(false);
351  var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
352  if (String.IsNullOrWhiteSpace(topicTimeoutString))
353  break;
354  if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
355  {
356  newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
357  break;
358  }
359  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
360  }
361  while (true);
362 
363  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
364  await console.WriteAsync("Enter a GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)", true, cancellationToken).ConfigureAwait(false);
365  await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
366  newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
367  if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
368  newGeneralConfiguration.GitHubAccessToken = null;
369  return newGeneralConfiguration;
370  }
371 
377  async Task<FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
378  {
379  var fileLoggingConfiguration = new FileLoggingConfiguration();
380  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
381  fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);
382 
383  if (!fileLoggingConfiguration.Disable)
384  {
385  do
386  {
387  await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
388  fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
389  if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
390  {
391  fileLoggingConfiguration.Directory = null;
392  break;
393  }
394  //test a write of it
395  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
396  await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);
397  try
398  {
399  await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);
400  var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
401  await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
402  try
403  {
404  await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
405  }
406  catch (OperationCanceledException)
407  {
408  throw;
409  }
410  catch (Exception e)
411  {
412  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);
413  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
414  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
415  }
416  break;
417  }
418  catch (OperationCanceledException)
419  {
420  throw;
421  }
422  catch (Exception e)
423  {
424  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
425  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
426  await console.WriteAsync("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
427  }
428  } while (true);
429 
430  async Task<LogLevel?> PromptLogLevel(string question)
431  {
432  do
433  {
434  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
435  await console.WriteAsync(question, true, cancellationToken).ConfigureAwait(false);
436  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Enter one of {0}/{1}/{2}/{3}/{4}/{5} (leave blank for default): ", nameof(LogLevel.Trace), nameof(LogLevel.Debug), nameof(LogLevel.Information), nameof(LogLevel.Warning), nameof(LogLevel.Error), nameof(LogLevel.Critical)), false, cancellationToken).ConfigureAwait(false);
437  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
438  if (String.IsNullOrWhiteSpace(responseString))
439  return null;
440  if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
441  return logLevel;
442  await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
443  } while (true);
444  }
445 
446  fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;
447  fileLoggingConfiguration.MicrosoftLogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for Microsoft logs (VERY verbose, default {0}).", fileLoggingConfiguration.MicrosoftLogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.MicrosoftLogLevel;
448  }
449  return fileLoggingConfiguration;
450  }
451 
457  async Task<ControlPanelConfiguration> ConfigureControlPanel(CancellationToken cancellationToken)
458  {
459  var config = new ControlPanelConfiguration
460  {
461  Enable = await PromptYesNo("Enable the web control panel? (y/n): ", cancellationToken).ConfigureAwait(false),
462  AllowAnyOrigin = await PromptYesNo("Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *) (y/n): ", cancellationToken).ConfigureAwait(false)
463  };
464 
465  if (!config.AllowAnyOrigin)
466  {
467  await console.WriteAsync("Enter a comma seperated list of CORS allowed origins (optional): ", false, cancellationToken).ConfigureAwait(false);
468  var commaSeperatedOrigins = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
469  if (!String.IsNullOrWhiteSpace(commaSeperatedOrigins))
470  {
471  var splits = commaSeperatedOrigins.Split(',');
472  config.AllowedOrigins = new List<string>(splits.Select(x => x.Trim()));
473  }
474  }
475 
476  return config;
477  }
478 
490  async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
491  {
492  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
493 
494  var map = new Dictionary<string, object>()
495  {
496  { DatabaseConfiguration.Section, databaseConfiguration },
497  { GeneralConfiguration.Section, newGeneralConfiguration },
498  { FileLoggingConfiguration.Section, fileLoggingConfiguration },
499  { ControlPanelConfiguration.Section, controlPanelConfiguration }
500  };
501 
502  if (hostingPort.HasValue)
503  map.Add("Kestrel", new
504  {
505  EndPoints = new
506  {
507  Http = new
508  {
509  Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort)
510  }
511  }
512  });
513 
514  var json = JsonConvert.SerializeObject(map, Formatting.Indented);
515  var configBytes = Encoding.UTF8.GetBytes(json);
516 
517  try
518  {
519  await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
520  }
521  catch (OperationCanceledException)
522  {
523  throw;
524  }
525  catch (Exception e)
526  {
527  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
528  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
529  await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
530  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
531  await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
532  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
533  await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
534  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
535  throw new OperationCanceledException();
536  }
537 
538  await console.WriteAsync("Waiting for configuration changes to reload...", true, cancellationToken).ConfigureAwait(false);
539 
540  //we need to wait for the configuration's file system watcher to read and reload the changes
541  await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
542  }
543 
550  async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
551  {
552  //welcome message
553  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
554  await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
555  await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
556 
557  var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
558 
559  var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
560 
561  var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
562 
563  var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false);
564 
565  var controlPanelConfiguration = await ConfigureControlPanel(cancellationToken).ConfigureAwait(false);
566 
567  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
568 
569  await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, controlPanelConfiguration, cancellationToken).ConfigureAwait(false);
570  }
571 
573  public async Task<bool> CheckRunWizard(CancellationToken cancellationToken)
574  {
575  var setupWizardMode = generalConfiguration.SetupWizardMode;
576  logger.LogTrace("Checking if setup wizard should run. SetupWizardMode: {0}", setupWizardMode);
577 
578  if (setupWizardMode == SetupWizardMode.Never)
579  {
580  logger.LogTrace("Skipping due to configuration...");
581  return false;
582  }
583 
584  var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
585  if (!console.Available)
586  {
587  if (forceRun)
588  throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!");
589  logger.LogTrace("Skipping due to console not being available...");
590  return false;
591  }
592 
593  var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
594  var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);
595 
596  bool shouldRunBasedOnAutodetect;
597  if (exists)
598  {
599  var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);
600  var contents = Encoding.UTF8.GetString(bytes);
601  var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}";
602  logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty);
603  shouldRunBasedOnAutodetect = existingConfigIsEmpty;
604  }
605  else
606  {
607  shouldRunBasedOnAutodetect = true;
608  logger.LogTrace("No configuration json detected");
609  }
610 
611 
612  if (!shouldRunBasedOnAutodetect)
613  {
614  if (forceRun)
615  {
616  logger.LogTrace("Asking user to bypass due to force run request...");
617  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
618 
619  forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
620  }
621  if (!forceRun)
622  return false;
623  }
624 
625  //flush the logs to prevent console conflicts
626  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
627 
628  await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
629  return true;
630  }
631  }
632 }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Hosting;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using MySql.Data.MySqlClient;
5 using Newtonsoft.Json;
6 using System;
7 using System.Collections.Generic;
8 using System.Data.Common;
9 using System.Data.SqlClient;
10 using System.Globalization;
11 using System.Linq;
12 using System.Text;
13 using System.Threading;
14 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
17 
18 namespace Tgstation.Server.Host.Core
19 {
21  sealed class SetupWizard : ISetupWizard
22  {
27 
31  readonly IConsole console;
32 
36  readonly IHostingEnvironment hostingEnvironment;
37 
42 
47 
52 
57 
61  readonly ILogger<SetupWizard> logger;
62 
67 
80  public SetupWizard(IIOManager ioManager, IConsole console, IHostingEnvironment hostingEnvironment, IApplication application, IDBConnectionFactory dbConnectionFactory, IPlatformIdentifier platformIdentifier, IAsyncDelayer asyncDelayer, ILogger<SetupWizard> logger, IOptions<GeneralConfiguration> generalConfigurationOptions)
81  {
82  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
83  this.console = console ?? throw new ArgumentNullException(nameof(console));
84  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
85  this.application = application ?? throw new ArgumentNullException(nameof(application));
86  this.dbConnectionFactory = dbConnectionFactory ?? throw new ArgumentNullException(nameof(dbConnectionFactory));
87  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
88  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
89  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
90  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
91  }
92 
99  async Task<bool> PromptYesNo(string question, CancellationToken cancellationToken)
100  {
101  do
102  {
103  await console.WriteAsync(question, false, cancellationToken).ConfigureAwait(false);
104  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
105  var upperResponse = responseString.ToUpperInvariant();
106  if (upperResponse == "Y" || upperResponse == "YES")
107  return true;
108  else if (upperResponse == "N" || upperResponse == "NO")
109  return false;
110  await console.WriteAsync("Invalid response!", true, cancellationToken).ConfigureAwait(false);
111  }
112  while (true);
113  }
114 
120  async Task<ushort?> PromptForHostingPort(CancellationToken cancellationToken)
121  {
122  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
123  await console.WriteAsync("What port would you like to connect to TGS on?", true, cancellationToken).ConfigureAwait(false);
124  await console.WriteAsync("Note: If this is a docker container with the default port already mapped, use the default.", true, cancellationToken).ConfigureAwait(false);
125 
126  do
127  {
128  await console.WriteAsync("API Port (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
129  var portString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
130  if (String.IsNullOrWhiteSpace(portString))
131  return null;
132  if (UInt16.TryParse(portString, out var port) && port != 0)
133  return port;
134  await console.WriteAsync("Invalid port! Please enter a value between 1 and 65535", true, cancellationToken).ConfigureAwait(false);
135  }
136  while (true);
137  }
138 
144  async Task<DatabaseConfiguration> ConfigureDatabase(CancellationToken cancellationToken)
145  {
146  do
147  {
148  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
149  await console.WriteAsync("What SQL database type will you be using?", true, cancellationToken).ConfigureAwait(false);
150 
151  var databaseConfiguration = new DatabaseConfiguration();
152  do
153  {
154  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Please enter one of {0}, {1}, or {2}: ", DatabaseType.MariaDB, DatabaseType.SqlServer, DatabaseType.MySql), false, cancellationToken).ConfigureAwait(false);
155  var databaseTypeString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
156  if (Enum.TryParse<DatabaseType>(databaseTypeString, out var databaseType))
157  {
158  databaseConfiguration.DatabaseType = databaseType;
159  break;
160  }
161 
162  await console.WriteAsync("Invalid database type!", true, cancellationToken).ConfigureAwait(false);
163  }
164  while (true);
165 
166  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
167  await console.WriteAsync("Enter the server's address and port (blank for local): ", false, cancellationToken).ConfigureAwait(false);
168  var serverAddress = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
169  if (String.IsNullOrWhiteSpace(serverAddress))
170  serverAddress = null;
171 
172  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
173  await console.WriteAsync("Enter the database name (Can be from previous installation. Otherwise, should not exist): ", false, cancellationToken).ConfigureAwait(false);
174  string databaseName;
175  do
176  {
177  databaseName = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
178  if (!String.IsNullOrWhiteSpace(databaseName))
179  break;
180  await console.WriteAsync("Invalid database name!", true, cancellationToken).ConfigureAwait(false);
181  }
182  while (true);
183 
184  var dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false);
185 
186  bool useWinAuth;
187  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer && platformIdentifier.IsWindows)
188  useWinAuth = await PromptYesNo("Use Windows Authentication? (y/n): ", cancellationToken).ConfigureAwait(false);
189  else
190  useWinAuth = false;
191 
192  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
193 
194  string username = null;
195  string password = null;
196  if (!useWinAuth)
197  {
198  await console.WriteAsync("Enter username: ", false, cancellationToken).ConfigureAwait(false);
199  username = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
200  await console.WriteAsync("Enter password: ", false, cancellationToken).ConfigureAwait(false);
201  password = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
202  }
203  else
204  {
205  await console.WriteAsync("IMPORTANT: If using the service runner, ensure this computer's LocalSystem account has CREATE DATABASE permissions on the target server!", true, cancellationToken).ConfigureAwait(false);
206  await console.WriteAsync("The account it uses in MSSQL is usually \"NT AUTHORITY\\SYSTEM\" and the role it needs is usually \"dbcreator\".", true, cancellationToken).ConfigureAwait(false);
207  await console.WriteAsync("We'll run a sanity test here, but it won't be indicative of the service's permissions if that is the case", true, cancellationToken).ConfigureAwait(false);
208  }
209 
210  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
211 
212  DbConnection testConnection;
213  void CreateTestConnection(string connectionString)
214  {
215  testConnection = dbConnectionFactory.CreateConnection(connectionString, databaseConfiguration.DatabaseType);
216  }
217 
218  if (databaseConfiguration.DatabaseType == DatabaseType.SqlServer)
219  {
220  var csb = new SqlConnectionStringBuilder
221  {
222  ApplicationName = application.VersionPrefix,
223  DataSource = serverAddress ?? "(local)"
224  };
225  if (useWinAuth)
226  csb.IntegratedSecurity = true;
227  else
228  {
229  csb.UserID = username;
230  csb.Password = password;
231  }
232 
233  CreateTestConnection(csb.ConnectionString);
234  csb.InitialCatalog = databaseName;
235  databaseConfiguration.ConnectionString = csb.ConnectionString;
236  }
237  else
238  {
239  var csb = new MySqlConnectionStringBuilder
240  {
241  Server = serverAddress ?? "127.0.0.1",
242  UserID = username,
243  Password = password
244  };
245 
246  CreateTestConnection(csb.ConnectionString);
247  csb.Database = databaseName;
248  databaseConfiguration.ConnectionString = csb.ConnectionString;
249  }
250 
251  try
252  {
253  using (testConnection)
254  {
255  await console.WriteAsync("Testing connection...", true, cancellationToken).ConfigureAwait(false);
256  await testConnection.OpenAsync(cancellationToken).ConfigureAwait(false);
257  await console.WriteAsync("Connection successful!", true, cancellationToken).ConfigureAwait(false);
258 
259  if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer)
260  {
261  await console.WriteAsync("Checking MySQL/MariaDB version...", true, cancellationToken).ConfigureAwait(false);
262  using (var command = testConnection.CreateCommand())
263  {
264  command.CommandText = "SELECT VERSION()";
265  var fullVersion = (string)await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
266  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Found {0}", fullVersion), true, cancellationToken).ConfigureAwait(false);
267  var splits = fullVersion.Split('-');
268  databaseConfiguration.MySqlServerVersion = splits[0];
269  }
270  }
271 
272  if (!dbExists)
273  {
274  await console.WriteAsync("Testing create DB permission...", true, cancellationToken).ConfigureAwait(false);
275  using (var command = testConnection.CreateCommand())
276  {
277  command.CommandText = String.Format(CultureInfo.InvariantCulture, "CREATE DATABASE {0}", databaseName);
278  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
279  }
280 
281  await console.WriteAsync("Success!", true, cancellationToken).ConfigureAwait(false);
282  await console.WriteAsync("Dropping test database...", true, cancellationToken).ConfigureAwait(false);
283  using (var command = testConnection.CreateCommand())
284  {
285  command.CommandText = String.Format(CultureInfo.InvariantCulture, "DROP DATABASE {0}", databaseName);
286  try
287  {
288  await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
289  }
290  catch (OperationCanceledException)
291  {
292  throw;
293  }
294  catch (Exception e)
295  {
296  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
297  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
298  await console.WriteAsync("This should be okay, but you may want to manually drop the database before continuing!", true, cancellationToken).ConfigureAwait(false);
299  await console.WriteAsync("Press any key to continue...", true, cancellationToken).ConfigureAwait(false);
300  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
301  }
302  }
303  }
304  }
305 
306  return databaseConfiguration;
307  }
308  catch (OperationCanceledException)
309  {
310  throw;
311  }
312  catch (Exception e)
313  {
314  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
315  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
316  await console.WriteAsync("Retrying database configuration...", true, cancellationToken).ConfigureAwait(false);
317  }
318  }
319  while (true);
320  }
321 
327  async Task<GeneralConfiguration> ConfigureGeneral(CancellationToken cancellationToken)
328  {
329  var newGeneralConfiguration = new GeneralConfiguration
330  {
332  };
333 
334  do
335  {
336  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
337  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Minimum database user password length (leave blank for default of {0}): ", newGeneralConfiguration.MinimumPasswordLength), false, cancellationToken).ConfigureAwait(false);
338  var passwordLengthString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
339  if (String.IsNullOrWhiteSpace(passwordLengthString))
340  break;
341  if (UInt32.TryParse(passwordLengthString, out var passwordLength) && passwordLength >= 0)
342  {
343  newGeneralConfiguration.MinimumPasswordLength = passwordLength;
344  break;
345  }
346 
347  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
348  }
349  while (true);
350 
351  do
352  {
353  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
354  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Timeout for sending and receiving BYOND topics (ms, 0 for infinite, leave blank for default of {0}): ", newGeneralConfiguration.ByondTopicTimeout), false, cancellationToken).ConfigureAwait(false);
355  var topicTimeoutString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
356  if (String.IsNullOrWhiteSpace(topicTimeoutString))
357  break;
358  if (Int32.TryParse(topicTimeoutString, out var topicTimeout) && topicTimeout >= 0)
359  {
360  newGeneralConfiguration.ByondTopicTimeout = topicTimeout;
361  break;
362  }
363 
364  await console.WriteAsync("Please enter a positive integer!", true, cancellationToken).ConfigureAwait(false);
365  }
366  while (true);
367 
368  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
369  await console.WriteAsync("Enter a GitHub personal access token to bypass some rate limits (this is optional and does not require any scopes)", true, cancellationToken).ConfigureAwait(false);
370  await console.WriteAsync("GitHub personal access token: ", false, cancellationToken).ConfigureAwait(false);
371  newGeneralConfiguration.GitHubAccessToken = await console.ReadLineAsync(true, cancellationToken).ConfigureAwait(false);
372  if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
373  newGeneralConfiguration.GitHubAccessToken = null;
374  return newGeneralConfiguration;
375  }
376 
382  async Task<FileLoggingConfiguration> ConfigureLogging(CancellationToken cancellationToken)
383  {
384  var fileLoggingConfiguration = new FileLoggingConfiguration();
385  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
386  fileLoggingConfiguration.Disable = !await PromptYesNo("Enable file logging? (y/n): ", cancellationToken).ConfigureAwait(false);
387 
388  if (!fileLoggingConfiguration.Disable)
389  {
390  do
391  {
392  await console.WriteAsync("Log file directory path (leave blank for default): ", false, cancellationToken).ConfigureAwait(false);
393  fileLoggingConfiguration.Directory = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
394  if (String.IsNullOrWhiteSpace(fileLoggingConfiguration.Directory))
395  {
396  fileLoggingConfiguration.Directory = null;
397  break;
398  }
399 
400  // test a write of it
401  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
402  await console.WriteAsync("Testing directory access...", true, cancellationToken).ConfigureAwait(false);
403  try
404  {
405  await ioManager.CreateDirectory(fileLoggingConfiguration.Directory, cancellationToken).ConfigureAwait(false);
406  var testFile = ioManager.ConcatPath(fileLoggingConfiguration.Directory, String.Format(CultureInfo.InvariantCulture, "WizardAccesTest.{0}.deleteme", Guid.NewGuid()));
407  await ioManager.WriteAllBytes(testFile, Array.Empty<byte>(), cancellationToken).ConfigureAwait(false);
408  try
409  {
410  await ioManager.DeleteFile(testFile, cancellationToken).ConfigureAwait(false);
411  }
412  catch (OperationCanceledException)
413  {
414  throw;
415  }
416  catch (Exception e)
417  {
418  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Error deleting test log file: {0}", testFile), true, cancellationToken).ConfigureAwait(false);
419  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
420  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
421  }
422 
423  break;
424  }
425  catch (OperationCanceledException)
426  {
427  throw;
428  }
429  catch (Exception e)
430  {
431  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
432  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
433  await console.WriteAsync("Please verify the path is valid and you have access to it!", true, cancellationToken).ConfigureAwait(false);
434  }
435  }
436  while (true);
437 
438  async Task<LogLevel?> PromptLogLevel(string question)
439  {
440  do
441  {
442  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
443  await console.WriteAsync(question, true, cancellationToken).ConfigureAwait(false);
444  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Enter one of {0}/{1}/{2}/{3}/{4}/{5} (leave blank for default): ", nameof(LogLevel.Trace), nameof(LogLevel.Debug), nameof(LogLevel.Information), nameof(LogLevel.Warning), nameof(LogLevel.Error), nameof(LogLevel.Critical)), false, cancellationToken).ConfigureAwait(false);
445  var responseString = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
446  if (String.IsNullOrWhiteSpace(responseString))
447  return null;
448  if (Enum.TryParse<LogLevel>(responseString, out var logLevel) && logLevel != LogLevel.None)
449  return logLevel;
450  await console.WriteAsync("Invalid log level!", true, cancellationToken).ConfigureAwait(false);
451  }
452  while (true);
453  }
454 
455  fileLoggingConfiguration.LogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for normal logs (default {0}).", fileLoggingConfiguration.LogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.LogLevel;
456  fileLoggingConfiguration.MicrosoftLogLevel = await PromptLogLevel(String.Format(CultureInfo.InvariantCulture, "Enter the level limit for Microsoft logs (VERY verbose, default {0}).", fileLoggingConfiguration.MicrosoftLogLevel)).ConfigureAwait(false) ?? fileLoggingConfiguration.MicrosoftLogLevel;
457  }
458 
459  return fileLoggingConfiguration;
460  }
461 
467  async Task<ControlPanelConfiguration> ConfigureControlPanel(CancellationToken cancellationToken)
468  {
469  var config = new ControlPanelConfiguration
470  {
471  Enable = await PromptYesNo("Enable the web control panel? (y/n): ", cancellationToken).ConfigureAwait(false),
472  AllowAnyOrigin = await PromptYesNo("Allow web control panels hosted elsewhere to access the server? (Access-Control-Allow-Origin: *) (y/n): ", cancellationToken).ConfigureAwait(false)
473  };
474 
475  if (!config.AllowAnyOrigin)
476  {
477  await console.WriteAsync("Enter a comma seperated list of CORS allowed origins (optional): ", false, cancellationToken).ConfigureAwait(false);
478  var commaSeperatedOrigins = await console.ReadLineAsync(false, cancellationToken).ConfigureAwait(false);
479  if (!String.IsNullOrWhiteSpace(commaSeperatedOrigins))
480  {
481  var splits = commaSeperatedOrigins.Split(',');
482  config.AllowedOrigins = new List<string>(splits.Select(x => x.Trim()));
483  }
484  }
485 
486  return config;
487  }
488 
500  async Task SaveConfiguration(string userConfigFileName, ushort? hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
501  {
502  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "Configuration complete! Saving to {0}", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
503 
504  var map = new Dictionary<string, object>()
505  {
506  { DatabaseConfiguration.Section, databaseConfiguration },
507  { GeneralConfiguration.Section, newGeneralConfiguration },
508  { FileLoggingConfiguration.Section, fileLoggingConfiguration },
509  { ControlPanelConfiguration.Section, controlPanelConfiguration }
510  };
511 
512  if (hostingPort.HasValue)
513  map.Add("Kestrel", new
514  {
515  EndPoints = new
516  {
517  Http = new
518  {
519  Url = String.Format(CultureInfo.InvariantCulture, "http://0.0.0.0:{0}", hostingPort)
520  }
521  }
522  });
523 
524  var json = JsonConvert.SerializeObject(map, Formatting.Indented);
525  var configBytes = Encoding.UTF8.GetBytes(json);
526 
527  try
528  {
529  await ioManager.WriteAllBytes(userConfigFileName, configBytes, cancellationToken).ConfigureAwait(false);
530  }
531  catch (OperationCanceledException)
532  {
533  throw;
534  }
535  catch (Exception e)
536  {
537  await console.WriteAsync(e.Message, true, cancellationToken).ConfigureAwait(false);
538  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
539  await console.WriteAsync("For your convienence, here's the json we tried to write out:", true, cancellationToken).ConfigureAwait(false);
540  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
541  await console.WriteAsync(json, true, cancellationToken).ConfigureAwait(false);
542  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
543  await console.WriteAsync("Press any key to exit...", true, cancellationToken).ConfigureAwait(false);
544  await console.PressAnyKeyAsync(cancellationToken).ConfigureAwait(false);
545  throw new OperationCanceledException();
546  }
547 
548  await console.WriteAsync("Waiting for configuration changes to reload...", true, cancellationToken).ConfigureAwait(false);
549 
550  // we need to wait for the configuration's file system watcher to read and reload the changes
551  await asyncDelayer.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
552  }
553 
560  async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
561  {
562  // welcome message
563  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
564  await console.WriteAsync("Welcome to tgstation-server 4!", true, cancellationToken).ConfigureAwait(false);
565  await console.WriteAsync("This wizard will help you configure your server.", true, cancellationToken).ConfigureAwait(false);
566 
567  var hostingPort = await PromptForHostingPort(cancellationToken).ConfigureAwait(false);
568 
569  var databaseConfiguration = await ConfigureDatabase(cancellationToken).ConfigureAwait(false);
570 
571  var newGeneralConfiguration = await ConfigureGeneral(cancellationToken).ConfigureAwait(false);
572 
573  var fileLoggingConfiguration = await ConfigureLogging(cancellationToken).ConfigureAwait(false);
574 
575  var controlPanelConfiguration = await ConfigureControlPanel(cancellationToken).ConfigureAwait(false);
576 
577  await console.WriteAsync(null, true, cancellationToken).ConfigureAwait(false);
578 
579  await SaveConfiguration(userConfigFileName, hostingPort, databaseConfiguration, newGeneralConfiguration, fileLoggingConfiguration, controlPanelConfiguration, cancellationToken).ConfigureAwait(false);
580  }
581 
583  public async Task<bool> CheckRunWizard(CancellationToken cancellationToken)
584  {
585  var setupWizardMode = generalConfiguration.SetupWizardMode;
586  logger.LogTrace("Checking if setup wizard should run. SetupWizardMode: {0}", setupWizardMode);
587 
588  if (setupWizardMode == SetupWizardMode.Never)
589  {
590  logger.LogTrace("Skipping due to configuration...");
591  return false;
592  }
593 
594  var forceRun = setupWizardMode == SetupWizardMode.Force || setupWizardMode == SetupWizardMode.Only;
595  if (!console.Available)
596  {
597  if (forceRun)
598  throw new InvalidOperationException("Asked to run setup wizard with no console avaliable!");
599  logger.LogTrace("Skipping due to console not being available...");
600  return false;
601  }
602 
603  var userConfigFileName = String.Format(CultureInfo.InvariantCulture, "appsettings.{0}.json", hostingEnvironment.EnvironmentName);
604  var exists = await ioManager.FileExists(userConfigFileName, cancellationToken).ConfigureAwait(false);
605 
606  bool shouldRunBasedOnAutodetect;
607  if (exists)
608  {
609  var bytes = await ioManager.ReadAllBytes(userConfigFileName, cancellationToken).ConfigureAwait(false);
610  var contents = Encoding.UTF8.GetString(bytes);
611  var existingConfigIsEmpty = String.IsNullOrWhiteSpace(contents) || contents.Trim() == "{}";
612  logger.LogTrace("Configuration json detected. Empty: {0}", existingConfigIsEmpty);
613  shouldRunBasedOnAutodetect = existingConfigIsEmpty;
614  }
615  else
616  {
617  shouldRunBasedOnAutodetect = true;
618  logger.LogTrace("No configuration json detected");
619  }
620 
621  if (!shouldRunBasedOnAutodetect)
622  {
623  if (forceRun)
624  {
625  logger.LogTrace("Asking user to bypass due to force run request...");
626  await console.WriteAsync(String.Format(CultureInfo.InvariantCulture, "The configuration settings are requesting the setup wizard be run, but you already appear to have a configuration file ({0})!", userConfigFileName), true, cancellationToken).ConfigureAwait(false);
627 
628  forceRun = await PromptYesNo("Continue running setup wizard? (y/n): ", cancellationToken).ConfigureAwait(false);
629  }
630 
631  if (!forceRun)
632  return false;
633  }
634 
635  // flush the logs to prevent console conflicts
636  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
637 
638  await RunWizard(userConfigFileName, cancellationToken).ConfigureAwait(false);
639  return true;
640  }
641  }
642 }
readonly IConsole console
The IConsole for the SetupWizard
Definition: SetupWizard.cs:31
List< string > AllowedOrigins
Origins allowed for CORS requests
-
async Task< ControlPanelConfiguration > ConfigureControlPanel(CancellationToken cancellationToken)
Prompts the user to create a ControlPanelConfiguration
Definition: SetupWizard.cs:457
+
async Task< ControlPanelConfiguration > ConfigureControlPanel(CancellationToken cancellationToken)
Prompts the user to create a ControlPanelConfiguration
Definition: SetupWizard.cs:467
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
SetupWizardMode
Determines if the Core.ISetupWizard will run
@@ -115,16 +115,16 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task< DatabaseConfiguration > ConfigureDatabase(CancellationToken cancellationToken)
Prompts the user to create a DatabaseConfiguration
Definition: SetupWizard.cs:144
-
async Task< GeneralConfiguration > ConfigureGeneral(CancellationToken cancellationToken)
Prompts the user to create a GeneralConfiguration
Definition: SetupWizard.cs:324
+
async Task< GeneralConfiguration > ConfigureGeneral(CancellationToken cancellationToken)
Prompts the user to create a GeneralConfiguration
Definition: SetupWizard.cs:327
-
async Task SaveConfiguration(string userConfigFileName, ushort?hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
Saves a given Configuration set to userConfigFileName
Definition: SetupWizard.cs:490
+
async Task SaveConfiguration(string userConfigFileName, ushort?hostingPort, DatabaseConfiguration databaseConfiguration, GeneralConfiguration newGeneralConfiguration, FileLoggingConfiguration fileLoggingConfiguration, ControlPanelConfiguration controlPanelConfiguration, CancellationToken cancellationToken)
Saves a given Configuration set to userConfigFileName
Definition: SetupWizard.cs:500
readonly IHostingEnvironment hostingEnvironment
The IHostingEnvironment for the SetupWizard
Definition: SetupWizard.cs:36
async Task< ushort?> PromptForHostingPort(CancellationToken cancellationToken)
Prompts the user to enter the port to host TGS on
Definition: SetupWizard.cs:120
Configuration options for the Models.DatabaseContext<TParentContext>
-
async Task< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration
Definition: SetupWizard.cs:377
+
async Task< FileLoggingConfiguration > ConfigureLogging(CancellationToken cancellationToken)
Prompts the user to create a FileLoggingConfiguration
Definition: SetupWizard.cs:382
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the SetupWizard
Definition: SetupWizard.cs:56
-
async Task< bool > CheckRunWizard(CancellationToken cancellationToken)
Run the setup wizard if necessary
Definition: SetupWizard.cs:573
+
async Task< bool > CheckRunWizard(CancellationToken cancellationToken)
Run the setup wizard if necessary
Definition: SetupWizard.cs:583
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SetupWizard
Definition: SetupWizard.cs:51
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
@@ -135,7 +135,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Interface for using filesystems
Definition: IIOManager.cs:11
For identifying the current platform
readonly ILogger< SetupWizard > logger
The ILogger for the SetupWizard
Definition: SetupWizard.cs:61
-
async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
Runs the SetupWizard
Definition: SetupWizard.cs:550
+
async Task RunWizard(string userConfigFileName, CancellationToken cancellationToken)
Runs the SetupWizard
Definition: SetupWizard.cs:560
uint MinimumPasswordLength
Minimum length of database user passwords
diff --git a/_setup_wizard_mode_8cs_source.html b/_setup_wizard_mode_8cs_source.html index 7d45aa87aa..9384f95e30 100644 --- a/_setup_wizard_mode_8cs_source.html +++ b/_setup_wizard_mode_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SetupWizardMode.cs
-Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Configuration
2 {
6  public enum SetupWizardMode
7  {
11  Autodetect,
15  Force,
19  Only,
23  Never
24  }
25 }
+Go to the documentation of this file.
1 namespace Tgstation.Server.Host.Configuration
2 {
6  public enum SetupWizardMode
7  {
11  Autodetect,
12 
16  Force,
17 
21  Only,
22 
26  Never
27  }
28 }
SetupWizardMode
Determines if the Core.ISetupWizard will run
diff --git a/_sql_server_database_context_8cs_source.html b/_sql_server_database_context_8cs_source.html index 327ef219f1..e145b493ca 100644 --- a/_sql_server_database_context_8cs_source.html +++ b/_sql_server_database_context_8cs_source.html @@ -99,7 +99,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseContext<TParentContext> for Sqlserver
SqlServerDatabaseContext(DbContextOptions< SqlServerDatabaseContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger< SqlServerDatabaseContext > logger)
Construct a SqlServerDatabaseContext
- +
Configuration options for the Models.DatabaseContext<TParentContext>
diff --git a/_sql_server_design_time_db_context_factory_8cs_source.html b/_sql_server_design_time_db_context_factory_8cs_source.html index 997f7a6e49..e03aeeb243 100644 --- a/_sql_server_design_time_db_context_factory_8cs_source.html +++ b/_sql_server_design_time_db_context_factory_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SqlServerDesignTimeDbContextFactory.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
6 
7 namespace Tgstation.Server.Host.Models.Migrations
8 {
10  sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqlServerDatabaseContext>
11  {
13  public SqlServerDatabaseContext CreateDbContext(string[] args) => new SqlServerDatabaseContext(new DbContextOptions<SqlServerDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())), new LoggerFactory().CreateLogger<SqlServerDatabaseContext>());
14  }
15 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Identity;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.EntityFrameworkCore.Design;
4 using Microsoft.Extensions.Logging;
6 
7 namespace Tgstation.Server.Host.Models.Migrations
8 {
10  sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqlServerDatabaseContext>
11  {
13  public SqlServerDatabaseContext CreateDbContext(string[] args) => new SqlServerDatabaseContext(new DbContextOptions<SqlServerDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())), new LoggerFactory().CreateLogger<SqlServerDatabaseContext>());
14  }
15 }
static IOptions< DatabaseConfiguration > GetDbContextOptions()
Get the IOptions<TOptions> for the DatabaseConfiguration
Contains helpers for creating design time Models.DatabaseContext<TParentContext>s ...
@@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
-Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Globalization;
4 using System.IO;
5 using System.Linq;
6 using System.Security.Cryptography;
7 using System.Threading;
8 
9 namespace Tgstation.Server.Host.IO
10 {
13  {
15  public bool CreateDirectory(string path, CancellationToken cancellationToken)
16  {
17  if (IsDirectory(path))
18  return true;
19  cancellationToken.ThrowIfCancellationRequested();
20  Directory.CreateDirectory(path);
21  return false;
22  }
23 
25  public bool DeleteDirectory(string path)
26  {
27  if (File.Exists(path))
28  return false;
29 
30  if (!Directory.Exists(path))
31  return true;
32 
33  if (Directory.EnumerateFileSystemEntries(path).Any())
34  return false;
35 
36  Directory.Delete(path);
37  return true;
38  }
39 
41  public IEnumerable<string> GetDirectories(string path, CancellationToken cancellationToken)
42  {
43  foreach (var I in Directory.EnumerateDirectories(path))
44  {
45  yield return Path.GetFileName(I);
46  cancellationToken.ThrowIfCancellationRequested();
47  }
48  }
49 
51  public IEnumerable<string> GetFiles(string path, CancellationToken cancellationToken)
52  {
53  foreach (var I in Directory.EnumerateFiles(path))
54  {
55  yield return Path.GetFileName(I);
56  cancellationToken.ThrowIfCancellationRequested();
57  }
58  }
59 
61  public bool IsDirectory(string path)
62  {
63  if (path == null)
64  throw new ArgumentNullException(nameof(path));
65  return Directory.Exists(path);
66  }
67 
69  public byte[] ReadFile(string path)
70  {
71  if (path == null)
72  throw new ArgumentNullException(nameof(path));
73  return File.ReadAllBytes(path);
74  }
75 
77  public bool WriteFileChecked(string path, byte[] data, ref string sha1InOut, CancellationToken cancellationToken)
78  {
79  if (path == null)
80  throw new ArgumentNullException(nameof(path));
81  cancellationToken.ThrowIfCancellationRequested();
82  var directory = Path.GetDirectoryName(path);
83  Directory.CreateDirectory(directory);
84  cancellationToken.ThrowIfCancellationRequested();
85  using (var file = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
86  {
87  cancellationToken.ThrowIfCancellationRequested();
88 
89  //as nice as it would be to not have to arrayify the memory stream, we have to
90  //because, oddly enough sha1(memorystream) != sha1(memorystream.ToArray())
91  // vOv
92  byte[] originalBytes;
93  using (var readMs = new MemoryStream())
94  {
95  file.CopyTo(readMs);
96  originalBytes = readMs.ToArray();
97  }
98  if (originalBytes.Length != 0 && sha1InOut == null)
99  //no sha1? no write
100  return false;
101 
102  //suppressed due to only using for consistency checks
103 #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1.
104  using (var sha1 = new SHA1Managed())
105 #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1.
106  {
107  string GetSha1(byte[] dataToHash) => dataToHash != null && dataToHash.Length != 0 ? String.Join("", sha1.ComputeHash(dataToHash).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))) : null;
108  var originalSha1 = GetSha1(originalBytes);
109  if (originalSha1 != sha1InOut)
110  {
111  sha1InOut = originalSha1;
112  return false;
113  }
114  sha1InOut = GetSha1(data);
115  }
116  cancellationToken.ThrowIfCancellationRequested();
117 
118  if (data != null)
119  {
120  file.Seek(0, SeekOrigin.Begin);
121 
122  cancellationToken.ThrowIfCancellationRequested();
123  file.SetLength(data.Length);
124  file.Write(data, 0, data.Length);
125  }
126  }
127  if (data == null)
128  File.Delete(path);
129  return true;
130  }
131  }
132 }
IEnumerable< string > GetDirectories(string path, CancellationToken cancellationToken)
Enumerate directories in a given path
+Go to the documentation of this file.
1 using System;
2 using System.Collections.Generic;
3 using System.Globalization;
4 using System.IO;
5 using System.Linq;
6 using System.Security.Cryptography;
7 using System.Threading;
8 
9 namespace Tgstation.Server.Host.IO
10 {
13  {
15  public bool CreateDirectory(string path, CancellationToken cancellationToken)
16  {
17  if (IsDirectory(path))
18  return true;
19  cancellationToken.ThrowIfCancellationRequested();
20  Directory.CreateDirectory(path);
21  return false;
22  }
23 
25  public bool DeleteDirectory(string path)
26  {
27  if (File.Exists(path))
28  return false;
29 
30  if (!Directory.Exists(path))
31  return true;
32 
33  if (Directory.EnumerateFileSystemEntries(path).Any())
34  return false;
35 
36  Directory.Delete(path);
37  return true;
38  }
39 
41  public IEnumerable<string> GetDirectories(string path, CancellationToken cancellationToken)
42  {
43  foreach (var I in Directory.EnumerateDirectories(path))
44  {
45  yield return Path.GetFileName(I);
46  cancellationToken.ThrowIfCancellationRequested();
47  }
48  }
49 
51  public IEnumerable<string> GetFiles(string path, CancellationToken cancellationToken)
52  {
53  foreach (var I in Directory.EnumerateFiles(path))
54  {
55  yield return Path.GetFileName(I);
56  cancellationToken.ThrowIfCancellationRequested();
57  }
58  }
59 
61  public bool IsDirectory(string path)
62  {
63  if (path == null)
64  throw new ArgumentNullException(nameof(path));
65  return Directory.Exists(path);
66  }
67 
69  public byte[] ReadFile(string path)
70  {
71  if (path == null)
72  throw new ArgumentNullException(nameof(path));
73  return File.ReadAllBytes(path);
74  }
75 
77  public bool WriteFileChecked(string path, byte[] data, ref string sha1InOut, CancellationToken cancellationToken)
78  {
79  if (path == null)
80  throw new ArgumentNullException(nameof(path));
81  cancellationToken.ThrowIfCancellationRequested();
82  var directory = Path.GetDirectoryName(path);
83  Directory.CreateDirectory(directory);
84  cancellationToken.ThrowIfCancellationRequested();
85  using (var file = File.Open(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None))
86  {
87  cancellationToken.ThrowIfCancellationRequested();
88 
89  // as nice as it would be to not have to arrayify the memory stream, we have to
90  // because, oddly enough sha1(memorystream) != sha1(memorystream.ToArray())
91  // vOv
92  byte[] originalBytes;
93  using (var readMs = new MemoryStream())
94  {
95  file.CopyTo(readMs);
96  originalBytes = readMs.ToArray();
97  }
98 
99  // no sha1? no write
100  if (originalBytes.Length != 0 && sha1InOut == null)
101  return false;
102 
103  // suppressed due to only using for consistency checks
104 #pragma warning disable CA5350 // Do not use insecure cryptographic algorithm SHA1.
105  using (var sha1 = new SHA1Managed())
106 #pragma warning restore CA5350 // Do not use insecure cryptographic algorithm SHA1.
107  {
108  string GetSha1(byte[] dataToHash) => dataToHash != null && dataToHash.Length != 0 ? String.Join(String.Empty, sha1.ComputeHash(dataToHash).Select(b => b.ToString("x2", CultureInfo.InvariantCulture))) : null;
109  var originalSha1 = GetSha1(originalBytes);
110  if (originalSha1 != sha1InOut)
111  {
112  sha1InOut = originalSha1;
113  return false;
114  }
115 
116  sha1InOut = GetSha1(data);
117  }
118 
119  cancellationToken.ThrowIfCancellationRequested();
120 
121  if (data != null)
122  {
123  file.Seek(0, SeekOrigin.Begin);
124 
125  cancellationToken.ThrowIfCancellationRequested();
126  file.SetLength(data.Length);
127  file.Write(data, 0, data.Length);
128  }
129  }
130 
131  if (data == null)
132  File.Delete(path);
133  return true;
134  }
135  }
136 }
IEnumerable< string > GetDirectories(string path, CancellationToken cancellationToken)
Enumerate directories in a given path
bool CreateDirectory(string path, CancellationToken cancellationToken)
Create an empty directory at path
byte[] ReadFile(string path)
Read the bytes of a file at a given path
For accessing the disk in a synchronous manner
-
bool WriteFileChecked(string path, byte[] data, ref string sha1InOut, CancellationToken cancellationToken)
Write data to a file at a given path .
+
bool WriteFileChecked(string path, byte[] data, ref string sha1InOut, CancellationToken cancellationToken)
Write data to a file at a given path
bool IsDirectory(string path)
Checks if a given path is a directory
bool DeleteDirectory(string path)
Deletes a directory at path if it&#39;s empty
diff --git a/_temporary_dmb_provider_8cs_source.html b/_temporary_dmb_provider_8cs_source.html index 542ed05ac2..b24a198c1e 100644 --- a/_temporary_dmb_provider_8cs_source.html +++ b/_temporary_dmb_provider_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TemporaryDmbProvider.cs
-Go to the documentation of this file.
1 using System;
3 
4 namespace Tgstation.Server.Host.Components.Compiler
5 {
10  {
12  public string DmbName { get; }
13 
15  public string PrimaryDirectory { get; }
16 
18  public string SecondaryDirectory => throw new NotSupportedException();
19 
21  public CompileJob CompileJob { get; }
22 
29  public TemporaryDmbProvider(string directory, string dmb, CompileJob compileJob)
30  {
31  DmbName = dmb ?? throw new ArgumentNullException(nameof(dmb));
32  PrimaryDirectory = directory ?? throw new ArgumentNullException(nameof(directory));
33  CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob));
34  }
35 
37  public void Dispose() { }
38 
39  public void KeepAlive() => throw new NotSupportedException();
40  }
41 }
TemporaryDmbProvider(string directory, string dmb, CompileJob compileJob)
Construct a TemporaryDmbProvider
+Go to the documentation of this file.
1 using System;
3 
4 namespace Tgstation.Server.Host.Components.Compiler
5 {
10  {
12  public string DmbName { get; }
13 
15  public string PrimaryDirectory { get; }
16 
18  public string SecondaryDirectory => throw new NotSupportedException();
19 
21  public CompileJob CompileJob { get; }
22 
29  public TemporaryDmbProvider(string directory, string dmb, CompileJob compileJob)
30  {
31  DmbName = dmb ?? throw new ArgumentNullException(nameof(dmb));
32  PrimaryDirectory = directory ?? throw new ArgumentNullException(nameof(directory));
33  CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob));
34  }
35 
37  public void Dispose() { }
38 
40  public void KeepAlive() => throw new NotSupportedException();
41  }
42 }
TemporaryDmbProvider(string directory, string dmb, CompileJob compileJob)
Construct a TemporaryDmbProvider
diff --git a/_test_application_8cs_source.html b/_test_application_8cs_source.html index 605eb1fda1..287529c41f 100644 --- a/_test_application_8cs_source.html +++ b/_test_application_8cs_source.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
1 using Microsoft.AspNetCore.Builder;
2 using Microsoft.AspNetCore.Hosting;
3 using Microsoft.Extensions.Configuration;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Options;
6 using Microsoft.VisualStudio.TestTools.UnitTesting;
7 using Moq;
8 using System;
9 using System.Collections.Generic;
10 using System.Threading;
11 using System.Threading.Tasks;
14 
15 namespace Tgstation.Server.Host.Core.Tests
16 {
20  [TestClass]
21  public sealed class TestApplication
22  {
23  [TestMethod]
24  public void TestMethodThrows()
25  {
26  Assert.ThrowsException<ArgumentNullException>(() => new Application(null, null));
27  var mockConfiguration = new Mock<IConfiguration>();
28  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
29 
30  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
31 
32  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
33 
34  Assert.ThrowsException<ArgumentNullException>(() => app.ConfigureServices(null));
35  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(null, null, null, null, null));
36 
37  var mockAppBuilder = new Mock<IApplicationBuilder>();
38  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, null, null, null, null));
39 
40  var mockServerControl = new Mock<IServerControl>();
41  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, null, null, null));
42 
43  var mockTokenFactory = new Mock<ITokenFactory>();
44  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, null, null));
45 
46  var mockControlPanelOptions = new Mock<IOptions<ControlPanelConfiguration>>();
47  mockControlPanelOptions.SetupGet(x => x.Value).Returns(new ControlPanelConfiguration()).Verifiable();
48  Assert.ThrowsException<ArgumentNullException>(() => app.Configure(mockAppBuilder.Object, mockServerControl.Object, mockTokenFactory.Object, mockControlPanelOptions.Object, null));
49  mockControlPanelOptions.VerifyAll();
50  }
51 
53  {
54  public Task<bool> CheckRunWizard(CancellationToken cancellationToken) => Task.FromResult(true);
55  }
56 
58  {
59  public CancellationToken ApplicationStarted => default;
60 
61  public CancellationToken ApplicationStopping => default;
62 
63  public CancellationToken ApplicationStopped => default;
64 
65  public void StopApplication() { }
66  }
67 
68  [TestMethod]
70  {
71  var mockConfiguration = new Mock<IConfiguration>();
72  Assert.ThrowsException<ArgumentNullException>(() => new Application(mockConfiguration.Object, null));
73 
74  var mockHostingEnvironment = new Mock<IHostingEnvironment>();
75 
76  var app = new Application(mockConfiguration.Object, mockHostingEnvironment.Object);
77 
78  var mockOptions = new Mock<IOptions<GeneralConfiguration>>();
79  mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration
80  {
82  }).Verifiable();
83 
84  var fakeServiceDescriptor = new List<ServiceDescriptor>()
85  {
86  new ServiceDescriptor(typeof(IApplicationLifetime), typeof(MockApplicationLifetime), ServiceLifetime.Singleton),
87  new ServiceDescriptor(typeof(ISetupWizard), typeof(MockSetupWizard), ServiceLifetime.Singleton),
88  new ServiceDescriptor(typeof(IOptions<GeneralConfiguration>), mockOptions.Object)
89  };
90 
91  var mockServiceCollection = new Mock<IServiceCollection>();
92 
93  var mockConfigSection = new Mock<IConfigurationSection>();
94 
95  mockConfiguration.Setup(x => x.GetSection(It.IsNotNull<string>())).Returns(mockConfigSection.Object).Verifiable();
96  mockServiceCollection.Setup(x => x.GetEnumerator()).Returns(() => fakeServiceDescriptor.GetEnumerator()).Verifiable();
97 
98  Assert.ThrowsException<OperationCanceledException>(() => app.ConfigureServices(mockServiceCollection.Object));
99 
100  mockOptions.VerifyAll();
101  mockConfiguration.VerifyAll();
102  mockServiceCollection.VerifyAll();
103  }
104  }
105 }
SetupWizardMode
Determines if the Core.ISetupWizard will run
- + diff --git a/_test_discord_provider_8cs_source.html b/_test_discord_provider_8cs_source.html index f1420d697c..66acad362a 100644 --- a/_test_discord_provider_8cs_source.html +++ b/_test_discord_provider_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TestDiscordProvider.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Threading;
6 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
9 {
10  [TestClass]
11  public sealed class TestDiscordProvider
12  {
13  string testToken1;
14 
15  [TestInitialize]
16  public void Initialize()
17  {
18  testToken1 = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN_1");
19  }
20 
21  [TestMethod]
23  {
24  Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(null, null));
25  var mockLogger = new Mock<ILogger<DiscordProvider>>();
26  Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockLogger.Object, null));
27  var mockToken = "asdf";
28  new DiscordProvider(mockLogger.Object, mockToken).Dispose();
29  }
30 
31  [TestMethod]
32  public async Task TestConnectWithFakeTokenFails()
33  {
34  var mockLogger = new Mock<ILogger<DiscordProvider>>();
35  using (var provider = new DiscordProvider(mockLogger.Object, "asdf"))
36  {
37  Assert.IsFalse(await provider.Connect(default).ConfigureAwait(false));
38  Assert.IsFalse(provider.Connected);
39  }
40  }
41 
42  [Ignore("Broken due to dependency issues after first call to .Connect()")]
43  [TestMethod]
44  public async Task TestConnectAndDisconnect()
45  {
46  if (testToken1 == null)
47  Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN_1 isn't set!");
48 
49 
50  var mockLogger = new Mock<ILogger<DiscordProvider>>();
51  using (var provider = new DiscordProvider(mockLogger.Object, testToken1))
52  {
53  Assert.IsFalse(provider.Connected);
54  await provider.Disconnect(default).ConfigureAwait(false);
55  Assert.IsFalse(provider.Connected);
56  Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
57  Assert.IsTrue(provider.Connected);
58  Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
59  Assert.IsTrue(provider.Connected);
60 
61  await provider.Disconnect(default).ConfigureAwait(false);
62  Assert.IsFalse(provider.Connected);
63  await provider.Disconnect(default).ConfigureAwait(false);
64  Assert.IsFalse(provider.Connected);
65 
66  //now try it with cancellationTokens
67  using (var cts = new CancellationTokenSource())
68  {
69  cts.Cancel();
70  var cancellationToken = cts.Token;
71  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => provider.Connect(cancellationToken)).ConfigureAwait(false);
72  Assert.IsFalse(provider.Connected);
73  Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
74  Assert.IsTrue(provider.Connected);
75  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false);
76  Assert.IsTrue(provider.Connected);
77  await provider.Disconnect(default).ConfigureAwait(false);
78  Assert.IsFalse(provider.Connected);
79  }
80 
81  }
82  }
83  }
84 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Threading;
6 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
9 {
10  [TestClass]
11  public sealed class TestDiscordProvider
12  {
13  string testToken1;
14 
15  [TestInitialize]
16  public void Initialize()
17  {
18  testToken1 = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN_1");
19  }
20 
21  [TestMethod]
23  {
24  Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(null, null));
25  var mockLogger = new Mock<ILogger<DiscordProvider>>();
26  Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockLogger.Object, null));
27  var mockToken = "asdf";
28  new DiscordProvider(mockLogger.Object, mockToken).Dispose();
29  }
30 
31  [TestMethod]
32  public async Task TestConnectWithFakeTokenFails()
33  {
34  var mockLogger = new Mock<ILogger<DiscordProvider>>();
35  using (var provider = new DiscordProvider(mockLogger.Object, "asdf"))
36  {
37  Assert.IsFalse(await provider.Connect(default).ConfigureAwait(false));
38  Assert.IsFalse(provider.Connected);
39  }
40  }
41 
42  [Ignore("Broken due to dependency issues after first call to .Connect()")]
43  [TestMethod]
44  public async Task TestConnectAndDisconnect()
45  {
46  if (testToken1 == null)
47  Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN_1 isn't set!");
48 
49 
50  var mockLogger = new Mock<ILogger<DiscordProvider>>();
51  using (var provider = new DiscordProvider(mockLogger.Object, testToken1))
52  {
53  Assert.IsFalse(provider.Connected);
54  await provider.Disconnect(default).ConfigureAwait(false);
55  Assert.IsFalse(provider.Connected);
56  Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
57  Assert.IsTrue(provider.Connected);
58  Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
59  Assert.IsTrue(provider.Connected);
60 
61  await provider.Disconnect(default).ConfigureAwait(false);
62  Assert.IsFalse(provider.Connected);
63  await provider.Disconnect(default).ConfigureAwait(false);
64  Assert.IsFalse(provider.Connected);
65 
66  //now try it with cancellationTokens
67  using (var cts = new CancellationTokenSource())
68  {
69  cts.Cancel();
70  var cancellationToken = cts.Token;
71  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => provider.Connect(cancellationToken)).ConfigureAwait(false);
72  Assert.IsFalse(provider.Connected);
73  Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
74  Assert.IsTrue(provider.Connected);
75  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false);
76  Assert.IsTrue(provider.Connected);
77  await provider.Disconnect(default).ConfigureAwait(false);
78  Assert.IsFalse(provider.Connected);
79  }
80 
81  }
82  }
83  }
84 }
diff --git a/_test_job_handler_8cs_source.html b/_test_job_handler_8cs_source.html index 0c17495db2..c062b7c690 100644 --- a/_test_job_handler_8cs_source.html +++ b/_test_job_handler_8cs_source.html @@ -93,7 +93,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Go to the documentation of this file.
1 using Microsoft.VisualStudio.TestTools.UnitTesting;
2 using System;
3 using System.Threading;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core.Tests
7 {
8  [TestClass]
9  public sealed class TestJobHandler
10  {
12  bool cancelled;
13 
14  async Task TestJob(CancellationToken cancellationToken)
15  {
16  await currentWaitTask.ConfigureAwait(false);
17  cancelled = cancellationToken.IsCancellationRequested;
18  }
19 
20  [TestMethod]
22  {
23  cancelled = false;
24  Assert.ThrowsException<ArgumentNullException>(() => new JobHandler(null));
25 
26  currentWaitTask = Task.CompletedTask;
27  new JobHandler(TestJob).Dispose();
28  Assert.IsFalse(cancelled);
29  }
30 
31  [TestMethod]
32  public async Task TestWait()
33  {
34  cancelled = false;
35  //test with a cancelled cts
36  using (var cts = new CancellationTokenSource())
37  {
38  var tcs = new TaskCompletionSource<object>();
39  currentWaitTask = tcs.Task;
40  cts.Cancel();
41  using(var handler = new JobHandler(TestJob))
42  {
43  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => handler.Wait(cts.Token)).ConfigureAwait(false);
44  tcs.SetResult(null);
45  await handler.Wait(default).ConfigureAwait(false);
46  }
47  }
48  Assert.IsFalse(cancelled);
49  }
50 
51  [TestMethod]
52  public void TestProgress()
53  {
54  currentWaitTask = Task.CompletedTask;
55  cancelled = false;
56  using (var handler = new JobHandler(TestJob))
57  {
58  Assert.IsFalse(handler.Progress.HasValue);
59  handler.Progress = 42;
60  Assert.AreEqual(42, handler.Progress);
61  }
62  Assert.IsFalse(cancelled);
63  }
64 
65  [TestMethod]
66  public async Task TestCancellation()
67  {
68  var tcs = new TaskCompletionSource<object>();
69  currentWaitTask = tcs.Task;
70  cancelled = false;
71  using(var handler = new JobHandler(TestJob))
72  {
73  handler.Cancel();
74  tcs.SetResult(null);
75  await handler.Wait(default).ConfigureAwait(false);
76  }
77  Assert.IsTrue(cancelled);
78  }
79  }
80 }
- + diff --git a/_test_posix_byond_installer_8cs_source.html b/_test_posix_byond_installer_8cs_source.html index a484b5ad2d..927e48683e 100644 --- a/_test_posix_byond_installer_8cs_source.html +++ b/_test_posix_byond_installer_8cs_source.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Threading.Tasks;
6 using Tgstation.Server.Host.IO;
7 
8 namespace Tgstation.Server.Host.Components.Byond.Tests
9 {
10  [TestClass]
11  public sealed class TestPosixByondInstaller
12  {
13  [TestMethod]
14  public void TestConstruction()
15  {
16  Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(null, null, null));
17  var mockIOManager = new Mock<IIOManager>();
18  Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, null, null));
19  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
20  Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, null));
21 
22  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
23  new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
24  }
25 
26  [TestMethod]
27  public async Task TestCacheClean()
28  {
29  var mockIOManager = new Mock<IIOManager>();
30  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
31  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
32  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
33 
34  const string ByondCachePath = "~/.byond/cache";
35 
36  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Returns(Task.CompletedTask).Verifiable();
37 
38  await installer.CleanCache(default);
39 
40  mockIOManager.Verify();
41 
42 
43  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new OperationCanceledException()).Verifiable();
44 
45  await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => installer.CleanCache(default)).ConfigureAwait(false);
46 
47  mockIOManager.Verify();
48 
49  mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new Exception()).Verifiable();
50 
51  await installer.CleanCache(default).ConfigureAwait(false);
52 
53  mockIOManager.Verify();
54  }
55 
56 
57  [TestMethod]
58  public async Task TestDownload()
59  {
60  var mockIOManager = new Mock<IIOManager>();
61  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
62  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
63  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
64 
65  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.DownloadVersion(null, default)).ConfigureAwait(false);
66 
67  var ourArray = Array.Empty<byte>();
68  mockIOManager.Setup(x => x.DownloadFile(It.Is<Uri>(uri => uri == new Uri("https://secure.byond.com/download/build/511/511.1385_byond_linux.zip")), default)).Returns(Task.FromResult(ourArray)).Verifiable();
69 
70  var result = await installer.DownloadVersion(new Version(511, 1385), default).ConfigureAwait(false);
71 
72  Assert.AreSame(ourArray, result);
73  mockIOManager.Verify();
74  }
75 
76  [TestMethod]
77  public async Task TestInstallByond()
78  {
79  var mockIOManager = new Mock<IIOManager>();
80  var mockPostWriteHandler = new Mock<IPostWriteHandler>();
81  var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
82  var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
83 
84  const string FakePath = "fake";
85  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(null, null, default)).ConfigureAwait(false);
86  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(FakePath, null, default)).ConfigureAwait(false);
87 
88  await installer.InstallByond(FakePath, new Version(511, 1385), default).ConfigureAwait(false);
89 
90  mockIOManager.Verify(x => x.ConcatPath(It.IsAny<string>(), It.IsNotNull<string>()), Times.Exactly(5));
91  mockIOManager.Verify(x => x.WriteAllBytes(It.IsAny<string>(), It.IsNotNull<byte[]>(), default), Times.Exactly(2));
92  mockPostWriteHandler.Verify(x => x.HandleWrite(It.IsAny<string>()), Times.Exactly(4));
93  }
94  }
95 }
- + diff --git a/_test_post_write_handler_8cs_source.html b/_test_post_write_handler_8cs_source.html index 7cd465a357..802ae3da32 100644 --- a/_test_post_write_handler_8cs_source.html +++ b/_test_post_write_handler_8cs_source.html @@ -94,7 +94,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Go to the documentation of this file.
1 using Microsoft.VisualStudio.TestTools.UnitTesting;
2 using Mono.Unix;
3 using System;
4 using System.Diagnostics;
5 using System.IO;
6 using System.Runtime.InteropServices;
7 using System.Text;
8 
9 namespace Tgstation.Server.Host.IO.Tests
10 {
11  [TestClass]
12  public sealed class TestPostWriteHandler
13  {
14  [TestMethod]
15  public void TestThrowsWithNullArg()
16  {
17  IPostWriteHandler postWriteHandler;
18  var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
19  if (isWindows)
20  postWriteHandler = new WindowsPostWriteHandler();
21  else
22  postWriteHandler = new PosixPostWriteHandler();
23 
24  Assert.ThrowsException<ArgumentNullException>(() => postWriteHandler.HandleWrite(null));
25  }
26 
27  [TestMethod]
28  public void TestPostWrite()
29  {
30  IPostWriteHandler postWriteHandler;
31  var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
32  if (isWindows)
33  postWriteHandler = new WindowsPostWriteHandler();
34  else
35  postWriteHandler = new PosixPostWriteHandler();
36 
37  //test on a valid file first
38  var tmpFile = Path.GetTempFileName();
39  try
40  {
41  postWriteHandler.HandleWrite(tmpFile);
42 
43  if (isWindows)
44  return; //you do nothing
45 
46  //ensure it is now executable
47  File.WriteAllBytes(tmpFile, Encoding.UTF8.GetBytes("#!/bin/sh\n"));
48 
49  using (var process = Process.Start(tmpFile))
50  {
51  process.WaitForExit();
52  Assert.AreEqual(0, process.ExitCode);
53  }
54 
55  //run it again for the code coverage on that part where no changes are made if it's already executable
56  postWriteHandler.HandleWrite(tmpFile);
57  }
58  finally
59  {
60  File.Delete(tmpFile);
61  }
62  }
63 
64  [TestMethod]
65  public void TestThrowsOnUnix()
66  {
67  if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
68  return;
69  var postWriteHandler = new PosixPostWriteHandler();
70  var tmpFile = Path.GetTempFileName();
71  File.Delete(tmpFile);
72  Assert.ThrowsException<UnixIOException>(() => postWriteHandler.HandleWrite(tmpFile));
73  }
74  }
75 }
IPostWriteHandler for POSIX systems
-
+
Handles changing file modes/permissions after writing
void TestThrowsWithNullArg()
diff --git a/_test_server_service_8cs_source.html b/_test_server_service_8cs_source.html index 3d33a53bc7..9c049c30e9 100644 --- a/_test_server_service_8cs_source.html +++ b/_test_server_service_8cs_source.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Reflection;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Service.Tests
11 {
15  [TestClass]
16  public sealed class TestServerService
17  {
18  [TestMethod]
20  {
21  Assert.ThrowsException<ArgumentNullException>(() => new ServerService(null, null, default));
22  var mockWatchdogFactory = new Mock<IWatchdogFactory>();
23  Assert.ThrowsException<ArgumentNullException>(() => new ServerService(mockWatchdogFactory.Object, null, default));
24  var mockLoggerFactory = new LoggerFactory();
25  new ServerService(mockWatchdogFactory.Object, mockLoggerFactory, default).Dispose();
26  }
27 
28  [TestMethod]
29  public void TestRun()
30  {
31  var type = typeof(ServerService);
32  var onStart = type.GetMethod("OnStart", BindingFlags.Instance | BindingFlags.NonPublic);
33  var onStop = type.GetMethod("OnStop", BindingFlags.Instance | BindingFlags.NonPublic);
34 
35  var mockWatchdog = new Mock<IWatchdog>();
36  var args = Array.Empty<string>();
37  CancellationToken cancellationToken, originalToken;
38  using(var cts = new CancellationTokenSource())
39  {
40  originalToken = cts.Token;
41  cancellationToken = originalToken;
42  }
43  var cancelled = false;
44 
45  mockWatchdog.Setup(x => x.RunAsync(false, args, It.IsAny<CancellationToken>())).Callback((bool x, string[] _, CancellationToken token) => token.Register(() => cancelled = true)).Returns(Task.CompletedTask).Verifiable();
46  var mockWatchdogFactory = new Mock<IWatchdogFactory>();
47  var mockLoggerFactory = new LoggerFactory();
48  mockWatchdogFactory.Setup(x => x.CreateWatchdog(mockLoggerFactory)).Returns(mockWatchdog.Object).Verifiable();
49 
50  using (var service = new ServerService(mockWatchdogFactory.Object, mockLoggerFactory, default))
51  {
52  Assert.IsFalse(cancelled);
53  onStart.Invoke(service, new object[] { args });
54  mockWatchdog.VerifyAll();
55 
56  Assert.AreNotSame(cancellationToken, originalToken);
57 
58  onStop.Invoke(service, Array.Empty<object>());
59  Assert.IsTrue(cancelled);
60  }
61  mockWatchdogFactory.VerifyAll();
62  }
63  }
64 }
-
override void Dispose(bool disposing)
+
override void Dispose(bool disposing)
Represents a IWatchdog as a ServiceBase
diff --git a/_tgs_authorize_attribute_8cs_source.html b/_tgs_authorize_attribute_8cs_source.html index 6bc17d17c3..df5c1a3043 100644 --- a/_tgs_authorize_attribute_8cs_source.html +++ b/_tgs_authorize_attribute_8cs_source.html @@ -92,24 +92,33 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TgsAuthorizeAttribute.cs
-Go to the documentation of this file.
1 using System;
2 using Microsoft.AspNetCore.Authorization;
4 
5 namespace Tgstation.Server.Host.Controllers
6 {
10  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
12  {
16  public TgsAuthorizeAttribute() { }
17 
22  public TgsAuthorizeAttribute(AdministrationRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
23 
28  public TgsAuthorizeAttribute(InstanceManagerRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
29 
34  public TgsAuthorizeAttribute(RepositoryRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
35 
40  public TgsAuthorizeAttribute(ByondRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
41 
46  public TgsAuthorizeAttribute(DreamMakerRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
47 
52  public TgsAuthorizeAttribute(DreamDaemonRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
53 
58  public TgsAuthorizeAttribute(ChatBotRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
59 
64  public TgsAuthorizeAttribute(ConfigurationRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
65 
70  public TgsAuthorizeAttribute(InstanceUserRights requiredRights) => Roles = RightsHelper.RoleNames(requiredRights);
71  }
72 }
ConfigurationRights
Rights for Models.ConfigurationFile
+Go to the documentation of this file.
1 using System;
2 using Microsoft.AspNetCore.Authorization;
4 
5 namespace Tgstation.Server.Host.Controllers
6 {
10  #pragma warning disable CA1019
11  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
13  {
17  public TgsAuthorizeAttribute() { }
18 
24  {
25  Roles = RightsHelper.RoleNames(requiredRights);
26  }
27 
33  {
34  Roles = RightsHelper.RoleNames(requiredRights);
35  }
36 
41  public TgsAuthorizeAttribute(RepositoryRights requiredRights)
42  {
43  Roles = RightsHelper.RoleNames(requiredRights);
44  }
45 
50  public TgsAuthorizeAttribute(ByondRights requiredRights)
51  {
52  Roles = RightsHelper.RoleNames(requiredRights);
53  }
54 
59  public TgsAuthorizeAttribute(DreamMakerRights requiredRights)
60  {
61  Roles = RightsHelper.RoleNames(requiredRights);
62  }
63 
69  {
70  Roles = RightsHelper.RoleNames(requiredRights);
71  }
72 
77  public TgsAuthorizeAttribute(ChatBotRights requiredRights)
78  {
79  Roles = RightsHelper.RoleNames(requiredRights);
80  }
81 
87  {
88  Roles = RightsHelper.RoleNames(requiredRights);
89  }
90 
96  {
97  Roles = RightsHelper.RoleNames(requiredRights);
98  }
99  }
100 }
ConfigurationRights
Rights for Models.ConfigurationFile
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
InstanceUserRights
Rights for an Models.Instance
-
Helper for using the AuthorizeAttribute with the Api.Rights system
+
TgsAuthorizeAttribute(ByondRights requiredRights)
Construct a TgsAuthorizeAttribute for ByondRights
+
Helper for using the AuthorizeAttribute with the Api.Rights system
+
TgsAuthorizeAttribute(InstanceManagerRights requiredRights)
Construct a TgsAuthorizeAttribute for InstanceManagerRights
AdministrationRights
Rights for Models.Administration
- +
DreamDaemonRights
Rights for Models.DreamDaemon
InstanceManagerRights
Rights for managing Models.Instances
+
TgsAuthorizeAttribute(AdministrationRights requiredRights)
Construct a TgsAuthorizeAttribute for AdministrationRights
DreamMakerRights
Rights for Models.DreamMaker
+
TgsAuthorizeAttribute(ConfigurationRights requiredRights)
Construct a TgsAuthorizeAttribute for ConfigurationRights
+
TgsAuthorizeAttribute(DreamMakerRights requiredRights)
Construct a TgsAuthorizeAttribute for DreamMakerRights
RepositoryRights
Rights for a Models.Repository
+
TgsAuthorizeAttribute(InstanceUserRights requiredRights)
Construct a TgsAuthorizeAttribute for InstanceUserRights
+
TgsAuthorizeAttribute(RepositoryRights requiredRights)
Construct a TgsAuthorizeAttribute for RepositoryRights
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
+
TgsAuthorizeAttribute(ChatBotRights requiredRights)
Construct a TgsAuthorizeAttribute for ChatBotRights
+
TgsAuthorizeAttribute(DreamDaemonRights requiredRights)
Construct a TgsAuthorizeAttribute for DreamDaemonRights
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 92ed8c34bc..580d83b288 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 @@ -92,7 +92,7 @@ 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  public string Name { get; set; }
21 
25  [Required]
26  public string Path { get; set; }
27 
31  [Required]
32  public bool? Online { get; set; }
33 
37  [Required]
38  public ConfigurationType? ConfigurationType { get; set; }
39 
43  [Required]
44  public uint? AutoUpdateInterval { get; set; }
45 
50  [NotMapped]
51  public Job MoveJob { get; set; }
52 
54  public Instance CloneMetadata() => new Instance
55  {
56  Id = Id,
57  Name = Name,
58  Path = Path,
59  Online = Online,
61  AutoUpdateInterval = AutoUpdateInterval
62  };
63  }
64 }
+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  public string Name { get; set; }
21 
25  [Required]
26  public string Path { get; set; }
27 
31  [Required]
32  public bool? Online { get; set; }
33 
37  [Required]
38  public ConfigurationType? ConfigurationType { get; set; }
39 
43  [Required]
44  public uint? AutoUpdateInterval { get; set; }
45 
50  [NotMapped]
51  public Job MoveJob { get; set; }
52 
57  public Instance CloneMetadata() => new Instance
58  {
59  Id = Id,
60  Name = Name,
61  Path = Path,
62  Online = Online,
64  AutoUpdateInterval = AutoUpdateInterval
65  };
66  }
67 }
ConfigurationType
The type of configuration allowed on an Instance
Metadata about a server instance
Definition: Instance.cs:9
diff --git a/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs_source.html b/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs_source.html index 16acfba64e..7c33abfbec 100644 --- a/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs_source.html +++ b/_tgstation_8_server_8_api_2_models_2_internal_2_revision_information_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RevisionInformation.cs
-Go to the documentation of this file.
1 using System.ComponentModel.DataAnnotations;
2 
3 namespace Tgstation.Server.Api.Models.Internal
4 {
8  public class RevisionInformation
9  {
13  [Required, StringLength(40)]
14  public string CommitSha { get; set; }
15 
19  [Required, StringLength(40)]
20  public string OriginCommitSha { get; set; }
21  }
22 }
+Go to the documentation of this file.
1 using System.ComponentModel.DataAnnotations;
2 
3 namespace Tgstation.Server.Api.Models.Internal
4 {
8  public class RevisionInformation
9  {
13  [Required]
14  [StringLength(40)]
15  public string CommitSha { get; set; }
16 
20  [Required]
21  [StringLength(40)]
22  public string OriginCommitSha { get; set; }
23  }
24 }
Represents information about a current git revison
diff --git a/_tgstation_8_server_8_api_8_assembly_info_8cs.html b/_tgstation_8_server_8_api_8_assembly_info_8cs.html index 88fc90fce7..823d805b2e 100644 --- a/_tgstation_8_server_8_api_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_api_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Api/obj/Release/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Api/obj/Debug/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html index 3f1185acce..a8d68ea62e 100644 --- a/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Api/obj/Release/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Api/obj/Debug/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Api.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("API definitions for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.0.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.0.0")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.0.0")]
23 
24 // Generated by the MSBuild WriteCodeFragment class.
25 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("API definitions for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.0.1")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.0.1")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.0.1")]
23 
24 // Generated by the MSBuild WriteCodeFragment class.
25 
diff --git a/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html index f0cb13b64e..8aba9d97b3 100644 --- a/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Api.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Api.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Api.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Api.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Api.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html index f735cdd6bf..e0e4ccf475 100644 --- a/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Api.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Api.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_client_8_assembly_info_8cs.html b/_tgstation_8_server_8_client_8_assembly_info_8cs.html index c1b2251f60..fb681e6583 100644 --- a/_tgstation_8_server_8_client_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_client_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Client/obj/Release/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Client/obj/Debug/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html index b98b2e720e..45ab24c0da 100644 --- a/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Client/obj/Release/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Client/obj/Debug/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Client.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station 13")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("Client library for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.2.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.2")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.2.0")]
23 [assembly: System.Resources.NeutralResourcesLanguageAttribute("en-CA")]
24 
25 // Generated by the MSBuild WriteCodeFragment class.
26 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station 13")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("Client library for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.2.1")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.2.1")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.2.1")]
23 [assembly: System.Resources.NeutralResourcesLanguageAttribute("en-CA")]
24 
25 // Generated by the MSBuild WriteCodeFragment class.
26 
diff --git a/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html index 3eef0f64ad..3c2a097db5 100644 --- a/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Client.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Client.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Client.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Client.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Client.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html index 0bb889fa5e..9ef26d4cd5 100644 --- a/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Client.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Client.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_2_components_2_compiler_2_dream_maker_8cs_source.html b/_tgstation_8_server_8_host_2_components_2_compiler_2_dream_maker_8cs_source.html index 99dceefe03..c6ab59db70 100644 --- a/_tgstation_8_server_8_host_2_components_2_compiler_2_dream_maker_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_compiler_2_dream_maker_8cs_source.html @@ -92,14 +92,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamMaker.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Linq;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
17 
18 namespace Tgstation.Server.Host.Components.Compiler
19 {
21  sealed class DreamMaker : IDreamMaker
22  {
26  public const string ADirectoryName = "A";
30  public const string BDirectoryName = "B";
34  public const string DmbExtension = ".dmb";
38  const string DmeExtension = "dme";
39 
43  readonly IByondManager byond;
51  readonly StaticFiles.IConfiguration configuration;
71  readonly IChat chat;
79  readonly IWatchdog watchdog;
83  readonly ILogger<DreamMaker> logger;
84 
88  bool compiling;
89 
104  public DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, IProcessExecutor processExecutor, IWatchdog watchdog, ILogger<DreamMaker> logger)
105  {
106  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
107  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
108  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
109  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
110  this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
111  this.application = application ?? throw new ArgumentNullException(nameof(application));
112  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
113  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
114  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
115  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
116  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
117  }
118 
129  async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
130  {
131  logger.LogTrace("Verifying DMAPI...");
132  var launchParameters = new DreamDaemonLaunchParameters
133  {
134  AllowWebClient = false,
135  PrimaryPort = portToUse,
136  SecurityLevel = securityLevel, //all it needs to read the file and exit
137  StartupTimeout = timeout
138  };
139 
140  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
141 
142  job.MinimumSecurityLevel = securityLevel; //needed for the TempDmbProvider
143  var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job);
144 
145  var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);
146  using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false))
147  {
148  var launchResult = await controller.LaunchResult.ConfigureAwait(false);
149 
150  var now = DateTimeOffset.Now;
151  if (now < timeoutAt && launchResult.StartupTime.HasValue)
152  {
153  var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken);
154 
155  await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);
156  cancellationToken.ThrowIfCancellationRequested();
157  }
158 
159  if (controller.Lifetime.IsCompleted)
160  {
161  var validationStatus = controller.ApiValidationStatus;
162  logger.LogTrace("API validation status: {0}", validationStatus);
163  switch (validationStatus)
164  {
165  case ApiValidationStatus.RequiresUltrasafe:
166  job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
167  return;
168  case ApiValidationStatus.RequiresSafe:
169  if (securityLevel == DreamDaemonSecurity.Ultrasafe)
170  throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!");
171  job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
172  return;
173  case ApiValidationStatus.RequiresTrusted:
174  if (securityLevel != DreamDaemonSecurity.Trusted)
175  throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!");
176  job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
177  return;
178  case ApiValidationStatus.NeverValidated:
179  break;
180  case ApiValidationStatus.BadValidationRequest:
181  throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!");
182  case ApiValidationStatus.UnaskedValidationRequest:
183  default:
184  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus));
185  }
186  }
187 
188  throw new JobException("DMAPI validation timed out!");
189  }
190  }
191 
199  async Task<int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
200  {
201  using (var dm = processExecutor.LaunchProcess(dreamMakerPath, ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)), String.Format(CultureInfo.InvariantCulture, "-clean {0}.{1}", job.DmeName, DmeExtension), true, true))
202  {
203  int exitCode;
204  using (cancellationToken.Register(() => dm.Terminate()))
205  exitCode = await dm.Lifetime.ConfigureAwait(false);
206  cancellationToken.ThrowIfCancellationRequested();
207 
208  logger.LogDebug("DreamMaker exit code: {0}", exitCode);
209  job.Output = dm.GetCombinedOutput();
210  logger.LogTrace("DreamMaker output: {0}{1}", Environment.NewLine, job.Output);
211  return exitCode;
212  }
213  }
214 
221  async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
222  {
223  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
224  var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
225  var dmePath = ioManager.ConcatPath(dirA, dmeFileName);
226  var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
227 
228  var dmeModificationsTask = configuration.CopyDMFilesTo(dmeFileName, ioManager.ResolvePath(dirA), cancellationToken);
229 
230  var dmeBytes = await dmeReadTask.ConfigureAwait(false);
231  var dme = Encoding.UTF8.GetString(dmeBytes);
232 
233  var dmeModifications = await dmeModificationsTask.ConfigureAwait(false);
234 
235  if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
236  {
237  if (dmeModifications != null)
238  logger.LogDebug(".dme replacement configured!");
239  else
240  logger.LogTrace("No .dme modifications required.");
241  return;
242  }
243 
244  if (dmeModifications.HeadIncludeLine != null)
245  logger.LogDebug("Head .dme include line: {0}", dmeModifications.HeadIncludeLine);
246  if (dmeModifications.TailIncludeLine != null)
247  logger.LogDebug("Tail .dme include line: {0}", dmeModifications.TailIncludeLine);
248 
249  var dmeLines = new List<string>(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None));
250  for (var I = 0; I < dmeLines.Count; ++I)
251  {
252  var line = dmeLines[I];
253  if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
254  {
255  dmeLines.Insert(I + 1, dmeModifications.HeadIncludeLine);
256  ++I;
257  }
258  else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
259  {
260  dmeLines.Insert(I, dmeModifications.TailIncludeLine);
261  break;
262  }
263  }
264 
265  dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines));
266  await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false);
267  }
268 
270  public async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
271  {
272  if (revisionInformation == null)
273  throw new ArgumentNullException(nameof(revisionInformation));
274 
275  if (dreamMakerSettings == null)
276  throw new ArgumentNullException(nameof(dreamMakerSettings));
277 
278  if (repository == null)
279  throw new ArgumentNullException(nameof(repository));
280 
281  if (progressReporter == null)
282  throw new ArgumentNullException(nameof(progressReporter));
283 
284  if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
285  throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!");
286 
287  logger.LogTrace("Begin Compile");
288 
289  var job = new Models.CompileJob
290  {
291  DirectoryName = Guid.NewGuid(),
292  DmeName = dreamMakerSettings.ProjectName,
293  RevisionInformation = revisionInformation
294  };
295 
296  logger.LogTrace("Compile output GUID: {0}", job.DirectoryName);
297 
298  lock (this)
299  {
300  if (compiling)
301  throw new JobException("There is already a compile job in progress!");
302  compiling = true;
303  }
304 
305  using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
306  {
307  async Task ProgressTask()
308  {
309  if (!estimatedDuration.HasValue)
310  return;
311 
312  progressReporter(0);
313  var ct = progressCts.Token;
314  var sleepInterval = estimatedDuration.Value / 100;
315 
316  try
317  {
318  for (var I = 0; I < 99; ++I)
319  {
320  await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false);
321  progressReporter(I + 1);
322  }
323  }
324  catch (OperationCanceledException) { }
325  }
326 
327  var progressTask = ProgressTask();
328  try
329  {
330 
331 
332  var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
333  string remoteCommitInsert;
334  if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
335  {
336  commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
337  remoteCommitInsert = String.Empty;
338  }
339  else
340  remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
341 
342  var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
343  String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
344  {
345  var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
346  if (x.Comment != null)
347  result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
348  return result;
349  })));
350 
351  using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false))
352  {
353  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false);
354 
355  async Task CleanupFailedCompile(bool cancelled)
356  {
357  logger.LogTrace("Cleaning compile directory...");
358  var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);
359  try
360  {
361  await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
362  }
363  catch (Exception e)
364  {
365  logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e);
366  }
367 
368  await chatTask.ConfigureAwait(false);
369  };
370 
371  try
372  {
373  await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
374 
375  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
376  var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
377 
378  logger.LogTrace("Copying repository to game directory...");
379  //copy the repository
380  var fullDirA = ioManager.ResolvePath(dirA);
381  var repoOrigin = repository.Origin;
382  using (repository)
383  await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
384 
385  //run precompile scripts
386  var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
387  await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
388 
389  //determine the dme
390  if (job.DmeName == null)
391  {
392  logger.LogTrace("Searching for available .dmes...");
393  var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
394  if (path == default)
395  throw new JobException("Unable to find any .dme!");
396  var dmeWithExtension = ioManager.GetFileName(path);
397  job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
398  }
399  else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false))
400  throw new JobException("Unable to locate specified .dme!");
401 
402  logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);
403 
404  await ModifyDme(job, cancellationToken).ConfigureAwait(false);
405 
406  //run compiler, verify api
407  job.ByondVersion = byondLock.Version.ToString();
408 
409  var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
410 
411  try
412  {
413  if (exitCode != 0)
414  throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
415 
416  await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
417  }
418  catch (JobException)
419  {
420  //server never validated or compile failed
421  await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
422  throw;
423  }
424 
425  logger.LogTrace("Running post compile event...");
426  await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false);
427 
428  logger.LogTrace("Duplicating compiled game...");
429 
430  //duplicate the dmb et al
431  await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
432 
433  logger.LogTrace("Applying static game file symlinks...");
434 
435  //symlink in the static data
436  var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
437  var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
438 
439  await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
440 
441  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false);
442 
443  logger.LogDebug("Compile complete!");
444  return job;
445  }
446  catch (Exception e)
447  {
448  await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false);
449  throw;
450  }
451  }
452  }
453  catch (OperationCanceledException)
454  {
455  await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);
456  throw;
457  }
458  finally
459  {
460  compiling = false;
461  progressCts.Cancel();
462  await progressTask.ConfigureAwait(false);
463  }
464  }
465  }
466  }
467 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Globalization;
5 using System.Linq;
6 using System.Text;
7 using System.Threading;
8 using System.Threading.Tasks;
16 using Tgstation.Server.Host.IO;
17 
18 namespace Tgstation.Server.Host.Components.Compiler
19 {
21  sealed class DreamMaker : IDreamMaker
22  {
26  public const string ADirectoryName = "A";
27 
31  public const string BDirectoryName = "B";
32 
36  public const string DmbExtension = ".dmb";
37 
41  const string DmeExtension = "dme";
42 
46  readonly IByondManager byond;
47 
52 
56  readonly StaticFiles.IConfiguration configuration;
57 
62 
67 
72 
77 
81  readonly IChat chat;
82 
87 
91  readonly IWatchdog watchdog;
92 
96  readonly ILogger<DreamMaker> logger;
97 
101  bool compiling;
102 
117  public DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, IProcessExecutor processExecutor, IWatchdog watchdog, ILogger<DreamMaker> logger)
118  {
119  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
120  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
121  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
122  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
123  this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
124  this.application = application ?? throw new ArgumentNullException(nameof(application));
125  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
126  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
127  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
128  this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
129  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
130  }
131 
142  async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
143  {
144  logger.LogTrace("Verifying DMAPI...");
145  var launchParameters = new DreamDaemonLaunchParameters
146  {
147  AllowWebClient = false,
148  PrimaryPort = portToUse,
149  SecurityLevel = securityLevel,
150  StartupTimeout = timeout
151  };
152 
153  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
154 
155  job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
156  var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job);
157 
158  var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);
159  using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false))
160  {
161  var launchResult = await controller.LaunchResult.ConfigureAwait(false);
162 
163  var now = DateTimeOffset.Now;
164  if (now < timeoutAt && launchResult.StartupTime.HasValue)
165  {
166  var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken);
167 
168  await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);
169  cancellationToken.ThrowIfCancellationRequested();
170  }
171 
172  if (controller.Lifetime.IsCompleted)
173  {
174  var validationStatus = controller.ApiValidationStatus;
175  logger.LogTrace("API validation status: {0}", validationStatus);
176  switch (validationStatus)
177  {
178  case ApiValidationStatus.RequiresUltrasafe:
179  job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe;
180  return;
181  case ApiValidationStatus.RequiresSafe:
182  if (securityLevel == DreamDaemonSecurity.Ultrasafe)
183  throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!");
184  job.MinimumSecurityLevel = DreamDaemonSecurity.Safe;
185  return;
186  case ApiValidationStatus.RequiresTrusted:
187  if (securityLevel != DreamDaemonSecurity.Trusted)
188  throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!");
189  job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted;
190  return;
191  case ApiValidationStatus.NeverValidated:
192  break;
193  case ApiValidationStatus.BadValidationRequest:
194  throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!");
195  case ApiValidationStatus.UnaskedValidationRequest:
196  default:
197  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus));
198  }
199  }
200 
201  throw new JobException("DMAPI validation timed out!");
202  }
203  }
204 
212  async Task<int> RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
213  {
214  using (var dm = processExecutor.LaunchProcess(dreamMakerPath, ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)), String.Format(CultureInfo.InvariantCulture, "-clean {0}.{1}", job.DmeName, DmeExtension), true, true))
215  {
216  int exitCode;
217  using (cancellationToken.Register(() => dm.Terminate()))
218  exitCode = await dm.Lifetime.ConfigureAwait(false);
219  cancellationToken.ThrowIfCancellationRequested();
220 
221  logger.LogDebug("DreamMaker exit code: {0}", exitCode);
222  job.Output = dm.GetCombinedOutput();
223  logger.LogTrace("DreamMaker output: {0}{1}", Environment.NewLine, job.Output);
224  return exitCode;
225  }
226  }
227 
234  async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
235  {
236  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
237  var dmeFileName = String.Join('.', job.DmeName, DmeExtension);
238  var dmePath = ioManager.ConcatPath(dirA, dmeFileName);
239  var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
240 
241  var dmeModificationsTask = configuration.CopyDMFilesTo(dmeFileName, ioManager.ResolvePath(dirA), cancellationToken);
242 
243  var dmeBytes = await dmeReadTask.ConfigureAwait(false);
244  var dme = Encoding.UTF8.GetString(dmeBytes);
245 
246  var dmeModifications = await dmeModificationsTask.ConfigureAwait(false);
247 
248  if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
249  {
250  if (dmeModifications != null)
251  logger.LogDebug(".dme replacement configured!");
252  else
253  logger.LogTrace("No .dme modifications required.");
254  return;
255  }
256 
257  if (dmeModifications.HeadIncludeLine != null)
258  logger.LogDebug("Head .dme include line: {0}", dmeModifications.HeadIncludeLine);
259  if (dmeModifications.TailIncludeLine != null)
260  logger.LogDebug("Tail .dme include line: {0}", dmeModifications.TailIncludeLine);
261 
262  var dmeLines = new List<string>(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None));
263  for (var I = 0; I < dmeLines.Count; ++I)
264  {
265  var line = dmeLines[I];
266  if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null)
267  {
268  dmeLines.Insert(I + 1, dmeModifications.HeadIncludeLine);
269  ++I;
270  }
271  else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null)
272  {
273  dmeLines.Insert(I, dmeModifications.TailIncludeLine);
274  break;
275  }
276  }
277 
278  dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines));
279  await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false);
280  }
281 
283  #pragma warning disable CA1506 //TODO: Decomplexify
284  public async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
285  {
286  if (revisionInformation == null)
287  throw new ArgumentNullException(nameof(revisionInformation));
288 
289  if (dreamMakerSettings == null)
290  throw new ArgumentNullException(nameof(dreamMakerSettings));
291 
292  if (repository == null)
293  throw new ArgumentNullException(nameof(repository));
294 
295  if (progressReporter == null)
296  throw new ArgumentNullException(nameof(progressReporter));
297 
298  if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe)
299  throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!");
300 
301  logger.LogTrace("Begin Compile");
302 
303  var job = new Models.CompileJob
304  {
305  DirectoryName = Guid.NewGuid(),
306  DmeName = dreamMakerSettings.ProjectName,
307  RevisionInformation = revisionInformation
308  };
309 
310  logger.LogTrace("Compile output GUID: {0}", job.DirectoryName);
311 
312  lock (this)
313  {
314  if (compiling)
315  throw new JobException("There is already a compile job in progress!");
316  compiling = true;
317  }
318 
319  using (var progressCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
320  {
321  async Task ProgressTask()
322  {
323  if (!estimatedDuration.HasValue)
324  return;
325 
326  progressReporter(0);
327  var ct = progressCts.Token;
328  var sleepInterval = estimatedDuration.Value / 100;
329 
330  try
331  {
332  for (var I = 0; I < 99; ++I)
333  {
334  await Task.Delay(sleepInterval, progressCts.Token).ConfigureAwait(false);
335  progressReporter(I + 1);
336  }
337  }
338  catch (OperationCanceledException) { }
339  }
340 
341  var progressTask = ProgressTask();
342  try
343  {
344  var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
345  string remoteCommitInsert;
346  if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
347  {
348  commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert);
349  remoteCommitInsert = String.Empty;
350  }
351  else
352  remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7));
353 
354  var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})",
355  String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x =>
356  {
357  var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7));
358  if (x.Comment != null)
359  result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment);
360  return result;
361  })));
362 
363  using (var byondLock = await byond.UseExecutables(null, cancellationToken).ConfigureAwait(false))
364  {
365  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false);
366 
367  async Task CleanupFailedCompile(bool cancelled)
368  {
369  logger.LogTrace("Cleaning compile directory...");
370  var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken);
371  try
372  {
373  await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
374  }
375  catch (Exception e)
376  {
377  logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e);
378  }
379 
380  await chatTask.ConfigureAwait(false);
381  }
382 
383  try
384  {
385  await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
386 
387  var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
388  var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
389 
390  // copy the repository
391  logger.LogTrace("Copying repository to game directory...");
392  var fullDirA = ioManager.ResolvePath(dirA);
393  var repoOrigin = repository.Origin;
394  using (repository)
395  await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
396 
397  // run precompile scripts
398  var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
399  await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
400 
401  // determine the dme
402  if (job.DmeName == null)
403  {
404  logger.LogTrace("Searching for available .dmes...");
405  var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
406  if (path == default)
407  throw new JobException("Unable to find any .dme!");
408  var dmeWithExtension = ioManager.GetFileName(path);
409  job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
410  }
411  else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false))
412  throw new JobException("Unable to locate specified .dme!");
413 
414  logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);
415 
416  await ModifyDme(job, cancellationToken).ConfigureAwait(false);
417 
418  // run compiler
419  job.ByondVersion = byondLock.Version.ToString();
420 
421  var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
422 
423  // verify api
424  try
425  {
426  if (exitCode != 0)
427  throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output));
428 
429  await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false);
430  }
431  catch (JobException)
432  {
433  // DD never validated or compile failed
434  await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
435  throw;
436  }
437 
438  logger.LogTrace("Running post compile event...");
439  await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false);
440 
441  logger.LogTrace("Duplicating compiled game...");
442 
443  // duplicate the dmb et al
444  await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
445 
446  logger.LogTrace("Applying static game file symlinks...");
447 
448  // symlink in the static data
449  var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
450  var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
451 
452  await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
453 
454  await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false);
455 
456  logger.LogDebug("Compile complete!");
457  return job;
458  }
459  catch (Exception e)
460  {
461  await CleanupFailedCompile(e is OperationCanceledException).ConfigureAwait(false);
462  throw;
463  }
464  }
465  }
466  catch (OperationCanceledException)
467  {
468  await eventConsumer.HandleEvent(EventType.CompileCancelled, null, default).ConfigureAwait(false);
469  throw;
470  }
471  finally
472  {
473  compiling = false;
474  progressCts.Cancel();
475  await progressTask.ConfigureAwait(false);
476  }
477  }
478  }
479  #pragma warning restore CA1506
480  }
481 }
-
readonly ILogger< DreamMaker > logger
The ILogger for DreamMaker
Definition: DreamMaker.cs:83
+
readonly ILogger< DreamMaker > logger
The ILogger for DreamMaker
Definition: DreamMaker.cs:96
Consumes EventTypes and takes the appropriate actions
-
readonly StaticFiles.IConfiguration configuration
The StaticFiles.IConfiguration for DreamMaker
Definition: DreamMaker.cs:51
+
readonly StaticFiles.IConfiguration configuration
The StaticFiles.IConfiguration for DreamMaker
Definition: DreamMaker.cs:56
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
@@ -109,40 +109,40 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
Represents an on-disk git repository
Definition: IRepository.cs:11
-
async Task< Models.CompileJob > Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action< int > progressReporter, TimeSpan?estimatedDuration, CancellationToken cancellationToken)
Definition: DreamMaker.cs:270
-
bool compiling
If a compile job is running
Definition: DreamMaker.cs:88
+
async Task< Models.CompileJob > Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action< int > progressReporter, TimeSpan?estimatedDuration, CancellationToken cancellationToken)
Definition: DreamMaker.cs:284
+
bool compiling
If a compile job is running
Definition: DreamMaker.cs:101
For managing connected chat services
Definition: IChat.cs:13
-
readonly IProcessExecutor processExecutor
The IProcessExecutor for DreamMaker
Definition: DreamMaker.cs:75
-
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for DreamMaker
Definition: DreamMaker.cs:55
-
readonly IEventConsumer eventConsumer
The IEventConsumer for DreamMaker
Definition: DreamMaker.cs:67
+
readonly IProcessExecutor processExecutor
The IProcessExecutor for DreamMaker
Definition: DreamMaker.cs:86
+
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for DreamMaker
Definition: DreamMaker.cs:61
+
readonly IEventConsumer eventConsumer
The IEventConsumer for DreamMaker
Definition: DreamMaker.cs:76
-
async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
Run a quick DD instance to test the DMAPI is installed on the target code
Definition: DreamMaker.cs:129
+
async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken)
Run a quick DD instance to test the DMAPI is installed on the target code
Definition: DreamMaker.cs:142
string Origin
The current origin remote the IRepository is using
Definition: IRepository.cs:46
For managing the BYOND installation
-
async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
Adds server side includes to the .dme being compiled
Definition: DreamMaker.cs:221
-
readonly IChat chat
The IChat for DreamMaker
Definition: DreamMaker.cs:71
+
async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
Adds server side includes to the .dme being compiled
Definition: DreamMaker.cs:234
+
readonly IChat chat
The IChat for DreamMaker
Definition: DreamMaker.cs:81
-
readonly IWatchdog watchdog
The IWatchdog for DreamMaker
Definition: DreamMaker.cs:79
+
readonly IWatchdog watchdog
The IWatchdog for DreamMaker
Definition: DreamMaker.cs:91
Task CopyTo(string path, CancellationToken cancellationToken)
Copies the current working directory to a given path
Represents usage of the two primary BYOND server executables
-
readonly ICompileJobConsumer compileJobConsumer
The ICompileJobConsumer for DreamMaker
Definition: DreamMaker.cs:59
+
readonly ICompileJobConsumer compileJobConsumer
The ICompileJobConsumer for DreamMaker
Definition: DreamMaker.cs:66
DreamDaemonSecurity
DreamDaemon&#39;s security level
-
readonly IIOManager ioManager
The IIOManager for DreamMaker
Definition: DreamMaker.cs:47
-
DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, IProcessExecutor processExecutor, IWatchdog watchdog, ILogger< DreamMaker > logger)
Construct DreamMaker
Definition: DreamMaker.cs:104
+
readonly IIOManager ioManager
The IIOManager for DreamMaker
Definition: DreamMaker.cs:51
+
DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, IProcessExecutor processExecutor, IWatchdog watchdog, ILogger< DreamMaker > logger)
Construct DreamMaker
Definition: DreamMaker.cs:117
Interface for using filesystems
Definition: IIOManager.cs:11
Represents information about a current git revison
-
readonly IApplication application
The IApplication for DreamMaker
Definition: DreamMaker.cs:63
+
readonly IApplication application
The IApplication for DreamMaker
Definition: DreamMaker.cs:71
-
async Task< int > RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
Compiles a .dme with DreamMaker
Definition: DreamMaker.cs:199
+
async Task< int > RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
Compiles a .dme with DreamMaker
Definition: DreamMaker.cs:212
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:8
-
readonly IByondManager byond
The IByondManager for DreamMaker
Definition: DreamMaker.cs:43
+
readonly IByondManager byond
The IByondManager for DreamMaker
Definition: DreamMaker.cs:46
Runs and monitors the twin server controllers
Definition: IWatchdog.cs:12
ApiValidationStatus
Status of DMAPI validation
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 954f0fc938..40a038742d 100644 --- a/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html @@ -92,18 +92,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Instance.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Octokit;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
18 
19 namespace Tgstation.Server.Host.Components
20 {
22  sealed class Instance : IInstance
23  {
26 
28  public IByondManager ByondManager { get; }
29 
31  public IDreamMaker DreamMaker { get; }
32 
34  public IWatchdog Watchdog { get; }
35 
37  public IChat Chat { get; }
38 
40  public StaticFiles.IConfiguration Configuration { get; }
41 
43  public ICompileJobConsumer CompileJobConsumer { get; }
44 
49 
54 
59 
64 
69 
73  readonly ILogger<Instance> logger;
74 
78  readonly Api.Models.Instance metadata;
79 
83  Task timerTask;
87  CancellationTokenSource timerCts;
88 
106  public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IGitHubClientFactory gitHubClientFactory, ILogger<Instance> logger)
107  {
108  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
109  RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
110  ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
111  DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
112  Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
113  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
114  Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
115  CompileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
116  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
117  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
118  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
119  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
120  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
121  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
122  }
123 
125  public void Dispose()
126  {
127  timerCts?.Dispose();
128  CompileJobConsumer.Dispose();
129  Configuration.Dispose();
130  Chat.Dispose();
131  Watchdog.Dispose();
133  }
134 
136  public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action<int> progressReporter, CancellationToken cancellationToken)
137  {
138  //DO NOT FOLLOW THE SUGGESTION FOR A THROW EXPRESSION HERE
139  if (job == null)
140  throw new ArgumentNullException(nameof(job));
141  if (databaseContext == null)
142  throw new ArgumentNullException(nameof(databaseContext));
143  if (progressReporter == null)
144  throw new ArgumentNullException(nameof(progressReporter));
145 
146  var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings
147  {
148  StartupTimeout = x.StartupTimeout,
149  }).FirstOrDefaultAsync(cancellationToken);
150 
151  var compileJobsTask = databaseContext.CompileJobs
152  .Where(x => x.Job.Instance.Id == metadata.Id)
153  .OrderByDescending(x => x.Job.StoppedAt)
154  //TODO: Replace with this select when the issues linked in https://github.com/tgstation/tgstation-server/issues/737 are fixed
155  //.Select(x => x.Job.StoppedAt.Value - x.Job.StartedAt.Value)
156  .Select(x => new Job
157  {
158  StoppedAt = x.Job.StoppedAt,
159  StartedAt = x.Job.StartedAt
160  })
161  .Take(10)
162  .ToListAsync(cancellationToken);
163 
164  var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
165  if (dreamMakerSettings == default)
166  throw new JobException("Missing DreamMakerSettings in DB!");
167  var ddSettings = await ddSettingsTask.ConfigureAwait(false);
168  if (ddSettings == default)
169  throw new JobException("Missing DreamDaemonSettings in DB!");
170 
171  Task<RepositorySettings> repositorySettingsTask = null;
172  string repoOwner = null;
173  string repoName = null;
174  CompileJob compileJob;
175  RevisionInformation revInfo;
176  using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
177  {
178  if (repo == null)
179  throw new JobException("Missing Repository!");
180 
181  if (repo.IsGitHubRepository)
182  {
183  repoOwner = repo.GitHubOwner;
184  repoName = repo.GitHubRepoName;
185  repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).Select(x => new RepositorySettings
186  {
187  AccessToken = x.AccessToken,
188  ShowTestMergeCommitters = x.ShowTestMergeCommitters
189  }).FirstOrDefaultAsync(cancellationToken);
190  }
191 
192  var repoSha = repo.Head;
193  revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy).FirstOrDefaultAsync().ConfigureAwait(false);
194 
195  if (revInfo == default)
196  {
197  revInfo = new RevisionInformation
198  {
199  CommitSha = repoSha,
200  OriginCommitSha = repoSha,
201  Instance = new Models.Instance
202  {
203  Id = metadata.Id
204  }
205  };
206  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
207  databaseContext.Instances.Attach(revInfo.Instance);
208  }
209 
210  TimeSpan? averageSpan = null;
211  var previousCompileJobs = await compileJobsTask.ConfigureAwait(false);
212  if(previousCompileJobs.Count != 0)
213  {
214  var totalSpan = TimeSpan.Zero;
215  foreach (var I in previousCompileJobs)
216  totalSpan += I.StoppedAt.Value - I.StartedAt.Value;
217  averageSpan = totalSpan / previousCompileJobs.Count;
218  }
219 
220  compileJob = await DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, progressReporter, averageSpan, cancellationToken).ConfigureAwait(false);
221  }
222 
223  compileJob.Job = job;
224 
225  databaseContext.CompileJobs.Add(compileJob); //will be saved by job context
226 
227  job.PostComplete = ct => CompileJobConsumer.LoadCompileJob(compileJob, ct);
228 
229  if (repositorySettingsTask != null)
230  {
231  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
232  if (repositorySettings == default)
233  throw new JobException("Missing repository settings!");
234 
235  if (repositorySettings.AccessToken != null)
236  {
237  //potential for commenting on a test merge change
238  var outgoingCompileJob = LatestCompileJob();
239 
240  if(outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha != compileJob.RevisionInformation.CommitSha)
241  {
242  var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken);
243 
244  async Task CommentOnPR(int prNumber, string comment)
245  {
246  try
247  {
248  await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
249  }
250  catch (ApiException e)
251  {
252  logger.LogWarning("Error posting GitHub comment! Exception: {0}", e);
253  }
254  }
255 
256  var tasks = new List<Task>();
257 
258  string FormatTestMerge(TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}",
259  Environment.NewLine,
260  repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty,
261  testMerge.PullRequestRevision,
262  testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty,
263  updated ? "Updated" : "Deployed",
264  metadata.Name,
266  compileJob.RevisionInformation.CommitSha
267  );
268 
269  //added prs
270  foreach (var I in compileJob
272  .ActiveTestMerges
273  .Select(x => x.TestMerge)
274  .Where(x => !outgoingCompileJob
276  .ActiveTestMerges
277  .Any(y => y.TestMerge.Number == x.Number)))
278  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, false)));
279 
280  //removed prs
281  foreach (var I in outgoingCompileJob
283  .ActiveTestMerges
284  .Select(x => x.TestMerge)
285  .Where(x => !compileJob
288  .Any(y => y.TestMerge.Number == x.Number)))
289  tasks.Add(CommentOnPR(I.Number.Value, "#### Test Merge Removed"));
290 
291  //updated prs
292  foreach(var I in compileJob
294  .ActiveTestMerges
295  .Select(x => x.TestMerge)
296  .Where(x => outgoingCompileJob
297  .RevisionInformation
298  .ActiveTestMerges
299  .Any(y => y.TestMerge.Number == x.Number)))
300  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, true)));
301 
302  if (tasks.Any())
303  await Task.WhenAll(tasks).ConfigureAwait(false);
304  }
305  }
306  }
307  }
308 
315  async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
316  {
317  while (true)
318  try
319  {
320  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
321  logger.LogDebug("Beginning auto update...");
322  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
323  try
324  {
325  Models.User user = null;
326  await databaseContextFactory.UseContext(async (db) => user = await db.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
327  var repositoryUpdateJob = new Job
328  {
329  Instance = new Models.Instance
330  {
331  Id = metadata.Id
332  },
333  Description = "Scheduled repository update",
334  CancelRightsType = RightsType.Repository,
335  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
336  StartedBy = user
337  };
338 
339  string deploySha = null;
340  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) =>
341  {
342  var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
343 
344  //assume 5 steps with synchronize
345  const int ProgressSections = 7;
346  const int ProgressStep = 100 / ProgressSections;
347 
348 
349  const int NumSteps = 3;
350  var doneSteps = 0;
351 
352  Action<int> NextProgressReporter()
353  {
354  var tmpDoneSteps = doneSteps;
355  ++doneSteps;
356  return progress => progressReporter((progress + 100 * tmpDoneSteps) / NumSteps);
357  };
358 
359  using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false))
360  {
361  if (repo == null)
362  {
363  logger.LogTrace("Aborting repo update, no repository!");
364  return;
365  }
366 
367  var startSha = repo.Head;
368  if (!repo.Tracking)
369  {
370  logger.LogTrace("Aborting repo update, not tracking origin!");
371  deploySha = startSha;
372  return;
373  }
374 
375  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
376 
377  //the main point of auto update is to pull the remote
378  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
379 
380  RevisionInformation currentRevInfo = null;
381  bool hasDbChanges = false;
382 
383  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
384  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
385  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
386  .FirstOrDefaultAsync(cancellationToken);
387 
388  async Task UpdateRevInfo(string currentHead, bool onOrigin)
389  {
390  if(currentRevInfo == null)
391  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
392 
393  if (currentRevInfo == default)
394  {
395  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
396  onOrigin = true;
397  }
398 
399  var attachedInstance = new Models.Instance
400  {
401  Id = metadata.Id
402  };
403  var oldRevInfo = currentRevInfo;
404  currentRevInfo = new RevisionInformation
405  {
406  CommitSha = currentHead,
407  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
408  Instance = attachedInstance
409  };
410  if (!onOrigin)
411  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
412 
413  databaseContext.Instances.Attach(attachedInstance);
414  databaseContext.RevisionInformations.Add(currentRevInfo);
415  hasDbChanges = true;
416  }
417 
418  //take appropriate auto update actions
419  bool shouldSyncTracked;
420  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
421  {
422  logger.LogTrace("Preserving test merges...");
423 
424  var currentRevInfoTask = LoadRevInfo();
425 
426  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
427 
428  if (!result.HasValue)
429  throw new JobException("Merge conflict while preserving test merges!");
430 
431  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
432 
433  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
434  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
435 
436  var currentHead = repo.Head;
437  if (currentHead != startSha)
438  {
439  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
440  shouldSyncTracked = stillOnOrigin;
441  }
442  else
443  shouldSyncTracked = false;
444  }
445  else
446  {
447  logger.LogTrace("Not preserving test merges...");
448  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
449 
450  var currentHead = repo.Head;
451 
452  currentRevInfo = await databaseContext.RevisionInformations
453  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
454  .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
455 
456  if (currentHead != startSha && currentRevInfo != default)
457  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
458 
459  shouldSyncTracked = true;
460  }
461 
462  //synch if necessary
463  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
464  {
465  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
466  var currentHead = repo.Head;
467  if (currentHead != currentRevInfo.CommitSha)
468  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
469  }
470 
471  if(hasDbChanges)
472  try
473  {
474  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
475  }
476  catch
477  {
478  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
479  throw;
480  }
481 
482  progressReporter(5 * ProgressStep);
483  deploySha = repo.Head;
484  }
485  }, cancellationToken).ConfigureAwait(false);
486 
487  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
488 
489  if (deploySha == null)
490  {
491  logger.LogTrace("Aborting auto update, repository error!");
492  continue;
493  }
494 
495  if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
496  {
497  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
498  continue;
499  }
500 
501  //finally set up the job
502  var compileProcessJob = new Job
503  {
504  StartedBy = user,
505  Instance = repositoryUpdateJob.Instance,
506  Description = "Scheduled code deployment",
507  CancelRightsType = RightsType.DreamMaker,
508  CancelRight = (ulong)DreamMakerRights.CancelCompile
509  };
510 
511  await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false);
512 
513  await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
514  }
515  catch (OperationCanceledException)
516  {
517  logger.LogDebug("Cancelled auto update job!");
518  throw;
519  }
520  catch (Exception e)
521  {
522  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
523  continue;
524  }
525  }
526  catch (OperationCanceledException)
527  {
528  break;
529  }
530  logger.LogTrace("Leaving auto update loop...");
531  }
532 
534  public void Rename(string newName)
535  {
536  if (String.IsNullOrWhiteSpace(newName))
537  throw new ArgumentNullException(nameof(newName));
538  metadata.Name = newName;
539  }
540 
542  public async Task StartAsync(CancellationToken cancellationToken)
543  {
544  await Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value), Configuration.StartAsync(cancellationToken), ByondManager.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), CompileJobConsumer.StartAsync(cancellationToken)).ConfigureAwait(false);
545 
546  //dependent on so many things, its just safer this way
547  await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
548 
549  CompileJob latestCompileJob = null;
550  await databaseContextFactory.UseContext(async db =>
551  {
552  latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
553  }).ConfigureAwait(false);
554  await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false);
555  }
556 
558  public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(0), Configuration.StopAsync(cancellationToken), ByondManager.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), CompileJobConsumer.StopAsync(cancellationToken));
559 
561  public async Task SetAutoUpdateInterval(uint newInterval)
562  {
563  Task toWait;
564  lock (this)
565  {
566  if (timerTask != null)
567  {
568  timerCts.Cancel();
569  toWait = timerTask;
570  }
571  else
572  toWait = Task.CompletedTask;
573  }
574  await toWait.ConfigureAwait(false);
575  if (newInterval == 0)
576  return;
577  lock (this)
578  {
579  //race condition, just quit
580  if (timerTask != null)
581  return;
582  timerCts?.Dispose();
583  timerCts = new CancellationTokenSource();
584  timerTask = TimerLoop(newInterval, timerCts.Token);
585  }
586  }
587 
590  {
591  if (!dmbFactory.DmbAvailable)
592  return null;
593  return dmbFactory.LockNextDmb(0)?.CompileJob;
594  }
595  }
596 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Octokit;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
18 
19 namespace Tgstation.Server.Host.Components
20 {
22  #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 IChat Chat { get; }
36 
38  public StaticFiles.IConfiguration Configuration { get; }
39 
44 
49 
54 
59 
64 
69 
74 
78  readonly ILogger<Instance> logger;
79 
83  readonly Api.Models.Instance metadata;
84 
88  Task timerTask;
89 
93  CancellationTokenSource timerCts;
94 
112  public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IGitHubClientFactory gitHubClientFactory, ILogger<Instance> logger)
113  {
114  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
115  RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
116  ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
117  this.dreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
118  Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
119  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
120  Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
121  this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
122  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
123  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
124  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
125  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
126  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
127  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
128  }
129 
131  public void Dispose()
132  {
133  timerCts?.Dispose();
134  compileJobConsumer.Dispose();
135  Configuration.Dispose();
136  Chat.Dispose();
137  Watchdog.Dispose();
139  }
140 
142  public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action<int> progressReporter, CancellationToken cancellationToken)
143  {
144 #pragma warning disable IDE0016 // Use 'throw' expression
145  if (job == null)
146  throw new ArgumentNullException(nameof(job));
147 #pragma warning restore IDE0016 // Use 'throw' expression
148  if (databaseContext == null)
149  throw new ArgumentNullException(nameof(databaseContext));
150  if (progressReporter == null)
151  throw new ArgumentNullException(nameof(progressReporter));
152 
153  var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings
154  {
155  StartupTimeout = x.StartupTimeout,
156  }).FirstOrDefaultAsync(cancellationToken);
157 
158  var compileJobsTask = databaseContext.CompileJobs
159  .Where(x => x.Job.Instance.Id == metadata.Id)
160  .OrderByDescending(x => x.Job.StoppedAt)
161  .Select(x => new Job
162  {
163  StoppedAt = x.Job.StoppedAt,
164  StartedAt = x.Job.StartedAt
165  })
166  .Take(10)
167  .ToListAsync(cancellationToken);
168 
169  var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
170  if (dreamMakerSettings == default)
171  throw new JobException("Missing DreamMakerSettings in DB!");
172  var ddSettings = await ddSettingsTask.ConfigureAwait(false);
173  if (ddSettings == default)
174  throw new JobException("Missing DreamDaemonSettings in DB!");
175 
176  Task<RepositorySettings> repositorySettingsTask = null;
177  string repoOwner = null;
178  string repoName = null;
179  CompileJob compileJob;
180  RevisionInformation revInfo;
181  using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
182  {
183  if (repo == null)
184  throw new JobException("Missing Repository!");
185 
186  if (repo.IsGitHubRepository)
187  {
188  repoOwner = repo.GitHubOwner;
189  repoName = repo.GitHubRepoName;
190  repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).Select(x => new RepositorySettings
191  {
192  AccessToken = x.AccessToken,
193  ShowTestMergeCommitters = x.ShowTestMergeCommitters
194  }).FirstOrDefaultAsync(cancellationToken);
195  }
196 
197  var repoSha = repo.Head;
198  revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy).FirstOrDefaultAsync().ConfigureAwait(false);
199 
200  if (revInfo == default)
201  {
202  revInfo = new RevisionInformation
203  {
204  CommitSha = repoSha,
205  OriginCommitSha = repoSha,
206  Instance = new Models.Instance
207  {
208  Id = metadata.Id
209  }
210  };
211  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
212  databaseContext.Instances.Attach(revInfo.Instance);
213  }
214 
215  TimeSpan? averageSpan = null;
216  var previousCompileJobs = await compileJobsTask.ConfigureAwait(false);
217  if(previousCompileJobs.Count != 0)
218  {
219  var totalSpan = TimeSpan.Zero;
220  foreach (var I in previousCompileJobs)
221  totalSpan += I.StoppedAt.Value - I.StartedAt.Value;
222  averageSpan = totalSpan / previousCompileJobs.Count;
223  }
224 
225  compileJob = await dreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, progressReporter, averageSpan, cancellationToken).ConfigureAwait(false);
226  }
227 
228  compileJob.Job = job;
229 
230  databaseContext.CompileJobs.Add(compileJob); // will be saved by job context
231 
232  job.PostComplete = ct => compileJobConsumer.LoadCompileJob(compileJob, ct);
233 
234  if (repositorySettingsTask != null)
235  {
236  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
237  if (repositorySettings == default)
238  throw new JobException("Missing repository settings!");
239 
240  if (repositorySettings.AccessToken != null)
241  {
242  // potential for commenting on a test merge change
243  var outgoingCompileJob = LatestCompileJob();
244 
245  if(outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha != compileJob.RevisionInformation.CommitSha)
246  {
247  var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken);
248 
249  async Task CommentOnPR(int prNumber, string comment)
250  {
251  try
252  {
253  await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
254  }
255  catch (ApiException e)
256  {
257  logger.LogWarning("Error posting GitHub comment! Exception: {0}", e);
258  }
259  }
260 
261  var tasks = new List<Task>();
262 
263  string FormatTestMerge(TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}",
264  Environment.NewLine,
265  repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty,
266  testMerge.PullRequestRevision,
267  testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty,
268  updated ? "Updated" : "Deployed",
269  metadata.Name,
271  compileJob.RevisionInformation.CommitSha);
272 
273  // added prs
274  foreach (var I in compileJob
276  .ActiveTestMerges
277  .Select(x => x.TestMerge)
278  .Where(x => !outgoingCompileJob
280  .ActiveTestMerges
281  .Any(y => y.TestMerge.Number == x.Number)))
282  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, false)));
283 
284  // removed prs
285  foreach (var I in outgoingCompileJob
287  .ActiveTestMerges
288  .Select(x => x.TestMerge)
289  .Where(x => !compileJob
292  .Any(y => y.TestMerge.Number == x.Number)))
293  tasks.Add(CommentOnPR(I.Number.Value, "#### Test Merge Removed"));
294 
295  // updated prs
296  foreach(var I in compileJob
298  .ActiveTestMerges
299  .Select(x => x.TestMerge)
300  .Where(x => outgoingCompileJob
301  .RevisionInformation
302  .ActiveTestMerges
303  .Any(y => y.TestMerge.Number == x.Number)))
304  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, true)));
305 
306  if (tasks.Any())
307  await Task.WhenAll(tasks).ConfigureAwait(false);
308  }
309  }
310  }
311  }
312 
319  #pragma warning disable CA1502 // TODO: Decomplexify
320  async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
321  {
322  while (true)
323  try
324  {
325  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
326  logger.LogDebug("Beginning auto update...");
327  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
328  try
329  {
330  Models.User user = null;
331  await databaseContextFactory.UseContext(async (db) => user = await db.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
332  var repositoryUpdateJob = new Job
333  {
334  Instance = new Models.Instance
335  {
336  Id = metadata.Id
337  },
338  Description = "Scheduled repository update",
339  CancelRightsType = RightsType.Repository,
340  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
341  StartedBy = user
342  };
343 
344  string deploySha = null;
345  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) =>
346  {
347  var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
348 
349  // assume 5 steps with synchronize
350  const int ProgressSections = 7;
351  const int ProgressStep = 100 / ProgressSections;
352 
353  const int NumSteps = 3;
354  var doneSteps = 0;
355 
356  Action<int> NextProgressReporter()
357  {
358  var tmpDoneSteps = doneSteps;
359  ++doneSteps;
360  return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps);
361  }
362 
363  using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false))
364  {
365  if (repo == null)
366  {
367  logger.LogTrace("Aborting repo update, no repository!");
368  return;
369  }
370 
371  var startSha = repo.Head;
372  if (!repo.Tracking)
373  {
374  logger.LogTrace("Aborting repo update, not tracking origin!");
375  deploySha = startSha;
376  return;
377  }
378 
379  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
380 
381  // the main point of auto update is to pull the remote
382  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
383 
384  RevisionInformation currentRevInfo = null;
385  bool hasDbChanges = false;
386 
387  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
388  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
389  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
390  .FirstOrDefaultAsync(cancellationToken);
391 
392  async Task UpdateRevInfo(string currentHead, bool onOrigin)
393  {
394  if(currentRevInfo == null)
395  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
396 
397  if (currentRevInfo == default)
398  {
399  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
400  onOrigin = true;
401  }
402 
403  var attachedInstance = new Models.Instance
404  {
405  Id = metadata.Id
406  };
407  var oldRevInfo = currentRevInfo;
408  currentRevInfo = new RevisionInformation
409  {
410  CommitSha = currentHead,
411  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
412  Instance = attachedInstance
413  };
414  if (!onOrigin)
415  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
416 
417  databaseContext.Instances.Attach(attachedInstance);
418  databaseContext.RevisionInformations.Add(currentRevInfo);
419  hasDbChanges = true;
420  }
421 
422  // take appropriate auto update actions
423  bool shouldSyncTracked;
424  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
425  {
426  logger.LogTrace("Preserving test merges...");
427 
428  var currentRevInfoTask = LoadRevInfo();
429 
430  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
431 
432  if (!result.HasValue)
433  throw new JobException("Merge conflict while preserving test merges!");
434 
435  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
436 
437  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
438  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
439 
440  var currentHead = repo.Head;
441  if (currentHead != startSha)
442  {
443  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
444  shouldSyncTracked = stillOnOrigin;
445  }
446  else
447  shouldSyncTracked = false;
448  }
449  else
450  {
451  logger.LogTrace("Not preserving test merges...");
452  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
453 
454  var currentHead = repo.Head;
455 
456  currentRevInfo = await databaseContext.RevisionInformations
457  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
458  .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
459 
460  if (currentHead != startSha && currentRevInfo != default)
461  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
462 
463  shouldSyncTracked = true;
464  }
465 
466  // synch if necessary
467  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
468  {
469  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
470  var currentHead = repo.Head;
471  if (currentHead != currentRevInfo.CommitSha)
472  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
473  }
474 
475  if(hasDbChanges)
476  try
477  {
478  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
479  }
480  catch
481  {
482  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
483  throw;
484  }
485 
486  progressReporter(5 * ProgressStep);
487  deploySha = repo.Head;
488  }
489  }, cancellationToken).ConfigureAwait(false);
490 
491  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
492 
493  if (deploySha == null)
494  {
495  logger.LogTrace("Aborting auto update, repository error!");
496  continue;
497  }
498 
499  if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
500  {
501  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
502  continue;
503  }
504 
505  // finally set up the job
506  var compileProcessJob = new Job
507  {
508  StartedBy = user,
509  Instance = repositoryUpdateJob.Instance,
510  Description = "Scheduled code deployment",
511  CancelRightsType = RightsType.DreamMaker,
512  CancelRight = (ulong)DreamMakerRights.CancelCompile
513  };
514 
515  await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false);
516 
517  await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
518  }
519  catch (OperationCanceledException)
520  {
521  logger.LogDebug("Cancelled auto update job!");
522  throw;
523  }
524  catch (Exception e)
525  {
526  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
527  continue;
528  }
529  }
530  catch (OperationCanceledException)
531  {
532  break;
533  }
534 
535  logger.LogTrace("Leaving auto update loop...");
536  }
537  #pragma warning restore CA1502
538 
540  public void Rename(string newName)
541  {
542  if (String.IsNullOrWhiteSpace(newName))
543  throw new ArgumentNullException(nameof(newName));
544  metadata.Name = newName;
545  }
546 
548  public async Task StartAsync(CancellationToken cancellationToken)
549  {
550  await Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value), Configuration.StartAsync(cancellationToken), ByondManager.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), compileJobConsumer.StartAsync(cancellationToken)).ConfigureAwait(false);
551 
552  // dependent on so many things, its just safer this way
553  await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
554 
555  CompileJob latestCompileJob = null;
556  await databaseContextFactory.UseContext(async db =>
557  {
558  latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
559  }).ConfigureAwait(false);
560  await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false);
561  }
562 
564  public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(0), Configuration.StopAsync(cancellationToken), ByondManager.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), compileJobConsumer.StopAsync(cancellationToken));
565 
567  public async Task SetAutoUpdateInterval(uint newInterval)
568  {
569  Task toWait;
570  lock (this)
571  {
572  if (timerTask != null)
573  {
574  timerCts.Cancel();
575  toWait = timerTask;
576  }
577  else
578  toWait = Task.CompletedTask;
579  }
580 
581  await toWait.ConfigureAwait(false);
582  if (newInterval == 0)
583  return;
584  lock (this)
585  {
586  // race condition, just quit
587  if (timerTask != null)
588  return;
589  timerCts?.Dispose();
590  timerCts = new CancellationTokenSource();
591  timerTask = TimerLoop(newInterval, timerCts.Token);
592  }
593  }
594 
597  {
598  if (!dmbFactory.DmbAvailable)
599  return null;
600  return dmbFactory.LockNextDmb(0)?.CompileJob;
601  }
602  }
603 }
string Name
The name of the User
Definition: User.cs:38
Manages the runtime of Jobs
Definition: IJobManager.cs:12
DbSet< Instance > Instances
The Instances in the IDatabaseContext
-
async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action< int > progressReporter, CancellationToken cancellationToken)
Run the compile job and insert it into the database. Meant to be called by a Core.IJobManager
Definition: Instance.cs:136
+
async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action< int > progressReporter, CancellationToken cancellationToken)
Run the compile job and insert it into the database. Meant to be called by a Core.IJobManager
Definition: Instance.cs:142
- -
async Task SetAutoUpdateInterval(uint newInterval)
Change the Api.Models.Instance.AutoUpdateInterval for the IInstance
Definition: Instance.cs:561
+ +
async Task SetAutoUpdateInterval(uint newInterval)
Change the Api.Models.Instance.AutoUpdateInterval for the IInstance
Definition: Instance.cs:567
- -
CompileJob LatestCompileJob()
The latest staged CompileJob
Definition: Instance.cs:589
+ +
CompileJob LatestCompileJob()
The latest staged CompileJob
Definition: Instance.cs:596
@@ -116,45 +116,45 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
- -
void Rename(string newName)
Rename the IInstance
Definition: Instance.cs:534
- + +
void Rename(string newName)
Rename the IInstance
Definition: Instance.cs:540
DateTimeOffset StoppedAt
When the Job stopped
Definition: Job.cs:37
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
-
async Task StartAsync(CancellationToken cancellationToken)
-
async Task< Models.CompileJob > Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action< int > progressReporter, TimeSpan?estimatedDuration, CancellationToken cancellationToken)
Definition: DreamMaker.cs:270
+
async Task StartAsync(CancellationToken cancellationToken)
+
readonly ICompileJobConsumer compileJobConsumer
The ICompileJobConsumer for the Instance
Definition: Instance.cs:48
For managing connected chat services
Definition: IChat.cs:13
-
Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger< Repository > logger, Action onDispose)
Construct a Repository
Definition: Repository.cs:118
+
Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger< Repository > logger, Action onDispose)
Construct a Repository
Definition: Repository.cs:103
Job Job
See Api.Models.CompileJob.Job
Definition: CompileJob.cs:13
-
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for the Instance
Definition: Instance.cs:68
-
readonly IEventConsumer eventConsumer
The IEventConsumer for the Instance
Definition: Instance.cs:63
-
Task timerTask
The auto update Task
Definition: Instance.cs:83
+
readonly IGitHubClientFactory gitHubClientFactory
The IGitHubClientFactory for the Instance
Definition: Instance.cs:73
+
readonly IEventConsumer eventConsumer
The IEventConsumer for the Instance
Definition: Instance.cs:68
+
Task timerTask
The auto update Task
Definition: Instance.cs:88
-
readonly Api.Models.Instance metadata
The Api.Models.Instance for the Instance
Definition: Instance.cs:78
-
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IGitHubClientFactory gitHubClientFactory, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:106
+
readonly Api.Models.Instance metadata
The Api.Models.Instance for the Instance
Definition: Instance.cs:83
+
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IGitHubClientFactory gitHubClientFactory, ILogger< Instance > logger)
Construct an Instance
Definition: Instance.cs:112
Func< CancellationToken, Task > PostComplete
A Task to run after the job completes. This will not affect the Api.Models.Internal.Job.StoppedAt time, unless it is cancelled or errors
Definition: Job.cs:34
-
CancellationTokenSource timerCts
CancellationTokenSource for timerTask
Definition: Instance.cs:87
- -
async Task StopAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:983
-
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Instance
Definition: Instance.cs:48
+
CancellationTokenSource timerCts
CancellationTokenSource for timerTask
Definition: Instance.cs:93
+ +
async Task StopAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:997
+
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Instance
Definition: Instance.cs:53
DbSet< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the IDatabaseContext
For interacting with the instance services
Definition: IInstance.cs:17
DreamMakerRights
Rights for Models.DreamMaker
-
readonly ILogger< Instance > logger
The ILogger for the Instance
Definition: Instance.cs:73
+
readonly ILogger< Instance > logger
The ILogger for the Instance
Definition: Instance.cs:78
For managing the BYOND installation
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: Chat.cs:537
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: Chat.cs:543
RepositoryRights
Rights for a Models.Repository
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:315
+
readonly IDreamMaker dreamMaker
The IDreamMaker for the Instance
Definition: Instance.cs:43
+
async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
Pull the repository and compile for every set of given minutes
Definition: Instance.cs:320
DbSet< CompileJob > CompileJobs
The CompileJobs in the IDatabaseContext
- +
DbSet< DreamMakerSettings > DreamMakerSettings
The Models.DreamMakerSettings in the IDatabaseContext
Instance Instance
The Models.Instance the RevisionInformation belongs to
@@ -163,26 +163,26 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for creating and loading IRepositorys
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:949
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Chat.cs:525
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:963
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Chat.cs:531
-
readonly IDmbFactory dmbFactory
The IDmbFactory for the Instance
Definition: Instance.cs:53
- - -
readonly IJobManager jobManager
The IJobManager for the Instance
Definition: Instance.cs:58
+
readonly IDmbFactory dmbFactory
The IDmbFactory for the Instance
Definition: Instance.cs:58
+ + +
readonly IJobManager jobManager
The IJobManager for the Instance
Definition: Instance.cs:63
RevisionInformation RevisionInformation
See Api.Models.CompileJob.RevisionInformation
Definition: CompileJob.cs:24
DbSet< DreamDaemonSettings > DreamDaemonSettings
The Models.DreamDaemonSettings in the IDatabaseContext
-
async Task< IRepository > LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Instance.cs:542
+
async Task< IRepository > LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Instance.cs:548
Task StopAsync(CancellationToken cancellationToken)
string AccessToken
The token/password to access the git repository with
- +
string PullRequestRevision
The sha of the pull request revision to merge. If not specified, the latest commit shall be used (sem...
List< RevInfoTestMerge > ActiveTestMerges
See Api.Models.RevisionInformation.ActiveTestMerges
string Comment
Optional comment about the test
-
string OriginCommitSha
The sha of the most recent remote commit
+
string OriginCommitSha
The sha of the most recent remote commit
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:8
diff --git a/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs_source.html b/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs_source.html index 67de21c1ae..c51af068bb 100644 --- a/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_repository_2_repository_8cs_source.html @@ -92,41 +92,41 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Repository.cs
-Go to the documentation of this file.
1 using LibGit2Sharp;
2 using LibGit2Sharp.Handlers;
3 using Microsoft.Extensions.Logging;
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;
12 using Tgstation.Server.Host.IO;
13 
14 namespace Tgstation.Server.Host.Components.Repository
15 {
17  sealed class Repository : IRepository
18  {
22  public const string GitHubUrl = "://github.com/";
23 
27  public const string OriginTrackingErrorTemplate = "Unable to determine most recent origin commit of {0}. Marking it as an origin commit. This may result in invalid git metadata until the next hard reset to an origin reference.";
28 
32  public const string RemoteTemporaryBranchName = "___TGSTempBranch";
33 
34  const string UnknownReference = "<UNKNOWN>";
35 
37  public bool IsGitHubRepository { get; }
38 
40  public string GitHubOwner { get; }
41 
43  public string GitHubRepoName { get; }
44 
46  public bool Tracking => Reference != null && repository.Head.IsTracking;
47 
49  public string Head => repository.Head.Tip.Sha;
50 
52  public string Reference => repository.Head.FriendlyName;
53 
55  public string Origin => repository.Network.Remotes.First().Url;
56 
60  readonly LibGit2Sharp.IRepository repository;
61 
66 
71 
76 
80  readonly ILogger<Repository> logger;
81 
85  readonly Action onDispose;
86 
87  void GetRepositoryOwnerName(string remote, out string owner, out string name)
88  {
89  //Assume standard gh format: [(git)|(https)]://github.com/owner/repo(.git)[0-1]
90  //Yes use .git twice in case it was weird
91  var toRemove = new string[] { ".git", "/", ".git" };
92  foreach (string item in toRemove)
93  if (remote.EndsWith(item, StringComparison.OrdinalIgnoreCase))
94  remote = remote.Substring(0, remote.LastIndexOf(item, StringComparison.OrdinalIgnoreCase));
95  var splits = remote.Split('/');
96  name = splits[splits.Length - 1];
97  owner = splits[splits.Length - 2].Split('.')[0];
98 
99  logger.LogTrace("GetRepositoryOwnerName({0}) => {1} / {2}", remote, owner, name);
100  }
101 
107  static CheckoutProgressHandler CheckoutProgressHandler(Action<int> progressReporter) => (a, completedSteps, totalSteps) => progressReporter((int)((((float)completedSteps) / totalSteps) * 100));
108 
118  public Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger<Repository> logger, Action onDispose)
119  {
120  this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
121  this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger));
122  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
123  this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
124  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
125  this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
126  IsGitHubRepository = Origin.Contains(GitHubUrl, StringComparison.InvariantCultureIgnoreCase);
127  if (IsGitHubRepository)
128  {
129  GetRepositoryOwnerName(Origin, out var owner, out var name);
130  GitHubOwner = owner;
131  GitHubRepoName = name;
132  }
133  }
134 
136  public void Dispose()
137  {
138  logger.LogTrace("Disposing...");
139  repository.Dispose();
140  onDispose.Invoke();
141  }
142 
151  PushOptions GeneratePushOptions(Action<int> progressReporter, string username, string password, CancellationToken cancellationToken) => new PushOptions
152  {
153  OnPackBuilderProgress = (stage, current, total) =>
154  {
155  var baseProgress = stage == PackBuilderStage.Counting ? 0 : 25;
156  progressReporter(baseProgress + ((int)(25 * ((float)current) / total)));
157  return !cancellationToken.IsCancellationRequested;
158  },
159  OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested,
160  OnPushTransferProgress = (a, sentBytes, totalBytes) =>
161  {
162  progressReporter(50 + ((int)(50 * ((float)sentBytes) / totalBytes)));
163  return !cancellationToken.IsCancellationRequested;
164  },
165  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
166  };
167 
174  void RawCheckout(string committish, Action<int> progressReporter, CancellationToken cancellationToken)
175  {
176  logger.LogTrace("Checkout: {0}", committish);
177 
178  progressReporter(0);
179  cancellationToken.ThrowIfCancellationRequested();
180 
181  Commands.Checkout(repository, committish, new CheckoutOptions
182  {
183  CheckoutModifiers = CheckoutModifiers.Force,
184  OnCheckoutProgress = CheckoutProgressHandler(progressReporter)
185  });
186 
187  cancellationToken.ThrowIfCancellationRequested();
188 
189  repository.RemoveUntrackedFiles();
190  }
191 
193  public async Task<bool?> AddTestMerge(TestMergeParameters testMergeParameters, string committerName, string committerEmail, string username, string password, Action<int> progressReporter, CancellationToken cancellationToken)
194  {
195  if (testMergeParameters == null)
196  throw new ArgumentNullException(nameof(testMergeParameters));
197  if (committerName == null)
198  throw new ArgumentNullException(nameof(committerName));
199  if (committerEmail == null)
200  throw new ArgumentNullException(nameof(committerEmail));
201  if (progressReporter == null)
202  throw new ArgumentNullException(nameof(progressReporter));
203 
204  logger.LogDebug("Begin AddTestMerge: #{0} at {1} ({4}) by <{2} ({3})>", testMergeParameters.Number, testMergeParameters.PullRequestRevision?.Substring(0, 7), committerName, committerEmail, testMergeParameters.Comment);
205 
206  if (!IsGitHubRepository)
207  throw new InvalidOperationException("Test merging is only available on GitHub hosted origin repositories!");
208 
209  var commitMessage = String.Format(CultureInfo.InvariantCulture, "Test merge of pull request #{0}{1}{2}", testMergeParameters.Number.Value, testMergeParameters.Comment != null ? Environment.NewLine : String.Empty, testMergeParameters.Comment ?? String.Empty);
210 
211  var prBranchName = String.Format(CultureInfo.InvariantCulture, "pr-{0}", testMergeParameters.Number);
212  var localBranchName = String.Format(CultureInfo.InvariantCulture, "pull/{0}/headrefs/heads/{1}", testMergeParameters.Number, prBranchName);
213 
214  var refSpec = String.Format(CultureInfo.InvariantCulture, "pull/{0}/head:{1}", testMergeParameters.Number, prBranchName);
215  var refSpecList = new List<string> { refSpec };
216  var logMessage = String.Format(CultureInfo.InvariantCulture, "Merge remote pull request #{0}", testMergeParameters.Number);
217 
218  var originalCommit = repository.Head;
219 
220  MergeResult result = null;
221 
222  var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now);
223  await Task.Factory.StartNew(() =>
224  {
225  try
226  {
227  try
228  {
229  logger.LogTrace("Fetching refspec {0}...", refSpec);
230 
231  var remote = repository.Network.Remotes.First();
232  progressReporter(0);
233  Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, refSpecList, new FetchOptions
234  {
235  Prune = true,
236  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
237  OnTransferProgress = (a) =>
238  {
239  var percentage = 50 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
240  progressReporter((int)percentage);
241  return !cancellationToken.IsCancellationRequested;
242  },
243  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
244  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
245  }, logMessage);
246  }
247  catch (UserCancelledException) { }
248 
249  cancellationToken.ThrowIfCancellationRequested();
250 
251  repository.RemoveUntrackedFiles();
252 
253  cancellationToken.ThrowIfCancellationRequested();
254 
255  testMergeParameters.PullRequestRevision = repository.Lookup(testMergeParameters.PullRequestRevision ?? localBranchName).Sha;
256 
257  cancellationToken.ThrowIfCancellationRequested();
258 
259  logger.LogTrace("Merging {0} into {1}...", testMergeParameters.PullRequestRevision.Substring(0, 7), Reference);
260 
261  result = repository.Merge(testMergeParameters.PullRequestRevision, sig, new MergeOptions
262  {
263  CommitOnSuccess = commitMessage == null,
264  FailOnConflict = true,
265  FastForwardStrategy = FastForwardStrategy.NoFastForward,
266  SkipReuc = true,
267  OnCheckoutProgress = (a, completedSteps, totalSteps) => progressReporter(50 + ((int)((((float)completedSteps) / totalSteps) * 50)))
268  });
269  }
270  finally
271  {
272  repository.Branches.Remove(localBranchName);
273  }
274 
275  cancellationToken.ThrowIfCancellationRequested();
276 
277  if (result.Status == MergeStatus.Conflicts)
278  {
279  var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha;
280  logger.LogDebug("Merge conflict, aborting and reverting to {0}", revertTo);
281  RawCheckout(revertTo, progressReporter, cancellationToken);
282  cancellationToken.ThrowIfCancellationRequested();
283  }
284 
285  repository.RemoveUntrackedFiles();
286  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
287 
288  if (result.Status == MergeStatus.Conflicts)
289  {
290  await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List<string> { originalCommit.Tip.Sha, testMergeParameters.PullRequestRevision, originalCommit.FriendlyName ?? UnknownReference, prBranchName }, cancellationToken).ConfigureAwait(false);
291  return null;
292  }
293 
294  if (commitMessage != null && result.Status != MergeStatus.UpToDate)
295  {
296  logger.LogTrace("Committing merge: \"{0}\"...", commitMessage);
297  await Task.Factory.StartNew(() => repository.Commit(commitMessage, sig, sig, new CommitOptions
298  {
299  PrettifyMessage = true
300  }), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
301  }
302 
303  await eventConsumer.HandleEvent(EventType.RepoMergePullRequest, new List<string> { testMergeParameters.Number.ToString(), testMergeParameters.PullRequestRevision, testMergeParameters.Comment }, cancellationToken).ConfigureAwait(false);
304 
305  return result.Status != MergeStatus.NonFastForward;
306  }
307 
309  public async Task CheckoutObject(string committish, Action<int> progressReporter, CancellationToken cancellationToken)
310  {
311  if (committish == null)
312  throw new ArgumentNullException(nameof(committish));
313  if (progressReporter == null)
314  throw new ArgumentNullException(nameof(progressReporter));
315  logger.LogDebug("Checkout object: {0}...", committish);
316  await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish }, cancellationToken).ConfigureAwait(false);
317  await Task.Factory.StartNew(() =>
318  {
319  repository.RemoveUntrackedFiles();
320  RawCheckout(committish, progressReporter, cancellationToken);
321  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
322  }
323 
325  public async Task FetchOrigin(string username, string password, Action<int> progressReporter, CancellationToken cancellationToken)
326  {
327  if (progressReporter == null)
328  throw new ArgumentNullException(nameof(progressReporter));
329  logger.LogDebug("Fetch origin...");
330  await eventConsumer.HandleEvent(EventType.RepoFetch, Array.Empty<string>(), cancellationToken).ConfigureAwait(false);
331  await Task.Factory.StartNew(() =>
332  {
333  var remote = repository.Network.Remotes.First();
334  try
335  {
336  Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, remote.FetchRefSpecs.Select(x => x.Specification), new FetchOptions
337  {
338  Prune = true,
339  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
340  OnTransferProgress = (a) =>
341  {
342  var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
343  progressReporter((int)percentage);
344  return !cancellationToken.IsCancellationRequested;
345  },
346  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
347  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
348  }, "Fetch origin commits");
349  }
350  catch (UserCancelledException)
351  {
352  cancellationToken.ThrowIfCancellationRequested();
353  }
354  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
355  }
356 
365  Task PushHeadToTemporaryBranch(string username, string password, Action<int> progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
366  {
367  logger.LogInformation("Pushing changes to temporary remote branch...");
368  var branch = repository.CreateBranch(RemoteTemporaryBranchName);
369  try
370  {
371  cancellationToken.ThrowIfCancellationRequested();
372  var remote = repository.Network.Remotes.First();
373  try
374  {
375  var forcePushString = String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName);
376  repository.Network.Push(remote,forcePushString, GeneratePushOptions(progress => progressReporter((int)(0.9f * progress)), username, password, cancellationToken));
377  var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName);
378  repository.Network.Push(remote, removalString, GeneratePushOptions(progress => progressReporter(90 + (int)(0.1f * progress)), username, password, cancellationToken));
379  }
380  catch (UserCancelledException)
381  {
382  cancellationToken.ThrowIfCancellationRequested();
383  }
384  catch(LibGit2SharpException e)
385  {
386  logger.LogWarning("Unable to push to temporary branch! Exception: {0}", e);
387  }
388  }
389  finally
390  {
391  repository.Branches.Remove(branch);
392  }
393  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
394 
396  public async Task ResetToOrigin(Action<int> progressReporter, CancellationToken cancellationToken)
397  {
398  if (progressReporter == null)
399  throw new ArgumentNullException(nameof(progressReporter));
400  if (!Tracking)
401  throw new JobException("Cannot reset to origin while not on a tracked reference!");
402  logger.LogTrace("Reset to origin...");
403  var trackedBranch = repository.Head.TrackedBranch;
404  await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List<string> { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, cancellationToken).ConfigureAwait(false);
405  await ResetToSha(trackedBranch.Tip.Sha, progressReporter, cancellationToken).ConfigureAwait(false);
406  }
407 
409  public Task ResetToSha(string sha, Action<int> progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
410  {
411  if (sha == null)
412  throw new ArgumentNullException(nameof(sha));
413  if (progressReporter == null)
414  throw new ArgumentNullException(nameof(progressReporter));
415 
416  logger.LogDebug("Reset to sha: {0}", sha.Substring(0, 7));
417 
418  repository.RemoveUntrackedFiles();
419  cancellationToken.ThrowIfCancellationRequested();
420 
421  var gitObject = repository.Lookup(sha, ObjectType.Commit);
422  cancellationToken.ThrowIfCancellationRequested();
423 
424  if (gitObject == null)
425  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot reset to non-existent SHA: {0}", sha));
426 
427  repository.Reset(ResetMode.Hard, gitObject.Peel<Commit>(), new CheckoutOptions
428  {
429  OnCheckoutProgress = CheckoutProgressHandler(progressReporter)
430  });
431  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
432 
434  public async Task CopyTo(string path, CancellationToken cancellationToken)
435  {
436  if (path == null)
437  throw new ArgumentNullException(nameof(path));
438  logger.LogTrace("Copying to {0}...", path);
439  await ioMananger.CopyDirectory(".", path, new List<string> { ".git" }, cancellationToken).ConfigureAwait(false);
440  }
441 
443  public async Task<bool?> MergeOrigin(string committerName, string committerEmail, Action<int> progressReporter, CancellationToken cancellationToken)
444  {
445  if (progressReporter == null)
446  throw new ArgumentNullException(nameof(progressReporter));
447 
448  MergeResult result = null;
449  Branch trackedBranch = null;
450 
451  var oldHead = repository.Head;
452  var oldTip = oldHead.Tip;
453 
454  await Task.Factory.StartNew(() =>
455  {
456  if (!Tracking)
457  throw new JobException("Cannot reset to origin while not on a tracked reference!");
458 
459  repository.RemoveUntrackedFiles();
460 
461  cancellationToken.ThrowIfCancellationRequested();
462 
463  trackedBranch = repository.Head.TrackedBranch;
464  logger.LogDebug("Merge origin/{2}: <{0} ({1})>", committerName, committerEmail, trackedBranch.FriendlyName);
465  result = repository.Merge(trackedBranch, new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now), new MergeOptions
466  {
467  CommitOnSuccess = true,
468  FailOnConflict = true,
469  FastForwardStrategy = FastForwardStrategy.Default,
470  SkipReuc = true,
471  OnCheckoutProgress = CheckoutProgressHandler(progressReporter)
472  });
473 
474  cancellationToken.ThrowIfCancellationRequested();
475 
476  if (result.Status == MergeStatus.Conflicts)
477  {
478  logger.LogDebug("Merge conflict, aborting and reverting to {0}", oldHead.FriendlyName);
479  repository.Reset(ResetMode.Hard, oldTip, new CheckoutOptions
480  {
481  OnCheckoutProgress = CheckoutProgressHandler(progressReporter)
482  });
483  cancellationToken.ThrowIfCancellationRequested();
484  }
485 
486  repository.RemoveUntrackedFiles();
487  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
488 
489  if (result.Status == MergeStatus.Conflicts)
490  {
491  await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List<string> { oldTip.Sha, trackedBranch.Tip.Sha, oldHead.FriendlyName ?? UnknownReference, trackedBranch.FriendlyName }, cancellationToken).ConfigureAwait(false);
492  return null;
493  }
494 
495  return result.Status == MergeStatus.FastForward;
496  }
497 
499  public async Task<bool> Sychronize(string username, string password, string committerName, string committerEmail, Action<int> progressReporter, bool synchronizeTrackedBranch, CancellationToken cancellationToken)
500  {
501  if (committerName == null)
502  throw new ArgumentNullException(nameof(committerName));
503  if (committerEmail == null)
504  throw new ArgumentNullException(nameof(committerEmail));
505  if (progressReporter == null)
506  throw new ArgumentNullException(nameof(progressReporter));
507 
508  if (username == null && password == null)
509  {
510  logger.LogTrace("Not synchronizing due to lack of credentials!");
511  return false;
512  }
513  logger.LogTrace("Begin Synchronize...");
514 
515  if (username == null)
516  throw new ArgumentNullException(nameof(username));
517  if (password == null)
518  throw new ArgumentNullException(nameof(password));
519 
520  var startHead = Head;
521 
522  logger.LogTrace("Configuring <{0} ({1})> as author/committer", committerName, committerEmail);
523  await Task.Factory.StartNew(() =>
524  {
525  repository.Config.Set("user.name", committerName);
526  cancellationToken.ThrowIfCancellationRequested();
527  repository.Config.Set("user.email", committerEmail);
528  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
529 
530  cancellationToken.ThrowIfCancellationRequested();
531  try
532  {
533  if (!await eventConsumer.HandleEvent(EventType.RepoPreSynchronize, new List<string> { ioMananger.ResolvePath(".") }, cancellationToken).ConfigureAwait(false))
534  {
535  logger.LogDebug("Aborted synchronize due to event handler response!");
536  return false;
537  }
538  }
539  finally
540  {
541  logger.LogTrace("Resetting and cleaning untracked files...");
542  await Task.Factory.StartNew(() =>
543  {
544  repository.RemoveUntrackedFiles();
545  cancellationToken.ThrowIfCancellationRequested();
546  repository.Reset(ResetMode.Hard, repository.Head.Tip, new CheckoutOptions
547  {
548  OnCheckoutProgress = CheckoutProgressHandler(progress => progressReporter(progress / 10))
549  });
550  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
551  }
552 
553  void FinalReporter(int progress) => progressReporter((int)(((float)progress) / 100 * 90));
554 
555  if (!synchronizeTrackedBranch)
556  {
557  await PushHeadToTemporaryBranch(username, password, FinalReporter, cancellationToken).ConfigureAwait(false);
558  return false;
559  }
560 
561  var sameHead = Head == startHead;
562  if (sameHead || !Tracking)
563  {
564  logger.LogTrace("Aborted synchronize due to {0}!", sameHead ? "lack of changes" : "not being on tracked reference");
565  return false;
566  }
567 
568  logger.LogInformation("Synchronizing with origin...");
569 
570  return await Task.Factory.StartNew(() =>
571  {
572  var remote = repository.Network.Remotes.First();
573  try
574  {
575  repository.Network.Push(repository.Head, GeneratePushOptions(FinalReporter, username, password, cancellationToken));
576  return true;
577  }
578  catch (NonFastForwardException)
579  {
580  logger.LogInformation("Synchronize aborted, non-fast forward!");
581  return false;
582  }
583  catch (UserCancelledException e)
584  {
585  cancellationToken.ThrowIfCancellationRequested();
586  throw new InvalidOperationException("Caught UserCancelledException without cancellationToken triggering", e);
587  }
588  catch (LibGit2SharpException e)
589  {
590  logger.LogWarning("Unable to make synchronization push! Exception: {0}", e);
591  return false;
592  }
593  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
594  }
595 
597  public Task<bool> IsSha(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
598  {
599  //check if it's a tag
600  var gitObject = repository.Lookup(committish, ObjectType.Tag);
601  if (gitObject != null)
602  return false;
603  cancellationToken.ThrowIfCancellationRequested();
604  //check if it's a branch
605  if (repository.Branches[committish] != null)
606  return false;
607  cancellationToken.ThrowIfCancellationRequested();
608  //err on the side of references, if we can't look it up, assume its a reference
609  if (repository.Lookup<Commit>(committish) != null)
610  return true;
611  return false;
612  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
613  }
614 }
Consumes EventTypes and takes the appropriate actions
+Go to the documentation of this file.
1 using LibGit2Sharp;
2 using LibGit2Sharp.Handlers;
3 using Microsoft.Extensions.Logging;
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;
12 using Tgstation.Server.Host.IO;
13 
14 namespace Tgstation.Server.Host.Components.Repository
15 {
17  sealed class Repository : IRepository
18  {
22  public const string GitHubUrl = "://github.com/";
23 
27  public const string OriginTrackingErrorTemplate = "Unable to determine most recent origin commit of {0}. Marking it as an origin commit. This may result in invalid git metadata until the next hard reset to an origin reference.";
28 
32  public const string RemoteTemporaryBranchName = "___TGSTempBranch";
33 
34  const string UnknownReference = "<UNKNOWN>";
35 
37  public bool IsGitHubRepository { get; }
38 
40  public string GitHubOwner { get; }
41 
43  public string GitHubRepoName { get; }
44 
46  public bool Tracking => Reference != null && repository.Head.IsTracking;
47 
49  public string Head => repository.Head.Tip.Sha;
50 
52  public string Reference => repository.Head.FriendlyName;
53 
55  public string Origin => repository.Network.Remotes.First().Url;
56 
60  readonly LibGit2Sharp.IRepository repository;
61 
66 
71 
76 
80  readonly ILogger<Repository> logger;
81 
85  readonly Action onDispose;
86 
92  static CheckoutProgressHandler CheckoutProgressHandler(Action<int> progressReporter) => (a, completedSteps, totalSteps) => progressReporter((int)(((float)completedSteps) / totalSteps * 100));
93 
103  public Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger<Repository> logger, Action onDispose)
104  {
105  this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
106  this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger));
107  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
108  this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider));
109  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
110  this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
111  IsGitHubRepository = Origin.Contains(GitHubUrl, StringComparison.InvariantCultureIgnoreCase);
112  if (IsGitHubRepository)
113  {
114  GetRepositoryOwnerName(Origin, out var owner, out var name);
115  GitHubOwner = owner;
116  GitHubRepoName = name;
117  }
118  }
119 
121  public void Dispose()
122  {
123  logger.LogTrace("Disposing...");
124  repository.Dispose();
125  onDispose.Invoke();
126  }
127 
128  void GetRepositoryOwnerName(string remote, out string owner, out string name)
129  {
130  // Assume standard gh format: [(git)|(https)]://github.com/owner/repo(.git)[0-1]
131  // Yes use .git twice in case it was weird
132  var toRemove = new string[] { ".git", "/", ".git" };
133  foreach (string item in toRemove)
134  if (remote.EndsWith(item, StringComparison.OrdinalIgnoreCase))
135  remote = remote.Substring(0, remote.LastIndexOf(item, StringComparison.OrdinalIgnoreCase));
136  var splits = remote.Split('/');
137  name = splits[splits.Length - 1];
138  owner = splits[splits.Length - 2].Split('.')[0];
139 
140  logger.LogTrace("GetRepositoryOwnerName({0}) => {1} / {2}", remote, owner, name);
141  }
142 
151  PushOptions GeneratePushOptions(Action<int> progressReporter, string username, string password, CancellationToken cancellationToken) => new PushOptions
152  {
153  OnPackBuilderProgress = (stage, current, total) =>
154  {
155  var baseProgress = stage == PackBuilderStage.Counting ? 0 : 25;
156  progressReporter(baseProgress + ((int)(25 * ((float)current) / total)));
157  return !cancellationToken.IsCancellationRequested;
158  },
159  OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested,
160  OnPushTransferProgress = (a, sentBytes, totalBytes) =>
161  {
162  progressReporter(50 + ((int)(50 * ((float)sentBytes) / totalBytes)));
163  return !cancellationToken.IsCancellationRequested;
164  },
165  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
166  };
167 
174  void RawCheckout(string committish, Action<int> progressReporter, CancellationToken cancellationToken)
175  {
176  logger.LogTrace("Checkout: {0}", committish);
177 
178  progressReporter(0);
179  cancellationToken.ThrowIfCancellationRequested();
180 
181  Commands.Checkout(repository, committish, new CheckoutOptions
182  {
183  CheckoutModifiers = CheckoutModifiers.Force,
184  OnCheckoutProgress = CheckoutProgressHandler(progressReporter)
185  });
186 
187  cancellationToken.ThrowIfCancellationRequested();
188 
189  repository.RemoveUntrackedFiles();
190  }
191 
193  #pragma warning disable CA1506 // TODO: Decomplexify
194  public async Task<bool?> AddTestMerge(TestMergeParameters testMergeParameters, string committerName, string committerEmail, string username, string password, Action<int> progressReporter, CancellationToken cancellationToken)
195  {
196  if (testMergeParameters == null)
197  throw new ArgumentNullException(nameof(testMergeParameters));
198  if (committerName == null)
199  throw new ArgumentNullException(nameof(committerName));
200  if (committerEmail == null)
201  throw new ArgumentNullException(nameof(committerEmail));
202  if (progressReporter == null)
203  throw new ArgumentNullException(nameof(progressReporter));
204 
205  logger.LogDebug("Begin AddTestMerge: #{0} at {1} ({4}) by <{2} ({3})>", testMergeParameters.Number, testMergeParameters.PullRequestRevision?.Substring(0, 7), committerName, committerEmail, testMergeParameters.Comment);
206 
207  if (!IsGitHubRepository)
208  throw new InvalidOperationException("Test merging is only available on GitHub hosted origin repositories!");
209 
210  var commitMessage = String.Format(CultureInfo.InvariantCulture, "Test merge of pull request #{0}{1}{2}", testMergeParameters.Number.Value, testMergeParameters.Comment != null ? Environment.NewLine : String.Empty, testMergeParameters.Comment ?? String.Empty);
211 
212  var prBranchName = String.Format(CultureInfo.InvariantCulture, "pr-{0}", testMergeParameters.Number);
213  var localBranchName = String.Format(CultureInfo.InvariantCulture, "pull/{0}/headrefs/heads/{1}", testMergeParameters.Number, prBranchName);
214 
215  var refSpec = String.Format(CultureInfo.InvariantCulture, "pull/{0}/head:{1}", testMergeParameters.Number, prBranchName);
216  var refSpecList = new List<string> { refSpec };
217  var logMessage = String.Format(CultureInfo.InvariantCulture, "Merge remote pull request #{0}", testMergeParameters.Number);
218 
219  var originalCommit = repository.Head;
220 
221  MergeResult result = null;
222 
223  var sig = new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now);
224  await Task.Factory.StartNew(() =>
225  {
226  try
227  {
228  try
229  {
230  logger.LogTrace("Fetching refspec {0}...", refSpec);
231 
232  var remote = repository.Network.Remotes.First();
233  progressReporter(0);
234  Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, refSpecList, new FetchOptions
235  {
236  Prune = true,
237  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
238  OnTransferProgress = (a) =>
239  {
240  var percentage = 50 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
241  progressReporter((int)percentage);
242  return !cancellationToken.IsCancellationRequested;
243  },
244  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
245  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
246  }, logMessage);
247  }
248  catch (UserCancelledException) { }
249 
250  cancellationToken.ThrowIfCancellationRequested();
251 
252  repository.RemoveUntrackedFiles();
253 
254  cancellationToken.ThrowIfCancellationRequested();
255 
256  testMergeParameters.PullRequestRevision = repository.Lookup(testMergeParameters.PullRequestRevision ?? localBranchName).Sha;
257 
258  cancellationToken.ThrowIfCancellationRequested();
259 
260  logger.LogTrace("Merging {0} into {1}...", testMergeParameters.PullRequestRevision.Substring(0, 7), Reference);
261 
262  result = repository.Merge(testMergeParameters.PullRequestRevision, sig, new MergeOptions
263  {
264  CommitOnSuccess = commitMessage == null,
265  FailOnConflict = true,
266  FastForwardStrategy = FastForwardStrategy.NoFastForward,
267  SkipReuc = true,
268  OnCheckoutProgress = (a, completedSteps, totalSteps) => progressReporter(50 + ((int)(((float)completedSteps) / totalSteps * 50)))
269  });
270  }
271  finally
272  {
273  repository.Branches.Remove(localBranchName);
274  }
275 
276  cancellationToken.ThrowIfCancellationRequested();
277 
278  if (result.Status == MergeStatus.Conflicts)
279  {
280  var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha;
281  logger.LogDebug("Merge conflict, aborting and reverting to {0}", revertTo);
282  RawCheckout(revertTo, progressReporter, cancellationToken);
283  cancellationToken.ThrowIfCancellationRequested();
284  }
285 
286  repository.RemoveUntrackedFiles();
287  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
288 
289  if (result.Status == MergeStatus.Conflicts)
290  {
291  await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List<string> { originalCommit.Tip.Sha, testMergeParameters.PullRequestRevision, originalCommit.FriendlyName ?? UnknownReference, prBranchName }, cancellationToken).ConfigureAwait(false);
292  return null;
293  }
294 
295  if (commitMessage != null && result.Status != MergeStatus.UpToDate)
296  {
297  logger.LogTrace("Committing merge: \"{0}\"...", commitMessage);
298  await Task.Factory.StartNew(() => repository.Commit(commitMessage, sig, sig, new CommitOptions
299  {
300  PrettifyMessage = true
301  }), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
302  }
303 
304  await eventConsumer.HandleEvent(EventType.RepoMergePullRequest, new List<string> { testMergeParameters.Number.ToString(), testMergeParameters.PullRequestRevision, testMergeParameters.Comment }, cancellationToken).ConfigureAwait(false);
305 
306  return result.Status != MergeStatus.NonFastForward;
307  }
308  #pragma warning restore CA1506
309 
311  public async Task CheckoutObject(string committish, Action<int> progressReporter, CancellationToken cancellationToken)
312  {
313  if (committish == null)
314  throw new ArgumentNullException(nameof(committish));
315  if (progressReporter == null)
316  throw new ArgumentNullException(nameof(progressReporter));
317  logger.LogDebug("Checkout object: {0}...", committish);
318  await eventConsumer.HandleEvent(EventType.RepoCheckout, new List<string> { committish }, cancellationToken).ConfigureAwait(false);
319  await Task.Factory.StartNew(() =>
320  {
321  repository.RemoveUntrackedFiles();
322  RawCheckout(committish, progressReporter, cancellationToken);
323  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
324  }
325 
327  public async Task FetchOrigin(string username, string password, Action<int> progressReporter, CancellationToken cancellationToken)
328  {
329  if (progressReporter == null)
330  throw new ArgumentNullException(nameof(progressReporter));
331  logger.LogDebug("Fetch origin...");
332  await eventConsumer.HandleEvent(EventType.RepoFetch, Array.Empty<string>(), cancellationToken).ConfigureAwait(false);
333  await Task.Factory.StartNew(() =>
334  {
335  var remote = repository.Network.Remotes.First();
336  try
337  {
338  Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, remote.FetchRefSpecs.Select(x => x.Specification), new FetchOptions
339  {
340  Prune = true,
341  OnProgress = (a) => !cancellationToken.IsCancellationRequested,
342  OnTransferProgress = (a) =>
343  {
344  var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2));
345  progressReporter((int)percentage);
346  return !cancellationToken.IsCancellationRequested;
347  },
348  OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
349  CredentialsProvider = credentialsProvider.GenerateHandler(username, password)
350  }, "Fetch origin commits");
351  }
352  catch (UserCancelledException)
353  {
354  cancellationToken.ThrowIfCancellationRequested();
355  }
356  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
357  }
358 
367  Task PushHeadToTemporaryBranch(string username, string password, Action<int> progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
368  {
369  logger.LogInformation("Pushing changes to temporary remote branch...");
370  var branch = repository.CreateBranch(RemoteTemporaryBranchName);
371  try
372  {
373  cancellationToken.ThrowIfCancellationRequested();
374  var remote = repository.Network.Remotes.First();
375  try
376  {
377  var forcePushString = String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName);
378  repository.Network.Push(remote, forcePushString, GeneratePushOptions(progress => progressReporter((int)(0.9f * progress)), username, password, cancellationToken));
379  var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName);
380  repository.Network.Push(remote, removalString, GeneratePushOptions(progress => progressReporter(90 + (int)(0.1f * progress)), username, password, cancellationToken));
381  }
382  catch (UserCancelledException)
383  {
384  cancellationToken.ThrowIfCancellationRequested();
385  }
386  catch(LibGit2SharpException e)
387  {
388  logger.LogWarning("Unable to push to temporary branch! Exception: {0}", e);
389  }
390  }
391  finally
392  {
393  repository.Branches.Remove(branch);
394  }
395  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
396 
398  public async Task ResetToOrigin(Action<int> progressReporter, CancellationToken cancellationToken)
399  {
400  if (progressReporter == null)
401  throw new ArgumentNullException(nameof(progressReporter));
402  if (!Tracking)
403  throw new JobException("Cannot reset to origin while not on a tracked reference!");
404  logger.LogTrace("Reset to origin...");
405  var trackedBranch = repository.Head.TrackedBranch;
406  await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List<string> { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, cancellationToken).ConfigureAwait(false);
407  await ResetToSha(trackedBranch.Tip.Sha, progressReporter, cancellationToken).ConfigureAwait(false);
408  }
409 
411  public Task ResetToSha(string sha, Action<int> progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
412  {
413  if (sha == null)
414  throw new ArgumentNullException(nameof(sha));
415  if (progressReporter == null)
416  throw new ArgumentNullException(nameof(progressReporter));
417 
418  logger.LogDebug("Reset to sha: {0}", sha.Substring(0, 7));
419 
420  repository.RemoveUntrackedFiles();
421  cancellationToken.ThrowIfCancellationRequested();
422 
423  var gitObject = repository.Lookup(sha, ObjectType.Commit);
424  cancellationToken.ThrowIfCancellationRequested();
425 
426  if (gitObject == null)
427  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot reset to non-existent SHA: {0}", sha));
428 
429  repository.Reset(ResetMode.Hard, gitObject.Peel<Commit>(), new CheckoutOptions
430  {
431  OnCheckoutProgress = CheckoutProgressHandler(progressReporter)
432  });
433  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
434 
436  public async Task CopyTo(string path, CancellationToken cancellationToken)
437  {
438  if (path == null)
439  throw new ArgumentNullException(nameof(path));
440  logger.LogTrace("Copying to {0}...", path);
441  await ioMananger.CopyDirectory(".", path, new List<string> { ".git" }, cancellationToken).ConfigureAwait(false);
442  }
443 
445  public async Task<bool?> MergeOrigin(string committerName, string committerEmail, Action<int> progressReporter, CancellationToken cancellationToken)
446  {
447  if (progressReporter == null)
448  throw new ArgumentNullException(nameof(progressReporter));
449 
450  MergeResult result = null;
451  Branch trackedBranch = null;
452 
453  var oldHead = repository.Head;
454  var oldTip = oldHead.Tip;
455 
456  await Task.Factory.StartNew(() =>
457  {
458  if (!Tracking)
459  throw new JobException("Cannot reset to origin while not on a tracked reference!");
460 
461  repository.RemoveUntrackedFiles();
462 
463  cancellationToken.ThrowIfCancellationRequested();
464 
465  trackedBranch = repository.Head.TrackedBranch;
466  logger.LogDebug("Merge origin/{2}: <{0} ({1})>", committerName, committerEmail, trackedBranch.FriendlyName);
467  result = repository.Merge(trackedBranch, new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now), new MergeOptions
468  {
469  CommitOnSuccess = true,
470  FailOnConflict = true,
471  FastForwardStrategy = FastForwardStrategy.Default,
472  SkipReuc = true,
473  OnCheckoutProgress = CheckoutProgressHandler(progressReporter)
474  });
475 
476  cancellationToken.ThrowIfCancellationRequested();
477 
478  if (result.Status == MergeStatus.Conflicts)
479  {
480  logger.LogDebug("Merge conflict, aborting and reverting to {0}", oldHead.FriendlyName);
481  repository.Reset(ResetMode.Hard, oldTip, new CheckoutOptions
482  {
483  OnCheckoutProgress = CheckoutProgressHandler(progressReporter)
484  });
485  cancellationToken.ThrowIfCancellationRequested();
486  }
487 
488  repository.RemoveUntrackedFiles();
489  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
490 
491  if (result.Status == MergeStatus.Conflicts)
492  {
493  await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List<string> { oldTip.Sha, trackedBranch.Tip.Sha, oldHead.FriendlyName ?? UnknownReference, trackedBranch.FriendlyName }, cancellationToken).ConfigureAwait(false);
494  return null;
495  }
496 
497  return result.Status == MergeStatus.FastForward;
498  }
499 
501  public async Task<bool> Sychronize(string username, string password, string committerName, string committerEmail, Action<int> progressReporter, bool synchronizeTrackedBranch, CancellationToken cancellationToken)
502  {
503  if (committerName == null)
504  throw new ArgumentNullException(nameof(committerName));
505  if (committerEmail == null)
506  throw new ArgumentNullException(nameof(committerEmail));
507  if (progressReporter == null)
508  throw new ArgumentNullException(nameof(progressReporter));
509 
510  if (username == null && password == null)
511  {
512  logger.LogTrace("Not synchronizing due to lack of credentials!");
513  return false;
514  }
515 
516  logger.LogTrace("Begin Synchronize...");
517 
518  if (username == null)
519  throw new ArgumentNullException(nameof(username));
520  if (password == null)
521  throw new ArgumentNullException(nameof(password));
522 
523  var startHead = Head;
524 
525  logger.LogTrace("Configuring <{0} ({1})> as author/committer", committerName, committerEmail);
526  await Task.Factory.StartNew(() =>
527  {
528  repository.Config.Set("user.name", committerName);
529  cancellationToken.ThrowIfCancellationRequested();
530  repository.Config.Set("user.email", committerEmail);
531  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
532 
533  cancellationToken.ThrowIfCancellationRequested();
534  try
535  {
536  if (!await eventConsumer.HandleEvent(EventType.RepoPreSynchronize, new List<string> { ioMananger.ResolvePath(".") }, cancellationToken).ConfigureAwait(false))
537  {
538  logger.LogDebug("Aborted synchronize due to event handler response!");
539  return false;
540  }
541  }
542  finally
543  {
544  logger.LogTrace("Resetting and cleaning untracked files...");
545  await Task.Factory.StartNew(() =>
546  {
547  repository.RemoveUntrackedFiles();
548  cancellationToken.ThrowIfCancellationRequested();
549  repository.Reset(ResetMode.Hard, repository.Head.Tip, new CheckoutOptions
550  {
551  OnCheckoutProgress = CheckoutProgressHandler(progress => progressReporter(progress / 10))
552  });
553  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
554  }
555 
556  void FinalReporter(int progress) => progressReporter((int)(((float)progress) / 100 * 90));
557 
558  if (!synchronizeTrackedBranch)
559  {
560  await PushHeadToTemporaryBranch(username, password, FinalReporter, cancellationToken).ConfigureAwait(false);
561  return false;
562  }
563 
564  var sameHead = Head == startHead;
565  if (sameHead || !Tracking)
566  {
567  logger.LogTrace("Aborted synchronize due to {0}!", sameHead ? "lack of changes" : "not being on tracked reference");
568  return false;
569  }
570 
571  logger.LogInformation("Synchronizing with origin...");
572 
573  return await Task.Factory.StartNew(() =>
574  {
575  var remote = repository.Network.Remotes.First();
576  try
577  {
578  repository.Network.Push(repository.Head, GeneratePushOptions(FinalReporter, username, password, cancellationToken));
579  return true;
580  }
581  catch (NonFastForwardException)
582  {
583  logger.LogInformation("Synchronize aborted, non-fast forward!");
584  return false;
585  }
586  catch (UserCancelledException e)
587  {
588  cancellationToken.ThrowIfCancellationRequested();
589  throw new InvalidOperationException("Caught UserCancelledException without cancellationToken triggering", e);
590  }
591  catch (LibGit2SharpException e)
592  {
593  logger.LogWarning("Unable to make synchronization push! Exception: {0}", e);
594  return false;
595  }
596  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
597  }
598 
600  public Task<bool> IsSha(string committish, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
601  {
602  // check if it's a tag
603  var gitObject = repository.Lookup(committish, ObjectType.Tag);
604  if (gitObject != null)
605  return false;
606  cancellationToken.ThrowIfCancellationRequested();
607 
608  // check if it's a branch
609  if (repository.Branches[committish] != null)
610  return false;
611  cancellationToken.ThrowIfCancellationRequested();
612 
613  // err on the side of references, if we can't look it up, assume its a reference
614  if (repository.Lookup<Commit>(committish) != null)
615  return true;
616  return false;
617  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
618  }
619 }
Consumes EventTypes and takes the appropriate actions
CredentialsHandler GenerateHandler(string username, string password)
Generate a CredentialsHandler from a given username and password
-
async Task CheckoutObject(string committish, Action< int > progressReporter, CancellationToken cancellationToken)
Checks out a given committish
Definition: Repository.cs:309
+
async Task CheckoutObject(string committish, Action< int > progressReporter, CancellationToken cancellationToken)
Checks out a given committish
Definition: Repository.cs:311
readonly IIOManager ioMananger
The IIOManager for the Repository
Definition: Repository.cs:65
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
Represents an on-disk git repository
Definition: IRepository.cs:11
readonly LibGit2Sharp.IRepository repository
The LibGit2Sharp.IRepository for the Repository
Definition: Repository.cs:60
-
async Task< bool?> MergeOrigin(string committerName, string committerEmail, Action< int > progressReporter, CancellationToken cancellationToken)
Requires the current HEAD to be a tracked reference. Merges the reference to what it tracks on the or...
Definition: Repository.cs:443
+
async Task< bool?> MergeOrigin(string committerName, string committerEmail, Action< int > progressReporter, CancellationToken cancellationToken)
Requires the current HEAD to be a tracked reference. Merges the reference to what it tracks on the or...
Definition: Repository.cs:445
readonly ICredentialsProvider credentialsProvider
The ICredentialsProvider for the Repository
Definition: Repository.cs:75
-
Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger< Repository > logger, Action onDispose)
Construct a Repository
Definition: Repository.cs:118
-
async Task CopyTo(string path, CancellationToken cancellationToken)
Copies the current working directory to a given path
Definition: Repository.cs:434
+
Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger< Repository > logger, Action onDispose)
Construct a Repository
Definition: Repository.cs:103
+
async Task CopyTo(string path, CancellationToken cancellationToken)
Copies the current working directory to a given path
Definition: Repository.cs:436
int Number
The number of the pull request
readonly IEventConsumer eventConsumer
The IEventConsumer for the Repository
Definition: Repository.cs:70
readonly Action onDispose
Action to be taken when Dispose is called
Definition: Repository.cs:85
- +
Represents a git repository
Definition: Repository.cs:8
-
void GetRepositoryOwnerName(string remote, out string owner, out string name)
Definition: Repository.cs:87
+
void GetRepositoryOwnerName(string remote, out string owner, out string name)
Definition: Repository.cs:128
-
async Task< bool?> AddTestMerge(TestMergeParameters testMergeParameters, string committerName, string committerEmail, string username, string password, Action< int > progressReporter, CancellationToken cancellationToken)
Attempt to merge a GitHub pull request into HEAD
Definition: Repository.cs:193
+
async Task< bool?> AddTestMerge(TestMergeParameters testMergeParameters, string committerName, string committerEmail, string username, string password, Action< int > progressReporter, CancellationToken cancellationToken)
Attempt to merge a GitHub pull request into HEAD
Definition: Repository.cs:194
Parameters for creating a TestMerge
-
async Task ResetToOrigin(Action< int > progressReporter, CancellationToken cancellationToken)
Requires the current HEAD to be a tracked reference. Hard resets the reference to what it tracks on t...
Definition: Repository.cs:396
+
async Task ResetToOrigin(Action< int > progressReporter, CancellationToken cancellationToken)
Requires the current HEAD to be a tracked reference. Hard resets the reference to what it tracks on t...
Definition: Repository.cs:398
string Head
The SHA of the IRepository HEAD
Definition: IRepository.cs:36
void RawCheckout(string committish, Action< int > progressReporter, CancellationToken cancellationToken)
Runs a blocking force checkout to committish
Definition: Repository.cs:174
-
async Task< bool > Sychronize(string username, string password, string committerName, string committerEmail, Action< int > progressReporter, bool synchronizeTrackedBranch, CancellationToken cancellationToken)
Runs the synchronize event script and attempts to push any changes made to the IRepository if on a tr...
Definition: Repository.cs:499
+
async Task< bool > Sychronize(string username, string password, string committerName, string committerEmail, Action< int > progressReporter, bool synchronizeTrackedBranch, CancellationToken cancellationToken)
Runs the synchronize event script and attempts to push any changes made to the IRepository if on a tr...
Definition: Repository.cs:501
Interface for using filesystems
Definition: IIOManager.cs:11
-
async Task FetchOrigin(string username, string password, Action< int > progressReporter, CancellationToken cancellationToken)
Fetch commits from the origin repository
Definition: Repository.cs:325
+
async Task FetchOrigin(string username, string password, Action< int > progressReporter, CancellationToken cancellationToken)
Fetch commits from the origin repository
Definition: Repository.cs:327
string PullRequestRevision
The sha of the pull request revision to merge. If not specified, the latest commit shall be used (sem...
string Comment
Optional comment about the test
diff --git a/_tgstation_8_server_8_host_2_models_2_instance_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_instance_8cs_source.html index 6396ce3e0c..0cdd3be308 100644 --- a/_tgstation_8_server_8_host_2_models_2_instance_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_instance_8cs_source.html @@ -92,12 +92,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Instance.cs
-Go to the documentation of this file.
1 using System.Collections.Generic;
2 
3 namespace Tgstation.Server.Host.Models
4 {
8  public sealed class Instance : Api.Models.Instance
9  {
10 
15 
20 
25 
30 
34  public List<InstanceUser> InstanceUsers { get; set; }
35 
39  public List<ChatBot> ChatSettings { get; set; }
40 
44  public List<RevisionInformation> RevisionInformations { get; set; }
45 
49  public List<Job> Jobs { get; set; }
50 
55  public Api.Models.Instance ToApi() => new Api.Models.Instance
56  {
57  AutoUpdateInterval = AutoUpdateInterval,
59  Id = Id,
60  Name = Name,
61  Path = Path,
62  Online = Online
63  };
64  }
65 }
+Go to the documentation of this file.
1 using System.Collections.Generic;
2 
3 namespace Tgstation.Server.Host.Models
4 {
8  public sealed class Instance : Api.Models.Instance
9  {
14 
19 
24 
29 
33  public List<InstanceUser> InstanceUsers { get; set; }
34 
38  public List<ChatBot> ChatSettings { get; set; }
39 
43  public List<RevisionInformation> RevisionInformations { get; set; }
44 
48  public List<Job> Jobs { get; set; }
49 
54  public Api.Models.Instance ToApi() => new Api.Models.Instance
55  {
56  AutoUpdateInterval = AutoUpdateInterval,
58  Id = Id,
59  Name = Name,
60  Path = Path,
61  Online = Online
62  };
63  }
64 }
Database representation of Components.Watchdog.WatchdogReattachInformation
- +
ConfigurationType
The type of configuration allowed on an Instance
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
diff --git a/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs_source.html index a9e4a5d005..8b86a2ccbc 100644 --- a/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_repository_settings_8cs_source.html @@ -92,10 +92,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RepositorySettings.cs
-Go to the documentation of this file.
1 using System;
2 using System.ComponentModel.DataAnnotations;
4 
5 namespace Tgstation.Server.Host.Models
6 {
8  public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings
9  {
13  public long Id { get; set; }
14 
18  public long InstanceId { get; set; }
19 
23  [Required]
24  public Instance Instance { get; set; }
25 
30  public Repository ToApi() => new Repository
31  {
32  //AccessToken = AccessToken, //never show this
33  AccessUser = AccessUser,
34  AutoUpdatesKeepTestMerges = AutoUpdatesKeepTestMerges,
35  AutoUpdatesSynchronize = AutoUpdatesSynchronize,
36  CommitterEmail = CommitterEmail,
37  CommitterName = CommitterName,
38  PushTestMergeCommits = PushTestMergeCommits,
39  ShowTestMergeCommitters = ShowTestMergeCommitters
40  //revision information and the rest retrieved by controller
41  };
42  }
43 }
+Go to the documentation of this file.
1 using System.ComponentModel.DataAnnotations;
3 
4 namespace Tgstation.Server.Host.Models
5 {
7  public sealed class RepositorySettings : Api.Models.Internal.RepositorySettings
8  {
12  public long Id { get; set; }
13 
17  public long InstanceId { get; set; }
18 
22  [Required]
23  public Instance Instance { get; set; }
24 
29  public Repository ToApi() => new Repository
30  {
31  // AccessToken = AccessToken, // never show this
32  AccessUser = AccessUser,
33  AutoUpdatesKeepTestMerges = AutoUpdatesKeepTestMerges,
34  AutoUpdatesSynchronize = AutoUpdatesSynchronize,
35  CommitterEmail = CommitterEmail,
36  CommitterName = CommitterName,
37  PushTestMergeCommits = PushTestMergeCommits,
38  ShowTestMergeCommitters = ShowTestMergeCommitters
39 
40  // revision information and the rest retrieved by controller
41  };
42  }
43 }
- +
Represents a git repository
Definition: Repository.cs:8
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
diff --git a/_tgstation_8_server_8_host_2_models_2_revision_information_8cs_source.html b/_tgstation_8_server_8_host_2_models_2_revision_information_8cs_source.html index 7f69038b38..30cfe6dfff 100644 --- a/_tgstation_8_server_8_host_2_models_2_revision_information_8cs_source.html +++ b/_tgstation_8_server_8_host_2_models_2_revision_information_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
RevisionInformation.cs
-Go to the documentation of this file.
1 using System.Collections.Generic;
2 using System.ComponentModel.DataAnnotations;
3 using System.Linq;
4 
5 namespace Tgstation.Server.Host.Models
6 {
8  public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation
9  {
13  public long Id { get; set; }
14 
18  [Required]
19  public Instance Instance { get; set; }
20 
24  public TestMerge PrimaryTestMerge { get; set; }
25 
29  public List<RevInfoTestMerge> ActiveTestMerges { get; set; }
30 
34  public List<CompileJob> CompileJobs { get; set; }
35 
40  public Api.Models.RevisionInformation ToApi() => new Api.Models.RevisionInformation
41  {
42  CommitSha = CommitSha,
43  OriginCommitSha = OriginCommitSha,
44  PrimaryTestMerge = PrimaryTestMerge?.ToApi(),
45  ActiveTestMerges = ActiveTestMerges.Select(x => x.TestMerge.ToApi()).ToList(),
46  CompileJobs = CompileJobs.Select(x => new Api.Models.CompileJob
47  {
48  Id = x.Id //anti recursion measure
49  }).ToList()
50  };
51  }
52 }
+Go to the documentation of this file.
1 using System.Collections.Generic;
2 using System.ComponentModel.DataAnnotations;
3 using System.Linq;
4 
5 namespace Tgstation.Server.Host.Models
6 {
8  public sealed class RevisionInformation : Api.Models.Internal.RevisionInformation
9  {
13  public long Id { get; set; }
14 
18  [Required]
19  public Instance Instance { get; set; }
20 
24  public TestMerge PrimaryTestMerge { get; set; }
25 
29  public List<RevInfoTestMerge> ActiveTestMerges { get; set; }
30 
34  public List<CompileJob> CompileJobs { get; set; }
35 
40  public Api.Models.RevisionInformation ToApi() => new Api.Models.RevisionInformation
41  {
42  CommitSha = CommitSha,
43  OriginCommitSha = OriginCommitSha,
44  PrimaryTestMerge = PrimaryTestMerge?.ToApi(),
45  ActiveTestMerges = ActiveTestMerges.Select(x => x.TestMerge.ToApi()).ToList(),
46  CompileJobs = CompileJobs.Select(x => new Api.Models.CompileJob
47  {
48  Id = x.Id // anti recursion measure
49  }).ToList()
50  };
51  }
52 }
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
diff --git a/_tgstation_8_server_8_host_8_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_assembly_info_8cs.html index df4548a1f4..43c0f7534c 100644 --- a/_tgstation_8_server_8_host_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_assembly_info_8cs_source.html index c45aa1af40..a3aa5b9790 100644 --- a/_tgstation_8_server_8_host_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_console_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_console_8_assembly_info_8cs_source.html index 52a84c0f9a..ac265aa2be 100644 --- a/_tgstation_8_server_8_host_8_console_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_console_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Console/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Console.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Host.Console/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Console.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Console.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html index 76fc39106d..5d18b1948e 100644 --- a/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html index 37466fca50..53fc29de6a 100644 --- a/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html index 3203ae4cf7..22f2c188b8 100644 --- a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html index c53270b03d..38a995518c 100644 --- a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_tests_2_components_2_watchdog_2_test_watchdog_8cs_source.html b/_tgstation_8_server_8_host_8_tests_2_components_2_watchdog_2_test_watchdog_8cs_source.html index b8530ab7fe..c7489a8277 100644 --- a/_tgstation_8_server_8_host_8_tests_2_components_2_watchdog_2_test_watchdog_8cs_source.html +++ b/_tgstation_8_server_8_host_8_tests_2_components_2_watchdog_2_test_watchdog_8cs_source.html @@ -103,9 +103,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - + - +
Handles Commands.ICommands that map to those defined in a IJsonTrackingContext
diff --git a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html index b50f09579f..13e5231737 100644 --- a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs File Reference +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html index 4e22852711..76db250606 100644 --- a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html index bd9abbdcce..6f4b8b8668 100644 --- a/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html index 33f02c41de..1966731549 100644 --- a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Release/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Debug/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html index 11fec32519..96cc6a8bf5 100644 --- a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Release/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Debug/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Watchdog.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Watchdog")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Watchdog")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Watchdog")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Watchdog")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Watchdog")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Watchdog")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_watchdog_8_tests_8_assembly_info_8cs_source.html index 6b02753515..1bc613db7b 100644 --- a/_tgstation_8_server_8_host_8_watchdog_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_watchdog_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Watchdog.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Watchdog.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Watchdog.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Watchdog.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Watchdog.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Watchdog.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Watchdog.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Watchdog.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Watchdog.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_host_8_watchdog_8_tests_8_program_8cs_source.html index 05521465ef..30b70e2f1d 100644 --- a/_tgstation_8_server_8_host_8_watchdog_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_host_8_watchdog_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Watchdog.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Watchdog.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Watchdog.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_tests_8_assembly_info_8cs.html b/_tgstation_8_server_8_tests_8_assembly_info_8cs.html index 6ffe9f992d..26e4c9ba82 100644 --- a/_tgstation_8_server_8_tests_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_tests_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Tests.AssemblyInfo.cs File Reference +tgstation-server: tests/Tgstation.Server.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Tests.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_tests_8_assembly_info_8cs_source.html index 4ac2885489..ce315e8a0d 100644 --- a/_tgstation_8_server_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_tests_8_program_8cs_source.html index ba2161b75f..fa15cfe690 100644 --- a/_tgstation_8_server_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_token_factory_8cs_source.html b/_token_factory_8cs_source.html index 12e32a4f96..7a89204a0b 100644 --- a/_token_factory_8cs_source.html +++ b/_token_factory_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TokenFactory.cs
-Go to the documentation of this file.
1 using Microsoft.IdentityModel.Tokens;
2 using System;
3 using System.Globalization;
4 using System.IdentityModel.Tokens.Jwt;
5 using System.Reflection;
6 using System.Security.Claims;
7 using System.Threading;
8 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Security
13 {
15  sealed class TokenFactory : ITokenFactory
16  {
20  const uint TokenExpiryMinutes = 15;
21 
25  const uint TokenClockSkewMinutes = 1;
26 
30  const uint TokenSigningKeyByteAmount = 256;
31 
33  public TokenValidationParameters ValidationParameters { get; }
34 
39 
45  public TokenFactory(IAsyncDelayer asyncDelayer, ICryptographySuite cryptographySuite)
46  {
47  ValidationParameters = new TokenValidationParameters
48  {
49  ValidateIssuerSigningKey = true,
50  IssuerSigningKey = new SymmetricSecurityKey(cryptographySuite.GetSecureBytes(TokenSigningKeyByteAmount)),
51 
52  ValidateIssuer = true,
53  ValidIssuer = Assembly.GetExecutingAssembly().GetName().Name,
54 
55  ValidateLifetime = true,
56  ValidateAudience = true,
57  ValidAudience = typeof(Token).Assembly.GetName().Name,
58 
59  ClockSkew = TimeSpan.FromMinutes(TokenClockSkewMinutes),
60 
61  RequireSignedTokens = true,
62 
63  RequireExpirationTime = true
64  };
65 
66  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
67  }
68 
70  public async Task<Token> CreateToken(Models.User user, CancellationToken cancellationToken)
71  {
72  if (user == null)
73  throw new ArgumentNullException(nameof(user));
74 
75  var now = DateTimeOffset.Now;
76 
77  var nowUnix = now.ToUnixTimeSeconds();
78  //this prevents validation conflicts down the line
79  //tldr we can (theoretically) send a token the same second we receive it
80  //since unix time rounds down, it looks like it came from before the user changed their password
81  //this happens occasionally in unit tests
82  //just delay a second so we can force a round up
83  var lpuUnix = user.LastPasswordUpdate?.ToUnixTimeSeconds();
84  if (nowUnix == lpuUnix)
85  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
86 
87  var expiry = now.AddMinutes(TokenExpiryMinutes);
88  var claims = new Claim[]
89  {
90  new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString(CultureInfo.InvariantCulture)),
91  new Claim(JwtRegisteredClaimNames.Exp, expiry.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)),
92  new Claim(JwtRegisteredClaimNames.Nbf, nowUnix.ToString(CultureInfo.InvariantCulture)),
93  new Claim(JwtRegisteredClaimNames.Iss, ValidationParameters.ValidIssuer),
94  new Claim(JwtRegisteredClaimNames.Aud, ValidationParameters.ValidAudience)
95  };
96 
97  var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(ValidationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
98  return new Token { Bearer = new JwtSecurityTokenHandler().WriteToken(token), ExpiresAt = expiry };
99  }
100  }
101 }
+Go to the documentation of this file.
1 using Microsoft.IdentityModel.Tokens;
2 using System;
3 using System.Globalization;
4 using System.IdentityModel.Tokens.Jwt;
5 using System.Reflection;
6 using System.Security.Claims;
7 using System.Threading;
8 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Security
13 {
15  sealed class TokenFactory : ITokenFactory
16  {
20  const uint TokenExpiryMinutes = 15;
21 
25  const uint TokenClockSkewMinutes = 1;
26 
30  const uint TokenSigningKeyByteAmount = 256;
31 
33  public TokenValidationParameters ValidationParameters { get; }
34 
39 
45  public TokenFactory(IAsyncDelayer asyncDelayer, ICryptographySuite cryptographySuite)
46  {
47  ValidationParameters = new TokenValidationParameters
48  {
49  ValidateIssuerSigningKey = true,
50  IssuerSigningKey = new SymmetricSecurityKey(cryptographySuite.GetSecureBytes(TokenSigningKeyByteAmount)),
51 
52  ValidateIssuer = true,
53  ValidIssuer = Assembly.GetExecutingAssembly().GetName().Name,
54 
55  ValidateLifetime = true,
56  ValidateAudience = true,
57  ValidAudience = typeof(Token).Assembly.GetName().Name,
58 
59  ClockSkew = TimeSpan.FromMinutes(TokenClockSkewMinutes),
60 
61  RequireSignedTokens = true,
62 
63  RequireExpirationTime = true
64  };
65 
66  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
67  }
68 
70  public async Task<Token> CreateToken(Models.User user, CancellationToken cancellationToken)
71  {
72  if (user == null)
73  throw new ArgumentNullException(nameof(user));
74 
75  var now = DateTimeOffset.Now;
76  var nowUnix = now.ToUnixTimeSeconds();
77 
78  // this prevents validation conflicts down the line
79  // tldr we can (theoretically) send a token the same second we receive it
80  // since unix time rounds down, it looks like it came from before the user changed their password
81  // this happens occasionally in unit tests
82  // just delay a second so we can force a round up
83  var lpuUnix = user.LastPasswordUpdate?.ToUnixTimeSeconds();
84  if (nowUnix == lpuUnix)
85  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
86 
87  var expiry = now.AddMinutes(TokenExpiryMinutes);
88  var claims = new Claim[]
89  {
90  new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString(CultureInfo.InvariantCulture)),
91  new Claim(JwtRegisteredClaimNames.Exp, expiry.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)),
92  new Claim(JwtRegisteredClaimNames.Nbf, nowUnix.ToString(CultureInfo.InvariantCulture)),
93  new Claim(JwtRegisteredClaimNames.Iss, ValidationParameters.ValidIssuer),
94  new Claim(JwtRegisteredClaimNames.Aud, ValidationParameters.ValidAudience)
95  };
96 
97  var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(ValidationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
98  return new Token { Bearer = new JwtSecurityTokenHandler().WriteToken(token), ExpiresAt = expiry };
99  }
100  }
101 }
byte[] GetSecureBytes(uint amount)
Generates a secure set of bytes
diff --git a/_user_controller_8cs_source.html b/_user_controller_8cs_source.html index d6e9de464d..0eed3bf44c 100644 --- a/_user_controller_8cs_source.html +++ b/_user_controller_8cs_source.html @@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger< UserController > logger, IOptions< GeneralConfiguration > generalConfigurationOptions)
Construct a UserController
@@ -120,7 +120,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManagerRights
Rights for managing Models.Instances
- +
override async Task< IActionResult > Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
An ApiController representing a TModel
diff --git a/_watchdog_2_watchdog_8cs_source.html b/_watchdog_2_watchdog_8cs_source.html index 52d47a2719..21344818ee 100644 --- a/_watchdog_2_watchdog_8cs_source.html +++ b/_watchdog_2_watchdog_8cs_source.html @@ -92,15 +92,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Watchdog.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.Globalization;
6 using System.IO;
7 using System.Linq;
8 using System.Reflection;
9 using System.Runtime.InteropServices;
10 using System.Threading;
11 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Watchdog
14 {
16  sealed class Watchdog : IWatchdog
17  {
18 
22  readonly ILogger<Watchdog> logger;
23 
28  public Watchdog(ILogger<Watchdog> logger)
29  {
30  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
31  }
32 
34  public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
35  {
36  logger.LogInformation("Host watchdog starting...");
37  logger.LogDebug("PID: {0}", Process.GetCurrentProcess().Id);
38  string updateDirectory = null;
39  try
40  {
41  var enviromentPath = Environment.GetEnvironmentVariable("PATH");
42  var paths = enviromentPath.Split(';');
43  var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
44 
45  var exeName = "dotnet";
46  IEnumerable<string> enumerator;
47  if (isWindows)
48  {
49  exeName += ".exe";
50  enumerator = paths;
51  }
52  else
53  enumerator = paths.Select(x => x.Split(':')).SelectMany(x => x);
54 
55  enumerator = enumerator.Select(x => Path.Combine(x, exeName));
56 
57  var dotnetPath = enumerator
58  .Where(x =>
59  {
60  logger.LogTrace("Checking for dotnet at {0}", x);
61  return File.Exists(x);
62  })
63  .FirstOrDefault();
64 
65  if (dotnetPath == default)
66  {
67  logger.LogCritical("Unable to locate dotnet executable in PATH! Please ensure the .NET Core runtime is installed and is in your PATH!");
68  return;
69  }
70  logger.LogInformation("Detected dotnet executable at {0}", dotnetPath);
71 
72  var rootLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
73 
74  var assemblyStoragePath = Path.Combine(rootLocation, "lib"); //always always next to watchdog
75 #if DEBUG
76  Directory.CreateDirectory(assemblyStoragePath);
77 #endif
78  var defaultAssemblyPath = Path.GetFullPath(Path.Combine(assemblyStoragePath, "Default"));
79 #if DEBUG
80  //just copy the shit where it belongs
81  Directory.Delete(assemblyStoragePath, true);
82  Directory.CreateDirectory(defaultAssemblyPath);
83 
84  var sourcePath = "../../../Tgstation.Server.Host/bin/Debug/netcoreapp2.1";
85  foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
86  Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath));
87 
88  foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
89  File.Copy(newPath, newPath.Replace(sourcePath, defaultAssemblyPath), true);
90 
91  const string AppSettingsJson = "appsettings.json";
92  var rootJson = Path.Combine(rootLocation, AppSettingsJson);
93  File.Delete(rootJson);
94  File.Move(Path.Combine(defaultAssemblyPath, AppSettingsJson), rootJson);
95 #endif
96 
97  var assemblyName = String.Join(".", nameof(Tgstation), nameof(Server), nameof(Host), "dll");
98  var assemblyPath = Path.Combine(defaultAssemblyPath, assemblyName);
99 
100  if (assemblyPath.Contains("\""))
101  {
102  logger.LogCritical("Running from paths with \"'s in the name is not supported!");
103  return;
104  }
105 
106  if (!File.Exists(assemblyPath))
107  {
108  logger.LogCritical("Unable to locate host assembly!");
109  return;
110  }
111 
112  while (!cancellationToken.IsCancellationRequested)
113  using (logger.BeginScope("Host invocation"))
114  {
115  updateDirectory = Path.GetFullPath(Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString()));
116  logger.LogInformation("Update path set to {0}", updateDirectory);
117  using (var process = new Process())
118  {
119  process.StartInfo.FileName = dotnetPath;
120  process.StartInfo.WorkingDirectory = rootLocation; //for appsettings
121 
122  var arguments = new List<string>
123  {
124  '"' + assemblyPath + '"',
125  '"' + updateDirectory + '"'
126  };
127 
128  if (Environment.GetCommandLineArgs().Any(x => x == "--attach-host-debugger"))
129  arguments.Add("--attach-debugger");
130 
131  if (runConfigure)
132  {
133  logger.LogInformation("Running configuration check and wizard if necessary...");
134  arguments.Add("General:SetupWizardMode=Only");
135  }
136 
137  arguments.AddRange(args);
138 
139  process.StartInfo.Arguments = String.Join(" ", arguments);
140 
141  process.StartInfo.UseShellExecute = false; //runs in the same console
142 
143  var tcs = new TaskCompletionSource<object>();
144  process.Exited += (a, b) =>
145  {
146  tcs.TrySetResult(null);
147  };
148  process.EnableRaisingEvents = true;
149 
150  logger.LogInformation("Launching host...");
151 
152  var killedHostProcess = false;
153  try
154  {
155  process.Start();
156 
157  using (var processCts = new CancellationTokenSource())
158  using (processCts.Token.Register(() => tcs.TrySetResult(null)))
159  using (cancellationToken.Register(() =>
160  {
161  if (!Directory.Exists(updateDirectory))
162  {
163  logger.LogInformation("Cancellation requested! Writing shutdown lock file...");
164  File.WriteAllBytes(updateDirectory, Array.Empty<byte>());
165  }
166  else
167  logger.LogWarning("Cancellation requested while update directory exists!");
168 
169  logger.LogInformation("Will force close host process if it doesn't exit in 10 seconds...");
170 
171  try
172  {
173  processCts.CancelAfter(TimeSpan.FromSeconds(10));
174  }
175  catch (ObjectDisposedException) { } //race conditions
176  }))
177  await tcs.Task.ConfigureAwait(false);
178  }
179  catch (InvalidOperationException) { }
180  finally
181  {
182  try
183  {
184  if (!process.HasExited)
185  {
186  killedHostProcess = true;
187  process.Kill();
188  process.WaitForExit();
189  }
190  }
191  catch (InvalidOperationException) { }
192  logger.LogInformation("Host exited!");
193  }
194 
195  if (runConfigure)
196  {
197  logger.LogInformation("Exiting due to configuration check...");
198  return;
199  }
200 
201  switch (process.ExitCode)
202  {
203  case 0:
204  return;
205  case 1:
206  if (!cancellationToken.IsCancellationRequested)
207  //just a restart
208  logger.LogInformation("Watchdog will restart host...");
209  else
210  logger.LogWarning("Host requested restart but watchdog shutdown is in progress!");
211  break;
212  case 2:
213  //update path is now an exception document
214  logger.LogCritical("Host crashed, propagating exception dump...");
215  var data = File.ReadAllText(updateDirectory);
216  try
217  {
218  File.Delete(updateDirectory);
219  }
220  catch (Exception e)
221  {
222  logger.LogWarning("Unable to delete exception dump file at {0}! Exception: {1}", updateDirectory, e);
223  }
224  throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host propagated exception: {0}", data));
225  default:
226  if (killedHostProcess)
227  {
228  logger.LogWarning("Watchdog forced to kill host process!");
229  cancellationToken.ThrowIfCancellationRequested();
230  }
231  throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host crashed with exit code {0}!", process.ExitCode));
232  }
233  }
234 
235  //HEY YOU
236  //BE WARNED THAT IF YOU DEBUGGED THE HOST PROCESS THAT JUST LAUNCHED THE DEBUGGER WILL HOLD A LOCK ON THE DIRECTORY
237  //THIS MEANS THE FIRST DIRECTORY.MOVE WILL THROW
238  if (Directory.Exists(updateDirectory))
239  {
240  logger.LogInformation("Applying server update...");
241  if (isWindows)
242  {
243  //windows dick sucking resource unlocking
244  GC.Collect(Int32.MaxValue, GCCollectionMode.Default, true);
245  await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
246  }
247  var tempPath = Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString());
248  try
249  {
250  Directory.Move(defaultAssemblyPath, tempPath);
251  try
252  {
253  Directory.Move(updateDirectory, defaultAssemblyPath);
254  logger.LogInformation("Server update complete, deleting old server...");
255  try
256  {
257  Directory.Delete(tempPath, true);
258  }
259  catch (Exception e)
260  {
261  logger.LogWarning("Error deleting old server at {0}! Exception: {1}", tempPath, e);
262  }
263  }
264  catch (Exception e)
265  {
266  logger.LogError("Error moving updated server directory, attempting revert! Exception: {0}", e);
267  Directory.Delete(defaultAssemblyPath, true);
268  Directory.Move(tempPath, defaultAssemblyPath);
269  logger.LogInformation("Revert successful!");
270  }
271  }
272  catch (Exception e)
273  {
274  logger.LogWarning("Failed to move out active host assembly! Exception: {0}", e);
275  }
276  }
277  }
278  }
279  catch (OperationCanceledException)
280  {
281  logger.LogDebug("Exiting due to cancellation...");
282  if (!Directory.Exists(updateDirectory))
283  File.Delete(updateDirectory);
284  else
285  Directory.Delete(updateDirectory, true);
286  }
287  catch (Exception e)
288  {
289  logger.LogCritical("Watchdog error! Exception: {0}", e);
290  }
291  finally
292  {
293  logger.LogInformation("Host watchdog exiting...");
294  }
295  }
296  }
297 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.Globalization;
6 using System.IO;
7 using System.Linq;
8 using System.Reflection;
9 using System.Runtime.InteropServices;
10 using System.Threading;
11 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Watchdog
14 {
16  sealed class Watchdog : IWatchdog
17  {
21  readonly ILogger<Watchdog> logger;
22 
27  public Watchdog(ILogger<Watchdog> logger)
28  {
29  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
30  }
31 
33  #pragma warning disable CA1502 // TODO: Decomplexify
34  public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
35  {
36  logger.LogInformation("Host watchdog starting...");
37  logger.LogDebug("PID: {0}", Process.GetCurrentProcess().Id);
38  string updateDirectory = null;
39  try
40  {
41  var enviromentPath = Environment.GetEnvironmentVariable("PATH");
42  var paths = enviromentPath.Split(';');
43  var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
44 
45  var exeName = "dotnet";
46  IEnumerable<string> enumerator;
47  if (isWindows)
48  {
49  exeName += ".exe";
50  enumerator = paths;
51  }
52  else
53  enumerator = paths.Select(x => x.Split(':')).SelectMany(x => x);
54 
55  enumerator = enumerator.Select(x => Path.Combine(x, exeName));
56 
57  var dotnetPath = enumerator
58  .Where(x =>
59  {
60  logger.LogTrace("Checking for dotnet at {0}", x);
61  return File.Exists(x);
62  })
63  .FirstOrDefault();
64 
65  if (dotnetPath == default)
66  {
67  logger.LogCritical("Unable to locate dotnet executable in PATH! Please ensure the .NET Core runtime is installed and is in your PATH!");
68  return;
69  }
70 
71  logger.LogInformation("Detected dotnet executable at {0}", dotnetPath);
72 
73  var rootLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
74 
75  var assemblyStoragePath = Path.Combine(rootLocation, "lib"); // always always next to watchdog
76 #if DEBUG
77  Directory.CreateDirectory(assemblyStoragePath);
78 #endif
79  var defaultAssemblyPath = Path.GetFullPath(Path.Combine(assemblyStoragePath, "Default"));
80 #if DEBUG
81  // just copy the shit where it belongs
82  Directory.Delete(assemblyStoragePath, true);
83  Directory.CreateDirectory(defaultAssemblyPath);
84 
85  var sourcePath = "../../../Tgstation.Server.Host/bin/Debug/netcoreapp2.1";
86  foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories))
87  Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath));
88 
89  foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories))
90  File.Copy(newPath, newPath.Replace(sourcePath, defaultAssemblyPath), true);
91 
92  const string AppSettingsJson = "appsettings.json";
93  var rootJson = Path.Combine(rootLocation, AppSettingsJson);
94  File.Delete(rootJson);
95  File.Move(Path.Combine(defaultAssemblyPath, AppSettingsJson), rootJson);
96 #endif
97 
98  var assemblyName = String.Join(".", nameof(Tgstation), nameof(Server), nameof(Host), "dll");
99  var assemblyPath = Path.Combine(defaultAssemblyPath, assemblyName);
100 
101  if (assemblyPath.Contains("\""))
102  {
103  logger.LogCritical("Running from paths with \"'s in the name is not supported!");
104  return;
105  }
106 
107  if (!File.Exists(assemblyPath))
108  {
109  logger.LogCritical("Unable to locate host assembly!");
110  return;
111  }
112 
113  while (!cancellationToken.IsCancellationRequested)
114  using (logger.BeginScope("Host invocation"))
115  {
116  updateDirectory = Path.GetFullPath(Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString()));
117  logger.LogInformation("Update path set to {0}", updateDirectory);
118  using (var process = new Process())
119  {
120  process.StartInfo.FileName = dotnetPath;
121  process.StartInfo.WorkingDirectory = rootLocation; // for appsettings
122 
123  var arguments = new List<string>
124  {
125  '"' + assemblyPath + '"',
126  '"' + updateDirectory + '"'
127  };
128 
129  if (Environment.GetCommandLineArgs().Any(x => x == "--attach-host-debugger"))
130  arguments.Add("--attach-debugger");
131 
132  if (runConfigure)
133  {
134  logger.LogInformation("Running configuration check and wizard if necessary...");
135  arguments.Add("General:SetupWizardMode=Only");
136  }
137 
138  arguments.AddRange(args);
139 
140  process.StartInfo.Arguments = String.Join(" ", arguments);
141 
142  process.StartInfo.UseShellExecute = false; // runs in the same console
143 
144  var tcs = new TaskCompletionSource<object>();
145  process.Exited += (a, b) =>
146  {
147  tcs.TrySetResult(null);
148  };
149  process.EnableRaisingEvents = true;
150 
151  logger.LogInformation("Launching host...");
152 
153  var killedHostProcess = false;
154  try
155  {
156  process.Start();
157 
158  using (var processCts = new CancellationTokenSource())
159  using (processCts.Token.Register(() => tcs.TrySetResult(null)))
160  using (cancellationToken.Register(() =>
161  {
162  if (!Directory.Exists(updateDirectory))
163  {
164  logger.LogInformation("Cancellation requested! Writing shutdown lock file...");
165  File.WriteAllBytes(updateDirectory, Array.Empty<byte>());
166  }
167  else
168  logger.LogWarning("Cancellation requested while update directory exists!");
169 
170  logger.LogInformation("Will force close host process if it doesn't exit in 10 seconds...");
171 
172  try
173  {
174  processCts.CancelAfter(TimeSpan.FromSeconds(10));
175  }
176  catch (ObjectDisposedException) { } // race conditions
177  }))
178  await tcs.Task.ConfigureAwait(false);
179  }
180  catch (InvalidOperationException) { }
181  finally
182  {
183  try
184  {
185  if (!process.HasExited)
186  {
187  killedHostProcess = true;
188  process.Kill();
189  process.WaitForExit();
190  }
191  }
192  catch (InvalidOperationException) { }
193  logger.LogInformation("Host exited!");
194  }
195 
196  if (runConfigure)
197  {
198  logger.LogInformation("Exiting due to configuration check...");
199  return;
200  }
201 
202  switch (process.ExitCode)
203  {
204  case 0:
205  return;
206  case 1:
207  if (!cancellationToken.IsCancellationRequested)
208  logger.LogInformation("Watchdog will restart host..."); // just a restart
209  else
210  logger.LogWarning("Host requested restart but watchdog shutdown is in progress!");
211  break;
212  case 2:
213  // update path is now an exception document
214  logger.LogCritical("Host crashed, propagating exception dump...");
215  var data = File.ReadAllText(updateDirectory);
216  try
217  {
218  File.Delete(updateDirectory);
219  }
220  catch (Exception e)
221  {
222  logger.LogWarning("Unable to delete exception dump file at {0}! Exception: {1}", updateDirectory, e);
223  }
224 
225 #pragma warning disable CA2201 // Do not raise reserved exception types
226  throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host propagated exception: {0}", data));
227 #pragma warning restore CA2201 // Do not raise reserved exception types
228  default:
229  if (killedHostProcess)
230  {
231  logger.LogWarning("Watchdog forced to kill host process!");
232  cancellationToken.ThrowIfCancellationRequested();
233  }
234 
235 #pragma warning disable CA2201 // Do not raise reserved exception types
236  throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host crashed with exit code {0}!", process.ExitCode));
237 #pragma warning restore CA2201 // Do not raise reserved exception types
238  }
239  }
240 
241  // HEY YOU
242  // BE WARNED THAT IF YOU DEBUGGED THE HOST PROCESS THAT JUST LAUNCHED THE DEBUGGER WILL HOLD A LOCK ON THE DIRECTORY
243  // THIS MEANS THE FIRST DIRECTORY.MOVE WILL THROW
244  if (Directory.Exists(updateDirectory))
245  {
246  logger.LogInformation("Applying server update...");
247  if (isWindows)
248  {
249  // windows dick sucking resource unlocking
250  GC.Collect(Int32.MaxValue, GCCollectionMode.Default, true);
251  await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
252  }
253 
254  var tempPath = Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString());
255  try
256  {
257  Directory.Move(defaultAssemblyPath, tempPath);
258  try
259  {
260  Directory.Move(updateDirectory, defaultAssemblyPath);
261  logger.LogInformation("Server update complete, deleting old server...");
262  try
263  {
264  Directory.Delete(tempPath, true);
265  }
266  catch (Exception e)
267  {
268  logger.LogWarning("Error deleting old server at {0}! Exception: {1}", tempPath, e);
269  }
270  }
271  catch (Exception e)
272  {
273  logger.LogError("Error moving updated server directory, attempting revert! Exception: {0}", e);
274  Directory.Delete(defaultAssemblyPath, true);
275  Directory.Move(tempPath, defaultAssemblyPath);
276  logger.LogInformation("Revert successful!");
277  }
278  }
279  catch (Exception e)
280  {
281  logger.LogWarning("Failed to move out active host assembly! Exception: {0}", e);
282  }
283  }
284  }
285  }
286  catch (OperationCanceledException)
287  {
288  logger.LogDebug("Exiting due to cancellation...");
289  if (!Directory.Exists(updateDirectory))
290  File.Delete(updateDirectory);
291  else
292  Directory.Delete(updateDirectory, true);
293  }
294  catch (Exception e)
295  {
296  logger.LogCritical("Watchdog error! Exception: {0}", e);
297  }
298  finally
299  {
300  logger.LogInformation("Host watchdog exiting...");
301  }
302  }
303  #pragma warning restore CA1502
304  }
305 }
async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
Run the IWatchdog
Definition: Watchdog.cs:34
-
readonly ILogger< Watchdog > logger
The ILogger for the Watchdog
Definition: Watchdog.cs:22
+
readonly ILogger< Watchdog > logger
The ILogger for the Watchdog
Definition: Watchdog.cs:21
The watchdog for a Host
Definition: IWatchdog.cs:9
-
Watchdog(ILogger< Watchdog > logger)
Construct a Watchdog
Definition: Watchdog.cs:28
+
Watchdog(ILogger< Watchdog > logger)
Construct a Watchdog
Definition: Watchdog.cs:27
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
4 using System.Text;
5 using System.Threading;
6 using System.Threading.Tasks;
8 using Tgstation.Server.Host.IO;
9 
10 namespace Tgstation.Server.Host.Components.Byond
11 {
16  {
20  const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
24  const string ByondConfigDir = "byond/cfg";
28  const string ByondDDConfig = "daemon.txt";
32  const string ByondNoPromptTrustedMode = "trusted-check 0";
36  const string ByondDXDir = "byond/directx";
37 
39  public string DreamDaemonName => "dreamdaemon.exe";
40 
42  public string DreamMakerName => "dm.exe";
43 
48 
53 
57  readonly ILogger<WindowsByondInstaller> logger;
58 
62  readonly SemaphoreSlim semaphore;
63 
68 
75  public WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger<WindowsByondInstaller> logger)
76  {
77  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
78  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
79  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
80 
81  semaphore = new SemaphoreSlim(1);
82  installedDirectX = false;
83  }
84 
86  public void Dispose() => semaphore.Dispose();
87 
89  public async Task CleanCache(CancellationToken cancellationToken)
90  {
91  try
92  {
93  await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
94  }
95  catch(OperationCanceledException)
96  {
97  throw;
98  }
99  catch (Exception e)
100  {
101  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
102  }
103  }
104 
106  public Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
107  {
108  var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor);
109 
110  return ioManager.DownloadFile(new Uri(url), cancellationToken);
111  }
112 
114  public async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
115  {
116  async Task SetNoPromptTrusted()
117  {
118  var configPath = ioManager.ConcatPath(path, ByondConfigDir);
119  await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
120  await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
121  };
122 
123  var setNoPromptTrustedModeTask = SetNoPromptTrusted();
124 
125  //after this version lummox made DD depend of directx lol
126  //but then he became amazing and not only fixed it but also gave us 30s compiles \[T]/
127  if (version.Major == 512 && version.Minor >= 1427 && version.Minor < 1452 && !installedDirectX)
128  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
129  //check again because race conditions
130  if (!installedDirectX)
131  {
132  //always install it, it's pretty fast and will do better redundancy checking than us
133  var rbdx = ioManager.ConcatPath(path, ByondDXDir);
134  //noShellExecute because we aren't doing runas shennanigans
135  IProcess directXInstaller;
136  try
137  {
138  directXInstaller = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
139  }
140  catch (Exception e)
141  {
142  throw new JobException("Unable to start DirectX installer process! Is the server running with admin privileges?", e);
143  }
144  using (directXInstaller)
145  {
146  int exitCode;
147  using (cancellationToken.Register(() => directXInstaller.Terminate()))
148  exitCode = await directXInstaller.Lifetime.ConfigureAwait(false);
149  cancellationToken.ThrowIfCancellationRequested();
150 
151  if (exitCode != 0)
152  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Failed to install included DirectX! Exit code: {0}", exitCode));
153  installedDirectX = true;
154  }
155  }
156 
157  await setNoPromptTrustedModeTask.ConfigureAwait(false);
158  }
159  }
160 }
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the WindowsByondInstaller
-
Task< byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
Download a given BYOND version
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
4 using System.Text;
5 using System.Threading;
6 using System.Threading.Tasks;
8 using Tgstation.Server.Host.IO;
9 
10 namespace Tgstation.Server.Host.Components.Byond
11 {
16  {
20  const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
21 
25  const string ByondConfigDir = "byond/cfg";
26 
30  const string ByondDDConfig = "daemon.txt";
31 
35  const string ByondNoPromptTrustedMode = "trusted-check 0";
36 
40  const string ByondDXDir = "byond/directx";
41 
43  public string DreamDaemonName => "dreamdaemon.exe";
44 
46  public string DreamMakerName => "dm.exe";
47 
52 
57 
61  readonly ILogger<WindowsByondInstaller> logger;
62 
66  readonly SemaphoreSlim semaphore;
67 
72 
79  public WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger<WindowsByondInstaller> logger)
80  {
81  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
82  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
83  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
84 
85  semaphore = new SemaphoreSlim(1);
86  installedDirectX = false;
87  }
88 
90  public void Dispose() => semaphore.Dispose();
91 
93  public async Task CleanCache(CancellationToken cancellationToken)
94  {
95  try
96  {
97  await ioManager.DeleteDirectory(ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "byond/cache"), cancellationToken).ConfigureAwait(false);
98  }
99  catch(OperationCanceledException)
100  {
101  throw;
102  }
103  catch (Exception e)
104  {
105  logger.LogWarning("Error deleting BYOND cache! Exception: {0}", e);
106  }
107  }
108 
110  public Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
111  {
112  var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
113 
114  return ioManager.DownloadFile(new Uri(url), cancellationToken);
115  }
116 
118  public async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
119  {
120  async Task SetNoPromptTrusted()
121  {
122  var configPath = ioManager.ConcatPath(path, ByondConfigDir);
123  await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
124  await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
125  }
126 
127  var setNoPromptTrustedModeTask = SetNoPromptTrusted();
128 
129  // after this version lummox made DD depend of directx lol
130  // but then he became amazing and not only fixed it but also gave us 30s compiles \[T]/
131  if (version.Major == 512 && version.Minor >= 1427 && version.Minor < 1452 && !installedDirectX)
132  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
133  if (!installedDirectX)
134  {
135  // ^check again because race conditions
136  // always install it, it's pretty fast and will do better redundancy checking than us
137  var rbdx = ioManager.ConcatPath(path, ByondDXDir);
138 
139  // noShellExecute because we aren't doing runas shennanigans
140  IProcess directXInstaller;
141  try
142  {
143  directXInstaller = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true);
144  }
145  catch (Exception e)
146  {
147  throw new JobException("Unable to start DirectX installer process! Is the server running with admin privileges?", e);
148  }
149 
150  using (directXInstaller)
151  {
152  int exitCode;
153  using (cancellationToken.Register(() => directXInstaller.Terminate()))
154  exitCode = await directXInstaller.Lifetime.ConfigureAwait(false);
155  cancellationToken.ThrowIfCancellationRequested();
156 
157  if (exitCode != 0)
158  throw new JobException(String.Format(CultureInfo.InvariantCulture, "Failed to install included DirectX! Exit code: {0}", exitCode));
159  installedDirectX = true;
160  }
161  }
162 
163  await setNoPromptTrustedModeTask.ConfigureAwait(false);
164  }
165  }
166 }
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the WindowsByondInstaller
+
Task< byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
Download a given BYOND version
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
-
readonly IProcessExecutor processExecutor
The IProcessExecutor for the WindowsByondInstaller
-
readonly ILogger< WindowsByondInstaller > logger
The ILogger for the WindowsByondInstaller
+
readonly IProcessExecutor processExecutor
The IProcessExecutor for the WindowsByondInstaller
+
readonly ILogger< WindowsByondInstaller > logger
The ILogger for the WindowsByondInstaller
For downloading and installing BYOND extractions for a given system
- -
async Task CleanCache(CancellationToken cancellationToken)
Attempts to cleans the BYOND cache folder for the system
+ +
async Task CleanCache(CancellationToken cancellationToken)
Attempts to cleans the BYOND cache folder for the system
void Terminate()
Terminates the process
Abstraction over a System.Diagnostics.Process
Definition: IProcess.cs:9
-
WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger< WindowsByondInstaller > logger)
Construct a WindowsByondInstaller
-
async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
Does actions necessary to get an extracted BYOND installation working
+
WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger< WindowsByondInstaller > logger)
Construct a WindowsByondInstaller
+
async Task InstallByond(string path, Version version, CancellationToken cancellationToken)
Does actions necessary to get an extracted BYOND installation working
Interface for using filesystems
Definition: IIOManager.cs:11
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
-
readonly IIOManager ioManager
The IIOManager for the WindowsByondInstaller
+
readonly IIOManager ioManager
The IIOManager for the WindowsByondInstaller
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:8
diff --git a/_windows_network_prompt_reaper_8cs_source.html b/_windows_network_prompt_reaper_8cs_source.html index 0b0f7a8010..0e42df07ce 100644 --- a/_windows_network_prompt_reaper_8cs_source.html +++ b/_windows_network_prompt_reaper_8cs_source.html @@ -92,14 +92,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WindowsNetworkPromptReaper.cs
-Go to the documentation of this file.
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Runtime.InteropServices;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Components.Watchdog
14 {
17  {
21  const int SendMessageCount = 5;
22 
26  const int RecheckDelayMs = 250;
27 
32 
36  readonly ILogger<WindowsNetworkPromptReaper> logger;
37 
41  readonly CancellationTokenSource cancellationTokenSource;
42 
46  readonly List<IProcess> registeredProcesses;
47 
51  Task runTask;
52 
53  static bool EnumWindow(IntPtr hWnd, IntPtr lParam)
54  {
55  var gcChildhandlesList = GCHandle.FromIntPtr(lParam);
56 
57  if (gcChildhandlesList.Target == null)
58  return false;
59 
60  var childHandles = (List<IntPtr>)gcChildhandlesList.Target;
61  childHandles.Add(hWnd);
62 
63  return true;
64  }
65 
66  static List<IntPtr> GetAllChildHandles(IntPtr main)
67  {
68  var childHandles = new List<IntPtr>();
69  var gcChildhandlesList = GCHandle.Alloc(childHandles);
70  try
71  {
72  var pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList);
73  NativeMethods.EnumWindowProc childProc = new NativeMethods.EnumWindowProc(EnumWindow);
74  NativeMethods.EnumChildWindows(main, childProc, pointerChildHandlesList);
75  }
76  finally
77  {
78  gcChildhandlesList.Free();
79  }
80  return childHandles;
81  }
82 
88  public WindowsNetworkPromptReaper(IAsyncDelayer asyncDelayer, ILogger<WindowsNetworkPromptReaper> logger)
89  {
90  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
91  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
92 
93  registeredProcesses = new List<IProcess>();
94  cancellationTokenSource = new CancellationTokenSource();
95  }
96 
98  public void Dispose() => cancellationTokenSource.Dispose();
99 
100  async Task Run(CancellationToken cancellationToken)
101  {
102  logger.LogDebug("Starting network prompt reaper...");
103  try
104  {
105  while (!cancellationToken.IsCancellationRequested)
106  {
107  await asyncDelayer.Delay(TimeSpan.FromMilliseconds(RecheckDelayMs), cancellationToken).ConfigureAwait(false);
108 
109  IntPtr window;
110  int processId;
111  lock (registeredProcesses)
112  {
113  if (registeredProcesses.Count == 0)
114  continue;
115 
116  window = NativeMethods.FindWindow(null, "Network Accessibility");
117  if (window == IntPtr.Zero)
118  continue;
119 
120  //found a bitch
121  var threadId = NativeMethods.GetWindowThreadProcessId(window, out processId);
122  if (!registeredProcesses.Any(x => x.Id == processId))
123  //not our bitch
124  continue;
125  }
126  logger.LogTrace("Identified \"Network Accessibility\" window in owned process {0}", processId);
127 
128  var found = false;
129  foreach (var I in GetAllChildHandles(window))
130  {
131  const int MaxLength = 10;
132  var stringBuilder = new StringBuilder(MaxLength + 1);
133 
134  if (NativeMethods.GetWindowText(I, stringBuilder, MaxLength) == 0)
135  {
136  logger.LogWarning("Error calling GetWindowText! Exception: {0}", new Win32Exception());
137  continue;
138  }
139 
140  var windowText = stringBuilder.ToString();
141  if (windowText == "Yes")
142  {
143  //smash_button_meme.jpg
144  logger.LogTrace("Sending \"Yes\" button clicks...");
145  for (var J = 0; J < SendMessageCount; ++J)
146  {
147  const int BM_CLICK = 0x00F5;
148  var result = NativeMethods.SendMessage(I, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
149  }
150  found = true;
151  break;
152  }
153  }
154 
155  if (!found)
156  logger.LogDebug("Unable to find \"Yes\" button for \"Network Accessibility\" window in owned process {0}!", processId);
157  }
158  }
159  catch (OperationCanceledException)
160  {
161  logger.LogTrace("Cancelled!");
162  }
163  finally
164  {
165  logger.LogDebug("Exiting network prompt reaper...");
166  }
167  }
168 
170  public Task StartAsync(CancellationToken cancellationToken)
171  {
172  runTask = Run(cancellationTokenSource.Token);
173  return Task.CompletedTask;
174  }
175 
177  public async Task StopAsync(CancellationToken cancellationToken)
178  {
179  logger.LogTrace("Stopping network prompt reaper...");
180  cancellationTokenSource.Cancel();
181  await runTask.ConfigureAwait(false);
182  registeredProcesses.Clear();
183  }
184 
186  public void RegisterProcess(IProcess process)
187  {
188  if (process == null)
189  throw new ArgumentNullException(nameof(process));
190 
191  lock (registeredProcesses)
192  {
193  if (registeredProcesses.Contains(process))
194  throw new InvalidOperationException("This process has already been registered for network prompt reaping!");
195  logger.LogTrace("Registering process {0}...", process.Id);
196  registeredProcesses.Add(process);
197  }
198 
199  process.Lifetime.ContinueWith(x =>
200  {
201  logger.LogTrace("Unregistering process {0}...", process.Id);
202  lock (registeredProcesses)
203  registeredProcesses.Remove(process);
204  }, TaskScheduler.Current);
205  }
206  }
207 }
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
+Go to the documentation of this file.
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Runtime.InteropServices;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Components.Watchdog
14 {
17  {
21  const int SendMessageCount = 5;
22 
26  const int RecheckDelayMs = 250;
27 
32 
36  readonly ILogger<WindowsNetworkPromptReaper> logger;
37 
41  readonly CancellationTokenSource cancellationTokenSource;
42 
46  readonly List<IProcess> registeredProcesses;
47 
51  Task runTask;
52 
53  static bool EnumWindow(IntPtr hWnd, IntPtr lParam)
54  {
55  var gcChildhandlesList = GCHandle.FromIntPtr(lParam);
56 
57  if (gcChildhandlesList.Target == null)
58  return false;
59 
60  var childHandles = (List<IntPtr>)gcChildhandlesList.Target;
61  childHandles.Add(hWnd);
62 
63  return true;
64  }
65 
66  static List<IntPtr> GetAllChildHandles(IntPtr main)
67  {
68  var childHandles = new List<IntPtr>();
69  var gcChildhandlesList = GCHandle.Alloc(childHandles);
70  try
71  {
72  var pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList);
73  NativeMethods.EnumWindowProc childProc = new NativeMethods.EnumWindowProc(EnumWindow);
74  NativeMethods.EnumChildWindows(main, childProc, pointerChildHandlesList);
75  }
76  finally
77  {
78  gcChildhandlesList.Free();
79  }
80 
81  return childHandles;
82  }
83 
89  public WindowsNetworkPromptReaper(IAsyncDelayer asyncDelayer, ILogger<WindowsNetworkPromptReaper> logger)
90  {
91  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
92  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
93 
94  registeredProcesses = new List<IProcess>();
95  cancellationTokenSource = new CancellationTokenSource();
96  }
97 
99  public void Dispose() => cancellationTokenSource.Dispose();
100 
101  async Task Run(CancellationToken cancellationToken)
102  {
103  logger.LogDebug("Starting network prompt reaper...");
104  try
105  {
106  while (!cancellationToken.IsCancellationRequested)
107  {
108  await asyncDelayer.Delay(TimeSpan.FromMilliseconds(RecheckDelayMs), cancellationToken).ConfigureAwait(false);
109 
110  IntPtr window;
111  int processId;
112  lock (registeredProcesses)
113  {
114  if (registeredProcesses.Count == 0)
115  continue;
116 
117  window = NativeMethods.FindWindow(null, "Network Accessibility");
118  if (window == IntPtr.Zero)
119  continue;
120 
121  // found a bitch
122  var threadId = NativeMethods.GetWindowThreadProcessId(window, out processId);
123  if (!registeredProcesses.Any(x => x.Id == processId))
124  continue; // not our bitch
125  }
126 
127  logger.LogTrace("Identified \"Network Accessibility\" window in owned process {0}", processId);
128 
129  var found = false;
130  foreach (var I in GetAllChildHandles(window))
131  {
132  const int MaxLength = 10;
133  var stringBuilder = new StringBuilder(MaxLength + 1);
134 
135  if (NativeMethods.GetWindowText(I, stringBuilder, MaxLength) == 0)
136  {
137  logger.LogWarning("Error calling GetWindowText! Exception: {0}", new Win32Exception());
138  continue;
139  }
140 
141  var windowText = stringBuilder.ToString();
142  if (windowText == "Yes")
143  {
144  // smash_button_meme.jpg
145  logger.LogTrace("Sending \"Yes\" button clicks...");
146  for (var J = 0; J < SendMessageCount; ++J)
147  {
148  const int BM_CLICK = 0x00F5;
149  var result = NativeMethods.SendMessage(I, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
150  }
151 
152  found = true;
153  break;
154  }
155  }
156 
157  if (!found)
158  logger.LogDebug("Unable to find \"Yes\" button for \"Network Accessibility\" window in owned process {0}!", processId);
159  }
160  }
161  catch (OperationCanceledException)
162  {
163  logger.LogTrace("Cancelled!");
164  }
165  finally
166  {
167  logger.LogDebug("Exiting network prompt reaper...");
168  }
169  }
170 
172  public Task StartAsync(CancellationToken cancellationToken)
173  {
174  runTask = Run(cancellationTokenSource.Token);
175  return Task.CompletedTask;
176  }
177 
179  public async Task StopAsync(CancellationToken cancellationToken)
180  {
181  logger.LogTrace("Stopping network prompt reaper...");
182  cancellationTokenSource.Cancel();
183  await runTask.ConfigureAwait(false);
184  registeredProcesses.Clear();
185  }
186 
188  public void RegisterProcess(IProcess process)
189  {
190  if (process == null)
191  throw new ArgumentNullException(nameof(process));
192 
193  lock (registeredProcesses)
194  {
195  if (registeredProcesses.Contains(process))
196  throw new InvalidOperationException("This process has already been registered for network prompt reaping!");
197  logger.LogTrace("Registering process {0}...", process.Id);
198  registeredProcesses.Add(process);
199  }
200 
201  process.Lifetime.ContinueWith(x =>
202  {
203  logger.LogTrace("Unregistering process {0}...", process.Id);
204  lock (registeredProcesses)
205  registeredProcesses.Remove(process);
206  }, TaskScheduler.Current);
207  }
208  }
209 }
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
static int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount)
See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowtextw ...
- + - +
readonly List< IProcess > registeredProcesses
The list of IProcesss registered
static IntPtr FindWindow(string lpClassName, string lpWindowName)
See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-findwindoww ...
@@ -108,11 +108,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam)
See https://msdn.microsoft.com/en-us/library/ms633493(v=VS.85).aspx
Task runTask
The Task representing the lifetime of the WindowsNetworkPromptReaper
Abstraction over a System.Diagnostics.Process
Definition: IProcess.cs:9
-
Native methods used by the code
+
Native methods used by the code
readonly ILogger< WindowsNetworkPromptReaper > logger
The ILogger for the WindowsNetworkPromptReaper
- +
static int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId)
See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowthreadprocessid ...
-
void RegisterProcess(IProcess process)
Register a given process for network prompt reaping
+
void RegisterProcess(IProcess process)
Register a given process for network prompt reaping
@@ -121,7 +121,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
static bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam)
See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-enumchildwindows ...
int Id
The IProcess&#39; ID
Definition: IProcess.cs:14
On Windows, DreamDaemon will show an unskippable prompt when using /world/proc/OpenPort(). This looks out for those prompts and immediately clicks "Yes" if the owning process has registered for it
-
WindowsNetworkPromptReaper(IAsyncDelayer asyncDelayer, ILogger< WindowsNetworkPromptReaper > logger)
Construct a WindowsNetworkPromptReaper
+
WindowsNetworkPromptReaper(IAsyncDelayer asyncDelayer, ILogger< WindowsNetworkPromptReaper > logger)
Construct a WindowsNetworkPromptReaper
diff --git a/_windows_post_write_handler_8cs.html b/_windows_post_write_handler_8cs.html index c51192af0b..ed901f8987 100644 --- a/_windows_post_write_handler_8cs.html +++ b/_windows_post_write_handler_8cs.html @@ -101,7 +101,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

Classes

class  Tgstation.Server.Host.IO.WindowsPostWriteHandler
 IPostWriteHandler for Windows systems More...
 IPostWriteHandler for Windows systems More...
 
- + @@ -251,10 +251,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - + - + @@ -385,14 +385,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - + - + - + diff --git a/class_auto_generated_program.html b/class_auto_generated_program.html index 180eeab0e0..126ab2dcb0 100644 --- a/class_auto_generated_program.html +++ b/class_auto_generated_program.html @@ -295,12 +295,12 @@ Static Private Member Functions
The documentation for this class was generated from the following files: diff --git a/class_tgstation_1_1_server_1_1_api_1_1_api_headers-members.html b/class_tgstation_1_1_server_1_1_api_1_1_api_headers-members.html index 7c54f4144b..0a2f13fa98 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_api_headers-members.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_api_headers-members.html @@ -105,20 +105,20 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - + - + - + - + - +

diff --git a/_windows_post_write_handler_8cs_source.html b/_windows_post_write_handler_8cs_source.html index 5cbcd98907..9ab48fae3d 100644 --- a/_windows_post_write_handler_8cs_source.html +++ b/_windows_post_write_handler_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WindowsPostWriteHandler.cs
-Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Host.IO
4 {
9  {
11  public void HandleWrite(string filePath)
12  {
13  if (filePath == null)
14  throw new ArgumentNullException(nameof(filePath));
15  }
16  }
17 }
+Go to the documentation of this file.
1 using System;
2 
3 namespace Tgstation.Server.Host.IO
4 {
9  {
11  public void HandleWrite(string filePath)
12  {
13  if (filePath == null)
14  throw new ArgumentNullException(nameof(filePath));
15  }
16  }
17 }
Handles changing file modes/permissions after writing
IPostWriteHandler for Windows systems
diff --git a/_windows_symlink_factory_8cs_source.html b/_windows_symlink_factory_8cs_source.html index 20033e69d7..f7a292aa02 100644 --- a/_windows_symlink_factory_8cs_source.html +++ b/_windows_symlink_factory_8cs_source.html @@ -92,13 +92,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WindowsSymlinkFactory.cs
-Go to the documentation of this file.
2 using System;
3 using System.IO;
4 using System.Threading;
5 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.IO
8 {
13  {
15  public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
16  {
17  if (targetPath == null)
18  throw new ArgumentNullException(nameof(targetPath));
19  if (linkPath == null)
20  throw new ArgumentNullException(nameof(linkPath));
21 
22  //check if its not a file
23  var flags = File.Exists(targetPath) ? NativeMethods.CreateSymbolicLinkFlags.None : NativeMethods.CreateSymbolicLinkFlags.Directory;
24 
25  //no don't fucking use this
26  //sure it works in SOME cases
27  //i.e. win10 1803+ and IN DEVELOPER MODE
28  //other times it throws ERROR_INVALID_PARAMETER
29  //but the fucking worst is there is some configuration of windows that accept the argument and allow the function to succeed
30  //BUT IT DOESN'T CREATE THE FUCKING LINK
31  //I AM NOT DEBUGGING THAT SHIT AGAIN AHHH
32  //flags |= NativeMethods.CreateSymbolicLinkFlags.AllowUnprivilegedCreate;
33 
34  cancellationToken.ThrowIfCancellationRequested();
35  if (!NativeMethods.CreateSymbolicLink(linkPath, targetPath, flags))
36  throw new Win32Exception();
37  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
38  }
39 }
+Go to the documentation of this file.
2 using System;
3 using System.IO;
4 using System.Threading;
5 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.IO
8 {
13  {
15  public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
16  {
17  if (targetPath == null)
18  throw new ArgumentNullException(nameof(targetPath));
19  if (linkPath == null)
20  throw new ArgumentNullException(nameof(linkPath));
21 
22  // check if its not a file
23  var flags = File.Exists(targetPath) ? NativeMethods.CreateSymbolicLinkFlags.None : NativeMethods.CreateSymbolicLinkFlags.Directory;
24 
25  /*
26  * no don't fucking use this
27  * sure it works in SOME cases
28  * i.e. win10 1803+ and IN DEVELOPER MODE
29  * other times it throws ERROR_INVALID_PARAMETER
30  * but the fucking worst is there is some configuration of windows that accept the argument and allow the function to succeed
31  * BUT IT DOESN'T CREATE THE FUCKING LINK
32  * I AM NOT DEBUGGING THAT SHIT AGAIN AHHH
33 
34  flags |= NativeMethods.CreateSymbolicLinkFlags.AllowUnprivilegedCreate;
35  */
36 
37  cancellationToken.ThrowIfCancellationRequested();
38  if (!NativeMethods.CreateSymbolicLink(linkPath, targetPath, flags))
39  throw new Win32Exception();
40  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
41  }
42 }
-
CreateSymbolicLinkFlags
See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createsymboliclinka#param...
+
CreateSymbolicLinkFlags
See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createsymboliclinka#param...
static bool CreateSymbolicLink(string lpSymlinkFileName, string lpTargetFileName, CreateSymbolicLinkFlags dwFlags)
See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-createsymboliclinkw ...
-
Native methods used by the code
+
Native methods used by the code
diff --git a/_windows_system_identity_8cs_source.html b/_windows_system_identity_8cs_source.html index 3cc8a06642..21bb24c01c 100644 --- a/_windows_system_identity_8cs_source.html +++ b/_windows_system_identity_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WindowsSystemIdentity.cs
-Go to the documentation of this file.
1 using System;
2 using System.DirectoryServices.AccountManagement;
3 using System.Security.Principal;
4 using System.Threading;
5 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Security
8 {
13  {
15  public string Uid => (userPrincipal?.Sid ?? identity.User).ToString();
16 
18  public string Username => userPrincipal?.Name ?? identity.Name;
19 
23  readonly WindowsIdentity identity;
24 
28  readonly UserPrincipal userPrincipal;
29 
34  public WindowsSystemIdentity(WindowsIdentity identity)
35  {
36  this.identity = identity ?? throw new ArgumentNullException(nameof(identity));
37  }
38 
43  public WindowsSystemIdentity(UserPrincipal userPrincipal)
44  {
45  this.userPrincipal = userPrincipal ?? throw new ArgumentNullException(nameof(userPrincipal));
46  }
47 
49  public void Dispose()
50  {
51  if (identity != null)
52  identity.Dispose();
53  else
54  {
55  var context = userPrincipal.Context;
56  userPrincipal.Dispose();
57  context.Dispose();
58  }
59  }
60 
63  {
64  if (identity != null)
65  {
66  //var newIdentity = (WindowsIdentity)identity.Clone(); //doesn't work because of https://github.com/dotnet/corefx/issues/31841
67 
68  var newIdentity = new WindowsIdentity(identity.Token); //the handle is cloned internally
69 
70  return new WindowsSystemIdentity(newIdentity);
71  }
72  //can't clone a UP, shouldn't be trying to anyway, cloning is for impersonation
73  throw new InvalidOperationException("Cannot clone a UserPrincipal based WindowsSystemIdentity!");
74  }
75 
77  public Task RunImpersonated(Action action, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
78  {
79  if (action == null)
80  throw new ArgumentNullException(nameof(action));
81  if (identity == null)
82  throw new InvalidOperationException("Impersonate using a UserPrincipal based WindowsSystemIdentity!");
83  WindowsIdentity.RunImpersonated(identity.AccessToken, action);
84  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
85  }
86 }
+Go to the documentation of this file.
1 using System;
2 using System.DirectoryServices.AccountManagement;
3 using System.Security.Principal;
4 using System.Threading;
5 using System.Threading.Tasks;
6 
7 namespace Tgstation.Server.Host.Security
8 {
13  {
15  public string Uid => (userPrincipal?.Sid ?? identity.User).ToString();
16 
18  public string Username => userPrincipal?.Name ?? identity.Name;
19 
23  readonly WindowsIdentity identity;
24 
28  readonly UserPrincipal userPrincipal;
29 
34  public WindowsSystemIdentity(WindowsIdentity identity)
35  {
36  this.identity = identity ?? throw new ArgumentNullException(nameof(identity));
37  }
38 
43  public WindowsSystemIdentity(UserPrincipal userPrincipal)
44  {
45  this.userPrincipal = userPrincipal ?? throw new ArgumentNullException(nameof(userPrincipal));
46  }
47 
49  public void Dispose()
50  {
51  if (identity != null)
52  identity.Dispose();
53  else
54  {
55  var context = userPrincipal.Context;
56  userPrincipal.Dispose();
57  context.Dispose();
58  }
59  }
60 
63  {
64  if (identity != null)
65  {
66  // var newIdentity = (WindowsIdentity)identity.Clone(); //doesn't work because of https://github.com/dotnet/corefx/issues/31841
67  var newIdentity = new WindowsIdentity(identity.Token); // the handle is cloned internally
68 
69  return new WindowsSystemIdentity(newIdentity);
70  }
71 
72  // can't clone a UP, shouldn't be trying to anyway, cloning is for impersonation
73  throw new InvalidOperationException("Cannot clone a UserPrincipal based WindowsSystemIdentity!");
74  }
75 
77  public Task RunImpersonated(Action action, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
78  {
79  if (action == null)
80  throw new ArgumentNullException(nameof(action));
81  if (identity == null)
82  throw new InvalidOperationException("Impersonate using a UserPrincipal based WindowsSystemIdentity!");
83  WindowsIdentity.RunImpersonated(identity.AccessToken, action);
84  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
85  }
86 }
Represents a user on the current System.Runtime.InteropServices.OSPlatform
diff --git a/_windows_system_identity_factory_8cs_source.html b/_windows_system_identity_factory_8cs_source.html index d2029a23a8..6d90699ce0 100644 --- a/_windows_system_identity_factory_8cs_source.html +++ b/_windows_system_identity_factory_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
WindowsSystemIdentityFactory.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.Win32.SafeHandles;
3 using System;
4 using System.DirectoryServices.AccountManagement;
5 using System.Security.Principal;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Security
11 {
16  {
20  readonly ILogger<WindowsSystemIdentityFactory> logger;
21 
28  static void GetUserAndDomainName(string input, out string username, out string domainName)
29  {
30  var splits = input.Split('\\');
31  username = splits.Length > 1 ? splits[1] : splits[0];
32  domainName = splits.Length > 1 ? splits[0] : null;
33  }
34 
39  public WindowsSystemIdentityFactory(ILogger<WindowsSystemIdentityFactory> logger)
40  {
41  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
42  }
43 
45  public Task<ISystemIdentity> CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
46  {
47  if (user == null)
48  throw new ArgumentNullException(nameof(user));
49 
50  if (user.SystemIdentifier == null)
51  throw new InvalidOperationException("User's SystemIdentifier must not be null!");
52 
53  PrincipalContext pc = null;
54  UserPrincipal principal = null;
55 
56  bool TryGetPrincipalFromContextType(ContextType contextType)
57  {
58  try
59  {
60  pc = new PrincipalContext(contextType);
61  cancellationToken.ThrowIfCancellationRequested();
62  principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
63  }
64  catch (OperationCanceledException)
65  {
66  throw;
67  }
68  catch (Exception e)
69  {
70  logger.LogWarning("Error loading user for context type {0}! Exception: {1}", contextType, e);
71  }
72  finally
73  {
74  if (principal == null)
75  {
76  pc?.Dispose();
77  cancellationToken.ThrowIfCancellationRequested();
78  }
79  }
80  return principal != null;
81  };
82 
83  if (!TryGetPrincipalFromContextType(ContextType.Machine) && !TryGetPrincipalFromContextType(ContextType.Domain))
84  return null;
85  return (ISystemIdentity)new WindowsSystemIdentity(principal);
86  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
87 
89  public Task<ISystemIdentity> CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
90  {
91  if (username == null)
92  throw new ArgumentNullException(nameof(username));
93  if (password == null)
94  throw new ArgumentNullException(nameof(password));
95 
96  var originalUsername = username;
97  GetUserAndDomainName(originalUsername, out username, out var domainName);
98 
99  var res = NativeMethods.LogonUser(username, domainName, password, 3 /*LOGON32_LOGON_NETWORK*/, 0 /*LOGON32_PROVIDER_DEFAULT*/, out var token);
100  if (!res)
101  {
102  logger.LogTrace("Failed to log in username {0}!", originalUsername);
103  return null;
104  }
105 
106  logger.LogTrace("Successfully logged in username {0}!", originalUsername);
107 
108  using (var handle = new SafeAccessTokenHandle(token)) //checked internally, windows identity always duplicates the handle when constructed with a userToken
109  return (ISystemIdentity)new WindowsSystemIdentity(new WindowsIdentity(handle.DangerousGetHandle())); //https://github.com/dotnet/corefx/blob/6ed61acebe3214fcf79b4274f2bb9b55c0604a4d/src/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L271
110  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
111  }
112 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Microsoft.Win32.SafeHandles;
3 using System;
4 using System.DirectoryServices.AccountManagement;
5 using System.Security.Principal;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Security
11 {
16  {
20  readonly ILogger<WindowsSystemIdentityFactory> logger;
21 
28  static void GetUserAndDomainName(string input, out string username, out string domainName)
29  {
30  var splits = input.Split('\\');
31  username = splits.Length > 1 ? splits[1] : splits[0];
32  domainName = splits.Length > 1 ? splits[0] : null;
33  }
34 
39  public WindowsSystemIdentityFactory(ILogger<WindowsSystemIdentityFactory> logger)
40  {
41  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
42  }
43 
45  public Task<ISystemIdentity> CreateSystemIdentity(User user, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
46  {
47  if (user == null)
48  throw new ArgumentNullException(nameof(user));
49 
50  if (user.SystemIdentifier == null)
51  throw new InvalidOperationException("User's SystemIdentifier must not be null!");
52 
53  PrincipalContext pc = null;
54  UserPrincipal principal = null;
55 
56  bool TryGetPrincipalFromContextType(ContextType contextType)
57  {
58  try
59  {
60  pc = new PrincipalContext(contextType);
61  cancellationToken.ThrowIfCancellationRequested();
62  principal = UserPrincipal.FindByIdentity(pc, user.SystemIdentifier);
63  }
64  catch (OperationCanceledException)
65  {
66  throw;
67  }
68  catch (Exception e)
69  {
70  logger.LogWarning("Error loading user for context type {0}! Exception: {1}", contextType, e);
71  }
72  finally
73  {
74  if (principal == null)
75  {
76  pc?.Dispose();
77  cancellationToken.ThrowIfCancellationRequested();
78  }
79  }
80 
81  return principal != null;
82  }
83 
84  if (!TryGetPrincipalFromContextType(ContextType.Machine) && !TryGetPrincipalFromContextType(ContextType.Domain))
85  return null;
86  return (ISystemIdentity)new WindowsSystemIdentity(principal);
87  },
88  cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
89 
91  public Task<ISystemIdentity> CreateSystemIdentity(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
92  {
93  if (username == null)
94  throw new ArgumentNullException(nameof(username));
95  if (password == null)
96  throw new ArgumentNullException(nameof(password));
97 
98  var originalUsername = username;
99  GetUserAndDomainName(originalUsername, out username, out var domainName);
100 
101  var res = NativeMethods.LogonUser(username, domainName, password, 3 /*LOGON32_LOGON_NETWORK*/, 0 /*LOGON32_PROVIDER_DEFAULT*/, out var token);
102  if (!res)
103  {
104  logger.LogTrace("Failed to log in username {0}!", originalUsername);
105  return null;
106  }
107 
108  logger.LogTrace("Successfully logged in username {0}!", originalUsername);
109 
110  using (var handle = new SafeAccessTokenHandle(token)) // checked internally, windows identity always duplicates the handle when constructed with a userToken
111  return (ISystemIdentity)new WindowsSystemIdentity(new WindowsIdentity(handle.DangerousGetHandle())); // https://github.com/dotnet/corefx/blob/6ed61acebe3214fcf79b4274f2bb9b55c0604a4d/src/System.Security.Principal.Windows/src/System/Security/Principal/WindowsIdentity.cs#L271
112  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
113  }
114 }
Represents a user on the current System.Runtime.InteropServices.OSPlatform
@@ -103,7 +103,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
static bool LogonUser(string lpszUsername, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken)
See https://msdn.microsoft.com/en-us/library/windows/desktop/aa378184(v=vs.85).aspx ...
-
Native methods used by the code
+
Native methods used by the code
static void GetUserAndDomainName(string input, out string username, out string domainName)
Extract the username and domain name from a string in the format "username\\domainname" ...
diff --git a/annotated.html b/annotated.html index ad354e2d2a..e647fa6632 100644 --- a/annotated.html +++ b/annotated.html @@ -113,7 +113,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

 NModels
ApiVersionTgstation.Server.Api.ApiHeaders
ApiVersionHeaderTgstation.Server.Api.ApiHeadersprivate
ApplicationJsonTgstation.Server.Api.ApiHeaders
assemblyNameTgstation.Server.Api.ApiHeadersprivatestatic
AssemblyNameTgstation.Server.Api.ApiHeadersprivatestatic
CheckCompatibility(Version otherVersion)Tgstation.Server.Api.ApiHeadersstatic
Compatible()Tgstation.Server.Api.ApiHeaders
InstanceIdTgstation.Server.Api.ApiHeaders
instanceIdHeaderTgstation.Server.Api.ApiHeadersprivate
InstanceIdHeaderTgstation.Server.Api.ApiHeadersprivate
IsTokenAuthenticationTgstation.Server.Api.ApiHeaders
jwtAuthenticationSchemeTgstation.Server.Api.ApiHeadersprivate
JwtAuthenticationSchemeTgstation.Server.Api.ApiHeadersprivate
PasswordTgstation.Server.Api.ApiHeaders
passwordAuthenticationSchemeTgstation.Server.Api.ApiHeadersprivate
PasswordAuthenticationSchemeTgstation.Server.Api.ApiHeadersprivate
SetRequestHeaders(HttpRequestHeaders headers, long?instanceId=null)Tgstation.Server.Api.ApiHeaders
TokenTgstation.Server.Api.ApiHeaders
UserAgentTgstation.Server.Api.ApiHeaders
UsernameTgstation.Server.Api.ApiHeaders
usernameHeaderTgstation.Server.Api.ApiHeadersprivate
UsernameHeaderTgstation.Server.Api.ApiHeadersprivate
VersionTgstation.Server.Api.ApiHeadersstatic
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 2f9ffc342a..318931a804 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 @@ -144,7 +144,7 @@ Public Attributes - +

Static Public Attributes

static Version Version => assemblyName.Version
static Version Version => AssemblyName.Version
 Get the version of the Api the caller is using More...
 
@@ -180,24 +180,24 @@ Private Attributes - - - - - - - - - - - - + + + + + + + + + + + +
const string ApiVersionHeader = "Api"
 The ApiVersion header key More...
 
const string usernameHeader = "Username"
 The Username header key More...
 
const string instanceIdHeader = "Instance"
 The InstanceId header key More...
 
const string jwtAuthenticationScheme = "Bearer"
 The JWT authentication header scheme More...
 
const string passwordAuthenticationScheme = "Password"
 The password authentication header scheme More...
 
const string UsernameHeader = "Username"
 The Username header key More...
 
const string InstanceIdHeader = "Instance"
 The InstanceId header key More...
 
const string JwtAuthenticationScheme = "Bearer"
 The JWT authentication header scheme More...
 
const string PasswordAuthenticationScheme = "Password"
 The password authentication header scheme More...
 
- - - + + +

Static Private Attributes

static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName()
 The current AssemblyName More...
 
static readonly AssemblyName AssemblyName = Assembly.GetExecutingAssembly().GetName()
 The current System.Reflection.AssemblyName More...
 

Detailed Description

Represents the header that must be present for every server request

@@ -308,20 +308,20 @@ Static Private Attributes

Definition at line 133 of file ApiHeaders.cs.

-
134  {
135  var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJson);
136  if (!requestHeaders.Accept.Any(x => x.MediaType == jsonAccept.MediaType))
137  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Client does not accept {0}!", ApplicationJson));
138 
139  if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || !ProductInfoHeaderValue.TryParse(userAgentValues.FirstOrDefault(), out var clientUserAgent))
140  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} headers!", HeaderNames.UserAgent));
141 
142  //assure the client user agent has a name and version
143  if (String.IsNullOrWhiteSpace(clientUserAgent.Product.Name) || !Version.TryParse(clientUserAgent.Product.Version, out var clientVersion))
144  throw new InvalidOperationException("Malformed client user agent!");
145 
146  //make sure the api header matches ours
147  if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != assemblyName.Name)
148  throw new InvalidOperationException("Missing API version!");
149 
150  if (!Version.TryParse(apiUserAgent.Product.Version, out var apiVersion))
151  throw new InvalidOperationException("Malformed API version!");
152 
153  ApiVersion = apiVersion;
154  UserAgent = clientUserAgent.Product;
155 
156  if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
157  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} header!", HeaderNames.Authorization));
158  var auth = authorization.First();
159  var splits = new List<string>(auth.Split(' '));
160  var scheme = splits.First();
161  if (String.IsNullOrWhiteSpace(scheme))
162  throw new InvalidOperationException("Missing authentication scheme!");
163 
164  splits.RemoveAt(0);
165  var parameter = String.Concat(splits);
166  if (String.IsNullOrEmpty(parameter))
167  throw new InvalidOperationException("Missing authentication parameter!");
168 
169  if (requestHeaders.Headers.TryGetValue(instanceIdHeader, out var instanceIdValues))
170  {
171  var instanceIdString = instanceIdValues.FirstOrDefault();
172  if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
173  InstanceId = instanceId;
174  }
175 
176  switch (scheme)
177  {
179  Token = parameter;
180  break;
182  Password = parameter;
183  var fail = !requestHeaders.Headers.TryGetValue(usernameHeader, out var values);
184  if (!fail)
185  {
186  Username = values.FirstOrDefault();
187  fail = String.IsNullOrWhiteSpace(Username);
188  }
189  if (fail)
190  throw new InvalidOperationException("Missing Username header!");
191  break;
192  default:
193  throw new InvalidOperationException("Invalid authentication scheme!");
194  }
195  }
const string jwtAuthenticationScheme
The JWT authentication header scheme
Definition: ApiHeaders.cs:41
+
134  {
135  var jsonAccept = new Microsoft.Net.Http.Headers.MediaTypeHeaderValue(ApplicationJson);
136  if (!requestHeaders.Accept.Any(x => x.MediaType == jsonAccept.MediaType))
137  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Client does not accept {0}!", ApplicationJson));
138 
139  if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || !ProductInfoHeaderValue.TryParse(userAgentValues.FirstOrDefault(), out var clientUserAgent))
140  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} headers!", HeaderNames.UserAgent));
141 
142  // assure the client user agent has a name and version
143  if (String.IsNullOrWhiteSpace(clientUserAgent.Product.Name) || !Version.TryParse(clientUserAgent.Product.Version, out var clientVersion))
144  throw new InvalidOperationException("Malformed client user agent!");
145 
146  // make sure the api header matches ours
147  if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != AssemblyName.Name)
148  throw new InvalidOperationException("Missing API version!");
149 
150  if (!Version.TryParse(apiUserAgent.Product.Version, out var apiVersion))
151  throw new InvalidOperationException("Malformed API version!");
152 
153  ApiVersion = apiVersion;
154  UserAgent = clientUserAgent.Product;
155 
156  if (!requestHeaders.Headers.TryGetValue(HeaderNames.Authorization, out StringValues authorization))
157  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} header!", HeaderNames.Authorization));
158  var auth = authorization.First();
159  var splits = new List<string>(auth.Split(' '));
160  var scheme = splits.First();
161  if (String.IsNullOrWhiteSpace(scheme))
162  throw new InvalidOperationException("Missing authentication scheme!");
163 
164  splits.RemoveAt(0);
165  var parameter = String.Concat(splits);
166  if (String.IsNullOrEmpty(parameter))
167  throw new InvalidOperationException("Missing authentication parameter!");
168 
169  if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
170  {
171  var instanceIdString = instanceIdValues.FirstOrDefault();
172  if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
173  InstanceId = instanceId;
174  }
175 
176  switch (scheme)
177  {
179  Token = parameter;
180  break;
182  Password = parameter;
183  var fail = !requestHeaders.Headers.TryGetValue(UsernameHeader, out var values);
184  if (!fail)
185  {
186  Username = values.FirstOrDefault();
187  fail = String.IsNullOrWhiteSpace(Username);
188  }
189 
190  if (fail)
191  throw new InvalidOperationException("Missing Username header!");
192  break;
193  default:
194  throw new InvalidOperationException("Invalid authentication scheme!");
195  }
196  }
static readonly AssemblyName AssemblyName
The current System.Reflection.AssemblyName
Definition: ApiHeaders.cs:51
const string ApplicationJson
TODO: Remove this when https://github.com/dotnet/corefx/pull/26701 makes it into the sdk ...
Definition: ApiHeaders.cs:21
Version ApiVersion
The client&#39;s API version
Definition: ApiHeaders.cs:71
+
const string PasswordAuthenticationScheme
The password authentication header scheme
Definition: ApiHeaders.cs:46
string Username
The client&#39;s username
Definition: ApiHeaders.cs:81
ProductHeaderValue UserAgent
The client&#39;s user agent
Definition: ApiHeaders.cs:66
-
const string instanceIdHeader
The InstanceId header key
Definition: ApiHeaders.cs:36
string Password
The client&#39;s password
Definition: ApiHeaders.cs:86
-
const string passwordAuthenticationScheme
The password authentication header scheme
Definition: ApiHeaders.cs:46
-
const string usernameHeader
The Username header key
Definition: ApiHeaders.cs:31
+
const string JwtAuthenticationScheme
The JWT authentication header scheme
Definition: ApiHeaders.cs:41
const string ApiVersionHeader
The ApiVersion header key
Definition: ApiHeaders.cs:26
+
const string InstanceIdHeader
The InstanceId header key
Definition: ApiHeaders.cs:36
string Token
The client&#39;s JWT
Definition: ApiHeaders.cs:76
long InstanceId
The Models.Instance.Id being accessed
Definition: ApiHeaders.cs:61
+
const string UsernameHeader
The Username header key
Definition: ApiHeaders.cs:31
static Version Version
Get the version of the Api the caller is using
Definition: ApiHeaders.cs:56
-
static readonly AssemblyName assemblyName
The current AssemblyName
Definition: ApiHeaders.cs:51
@@ -380,8 +380,8 @@ Static Private Attributes -

Definition at line 204 of file ApiHeaders.cs.

-
205  {
206  UserAgent = userAgent;
207  Token = token;
208  Username = username;
209  Password = password;
211  }
Version ApiVersion
The client&#39;s API version
Definition: ApiHeaders.cs:71
+

Definition at line 205 of file ApiHeaders.cs.

+
206  {
207  UserAgent = userAgent;
208  Token = token;
209  Username = username;
210  Password = password;
212  }
Version ApiVersion
The client&#39;s API version
Definition: ApiHeaders.cs:71
string Username
The client&#39;s username
Definition: ApiHeaders.cs:81
ProductHeaderValue UserAgent
The client&#39;s user agent
Definition: ApiHeaders.cs:66
string Password
The client&#39;s password
Definition: ApiHeaders.cs:86
@@ -440,7 +440,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().

@@ -477,23 +477,23 @@ Static Private Attributes -

Definition at line 224 of file ApiHeaders.cs.

+

Definition at line 225 of file ApiHeaders.cs.

-

Referenced by Tgstation.Server.Client.ApiClient.RunRequest< TResult >().

-
225  {
226  if (headers == null)
227  throw new ArgumentNullException(nameof(headers));
228  if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
229  throw new InvalidOperationException("Specified instance ID in constructor and SetRequestHeaders!");
230 
231  headers.Clear();
232  headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
234  headers.Authorization = new AuthenticationHeaderValue(jwtAuthenticationScheme, Token);
235  else
236  {
237  headers.Authorization = new AuthenticationHeaderValue(passwordAuthenticationScheme, Password);
238  headers.Add(usernameHeader, Username);
239  }
240  headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
241  headers.Add(ApiVersionHeader, new ProductHeaderValue(assemblyName.Name, ApiVersion.ToString()).ToString());
242  instanceId = instanceId ?? InstanceId;
243  if (instanceId.HasValue)
244  headers.Add(instanceIdHeader, instanceId.ToString());
245  }
const string jwtAuthenticationScheme
The JWT authentication header scheme
Definition: ApiHeaders.cs:41
+

Referenced by Tgstation.Server.Client.ApiClient.RunRequest< TResult >().

+
226  {
227  if (headers == null)
228  throw new ArgumentNullException(nameof(headers));
229  if (instanceId.HasValue && InstanceId.HasValue && instanceId != InstanceId)
230  throw new InvalidOperationException("Specified instance ID in constructor and SetRequestHeaders!");
231 
232  headers.Clear();
233  headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
235  headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
236  else
237  {
238  headers.Authorization = new AuthenticationHeaderValue(PasswordAuthenticationScheme, Password);
239  headers.Add(UsernameHeader, Username);
240  }
241 
242  headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
243  headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
244  instanceId = instanceId ?? InstanceId;
245  if (instanceId.HasValue)
246  headers.Add(InstanceIdHeader, instanceId.ToString());
247  }
static readonly AssemblyName AssemblyName
The current System.Reflection.AssemblyName
Definition: ApiHeaders.cs:51
const string ApplicationJson
TODO: Remove this when https://github.com/dotnet/corefx/pull/26701 makes it into the sdk ...
Definition: ApiHeaders.cs:21
Version ApiVersion
The client&#39;s API version
Definition: ApiHeaders.cs:71
+
const string PasswordAuthenticationScheme
The password authentication header scheme
Definition: ApiHeaders.cs:46
string Username
The client&#39;s username
Definition: ApiHeaders.cs:81
ProductHeaderValue UserAgent
The client&#39;s user agent
Definition: ApiHeaders.cs:66
-
const string instanceIdHeader
The InstanceId header key
Definition: ApiHeaders.cs:36
string Password
The client&#39;s password
Definition: ApiHeaders.cs:86
-
const string passwordAuthenticationScheme
The password authentication header scheme
Definition: ApiHeaders.cs:46
bool IsTokenAuthentication
If the header uses password or JWT authentication
Definition: ApiHeaders.cs:91
-
const string usernameHeader
The Username header key
Definition: ApiHeaders.cs:31
+
const string JwtAuthenticationScheme
The JWT authentication header scheme
Definition: ApiHeaders.cs:41
const string ApiVersionHeader
The ApiVersion header key
Definition: ApiHeaders.cs:26
+
const string InstanceIdHeader
The InstanceId header key
Definition: ApiHeaders.cs:36
string Token
The client&#39;s JWT
Definition: ApiHeaders.cs:76
long InstanceId
The Models.Instance.Id being accessed
Definition: ApiHeaders.cs:61
-
static readonly AssemblyName assemblyName
The current AssemblyName
Definition: ApiHeaders.cs:51
+
const string UsernameHeader
The Username header key
Definition: ApiHeaders.cs:31
@@ -536,11 +536,11 @@ Static Private Attributes

Definition at line 21 of file ApiHeaders.cs.

-

Referenced by Tgstation.Server.Client.ApiClient.RunRequest< TResult >().

+

Referenced by Tgstation.Server.Client.ApiClient.RunRequest< TResult >().

- +
@@ -548,7 +548,7 @@ Static Private Attributes @@ -558,13 +558,13 @@ Static Private Attributes
- +
readonly AssemblyName Tgstation.Server.Api.ApiHeaders.assemblyName = Assembly.GetExecutingAssembly().GetName()readonly AssemblyName Tgstation.Server.Api.ApiHeaders.AssemblyName = Assembly.GetExecutingAssembly().GetName()
-

The current AssemblyName

+

The current System.Reflection.AssemblyName

Definition at line 51 of file ApiHeaders.cs.

- +
@@ -572,7 +572,7 @@ Static Private Attributes @@ -602,11 +602,11 @@ Static Private Attributes

Definition at line 91 of file ApiHeaders.cs.

-

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

+

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

- +
- +
const string Tgstation.Server.Api.ApiHeaders.instanceIdHeader = "Instance"const string Tgstation.Server.Api.ApiHeaders.InstanceIdHeader = "Instance"
@@ -614,7 +614,7 @@ Static Private Attributes @@ -630,7 +630,7 @@ Static Private Attributes - +
- +
const string Tgstation.Server.Api.ApiHeaders.jwtAuthenticationScheme = "Bearer"const string Tgstation.Server.Api.ApiHeaders.JwtAuthenticationScheme = "Bearer"
@@ -638,7 +638,7 @@ Static Private Attributes @@ -654,7 +654,7 @@ Static Private Attributes - +
- +
const string Tgstation.Server.Api.ApiHeaders.passwordAuthenticationScheme = "Password"const string Tgstation.Server.Api.ApiHeaders.PasswordAuthenticationScheme = "Password"
@@ -662,7 +662,7 @@ Static Private Attributes @@ -686,7 +686,7 @@ Static Private Attributes @@ -727,7 +727,7 @@ Static Private Attributes

Definition at line 71 of file ApiHeaders.cs.

-

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

+

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

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

Definition at line 61 of file ApiHeaders.cs.

-

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

+

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

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

Definition at line 86 of file ApiHeaders.cs.

-

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

+

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

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

Definition at line 66 of file ApiHeaders.cs.

-

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

+

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

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

Definition at line 81 of file ApiHeaders.cs.

-

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

+

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

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_administration.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_administration.html index 5b116e921e..5e3d342495 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_administration.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_administration.html @@ -99,7 +99,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-

+

Represents administrative server information More...

- +
const string Tgstation.Server.Api.ApiHeaders.usernameHeader = "Username"const string Tgstation.Server.Api.ApiHeaders.UsernameHeader = "Username"
- +
Version Tgstation.Server.Api.ApiHeaders.Version => assemblyName.VersionVersion Tgstation.Server.Api.ApiHeaders.Version => AssemblyName.Version

@@ -118,8 +118,9 @@ Properties

 

Detailed Description

-
-

Definition at line 6 of file Administration.cs.

+

Represents administrative server information

+ +

Definition at line 8 of file Administration.cs.

Property Documentation

@@ -141,7 +142,7 @@ Properties

The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If Version.Minor is higher than NewVersion's the update cannot be applied due to API changes

-

Definition at line 21 of file Administration.cs.

+

Definition at line 23 of file Administration.cs.

Referenced by Tgstation.Server.Tests.AdministrationTest.TestRead().

@@ -167,7 +168,7 @@ Properties

Changes the version of Tgstation.Server.Host to the given version from the upstream repository

-

Definition at line 26 of file Administration.cs.

+

Definition at line 28 of file Administration.cs.

@@ -191,7 +192,7 @@ Properties

The GitHub repository the server is built to recieve updates from

-

Definition at line 16 of file Administration.cs.

+

Definition at line 18 of file Administration.cs.

@@ -215,7 +216,7 @@ Properties

If the server is running on a windows operating system

-

Definition at line 11 of file Administration.cs.

+

Definition at line 13 of file Administration.cs.

Referenced by Tgstation.Server.Tests.AdministrationTest.TestRead().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_chat_channel.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_chat_channel.html index f68be92b14..e4f2c3087e 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_chat_channel.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_chat_channel.html @@ -159,7 +159,7 @@ Properties

Definition at line 18 of file ChatChannel.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.MapChannels().

+

Referenced by Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.MapChannels().

@@ -209,7 +209,7 @@ Properties

Definition at line 24 of file ChatChannel.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.MapChannels().

+

Referenced by Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.MapChannels().

@@ -283,7 +283,7 @@ Properties

Definition at line 41 of file ChatChannel.cs.

-

Referenced by Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.MapChannels().

+

Referenced by Tgstation.Server.Host.Components.Chat.Providers.DiscordProvider.MapChannels().

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 a0338366e9..18ac7893c5 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 @@ -165,7 +165,7 @@ Properties

Definition at line 21 of file CompileJob.cs.

-

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

+

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

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_configuration_file.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_configuration_file.html index 87e1fb057f..af1cc8632b 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_configuration_file.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_configuration_file.html @@ -147,7 +147,7 @@ Properties

Definition at line 16 of file ConfigurationFile.cs.

-

Referenced by Tgstation.Server.Host.Components.StaticFiles.Configuration.Read(), and Tgstation.Server.Host.Components.StaticFiles.Configuration.Write().

+

Referenced by Tgstation.Server.Host.Components.StaticFiles.Configuration.Read(), and Tgstation.Server.Host.Components.StaticFiles.Configuration.Write().

@@ -171,7 +171,7 @@ Properties

The content of the ConfigurationFile. Will be if AccessDenied is or during listing and write operations

-

Definition at line 31 of file ConfigurationFile.cs.

+

Definition at line 32 of file ConfigurationFile.cs.

@@ -197,7 +197,7 @@ Properties

Definition at line 21 of file ConfigurationFile.cs.

-

Referenced by Tgstation.Server.Host.Components.StaticFiles.Configuration.Read(), and Tgstation.Server.Host.Components.StaticFiles.Configuration.Write().

+

Referenced by Tgstation.Server.Host.Components.StaticFiles.Configuration.Read(), and Tgstation.Server.Host.Components.StaticFiles.Configuration.Write().

@@ -247,7 +247,7 @@ Properties

Definition at line 11 of file ConfigurationFile.cs.

-

Referenced by Tgstation.Server.Tests.Instance.ConfigurationTest.FileExists(), Tgstation.Server.Client.Components.ConfigurationClient.Read(), and Tgstation.Server.Tests.Instance.ConfigurationTest.TestDeleteDirectory().

+

Referenced by Tgstation.Server.Tests.Instance.ConfigurationTest.FileExists(), Tgstation.Server.Client.Components.ConfigurationClient.Read(), and Tgstation.Server.Tests.Instance.ConfigurationTest.TestDeleteDirectory().

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

Definition at line 13 of file ErrorMessage.cs.

-

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

+

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

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance.html index 554ccf2cb0..6fb13bdbc4 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 @@ -115,6 +115,7 @@ Inheritance diagram for Tgstation.Server.Api.Models.Instance:

Public Member Functions

Instance CloneMetadata () + Create a clone of the essential Instance metadata More...
 

@@ -159,7 +160,8 @@ Properties

-

+

Create a clone of the essential Instance metadata

+
Returns
A clone of the essential Instance metadata
@@ -236,7 +238,7 @@ Properties

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.InstanceUserController.Delete(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.DreamMakerController.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.Core.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().

+

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.InstanceUserController.Delete(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.DreamMakerController.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.Core.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().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance_user.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance_user.html index a48c90988c..64a740c286 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance_user.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_instance_user.html @@ -261,7 +261,7 @@ Properties

Definition at line 39 of file InstanceUser.cs.

-

Referenced by Tgstation.Server.Host.Controllers.DreamMakerController.Update().

+

Referenced by Tgstation.Server.Host.Controllers.DreamMakerController.Update().

@@ -335,7 +335,7 @@ Properties

Definition at line 15 of file InstanceUser.cs.

-

Referenced by Tgstation.Server.Client.Components.InstanceUserClient.InstanceUserClient().

+

Referenced by Tgstation.Server.Client.Components.InstanceUserClient.InstanceUserClient().

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 beb804972b..bbcf126f23 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 @@ -208,7 +208,7 @@ Properties

Definition at line 26 of file ChatBot.cs.

-

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

+

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

@@ -234,7 +234,7 @@ Properties

Definition at line 15 of file ChatBot.cs.

-

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

+

Referenced by Tgstation.Server.Host.Components.Chat.Chat.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_chat_connection_string_builder.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_chat_connection_string_builder.html index 1ac01442d4..09812792f6 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_chat_connection_string_builder.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_chat_connection_string_builder.html @@ -152,7 +152,7 @@ Properties

Gets the ChatBot.ConnectionString associated with the ChatConnectionStringBuilder

-
Returns
+
Returns
The ChatBot.ConnectionString associated with the ChatConnectionStringBuilder

Implemented in Tgstation.Server.Api.Models.IrcConnectionStringBuilder, and Tgstation.Server.Api.Models.DiscordConnectionStringBuilder.

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 c3a20756ce..34f2b3f80e 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 @@ -157,7 +157,7 @@ Properties

Definition at line 32 of file CompileJob.cs.

-

Referenced by Tgstation.Server.Host.Components.Compiler.DmbFactory.CleanJob(), and Tgstation.Server.Host.Components.Compiler.DmbFactory.CleanUnusedCompileJobs().

+

Referenced by Tgstation.Server.Host.Components.Compiler.DmbFactory.CleanJob(), and Tgstation.Server.Host.Components.Compiler.DmbFactory.CleanUnusedCompileJobs().

@@ -207,7 +207,7 @@ Properties

Definition at line 14 of file CompileJob.cs.

-

Referenced by Tgstation.Server.Host.Components.Compiler.DmbFactory.CleanJob(), Tgstation.Server.Client.Components.DreamMakerClient.DreamMakerClient(), Tgstation.Server.Host.Components.Compiler.DmbFactory.FromCompileJob(), Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Compiler.DmbFactory.LockNextDmb().

+

Referenced by Tgstation.Server.Host.Components.Compiler.DmbFactory.CleanJob(), Tgstation.Server.Client.Components.DreamMakerClient.DreamMakerClient(), Tgstation.Server.Host.Components.Compiler.DmbFactory.FromCompileJob(), Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Compiler.DmbFactory.LockNextDmb().

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 aac331b237..c433ba9635 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 @@ -166,7 +166,7 @@ Properties
Returns
if they match, otherwise
-

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

+

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

@@ -193,7 +193,7 @@ Properties

Definition at line 15 of file DreamDaemonLaunchParameters.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

@@ -219,7 +219,7 @@ Properties

Definition at line 27 of file DreamDaemonLaunchParameters.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

@@ -245,7 +245,7 @@ Properties

Definition at line 33 of file DreamDaemonLaunchParameters.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

@@ -271,7 +271,7 @@ Properties

Definition at line 21 of file DreamDaemonLaunchParameters.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

@@ -297,7 +297,7 @@ Properties

Definition at line 39 of file DreamDaemonLaunchParameters.cs.

-

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

+

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_dream_daemon_settings.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_dream_daemon_settings.html index 2ada27ca7d..a3ac460543 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_dream_daemon_settings.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_dream_daemon_settings.html @@ -175,7 +175,7 @@ Additional Inherited Members

Definition at line 14 of file DreamDaemonSettings.cs.

-

Referenced by Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl(), and Tgstation.Server.Host.Components.Watchdog.WatchdogFactory.WatchdogFactory().

+

Referenced by Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl(), and Tgstation.Server.Host.Components.Watchdog.WatchdogFactory.WatchdogFactory().

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 8e4c3cbf9c..bcc6b80746 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 @@ -292,7 +292,7 @@ Properties

Definition at line 15 of file Job.cs.

-

Referenced by Tgstation.Server.Host.Core.JobManager.CancelJob(), Tgstation.Server.Host.Core.JobManager.CheckGetJob(), Tgstation.Server.Host.Core.JobManager.JobProgress(), Tgstation.Server.Client.Components.JobsClient.JobsClient(), Tgstation.Server.Host.Core.JobManager.RunJob(), and Tgstation.Server.Host.Core.JobManager.WaitForJobCompletion().

+

Referenced by Tgstation.Server.Host.Core.JobManager.CancelJob(), Tgstation.Server.Host.Core.JobManager.CheckGetJob(), Tgstation.Server.Host.Core.JobManager.JobProgress(), Tgstation.Server.Client.Components.JobsClient.JobsClient(), Tgstation.Server.Host.Core.JobManager.RunJob(), and Tgstation.Server.Host.Core.JobManager.WaitForJobCompletion().

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

Definition at line 37 of file Job.cs.

-

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), and Tgstation.Server.Host.Core.JobManager.RunJob().

+

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), and Tgstation.Server.Host.Core.JobManager.RunJob().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_repository_settings.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_repository_settings.html index 5817ec3206..6b4dcdbf21 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_repository_settings.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_repository_settings.html @@ -166,7 +166,7 @@ Properties

Definition at line 30 of file RepositorySettings.cs.

-

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

+

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

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 95f9519829..c0a9ff4949 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 @@ -146,9 +146,9 @@ Properties

The revision sha

-

Definition at line 14 of file RevisionInformation.cs.

+

Definition at line 15 of file RevisionInformation.cs.

-

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Components.Instance.TimerLoop().

+

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Components.Instance.TimerLoop().

@@ -172,9 +172,9 @@ Properties

The sha of the most recent remote commit

-

Definition at line 20 of file RevisionInformation.cs.

+

Definition at line 22 of file RevisionInformation.cs.

-

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), and Tgstation.Server.Host.Components.Instance.TimerLoop().

+

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory.LaunchNew(), 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_test_merge_base.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_test_merge_base.html index d0417eeaf1..d05d8f0271 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_test_merge_base.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_test_merge_base.html @@ -207,7 +207,7 @@ Properties

Construct a TestMergeBase from a copy

Parameters
- +
copy
copyThe TestMergeBase to copy data from
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 4a03e91cb3..599ef9418e 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_internal_1_1_user.html @@ -240,7 +240,7 @@ Properties

Definition at line 15 of file User.cs.

-

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

+

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

@@ -266,7 +266,7 @@ Properties

Definition at line 50 of file User.cs.

-

Referenced by Tgstation.Server.Host.Controllers.InstanceController.GetId(), and Tgstation.Server.Host.Controllers.InstanceController.List().

+

Referenced by Tgstation.Server.Host.Controllers.InstanceController.GetId(), and Tgstation.Server.Host.Controllers.InstanceController.List().

@@ -292,7 +292,7 @@ Properties

Definition at line 38 of file User.cs.

-

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), Tgstation.Server.Host.Controllers.InstanceController.Create(), and Tgstation.Server.Host.Controllers.RepositoryController.Update().

+

Referenced by Tgstation.Server.Host.Components.Instance.CompileProcess(), Tgstation.Server.Host.Controllers.InstanceController.Create(), and Tgstation.Server.Host.Controllers.RepositoryController.Update().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_irc_connection_string_builder.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_irc_connection_string_builder.html index e68eeed032..8be3c26118 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_irc_connection_string_builder.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_irc_connection_string_builder.html @@ -202,7 +202,7 @@ Properties

Definition at line 55 of file IrcConnectionStringBuilder.cs.

-
56  {
57  if (connectionString == null)
58  throw new ArgumentNullException(nameof(connectionString));
59  var splits = connectionString.Split(';');
60 
61  Address = splits[0];
62 
63  if (splits.Length < 2)
64  return;
65 
66  if (UInt16.TryParse(splits[1], out var port))
67  Port = port;
68 
69  if (splits.Length < 3)
70  return;
71 
72  Nickname = splits[2];
73 
74  if (splits.Length < 4)
75  return;
76 
77  if (Int32.TryParse(splits[3], out var intSsl))
78  UseSsl = Convert.ToBoolean(intSsl);
79 
80  if (splits.Length < 5)
81  return;
82  if (Enum.TryParse<IrcPasswordType>(splits[4], out var passwordType))
83  switch (passwordType)
84  {
85  case IrcPasswordType.NickServ:
86  case IrcPasswordType.Sasl:
87  case IrcPasswordType.Server:
88  PasswordType = passwordType;
89  break;
90  }
91 
92  if (splits.Length < 6)
93  return;
94 
95  var rest = new List<string>(splits);
96  rest.RemoveRange(0, 5);
97  Password = String.Join(";", rest);
98  }
string Address
The IP address or URL of the IRC server
+
56  {
57  if (connectionString == null)
58  throw new ArgumentNullException(nameof(connectionString));
59  var splits = connectionString.Split(';');
60 
61  Address = splits[0];
62 
63  if (splits.Length < 2)
64  return;
65 
66  if (UInt16.TryParse(splits[1], out var port))
67  Port = port;
68 
69  if (splits.Length < 3)
70  return;
71 
72  Nickname = splits[2];
73 
74  if (splits.Length < 4)
75  return;
76 
77  if (Int32.TryParse(splits[3], out var intSsl))
78  UseSsl = Convert.ToBoolean(intSsl);
79 
80  if (splits.Length < 5)
81  return;
82  if (Enum.TryParse<IrcPasswordType>(splits[4], out var passwordType))
83  switch (passwordType)
84  {
85  case IrcPasswordType.NickServ:
86  case IrcPasswordType.Sasl:
87  case IrcPasswordType.Server:
88  PasswordType = passwordType;
89  break;
90  default:
91  break;
92  }
93 
94  if (splits.Length < 6)
95  return;
96 
97  var rest = new List<string>(splits);
98  rest.RemoveRange(0, 5);
99  Password = String.Join(";", rest);
100  }
string Address
The IP address or URL of the IRC server
bool UseSsl
If the connection should be made using SSL
@@ -238,8 +238,8 @@ Properties

Implements Tgstation.Server.Api.Models.Internal.ChatConnectionStringBuilder.

-

Definition at line 101 of file IrcConnectionStringBuilder.cs.

-
102  {
103  var sb = new StringBuilder();
104  sb.Append(Address);
105  sb.Append(';');
106  sb.Append(Port);
107  sb.Append(';');
108  sb.Append(Nickname);
109  sb.Append(';');
110  if(UseSsl.HasValue)
111  sb.Append(Convert.ToInt32(UseSsl.Value));
112  if (PasswordType.HasValue)
113  {
114  sb.Append(';');
115  sb.Append((int)PasswordType);
116  sb.Append(';');
117  sb.Append(Password);
118  }
119  return sb.ToString();
120  }
string Address
The IP address or URL of the IRC server
+

Definition at line 103 of file IrcConnectionStringBuilder.cs.

+
104  {
105  var sb = new StringBuilder();
106  sb.Append(Address);
107  sb.Append(';');
108  sb.Append(Port);
109  sb.Append(';');
110  sb.Append(Nickname);
111  sb.Append(';');
112  if(UseSsl.HasValue)
113  sb.Append(Convert.ToInt32(UseSsl.Value));
114  if (PasswordType.HasValue)
115  {
116  sb.Append(';');
117  sb.Append((int)PasswordType);
118  sb.Append(';');
119  sb.Append(Password);
120  }
121 
122  return sb.ToString();
123  }
string Address
The IP address or URL of the IRC server
bool UseSsl
If the connection should be made using SSL
diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_repository.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_repository.html index e7340ba7a5..f2633cd572 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_repository.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_repository.html @@ -241,7 +241,7 @@ Properties

Definition at line 33 of file Repository.cs.

-

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

+

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

@@ -267,7 +267,7 @@ Properties

Definition at line 28 of file Repository.cs.

-

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

+

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

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

Definition at line 13 of file Repository.cs.

-

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

+

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

@@ -343,7 +343,7 @@ Properties

Definition at line 48 of file Repository.cs.

-

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

+

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

@@ -369,7 +369,7 @@ Properties

Definition at line 23 of file Repository.cs.

-

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

+

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.PopulateApi().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_test_merge_parameters.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_test_merge_parameters.html index 7becd54391..59fae6cca5 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_test_merge_parameters.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_test_merge_parameters.html @@ -154,7 +154,7 @@ Properties

Definition at line 25 of file TestMergeParameters.cs.

-

Referenced by Tgstation.Server.Host.Components.Repository.Repository.AddTestMerge(), Tgstation.Server.Host.Components.Instance.CompileProcess(), and Tgstation.Server.Api.Models.Internal.TestMergeBase.TestMergeBase().

+

Referenced by Tgstation.Server.Host.Components.Repository.Repository.AddTestMerge(), Tgstation.Server.Host.Components.Instance.CompileProcess(), and Tgstation.Server.Api.Models.Internal.TestMergeBase.TestMergeBase().

@@ -180,7 +180,7 @@ Properties

Definition at line 14 of file TestMergeParameters.cs.

-

Referenced by Tgstation.Server.Host.Components.Repository.Repository.AddTestMerge(), and Tgstation.Server.Api.Models.Internal.TestMergeBase.TestMergeBase().

+

Referenced by Tgstation.Server.Host.Components.Repository.Repository.AddTestMerge(), and Tgstation.Server.Api.Models.Internal.TestMergeBase.TestMergeBase().

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

Definition at line 20 of file TestMergeParameters.cs.

-

Referenced by Tgstation.Server.Host.Components.Repository.Repository.AddTestMerge(), Tgstation.Server.Host.Components.Instance.CompileProcess(), and Tgstation.Server.Api.Models.Internal.TestMergeBase.TestMergeBase().

+

Referenced by Tgstation.Server.Host.Components.Repository.Repository.AddTestMerge(), Tgstation.Server.Host.Components.Instance.CompileProcess(), and Tgstation.Server.Api.Models.Internal.TestMergeBase.TestMergeBase().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_token.html b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_token.html index a91d270c67..95669c3be0 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_token.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_models_1_1_token.html @@ -138,7 +138,7 @@ Properties

Definition at line 13 of file Token.cs.

-

Referenced by Tgstation.Server.Client.ServerClientFactory.CreateServerClient(), Tgstation.Server.Client.ServerClient.ServerClient(), and Tgstation.Server.Tests.IntegrationTest.TestStandardOperation().

+

Referenced by Tgstation.Server.Client.ServerClientFactory.CreateServerClient(), Tgstation.Server.Client.ServerClient.ServerClient(), and Tgstation.Server.Tests.IntegrationTest.TestStandardOperation().

diff --git a/class_tgstation_1_1_server_1_1_api_1_1_rights_1_1_rights_helper-members.html b/class_tgstation_1_1_server_1_1_api_1_1_rights_1_1_rights_helper-members.html index 21d7687c5e..b3a4316926 100644 --- a/class_tgstation_1_1_server_1_1_api_1_1_rights_1_1_rights_helper-members.html +++ b/class_tgstation_1_1_server_1_1_api_1_1_rights_1_1_rights_helper-members.html @@ -102,7 +102,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); RightToType(RightsType rightsType)Tgstation.Server.Api.Rights.RightsHelperstatic RoleName(RightsType rightsType, Enum right)Tgstation.Server.Api.Rights.RightsHelperstatic RoleNames< TRight >(TRight right)Tgstation.Server.Api.Rights.RightsHelperstatic - typeMapTgstation.Server.Api.Rights.RightsHelperprivatestatic + TypeMapTgstation.Server.Api.Rights.RightsHelperprivatestatic