From 21a97247c3bc17719c53103a2cf2b34aa4840142 Mon Sep 17 00:00:00 2001 From: tgstation-server Date: Sun, 25 Nov 2018 15:09:01 +0000 Subject: [PATCH] Deploy code docs to GitHub Pages for Appveyor build 1040 Commit: 9c3a4760ff8b515b5deb4825ff65d56d0d2bc990 --- _administration_controller_8cs_source.html | 2 +- _api_controller_8cs_source.html | 4 +- _application_8cs_source.html | 4 +- ...entication_context_factory_8cs_source.html | 2 +- _byond_controller_8cs_source.html | 2 +- _channel_8cs_source.html | 2 +- _chat_8cs_source.html | 6 +- _chat_command_8cs_source.html | 2 +- _chat_controller_8cs_source.html | 8 +- _claims_injector_8cs_source.html | 2 +- _comm_command_8cs_source.html | 2 +- _comm_context_8cs_source.html | 2 +- ...ents_2_watchdog_2_watchdog_8cs_source.html | 28 ++-- _configuration_controller_8cs_source.html | 2 +- _database_context_8cs_source.html | 14 +- _database_context_factory_8cs_source.html | 2 +- _database_seeder_8cs_source.html | 4 +- _discord_provider_8cs_source.html | 2 +- _dream_daemon_controller_8cs_source.html | 6 +- _dream_maker_controller_8cs_source.html | 8 +- _home_controller_8cs_source.html | 6 +- _i_database_context_8cs_source.html | 2 +- _i_database_seeder_8cs_source.html | 2 +- _i_instance_8cs_source.html | 2 +- _i_job_manager_8cs_source.html | 2 +- _instance_controller_8cs_source.html | 12 +- _instance_manager_8cs_source.html | 23 +-- _instance_user_controller_8cs_source.html | 8 +- _irc_provider_8cs_source.html | 4 +- _job_controller_8cs_source.html | 6 +- _job_manager_8cs_source.html | 2 +- _model_controller_8cs_source.html | 2 +- _my_sql_database_context_8cs_source.html | 4 +- _repository_controller_8cs_source.html | 8 +- _session_controller_8cs_source.html | 23 +-- _sql_server_database_context_8cs_source.html | 4 +- ...t_database_context_factory_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 +- ...st_2_components_2_instance_8cs_source.html | 18 +-- ...n_8_server_8_host_8_assembly_info_8cs.html | 4 +- ...ver_8_host_8_assembly_info_8cs_source.html | 6 +- ..._8_host_8_console_8_assembly_info_8cs.html | 4 +- ..._8_console_8_assembly_info_8cs_source.html | 6 +- ...8_console_8_tests_8_assembly_info_8cs.html | 4 +- ...le_8_tests_8_assembly_info_8cs_source.html | 6 +- ..._host_8_console_8_tests_8_program_8cs.html | 4 +- ..._console_8_tests_8_program_8cs_source.html | 4 +- ...rver_8_host_8_razor_assembly_info_8cs.html | 4 +- ...host_8_razor_assembly_info_8cs_source.html | 4 +- ...er_8_host_8_tests_8_assembly_info_8cs.html | 4 +- ...st_8_tests_8_assembly_info_8cs_source.html | 6 +- ...8_server_8_host_8_tests_8_program_8cs.html | 4 +- ...r_8_host_8_tests_8_program_8cs_source.html | 4 +- ...8_host_8_watchdog_8_assembly_info_8cs.html | 4 +- ...8_watchdog_8_assembly_info_8cs_source.html | 6 +- ..._watchdog_8_tests_8_assembly_info_8cs.html | 4 +- ...og_8_tests_8_assembly_info_8cs_source.html | 6 +- ...host_8_watchdog_8_tests_8_program_8cs.html | 4 +- ...watchdog_8_tests_8_program_8cs_source.html | 4 +- ..._8_server_8_tests_8_assembly_info_8cs.html | 4 +- ...er_8_tests_8_assembly_info_8cs_source.html | 6 +- ...tation_8_server_8_tests_8_program_8cs.html | 4 +- ...8_server_8_tests_8_program_8cs_source.html | 4 +- _user_controller_8cs_source.html | 6 +- class_auto_generated_program.html | 12 +- ...al_1_1_dream_daemon_launch_parameters.html | 2 +- ...mponents_1_1_chat_1_1_channel-members.html | 4 +- ...t_1_1_components_1_1_chat_1_1_channel.html | 24 ++-- ...host_1_1_components_1_1_chat_1_1_chat.html | 8 +- ...1_chat_1_1_commands_1_1_byond_command.html | 2 +- ..._chat_1_1_commands_1_1_custom_command.html | 2 +- ...1_1_chat_1_1_commands_1_1_kek_command.html | 2 +- ..._1_commands_1_1_pull_requests_command.html | 2 +- ...hat_1_1_commands_1_1_revision_command.html | 2 +- ...chat_1_1_commands_1_1_version_command.html | 2 +- ...at_1_1_providers_1_1_discord_provider.html | 4 +- ...1_chat_1_1_providers_1_1_irc_provider.html | 4 +- ..._1_1_host_1_1_components_1_1_instance.html | 20 +-- ...mponents_1_1_instance_manager-members.html | 6 +- ...t_1_1_components_1_1_instance_manager.html | 121 +++++++++++++--- ...st_1_1_components_1_1_instance_manager.png | Bin 1978 -> 2385 bytes ..._1_1_interop_1_1_chat_command-members.html | 2 +- ...mponents_1_1_interop_1_1_chat_command.html | 8 +- ..._1_1_interop_1_1_comm_command-members.html | 2 +- ...mponents_1_1_interop_1_1_comm_command.html | 14 +- ...mponents_1_1_interop_1_1_comm_context.html | 2 +- ..._components_1_1_interop_1_1_constants.html | 30 ++-- ...onents_1_1_watchdog_1_1_monitor_state.html | 2 +- ...1_1_watchdog_1_1_reattach_information.html | 2 +- ...s_1_1_watchdog_1_1_session_controller.html | 55 ++++---- ..._components_1_1_watchdog_1_1_watchdog.html | 72 +++++----- ...dog_1_1_watchdog_reattach_information.html | 4 +- ...figuration_1_1_database_configuration.html | 4 +- ...st_1_1_controllers_1_1_api_controller.html | 2 +- ...t_1_1_controllers_1_1_chat_controller.html | 32 ++--- ...ntrollers_1_1_dream_daemon_controller.html | 12 +- ...ontrollers_1_1_dream_maker_controller.html | 24 ++-- ...t_1_1_controllers_1_1_home_controller.html | 6 +- ...1_controllers_1_1_instance_controller.html | 36 ++--- ...trollers_1_1_instance_user_controller.html | 30 ++-- ...st_1_1_controllers_1_1_job_controller.html | 24 ++-- ...controllers_1_1_repository_controller.html | 28 ++-- ...t_1_1_controllers_1_1_user_controller.html | 24 ++-- ...ver_1_1_host_1_1_core_1_1_application.html | 4 +- ...1_1_core_1_1_database_context_factory.html | 4 +- ...t_1_1_core_1_1_semaphore_slim_context.html | 2 +- ...sts_1_1_test_database_context_factory.html | 4 +- ...1_models_1_1_database_context-members.html | 4 +- ..._host_1_1_models_1_1_database_context.html | 131 +++++++++++++----- ...1_host_1_1_models_1_1_database_seeder.html | 4 +- ...s_1_1_my_sql_database_context-members.html | 4 +- ..._1_models_1_1_my_sql_database_context.html | 36 ++++- ..._models_1_1_reattach_information_base.html | 2 +- ...1_sql_server_database_context-members.html | 6 +- ...odels_1_1_sql_server_database_context.html | 34 ++++- ..._1_watchdog_reattach_information_base.html | 2 +- ...ty_1_1_authentication_context_factory.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_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_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 | 44 +++--- functions_d.html | 3 + functions_func_h.html | 1 + functions_func_i.html | 2 +- functions_func_s.html | 4 + functions_func_u.html | 7 +- functions_h.html | 1 + functions_i.html | 12 +- functions_p.html | 6 +- functions_prop_i.html | 8 +- functions_prop_p.html | 6 +- functions_r.html | 4 +- functions_s.html | 4 + functions_u.html | 5 + functions_vars_d.html | 3 + hierarchy.html | 3 +- ...s_1_1_chat_1_1_commands_1_1_i_command.html | 4 +- ...1_1_watchdog_1_1_i_session_controller.html | 10 +- ..._1_1_host_1_1_core_1_1_i_process_base.html | 4 +- ...1_host_1_1_core_1_1_i_restart_handler.html | 5 +- ..._1_host_1_1_core_1_1_i_restart_handler.png | Bin 1590 -> 1982 bytes ..._1_host_1_1_core_1_1_i_server_control.html | 2 +- ...models_1_1_i_database_context-members.html | 5 +- ...ost_1_1_models_1_1_i_database_context.html | 69 +++++++-- search/all_10.js | 9 +- search/all_12.js | 4 +- search/all_13.js | 5 +- search/all_14.js | 2 +- search/all_15.js | 3 +- search/all_4.js | 17 +-- search/all_8.js | 2 +- search/all_9.js | 21 ++- search/all_c.js | 2 +- search/all_e.js | 2 +- search/functions_11.js | 1 + search/functions_13.js | 1 + search/functions_7.js | 2 +- search/functions_8.js | 2 +- search/properties_8.js | 5 +- search/properties_e.js | 3 +- search/variables_11.js | 2 +- search/variables_3.js | 1 + ...properties_2_assembly_info_8cs_source.html | 2 +- 204 files changed, 1021 insertions(+), 710 deletions(-) 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_7ae0efc0613c36c848fa310214e0aee3.html => dir_2e67631cb23401d696bcfdbe81f86f0f.html (92%) rename dir_4122b363a9d577bed19bef337e757ff3.html => dir_48968f849f7ddb061f7130e079fe66a6.html (92%) rename dir_d45f494620bc0048ee4a31fa4f7391e1.html => dir_4c0c7e2f4fc60f51e2347a2d8dda94ce.html (94%) rename dir_0e792d76844bf2d7c79795a56a50ebec.html => dir_87039fa7b841d848ca75981d64237477.html (92%) rename dir_6db5af7fcfa17a34c7abf23e195af4db.html => dir_9257d56e926145e494a5aa5279887044.html (94%) rename dir_13bee0ed20ecc83b9bcd4b869b726d42.html => dir_a3714ab037275c284988fc90b4d045a5.html (95%) rename dir_fcc41a8faa41492aba200a49fc7d0aed.html => dir_a8807eef6592489c6a360d587f5b1a66.html (93%) rename dir_900257602d294c4a4799e88420919068.html => dir_b551a02288ef39e099594710382363a0.html (95%) rename dir_e928c7e494884ad322b5f6005c2954e1.html => dir_b87cbb2c60cf682b61fbfcceb9bced43.html (94%) rename dir_40138fd4849573e743490d583f6d06d6.html => dir_b92573f47b933d7a3661c26d1af2098b.html (93%) rename dir_18a5e30c14613227f95d12bd9fddc614.html => dir_be9f3ed3485a880e3213506a311823d1.html (92%) rename dir_298f2a364723f4f4cea115538f944a94.html => dir_c5f98c05147bcbba78e71856485f5049.html (95%) rename dir_83a6f600c846cd444f33e44bb9b4f9cb.html => dir_d8a279f7113e0073f6cd65dd050c1475.html (94%) rename dir_c65d377d3c73fb4a07b3637f9c0caf4d.html => dir_e97d380c3eebaf0856a4ec180d749620.html (93%) rename dir_6c8d8f20501e8e412458f542d0998195.html => dir_ea36b5ab01ad6e8b607438a02ae91964.html (93%) rename dir_383ca23c40357ede4cca827b4a365c76.html => dir_f88948f9bde0555374e8a59a663bdfc9.html (94%) rename dir_7a9d035dcfcd6d9c7aa1efef4b6b58a4.html => dir_fab1362e86740a2c573ef875dd474a67.html (92%) diff --git a/_administration_controller_8cs_source.html b/_administration_controller_8cs_source.html index 8716d9a377..723546d96e 100644 --- a/_administration_controller_8cs_source.html +++ b/_administration_controller_8cs_source.html @@ -92,7 +92,7 @@ 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.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 }
override async Task< IActionResult > Read(CancellationToken cancellationToken)
Attempt to read a TModel
diff --git a/_api_controller_8cs_source.html b/_api_controller_8cs_source.html index 960f8373f9..60d0697d52 100644 --- a/_api_controller_8cs_source.html +++ b/_api_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.AspNetCore.Mvc.Filters;
4 using Microsoft.Extensions.Logging;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Controllers
16 {
20  [Produces(ApiHeaders.ApplicationJson)]
21  [Consumes(ApiHeaders.ApplicationJson)]
22  public abstract class ApiController : Controller
23  {
27  protected ApiHeaders ApiHeaders { get; private set; }
28 
32  protected IDatabaseContext DatabaseContext { get; }
33 
38 
42  protected ILogger Logger { get; }
43 
47  protected Models.Instance Instance { get; }
48 
52  readonly bool requireInstance;
53 
61  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
62  {
63  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
64  if (authenticationContextFactory == null)
65  throw new ArgumentNullException(nameof(authenticationContextFactory));
66  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
69  this.requireInstance = requireInstance;
70  }
71 
73  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
74  {
75  //ALL valid token and login requests that match a route go through this function
76  //404 is returned before
77 
78  if (AuthenticationContext != null && AuthenticationContext.User == null)
79  {
80  //valid token, expired password
81  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
82  return;
83  }
84 
85  //validate the headers
86  try
87  {
88  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
89 
90  if (!ApiHeaders.Compatible())
91  {
92  await StatusCode((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
93  {
94  Message = "Provided API version is incompatible with server version!"
95  }).ExecuteResultAsync(context).ConfigureAwait(false);
96  return;
97  }
98 
99  if (requireInstance)
100  {
101  if (!ApiHeaders.InstanceId.HasValue)
102  {
103  await BadRequest(new ErrorMessage { Message = "Missing Instance header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
104  return;
105  }
107  {
108  //accessing an instance they don't have access to or one that's disabled
109  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
110  return;
111  }
112  }
113  }
114  catch (InvalidOperationException e)
115  {
116  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
117  return;
118  }
119 
120  if (ModelState?.IsValid == false)
121  {
122  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
123  //HACK
124  //do some fuckery to remove RequiredAttribute errors
125  for (var I = 0; I < errorMessages.Count; ++I)
126  {
127  var message = errorMessages[I];
128  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
129  {
130  errorMessages.RemoveAt(I);
131  --I;
132  }
133  }
134  if (errorMessages.Count > 0)
135  {
136  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
137  return;
138  }
139  }
140 
141  Logger.LogDebug("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path, Request.QueryString, ApiHeaders.InstanceId);
142 
143  try
144  {
145  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
146  }
147  catch (OperationCanceledException e)
148  {
149  Logger.LogDebug("Request cancelled! Exception: {0}", e);
150  throw;
151  }
152  }
153  }
154 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Http;
2 using Microsoft.AspNetCore.Mvc;
3 using Microsoft.AspNetCore.Mvc.Filters;
4 using Microsoft.Extensions.Logging;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Net;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
14 
15 namespace Tgstation.Server.Host.Controllers
16 {
20  [Produces(ApiHeaders.ApplicationJson)]
21  [Consumes(ApiHeaders.ApplicationJson)]
22  public abstract class ApiController : Controller
23  {
27  protected ApiHeaders ApiHeaders { get; private set; }
28 
32  protected IDatabaseContext DatabaseContext { get; }
33 
38 
42  protected ILogger Logger { get; }
43 
47  protected Models.Instance Instance { get; }
48 
52  readonly bool requireInstance;
53 
61  public ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
62  {
63  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
64  if (authenticationContextFactory == null)
65  throw new ArgumentNullException(nameof(authenticationContextFactory));
66  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
69  this.requireInstance = requireInstance;
70  }
71 
73  public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
74  {
75  //ALL valid token and login requests that match a route go through this function
76  //404 is returned before
77 
78  if (AuthenticationContext != null && AuthenticationContext.User == null)
79  {
80  //valid token, expired password
81  await Unauthorized().ExecuteResultAsync(context).ConfigureAwait(false);
82  return;
83  }
84 
85  //validate the headers
86  try
87  {
88  ApiHeaders = new ApiHeaders(Request.GetTypedHeaders());
89 
90  if (!ApiHeaders.Compatible())
91  {
92  await StatusCode((int)HttpStatusCode.UpgradeRequired, new ErrorMessage
93  {
94  Message = "Provided API version is incompatible with server version!"
95  }).ExecuteResultAsync(context).ConfigureAwait(false);
96  return;
97  }
98 
99  if (requireInstance)
100  {
101  if (!ApiHeaders.InstanceId.HasValue)
102  {
103  await BadRequest(new ErrorMessage { Message = "Missing Instance header!" }).ExecuteResultAsync(context).ConfigureAwait(false);
104  return;
105  }
107  {
108  //accessing an instance they don't have access to or one that's disabled
109  await Forbid().ExecuteResultAsync(context).ConfigureAwait(false);
110  return;
111  }
112  }
113  }
114  catch (InvalidOperationException e)
115  {
116  await BadRequest(new ErrorMessage { Message = e.Message }).ExecuteResultAsync(context).ConfigureAwait(false);
117  return;
118  }
119 
120  if (ModelState?.IsValid == false)
121  {
122  var errorMessages = ModelState.SelectMany(x => x.Value.Errors).Select(x => x.ErrorMessage).ToList();
123  //HACK
124  //do some fuckery to remove RequiredAttribute errors
125  for (var I = 0; I < errorMessages.Count; ++I)
126  {
127  var message = errorMessages[I];
128  if (message.StartsWith("The ", StringComparison.Ordinal) && message.EndsWith(" field is required.", StringComparison.Ordinal))
129  {
130  errorMessages.RemoveAt(I);
131  --I;
132  }
133  }
134  if (errorMessages.Count > 0)
135  {
136  await BadRequest(new ErrorMessage { Message = String.Join(Environment.NewLine, errorMessages) }).ExecuteResultAsync(context).ConfigureAwait(false);
137  return;
138  }
139  }
140 
141  Logger.LogDebug("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}{5} to Instance {6}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path, Request.QueryString, ApiHeaders.InstanceId);
142 
143  try
144  {
145  await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
146  }
147  catch (OperationCanceledException e)
148  {
149  Logger.LogDebug("Request cancelled! Exception: {0}", e);
150  throw;
151  }
152  }
153  }
154 }
ApiController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
Construct an ApiController
@@ -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
- +
long Id
The ID of the User
Definition: User.cs:15
diff --git a/_application_8cs_source.html b/_application_8cs_source.html index a3c95a5ce6..92bcb81805 100644 --- a/_application_8cs_source.html +++ b/_application_8cs_source.html @@ -93,7 +93,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Cyberboss.AspNetCore.AsyncInitializer;
3 using Microsoft.AspNetCore.Authentication.JwtBearer;
4 using Microsoft.AspNetCore.Builder;
5 using Microsoft.AspNetCore.Hosting;
6 using Microsoft.AspNetCore.Identity;
7 using Microsoft.Extensions.Configuration;
8 using Microsoft.Extensions.DependencyInjection;
9 using Microsoft.Extensions.Hosting;
10 using Microsoft.Extensions.Logging;
11 using Microsoft.Extensions.Options;
12 using Microsoft.Extensions.Primitives;
13 using Newtonsoft.Json;
14 using Newtonsoft.Json.Converters;
15 using Serilog;
16 using Serilog.Events;
17 using Serilog.Formatting.Display;
18 using System;
19 using System.Globalization;
20 using System.IdentityModel.Tokens.Jwt;
21 using System.Reflection;
22 using System.Runtime.InteropServices;
23 using System.Threading.Tasks;
30 using Tgstation.Server.Host.IO;
33 
34 namespace Tgstation.Server.Host.Core
35 {
37  sealed class Application : IApplication
38  {
40  public string VersionPrefix => "tgstation-server";
41 
43  public Version Version { get; }
44 
46  public string VersionString { get; }
47 
51  readonly IConfiguration configuration;
52 
56  readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment;
57 
61  readonly TaskCompletionSource<object> startupTcs;
62 
67 
73  public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
74  {
75  this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
76  this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
77 
78  startupTcs = new TaskCompletionSource<object>();
79 
80  Version = Assembly.GetExecutingAssembly().GetName().Version;
81  VersionString = String.Format(CultureInfo.InvariantCulture, "{0} v{1}", VersionPrefix, Version);
82  }
83 
88  public void ConfigureServices(IServiceCollection services)
89  {
90  if (services == null)
91  throw new ArgumentNullException(nameof(services));
92 
93  //needful
94  services.AddSingleton<IApplication>(this);
95 
96  //configure configuration
97  services.Configure<UpdatesConfiguration>(configuration.GetSection(UpdatesConfiguration.Section));
98  services.Configure<DatabaseConfiguration>(configuration.GetSection(DatabaseConfiguration.Section));
99  services.Configure<GeneralConfiguration>(configuration.GetSection(GeneralConfiguration.Section));
100  services.Configure<FileLoggingConfiguration>(configuration.GetSection(FileLoggingConfiguration.Section));
101 
102  //enable options which give us config reloading
103  services.AddOptions();
104 
105  //other stuff needed for for setup wizard and configuration
106  services.AddSingleton<IIOManager, DefaultIOManager>();
107  services.AddSingleton<IConsole, IO.Console>();
108  services.AddSingleton<IDBConnectionFactory, DBConnectionFactory>();
109  services.AddSingleton<ISetupWizard, SetupWizard>();
110  services.AddSingleton<IPlatformIdentifier, PlatformIdentifier>();
111  services.AddSingleton<IAsyncDelayer, AsyncDelayer>();
112 
113  GeneralConfiguration generalConfiguration;
114  DatabaseConfiguration databaseConfiguration;
115  FileLoggingConfiguration fileLoggingConfiguration;
116  IIOManager ioManager;
117  IPlatformIdentifier platformIdentifier;
118 
119  //temporarily build the service provider in it's current state
120  //do it here so we can run the setup wizard if necessary
121  //also allows us to get some options and other services we need for continued configuration
122  using (var provider = services.BuildServiceProvider())
123  {
124  //run the wizard if necessary
125  var setupWizard = provider.GetRequiredService<ISetupWizard>();
126  var applicationLifetime = provider.GetRequiredService<Microsoft.AspNetCore.Hosting.IApplicationLifetime>();
127  var setupWizardRan = setupWizard.CheckRunWizard(applicationLifetime.ApplicationStopping).GetAwaiter().GetResult();
128 
129  //load the configuration options we need
130  var generalOptions = provider.GetRequiredService<IOptions<GeneralConfiguration>>();
131  generalConfiguration = generalOptions.Value;
132 
133  //unless this is set, in which case, we leave
134  if (setupWizardRan && generalConfiguration.SetupWizardMode == SetupWizardMode.Only)
135  //we don't inject a logger in the constuctor to log this because it's not yet configured
136  throw new OperationCanceledException("Exiting due to SetupWizardMode configuration!");
137 
138  var dbOptions = provider.GetRequiredService<IOptions<DatabaseConfiguration>>();
139  databaseConfiguration = dbOptions.Value;
140 
141  var loggingOptions = provider.GetRequiredService<IOptions<FileLoggingConfiguration>>();
142  fileLoggingConfiguration = loggingOptions.Value;
143 
144  ioManager = provider.GetRequiredService<IIOManager>();
145  platformIdentifier = provider.GetRequiredService<IPlatformIdentifier>();
146  }
147 
148  //setup file logging via serilog
149  if (!fileLoggingConfiguration.Disable)
150  services.AddLogging(builder =>
151  {
152  //common app data is C:/ProgramData on windows, else /usr/shar
153  var logPath = !String.IsNullOrEmpty(fileLoggingConfiguration.Directory) ? fileLoggingConfiguration.Directory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs");
154 
155  logPath = ioManager.ConcatPath(logPath, "tgs-{Date}.log");
156 
157  LogEventLevel? ConvertLogLevel(LogLevel logLevel)
158  {
159  switch (logLevel)
160  {
161  case LogLevel.Critical:
162  return LogEventLevel.Fatal;
163  case LogLevel.Debug:
164  return LogEventLevel.Debug;
165  case LogLevel.Error:
166  return LogEventLevel.Error;
167  case LogLevel.Information:
168  return LogEventLevel.Information;
169  case LogLevel.Trace:
170  return LogEventLevel.Verbose;
171  case LogLevel.Warning:
172  return LogEventLevel.Warning;
173  case LogLevel.None:
174  return null;
175  default:
176  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid log level {0}", logLevel));
177  }
178  };
179 
180  var logEventLevel = ConvertLogLevel(fileLoggingConfiguration.LogLevel);
181  var microsoftEventLevel = ConvertLogLevel(fileLoggingConfiguration.MicrosoftLogLevel);
182 
183  var formatter = new MessageTemplateTextFormatter("{Timestamp:o} {RequestId,13} [{Level:u3}] {SourceContext:l}: {Message} ({EventId:x8}){NewLine}{Exception}", null);
184 
185  var configuration = new LoggerConfiguration()
186  .Enrich.FromLogContext()
187  .WriteTo.Async(w => w.RollingFile(formatter, logPath, shared: true, flushToDiskInterval: TimeSpan.FromSeconds(2)));
188 
189  if (logEventLevel.HasValue)
190  configuration.MinimumLevel.Is(logEventLevel.Value);
191 
192  if (microsoftEventLevel.HasValue)
193  configuration.MinimumLevel.Override("Microsoft", microsoftEventLevel.Value);
194 
195  builder.AddSerilog(configuration.CreateLogger(), true);
196  });
197 
198  //configure bearer token validation
199  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(jwtBearerOptions =>
200  {
201  //this line isn't actually run until the first request is made
202  //at that point tokenFactory will be populated
203  jwtBearerOptions.TokenValidationParameters = tokenFactory.ValidationParameters;
204  jwtBearerOptions.Events = new JwtBearerEvents
205  {
206  //Application is our composition root so this monstrosity of a line is okay
207  OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService<IClaimsInjector>().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted)
208  };
209  });
210  //fucking converts 'sub' to M$ bs
211  //can't be done in the above lambda, that's too late
212  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
213 
214  //add mvc, configure the json serializer settings
215  services.AddMvc().AddJsonOptions(options =>
216  {
217  options.AllowInputFormatterExceptionMessages = true;
218  options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
219  options.SerializerSettings.CheckAdditionalContent = true;
220  options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error;
221  options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
222  options.SerializerSettings.Converters = new[] { new VersionConverter() };
223  });
224 
225  void AddTypedContext<TContext>() where TContext : DatabaseContext<TContext>
226  {
227  services.AddDbContext<TContext>(builder =>
228  {
229  if (hostingEnvironment.IsDevelopment())
230  builder.EnableSensitiveDataLogging();
231  });
232  services.AddScoped<IDatabaseContext>(x => x.GetRequiredService<TContext>());
233  }
234 
235  //add the correct database context type
236  var dbType = databaseConfiguration.DatabaseType;
237  switch (dbType)
238  {
239  case DatabaseType.MySql:
240  case DatabaseType.MariaDB:
241  AddTypedContext<MySqlDatabaseContext>();
242  break;
243  case DatabaseType.SqlServer:
244  AddTypedContext<SqlServerDatabaseContext>();
245  break;
246  default:
247  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType));
248  }
249 
250  //configure other database services
251  services.AddSingleton<IDatabaseContextFactory, DatabaseContextFactory>();
252  services.AddSingleton<IDatabaseSeeder, DatabaseSeeder>();
253 
254  //configure security services
256  services.AddScoped<IClaimsInjector, ClaimsInjector>();
257  services.AddSingleton<IIdentityCache, IdentityCache>();
258  services.AddSingleton<ICryptographySuite, CryptographySuite>();
259  services.AddSingleton<ITokenFactory, TokenFactory>();
260  services.AddSingleton<IPasswordHasher<Models.User>, PasswordHasher<Models.User>>();
261 
262  //configure platform specific services
263  if (platformIdentifier.IsWindows)
264  {
265  services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
266  services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
267  services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
268  services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
269 
270  services.AddSingleton<WindowsNetworkPromptReaper>();
271  services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
272  services.AddSingleton<IHostedService>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
273  }
274  else
275  {
276  services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
277  services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
278  services.AddSingleton<IByondInstaller, PosixByondInstaller>();
279  services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
280  services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
281  }
282 
283  //configure misc services
284  services.AddSingleton<ISynchronousIOManager, SynchronousIOManager>();
285  services.AddSingleton<IGitHubClientFactory, GitHubClientFactory>();
286  services.AddSingleton<IProcessExecutor, ProcessExecutor>();
287  services.AddSingleton<IByondTopicSender>(new ByondTopicSender
288  {
289  ReceiveTimeout = generalConfiguration.ByondTopicTimeout,
290  SendTimeout = generalConfiguration.ByondTopicTimeout
291  });
292 
293  //configure component services
294  services.AddSingleton<ICredentialsProvider, CredentialsProvider>();
295  services.AddSingleton<IProviderFactory, ProviderFactory>();
296  services.AddSingleton<IChatFactory, ChatFactory>();
297  services.AddSingleton<IWatchdogFactory, WatchdogFactory>();
298  services.AddSingleton<IInstanceFactory, InstanceFactory>();
299 
300  //configure root services
301  services.AddSingleton<InstanceManager>();
302  services.AddSingleton<IInstanceManager>(x => x.GetRequiredService<InstanceManager>());
303  services.AddSingleton<IHostedService>(x => x.GetRequiredService<InstanceManager>());
304 
305  services.AddSingleton<IJobManager, JobManager>();
306  }
307 
315  public void Configure(IApplicationBuilder applicationBuilder, IServerControl serverControl, ITokenFactory tokenFactory, ILogger<Application> logger)
316  {
317  if (applicationBuilder == null)
318  throw new ArgumentNullException(nameof(applicationBuilder));
319  if (serverControl == null)
320  throw new ArgumentNullException(nameof(serverControl));
321 
322  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
323 
324  if (logger == null)
325  throw new ArgumentNullException(nameof(logger));
326 
327  logger.LogInformation(VersionString);
328 
329  //attempt to restart the server if the configuration changes
330  if(serverControl.WatchdogPresent)
331  ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart());
332 
333  //now setup the HTTP request pipeline
334 
335  //should anything after this throw an exception, catch it and display a detailed html page
336  applicationBuilder.UseDeveloperExceptionPage(); //it is not worth it to limit this, you should only ever get it if you're an authorized user
337 
338  //suppress OperationCancelledExceptions, they are just aborted HTTP requests
339  applicationBuilder.UseCancelledRequestSuppression();
340 
341  //Do not service requests until Ready is called, this will return 503 until that point
342  applicationBuilder.UseAsyncInitialization(async cancellationToken =>
343  {
344  using (cancellationToken.Register(() => startupTcs.SetCanceled()))
345  await startupTcs.Task.ConfigureAwait(false);
346  });
347 
348  //authenticate JWT tokens using our security pipeline if present, returns 401 if bad
349  applicationBuilder.UseAuthentication();
350 
351  //suppress and log database exceptions
352  applicationBuilder.UseDbConflictHandling();
353 
354  //majority of handling is done in the controllers
355  applicationBuilder.UseMvc();
356 
357  //404 anything that gets this far
358  }
359 
361  public void Ready(Exception initializationError)
362  {
363  lock (startupTcs)
364  {
365  if (startupTcs.Task.IsCompleted)
366  throw new InvalidOperationException("Ready has already been called!");
367  if (initializationError == null)
368  startupTcs.SetResult(null);
369  else
370  startupTcs.SetException(initializationError);
371  }
372  }
373  }
374 }
LogLevel MicrosoftLogLevel
The minimum Microsoft.Extensions.Logging.LogLevel to display in logs for Microsoft library sources ...
- +
Manages the runtime of Jobs
Definition: IJobManager.cs:12
@@ -155,7 +155,7 @@ 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
- + diff --git a/_authentication_context_factory_8cs_source.html b/_authentication_context_factory_8cs_source.html index d8485f7437..f0a6ac58fa 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  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 }
AuthenticationContextFactory(ISystemIdentityFactory systemIdentityFactory, IDatabaseContext databaseContext, IIdentityCache identityCache)
Construct an AuthenticationContextFactory
diff --git a/_byond_controller_8cs_source.html b/_byond_controller_8cs_source.html index c2522b73ff..48e407eee5 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  //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 }
const string Byond
The Models.Byond controller
Definition: Routes.cs:34
Manages the runtime of Jobs
Definition: IJobManager.cs:12
diff --git a/_channel_8cs_source.html b/_channel_8cs_source.html index 6ae63395fc..db6449eddc 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 IsAdmin { get; set; }
42 
46  public bool IsPrivate { 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 6af5373869..61da661081 100644 --- a/_chat_8cs_source.html +++ b/_chat_8cs_source.html @@ -92,13 +92,12 @@ 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.IsPrivate)
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.IsAdmin = mapping.Channel.IsAdmin;
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.IsPrivate)
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.IsAdmin)
306  {
307  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
308  return;
309  }
310 
311  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
312  if (result != null)
313  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
314  }
315  catch (OperationCanceledException)
316  {
317  throw;
318  }
319  catch (Exception e)
320  {
321  //error bc custom commands should reply about why it failed
322  logger.LogError("Error processing chat command: {0}", e);
323  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
324  }
325  }
326 
332  async Task MonitorMessages(CancellationToken cancellationToken)
333  {
334  var messageTasks = new Dictionary<IProvider, Task<Message>>();
335  try
336  {
337  while (!cancellationToken.IsCancellationRequested)
338  {
339  //prune disconnected providers
340  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
341  messageTasks.Remove(I.Key);
342 
343  //add new ones
344  Task updatedTask;
345  lock (this)
346  updatedTask = connectionsUpdated.Task;
347  lock (providers)
348  foreach (var I in providers)
349  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
350  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
351 
352  if (messageTasks.Count == 0)
353  {
354  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
355  continue;
356  }
357 
358  //wait for a message
359  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
360 
361  //process completed ones
362  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
363  {
364  var message = await I.Value.ConfigureAwait(false);
365 
366  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
367 
368  messageTasks.Remove(I.Key);
369  }
370  }
371  }
372  catch (OperationCanceledException) { }
373  catch (Exception e)
374  {
375  logger.LogError("Message monitor crashed!: Exception: {0}", e);
376  }
377  }
378 
380  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
381  {
382  if (newChannels == null)
383  throw new ArgumentNullException(nameof(newChannels));
384  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
385  if (provider == null)
386  return;
387  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
388  if (results == null) //aborted
389  return;
390  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
391  {
392  IsWatchdogChannel = x.IsWatchdogChannel == true,
393  IsUpdatesChannel = x.IsUpdatesChannel == true,
394  ProviderChannelId = y.RealId,
395  ProviderId = connectionId,
396  Channel = y
397  });
398 
399  ulong baseId;
400  lock (this)
401  {
402  baseId = channelIdCounter;
403  channelIdCounter += (ulong)results.Count;
404  }
405 
406  Task task;
407  lock (mappedChannels)
408  {
409  lock (providers)
410  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again
411  return;
412  foreach (var I in mappings)
413  {
414  var newId = baseId++;
415  mappedChannels.Add(newId, I);
416  I.Channel.RealId = newId;
417  }
418 
419  lock (trackingContexts)
420  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
421  }
422  await task.ConfigureAwait(false);
423  }
424 
426  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
427  {
428  if (newSettings == null)
429  throw new ArgumentNullException(nameof(newSettings));
430  IProvider provider;
431 
432  async Task DisconnectProvider(IProvider p)
433  {
434  try
435  {
436  await p.Disconnect(cancellationToken).ConfigureAwait(false);
437  }
438  finally
439  {
440  p.Dispose();
441  }
442  }
443 
444  Task disconnectTask;
445  lock (providers)
446  {
447  //raw settings changes forces a rebuild of the provider
448  if (providers.TryGetValue(newSettings.Id, out provider))
449  {
450  providers.Remove(newSettings.Id);
451  disconnectTask = DisconnectProvider(provider);
452  }
453  else
454  disconnectTask = Task.CompletedTask;
455  if (newSettings.Enabled.Value)
456  {
457  provider = providerFactory.CreateProvider(newSettings);
458  providers.Add(newSettings.Id, provider);
459  }
460  }
461 
462  lock (mappedChannels)
463  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
464  mappedChannels.Remove(I);
465 
466  await disconnectTask.ConfigureAwait(false);
467 
468  if (started)
469  {
470  if (newSettings.Enabled.Value)
471  await provider.Connect(cancellationToken).ConfigureAwait(false);
472  lock (this)
473  {
474  //same thread shennanigans
475  var oldOne = connectionsUpdated;
476  connectionsUpdated = new TaskCompletionSource<object>();
477  oldOne.SetResult(null);
478  }
479  }
480  }
481 
483  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
484  {
485  if (message == null)
486  throw new ArgumentNullException(nameof(message));
487  if (channelIds == null)
488  throw new ArgumentNullException(nameof(channelIds));
489 
490  return Task.WhenAll(channelIds.Select(x =>
491  {
492  ChannelMapping channelMapping;
493  lock (mappedChannels)
494  if (!mappedChannels.TryGetValue(x, out channelMapping))
495  return Task.CompletedTask;
496  IProvider provider;
497  lock (providers)
498  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
499  return Task.CompletedTask;
500  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
501  }));
502  }
503 
505  public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
506  {
507  List<ulong> wdChannels;
508  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
509  lock (mappedChannels) //so it doesn't change while we're using it
510  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
511  return SendMessage(message, wdChannels, cancellationToken);
512  }
513 
515  public Task SendUpdateMessage(string message, CancellationToken cancellationToken)
516  {
517  List<ulong> wdChannels;
518  message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message);
519  lock (mappedChannels) //so it doesn't change while we're using it
520  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
521  return SendMessage(message, wdChannels, cancellationToken);
522  }
523 
525  public async Task StartAsync(CancellationToken cancellationToken)
526  {
527  foreach (var I in commandFactory.GenerateCommands())
528  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
529  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
530  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
531  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
532  chatHandler = MonitorMessages(handlerCts.Token);
533  started = true;
534  }
535 
537  public async Task StopAsync(CancellationToken cancellationToken)
538  {
539  handlerCts.Cancel();
540  await chatHandler.ConfigureAwait(false);
541  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
542  }
543 
545  public async Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
546  {
547  if (customCommandHandler == null)
548  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
549  JsonTrackingContext context = null;
550  context = new JsonTrackingContext(ioManager, customCommandHandler, loggerFactory.CreateLogger<JsonTrackingContext>(), () =>
551  {
552  lock (trackingContexts)
553  trackingContexts.Remove(context);
554  }, ioManager.ConcatPath(basePath, commandsJsonName), ioManager.ConcatPath(basePath, channelsJsonName));
555  Task task;
556  lock (trackingContexts)
557  {
558  trackingContexts.Add(context);
559  lock (mappedChannels)
560  task = context.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken);
561  }
562  await task.ConfigureAwait(false);
563  return context;
564  }
565 
567  public bool Connected(long connectionId)
568  {
569  lock (providers)
570  return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
571  }
572 
574  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
575  {
576  if (this.customCommandHandler != null)
577  throw new InvalidOperationException("RegisterCommandHandler() already called!");
578  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
579  }
580 
582  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
583  {
584  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
585  if (provider != null)
586  try
587  {
588  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
589  }
590  finally
591  {
592  provider.Dispose();
593  }
594  }
595 
597  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
598  {
599  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
600  List<ulong> wdChannels;
601  lock (mappedChannels) //so it doesn't change while we're using it
602  wdChannels = mappedChannels.Select(x => x.Key).ToList();
603  return SendMessage(message, wdChannels, cancellationToken);
604  }
605  }
606 }
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: Chat.cs:574
+Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using Newtonsoft.Json;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
13 using Tgstation.Server.Host.IO;
14 
15 namespace Tgstation.Server.Host.Components.Chat
16 {
18  sealed class Chat : IChat, IRestartHandler
19  {
20  const string CommonMention = "!tgs";
21 
26 
31 
36 
41 
46 
50  readonly ILoggerFactory loggerFactory;
51 
55  readonly ILogger<Chat> logger;
56 
60  readonly Dictionary<string, ICommand> builtinCommands;
61 
65  readonly Dictionary<long, IProvider> providers;
66 
70  readonly Dictionary<ulong, ChannelMapping> mappedChannels;
71 
75  readonly List<IJsonTrackingContext> trackingContexts;
76 
80  readonly CancellationTokenSource handlerCts;
81 
85  readonly List<Models.ChatBot> initialChatBots;
86 
91 
96 
100  TaskCompletionSource<object> connectionsUpdated;
101 
106 
110  bool started;
111 
123  public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILoggerFactory loggerFactory, ILogger<Chat> logger, IEnumerable<Models.ChatBot> initialChatBots)
124  {
125  this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
126  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
127  this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory));
128  if (serverControl == null)
129  throw new ArgumentNullException(nameof(serverControl));
130  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
131  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
132  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
133  this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots));
134 
135  restartRegistration = serverControl.RegisterForRestart(this);
136 
137  builtinCommands = new Dictionary<string, ICommand>();
138  providers = new Dictionary<long, IProvider>();
139  mappedChannels = new Dictionary<ulong, ChannelMapping>();
140  trackingContexts = new List<IJsonTrackingContext>();
141  handlerCts = new CancellationTokenSource();
142  connectionsUpdated = new TaskCompletionSource<object>();
143  channelIdCounter = 1;
144  }
145 
147  public void Dispose()
148  {
149  restartRegistration.Dispose();
150  handlerCts.Dispose();
151  foreach (var I in providers)
152  I.Value.Dispose();
153  }
154 
162  async Task<IProvider> RemoveProvider(long connectionId, bool updateTrackings, CancellationToken cancellationToken)
163  {
164  IProvider provider;
165  lock (providers)
166  if (!providers.TryGetValue(connectionId, out provider))
167  return null;
168  Task task;
169  lock (mappedChannels)
170  {
171  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == connectionId).Select(x => x.Key).ToList())
172  mappedChannels.Remove(I);
173 
174  if (updateTrackings)
175  lock (trackingContexts)
176  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
177  else
178  task = Task.CompletedTask;
179  }
180  await task.ConfigureAwait(false);
181  return provider;
182  }
183 
191  async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
192  {
193  //map the channel if it's private and we haven't seen it
194  lock (providers)
195  {
196  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
197  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
198  if (message.User.Channel.IsPrivateChannel)
199  lock (mappedChannels)
200  {
201  if (!provider.Connected)
202  return;
203  if (!enumerable.Any())
204  {
205  ulong newId;
206  lock (this)
207  newId = channelIdCounter++;
208  mappedChannels.Add(newId, new ChannelMapping
209  {
210  IsWatchdogChannel = false,
211  ProviderChannelId = message.User.Channel.RealId,
212  ProviderId = providerId,
213  Channel = message.User.Channel
214  });
215  message.User.Channel.RealId = newId;
216  }
217  else
218  message.User.Channel.RealId = enumerable.First().Key;
219  }
220  else
221  {
222  //need to add tag and isAdminChannel
223  var mapping = enumerable.First().Value;
224  message.User.Channel.Id = mapping.Channel.Id;
225  message.User.Channel.Tag = mapping.Channel.Tag;
226  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
227  }
228  }
229 
230  var splits = new List<string>(message.Content.Trim().Split(' '));
231  var address = splits[0];
232  if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ','))
233  address = address.Substring(0, address.Length - 1);
234 
235  address = address.ToUpperInvariant();
236 
237  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
238 
239  if (!addressed && !message.User.Channel.IsPrivateChannel)
240  //no mention
241  return;
242 
243  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
244 
245  if (addressed)
246  splits.RemoveAt(0);
247 
248  if (splits.Count == 0)
249  {
250  //just a mention
251  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
252  return;
253  }
254 
255  var command = splits[0].ToUpperInvariant();
256  splits.RemoveAt(0);
257  var arguments = String.Join(" ", splits);
258 
259  try
260  {
261  async Task<ICommand> GetCommand(string commandName)
262  {
263  if (!builtinCommands.TryGetValue(commandName, out var handler))
264  {
265  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken));
266  await Task.WhenAll(tasks).ConfigureAwait(false);
267  handler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == commandName).FirstOrDefault();
268  }
269  return handler;
270  };
271 
272  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
273 
274  if (command == "HELP" || command == "?")
275  {
276  string helpText;
277  if (splits.Count == 0)
278  {
279  var allCommands = builtinCommands.Select(x => x.Value).ToList();
280  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)).ToList();
281  await Task.WhenAll(tasks).ConfigureAwait(false);
282  allCommands.AddRange(tasks.SelectMany(x => x.Result));
283  helpText = String.Format(CultureInfo.InvariantCulture, "Available commands (Type '?' or 'help' and then a command name for more details): {0}", String.Join(", ", allCommands.Select(x => x.Name)));
284  }
285  else
286  {
287  var helpHandler = await GetCommand(splits[0].ToUpperInvariant()).ConfigureAwait(false);
288  if (helpHandler != default)
289  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
290  else
291  helpText = UnknownCommandMessage;
292  }
293  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
294  return;
295  }
296 
297  var commandHandler = await GetCommand(command).ConfigureAwait(false);
298 
299  if (commandHandler == default)
300  {
301  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
302  return;
303  }
304 
305  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
306  {
307  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
308  return;
309  }
310 
311  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
312  if (result != null)
313  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
314  }
315  catch (OperationCanceledException)
316  {
317  throw;
318  }
319  catch (Exception e)
320  {
321  //error bc custom commands should reply about why it failed
322  logger.LogError("Error processing chat command: {0}", e);
323  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
324  }
325  }
326 
332  async Task MonitorMessages(CancellationToken cancellationToken)
333  {
334  var messageTasks = new Dictionary<IProvider, Task<Message>>();
335  try
336  {
337  while (!cancellationToken.IsCancellationRequested)
338  {
339  //prune disconnected providers
340  foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
341  messageTasks.Remove(I.Key);
342 
343  //add new ones
344  Task updatedTask;
345  lock (this)
346  updatedTask = connectionsUpdated.Task;
347  lock (providers)
348  foreach (var I in providers)
349  if (I.Value.Connected && !messageTasks.ContainsKey(I.Value))
350  messageTasks.Add(I.Value, I.Value.NextMessage(cancellationToken));
351 
352  if (messageTasks.Count == 0)
353  {
354  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
355  continue;
356  }
357 
358  //wait for a message
359  await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
360 
361  //process completed ones
362  foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
363  {
364  var message = await I.Value.ConfigureAwait(false);
365 
366  await ProcessMessage(I.Key, message, cancellationToken).ConfigureAwait(false);
367 
368  messageTasks.Remove(I.Key);
369  }
370  }
371  }
372  catch (OperationCanceledException) { }
373  catch (Exception e)
374  {
375  logger.LogError("Message monitor crashed!: Exception: {0}", e);
376  }
377  }
378 
380  public async Task ChangeChannels(long connectionId, IEnumerable<Api.Models.ChatChannel> newChannels, CancellationToken cancellationToken)
381  {
382  if (newChannels == null)
383  throw new ArgumentNullException(nameof(newChannels));
384  var provider = await RemoveProvider(connectionId, false, cancellationToken).ConfigureAwait(false);
385  if (provider == null)
386  return;
387  var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
388  if (results == null) //aborted
389  return;
390  var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
391  {
392  IsWatchdogChannel = x.IsWatchdogChannel == true,
393  IsUpdatesChannel = x.IsUpdatesChannel == true,
394  ProviderChannelId = y.RealId,
395  ProviderId = connectionId,
396  Channel = y
397  });
398 
399  ulong baseId;
400  lock (this)
401  {
402  baseId = channelIdCounter;
403  channelIdCounter += (ulong)results.Count;
404  }
405 
406  Task task;
407  lock (mappedChannels)
408  {
409  lock (providers)
410  if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again
411  return;
412  foreach (var I in mappings)
413  {
414  var newId = baseId++;
415  mappedChannels.Add(newId, I);
416  I.Channel.RealId = newId;
417  }
418 
419  lock (trackingContexts)
420  task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
421  }
422  await task.ConfigureAwait(false);
423  }
424 
426  public async Task ChangeSettings(ChatBot newSettings, CancellationToken cancellationToken)
427  {
428  if (newSettings == null)
429  throw new ArgumentNullException(nameof(newSettings));
430  IProvider provider;
431 
432  async Task DisconnectProvider(IProvider p)
433  {
434  try
435  {
436  await p.Disconnect(cancellationToken).ConfigureAwait(false);
437  }
438  finally
439  {
440  p.Dispose();
441  }
442  }
443 
444  Task disconnectTask;
445  lock (providers)
446  {
447  //raw settings changes forces a rebuild of the provider
448  if (providers.TryGetValue(newSettings.Id, out provider))
449  {
450  providers.Remove(newSettings.Id);
451  disconnectTask = DisconnectProvider(provider);
452  }
453  else
454  disconnectTask = Task.CompletedTask;
455  if (newSettings.Enabled.Value)
456  {
457  provider = providerFactory.CreateProvider(newSettings);
458  providers.Add(newSettings.Id, provider);
459  }
460  }
461 
462  lock (mappedChannels)
463  foreach (var I in mappedChannels.Where(x => x.Value.ProviderId == newSettings.Id).Select(x => x.Key).ToList())
464  mappedChannels.Remove(I);
465 
466  await disconnectTask.ConfigureAwait(false);
467 
468  if (started)
469  {
470  if (newSettings.Enabled.Value)
471  await provider.Connect(cancellationToken).ConfigureAwait(false);
472  lock (this)
473  {
474  //same thread shennanigans
475  var oldOne = connectionsUpdated;
476  connectionsUpdated = new TaskCompletionSource<object>();
477  oldOne.SetResult(null);
478  }
479  }
480  }
481 
483  public Task SendMessage(string message, IEnumerable<ulong> channelIds, CancellationToken cancellationToken)
484  {
485  if (message == null)
486  throw new ArgumentNullException(nameof(message));
487  if (channelIds == null)
488  throw new ArgumentNullException(nameof(channelIds));
489 
490  return Task.WhenAll(channelIds.Select(x =>
491  {
492  ChannelMapping channelMapping;
493  lock (mappedChannels)
494  if (!mappedChannels.TryGetValue(x, out channelMapping))
495  return Task.CompletedTask;
496  IProvider provider;
497  lock (providers)
498  if (!providers.TryGetValue(channelMapping.ProviderId, out provider))
499  return Task.CompletedTask;
500  return provider.SendMessage(channelMapping.ProviderChannelId, message, cancellationToken);
501  }));
502  }
503 
505  public Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
506  {
507  List<ulong> wdChannels;
508  message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
509  lock (mappedChannels) //so it doesn't change while we're using it
510  wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
511  return SendMessage(message, wdChannels, cancellationToken);
512  }
513 
515  public Task SendUpdateMessage(string message, CancellationToken cancellationToken)
516  {
517  List<ulong> wdChannels;
518  message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message);
519  lock (mappedChannels) //so it doesn't change while we're using it
520  wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
521  return SendMessage(message, wdChannels, cancellationToken);
522  }
523 
525  public async Task StartAsync(CancellationToken cancellationToken)
526  {
527  foreach (var I in commandFactory.GenerateCommands())
528  builtinCommands.Add(I.Name.ToUpperInvariant(), I);
529  await Task.WhenAll(initialChatBots.Select(x => ChangeSettings(x, cancellationToken))).ConfigureAwait(false);
530  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Connect(cancellationToken))).ConfigureAwait(false);
531  await Task.WhenAll(initialChatBots.Select(x => ChangeChannels(x.Id, x.Channels, cancellationToken))).ConfigureAwait(false);
532  chatHandler = MonitorMessages(handlerCts.Token);
533  started = true;
534  }
535 
537  public async Task StopAsync(CancellationToken cancellationToken)
538  {
539  handlerCts.Cancel();
540  await chatHandler.ConfigureAwait(false);
541  await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false);
542  }
543 
545  public async Task<IJsonTrackingContext> TrackJsons(string basePath, string channelsJsonName, string commandsJsonName, CancellationToken cancellationToken)
546  {
547  if (customCommandHandler == null)
548  throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
549  JsonTrackingContext context = null;
550  context = new JsonTrackingContext(ioManager, customCommandHandler, loggerFactory.CreateLogger<JsonTrackingContext>(), () =>
551  {
552  lock (trackingContexts)
553  trackingContexts.Remove(context);
554  }, ioManager.ConcatPath(basePath, commandsJsonName), ioManager.ConcatPath(basePath, channelsJsonName));
555  Task task;
556  lock (trackingContexts)
557  {
558  trackingContexts.Add(context);
559  lock (mappedChannels)
560  task = context.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken);
561  }
562  await task.ConfigureAwait(false);
563  return context;
564  }
565 
567  public bool Connected(long connectionId)
568  {
569  lock (providers)
570  return providers.TryGetValue(connectionId, out var provider) && provider.Connected;
571  }
572 
574  public void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
575  {
576  if (this.customCommandHandler != null)
577  throw new InvalidOperationException("RegisterCommandHandler() already called!");
578  this.customCommandHandler = customCommandHandler ?? throw new ArgumentNullException(nameof(customCommandHandler));
579  }
580 
582  public async Task DeleteConnection(long connectionId, CancellationToken cancellationToken)
583  {
584  var provider = await RemoveProvider(connectionId, true, cancellationToken).ConfigureAwait(false);
585  if (provider != null)
586  try
587  {
588  await provider.Disconnect(cancellationToken).ConfigureAwait(false);
589  }
590  finally
591  {
592  provider.Dispose();
593  }
594  }
595 
597  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
598  {
599  var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
600  List<ulong> wdChannels;
601  lock (mappedChannels) //so it doesn't change while we're using it
602  wdChannels = mappedChannels.Select(x => x.Key).ToList();
603  return SendMessage(message, wdChannels, cancellationToken);
604  }
605  }
606 }
void RegisterCommandHandler(ICustomCommandHandler customCommandHandler)
Registers a customCommandHandler to use
Definition: Chat.cs:574
bool IsWatchdogChannel
If Channel is a watchdog channel
Represents a mapping of a Channel.RealId
User User
The Components.Chat.User who sent the Message
Definition: Message.cs:16
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the Chat
Definition: Chat.cs:50
-
bool IsPrivate
If this is a 1-to-1 chat channel
Definition: Channel.cs:46
async Task SetChannels(IEnumerable< Channel > channels, CancellationToken cancellationToken)
Writes information about connected channels to the IJsonTrackingContext
@@ -130,6 +129,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IProviderFactory providerFactory
The IProviderFactory for the Chat
Definition: Chat.cs:25
+
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
@@ -143,7 +143,6 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
async Task MonitorMessages(CancellationToken cancellationToken)
Monitors active providers for new Messages
Definition: Chat.cs:332
-
bool IsAdmin
If this is considered a channel for admin commands
Definition: Channel.cs:41
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
@@ -163,6 +162,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Task SendMessage(string message, IEnumerable< ulong > channelIds, CancellationToken cancellationToken)
Send a chat message to a given set of channelIds
Definition: Chat.cs:483
Represents the lifetime of a IRestartHandler registration
readonly List< IJsonTrackingContext > trackingContexts
The active IJsonTrackingContexts for the Chat
Definition: Chat.cs:75
+
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
diff --git a/_chat_command_8cs_source.html b/_chat_command_8cs_source.html index 8fcb7b704b..4fda24028f 100644 --- a/_chat_command_8cs_source.html +++ b/_chat_command_8cs_source.html @@ -92,7 +92,7 @@ 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 Parameters { 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 {
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 }
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 8422835c83..4a2838d659 100644 --- a/_chat_controller_8cs_source.html +++ b/_chat_controller_8cs_source.html @@ -92,16 +92,16 @@ 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  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 }
- +
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
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
@@ -113,7 +113,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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
- +
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
diff --git a/_claims_injector_8cs_source.html b/_claims_injector_8cs_source.html index 2a5ad7af07..e39c419c46 100644 --- a/_claims_injector_8cs_source.html +++ b/_claims_injector_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.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_command_8cs_source.html b/_comm_command_8cs_source.html index 11c3d80606..b069272bb7 100644 --- a/_comm_command_8cs_source.html +++ b/_comm_command_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
CommCommand.cs
-Go to the documentation of this file.
1 using System.Collections.Generic;
2 
3 namespace Tgstation.Server.Host.Components.Interop
4 {
8  sealed class CommCommand
9  {
13  public IReadOnlyDictionary<string, string> Parameters { get; set; }
14 
18  public string RawJson { get; set; }
19  }
20 }
+Go to the documentation of this file.
1 using System.Collections.Generic;
2 
3 namespace Tgstation.Server.Host.Components.Interop
4 {
8  sealed class CommCommand
9  {
13  public IReadOnlyDictionary<string, object> Parameters { get; set; }
14 
18  public string RawJson { get; set; }
19  }
20 }
diff --git a/_comm_context_8cs_source.html b/_comm_context_8cs_source.html index cc723ed486..3fe3fdf25c 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, string>>(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
diff --git a/_components_2_watchdog_2_watchdog_8cs_source.html b/_components_2_watchdog_2_watchdog_8cs_source.html index ee2787c9b8..5110e9d684 100644 --- a/_components_2_watchdog_2_watchdog_8cs_source.html +++ b/_components_2_watchdog_2_watchdog_8cs_source.html @@ -92,11 +92,11 @@ 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
457  monitorState.ActiveServer.ClosePortOnReboot = true;
458 
459  if (!restartOnceSwapped)
460  {
461  //inactive server still valid
462 
463  //disable this
464  monitorState.InactiveServer.ClosePortOnReboot = false;
465 
466  //now try to reopen it on the private port
467  //failing that, just reboot it
468  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
469  }
470 
471  //break either way because any issues past this point would be solved by the reboot
472  if (restartOnceSwapped)
473  //for one reason or another
474  //update and reboot
475  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
476  else
477  //only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
478  monitorState.NextAction = MonitorAction.Skip;
479  break;
480  case MonitorActivationReason.InactiveServerRebooted:
481  //just don't let the active server close it's port if the inactive server isn't ready
482  monitorState.RebootingInactiveServer = true;
483  monitorState.InactiveServer.ResetRebootState();
484  monitorState.ActiveServer.ClosePortOnReboot = false;
485  monitorState.NextAction = MonitorAction.Continue;
486  break;
487  case MonitorActivationReason.InactiveServerStartupComplete:
488  //opposite of above case
489  monitorState.RebootingInactiveServer = false;
490  monitorState.ActiveServer.ClosePortOnReboot = true;
491  monitorState.NextAction = MonitorAction.Continue;
492  break;
493  case MonitorActivationReason.NewDmbAvailable:
494  case MonitorActivationReason.ActiveLaunchParametersUpdated:
495  //just reload the inactive server and wait for a swap to apply the changes
496  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
497  break;
498  }
499  }
500 
506  async Task MonitorLifetimes(CancellationToken cancellationToken)
507  {
508  logger.LogTrace("Entered MonitorLifetimes");
509 
510  //this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
511 
512  var iteration = 1;
513  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
514  {
515  //always start out with continue
516  monitorState.NextAction = MonitorAction.Continue;
517 
518  //dump some info to the logs
519  logger.LogDebug("Iteration {0} of monitor loop", iteration);
520  try
521  {
522  if (AlphaIsActive)
523  logger.LogDebug("Alpha is the active server");
524  else
525  logger.LogDebug("Bravo is the active server");
526 
527 
528  if (monitorState.RebootingInactiveServer)
529  logger.LogDebug("Inactive server is rebooting");
530 
531  //update the monitor state with the inactive/active servers
532  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
533  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
534 
535  if (monitorState.ActiveServer.ClosePortOnReboot)
536  logger.LogDebug("Active server will close port on reboot");
537  if (monitorState.InactiveServer.ClosePortOnReboot)
538  logger.LogDebug("Inactive server will close port on reboot");
539 
540  logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
541  logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
542 
543  //load the activation tasks into local variables
544  Task activeServerLifetime = monitorState.ActiveServer.Lifetime;
545  Task inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
546  var activeServerReboot = monitorState.ActiveServer.OnReboot;
547  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
548  Task inactiveServerStartup = monitorState.RebootingInactiveServer ? monitorState.InactiveServer.LaunchResult : null;
549  Task activeLaunchParametersChanged = activeParametersUpdated.Task;
550  var newDmbAvailable = dmbFactory.OnNewerDmb;
551 
552  //cancel waiting if requested
553  var cancelTcs = new TaskCompletionSource<object>();
554  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
555  {
556  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
557  if (monitorState.RebootingInactiveServer)
558  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
559  //wait for something to happen
560  await toWaitOn.ConfigureAwait(false);
561  cancellationToken.ThrowIfCancellationRequested();
562  }
563 
564  var chatTask = Task.CompletedTask;
565  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
566  {
567  //always run HandleMonitorWakeup from the context of the semaphore lock
568  //multiple things may have happened, handle them one at a time
569  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
570  {
571  MonitorActivationReason activationReason = default; //this will always be assigned before being used
572 
573  //process the tasks in this order and call HandlerMonitorWakup for each
574 
575  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
576  {
577  var taskCompleted = task?.IsCompleted == true;
578  task = null;
579  if (monitorState.NextAction == MonitorAction.Skip)
580  monitorState.NextAction = MonitorAction.Continue;
581  else if (taskCompleted)
582  {
583  activationReason = testActivationReason;
584  return true;
585  }
586  return false;
587  };
588 
589  if (CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
590  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
591  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
592  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
593  || CheckActivationReason(ref inactiveServerStartup, MonitorActivationReason.InactiveServerStartupComplete)
594  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
595  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated))
596  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
597  else
598  moreActivationsToProcess = false;
599  }
600 
601  //writeback alphaServer and bravoServer from monitor state in case they changesd
602  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
603  bravoServer = !AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
604  }
605 
606  //full reboot required
607  if (monitorState.NextAction == MonitorAction.Restart)
608  {
609  logger.LogDebug("Next state action is to restart");
610  DisposeAndNullControllers();
611  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
612 
613  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
614  {
615  Exception launchException = null;
616  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
617  try
618  {
619  //use LaunchImplNoLock without announcements or restarting the monitor
620  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
621  if (Running)
622  {
623  logger.LogDebug("Relaunch successful, resetting monitor state...");
624  monitorState = new MonitorState(); //clean the slate and continue
625  }
626  }
627  catch (OperationCanceledException)
628  {
629  throw;
630  }
631  catch (Exception e)
632  {
633  launchException = e;
634  }
635 
636  await chatTask.ConfigureAwait(false);
637  if (!Running)
638  {
639  if (launchException == null)
640  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
641  else
642  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
643  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); //max of one hour, increasing by a power of 2 each time
644  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
645  await Task.WhenAll(asyncDelayer.Delay(TimeSpan.FromSeconds(retryDelay), cancellationToken), chatTask).ConfigureAwait(false);
646  }
647  }
648  }
649  }
650  catch (OperationCanceledException)
651  {
652  logger.LogDebug("Monitor cancelled");
653  break;
654  }
655  catch (Exception e)
656  {
657  //really, this should NEVER happen
658  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}, Exception: {2}", iteration, JsonConvert.SerializeObject(monitorState), e);
659  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);
660  }
661  }
662  logger.LogTrace("Monitor exiting...");
663  }
664 
669  async Task<bool> StopMonitor()
670  {
671  logger.LogTrace("StopMonitor");
672  if (monitorTask == null)
673  return false;
674  monitorCts.Cancel();
675  await monitorTask.ConfigureAwait(false);
676  monitorCts.Dispose();
677  monitorTask = null;
678  return true;
679  }
680 
681 
683  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
684  {
685  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
686  {
687  if (launchParameters.Match(ActiveLaunchParameters))
688  return;
689  ActiveLaunchParameters = launchParameters;
690  if (Running)
691  //queue an update
692  activeParametersUpdated.TrySetResult(null);
693  }
694  }
695 
704  async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
705  {
706  logger.LogTrace("Begin LaunchNoLock");
707 
708  if (Running)
709  throw new JobException("Watchdog already running!");
710 
711  Task chatTask;
712  //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
713  if (startMonitor && await StopMonitor().ConfigureAwait(false))
714  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
715  else if (announce)
716  //simple announce
717  chatTask = chat.SendWatchdogMessage(reattachInfo == null ? "Starting..." : "Reattaching...", cancellationToken);
718  else
719  //no announce
720  chatTask = Task.CompletedTask;
721 
722  //since neither server is running, this is safe to do
723  LastLaunchParameters = ActiveLaunchParameters;
724 
725  //for when we call ourself and want to not catch thrown exceptions
726  var ignoreNestedException = false;
727  try
728  {
729  //good ole sanity, should never fucking trigger but i don't trust myself even though I should
730  if (alphaServer != null || bravoServer != null)
731  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
732 
733  //don't need a new dmb if reattaching
734  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
735  var dmbToUse = doesntNeedNewDmb ? null : dmbFactory.LockNextDmb(2);
736 
737  //if this try catches something, both servers are killed
738  try
739  {
740  //start the alpha server task, either by launch a new process or attaching to an existing one
741  //The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
742  //The tasks pertaining to server startup times are in the ISessionControllers
743  Task<ISessionController> alphaServerTask;
744  if (!doesntNeedNewDmb)
745  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken);
746  else
747  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
748 
749  //retrieve the session controller
750  var startTime = DateTimeOffset.Now;
751  alphaServer = await alphaServerTask.ConfigureAwait(false);
752  //failed reattaches will return null
753  alphaServer?.SetHighPriority();
754 
755  //extra delay for total ordering
756  var now = DateTimeOffset.Now;
757  var delay = now - startTime;
758 
759  //definitely not if reattaching though
760  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
761  await asyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
762 
763  //now bring bravo up
764  if (!doesntNeedNewDmb)
765  bravoServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken).ConfigureAwait(false);
766  else
767  bravoServer = await sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
768  //failed reattaches will return null
769  bravoServer?.SetHighPriority();
770 
771  //possiblity of null servers due to failed reattaches
772  if (alphaServer == null || bravoServer == null)
773  {
774  await chatTask.ConfigureAwait(false);
775  var bothServersDead = alphaServer == null && bravoServer == null;
776  if (bothServersDead
777  || (alphaServer == null && reattachInfo.AlphaIsActive)
778  || (bravoServer == null && !reattachInfo.AlphaIsActive))
779  {
780  //we lost the active server, just restart entirely
781  DisposeAndNullControllers();
782  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
783  logger.LogWarning(FailReattachMessage);
784  logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
785  chatTask = chat.SendWatchdogMessage(FailReattachMessage, cancellationToken);
786  ignoreNestedException = true;
787  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
788  return;
789  }
790 
791  //we still have the active server but the other one is dead to us, hand it off to the monitor to restart
792  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
793  chatTask = chat.SendWatchdogMessage(InactiveReattachFailureMessage, cancellationToken);
794  logger.LogWarning(InactiveReattachFailureMessage);
795 
796  if (reattachInfo.AlphaIsActive)
797  bravoServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
798  else
799  alphaServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
800  }
801 
802  //throws a JobException if something went wrong with a launch
803  //Dead sessions won't trigger this
804  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
805  {
806  var launch = await controller.LaunchResult.ConfigureAwait(false);
807  if (launch.ExitCode.HasValue)
808  //you killed us ray...
809  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
810  if (!launch.StartupTime.HasValue)
811  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server timed out on startup: {0}s", launch.ToString(), ActiveLaunchParameters.StartupTimeout.Value));
812  return launch;
813  }
814 
815  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
816  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
817  //this task completes when both serers have finished booting
818  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
819 
820  //don't forget about the cancellationToken
821  var cancelTcs = new TaskCompletionSource<object>();
822  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
823  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
824  cancellationToken.ThrowIfCancellationRequested();
825 
826  await allTask.ConfigureAwait(false);
827 
828  //both servers are now running, alpha is the active server(unless reattach), huzzah
829  AlphaIsActive = reattachInfo?.AlphaIsActive ?? true;
830 
831  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
832  activeServer.EnableCustomChatCommands();
833  activeServer.ClosePortOnReboot = true;
834 
835  logger.LogInformation("Launched servers successfully");
836  Running = true;
837 
838  if (startMonitor)
839  {
840  monitorCts = new CancellationTokenSource();
841  monitorTask = MonitorLifetimes(monitorCts.Token);
842  }
843  }
844  catch
845  {
846  if (dmbToUse != null)
847  {
848  //we locked 2 dmbs
849  if (bravoServer == null)
850  {
851  //bravo didn't get control of his
852  dmbToUse.Dispose();
853  if (alphaServer == null)
854  //alpha didn't get control of his
855  dmbToUse.Dispose();
856  }
857  }
858  else if (doesntNeedNewDmb)
859  //we have reattachInfo
860  if (bravoServer == null)
861  {
862  //bravo didn't get control of his
863  reattachInfo.Bravo?.Dmb.Dispose();
864  if (alphaServer == null)
865  //alpha didn't get control of his
866  reattachInfo.Alpha?.Dmb.Dispose();
867  }
868  //kill the controllers
869  DisposeAndNullControllers();
870  throw;
871  }
872  }
873  catch (Exception e)
874  {
875  //don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
876  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
877  {
878  var originalChatTask = chatTask;
879  async Task ChainChatTaskWithErrorMessage()
880  {
881  await originalChatTask.ConfigureAwait(false);
882  await chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false);
883  }
884  chatTask = ChainChatTaskWithErrorMessage();
885  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
886  }
887  throw;
888  }
889  finally
890  {
891  //finish the chat task that's in flight
892  try
893  {
894  await chatTask.ConfigureAwait(false);
895  }
896  catch (OperationCanceledException) { }
897  }
898  }
899 
901  public async Task Launch(CancellationToken cancellationToken)
902  {
903  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
904  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
905  }
906 
908  public async Task ResetRebootState(CancellationToken cancellationToken)
909  {
910  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
911  {
912  if (!Running)
913  return;
914  var toClear = AlphaIsActive ? alphaServer : bravoServer;
915  if (toClear != null)
916  toClear.ResetRebootState();
917  }
918  }
919 
921  public async Task Restart(bool graceful, CancellationToken cancellationToken)
922  {
923  logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
924  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
925  {
926  if (!graceful || !Running)
927  {
928  Task chatTask;
929  if (Running)
930  {
931  chatTask = chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken);
932  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
933  }
934  else
935  chatTask = Task.CompletedTask;
936  await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
937  await chatTask.ConfigureAwait(false);
938  }
939  var toReboot = AlphaIsActive ? alphaServer : bravoServer;
940  if (toReboot != null)
941  {
942  if (!await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false))
943  logger.LogWarning("Unable to send reboot state change event!");
944  }
945  }
946  }
947 
949  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
950  {
951  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
952  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
953  }
954 
956  public async Task StartAsync(CancellationToken cancellationToken)
957  {
958  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
959  if (!autoStart && reattachInfo == null)
960  return;
961 
962  long? adminUserId = null;
963 
964  await databaseContextFactory.UseContext(async db => adminUserId = await db.Users
965  .Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant())
966  .Select(x => x.Id)
967  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
968  var job = new Models.Job
969  {
970  StartedBy = new Models.User
971  {
972  Id = adminUserId.Value
973  },
974  Instance = new Models.Instance
975  {
976  Id = instance.Id
977  },
978  Description = "Instance startup watchdog launch",
979  CancelRight = (ulong)DreamDaemonRights.Shutdown,
980  CancelRightsType = RightsType.DreamDaemon
981  };
982  await jobManager.RegisterOperation(job, async (j, databaseContext, progressFunction, ct) =>
983  {
984  using (await SemaphoreSlimContext.Lock(semaphore, ct).ConfigureAwait(false))
985  await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
986  }, cancellationToken).ConfigureAwait(false);
987  }
988 
990  public async Task StopAsync(CancellationToken cancellationToken)
991  {
992  if (releaseServers && Running)
993  {
994  await StopMonitor().ConfigureAwait(false);
995 
996  var reattachInformation = new WatchdogReattachInformation
997  {
998  AlphaIsActive = AlphaIsActive
999  };
1000  reattachInformation.Alpha = alphaServer?.Release();
1001  reattachInformation.Bravo = bravoServer?.Release();
1002  await reattachInfoHandler.Save(reattachInformation, cancellationToken).ConfigureAwait(false);
1003  }
1004  await Terminate(false, cancellationToken).ConfigureAwait(false);
1005  }
1006 
1008  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
1009  {
1010  string results;
1011  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1012  {
1013  if (!Running)
1014  return true;
1015 
1016  var builder = new StringBuilder(Constants.DMTopicEvent);
1017  builder.Append('&');
1018  var notification = new EventNotification
1019  {
1020  Type = eventType,
1021  Parameters = parameters
1022  };
1023  var json = JsonConvert.SerializeObject(notification);
1024  builder.Append(byondTopicSender.SanitizeString(Constants.DMParameterData));
1025  builder.Append('=');
1026  builder.Append(byondTopicSender.SanitizeString(json));
1027 
1028  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1029  results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
1030  }
1031 
1032  if (results == Constants.DMResponseSuccess)
1033  return true;
1034 
1035  List<Response> responses;
1036  try
1037  {
1038  responses = JsonConvert.DeserializeObject<List<Response>>(results);
1039  }
1040  catch
1041  {
1042  logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
1043  return true;
1044  }
1045 
1046  await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
1047 
1048  return true;
1049  }
1050 
1052  public async Task<string> HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
1053  {
1054  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1055  {
1056  if (!Running)
1057  return "ERROR: Server offline!";
1058 
1059  var commandObject = new ChatCommand
1060  {
1061  Command = commandName,
1062  Parameters = arguments,
1063  User = sender
1064  };
1065 
1066  var json = JsonConvert.SerializeObject(commandObject, new JsonSerializerSettings
1067  {
1068  ContractResolver = new CamelCasePropertyNamesContractResolver()
1069  });
1070 
1071  var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json));
1072 
1073  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1074  return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
1075  }
1076  }
1077 
1079  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
1080  {
1081  releaseServers = true;
1082  if (Running)
1083  await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false);
1084  }
1085  }
1086 }
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Newtonsoft.Json;
5 using Newtonsoft.Json.Serialization;
6 using System;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Linq;
10 using System.Text;
11 using System.Threading;
12 using System.Threading.Tasks;
19 
20 namespace Tgstation.Server.Host.Components.Watchdog
21 {
24  {
28  const int AlphaBravoStartupSeperationInterval = 10;
29 
31  public bool Running { get; private set; }
32 
34  public bool AlphaIsActive { get; private set; }
35 
37  public Models.CompileJob ActiveCompileJob => (AlphaIsActive ? alphaServer : bravoServer)?.Dmb.CompileJob;
38 
40  public DreamDaemonLaunchParameters ActiveLaunchParameters { get; private set; }
41 
43  public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; }
44 
46  public RebootState? RebootState => Running ? (AlphaIsActive ? alphaServer?.RebootState : bravoServer?.RebootState) : null;
47 
51  readonly IChat chat;
52 
57 
62 
67 
72 
76  readonly IByondTopicSender byondTopicSender;
77 
82 
87 
92 
97 
101  readonly ILogger<Watchdog> logger;
102 
106  readonly SemaphoreSlim semaphore;
107 
111  readonly Api.Models.Instance instance;
112 
116  readonly bool autoStart;
117 
121  CancellationTokenSource monitorCts;
122 
127 
131  TaskCompletionSource<object> activeParametersUpdated;
132 
141 
146 
164  public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, ILogger<Watchdog> logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart)
165  {
166  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
167  this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory));
168  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
169  this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler));
170  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
171  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
172  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
173  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
174  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
175  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
176  ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters));
177  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
178  this.autoStart = autoStart;
179 
180  if (serverControl == null)
181  throw new ArgumentNullException(nameof(serverControl));
182 
183  chat.RegisterCommandHandler(this);
184 
185  AlphaIsActive = true;
186  ActiveLaunchParameters = initialLaunchParameters;
187  releaseServers = false;
188  activeParametersUpdated = new TaskCompletionSource<object>();
189 
190  restartRegistration = serverControl.RegisterForRestart(this);
191  try
192  {
193  semaphore = new SemaphoreSlim(1);
194  }
195  catch
196  {
197  restartRegistration.Dispose();
198  throw;
199  }
200  }
201 
203  public void Dispose()
204  {
205  DisposeAndNullControllers();
206  semaphore.Dispose();
207  restartRegistration.Dispose();
208  }
209 
214  {
215  alphaServer?.Dispose();
216  alphaServer = null;
217  bravoServer?.Dispose();
218  bravoServer = null;
219  Running = false;
220  }
221 
228  async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
229  {
230  if (!Running)
231  return;
232  if (!graceful)
233  {
234  var chatTask = announce ? chat.SendWatchdogMessage("Terminating...", cancellationToken) : Task.CompletedTask;
235  await StopMonitor().ConfigureAwait(false);
236  DisposeAndNullControllers();
237  LastLaunchParameters = null;
238  await chatTask.ConfigureAwait(false);
239  return;
240  }
241 
242  //merely set the reboot state
243  var toKill = AlphaIsActive ? alphaServer : bravoServer;
244  var other = AlphaIsActive ? bravoServer : alphaServer;
245  if (toKill != null)
246  await toKill.SetRebootState(Components.Watchdog.RebootState.Shutdown, cancellationToken).ConfigureAwait(false);
247  }
248 
256  async Task HandlerMonitorWakeup(MonitorActivationReason activationReason, MonitorState monitorState, CancellationToken cancellationToken)
257  {
258  logger.LogDebug("Monitor activation. Reason: {0}", activationReason);
259 
260  //this is where the bulk of the watchdog handling code lives and is fraught with lambdas, sorry not sorry
261  //I'll do my best to walk you through it
262 
263  //returns true if the inactive server can't be used immediately
264  //also sets monitor to restart if the above holds
265  bool FullRestartDeadInactive()
266  {
267  if (monitorState.RebootingInactiveServer || monitorState.InactiveServerCritFail)
268  {
269  logger.LogInformation("Inactive server is {0}! Restarting monitor...", monitorState.InactiveServerCritFail ? "critically failed" : "still rebooting");
270  monitorState.NextAction = MonitorAction.Restart; //will dispose server
271  return true;
272  }
273  return false;
274  };
275 
276  //trys to set inactive server's port to the public game port
277  //doesn't handle closing active server's port
278  //returns true on success and swaps inactiveserver and activeserver also sets LastLaunchParameters to ActiveLaunchParameters
279  //on failure, sets monitor to restart
280  async Task<bool> MakeInactiveActive()
281  {
282  logger.LogDebug("Setting inactive server to port {0}...", ActiveLaunchParameters.PrimaryPort.Value);
283  var result = await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.PrimaryPort.Value, cancellationToken).ConfigureAwait(false);
284 
285  if (!result)
286  {
287  logger.LogWarning("Failed to activate inactive server! Restarting monitor...");
288  monitorState.NextAction = MonitorAction.Restart; //will dispose server
289  return false;
290  }
291 
292  //inactive server should always be using active launch parameters
293  LastLaunchParameters = ActiveLaunchParameters;
294 
295  var tmp = monitorState.ActiveServer;
296  monitorState.ActiveServer = monitorState.InactiveServer;
297  monitorState.InactiveServer = tmp;
298  AlphaIsActive = !AlphaIsActive;
299  monitorState.ActiveServer.EnableCustomChatCommands();
300  return true;
301  }
302 
303  // Kills and tries to launch inactive server with the latest dmb
304  // falls back to current dmb on failure
305  // Sets critfail on inactive server failing that
306  // returns false if the backup dmb was used successfully, true otherwise
307  async Task UpdateAndRestartInactiveServer(bool breakAfter)
308  {
309  activeParametersUpdated = new TaskCompletionSource<object>();
310  monitorState.InactiveServer.Dispose(); //kill or recycle it
311  var desiredNextAction = breakAfter ? MonitorAction.Break : MonitorAction.Continue;
312  monitorState.NextAction = desiredNextAction;
313 
314  logger.LogInformation("Rebooting inactive server...");
315  var newDmb = dmbFactory.LockNextDmb(1);
316  try
317  {
318  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, newDmb, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
319  monitorState.InactiveServer.SetHighPriority();
320  }
321  catch (OperationCanceledException)
322  {
323  throw;
324  }
325  catch (Exception e)
326  {
327  logger.LogError("Error occurred while recreating server! Attempting backup strategy of running DMB of running server! Exception: {0}", e.ToString());
328  //ahh jeez, what do we do here?
329  //this is our fault, so it should never happen but
330  //idk maybe a database error while handling the newest dmb?
331  //either way try to start it using the active server's dmb as a backup
332  try
333  {
334  var dmbBackup = await dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
335 
336  if (dmbBackup == null) //NANI!?
337  //just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
338  throw new JobException("Creating backup DMB provider failed!");
339 
340  monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
341  monitorState.InactiveServer.SetHighPriority();
342  await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
343  }
344  catch (OperationCanceledException)
345  {
346  throw;
347  }
348  catch (Exception e2)
349  {
350  //fuuuuucckkk
351  logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
352  monitorState.InactiveServerCritFail = true;
353  await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
354  return;
355  }
356  }
357 
358  logger.LogInformation("Successfully relaunched inactive server!");
359  monitorState.RebootingInactiveServer = true;
360  }
361 
362  string ExitWord(ISessionController controller) => controller.TerminationWasRequested ? "exited" : "crashed";
363 
364  //reason handling
365  switch (activationReason)
366  {
367  case MonitorActivationReason.ActiveServerCrashed:
368  if (monitorState.ActiveServer.RebootState == Components.Watchdog.RebootState.Shutdown)
369  {
370  //the time for graceful shutdown is now
371  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Exiting due to graceful termination request...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
372  DisposeAndNullControllers();
373  monitorState.NextAction = MonitorAction.Exit;
374  break;
375  }
376 
377  if (FullRestartDeadInactive())
378  {
379  //tell chat about it and go ahead
380  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Inactive server unable to online!", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
381  //we've already been set to restart
382  break;
383  }
384 
385  //tell chat about it
386  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Active server {0}! Onlining inactive server...", ExitWord(monitorState.ActiveServer)), cancellationToken).ConfigureAwait(false);
387 
388  //try to active the inactive server
389  if (!await MakeInactiveActive().ConfigureAwait(false))
390  //failing that, we've already been set to restart
391  break;
392 
393  //bring up another inactive server
394  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
395  break;
396  case MonitorActivationReason.InactiveServerCrashed:
397  //just announce and try to bring it back
398  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Inactive server {0}! Rebooting...", ExitWord(monitorState.InactiveServer)), cancellationToken).ConfigureAwait(false);
399  await UpdateAndRestartInactiveServer(false).ConfigureAwait(false);
400  break;
401  case MonitorActivationReason.ActiveServerRebooted:
402  //ideal goal: active server just closed its port
403  //tell inactive server to open it's port and that's now the active server
404  var rebootState = monitorState.ActiveServer.RebootState;
405  monitorState.ActiveServer.ResetRebootState(); //the DMAPI has already done this internally
406 
407  if (FullRestartDeadInactive() && rebootState != Components.Watchdog.RebootState.Shutdown)
408  //full restart if the inactive server is being fucky
409  break;
410 
411  //what matters here is the RebootState
412  var restartOnceSwapped = false;
413 
414  switch (rebootState)
415  {
416  case Components.Watchdog.RebootState.Normal:
417  //life as normal
418  break;
419  case Components.Watchdog.RebootState.Restart:
420  //reboot the current active server once the inactive one activates
421  restartOnceSwapped = true;
422  break;
423  case Components.Watchdog.RebootState.Shutdown:
424  //graceful shutdown time
425  await chat.SendWatchdogMessage("Active server rebooted! Exiting due to graceful termination request...", cancellationToken).ConfigureAwait(false);
426  DisposeAndNullControllers();
427  monitorState.NextAction = MonitorAction.Exit;
428  return;
429  }
430 
431  //are both servers now running the same CompileJob?
432  var sameCompileJob = monitorState.InactiveServer.Dmb.CompileJob.Id == monitorState.ActiveServer.Dmb.CompileJob.Id;
433 
434  if (!sameCompileJob || ActiveLaunchParameters != LastLaunchParameters)
435  //need a new launch to update either settings or compile job
436  restartOnceSwapped = true;
437 
438  if (restartOnceSwapped)
439  //we need to manually restart active server
440  //just kill it here, easier that way
441  monitorState.ActiveServer.Dispose();
442 
443  var activeServerStillHasPortOpen = !restartOnceSwapped && !monitorState.ActiveServer.ClosePortOnReboot;
444 
445  if (activeServerStillHasPortOpen)
446  //we didn't want active server to swap for some reason and it still has it's port open
447  //just continue as normal
448  break;
449 
450  if (!await MakeInactiveActive().ConfigureAwait(false))
451  //monitor will restart
452  break;
453 
454  //servers now swapped
455 
456  //enable this now if inactive server is not still valid
457  monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
458 
459  if (!restartOnceSwapped)
460  //now try to reopen it on the private port
461  //failing that, just reboot it
462  restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
463 
464  //break either way because any issues past this point would be solved by the reboot
465  if (restartOnceSwapped)
466  //for one reason or another
467  //update and reboot
468  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
469  else
470  //only skip checking inactive server rebooted, it's guaranteed InactiveServerStartup complete wouldn't fire this iteration
471  monitorState.NextAction = MonitorAction.Skip;
472  break;
473  case MonitorActivationReason.InactiveServerRebooted:
474  //just don't let the active server close it's port if the inactive server isn't ready
475  monitorState.RebootingInactiveServer = true;
476  monitorState.InactiveServer.ResetRebootState();
477  monitorState.ActiveServer.ClosePortOnReboot = false;
478  monitorState.NextAction = MonitorAction.Continue;
479  break;
480  case MonitorActivationReason.InactiveServerStartupComplete:
481  //opposite of above case
482  monitorState.RebootingInactiveServer = false;
483  monitorState.ActiveServer.ClosePortOnReboot = true;
484  monitorState.NextAction = MonitorAction.Continue;
485  break;
486  case MonitorActivationReason.NewDmbAvailable:
487  case MonitorActivationReason.ActiveLaunchParametersUpdated:
488  //just reload the inactive server and wait for a swap to apply the changes
489  await UpdateAndRestartInactiveServer(true).ConfigureAwait(false);
490  break;
491  }
492  }
493 
499  async Task MonitorLifetimes(CancellationToken cancellationToken)
500  {
501  logger.LogTrace("Entered MonitorLifetimes");
502 
503  //this function is responsible for calling HandlerMonitorWakeup when necessary and manitaining the MonitorState
504 
505  var iteration = 1;
506  for (var monitorState = new MonitorState(); monitorState.NextAction != MonitorAction.Exit; ++iteration)
507  {
508  //always start out with continue
509  monitorState.NextAction = MonitorAction.Continue;
510 
511  //dump some info to the logs
512  logger.LogDebug("Iteration {0} of monitor loop", iteration);
513  try
514  {
515  if (AlphaIsActive)
516  logger.LogDebug("Alpha is the active server");
517  else
518  logger.LogDebug("Bravo is the active server");
519 
520 
521  if (monitorState.RebootingInactiveServer)
522  logger.LogDebug("Inactive server is rebooting");
523 
524  //update the monitor state with the inactive/active servers
525  monitorState.ActiveServer = AlphaIsActive ? alphaServer : bravoServer;
526  monitorState.InactiveServer = AlphaIsActive ? bravoServer : alphaServer;
527 
528  if (monitorState.ActiveServer.ClosePortOnReboot)
529  logger.LogDebug("Active server will close port on reboot");
530  if (monitorState.InactiveServer.ClosePortOnReboot)
531  logger.LogDebug("Inactive server will close port on reboot");
532 
533  logger.LogDebug("Active server Compile Job ID: {0}", monitorState.ActiveServer.Dmb.CompileJob.Id);
534  logger.LogDebug("Inactive server Compile Job ID: {0}", monitorState.InactiveServer.Dmb.CompileJob.Id);
535 
536  //load the activation tasks into local variables
537  Task activeServerLifetime = monitorState.ActiveServer.Lifetime;
538  Task inactiveServerLifetime = monitorState.InactiveServer.Lifetime;
539  var activeServerReboot = monitorState.ActiveServer.OnReboot;
540  var inactiveServerReboot = monitorState.InactiveServer.OnReboot;
541  Task inactiveServerStartup = monitorState.RebootingInactiveServer ? monitorState.InactiveServer.LaunchResult : null;
542  Task activeLaunchParametersChanged = activeParametersUpdated.Task;
543  var newDmbAvailable = dmbFactory.OnNewerDmb;
544 
545  //cancel waiting if requested
546  var cancelTcs = new TaskCompletionSource<object>();
547  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
548  {
549  var toWaitOn = Task.WhenAny(activeServerLifetime, inactiveServerLifetime, activeServerReboot, inactiveServerReboot, newDmbAvailable, cancelTcs.Task, activeLaunchParametersChanged);
550  if (monitorState.RebootingInactiveServer)
551  toWaitOn = Task.WhenAny(toWaitOn, inactiveServerStartup);
552  //wait for something to happen
553  await toWaitOn.ConfigureAwait(false);
554  cancellationToken.ThrowIfCancellationRequested();
555  }
556 
557  var chatTask = Task.CompletedTask;
558  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
559  {
560  //always run HandleMonitorWakeup from the context of the semaphore lock
561  //multiple things may have happened, handle them one at a time
562  for (var moreActivationsToProcess = true; moreActivationsToProcess && (monitorState.NextAction == MonitorAction.Continue || monitorState.NextAction == MonitorAction.Skip);)
563  {
564  MonitorActivationReason activationReason = default; //this will always be assigned before being used
565 
566  //process the tasks in this order and call HandlerMonitorWakup for each
567 
568  bool CheckActivationReason(ref Task task, MonitorActivationReason testActivationReason)
569  {
570  var taskCompleted = task?.IsCompleted == true;
571  task = null;
572  if (monitorState.NextAction == MonitorAction.Skip)
573  monitorState.NextAction = MonitorAction.Continue;
574  else if (taskCompleted)
575  {
576  activationReason = testActivationReason;
577  return true;
578  }
579  return false;
580  };
581 
582  if (CheckActivationReason(ref activeServerLifetime, MonitorActivationReason.ActiveServerCrashed)
583  || CheckActivationReason(ref inactiveServerLifetime, MonitorActivationReason.InactiveServerCrashed)
584  || CheckActivationReason(ref activeServerReboot, MonitorActivationReason.ActiveServerRebooted)
585  || CheckActivationReason(ref inactiveServerReboot, MonitorActivationReason.InactiveServerRebooted)
586  || CheckActivationReason(ref inactiveServerStartup, MonitorActivationReason.InactiveServerStartupComplete)
587  || CheckActivationReason(ref newDmbAvailable, MonitorActivationReason.NewDmbAvailable)
588  || CheckActivationReason(ref activeLaunchParametersChanged, MonitorActivationReason.ActiveLaunchParametersUpdated))
589  await HandlerMonitorWakeup(activationReason, monitorState, cancellationToken).ConfigureAwait(false);
590  else
591  moreActivationsToProcess = false;
592  }
593 
594  //writeback alphaServer and bravoServer from monitor state in case they changesd
595  alphaServer = AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
596  bravoServer = !AlphaIsActive ? monitorState.ActiveServer : monitorState.InactiveServer;
597  }
598 
599  //full reboot required
600  if (monitorState.NextAction == MonitorAction.Restart)
601  {
602  logger.LogDebug("Next state action is to restart");
603  DisposeAndNullControllers();
604  chatTask = chat.SendWatchdogMessage("Restarting entirely due to complications...", cancellationToken);
605 
606  for (var retryAttempts = 1; monitorState.NextAction == MonitorAction.Restart; ++retryAttempts)
607  {
608  Exception launchException = null;
609  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
610  try
611  {
612  //use LaunchImplNoLock without announcements or restarting the monitor
613  await LaunchImplNoLock(false, false, null, cancellationToken).ConfigureAwait(false);
614  if (Running)
615  {
616  logger.LogDebug("Relaunch successful, resetting monitor state...");
617  monitorState = new MonitorState(); //clean the slate and continue
618  }
619  }
620  catch (OperationCanceledException)
621  {
622  throw;
623  }
624  catch (Exception e)
625  {
626  launchException = e;
627  }
628 
629  await chatTask.ConfigureAwait(false);
630  if (!Running)
631  {
632  if (launchException == null)
633  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}", retryAttempts);
634  else
635  logger.LogWarning("Failed to automatically restart the watchdog! Attempt: {0}, Exception: {1}", retryAttempts, launchException);
636  var retryDelay = Math.Min(Math.Pow(2, retryAttempts), 3600); //max of one hour, increasing by a power of 2 each time
637  chatTask = chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Failed to restart watchdog (Attempt: {0}), retrying in {1} seconds...", retryAttempts, retryDelay), cancellationToken);
638  await Task.WhenAll(asyncDelayer.Delay(TimeSpan.FromSeconds(retryDelay), cancellationToken), chatTask).ConfigureAwait(false);
639  }
640  }
641  }
642  }
643  catch (OperationCanceledException)
644  {
645  logger.LogDebug("Monitor cancelled");
646  break;
647  }
648  catch (Exception e)
649  {
650  //really, this should NEVER happen
651  logger.LogError("Monitor crashed! Iteration: {0}, State: {1}, Exception: {2}", iteration, JsonConvert.SerializeObject(monitorState), e);
652  await chat.SendWatchdogMessage(String.Format(CultureInfo.InvariantCulture, "Monitor crashed, this should NEVER happen! Please report this, full details in logs! Restarting monitor... Error: {0}", e.Message), cancellationToken).ConfigureAwait(false);
653  }
654  }
655  logger.LogTrace("Monitor exiting...");
656  }
657 
662  async Task<bool> StopMonitor()
663  {
664  logger.LogTrace("StopMonitor");
665  if (monitorTask == null)
666  return false;
667  monitorCts.Cancel();
668  await monitorTask.ConfigureAwait(false);
669  monitorCts.Dispose();
670  monitorTask = null;
671  return true;
672  }
673 
674 
676  public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
677  {
678  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
679  {
680  if (launchParameters.Match(ActiveLaunchParameters))
681  return;
682  ActiveLaunchParameters = launchParameters;
683  if (Running)
684  //queue an update
685  activeParametersUpdated.TrySetResult(null);
686  }
687  }
688 
697  async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
698  {
699  logger.LogTrace("Begin LaunchNoLock");
700 
701  if (Running)
702  throw new JobException("Watchdog already running!");
703 
704  Task chatTask;
705  //this is necessary, the monitor could be in it's sleep loop trying to restart, if so cancel THAT monitor and start our own with blackjack and hookers
706  if (startMonitor && await StopMonitor().ConfigureAwait(false))
707  chatTask = chat.SendWatchdogMessage("Automatic retry sequence cancelled by manual launch. Restarting...", cancellationToken);
708  else if (announce)
709  //simple announce
710  chatTask = chat.SendWatchdogMessage(reattachInfo == null ? "Starting..." : "Reattaching...", cancellationToken);
711  else
712  //no announce
713  chatTask = Task.CompletedTask;
714 
715  //since neither server is running, this is safe to do
716  LastLaunchParameters = ActiveLaunchParameters;
717 
718  //for when we call ourself and want to not catch thrown exceptions
719  var ignoreNestedException = false;
720  try
721  {
722  //good ole sanity, should never fucking trigger but i don't trust myself even though I should
723  if (alphaServer != null || bravoServer != null)
724  throw new InvalidOperationException("Entered LaunchNoLock with one or more of the servers not being null!");
725 
726  //don't need a new dmb if reattaching
727  var doesntNeedNewDmb = reattachInfo?.Alpha != null && reattachInfo?.Bravo != null;
728  var dmbToUse = doesntNeedNewDmb ? null : dmbFactory.LockNextDmb(2);
729 
730  //if this try catches something, both servers are killed
731  try
732  {
733  //start the alpha server task, either by launch a new process or attaching to an existing one
734  //The tasks returned are mainly for writing interop files to the directories among other things and should generally never fail
735  //The tasks pertaining to server startup times are in the ISessionControllers
736  Task<ISessionController> alphaServerTask;
737  if (!doesntNeedNewDmb)
738  alphaServerTask = sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, true, true, false, cancellationToken);
739  else
740  alphaServerTask = sessionControllerFactory.Reattach(reattachInfo.Alpha, cancellationToken);
741 
742  //retrieve the session controller
743  var startTime = DateTimeOffset.Now;
744  alphaServer = await alphaServerTask.ConfigureAwait(false);
745  //failed reattaches will return null
746  alphaServer?.SetHighPriority();
747 
748  //extra delay for total ordering
749  var now = DateTimeOffset.Now;
750  var delay = now - startTime;
751 
752  //definitely not if reattaching though
753  if (reattachInfo == null && delay.TotalSeconds < AlphaBravoStartupSeperationInterval)
754  await asyncDelayer.Delay(startTime.AddSeconds(AlphaBravoStartupSeperationInterval) - now, cancellationToken).ConfigureAwait(false);
755 
756  //now bring bravo up
757  if (!doesntNeedNewDmb)
758  bravoServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbToUse, null, false, false, false, cancellationToken).ConfigureAwait(false);
759  else
760  bravoServer = await sessionControllerFactory.Reattach(reattachInfo.Bravo, cancellationToken).ConfigureAwait(false);
761  //failed reattaches will return null
762  bravoServer?.SetHighPriority();
763 
764  //possiblity of null servers due to failed reattaches
765  if (alphaServer == null || bravoServer == null)
766  {
767  await chatTask.ConfigureAwait(false);
768  var bothServersDead = alphaServer == null && bravoServer == null;
769  if (bothServersDead
770  || (alphaServer == null && reattachInfo.AlphaIsActive)
771  || (bravoServer == null && !reattachInfo.AlphaIsActive))
772  {
773  //we lost the active server, just restart entirely
774  DisposeAndNullControllers();
775  const string FailReattachMessage = "Unable to properly reattach to active server! Restarting...";
776  logger.LogWarning(FailReattachMessage);
777  logger.LogDebug(bothServersDead ? "Also could not reattach to inactive server!" : "Inactive server was reattached successfully!");
778  chatTask = chat.SendWatchdogMessage(FailReattachMessage, cancellationToken);
779  ignoreNestedException = true;
780  await LaunchImplNoLock(true, false, null, cancellationToken).ConfigureAwait(false);
781  return;
782  }
783 
784  //we still have the active server but the other one is dead to us, hand it off to the monitor to restart
785  const string InactiveReattachFailureMessage = "Unable to reattach to inactive server. Leaving for monitor to reboot...";
786  chatTask = chat.SendWatchdogMessage(InactiveReattachFailureMessage, cancellationToken);
787  logger.LogWarning(InactiveReattachFailureMessage);
788 
789  if (reattachInfo.AlphaIsActive)
790  bravoServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Bravo.Dmb);
791  else
792  alphaServer = sessionControllerFactory.CreateDeadSession(reattachInfo.Alpha.Dmb);
793  }
794 
795  //throws a JobException if something went wrong with a launch
796  //Dead sessions won't trigger this
797  async Task<LaunchResult> CheckLaunch(ISessionController controller, string serverName)
798  {
799  var launch = await controller.LaunchResult.ConfigureAwait(false);
800  if (launch.ExitCode.HasValue)
801  //you killed us ray...
802  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server failed to start: {0}", launch.ToString(), serverName));
803  if (!launch.StartupTime.HasValue)
804  throw new JobException(String.Format(CultureInfo.InvariantCulture, "{1} server timed out on startup: {0}s", launch.ToString(), ActiveLaunchParameters.StartupTimeout.Value));
805  return launch;
806  }
807 
808  var alphaLrt = CheckLaunch(alphaServer, "Alpha");
809  var bravoLrt = CheckLaunch(bravoServer, "Bravo");
810  //this task completes when both serers have finished booting
811  var allTask = Task.WhenAll(alphaLrt, bravoLrt);
812 
813  //don't forget about the cancellationToken
814  var cancelTcs = new TaskCompletionSource<object>();
815  using (cancellationToken.Register(() => cancelTcs.SetCanceled()))
816  await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false);
817  cancellationToken.ThrowIfCancellationRequested();
818 
819  await allTask.ConfigureAwait(false);
820 
821  //both servers are now running, alpha is the active server(unless reattach), huzzah
822  AlphaIsActive = reattachInfo?.AlphaIsActive ?? true;
823 
824  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
825  activeServer.EnableCustomChatCommands();
826  activeServer.ClosePortOnReboot = true;
827 
828  logger.LogInformation("Launched servers successfully");
829  Running = true;
830 
831  if (startMonitor)
832  {
833  monitorCts = new CancellationTokenSource();
834  monitorTask = MonitorLifetimes(monitorCts.Token);
835  }
836  }
837  catch
838  {
839  if (dmbToUse != null)
840  {
841  //we locked 2 dmbs
842  if (bravoServer == null)
843  {
844  //bravo didn't get control of his
845  dmbToUse.Dispose();
846  if (alphaServer == null)
847  //alpha didn't get control of his
848  dmbToUse.Dispose();
849  }
850  }
851  else if (doesntNeedNewDmb)
852  //we have reattachInfo
853  if (bravoServer == null)
854  {
855  //bravo didn't get control of his
856  reattachInfo.Bravo?.Dmb.Dispose();
857  if (alphaServer == null)
858  //alpha didn't get control of his
859  reattachInfo.Alpha?.Dmb.Dispose();
860  }
861  //kill the controllers
862  DisposeAndNullControllers();
863  throw;
864  }
865  }
866  catch (Exception e)
867  {
868  //don't try to send chat tasks or warning logs if were suppressing exceptions or cancelled
869  if (!ignoreNestedException && !cancellationToken.IsCancellationRequested)
870  {
871  var originalChatTask = chatTask;
872  async Task ChainChatTaskWithErrorMessage()
873  {
874  await originalChatTask.ConfigureAwait(false);
875  await chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false);
876  }
877  chatTask = ChainChatTaskWithErrorMessage();
878  logger.LogWarning("Failed to start watchdog: {0}", e.ToString());
879  }
880  throw;
881  }
882  finally
883  {
884  //finish the chat task that's in flight
885  try
886  {
887  await chatTask.ConfigureAwait(false);
888  }
889  catch (OperationCanceledException) { }
890  }
891  }
892 
894  public async Task Launch(CancellationToken cancellationToken)
895  {
896  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
897  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
898  }
899 
901  public async Task ResetRebootState(CancellationToken cancellationToken)
902  {
903  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
904  {
905  if (!Running)
906  return;
907  var toClear = AlphaIsActive ? alphaServer : bravoServer;
908  if (toClear != null)
909  toClear.ResetRebootState();
910  }
911  }
912 
914  public async Task Restart(bool graceful, CancellationToken cancellationToken)
915  {
916  logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
917  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
918  {
919  if (!graceful || !Running)
920  {
921  Task chatTask;
922  if (Running)
923  {
924  chatTask = chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken);
925  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
926  }
927  else
928  chatTask = Task.CompletedTask;
929  await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
930  await chatTask.ConfigureAwait(false);
931  }
932  var toReboot = AlphaIsActive ? alphaServer : bravoServer;
933  if (toReboot != null)
934  {
935  if (!await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false))
936  logger.LogWarning("Unable to send reboot state change event!");
937  }
938  }
939  }
940 
942  public async Task Terminate(bool graceful, CancellationToken cancellationToken)
943  {
944  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
945  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
946  }
947 
949  public async Task StartAsync(CancellationToken cancellationToken)
950  {
951  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
952  if (!autoStart && reattachInfo == null)
953  return;
954 
955  long? adminUserId = null;
956 
957  await databaseContextFactory.UseContext(async db => adminUserId = await db.Users
958  .Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant())
959  .Select(x => x.Id)
960  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
961  var job = new Models.Job
962  {
963  StartedBy = new Models.User
964  {
965  Id = adminUserId.Value
966  },
967  Instance = new Models.Instance
968  {
969  Id = instance.Id
970  },
971  Description = "Instance startup watchdog launch",
972  CancelRight = (ulong)DreamDaemonRights.Shutdown,
973  CancelRightsType = RightsType.DreamDaemon
974  };
975  await jobManager.RegisterOperation(job, async (j, databaseContext, progressFunction, ct) =>
976  {
977  using (await SemaphoreSlimContext.Lock(semaphore, ct).ConfigureAwait(false))
978  await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
979  }, cancellationToken).ConfigureAwait(false);
980  }
981 
983  public async Task StopAsync(CancellationToken cancellationToken)
984  {
985  if (releaseServers && Running)
986  {
987  await StopMonitor().ConfigureAwait(false);
988 
989  var reattachInformation = new WatchdogReattachInformation
990  {
991  AlphaIsActive = AlphaIsActive
992  };
993  reattachInformation.Alpha = alphaServer?.Release();
994  reattachInformation.Bravo = bravoServer?.Release();
995  await reattachInfoHandler.Save(reattachInformation, cancellationToken).ConfigureAwait(false);
996  }
997  await Terminate(false, cancellationToken).ConfigureAwait(false);
998  }
999 
1001  public async Task<bool> HandleEvent(EventType eventType, IEnumerable<string> parameters, CancellationToken cancellationToken)
1002  {
1003  string results;
1004  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1005  {
1006  if (!Running)
1007  return true;
1008 
1009  var builder = new StringBuilder(Constants.DMTopicEvent);
1010  builder.Append('&');
1011  var notification = new EventNotification
1012  {
1013  Type = eventType,
1014  Parameters = parameters
1015  };
1016  var json = JsonConvert.SerializeObject(notification);
1017  builder.Append(byondTopicSender.SanitizeString(Constants.DMParameterData));
1018  builder.Append('=');
1019  builder.Append(byondTopicSender.SanitizeString(json));
1020 
1021  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1022  results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
1023  }
1024 
1025  if (results == Constants.DMResponseSuccess)
1026  return true;
1027 
1028  List<Response> responses;
1029  try
1030  {
1031  responses = JsonConvert.DeserializeObject<List<Response>>(results);
1032  }
1033  catch
1034  {
1035  logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
1036  return true;
1037  }
1038 
1039  await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
1040 
1041  return true;
1042  }
1043 
1045  public async Task<string> HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
1046  {
1047  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1048  {
1049  if (!Running)
1050  return "ERROR: Server offline!";
1051 
1052  var commandObject = new ChatCommand
1053  {
1054  Command = commandName,
1055  Params = arguments,
1056  User = sender
1057  };
1058 
1059  var json = JsonConvert.SerializeObject(commandObject, new JsonSerializerSettings
1060  {
1061  ContractResolver = new CamelCasePropertyNamesContractResolver()
1062  });
1063 
1064  var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json));
1065 
1066  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1067  return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
1068  }
1069  }
1070 
1072  public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
1073  {
1074  releaseServers = true;
1075  if (Running)
1076  await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false);
1077  }
1078  }
1079 }
bool InactiveServerCritFail
If the inactive server is in an unrecoverable state
Definition: MonitorState.cs:18
Manages the runtime of Jobs
Definition: IJobManager.cs:12
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Watchdog
Definition: Watchdog.cs:71
-
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
Definition: Watchdog.cs:901
+
async Task Launch(CancellationToken cancellationToken)
Start the IWatchdog
Definition: Watchdog.cs:894
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:101
@@ -107,10 +107,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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:949
+
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Definition: Watchdog.cs:942
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:921
+
async Task Restart(bool graceful, CancellationToken cancellationToken)
Restarts the watchdog
Definition: Watchdog.cs:914
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 ...
@@ -125,13 +125,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Factory for scoping usage of IDatabaseContexts. Meant for use by Components
-
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:704
+
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:697
EventType
Types of events. Mirror in tgs.dm
Definition: EventType.cs:6
Task monitorTask
The Task running the monitor loop
Definition: Watchdog.cs:126
-
async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
Changes the ActiveLaunchParameters. If currently Running triggers a graceful restart ...
Definition: Watchdog.cs:683
-
async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
Definition: Watchdog.cs:1079
+
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
For managing connected chat services
Definition: IChat.cs:13
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
@@ -141,7 +141,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IEventConsumer eventConsumer
The IEventConsumer for the Watchdog
Definition: Watchdog.cs:81
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:1052
+
async Task< string > HandleChatCommand(string commandName, string arguments, Chat.User sender, CancellationToken cancellationToken)
Definition: Watchdog.cs:1045
void DisposeAndNullControllers()
Call IDisposable.Dispose on alphaServer and bravoServer and set them to
Definition: Watchdog.cs:213
@@ -151,15 +151,15 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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
-
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
Definition: Watchdog.cs:669
+
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
Definition: Watchdog.cs:662
-
async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
Definition: Watchdog.cs:908
+
async Task ResetRebootState(CancellationToken cancellationToken)
Cancels pending graceful actions
Definition: Watchdog.cs:901
ReattachInformation Alpha
ReattachInformation for the Alpha session
-
async Task MonitorLifetimes(CancellationToken cancellationToken)
The loop that watches the watchdog
Definition: Watchdog.cs:506
+
async Task MonitorLifetimes(CancellationToken cancellationToken)
The loop that watches the watchdog
Definition: Watchdog.cs:499
readonly ISessionControllerFactory sessionControllerFactory
The ISessionControllerFactory for the Watchdog
Definition: Watchdog.cs:56
-
async Task StopAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:990
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:983
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
@@ -176,11 +176,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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:1008
+
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
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:956
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:949
ISessionController alphaServer
Server designation alpha
Definition: Watchdog.cs:136
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
diff --git a/_configuration_controller_8cs_source.html b/_configuration_controller_8cs_source.html index 54dd0bc033..b1b7b44e70 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  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 }
ConfigurationRights
Rights for Models.ConfigurationFile
diff --git a/_database_context_8cs_source.html b/_database_context_8cs_source.html index a520b8de9c..e12946dd96 100644 --- a/_database_context_8cs_source.html +++ b/_database_context_8cs_source.html @@ -92,7 +92,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseContext.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using System;
5 using System.Linq;
6 using System.Threading;
7 using System.Threading.Tasks;
9 
10 namespace Tgstation.Server.Host.Models
11 {
13  abstract class DatabaseContext<TParentContext> : DbContext, IDatabaseContext where TParentContext : DbContext
14  {
16  public DbSet<User> Users { get; set; }
17 
19  public DbSet<Instance> Instances { get; set; }
20 
22  public DbSet<CompileJob> CompileJobs { get; set; }
23 
25  public DbSet<RevisionInformation> RevisionInformations { get; set; }
26 
28  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
29 
31  public DbSet<ChatBot> ChatBots { get; set; }
32 
34  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
35 
37  public DbSet<RepositorySettings> RepositorySettings { get; set; }
38 
40  public DbSet<InstanceUser> InstanceUsers { get; set; }
41 
43  public DbSet<ChatChannel> ChatChannels { get; set; }
44 
46  public DbSet<Job> Jobs { get; set; }
47 
49  public DbSet<ReattachInformation> ReattachInformations { get; set; }
50 
52  public DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
53 
57  public DbSet<TestMerge> TestMerges { get; set; }
58 
62  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
63 
67  protected ILogger Logger { get; }
68 
73 
78 
86  public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
87  {
88  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
89  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
90  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
91  }
92 
94  protected override void OnModelCreating(ModelBuilder modelBuilder)
95  {
96  Logger.LogTrace("Building entity framework context...");
97  base.OnModelCreating(modelBuilder);
98 
99  var userModel = modelBuilder.Entity<User>();
100  userModel.HasIndex(x => x.CanonicalName).IsUnique();
101  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
102 
103  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
104 
105  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.Cascade);
106 
107  var revInfo = modelBuilder.Entity<RevisionInformation>();
108  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
109  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
110  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Restrict);
111  revInfo.HasIndex(x => x.CommitSha).IsUnique();
112 
113  modelBuilder.Entity<CompileJob>().HasIndex(x => x.DirectoryName);
114 
115  modelBuilder.Entity<Job>().HasOne<CompileJob>().WithOne(x => x.Job).OnDelete(DeleteBehavior.Restrict);
116 
117  var chatChannel = modelBuilder.Entity<ChatChannel>();
118  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
119  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
120  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
121 
122  modelBuilder.Entity<ChatBot>().HasIndex(x => x.Name).IsUnique();
123 
124  var instanceModel = modelBuilder.Entity<Instance>();
125  instanceModel.HasIndex(x => x.Path).IsUnique();
126  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
127  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
128  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
129  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
130  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
131  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
132  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
133  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
134  }
135 
137  public async Task Initialize(CancellationToken cancellationToken)
138  {
140  {
141  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
142  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
143  }
144 
145  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
146  var wasEmpty = !migrations.Any();
147 
148  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
149  {
150  Logger.LogInformation("Migrating database...");
151  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
152  }
153  else
154  Logger.LogDebug("No migrations to apply.");
155 
156  wasEmpty |= (await Users.CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
157 
158  if (wasEmpty)
159  {
160  Logger.LogInformation("Seeding database...");
161  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
162  }
164  {
165  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
166  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
167  }
168  }
169 
171  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
172  }
173 }
+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 }
+ @@ -102,19 +103,20 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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)
+
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_context_factory_8cs_source.html b/_database_context_factory_8cs_source.html index 881b5085cb..cd109556f1 100644 --- a/_database_context_factory_8cs_source.html +++ b/_database_context_factory_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseContextFactory.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.DependencyInjection;
2 using System;
3 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
10  {
14  readonly IServiceScopeFactory scopeFactory;
15 
20  public DatabaseContextFactory(IServiceScopeFactory scopeFactory)
21  {
22  this.scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
23 
24  using (var scope = scopeFactory.CreateScope())
25  scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
26  }
27 
29  public async Task UseContext(Func<IDatabaseContext, Task> operation)
30  {
31  if (operation == null)
32  throw new ArgumentNullException(nameof(operation));
33 
34  using (var scope = scopeFactory.CreateScope())
35  await operation(scope.ServiceProvider.GetRequiredService<IDatabaseContext>()).ConfigureAwait(false);
36  }
37  }
38 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.DependencyInjection;
2 using System;
3 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Core
7 {
10  {
14  readonly IServiceScopeFactory scopeFactory;
15 
20  public DatabaseContextFactory(IServiceScopeFactory scopeFactory)
21  {
22  this.scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
23 
24  using (var scope = scopeFactory.CreateScope())
25  scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
26  }
27 
29  public async Task UseContext(Func<IDatabaseContext, Task> operation)
30  {
31  if (operation == null)
32  throw new ArgumentNullException(nameof(operation));
33 
34  using (var scope = scopeFactory.CreateScope())
35  await operation(scope.ServiceProvider.GetRequiredService<IDatabaseContext>()).ConfigureAwait(false);
36  }
37  }
38 }
diff --git a/_database_seeder_8cs_source.html b/_database_seeder_8cs_source.html index 9397fae594..a9694cc313 100644 --- a/_database_seeder_8cs_source.html +++ b/_database_seeder_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DatabaseSeeder.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
8 
9 namespace Tgstation.Server.Host.Models
10 {
13  {
18 
23  public DatabaseSeeder(ICryptographySuite cryptographySuite) => this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
24 
29  void SeedAdminUser(IDatabaseContext databaseContext)
30  {
31  var admin = new User
32  {
34  CreatedAt = DateTimeOffset.Now,
36  Name = Api.Models.User.AdminName,
37  CanonicalName = Api.Models.User.AdminName.ToUpperInvariant(),
38  Enabled = true,
39  };
40  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, true);
41  databaseContext.Users.Add(admin);
42  }
43 
45  public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
46  {
47  SeedAdminUser(databaseContext);
48  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
49  }
50 
52  public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
53  {
54  var admin = await databaseContext.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
55  if (admin == default)
56  SeedAdminUser(databaseContext);
57  else
58  {
59  admin.Enabled = true;
60  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, false);
61  }
62 
63  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
64  }
65  }
66 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Linq;
4 using System.Threading;
5 using System.Threading.Tasks;
8 
9 namespace Tgstation.Server.Host.Models
10 {
13  {
18 
23  public DatabaseSeeder(ICryptographySuite cryptographySuite) => this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
24 
29  void SeedAdminUser(IDatabaseContext databaseContext)
30  {
31  var admin = new User
32  {
34  CreatedAt = DateTimeOffset.Now,
36  Name = Api.Models.User.AdminName,
37  CanonicalName = Api.Models.User.AdminName.ToUpperInvariant(),
38  Enabled = true,
39  };
40  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, true);
41  databaseContext.Users.Add(admin);
42  }
43 
45  public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
46  {
47  SeedAdminUser(databaseContext);
48  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
49  }
50 
52  public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
53  {
54  var admin = await databaseContext.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
55  if (admin == default)
56  SeedAdminUser(databaseContext);
57  else
58  {
59  admin.Enabled = true;
60  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, false);
61  }
62 
63  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
64  }
65  }
66 }
async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Changes the admin password in IDatabaseContext back to it&#39;s default and enables the account ...
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the DatabaseContext<TParentContext>
@@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
For initially seeding a database
-
DbSet< User > Users
The Users in the IDatabaseContext
+
DbSet< User > Users
The Users in the IDatabaseContext
AdministrationRights
Rights for Models.Administration
async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Initially seed a given databaseContext
Contains various cryptographic functions
diff --git a/_discord_provider_8cs_source.html b/_discord_provider_8cs_source.html index d048c2bbe2..1ca53128b2 100644 --- a/_discord_provider_8cs_source.html +++ b/_discord_provider_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DiscordProvider.cs
-Go to the documentation of this file.
1 using Discord;
2 using Discord.WebSocket;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Components.Chat.Providers
12 {
16  sealed class DiscordProvider : Provider
17  {
19  public override bool Connected => client.ConnectionState == ConnectionState.Connected;
20 
22  public override string BotMention
23  {
24  get
25  {
26  if (!Connected)
27  throw new InvalidOperationException("Provider not connected");
28  return NormalizeMention(client.CurrentUser.Mention);
29  }
30  }
31 
35  readonly ILogger<DiscordProvider> logger;
36 
40  readonly DiscordSocketClient client;
41 
45  readonly string botToken;
46 
50  readonly List<ulong> mappedChannels;
51 
57  static string NormalizeMention(string fromDiscord) => fromDiscord.Replace("!", "", StringComparison.Ordinal);
58 
64  public DiscordProvider(ILogger<DiscordProvider> logger, string botToken)
65  {
66  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
68  client = new DiscordSocketClient();
69  client.MessageReceived += Client_MessageReceived;
70  mappedChannels = new List<ulong>();
71  }
72 
74  public override void Dispose() => client.Dispose();
75 
81  Task Client_MessageReceived(SocketMessage e)
82  {
83  if (e.Author.Id == client.CurrentUser.Id)
84  return Task.CompletedTask;
85 
86  var pm = e.Channel is IPrivateChannel;
87 
88  if (!pm && !mappedChannels.Contains(e.Channel.Id))
89  return e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id) ? SendMessage(e.Channel.Id, "I do not respond to this channel!", default) : Task.CompletedTask;
90 
91  var result = new Message
92  {
93  Content = e.Content,
94  User = new User
95  {
96  RealId = e.Author.Id,
97  Channel = new Channel
98  {
99  RealId = e.Channel.Id,
100  IsPrivate = 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  IsAdmin = channel.IsAdminChannel == true,
192  ConnectionName = discordChannel.Guild.Name,
193  FriendlyName = discordChannel.Name,
194  IsPrivate = 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 }
+Go to the documentation of this file.
1 using Discord;
2 using Discord.WebSocket;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 
11 namespace Tgstation.Server.Host.Components.Chat.Providers
12 {
16  sealed class DiscordProvider : Provider
17  {
19  public override bool Connected => client.ConnectionState == ConnectionState.Connected;
20 
22  public override string BotMention
23  {
24  get
25  {
26  if (!Connected)
27  throw new InvalidOperationException("Provider not connected");
28  return NormalizeMention(client.CurrentUser.Mention);
29  }
30  }
31 
35  readonly ILogger<DiscordProvider> logger;
36 
40  readonly DiscordSocketClient client;
41 
45  readonly string botToken;
46 
50  readonly List<ulong> mappedChannels;
51 
57  static string NormalizeMention(string fromDiscord) => fromDiscord.Replace("!", "", StringComparison.Ordinal);
58 
64  public DiscordProvider(ILogger<DiscordProvider> logger, string botToken)
65  {
66  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  this.botToken = botToken ?? throw new ArgumentNullException(nameof(botToken));
68  client = new DiscordSocketClient();
69  client.MessageReceived += Client_MessageReceived;
70  mappedChannels = new List<ulong>();
71  }
72 
74  public override void Dispose() => client.Dispose();
75 
81  Task Client_MessageReceived(SocketMessage e)
82  {
83  if (e.Author.Id == client.CurrentUser.Id)
84  return Task.CompletedTask;
85 
86  var pm = e.Channel is IPrivateChannel;
87 
88  if (!pm && !mappedChannels.Contains(e.Channel.Id))
89  return e.MentionedUsers.Any(x => x.Id == client.CurrentUser.Id) ? SendMessage(e.Channel.Id, "I do not respond to this channel!", default) : Task.CompletedTask;
90 
91  var result = new Message
92  {
93  Content = e.Content,
94  User = new User
95  {
96  RealId = e.Author.Id,
97  Channel = new Channel
98  {
99  RealId = e.Channel.Id,
100  IsPrivateChannel = pm,
101  ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
102  FriendlyName = e.Channel.Name
103  //isAdmin and Tag populated by manager
104  },
105  FriendlyName = e.Author.Username,
106  Mention = NormalizeMention(e.Author.Mention)
107  }
108  };
109  EnqueueMessage(result);
110  return Task.CompletedTask;
111  }
112 
114  public override async Task<bool> Connect(CancellationToken cancellationToken)
115  {
116  if (Connected)
117  return true;
118 
119  try
120  {
121  await client.LoginAsync(TokenType.Bot, botToken, true).ConfigureAwait(false);
122 
123  cancellationToken.ThrowIfCancellationRequested();
124 
125  await client.StartAsync().ConfigureAwait(false);
126 
127  var channelsAvailable = new TaskCompletionSource<object>();
128  client.Ready += () =>
129  {
130  channelsAvailable.TrySetResult(null);
131  return Task.CompletedTask;
132  };
133  using (cancellationToken.Register(() => channelsAvailable.SetCanceled()))
134  await channelsAvailable.Task.ConfigureAwait(false);
135  }
136  catch (OperationCanceledException)
137  {
138  throw;
139  }
140  catch (Exception e)
141  {
142  logger.LogWarning("Error connecting to Discord: {0}", e);
143  return false;
144  }
145 
146  return true;
147  }
148 
150  public override async Task Disconnect(CancellationToken cancellationToken)
151  {
152  if (!Connected)
153  return;
154 
155  try
156  {
157  await client.StopAsync().ConfigureAwait(false);
158  cancellationToken.ThrowIfCancellationRequested();
159  await client.LogoutAsync().ConfigureAwait(false);
160  }
161  catch (OperationCanceledException)
162  {
163  throw;
164  }
165  catch (Exception e)
166  {
167  logger.LogWarning("Error disconnecting from discord: {0}", e);
168  }
169  }
170 
172  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken)
173  {
174  if (channels == null)
175  throw new ArgumentNullException(nameof(channels));
176 
177  if (!Connected)
178  throw new InvalidOperationException("Provider not connected!");
179 
180  Channel GetChannelForChatChannel(ChatChannel channel)
181  {
182  if (!channel.DiscordChannelId.HasValue)
183  throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
184 
185  if (!(client.GetChannel(channel.DiscordChannelId.Value) is ITextChannel discordChannel))
186  return null;
187 
188  return new Channel
189  {
190  RealId = discordChannel.Id,
191  IsAdminChannel = channel.IsAdminChannel == true,
192  ConnectionName = discordChannel.Guild.Name,
193  FriendlyName = discordChannel.Name,
194  IsPrivateChannel = false,
195  Tag = channel.Tag
196  };
197  };
198 
199  var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null).ToList();
200 
201  lock (this)
202  {
203  mappedChannels.Clear();
204  mappedChannels.AddRange(enumerator.Select(x => x.RealId));
205  }
206 
207  return Task.FromResult<IReadOnlyList<Channel>>(enumerator);
208  }
209 
211  public override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
212  {
213  try
214  {
215  var channel = client.GetChannel(channelId) as IMessageChannel;
216  await (channel?.SendMessageAsync(message, false, null, new RequestOptions
217  {
218  CancelToken = cancellationToken
219  }) ?? Task.CompletedTask).ConfigureAwait(false);
220  }
221  catch (OperationCanceledException)
222  {
223  throw;
224  }
225  catch (Exception e)
226  {
227  logger.LogWarning("Error sending discord message: {0}", e);
228  }
229  }
230  }
231 }
Indicates a chat channel
Definition: ChatChannel.cs:8
diff --git a/_dream_daemon_controller_8cs_source.html b/_dream_daemon_controller_8cs_source.html index 6a2c32195a..c1fecb772f 100644 --- a/_dream_daemon_controller_8cs_source.html +++ b/_dream_daemon_controller_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.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)
- +
Manages the runtime of Jobs
Definition: IJobManager.cs:12
@@ -104,7 +104,7 @@ 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
@@ -117,7 +117,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
ModelController<TModel> for managing the DreamDaemon
diff --git a/_dream_maker_controller_8cs_source.html b/_dream_maker_controller_8cs_source.html index 5390f6ad58..7d8a11409d 100644 --- a/_dream_maker_controller_8cs_source.html +++ b/_dream_maker_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
DreamMakerController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.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  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 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
@@ -100,7 +100,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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
@@ -115,7 +115,7 @@ 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
@@ -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/_home_controller_8cs_source.html b/_home_controller_8cs_source.html index 0010048c05..350d36aec0 100644 --- a/_home_controller_8cs_source.html +++ b/_home_controller_8cs_source.html @@ -92,13 +92,13 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
HomeController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Net.Http.Headers;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Controllers
15 {
19  [Route(Routes.Root)]
20  public sealed class HomeController : ApiController
21  {
42 
54  public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, ILogger<HomeController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
55  {
56  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
57  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
58  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
59  this.application = application ?? throw new ArgumentNullException(nameof(application));
60  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
61  }
62 
67  [TgsAuthorize]
68  [HttpGet]
69  public JsonResult Home() => Json(new Api.Models.ServerInformation
70  {
71  Version = application.Version,
72  ApiVersion = ApiHeaders.Version
73  });
74 
80  [HttpPost]
81  public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
82  {
84  return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
85 
86  ISystemIdentity identity;
87  try
88  {
89  //trust the system over the database because a user's name can change while still having the same SID
90  identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
91  }
92  catch (NotImplementedException)
93  {
94  identity = null;
95  }
96  using (identity)
97  {
98  IQueryable<User> query;
99  if (identity == null)
100  query = DatabaseContext.Users.Where(x => x.CanonicalName == ApiHeaders.Username.ToUpperInvariant());
101  else
102  query = DatabaseContext.Users.Where(x => x.SystemIdentifier == identity.Uid);
103  var user = await query.Select(x => new User
104  {
105  Id = x.Id,
106  PasswordHash = x.PasswordHash,
107  Enabled = x.Enabled,
108  Name = x.Name
109  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
110 
111  if (user == null)
112  return Unauthorized();
113 
114  if (identity == null)
115  {
116  var originalHash = user.PasswordHash;
117  if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
118  return Unauthorized();
119  if (user.PasswordHash != originalHash)
120  {
121  var updatedUser = new User
122  {
123  Id = user.Id
124  };
125  DatabaseContext.Users.Attach(updatedUser);
126  updatedUser.PasswordHash = user.PasswordHash;
127  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
128  }
129  }
130  //check if the name changed and updoot accordingly
131  else if (identity.Username != user.Name)
132  {
133  DatabaseContext.Users.Attach(user);
134  user.Name = identity.Username;
135  user.CanonicalName = user.Name.ToUpperInvariant();
136  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
137  }
138 
139  if (!user.Enabled.Value)
140  return Forbid();
141 
142  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
143  if (identity != null)
144  {
145  //expire the identity slightly after the auth token in case of lag
146  var identExpiry = token.ExpiresAt.Value;
147  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
148  identExpiry += TimeSpan.FromSeconds(15);
149  identityCache.CacheSystemIdentity(user, identity, identExpiry);
150  }
151 
152  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
153 
154  return Json(token);
155  }
156  }
157  }
158 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Linq;
6 using System.Net.Http.Headers;
7 using System.Threading;
8 using System.Threading.Tasks;
9 using Tgstation.Server.Api;
13 
14 namespace Tgstation.Server.Host.Controllers
15 {
19  [Route(Routes.Root)]
20  public sealed class HomeController : ApiController
21  {
42 
54  public HomeController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ITokenFactory tokenFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, IApplication application, IIdentityCache identityCache, ILogger<HomeController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
55  {
56  this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
57  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
58  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
59  this.application = application ?? throw new ArgumentNullException(nameof(application));
60  this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
61  }
62 
67  [TgsAuthorize]
68  [HttpGet]
69  public JsonResult Home() => Json(new Api.Models.ServerInformation
70  {
71  Version = application.Version,
72  ApiVersion = ApiHeaders.Version
73  });
74 
80  [HttpPost]
81  public async Task<IActionResult> CreateToken(CancellationToken cancellationToken)
82  {
84  return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
85 
86  ISystemIdentity identity;
87  try
88  {
89  //trust the system over the database because a user's name can change while still having the same SID
90  identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
91  }
92  catch (NotImplementedException)
93  {
94  identity = null;
95  }
96  using (identity)
97  {
98  IQueryable<User> query;
99  if (identity == null)
100  query = DatabaseContext.Users.Where(x => x.CanonicalName == ApiHeaders.Username.ToUpperInvariant());
101  else
102  query = DatabaseContext.Users.Where(x => x.SystemIdentifier == identity.Uid);
103  var user = await query.Select(x => new User
104  {
105  Id = x.Id,
106  PasswordHash = x.PasswordHash,
107  Enabled = x.Enabled,
108  Name = x.Name
109  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
110 
111  if (user == null)
112  return Unauthorized();
113 
114  if (identity == null)
115  {
116  var originalHash = user.PasswordHash;
117  if (!cryptographySuite.CheckUserPassword(user, ApiHeaders.Password))
118  return Unauthorized();
119  if (user.PasswordHash != originalHash)
120  {
121  var updatedUser = new User
122  {
123  Id = user.Id
124  };
125  DatabaseContext.Users.Attach(updatedUser);
126  updatedUser.PasswordHash = user.PasswordHash;
127  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
128  }
129  }
130  //check if the name changed and updoot accordingly
131  else if (identity.Username != user.Name)
132  {
133  DatabaseContext.Users.Attach(user);
134  user.Name = identity.Username;
135  user.CanonicalName = user.Name.ToUpperInvariant();
136  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
137  }
138 
139  if (!user.Enabled.Value)
140  return Forbid();
141 
142  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
143  if (identity != null)
144  {
145  //expire the identity slightly after the auth token in case of lag
146  var identExpiry = token.ExpiresAt.Value;
147  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
148  identExpiry += TimeSpan.FromSeconds(15);
149  identityCache.CacheSystemIdentity(user, identity, identExpiry);
150  }
151 
152  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
153 
154  return Json(token);
155  }
156  }
157  }
158 }
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
@@ -117,7 +117,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController
bool IsTokenAuthentication
If the header uses password or JWT authentication
Definition: ApiHeaders.cs:91
- +
const string Root
The root controller
Definition: Routes.cs:14
diff --git a/_i_database_context_8cs_source.html b/_i_database_context_8cs_source.html index 4a5326b336..01b1bdfc47 100644 --- a/_i_database_context_8cs_source.html +++ b/_i_database_context_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IDatabaseContext.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System.Threading;
3 using System.Threading.Tasks;
4 
5 namespace Tgstation.Server.Host.Models
6 {
10  public interface IDatabaseContext
11  {
15  DbSet<User> Users { get; }
16 
20  DbSet<Instance> Instances { get; }
21 
25  DbSet<InstanceUser> InstanceUsers { get; }
26 
30  DbSet<Job> Jobs { get; }
31 
35  DbSet<CompileJob> CompileJobs { get; }
36 
40  DbSet<RevisionInformation> RevisionInformations { get; }
41 
45  DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
46 
50  DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
51 
55  DbSet<ChatBot> ChatBots { get; set; }
56 
60  DbSet<ChatChannel> ChatChannels { get; set; }
61 
65  DbSet<RepositorySettings> RepositorySettings { get; set; }
66 
70  DbSet<ReattachInformation> ReattachInformations { get; set; }
71 
75  DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
76 
82  Task Save(CancellationToken cancellationToken);
83 
89  Task Initialize(CancellationToken cancellationToken);
90  }
91 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using System;
3 using System.Threading;
4 using System.Threading.Tasks;
5 
6 namespace Tgstation.Server.Host.Models
7 {
11  public interface IDatabaseContext
12  {
16  DbSet<User> Users { get; }
17 
21  DbSet<Instance> Instances { get; }
22 
26  DbSet<InstanceUser> InstanceUsers { get; }
27 
31  DbSet<Job> Jobs { get; }
32 
36  DbSet<CompileJob> CompileJobs { get; }
37 
41  DbSet<RevisionInformation> RevisionInformations { get; }
42 
46  DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
47 
51  DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
52 
56  DbSet<ChatBot> ChatBots { get; set; }
57 
61  DbSet<ChatChannel> ChatChannels { get; set; }
62 
66  DbSet<RepositorySettings> RepositorySettings { get; set; }
67 
71  DbSet<ReattachInformation> ReattachInformations { get; set; }
72 
76  DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
77 
83  Task Save(CancellationToken cancellationToken);
84 
90  Task Initialize(CancellationToken cancellationToken);
91 
98  Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken);
99  }
100 }
diff --git a/_i_database_seeder_8cs_source.html b/_i_database_seeder_8cs_source.html index 47dc63553b..76df6b208e 100644 --- a/_i_database_seeder_8cs_source.html +++ b/_i_database_seeder_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IDatabaseSeeder.cs
-Go to the documentation of this file.
1 using System.Threading;
2 using System.Threading.Tasks;
3 
4 namespace Tgstation.Server.Host.Models
5 {
9  interface IDatabaseSeeder
10  {
17  Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken);
18 
25  Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken);
26  }
27 }
+Go to the documentation of this file.
1 using System.Threading;
2 using System.Threading.Tasks;
3 
4 namespace Tgstation.Server.Host.Models
5 {
9  interface IDatabaseSeeder
10  {
17  Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken);
18 
25  Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken);
26  }
27 }
For initially seeding a database
diff --git a/_i_instance_8cs_source.html b/_i_instance_8cs_source.html index 0ffeb86bcb..d6d5ea30b9 100644 --- a/_i_instance_8cs_source.html +++ b/_i_instance_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IInstance.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.Hosting;
2 using System;
3 using System.Threading;
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 }
diff --git a/_i_job_manager_8cs_source.html b/_i_job_manager_8cs_source.html index b975a50d06..0e448f7036 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  {
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 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
diff --git a/_instance_controller_8cs_source.html b/_instance_controller_8cs_source.html index 85656f8e64..fef5fbbd4f 100644 --- a/_instance_controller_8cs_source.html +++ b/_instance_controller_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.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.IO;
8 using System.Linq;
9 using System.Linq.Expressions;
10 using System.Net;
11 using System.Reflection;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using Tgstation.Server.Api;
19 using Tgstation.Server.Host.IO;
22 
23 namespace Tgstation.Server.Host.Controllers
24 {
28  [Route(Routes.InstanceManager)]
29  public sealed class InstanceController : ModelController<Api.Models.Instance>
30  {
34  const string InstanceAttachFileName = "TGS4_ALLOW_INSTANCE_ATTACH";
35 
36  const string MoveInstanceJobPrefix = "Move instance ID ";
37 
42 
47 
52 
57 
62 
74  public InstanceController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager, IIOManager ioManager, IApplication application, IPlatformIdentifier platformIdentifier, ILogger<InstanceController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
75  {
76  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
77  this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
78  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
79  this.application = application ?? throw new ArgumentNullException(nameof(application));
80  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
81  }
82 
83  void NormalizeModelPath(Api.Models.Instance model, out string absolutePath)
84  {
85  if (model.Path == null)
86  {
87  absolutePath = null;
88  return;
89  }
90  absolutePath = ioManager.ResolvePath(model.Path);
91  if (platformIdentifier.IsWindows)
92  model.Path = absolutePath.ToUpperInvariant();
93  else
94  model.Path = absolutePath;
95  }
96 
97  Models.InstanceUser InstanceAdminUser() => new Models.InstanceUser
98  {
99  ByondRights = (ByondRights)~0U,
106  UserId = AuthenticationContext.User.Id
107  };
108 
110  [TgsAuthorize(InstanceManagerRights.Create)]
111  public override async Task<IActionResult> Create([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
112  {
113  if (model == null)
114  throw new ArgumentNullException(nameof(model));
115 
116  if (String.IsNullOrWhiteSpace(model.Name))
117  return BadRequest(new ErrorMessage { Message = "name must not be empty!" });
118 
119  if (model.Path == null)
120  return BadRequest(new ErrorMessage { Message = "path must not be empty!" });
121 
122  NormalizeModelPath(model, out var rawPath);
123 
124  var localPath = ioManager.ResolvePath(".");
125  NormalizeModelPath(new Api.Models.Instance
126  {
127  Path = localPath
128  }, out var normalizedLocalPath);
129 
130  if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal))
131  return Conflict("Instances cannot be created in the installation directory!");
132 
133 
134  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
135  bool attached = false;
136  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
137  if (!await ioManager.FileExists(ioManager.ConcatPath(model.Path, InstanceAttachFileName), cancellationToken).ConfigureAwait(false))
138  return Conflict(new ErrorMessage { Message = "Path not empty!" });
139  else
140  attached = true;
141 
142  var newInstance = new Models.Instance
143  {
144  ConfigurationType = model.ConfigurationType ?? ConfigurationType.Disallowed,
146  {
147  AllowWebClient = false,
148  AutoStart = false,
149  PrimaryPort = 1337,
150  SecondaryPort = 1338,
151  SecurityLevel = DreamDaemonSecurity.Safe,
152  SoftRestart = false,
153  SoftShutdown = false,
154  StartupTimeout = 20
155  },
157  {
158  ApiValidationPort = 1339,
159  ApiValidationSecurityLevel = DreamDaemonSecurity.Safe
160  },
161  Name = model.Name,
162  Online = false,
163  Path = model.Path,
164  AutoUpdateInterval = model.AutoUpdateInterval ?? 0,
166  {
167  CommitterEmail = "tgstation-server@users.noreply.github.com",
168  CommitterName = application.VersionPrefix,
169  PushTestMergeCommits = false,
170  ShowTestMergeCommitters = false,
171  AutoUpdatesKeepTestMerges = false,
172  AutoUpdatesSynchronize = false
173  },
174  //give this user full privileges on the instance
175  InstanceUsers = new List<Models.InstanceUser>
176  {
177  InstanceAdminUser()
178  }
179  };
180 
181  DatabaseContext.Instances.Add(newInstance);
182  try
183  {
184  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
185 
186  try
187  {
188  //actually reserve it now
189  await ioManager.CreateDirectory(rawPath, cancellationToken).ConfigureAwait(false);
190  await ioManager.DeleteFile(ioManager.ConcatPath(rawPath, InstanceAttachFileName), cancellationToken).ConfigureAwait(false);
191  }
192  catch
193  {
194  //oh shit delete the model
195  DatabaseContext.Instances.Remove(newInstance);
196 
197  await DatabaseContext.Save(default).ConfigureAwait(false);
198  throw;
199  }
200  }
201  catch (IOException e)
202  {
203  return Conflict(new ErrorMessage { Message = e.Message });
204  }
205  catch (DbUpdateException e)
206  {
207  return Conflict(new ErrorMessage { Message = e.Message });
208  }
209 
210  Logger.LogInformation("{0} {1} instance {2}: {3} ({4})", AuthenticationContext.User.Name, attached ? "attached" : "created", newInstance.Name, newInstance.Id, newInstance.Path);
211 
212  var api = newInstance.ToApi();
213  return attached ? (IActionResult)Json(api) : StatusCode((int)HttpStatusCode.Created, api);
214  }
215 
217  [TgsAuthorize(InstanceManagerRights.Delete)]
218  public override async Task<IActionResult> Delete(long id, CancellationToken cancellationToken)
219  {
220  var originalModel = await DatabaseContext.Instances.Where(x => x.Id == id)
221  .Include(x => x.WatchdogReattachInformation)
222  .Include(x => x.WatchdogReattachInformation.Alpha)
223  .Include(x => x.WatchdogReattachInformation.Bravo)
224  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
225  if (originalModel == default)
226  return StatusCode((int)HttpStatusCode.Gone);
227  if (originalModel.Online.Value)
228  return Conflict(new ErrorMessage
229  {
230  Message = "Cannot detach an online instance!"
231  });
232 
233  if (originalModel.WatchdogReattachInformation != null)
234  {
235  DatabaseContext.WatchdogReattachInformations.Remove(originalModel.WatchdogReattachInformation);
236  if (originalModel.WatchdogReattachInformation.Alpha != null)
237  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Alpha);
238  if (originalModel.WatchdogReattachInformation.Bravo != null)
239  DatabaseContext.ReattachInformations.Remove(originalModel.WatchdogReattachInformation.Bravo);
240  }
241 
242  DatabaseContext.Instances.Remove(originalModel);
243 
244  var attachFileName = ioManager.ConcatPath(originalModel.Path, InstanceAttachFileName);
245  await ioManager.WriteAllBytes(attachFileName, Array.Empty<byte>(), default).ConfigureAwait(false);
246  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); //cascades everything
247  return Ok();
248  }
249 
251  [TgsAuthorize(InstanceManagerRights.Relocate | InstanceManagerRights.Rename | InstanceManagerRights.SetAutoUpdate | InstanceManagerRights.SetConfiguration | InstanceManagerRights.SetOnline)]
252  public override async Task<IActionResult> Update([FromBody] Api.Models.Instance model, CancellationToken cancellationToken)
253  {
254  var instanceQuery = DatabaseContext.Instances.Where(x => x.Id == model.Id);
255 
256  var moveJob = await instanceQuery
257  .SelectMany(x => x.Jobs).
258 #pragma warning disable CA1307 // Specify StringComparison
259  Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
260 #pragma warning restore CA1307 // Specify StringComparison
261  .Select(x => new Models.Job
262  {
263  Id = x.Id
264  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
265 
266  if (moveJob != default)
267  //cancel it now
268  await jobManager.CancelJob(moveJob, AuthenticationContext.User, true, cancellationToken).ConfigureAwait(false);
269 
270  var usersInstanceUserTask = instanceQuery.SelectMany(x => x.InstanceUsers).Where(x => x.UserId == AuthenticationContext.User.Id).FirstOrDefaultAsync(cancellationToken);
271 
272  var originalModel = await instanceQuery
273  .Include(x => x.RepositorySettings)
274  .Include(x => x.ChatSettings)
275  .ThenInclude(x => x.Channels)
276  .Include(x => x.DreamDaemonSettings) //need these for onlining
277  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
278  if (originalModel == default(Models.Instance))
279  return StatusCode((int)HttpStatusCode.Gone);
280 
281  var userRights = (InstanceManagerRights)AuthenticationContext.GetRight(RightsType.InstanceManager);
282  bool CheckModified<T>(Expression<Func<Api.Models.Instance, T>> expression, InstanceManagerRights requiredRight)
283  {
284  var memberSelectorExpression = (MemberExpression)expression.Body;
285  var property = (PropertyInfo)memberSelectorExpression.Member;
286 
287  var newVal = property.GetValue(model);
288  if (newVal == null)
289  return false;
290  if (!userRights.HasFlag(requiredRight) && property.GetValue(originalModel) != newVal)
291  return true;
292 
293  property.SetValue(originalModel, newVal);
294  return false;
295  };
296 
297  string originalModelPath = null;
298  string rawPath = null;
299  if (model.Path != null)
300  {
301  NormalizeModelPath(model, out rawPath);
302 
303  if (model.Path != originalModel.Path)
304  {
305  if (!userRights.HasFlag(InstanceManagerRights.Relocate))
306  return Forbid();
307  if (originalModel.Online.Value && model.Online != true)
308  return Conflict(new ErrorMessage { Message = "Cannot relocate an online instance!" });
309 
310  var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
311  if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
312  return Conflict(new ErrorMessage { Message = "Path not empty!" });
313 
314  originalModelPath = originalModel.Path;
315  originalModel.Path = model.Path;
316  }
317  }
318 
319  var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value;
320  var originalOnline = originalModel.Online.Value;
321  var renamed = model.Name != null && originalModel.Name != model.Name;
322 
323  if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate)
324  || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration)
325  || CheckModified(x => x.Name, InstanceManagerRights.Rename)
326  || CheckModified(x => x.Online, InstanceManagerRights.SetOnline))
327  return Forbid();
328 
329  //ensure the current user has write privilege on the instance
330  var usersInstanceUser = await usersInstanceUserTask.ConfigureAwait(false);
331  if (usersInstanceUser == default)
332  {
333  var instanceAdminUser = InstanceAdminUser();
334  instanceAdminUser.InstanceId = originalModel.Id;
335  DatabaseContext.InstanceUsers.Add(instanceAdminUser);
336  }
337  else
338  usersInstanceUser.InstanceUserRights |= InstanceUserRights.WriteUsers;
339 
340  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
341 
342  if (renamed)
343  instanceManager.GetInstance(originalModel).Rename(originalModel.Name);
344 
345  var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart;
346  try
347  {
348  if (originalOnline && model.Online == false)
349  await instanceManager.OfflineInstance(originalModel, AuthenticationContext.User, cancellationToken).ConfigureAwait(false);
350  else if (!originalOnline && model.Online == true)
351  {
352  //force autostart false here because we don't want any long running jobs right now
353  //remember to document this
354  originalModel.DreamDaemonSettings.AutoStart = false;
355  await instanceManager.OnlineInstance(originalModel, cancellationToken).ConfigureAwait(false);
356  }
357  }
358  catch (Exception e)
359  {
360  if(!(e is OperationCanceledException))
361  Logger.LogError("Error changing instance online state! Exception: {0}", e);
362  originalModel.Online = originalOnline;
363  originalModel.DreamDaemonSettings.AutoStart = oldAutoStart;
364  if (originalModelPath != null)
365  originalModel.Path = originalModelPath;
366  await DatabaseContext.Save(default).ConfigureAwait(false);
367  throw;
368  }
369 
370  var api = (AuthenticationContext.GetRight(RightsType.InstanceManager) & (ulong)InstanceManagerRights.Read) != 0 ? originalModel.ToApi() : new Api.Models.Instance
371  {
372  Id = originalModel.Id
373  };
374  if (originalModelPath != null)
375  {
376  var job = new Models.Job
377  {
378  Description = String.Format(CultureInfo.InvariantCulture, MoveInstanceJobPrefix + "{0} from {1} to {2}", originalModel.Id, originalModel.Path, rawPath),
379  Instance = originalModel,
380  CancelRightsType = RightsType.InstanceManager,
381  CancelRight = (ulong)InstanceManagerRights.Relocate,
382  StartedBy = AuthenticationContext.User
383  };
384 
385  await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
386  api.MoveJob = job.ToApi();
387  }
388 
389  if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval)
390  await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false);
391 
392  return Json(api);
393  }
394 
396  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
397  public override async Task<IActionResult> List(CancellationToken cancellationToken)
398  {
399  IQueryable<Models.Instance> query = DatabaseContext.Instances;
401  query = query.Where(x => x.InstanceUsers.Any(y => y.UserId == AuthenticationContext.User.Id)).Where(x => x.InstanceUsers.Any(y => y.AnyRights));
402 
403  var moveJobTasks = query
404  .SelectMany(x => x.Jobs)
405 #pragma warning disable CA1307 // Specify StringComparison
406  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
407 #pragma warning restore CA1307 // Specify StringComparison
408  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
409  .Include(x => x.Instance)
410  .ToListAsync(cancellationToken);
411  var instances = await query.ToListAsync(cancellationToken).ConfigureAwait(false);
412  var apis = instances.Select(x => x.ToApi());
413  var moveJobs = await moveJobTasks.ConfigureAwait(false);
414  foreach(var I in moveJobs)
415  apis.Where(x => x.Id == I.Instance.Id).First().MoveJob = I.ToApi(); //if this .First() fails i will personally murder kevinz000 because I just know he is somehow responsible
416  return Json(apis);
417  }
418 
420  [TgsAuthorize(InstanceManagerRights.List | InstanceManagerRights.Read)]
421  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
422  {
423  var query = DatabaseContext.Instances.Where(x => x.Id == id);
424  var cantList = !AuthenticationContext.User.InstanceManagerRights.Value.HasFlag(InstanceManagerRights.List);
425 
426  if (cantList)
427  query = query.Include(x => x.InstanceUsers);
428 
429  var moveJobTask = query
430  .SelectMany(x => x.Jobs)
431 #pragma warning disable CA1307 // Specify StringComparison
432  .Where(x => !x.StoppedAt.HasValue && x.Description.StartsWith(MoveInstanceJobPrefix))
433 #pragma warning restore CA1307 // Specify StringComparison
434  .Include(x => x.StartedBy).ThenInclude(x => x.CreatedBy)
435  .FirstOrDefaultAsync(cancellationToken);
436  var instance = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
437 
438  if (instance == null)
439  return StatusCode((int)HttpStatusCode.Gone);
440 
441  if (cantList && !instance.InstanceUsers.Any(x => x.UserId == AuthenticationContext.User.Id && x.AnyRights))
442  return Forbid();
443 
444  var api = instance.ToApi();
445  api.MoveJob = (await moveJobTask.ConfigureAwait(false))?.ToApi();
446  return Json(api);
447  }
448  }
449 }
string Name
The name of the User
Definition: User.cs:38
- +
Manages the runtime of Jobs
Definition: IJobManager.cs:12
@@ -105,7 +105,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
-
DbSet< ReattachInformation > ReattachInformations
+
DbSet< ReattachInformation > ReattachInformations
InstanceUserRights
Rights for an Models.Instance
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
@@ -117,7 +117,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +
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
@@ -131,8 +131,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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)
RepositoryRights
Rights for a Models.Repository
An ApiController representing a TModel
@@ -147,7 +147,7 @@ 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
-
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)
diff --git a/_instance_manager_8cs_source.html b/_instance_manager_8cs_source.html index 6a6f34db52..75978513e9 100644 --- a/_instance_manager_8cs_source.html +++ b/_instance_manager_8cs_source.html @@ -92,24 +92,26 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
InstanceManager.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components
13 {
16  {
21 
26 
31 
36 
41 
45  readonly ILogger<InstanceManager> logger;
46 
50  readonly Dictionary<long, IInstance> instances;
51 
55  bool disposed;
56 
66  public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, ILogger<InstanceManager> logger)
67  {
68  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
69  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
70  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
71  this.application = application ?? throw new ArgumentNullException(nameof(application));
72  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
73  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
74 
75  instances = new Dictionary<long, IInstance>();
76  }
77 
79  public void Dispose()
80  {
81  lock (this)
82  {
83  if (disposed)
84  return;
85  disposed = true;
86  }
87  foreach (var I in instances)
88  I.Value.Dispose();
89  }
90 
92  public IInstance GetInstance(Models.Instance metadata)
93  {
94  if (metadata == null)
95  throw new ArgumentNullException(nameof(metadata));
96  lock (this)
97  {
98  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
99  throw new InvalidOperationException("Instance not online!");
100  return instance;
101  }
102  }
103 
105  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
106  {
107  if (newPath == null)
108  throw new ArgumentNullException(nameof(newPath));
109  if (instance.Online.Value)
110  throw new InvalidOperationException("Cannot move an online instance!");
111  var oldPath = instance.Path;
112  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
113  await databaseContextFactory.UseContext(db =>
114  {
115  var targetInstance = new Models.Instance
116  {
117  Id = instance.Id
118  };
119  db.Instances.Attach(targetInstance);
120  targetInstance.Path = newPath;
121  return db.Save(cancellationToken);
122  }).ConfigureAwait(false);
123  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
124  }
125 
127  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
128  {
129  if (metadata == null)
130  throw new ArgumentNullException(nameof(metadata));
131  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
132  IInstance instance;
133  lock (this)
134  {
135  if (!instances.TryGetValue(metadata.Id, out instance))
136  throw new InvalidOperationException("Instance not online!");
137  instances.Remove(metadata.Id);
138  }
139  try
140  {
141  //we are the one responsible for cancelling his jobs
142  var tasks = new List<Task>();
143  await databaseContextFactory.UseContext(async db =>
144  {
145  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
146  {
147  Id = x.Id
148  }).ToAsyncEnumerable();
149  await jobs.ForEachAsync(job =>
150  {
151  lock (tasks)
152  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
153  }, cancellationToken).ConfigureAwait(false);
154  }).ConfigureAwait(false);
155 
156  await Task.WhenAll(tasks).ConfigureAwait(false);
157 
158  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
159  }
160  finally
161  {
162  instance.Dispose();
163  }
164  }
165 
167  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
168  {
169  if (metadata == null)
170  throw new ArgumentNullException(nameof(metadata));
171  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
172  var instance = instanceFactory.CreateInstance(metadata);
173  try
174  {
175  lock (this)
176  {
177  if (instances.ContainsKey(metadata.Id))
178  throw new InvalidOperationException("Instance already online!");
179  instances.Add(metadata.Id, instance);
180  }
181  }
182  catch
183  {
184  instance.Dispose();
185  throw;
186  }
187  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
188  }
189 
191  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
192  {
193  try
194  {
195  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
196  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
197  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
198  var dbInstances = databaseContext.Instances.Where(x => x.Online.Value)
199  .Include(x => x.RepositorySettings)
200  .Include(x => x.ChatSettings)
201  .ThenInclude(x => x.Channels)
202  .Include(x => x.DreamDaemonSettings)
203  .ToAsyncEnumerable();
204  var tasks = new List<Task>();
205  await factoryStartup.ConfigureAwait(false);
206  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
207  await Task.WhenAll(tasks).ConfigureAwait(false);
208  logger.LogInformation("Instance manager ready!");
209  application.Ready(null);
210  }
211  catch (OperationCanceledException)
212  {
213  logger.LogInformation("Cancelled instance manager initialization!");
214  }
215  catch (Exception e)
216  {
217  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
218  application.Ready(e);
219  }
220  });
221 
223  public async Task StopAsync(CancellationToken cancellationToken)
224  {
225  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
226  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
227  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
228  }
229  }
230 }
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Hosting;
3 using Microsoft.Extensions.Logging;
4 using System;
5 using System.Collections.Generic;
6 using System.Linq;
7 using System.Threading;
8 using System.Threading.Tasks;
10 using Tgstation.Server.Host.IO;
11 
12 namespace Tgstation.Server.Host.Components
13 {
16  {
21 
26 
31 
36 
41 
45  readonly ILogger<InstanceManager> logger;
46 
50  readonly Dictionary<long, IInstance> instances;
51 
56 
60  bool disposed;
61 
72  public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger<InstanceManager> logger)
73  {
74  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
75  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
76  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
77  this.application = application ?? throw new ArgumentNullException(nameof(application));
78  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
79  if (serverControl == null)
80  throw new ArgumentNullException(nameof(serverControl));
81  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
82 
83  serverControl.RegisterForRestart(this);
84 
85  instances = new Dictionary<long, IInstance>();
86  }
87 
89  public void Dispose()
90  {
91  lock (this)
92  {
93  if (disposed)
94  return;
95  disposed = true;
96  }
97  foreach (var I in instances)
98  I.Value.Dispose();
99  }
100 
102  public IInstance GetInstance(Models.Instance metadata)
103  {
104  if (metadata == null)
105  throw new ArgumentNullException(nameof(metadata));
106  lock (this)
107  {
108  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
109  throw new InvalidOperationException("Instance not online!");
110  return instance;
111  }
112  }
113 
115  public async Task MoveInstance(Models.Instance instance, string newPath, CancellationToken cancellationToken)
116  {
117  if (newPath == null)
118  throw new ArgumentNullException(nameof(newPath));
119  if (instance.Online.Value)
120  throw new InvalidOperationException("Cannot move an online instance!");
121  var oldPath = instance.Path;
122  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
123  await databaseContextFactory.UseContext(db =>
124  {
125  var targetInstance = new Models.Instance
126  {
127  Id = instance.Id
128  };
129  db.Instances.Attach(targetInstance);
130  targetInstance.Path = newPath;
131  return db.Save(cancellationToken);
132  }).ConfigureAwait(false);
133  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
134  }
135 
137  public async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
138  {
139  if (metadata == null)
140  throw new ArgumentNullException(nameof(metadata));
141  logger.LogInformation("Offlining instance ID {0}", metadata.Id);
142  IInstance instance;
143  lock (this)
144  {
145  if (!instances.TryGetValue(metadata.Id, out instance))
146  throw new InvalidOperationException("Instance not online!");
147  instances.Remove(metadata.Id);
148  }
149  try
150  {
151  //we are the one responsible for cancelling his jobs
152  var tasks = new List<Task>();
153  await databaseContextFactory.UseContext(async db =>
154  {
155  var jobs = db.Jobs.Where(x => x.Instance.Id == metadata.Id).Select(x => new Models.Job
156  {
157  Id = x.Id
158  }).ToAsyncEnumerable();
159  await jobs.ForEachAsync(job =>
160  {
161  lock (tasks)
162  tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken));
163  }, cancellationToken).ConfigureAwait(false);
164  }).ConfigureAwait(false);
165 
166  await Task.WhenAll(tasks).ConfigureAwait(false);
167 
168  await instance.StopAsync(cancellationToken).ConfigureAwait(false);
169  }
170  finally
171  {
172  instance.Dispose();
173  }
174  }
175 
177  public async Task OnlineInstance(Models.Instance metadata, CancellationToken cancellationToken)
178  {
179  if (metadata == null)
180  throw new ArgumentNullException(nameof(metadata));
181  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
182  var instance = instanceFactory.CreateInstance(metadata);
183  try
184  {
185  lock (this)
186  {
187  if (instances.ContainsKey(metadata.Id))
188  throw new InvalidOperationException("Instance already online!");
189  instances.Add(metadata.Id, instance);
190  }
191  }
192  catch
193  {
194  instance.Dispose();
195  throw;
196  }
197  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
198  }
199 
201  public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
202  {
203  try
204  {
205  var factoryStartup = instanceFactory.StartAsync(cancellationToken);
206  await databaseContext.Initialize(cancellationToken).ConfigureAwait(false);
207  await jobManager.StartAsync(cancellationToken).ConfigureAwait(false);
208  var dbInstances = databaseContext.Instances.Where(x => x.Online.Value)
209  .Include(x => x.RepositorySettings)
210  .Include(x => x.ChatSettings)
211  .ThenInclude(x => x.Channels)
212  .Include(x => x.DreamDaemonSettings)
213  .ToAsyncEnumerable();
214  var tasks = new List<Task>();
215  await factoryStartup.ConfigureAwait(false);
216  await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
217  await Task.WhenAll(tasks).ConfigureAwait(false);
218  logger.LogInformation("Instance manager ready!");
219  application.Ready(null);
220  }
221  catch (OperationCanceledException)
222  {
223  logger.LogInformation("Cancelled instance manager initialization!");
224  }
225  catch (Exception e)
226  {
227  logger.LogCritical("Instance manager startup error! Exception: {0}", e);
228  application.Ready(e);
229  }
230  });
231 
233  public async Task StopAsync(CancellationToken cancellationToken)
234  {
235  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
236  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
237  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
238 
239  //downgrade the db if necessary
240  if (downgradeVersion != null)
241  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
242  }
243 
245  public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
246  {
247  downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
248  return Task.CompletedTask;
249  }
250  }
251 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
-
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
+
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
-
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
+
bool disposed
If the InstanceManager has been Disposed
+
Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
Handle a restart of the server
+
async Task OfflineInstance(Models.Instance metadata, Models.User user, CancellationToken cancellationToken)
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
readonly IJobManager jobManager
The IJobManager for the InstanceManager
+ +
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
For interacting with the instance services
Definition: IInstance.cs:17
- -
async Task StopAsync(CancellationToken cancellationToken)
-
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, ILogger< InstanceManager > logger)
Construct an InstanceManager
+ +
async Task StopAsync(CancellationToken cancellationToken)
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
@@ -121,9 +123,12 @@ 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
+
InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger< InstanceManager > logger)
Construct an InstanceManager
+
Represents a service that may take an updated Host assembly and run it, stopping the current assembly...
diff --git a/_instance_user_controller_8cs_source.html b/_instance_user_controller_8cs_source.html index 571ffac591..c7d4c32db9 100644 --- a/_instance_user_controller_8cs_source.html +++ b/_instance_user_controller_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.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
- +
ConfigurationRights
Rights for Models.ConfigurationFile
@@ -105,7 +105,7 @@ 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
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
override async Task< IActionResult > Create([FromBody] Api.Models.InstanceUser model, CancellationToken cancellationToken)
@@ -118,8 +118,8 @@ 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
diff --git a/_irc_provider_8cs_source.html b/_irc_provider_8cs_source.html index 531c2a0d20..96faeb92a2 100644 --- a/_irc_provider_8cs_source.html +++ b/_irc_provider_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
IrcProvider.cs
-Go to the documentation of this file.
1 using Meebey.SmartIrc4net;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Components.Chat.Providers
14 {
18  sealed class IrcProvider : Provider
19  {
20  const int TimeoutSeconds = 5;
21 
23  public override bool Connected => client.IsConnected;
24 
26  public override string BotMention => client.Nickname;
27 
32 
36  readonly ILogger<IrcProvider> logger;
37 
41  readonly IrcFeatures client;
42 
46  readonly string address;
50  readonly ushort port;
54  readonly string nickname;
58  readonly string password;
63 
67  readonly Dictionary<ulong, string> channelIdMap;
68 
72  readonly Dictionary<ulong, string> queryChannelIdMap;
73 
78 
82  Task listenTask;
83 
88 
101  public IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger<IrcProvider> logger, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl)
102  {
103  if (application == null)
104  throw new ArgumentNullException(nameof(application));
105  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
106  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
107 
108  this.address = address ?? throw new ArgumentNullException(nameof(address));
109  this.port = port;
110  this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
111 
112  if (passwordType.HasValue && password == null)
113  throw new ArgumentNullException(nameof(password));
114 
115  if (password != null && !passwordType.HasValue)
116  throw new ArgumentNullException(nameof(passwordType));
117 
118  this.password = password;
119  this.passwordType = passwordType;
120 
121  client = new IrcFeatures
122  {
123  SupportNonRfc = true,
124  CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
125  AutoRejoin = true,
126  AutoRejoinOnKick = true,
127  AutoRelogin = true,
128  AutoRetry = true,
129  AutoRetryLimit = TimeoutSeconds,
130  AutoRetryDelay = TimeoutSeconds,
131  ActiveChannelSyncing = true,
132  AutoNickHandling = true,
133  CtcpVersion = application.VersionString,
134  UseSsl = useSsl
135  };
136  if (useSsl)
137  client.ValidateServerCertificate = true; //dunno if it defaults to that or what
138 
139  client.OnChannelMessage += Client_OnChannelMessage;
140  client.OnQueryMessage += Client_OnQueryMessage;
141 
142  channelIdMap = new Dictionary<ulong, string>();
143  queryChannelIdMap = new Dictionary<ulong, string>();
144  channelIdCounter = 1;
145  disconnecting = false;
146  }
147 
149  public override void Dispose()
150  {
151  if (Connected)
152  {
153  disconnecting = true;
154  client.Disconnect(); //just closes the socket
155  }
156  }
157 
163  void HandleMessage(IrcEventArgs e, bool isPrivate)
164  {
165  if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
166  return;
167 
168  var username = e.Data.Nick;
169  var channelName = isPrivate ? username : e.Data.Channel;
170 
171  ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
172  {
173  ulong? resultId = null;
174  if (!dicToCheck.Any(x =>
175  {
176  if (x.Value != channelName)
177  return false;
178  resultId = x.Key;
179  return true;
180  }))
181  {
182  resultId = channelIdCounter++;
183  dicToCheck.Add(resultId.Value, channelName);
184  if (dicToCheck == queryChannelIdMap)
185  channelIdMap.Add(resultId.Value, null);
186  }
187  return resultId.Value;
188  };
189 
190  ulong userId, channelId;
191  lock (this)
192  {
193  userId = MapAndGetChannelId(queryChannelIdMap);
194  channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
195  }
196 
197  var message = new Message
198  {
199  Content = e.Data.Message,
200  User = new User
201  {
202  Channel = new Channel
203  {
204  ConnectionName = address,
205  FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
206  RealId = channelId,
207  IsPrivate = 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  IsAdmin = x.IsAdminChannel == true,
397  ConnectionName = address,
398  FriendlyName = channelIdMap[id.Value],
399  IsPrivate = 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 }
+Go to the documentation of this file.
1 using Meebey.SmartIrc4net;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Collections.Generic;
5 using System.Globalization;
6 using System.Linq;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
12 
13 namespace Tgstation.Server.Host.Components.Chat.Providers
14 {
18  sealed class IrcProvider : Provider
19  {
20  const int TimeoutSeconds = 5;
21 
23  public override bool Connected => client.IsConnected;
24 
26  public override string BotMention => client.Nickname;
27 
32 
36  readonly ILogger<IrcProvider> logger;
37 
41  readonly IrcFeatures client;
42 
46  readonly string address;
50  readonly ushort port;
54  readonly string nickname;
58  readonly string password;
63 
67  readonly Dictionary<ulong, string> channelIdMap;
68 
72  readonly Dictionary<ulong, string> queryChannelIdMap;
73 
78 
82  Task listenTask;
83 
88 
101  public IrcProvider(IApplication application, IAsyncDelayer asyncDelayer, ILogger<IrcProvider> logger, string address, ushort port, string nickname, string password, IrcPasswordType? passwordType, bool useSsl)
102  {
103  if (application == null)
104  throw new ArgumentNullException(nameof(application));
105  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
106  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
107 
108  this.address = address ?? throw new ArgumentNullException(nameof(address));
109  this.port = port;
110  this.nickname = nickname ?? throw new ArgumentNullException(nameof(nickname));
111 
112  if (passwordType.HasValue && password == null)
113  throw new ArgumentNullException(nameof(password));
114 
115  if (password != null && !passwordType.HasValue)
116  throw new ArgumentNullException(nameof(passwordType));
117 
118  this.password = password;
119  this.passwordType = passwordType;
120 
121  client = new IrcFeatures
122  {
123  SupportNonRfc = true,
124  CtcpUserInfo = "You are going to play. And I am going to watch. And everything will be just fine...",
125  AutoRejoin = true,
126  AutoRejoinOnKick = true,
127  AutoRelogin = true,
128  AutoRetry = true,
129  AutoRetryLimit = TimeoutSeconds,
130  AutoRetryDelay = TimeoutSeconds,
131  ActiveChannelSyncing = true,
132  AutoNickHandling = true,
133  CtcpVersion = application.VersionString,
134  UseSsl = useSsl
135  };
136  if (useSsl)
137  client.ValidateServerCertificate = true; //dunno if it defaults to that or what
138 
139  client.OnChannelMessage += Client_OnChannelMessage;
140  client.OnQueryMessage += Client_OnQueryMessage;
141 
142  channelIdMap = new Dictionary<ulong, string>();
143  queryChannelIdMap = new Dictionary<ulong, string>();
144  channelIdCounter = 1;
145  disconnecting = false;
146  }
147 
149  public override void Dispose()
150  {
151  if (Connected)
152  {
153  disconnecting = true;
154  client.Disconnect(); //just closes the socket
155  }
156  }
157 
163  void HandleMessage(IrcEventArgs e, bool isPrivate)
164  {
165  if (e.Data.Nick.ToUpperInvariant() == client.Nickname.ToUpperInvariant())
166  return;
167 
168  var username = e.Data.Nick;
169  var channelName = isPrivate ? username : e.Data.Channel;
170 
171  ulong MapAndGetChannelId(Dictionary<ulong, string> dicToCheck)
172  {
173  ulong? resultId = null;
174  if (!dicToCheck.Any(x =>
175  {
176  if (x.Value != channelName)
177  return false;
178  resultId = x.Key;
179  return true;
180  }))
181  {
182  resultId = channelIdCounter++;
183  dicToCheck.Add(resultId.Value, channelName);
184  if (dicToCheck == queryChannelIdMap)
185  channelIdMap.Add(resultId.Value, null);
186  }
187  return resultId.Value;
188  };
189 
190  ulong userId, channelId;
191  lock (this)
192  {
193  userId = MapAndGetChannelId(queryChannelIdMap);
194  channelId = isPrivate ? userId : MapAndGetChannelId(channelIdMap);
195  }
196 
197  var message = new Message
198  {
199  Content = e.Data.Message,
200  User = new User
201  {
202  Channel = new Channel
203  {
204  ConnectionName = address,
205  FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
206  RealId = channelId,
207  IsPrivateChannel = isPrivate
208  //isAdmin and Tag populated by manager
209  },
210  FriendlyName = username,
211  RealId = userId,
212  Mention = username
213  }
214  };
215 
216  EnqueueMessage(message);
217  }
218 
224  void Client_OnQueryMessage(object sender, IrcEventArgs e) => HandleMessage(e, true);
225 
231  void Client_OnChannelMessage(object sender, IrcEventArgs e) => HandleMessage(e, false);
232 
234  public override Task<bool> Connect(CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
235  {
236  disconnecting = false;
237  lock (this)
238  try
239  {
240  client.Connect(address, port);
241 
242  cancellationToken.ThrowIfCancellationRequested();
243 
244  if (passwordType == IrcPasswordType.Server)
245  client.Login(nickname, nickname, 0, nickname, password);
246  else
247  {
248  if (passwordType == IrcPasswordType.Sasl)
249  {
250  client.WriteLine("CAP REQ :sasl", Priority.Critical); //needs to be put in the buffer before anything else
251  cancellationToken.ThrowIfCancellationRequested();
252  }
253  client.Login(nickname, nickname, 0, nickname);
254  }
255 
256  if (passwordType == IrcPasswordType.NickServ)
257  {
258  cancellationToken.ThrowIfCancellationRequested();
259  client.SendMessage(SendType.Message, "NickServ", String.Format(CultureInfo.InvariantCulture, "IDENTIFY {0}", password));
260  }
261  else if (passwordType == IrcPasswordType.Sasl)
262  {
263  //wait for the sasl ack or timeout
264  var recievedAck = false;
265  var recievedPlus = false;
266  client.OnReadLine += (sender, e) =>
267  {
268  if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal))
269  recievedAck = true;
270  else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal))
271  recievedPlus = true;
272  };
273 
274  var startTime = DateTimeOffset.Now;
275  var endTime = DateTimeOffset.Now.AddSeconds(TimeoutSeconds);
276  cancellationToken.ThrowIfCancellationRequested();
277 
278  var listenTimeSpan = TimeSpan.FromMilliseconds(10);
279  for (; !recievedAck && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
280  client.Listen(false);
281 
282  client.WriteLine("AUTHENTICATE PLAIN", Priority.Critical);
283  cancellationToken.ThrowIfCancellationRequested();
284 
285  for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
286  client.Listen(false);
287 
288  //Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
289  var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password);
290  var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
291  var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64);
292  var chars = authLine.ToCharArray();
293  client.WriteLine(authLine, Priority.Critical);
294 
295  cancellationToken.ThrowIfCancellationRequested();
296  client.WriteLine("CAP END", Priority.Critical);
297  }
298 
299  client.Listen(false);
300 
301  listenTask = Task.Factory.StartNew(() =>
302  {
303  while (!disconnecting && client.IsConnected && client.Nickname != nickname)
304  {
305  client.ListenOnce(true);
306  if (disconnecting || !client.IsConnected)
307  break;
308  client.Listen(false);
309  //ensure we have the correct nick
310  if (client.GetIrcUser(nickname) == null)
311  client.RfcNick(nickname);
312  }
313  client.Listen();
314  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
315  }
316  catch (OperationCanceledException)
317  {
318  throw;
319  }
320  catch (Exception e)
321  {
322  logger.LogWarning("Unable to connect to IRC: {0}", e);
323  return false;
324  }
325  return true;
326  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
327 
329  public override async Task Disconnect(CancellationToken cancellationToken)
330  {
331  if (!Connected)
332  return;
333  try
334  {
335  await Task.Factory.StartNew(() =>
336  {
337  try
338  {
339  client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise it wont go through
340  }
341  catch (Exception e)
342  {
343  logger.LogWarning("Error quitting IRC: {0}", e);
344  }
345  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
346  Dispose();
347  await listenTask.ConfigureAwait(false);
348  }
349  catch (OperationCanceledException)
350  {
351  throw;
352  }
353  catch (Exception e)
354  {
355  logger.LogWarning("Error disconnecting from IRC! Exception: {0}", e);
356  }
357  }
358 
360  public override Task<IReadOnlyList<Channel>> MapChannels(IEnumerable<ChatChannel> channels, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
361  {
362  if (channels.Any(x => x.IrcChannel == null))
363  throw new InvalidOperationException("ChatChannel missing IrcChannel!");
364  lock (this)
365  {
366  var hs = new HashSet<string>(); //for unique inserts
367  foreach (var I in channels)
368  hs.Add(I.IrcChannel);
369  var toPart = new List<string>();
370  foreach (var I in client.JoinedChannels)
371  if (!hs.Remove(I))
372  toPart.Add(I);
373 
374  foreach (var I in toPart)
375  client.RfcPart(I, "Pretty nice abscond!");
376  foreach (var I in hs)
377  client.RfcJoin(I);
378 
379  return (IReadOnlyList<Channel>)channels.Select(x =>
380  {
381  ulong? id = null;
382  if (!channelIdMap.Any(y =>
383  {
384  if (y.Value != x.IrcChannel)
385  return false;
386  id = y.Key;
387  return true;
388  }))
389  {
390  id = channelIdCounter++;
391  channelIdMap.Add(id.Value, x.IrcChannel);
392  }
393  return new Channel
394  {
395  RealId = id.Value,
396  IsAdminChannel = x.IsAdminChannel == true,
397  ConnectionName = address,
398  FriendlyName = channelIdMap[id.Value],
399  IsPrivateChannel = false,
400  Tag = x.Tag
401  };
402  }).ToList();
403  }
404  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
405 
407  public override Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken) => Task.Factory.StartNew(() =>
408  {
409  var channelName = channelIdMap[channelId];
410  SendType sendType;
411  if (channelName == null)
412  {
413  channelName = queryChannelIdMap[channelId];
414  sendType = SendType.Notice;
415  }
416  else
417  sendType = SendType.Message;
418  try
419  {
420  client.SendMessage(sendType, channelName, message);
421  }
422  catch (Exception e)
423  {
424  logger.LogWarning("Unable to send to channel: {0}", e);
425  }
426  }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
427  }
428 }
ulong channelIdCounter
Id counter for channelIdMap
Definition: IrcProvider.cs:77
readonly Dictionary< ulong, string > queryChannelIdMap
Map of Channel.RealIds to query users
Definition: IrcProvider.cs:72
readonly IrcFeatures client
The IrcFeatures client
Definition: IrcProvider.cs:41
@@ -100,7 +100,6 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly ushort port
Port of the server to connect to
Definition: IrcProvider.cs:50
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
-
readonly ILogger< IrcProvider > logger
The ILogger for the IrcProvider
Definition: IrcProvider.cs:36
@@ -123,6 +122,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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
diff --git a/_job_controller_8cs_source.html b/_job_controller_8cs_source.html index b0ab55245a..0448561b76 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_manager_8cs_source.html b/_job_manager_8cs_source.html index cfab0b9161..8a8a73a05e 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  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
202  }
203  }).ConfigureAwait(false);
204  logger.LogDebug("Job manager started!");
205  }
206 
208  public async Task StopAsync(CancellationToken cancellationToken)
209  {
210  var joinTasks = jobs.Select(x =>
211  {
212  x.Value.Cancel();
213  return x.Value.Wait(cancellationToken);
214  });
215  await Task.WhenAll(joinTasks).ConfigureAwait(false);
216  }
217 
219  public async Task<bool> CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
220  {
221  if (job == null)
222  throw new ArgumentNullException(nameof(job));
223  if (user == null)
224  throw new ArgumentNullException(nameof(user));
225  JobHandler handler;
226  try
227  {
228  handler = CheckGetJob(job);
229  }
230  catch (InvalidOperationException)
231  {
232  //this is fine
233  return false;
234  }
235  handler.Cancel(); //this will ensure the db update is only done once
236  await databaseContextFactory.UseContext(async databaseContext =>
237  {
238  job = new Job { Id = job.Id };
239  databaseContext.Jobs.Attach(job);
240  user = new User { Id = user.Id };
241  databaseContext.Users.Attach(user);
242  job.CancelledBy = user;
243  //let either startup or cancellation set job.cancelled
244  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
245  }).ConfigureAwait(false);
246  if (blocking)
247  await handler.Wait(cancellationToken).ConfigureAwait(false);
248  return true;
249  }
250 
252  public int? JobProgress(Job job)
253  {
254  if (job == null)
255  throw new ArgumentNullException(nameof(job));
256  lock (this)
257  {
258  if (!jobs.TryGetValue(job.Id, out var handler))
259  return null;
260  return handler.Progress;
261  }
262  }
263 
265  public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
266  {
267  if (job == null)
268  throw new ArgumentNullException(nameof(job));
269  if (canceller == null)
270  throw new ArgumentNullException(nameof(canceller));
271  JobHandler handler;
272  lock (this)
273  {
274  if (!jobs.TryGetValue(job.Id, out handler))
275  return;
276  }
277  Task cancelTask = null;
278  using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken)))
279  await handler.Wait(cancellationToken).ConfigureAwait(false);
280 
281  if (cancelTask != null)
282  await cancelTask.ConfigureAwait(false);
283  }
284  }
285 }
Manages the runtime of Jobs
Definition: IJobManager.cs:12
diff --git a/_model_controller_8cs_source.html b/_model_controller_8cs_source.html index d331cef745..e925a3364f 100644 --- a/_model_controller_8cs_source.html +++ b/_model_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ModelController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System.Threading;
4 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Controllers
9 {
14  public abstract class ModelController<TModel> : ApiController where TModel : class
15  {
23  public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance) : base(databaseContext, authenticationContextFactory, logger, requireInstance) { }
24 
31  [HttpPut]
32  public virtual Task<IActionResult> Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
33 
39  [HttpGet]
40  public virtual Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
41 
48  [HttpGet("{id}")]
49  public virtual Task<IActionResult> GetId(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
50 
57  [HttpPost]
58  public virtual Task<IActionResult> Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
59 
66  [HttpDelete("{id}")]
67  public virtual Task<IActionResult> Delete(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
68 
74  [HttpGet("List")]
75  public virtual Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
76  }
77 }
+Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.Extensions.Logging;
3 using System.Threading;
4 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Controllers
9 {
14  public abstract class ModelController<TModel> : ApiController where TModel : class
15  {
23  public ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance) : base(databaseContext, authenticationContextFactory, logger, requireInstance) { }
24 
31  [HttpPut]
32  public virtual Task<IActionResult> Create([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
33 
39  [HttpGet]
40  public virtual Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
41 
48  [HttpGet("{id}")]
49  public virtual Task<IActionResult> GetId(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
50 
57  [HttpPost]
58  public virtual Task<IActionResult> Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
59 
66  [HttpDelete("{id}")]
67  public virtual Task<IActionResult> Delete(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
68 
74  [HttpGet("List")]
75  public virtual Task<IActionResult> List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
76  }
77 }
ModelController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger logger, bool requireInstance)
Construct a ModelController<TModel>
diff --git a/_my_sql_database_context_8cs_source.html b/_my_sql_database_context_8cs_source.html index 23ddb4adf1..60a3afdf66 100644 --- a/_my_sql_database_context_8cs_source.html +++ b/_my_sql_database_context_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
MySqlDatabaseContext.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using MySql.Data.MySqlClient;
5 using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
6 using System;
8 
9 namespace Tgstation.Server.Host.Models
10 {
14  sealed class MySqlDatabaseContext : DatabaseContext<MySqlDatabaseContext>
15  {
23  public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<MySqlDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
24  { }
25 
27  protected override void OnConfiguring(DbContextOptionsBuilder options)
28  {
29  base.OnConfiguring(options);
30  var stringDeconstructor = new MySqlConnectionStringBuilder
31  {
32  ConnectionString = DatabaseConfiguration.ConnectionString
33  };
34  if (stringDeconstructor.Server == "localhost")
35  Logger.LogWarning("MariaDB/MySQL server address is set to 'localhost'! If there are connection issues, try setting it to '127.0.0.1'!");
36  if (!String.IsNullOrEmpty(DatabaseConfiguration.MySqlServerVersion))
37  options.UseMySql(DatabaseConfiguration.ConnectionString, mySqlOptions => mySqlOptions.ServerVersion(Version.Parse(DatabaseConfiguration.MySqlServerVersion), DatabaseConfiguration.DatabaseType == DatabaseType.MariaDB ? ServerType.MariaDb : ServerType.MySql));
38  else
39  options.UseMySql(DatabaseConfiguration.ConnectionString);
40  }
41  }
42 }
string ConnectionString
The connection string for the database
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
4 using MySql.Data.MySqlClient;
5 using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
6 using System;
8 
9 namespace Tgstation.Server.Host.Models
10 {
14  sealed class MySqlDatabaseContext : DatabaseContext<MySqlDatabaseContext>
15  {
23  public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<MySqlDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
24  { }
25 
27  protected override void OnConfiguring(DbContextOptionsBuilder options)
28  {
29  base.OnConfiguring(options);
30  var stringDeconstructor = new MySqlConnectionStringBuilder
31  {
32  ConnectionString = DatabaseConfiguration.ConnectionString
33  };
34  if (stringDeconstructor.Server == "localhost")
35  Logger.LogWarning("MariaDB/MySQL server address is set to 'localhost'! If there are connection issues, try setting it to '127.0.0.1'!");
36  if (!String.IsNullOrEmpty(DatabaseConfiguration.MySqlServerVersion))
37  options.UseMySql(DatabaseConfiguration.ConnectionString, mySqlOptions => mySqlOptions.ServerVersion(Version.Parse(DatabaseConfiguration.MySqlServerVersion), DatabaseConfiguration.DatabaseType == DatabaseType.MariaDB ? ServerType.MariaDb : ServerType.MySql));
38  else
39  options.UseMySql(DatabaseConfiguration.ConnectionString);
40  }
41 
43  protected override bool UseMySQLMigrations() => true;
44  }
45 }
string ConnectionString
The connection string for the database
MySqlDatabaseContext(DbContextOptions< MySqlDatabaseContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger< MySqlDatabaseContext > logger)
Construct a MySqlDatabaseContext
override void OnConfiguring(DbContextOptionsBuilder options)
@@ -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/_repository_controller_8cs_source.html b/_repository_controller_8cs_source.html index a273354399..17120d14bf 100644 --- a/_repository_controller_8cs_source.html +++ b/_repository_controller_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.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
- +
Manages the runtime of Jobs
Definition: IJobManager.cs:12
@@ -122,8 +122,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Represents a git repository
Definition: Repository.cs:8
const string Repository
The Models.Repository controller
Definition: Routes.cs:39
RevisionInformation RevisionInformation
The current Models.RevisionInformation for the Repository
Definition: Repository.cs:23
- -
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
@@ -136,7 +136,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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
+
DbSet< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
override async Task< IActionResult > Create([FromBody] Repository model, CancellationToken cancellationToken)
diff --git a/_session_controller_8cs_source.html b/_session_controller_8cs_source.html index 93eeca2195..60f23e9727 100644 --- a/_session_controller_8cs_source.html +++ b/_session_controller_8cs_source.html @@ -92,30 +92,30 @@ 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 
230 #pragma warning disable CA1821 // Remove empty Finalizers //TODO remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
231  ~SessionController() => Dispose(false);
232 #pragma warning restore CA1821 // Remove empty Finalizers
233 
235  public void Dispose()
236  {
237  Dispose(true);
238  GC.SuppressFinalize(this);
239  }
240 
242  void Dispose(bool disposing)
243  {
244  lock (this)
245  {
246  if (disposed)
247  return;
248  if (disposing)
249  {
250  if (!released)
251  {
252  process.Terminate();
253  byondLock.Dispose();
254  }
255  process.Dispose();
256  interopContext.Dispose();
257  Dmb?.Dispose(); //will be null when released
258  chatJsonTrackingContext.Dispose();
259  disposed = true;
260  }
261  else
262  {
263  if (logger != null)
264  logger.LogError("Being disposed via finalizer!");
265  if (!released)
266  if (process != null)
267  process.Terminate();
268  else if (logger != null)
269  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
270  }
271  }
272  }
273 
275  public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
276  {
277  if (command == null)
278  throw new ArgumentNullException(nameof(command));
279 
280  var query = command.Parameters;
281 
282  object content;
283  Action postRespond = null;
284  ushort? overrideResponsePort = null;
285  if (query.TryGetValue(Constants.DMParameterCommand, out var method))
286  {
287  content = new object();
288  switch (method)
289  {
291  try
292  {
293  var message = JsonConvert.DeserializeObject<Response>(command.RawJson, new JsonSerializerSettings
294  {
295  ContractResolver = new CamelCasePropertyNamesContractResolver()
296  });
297  if (message.ChannelIds == null)
298  throw new InvalidOperationException("Missing ChannelIds field!");
299  if (message.Message == null)
300  throw new InvalidOperationException("Missing Message field!");
301  await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false);
302  }
303  catch (Exception e)
304  {
305  logger.LogDebug("Exception while decoding chat message! Exception: {0}", e);
306  goto default;
307  }
308  break;
310  //currently unused, maybe in the future
311  break;
313  TerminationWasRequested = true;
314  process.Terminate();
315  return;
317  lock (this)
318  {
319  if (!query.TryGetValue(Constants.DMParameterData, out var stringPort) || !UInt16.TryParse(stringPort, out var currentPort))
320  {
322  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
323  content = new ErrorMessage { Message = "Missing stringified port as data parameter!" };
324  break;
325  }
326 
327  if (!nextPort.HasValue)
328  //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
329  reattachInformation.Port = currentPort;
330  else
331  {
332  //nextPort is ready, tell DD to switch to that
333  //if it fails it'll kill itself
334  content = new Dictionary<string, ushort> { { Constants.DMParameterData, nextPort.Value } };
335  reattachInformation.Port = nextPort.Value;
336  overrideResponsePort = currentPort;
337  nextPort = null;
338 
339  //we'll also get here from SetPort so complete that task
340  var tmpTcs = portAssignmentTcs;
341  portAssignmentTcs = null;
342  if (tmpTcs != null)
343  postRespond = () => tmpTcs.SetResult(true);
344  }
345 
346  portClosedForReboot = false;
347  }
348  break;
350  if (!launchSecurityLevel.HasValue)
351  {
352  logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!");
353  apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest;
354  content = new ErrorMessage { Message = "Invalid API validation request!" };
355  break;
356  }
357  if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevel) || !Enum.TryParse<DreamDaemonSecurity>(stringMinimumSecurityLevel, out var minimumSecurityLevel))
358  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
359  else
360  switch (minimumSecurityLevel)
361  {
362  case DreamDaemonSecurity.Safe:
363  apiValidationStatus = ApiValidationStatus.RequiresSafe;
364  break;
365  case DreamDaemonSecurity.Ultrasafe:
366  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
367  break;
368  case DreamDaemonSecurity.Trusted:
369  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
370  break;
371  default:
372  throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!");
373  }
374  break;
376  if (ClosePortOnReboot)
377  {
378  chatJsonTrackingContext.Active = false;
379  content = new Dictionary<string, int> { { Constants.DMParameterData, 0 } };
380  portClosedForReboot = true;
381  }
382  var oldTcs = rebootTcs;
383  rebootTcs = new TaskCompletionSource<object>();
384  postRespond = () => oldTcs.SetResult(null);
385  break;
386  default:
387  content = new ErrorMessage { Message = "Requested command not supported!" };
388  break;
389  }
390  }
391  else
392  content = new ErrorMessage { Message = "Missing command parameter!" };
393 
394  var json = JsonConvert.SerializeObject(content);
395  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);
396 
397  if (response != Constants.DMResponseSuccess)
398  logger.LogWarning("Received error response while responding to interop: {0}", response);
399 
400  postRespond?.Invoke();
401  }
402 
407  {
408  if (disposed)
409  throw new ObjectDisposedException(nameof(SessionController));
410  }
411 
413  public void EnableCustomChatCommands() => chatJsonTrackingContext.Active = true;
414 
417  {
418  CheckDisposed();
419  //we still don't want to dispose the dmb yet, even though we're keeping it alive
420  var tmpProvider = reattachInformation.Dmb;
421  reattachInformation.Dmb = null;
422  released = true;
423  Dispose();
424  byondLock.DoNotDeleteThisSession();
425  tmpProvider.KeepAlive();
426  reattachInformation.Dmb = tmpProvider;
427  return reattachInformation;
428  }
429 
431  public Task<string> SendCommand(string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken);
432 
433  async Task<string> SendCommand(string command, ushort? overridePort, CancellationToken cancellationToken)
434  {
435  try
436  {
437  var commandString = String.Format(CultureInfo.InvariantCulture,
438  "?{0}={1}&{2}={3}",
439  byondTopicSender.SanitizeString(Constants.DMInteropAccessIdentifier),
440  byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier),
441  byondTopicSender.SanitizeString(Constants.DMParameterCommand),
442  //intentionally don't sanitize command, that's up to the caller
443  command);
444 
445  var targetPort = overridePort ?? reattachInformation.Port;
446  logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString);
447 
448  return await byondTopicSender.SendTopic(
449  new IPEndPoint(IPAddress.Loopback, targetPort),
450  commandString,
451  cancellationToken).ConfigureAwait(false);
452  }
453  catch (OperationCanceledException)
454  {
455  throw;
456  }
457  catch (Exception e)
458  {
459  logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message);
460  return null;
461  }
462  }
463 
465  public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
466  {
467  CheckDisposed();
468 
469  if (port == 0)
470  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
471 
472  async Task<bool> ImmediateTopicPortChange()
473  {
474  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);
475 
476  if (commandResult != Constants.DMResponseSuccess)
477  {
478  logger.LogWarning("Failed port change! DD says: {0}", commandResult);
479  return false;
480  }
481 
482  return true;
483  }
484 
485  lock (this)
486  if (portClosedForReboot)
487  {
488  if (portAssignmentTcs != null)
489  throw new InvalidOperationException("A port change operation is already in progress!");
490  nextPort = port;
491  portAssignmentTcs = new TaskCompletionSource<bool>();
492  return portAssignmentTcs.Task;
493  }
494  else
495  return ImmediateTopicPortChange();
496  }
497 
499  public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
500  {
501  if (RebootState == newRebootState)
502  return true;
503  reattachInformation.RebootState = newRebootState;
504  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;
505  }
506 
508  public void ResetRebootState()
509  {
510  CheckDisposed();
511  reattachInformation.RebootState = RebootState.Normal;
512  }
513 
515  public void SetHighPriority() => process.SetHighPriority();
516  }
517 }
+Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Newtonsoft.Json.Serialization;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Net;
9 using System.Threading;
10 using System.Threading.Tasks;
16 
17 namespace Tgstation.Server.Host.Components.Watchdog
18 {
21  {
23  public bool IsPrimary
24  {
25  get
26  {
27  CheckDisposed();
28  return reattachInformation.IsPrimary;
29  }
30  }
31 
34  {
35  get
36  {
37  if (!Lifetime.IsCompleted)
38  throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
39  return apiValidationStatus;
40  }
41  }
42 
44  public IDmbProvider Dmb
45  {
46  get
47  {
48  CheckDisposed();
49  return reattachInformation.Dmb;
50  }
51  }
52 
54  public ushort? Port
55  {
56  get
57  {
58  CheckDisposed();
59  if (portClosedForReboot)
60  return null;
61  return reattachInformation.Port;
62  }
63  }
64 
67  {
68  get
69  {
70  CheckDisposed();
71  return reattachInformation.RebootState;
72  }
73  }
74 
76  public bool ClosePortOnReboot { get; set; }
77 
79  public bool TerminationWasRequested { get; private set; }
80 
82  public Task<LaunchResult> LaunchResult { get; }
83 
85  public Task<int> Lifetime => process.Lifetime;
86 
88  public Task OnReboot => rebootTcs.Task;
89 
94 
98  readonly IByondTopicSender byondTopicSender;
99 
104 
108  readonly IProcess process;
109 
114 
119 
123  readonly IChat chat;
124 
128  readonly ILogger<SessionController> logger;
129 
134 
138  TaskCompletionSource<bool> portAssignmentTcs;
142  ushort? nextPort;
143 
147  TaskCompletionSource<object> rebootTcs;
148 
153 
157  bool disposed;
158 
163 
167  bool released;
168 
182  public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger<SessionController> logger, DreamDaemonSecurity? launchSecurityLevel, uint? startupTimeout)
183  {
184  this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid
185  this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
186  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
187  this.process = process ?? throw new ArgumentNullException(nameof(process));
188  this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
189  this.interopContext = interopContext ?? throw new ArgumentNullException(nameof(interopContext));
190  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
191  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
192 
193  this.launchSecurityLevel = launchSecurityLevel;
194 
195  interopContext.RegisterHandler(this);
196 
197  portClosedForReboot = false;
198  disposed = false;
199  apiValidationStatus = ApiValidationStatus.NeverValidated;
200  released = false;
201 
202  rebootTcs = new TaskCompletionSource<object>();
203 
204  process.Lifetime.ContinueWith(x => chatJsonTrackingContext.Active = false, TaskScheduler.Current);
205 
206  async Task<LaunchResult> GetLaunchResult()
207  {
208  var startTime = DateTimeOffset.Now;
209  Task toAwait = process.Startup;
210 
211  if (startupTimeout.HasValue)
212  toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
213 
214  await toAwait.ConfigureAwait(false);
215 
216  var result = new LaunchResult
217  {
218  ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
219  StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
220  };
221  return result;
222  };
223  LaunchResult = GetLaunchResult();
224 
225  logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port);
226  }
227 
232 #pragma warning disable CA1821 // Remove empty Finalizers //TODO remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
233  ~SessionController() => Dispose(false);
234 #pragma warning restore CA1821 // Remove empty Finalizers
235 
237  public void Dispose()
238  {
239  Dispose(true);
240  GC.SuppressFinalize(this);
241  }
242 
244  void Dispose(bool disposing)
245  {
246  lock (this)
247  {
248  if (disposed)
249  return;
250  if (disposing)
251  {
252  if (!released)
253  {
254  process.Terminate();
255  byondLock.Dispose();
256  }
257  process.Dispose();
258  interopContext.Dispose();
259  Dmb?.Dispose(); //will be null when released
260  chatJsonTrackingContext.Dispose();
261  disposed = true;
262  }
263  else
264  {
265  if (logger != null)
266  logger.LogError("Being disposed via finalizer!");
267  if (!released)
268  if (process != null)
269  process.Terminate();
270  else if (logger != null)
271  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
272  }
273  }
274  }
275 
277  public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
278  {
279  if (command == null)
280  throw new ArgumentNullException(nameof(command));
281 
282  var query = command.Parameters;
283 
284  object content;
285  Action postRespond = null;
286  ushort? overrideResponsePort = null;
287  if (query.TryGetValue(Constants.DMParameterCommand, out var method))
288  {
289  content = new object();
290  switch (method)
291  {
293  try
294  {
295  var message = JsonConvert.DeserializeObject<Response>(command.RawJson, new JsonSerializerSettings
296  {
297  ContractResolver = new CamelCasePropertyNamesContractResolver()
298  });
299  if (message.ChannelIds == null)
300  throw new InvalidOperationException("Missing ChannelIds field!");
301  if (message.Message == null)
302  throw new InvalidOperationException("Missing Message field!");
303  await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false);
304  }
305  catch (Exception e)
306  {
307  logger.LogDebug("Exception while decoding chat message! Exception: {0}", e);
308  goto default;
309  }
310  break;
312  //currently unused, maybe in the future
313  break;
315  TerminationWasRequested = true;
316  process.Terminate();
317  return;
319  lock (this)
320  {
321  if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort))
322  {
324  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
325  content = new ErrorMessage { Message = "Missing stringified port as data parameter!" };
326  break;
327  }
328 
329  if (!nextPort.HasValue)
330  //not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
331  reattachInformation.Port = currentPort;
332  else
333  {
334  //nextPort is ready, tell DD to switch to that
335  //if it fails it'll kill itself
336  content = new Dictionary<string, ushort> { { Constants.DMParameterData, nextPort.Value } };
337  reattachInformation.Port = nextPort.Value;
338  overrideResponsePort = currentPort;
339  nextPort = null;
340 
341  //we'll also get here from SetPort so complete that task
342  var tmpTcs = portAssignmentTcs;
343  portAssignmentTcs = null;
344  if (tmpTcs != null)
345  postRespond = () => tmpTcs.SetResult(true);
346  }
347 
348  portClosedForReboot = false;
349  }
350  break;
352  if (!launchSecurityLevel.HasValue)
353  {
354  logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!");
355  apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest;
356  content = new ErrorMessage { Message = "Invalid API validation request!" };
357  break;
358  }
359  if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse<DreamDaemonSecurity>(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel))
360  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
361  else
362  switch (minimumSecurityLevel)
363  {
364  case DreamDaemonSecurity.Safe:
365  apiValidationStatus = ApiValidationStatus.RequiresSafe;
366  break;
367  case DreamDaemonSecurity.Ultrasafe:
368  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
369  break;
370  case DreamDaemonSecurity.Trusted:
371  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
372  break;
373  default:
374  throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!");
375  }
376  break;
378  if (ClosePortOnReboot)
379  {
380  chatJsonTrackingContext.Active = false;
381  content = new Dictionary<string, int> { { Constants.DMParameterData, 0 } };
382  portClosedForReboot = true;
383  }
384  var oldTcs = rebootTcs;
385  rebootTcs = new TaskCompletionSource<object>();
386  postRespond = () => oldTcs.SetResult(null);
387  break;
388  default:
389  content = new ErrorMessage { Message = "Requested command not supported!" };
390  break;
391  }
392  }
393  else
394  content = new ErrorMessage { Message = "Missing command parameter!" };
395 
396  var json = JsonConvert.SerializeObject(content);
397  var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false);
398 
399  if (response != Constants.DMResponseSuccess)
400  logger.LogWarning("Received error response while responding to interop: {0}", response);
401 
402  postRespond?.Invoke();
403  }
404 
409  {
410  if (disposed)
411  throw new ObjectDisposedException(nameof(SessionController));
412  }
413 
415  public void EnableCustomChatCommands() => chatJsonTrackingContext.Active = true;
416 
419  {
420  CheckDisposed();
421  //we still don't want to dispose the dmb yet, even though we're keeping it alive
422  var tmpProvider = reattachInformation.Dmb;
423  reattachInformation.Dmb = null;
424  released = true;
425  Dispose();
426  byondLock.DoNotDeleteThisSession();
427  tmpProvider.KeepAlive();
428  reattachInformation.Dmb = tmpProvider;
429  return reattachInformation;
430  }
431 
433  public Task<string> SendCommand(string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken);
434 
435  async Task<string> SendCommand(string command, ushort? overridePort, CancellationToken cancellationToken)
436  {
437  try
438  {
439  var commandString = String.Format(CultureInfo.InvariantCulture,
440  "?{0}={1}&{2}={3}",
441  byondTopicSender.SanitizeString(Constants.DMInteropAccessIdentifier),
442  byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier),
443  byondTopicSender.SanitizeString(Constants.DMParameterCommand),
444  //intentionally don't sanitize command, that's up to the caller
445  command);
446 
447  var targetPort = overridePort ?? reattachInformation.Port;
448  logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString);
449 
450  return await byondTopicSender.SendTopic(
451  new IPEndPoint(IPAddress.Loopback, targetPort),
452  commandString,
453  cancellationToken).ConfigureAwait(false);
454  }
455  catch (OperationCanceledException)
456  {
457  throw;
458  }
459  catch (Exception e)
460  {
461  logger.LogInformation("Send command exception:{0}{1}", Environment.NewLine, e.Message);
462  return null;
463  }
464  }
465 
467  public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
468  {
469  CheckDisposed();
470 
471  if (port == 0)
472  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
473 
474  async Task<bool> ImmediateTopicPortChange()
475  {
476  var commandResult = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangePort), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(false);
477 
478  if (commandResult != Constants.DMResponseSuccess)
479  {
480  logger.LogWarning("Failed port change! DD says: {0}", commandResult);
481  return false;
482  }
483 
484  reattachInformation.Port = port;
485  return true;
486  }
487 
488  lock (this)
489  if (portClosedForReboot)
490  {
491  if (portAssignmentTcs != null)
492  throw new InvalidOperationException("A port change operation is already in progress!");
493  nextPort = port;
494  portAssignmentTcs = new TaskCompletionSource<bool>();
495  return portAssignmentTcs.Task;
496  }
497  else
498  return ImmediateTopicPortChange();
499  }
500 
502  public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
503  {
504  if (RebootState == newRebootState)
505  return true;
506  reattachInformation.RebootState = newRebootState;
507  return await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChangeReboot), byondTopicSender.SanitizeString(Constants.DMParameterData), (int)newRebootState), cancellationToken).ConfigureAwait(false) == Constants.DMResponseSuccess;
508  }
509 
511  public void ResetRebootState()
512  {
513  CheckDisposed();
514  reattachInformation.RebootState = RebootState.Normal;
515  }
516 
518  public void SetHighPriority() => process.SetHighPriority();
519  }
520 }
bool disposed
If the SessionController has been disposed
readonly ICommContext interopContext
The ICommContext for the SessionController
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
-
IReadOnlyDictionary< string, string > Parameters
The dictionary of the CommCommand
Definition: CommCommand.cs:13
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
- +
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
@@ -128,9 +128,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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
+
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
-
async Task< string > SendCommand(string command, ushort?overridePort, CancellationToken cancellationToken)
+
async Task< string > SendCommand(string command, ushort?overridePort, CancellationToken cancellationToken)
ushort nextPort
The port to assign DreamDaemon when it queries for it
@@ -149,7 +149,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController
-
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
@@ -159,7 +159,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the SessionController
-
async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
Handle a command
+
async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
Handle a command
Represents an error message returned by the server
Definition: ErrorMessage.cs:8
@@ -167,6 +167,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Parameters necessary for duplicating a ISessionController session
ApiValidationStatus
Status of DMAPI validation
+
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance
bool released
If process should be kept alive instead
Represents a chat message requested by DD
Definition: Response.cs:8
diff --git a/_sql_server_database_context_8cs_source.html b/_sql_server_database_context_8cs_source.html index b65d0d2a55..a2fef729fc 100644 --- a/_sql_server_database_context_8cs_source.html +++ b/_sql_server_database_context_8cs_source.html @@ -92,14 +92,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
SqlServerDatabaseContext.cs
-Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
5 
6 namespace Tgstation.Server.Host.Models
7 {
11  sealed class SqlServerDatabaseContext : DatabaseContext<SqlServerDatabaseContext>
12  {
20  public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqlServerDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
21  { }
22 
24  protected override void OnConfiguring(DbContextOptionsBuilder options)
25  {
26  base.OnConfiguring(options);
27  options.UseSqlServer(DatabaseConfiguration.ConnectionString);
28  }
29  }
30 }
string ConnectionString
The connection string for the database
+Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Microsoft.Extensions.Options;
5 
6 namespace Tgstation.Server.Host.Models
7 {
11  sealed class SqlServerDatabaseContext : DatabaseContext<SqlServerDatabaseContext>
12  {
20  public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqlServerDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
21  { }
22 
24  protected override void OnConfiguring(DbContextOptionsBuilder options)
25  {
26  base.OnConfiguring(options);
27  options.UseSqlServer(DatabaseConfiguration.ConnectionString);
28  }
29 
31  protected override bool UseMySQLMigrations() => false;
32  }
33 }
string ConnectionString
The connection string for the database
override void OnConfiguring(DbContextOptionsBuilder options)
For initially seeding a database
DatabaseContext<TParentContext> for Sqlserver
SqlServerDatabaseContext(DbContextOptions< SqlServerDatabaseContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger< SqlServerDatabaseContext > logger)
Construct a SqlServerDatabaseContext
- +
Configuration options for the Models.DatabaseContext<TParentContext>
diff --git a/_test_database_context_factory_8cs_source.html b/_test_database_context_factory_8cs_source.html index c1c39d8bd9..4715e87579 100644 --- a/_test_database_context_factory_8cs_source.html +++ b/_test_database_context_factory_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
TestDatabaseContextFactory.cs
-Go to the documentation of this file.
1 using Microsoft.Extensions.DependencyInjection;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Core.Tests
9 {
10  [TestClass]
11  public sealed class TestDatabaseContextFactory
12  {
13  [TestMethod]
14  public void TestConstructionThrows()
15  {
16  Assert.ThrowsException<ArgumentNullException>(() => new DatabaseContextFactory(null));
17  var mockProvider = new Mock<IServiceProvider>();
18  mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Verifiable();
19  var mockScope = new Mock<IServiceScope>();
20  mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
21  var mockScopeFactory = new Mock<IServiceScopeFactory>();
22  mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
23  Assert.ThrowsException<InvalidOperationException>(() => new DatabaseContextFactory(mockScopeFactory.Object));
24  mockScopeFactory.VerifyAll();
25  mockScope.VerifyAll();
26  mockProvider.VerifyAll();
27  }
28 
29  [TestMethod]
30  public async Task TestWorks()
31  {
32  var mockDatabase = new Mock<IDatabaseContext>();
33  var mockProvider = new Mock<IServiceProvider>();
34  var mockDbo = mockDatabase.Object;
35  mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Returns(mockDbo).Verifiable();
36  var mockScope = new Mock<IServiceScope>();
37  mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
38  var mockScopeFactory = new Mock<IServiceScopeFactory>();
39  mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
40 
41  var factory = new DatabaseContextFactory(mockScopeFactory.Object);
42 
43  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => factory.UseContext(null)).ConfigureAwait(false);
44 
45  await factory.UseContext(context =>
46  {
47  Assert.AreSame(mockDbo, context);
48  return Task.CompletedTask;
49  });
50 
51  mockScopeFactory.VerifyAll();
52  mockScope.VerifyAll();
53  mockProvider.VerifyAll();
54  }
55  }
56 }
+Go to the documentation of this file.
1 using Microsoft.Extensions.DependencyInjection;
2 using Microsoft.VisualStudio.TestTools.UnitTesting;
3 using Moq;
4 using System;
5 using System.Threading.Tasks;
7 
8 namespace Tgstation.Server.Host.Core.Tests
9 {
10  [TestClass]
11  public sealed class TestDatabaseContextFactory
12  {
13  [TestMethod]
14  public void TestConstructionThrows()
15  {
16  Assert.ThrowsException<ArgumentNullException>(() => new DatabaseContextFactory(null));
17  var mockProvider = new Mock<IServiceProvider>();
18  mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Verifiable();
19  var mockScope = new Mock<IServiceScope>();
20  mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
21  var mockScopeFactory = new Mock<IServiceScopeFactory>();
22  mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
23  Assert.ThrowsException<InvalidOperationException>(() => new DatabaseContextFactory(mockScopeFactory.Object));
24  mockScopeFactory.VerifyAll();
25  mockScope.VerifyAll();
26  mockProvider.VerifyAll();
27  }
28 
29  [TestMethod]
30  public async Task TestWorks()
31  {
32  var mockDatabase = new Mock<IDatabaseContext>();
33  var mockProvider = new Mock<IServiceProvider>();
34  var mockDbo = mockDatabase.Object;
35  mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Returns(mockDbo).Verifiable();
36  var mockScope = new Mock<IServiceScope>();
37  mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
38  var mockScopeFactory = new Mock<IServiceScopeFactory>();
39  mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
40 
41  var factory = new DatabaseContextFactory(mockScopeFactory.Object);
42 
43  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => factory.UseContext(null)).ConfigureAwait(false);
44 
45  await factory.UseContext(context =>
46  {
47  Assert.AreSame(mockDbo, context);
48  return Task.CompletedTask;
49  });
50 
51  mockScopeFactory.VerifyAll();
52  mockScope.VerifyAll();
53  mockProvider.VerifyAll();
54  }
55  }
56 }
diff --git a/_tgstation_8_server_8_api_8_assembly_info_8cs.html b/_tgstation_8_server_8_api_8_assembly_info_8cs.html index 88fc90fce7..823d805b2e 100644 --- a/_tgstation_8_server_8_api_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_api_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Api/obj/Release/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Api/obj/Debug/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html index 3f1185acce..c303cd7fb3 100644 --- a/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_api_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Api/obj/Release/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Api/obj/Debug/netstandard2.0/Tgstation.Server.Api.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Api.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("API definitions for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.0.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.0.0")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.0.0")]
23 
24 // Generated by the MSBuild WriteCodeFragment class.
25 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("API definitions for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.0.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.0.0")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.0.0")]
23 
24 // Generated by the MSBuild WriteCodeFragment class.
25 
diff --git a/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html index f0cb13b64e..8aba9d97b3 100644 --- a/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_api_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Api.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Api.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Api.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Api.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Api.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Api.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Api.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html index f735cdd6bf..e0e4ccf475 100644 --- a/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_api_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Api.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Api.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Api.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_client_8_assembly_info_8cs.html b/_tgstation_8_server_8_client_8_assembly_info_8cs.html index c1b2251f60..fb681e6583 100644 --- a/_tgstation_8_server_8_client_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_client_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Client/obj/Release/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Client/obj/Debug/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html index b98b2e720e..5c3ccaf7e9 100644 --- a/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_client_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Client/obj/Release/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Client/obj/Debug/netstandard2.0/Tgstation.Server.Client.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Client.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station 13")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("Client library for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.2.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.2")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.2.0")]
23 [assembly: System.Resources.NeutralResourcesLanguageAttribute("en-CA")]
24 
25 // Generated by the MSBuild WriteCodeFragment class.
26 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("/tg/station 13")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyCopyrightAttribute("2018")]
17 [assembly: System.Reflection.AssemblyDescriptionAttribute("Client library for tgstation-server")]
18 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.2.0")]
19 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.2")]
20 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client")]
21 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client")]
22 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.2.0")]
23 [assembly: System.Resources.NeutralResourcesLanguageAttribute("en-CA")]
24 
25 // Generated by the MSBuild WriteCodeFragment class.
26 
diff --git a/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html index 3eef0f64ad..3c2a097db5 100644 --- a/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_client_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Client.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Client.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Client.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Client.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Client.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Client.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Client.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html index 0bb889fa5e..9ef26d4cd5 100644 --- a/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_client_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Client.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Client.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html b/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html index 26330c723a..49b8ec6190 100644 --- a/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html +++ b/_tgstation_8_server_8_host_2_components_2_instance_8cs_source.html @@ -94,10 +94,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.Extensions.Logging;
3 using Octokit;
4 using System;
5 using System.Collections.Generic;
6 using System.Globalization;
7 using System.Linq;
8 using System.Threading;
9 using System.Threading.Tasks;
18 
19 namespace Tgstation.Server.Host.Components
20 {
22  sealed class Instance : IInstance
23  {
26 
28  public IByondManager ByondManager { get; }
29 
31  public IDreamMaker DreamMaker { get; }
32 
34  public IWatchdog Watchdog { get; }
35 
37  public IChat Chat { get; }
38 
40  public StaticFiles.IConfiguration Configuration { get; }
41 
43  public ICompileJobConsumer CompileJobConsumer { get; }
44 
49 
54 
59 
64 
69 
73  readonly ILogger<Instance> logger;
74 
78  readonly Api.Models.Instance metadata;
79 
83  Task timerTask;
87  CancellationTokenSource timerCts;
88 
106  public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IGitHubClientFactory gitHubClientFactory, ILogger<Instance> logger)
107  {
108  this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
109  RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
110  ByondManager = byondManager ?? throw new ArgumentNullException(nameof(byondManager));
111  DreamMaker = dreamMaker ?? throw new ArgumentNullException(nameof(dreamMaker));
112  Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
113  Chat = chat ?? throw new ArgumentNullException(nameof(chat));
114  Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
115  CompileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
116  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
117  this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
118  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
119  this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
120  this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory));
121  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
122  }
123 
125  public void Dispose()
126  {
127  timerCts?.Dispose();
128  CompileJobConsumer.Dispose();
129  Configuration.Dispose();
130  Chat.Dispose();
131  Watchdog.Dispose();
133  }
134 
136  public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action<int> progressReporter, CancellationToken cancellationToken)
137  {
138  //DO NOT FOLLOW THE SUGGESTION FOR A THROW EXPRESSION HERE
139  if (job == null)
140  throw new ArgumentNullException(nameof(job));
141  if (databaseContext == null)
142  throw new ArgumentNullException(nameof(databaseContext));
143  if (progressReporter == null)
144  throw new ArgumentNullException(nameof(progressReporter));
145 
146  var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings
147  {
148  StartupTimeout = x.StartupTimeout,
149  }).FirstOrDefaultAsync(cancellationToken);
150 
151  var compileJobsTask = databaseContext.CompileJobs
152  .Where(x => x.Job.Instance.Id == metadata.Id)
153  .OrderByDescending(x => x.Job.StoppedAt)
154  //TODO: Replace with this select when the issues linked in https://github.com/tgstation/tgstation-server/issues/737 are fixed
155  //.Select(x => x.Job.StoppedAt.Value - x.Job.StartedAt.Value)
156  .Select(x => new Job
157  {
158  StoppedAt = x.Job.StoppedAt,
159  StartedAt = x.Job.StartedAt
160  })
161  .Take(10)
162  .ToListAsync(cancellationToken);
163 
164  var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
165  if (dreamMakerSettings == default)
166  throw new JobException("Missing DreamMakerSettings in DB!");
167  var ddSettings = await ddSettingsTask.ConfigureAwait(false);
168  if (ddSettings == default)
169  throw new JobException("Missing DreamDaemonSettings in DB!");
170 
171  Task<RepositorySettings> repositorySettingsTask = null;
172  string repoOwner = null;
173  string repoName = null;
174  CompileJob compileJob;
175  RevisionInformation revInfo;
176  using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
177  {
178  if (repo == null)
179  throw new JobException("Missing Repository!");
180 
181  if (repo.IsGitHubRepository)
182  {
183  repoOwner = repo.GitHubOwner;
184  repoName = repo.GitHubRepoName;
185  repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).Select(x => new RepositorySettings
186  {
187  AccessToken = x.AccessToken,
188  ShowTestMergeCommitters = x.ShowTestMergeCommitters
189  }).FirstOrDefaultAsync(cancellationToken);
190  }
191 
192  var repoSha = repo.Head;
193  revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy).FirstOrDefaultAsync().ConfigureAwait(false);
194 
195  if (revInfo == default)
196  {
197  revInfo = new RevisionInformation
198  {
199  CommitSha = repoSha,
200  OriginCommitSha = repoSha,
201  Instance = new Models.Instance
202  {
203  Id = metadata.Id
204  }
205  };
206  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
207  databaseContext.Instances.Attach(revInfo.Instance);
208  }
209 
210  TimeSpan? averageSpan = null;
211  var previousCompileJobs = await compileJobsTask.ConfigureAwait(false);
212  if(previousCompileJobs.Count != 0)
213  {
214  var totalSpan = TimeSpan.Zero;
215  foreach (var I in previousCompileJobs)
216  totalSpan += I.StoppedAt.Value - I.StartedAt.Value;
217  averageSpan = totalSpan / previousCompileJobs.Count;
218  }
219 
220  compileJob = await DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, progressReporter, averageSpan, cancellationToken).ConfigureAwait(false);
221  }
222 
223  compileJob.Job = job;
224 
225  databaseContext.CompileJobs.Add(compileJob); //will be saved by job context
226 
227  job.PostComplete = ct => CompileJobConsumer.LoadCompileJob(compileJob, ct);
228 
229  if (repositorySettingsTask != null)
230  {
231  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
232  if (repositorySettings == default)
233  throw new JobException("Missing repository settings!");
234 
235  if (repositorySettings.AccessToken != null)
236  {
237  //potential for commenting on a test merge change
238  var outgoingCompileJob = LatestCompileJob();
239 
240  if(outgoingCompileJob != null && outgoingCompileJob.RevisionInformation.CommitSha != compileJob.RevisionInformation.CommitSha)
241  {
242  var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken);
243 
244  async Task CommentOnPR(int prNumber, string comment)
245  {
246  try
247  {
248  await gitHubClient.Issue.Comment.Create(repoOwner, repoName, prNumber, comment).ConfigureAwait(false);
249  }
250  catch (ApiException e)
251  {
252  logger.LogWarning("Error posting GitHub comment! Exception: {0}", e);
253  }
254  }
255 
256  var tasks = new List<Task>();
257 
258  string FormatTestMerge(TestMerge testMerge, bool updated) => String.Format(CultureInfo.InvariantCulture, "#### Test Merge {4}{0}{0}##### Server Instance{0}{5}{1}{0}{0}##### Revision{0}Origin: {6}{0}Pull Request: {2}{0}Server: {7}{3}",
259  Environment.NewLine,
260  repositorySettings.ShowTestMergeCommitters.Value ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Merged By{0}{1}", Environment.NewLine, testMerge.MergedBy.Name) : String.Empty,
261  testMerge.PullRequestRevision,
262  testMerge.Comment != null ? String.Format(CultureInfo.InvariantCulture, "{0}{0}##### Comment{0}{1}", Environment.NewLine, testMerge.Comment) : String.Empty,
263  updated ? "Updated" : "Deployed",
264  metadata.Name,
266  compileJob.RevisionInformation.CommitSha
267  );
268 
269  //added prs
270  foreach (var I in compileJob
272  .ActiveTestMerges
273  .Select(x => x.TestMerge)
274  .Where(x => !outgoingCompileJob
276  .ActiveTestMerges
277  .Any(y => y.TestMerge.Number == x.Number)))
278  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, false)));
279 
280  //removed prs
281  foreach (var I in outgoingCompileJob
283  .ActiveTestMerges
284  .Select(x => x.TestMerge)
285  .Where(x => !compileJob
288  .Any(y => y.TestMerge.Number == x.Number)))
289  tasks.Add(CommentOnPR(I.Number.Value, "#### Test Merge Removed"));
290 
291  //updated prs
292  foreach(var I in compileJob
294  .ActiveTestMerges
295  .Select(x => x.TestMerge)
296  .Where(x => outgoingCompileJob
297  .RevisionInformation
298  .ActiveTestMerges
299  .Any(y => y.TestMerge.Number == x.Number)))
300  tasks.Add(CommentOnPR(I.Number.Value, FormatTestMerge(I, true)));
301 
302  if (tasks.Any())
303  await Task.WhenAll(tasks).ConfigureAwait(false);
304  }
305  }
306  }
307  }
308 
315  async Task TimerLoop(uint minutes, CancellationToken cancellationToken)
316  {
317  while (true)
318  try
319  {
320  await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false);
321  logger.LogDebug("Beginning auto update...");
322  await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, new List<string>(), cancellationToken).ConfigureAwait(false);
323  try
324  {
325  Models.User user = null;
326  await databaseContextFactory.UseContext(async (db) => user = await db.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
327  var repositoryUpdateJob = new Job
328  {
329  Instance = new Models.Instance
330  {
331  Id = metadata.Id
332  },
333  Description = "Scheduled repository update",
334  CancelRightsType = RightsType.Repository,
335  CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
336  StartedBy = user
337  };
338 
339  string deploySha = null;
340  await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) =>
341  {
342  var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
343 
344  //assume 5 steps with synchronize
345  const int ProgressSections = 7;
346  const int ProgressStep = 100 / ProgressSections;
347 
348 
349  const int NumSteps = 3;
350  var doneSteps = 0;
351 
352  Action<int> NextProgressReporter()
353  {
354  var tmpDoneSteps = doneSteps;
355  ++doneSteps;
356  return progress => progressReporter((progress + 100 * tmpDoneSteps) / NumSteps);
357  };
358 
359  using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false))
360  {
361  if (repo == null)
362  {
363  logger.LogTrace("Aborting repo update, no repository!");
364  return;
365  }
366 
367  var startSha = repo.Head;
368  if (!repo.Tracking)
369  {
370  logger.LogTrace("Aborting repo update, not tracking origin!");
371  deploySha = startSha;
372  return;
373  }
374 
375  var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
376 
377  //the main point of auto update is to pull the remote
378  await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
379 
380  RevisionInformation currentRevInfo = null;
381  bool hasDbChanges = false;
382 
383  Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
384  .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
385  .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
386  .FirstOrDefaultAsync(cancellationToken);
387 
388  async Task UpdateRevInfo(string currentHead, bool onOrigin)
389  {
390  if(currentRevInfo == null)
391  currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
392 
393  if (currentRevInfo == default)
394  {
395  logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
396  onOrigin = true;
397  }
398 
399  var attachedInstance = new Models.Instance
400  {
401  Id = metadata.Id
402  };
403  var oldRevInfo = currentRevInfo;
404  currentRevInfo = new RevisionInformation
405  {
406  CommitSha = currentHead,
407  OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
408  Instance = attachedInstance
409  };
410  if (!onOrigin)
411  currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
412 
413  databaseContext.Instances.Attach(attachedInstance);
414  databaseContext.RevisionInformations.Add(currentRevInfo);
415  hasDbChanges = true;
416  }
417 
418  //take appropriate auto update actions
419  bool shouldSyncTracked;
420  if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
421  {
422  logger.LogTrace("Preserving test merges...");
423 
424  var currentRevInfoTask = LoadRevInfo();
425 
426  var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
427 
428  if (!result.HasValue)
429  throw new JobException("Merge conflict while preserving test merges!");
430 
431  currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
432 
433  var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
434  var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
435 
436  var currentHead = repo.Head;
437  if (currentHead != startSha)
438  {
439  await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
440  shouldSyncTracked = stillOnOrigin;
441  }
442  else
443  shouldSyncTracked = false;
444  }
445  else
446  {
447  logger.LogTrace("Not preserving test merges...");
448  await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
449 
450  var currentHead = repo.Head;
451 
452  currentRevInfo = await databaseContext.RevisionInformations
453  .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
454  .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
455 
456  if (currentHead != startSha && currentRevInfo != default)
457  await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
458 
459  shouldSyncTracked = true;
460  }
461 
462  //synch if necessary
463  if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
464  {
465  var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
466  var currentHead = repo.Head;
467  if (currentHead != currentRevInfo.CommitSha)
468  await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
469  }
470 
471  if(hasDbChanges)
472  try
473  {
474  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
475  }
476  catch
477  {
478  await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
479  throw;
480  }
481 
482  progressReporter(5 * ProgressStep);
483  deploySha = repo.Head;
484  }
485  }, cancellationToken).ConfigureAwait(false);
486 
487  await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
488 
489  if (deploySha == null)
490  {
491  logger.LogTrace("Aborting auto update, repository error!");
492  continue;
493  }
494 
495  if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha)
496  {
497  logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
498  continue;
499  }
500 
501  //finally set up the job
502  var compileProcessJob = new Job
503  {
504  StartedBy = user,
505  Instance = repositoryUpdateJob.Instance,
506  Description = "Scheduled code deployment",
507  CancelRightsType = RightsType.DreamMaker,
508  CancelRight = (ulong)DreamMakerRights.CancelCompile
509  };
510 
511  await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false);
512 
513  await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false);
514  }
515  catch (OperationCanceledException)
516  {
517  logger.LogDebug("Cancelled auto update job!");
518  throw;
519  }
520  catch (Exception e)
521  {
522  logger.LogWarning("Error in auto update loop! Exception: {0}", e);
523  continue;
524  }
525  }
526  catch (OperationCanceledException)
527  {
528  break;
529  }
530  logger.LogTrace("Leaving auto update loop...");
531  }
532 
534  public void Rename(string newName)
535  {
536  if (String.IsNullOrWhiteSpace(newName))
537  throw new ArgumentNullException(nameof(newName));
538  metadata.Name = newName;
539  }
540 
542  public async Task StartAsync(CancellationToken cancellationToken)
543  {
544  await Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval.Value), Configuration.StartAsync(cancellationToken), ByondManager.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), CompileJobConsumer.StartAsync(cancellationToken)).ConfigureAwait(false);
545 
546  //dependent on so many things, its just safer this way
547  await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
548 
549  CompileJob latestCompileJob = null;
550  await databaseContextFactory.UseContext(async db =>
551  {
552  latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
553  }).ConfigureAwait(false);
554  await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false);
555  }
556 
558  public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(0), Configuration.StopAsync(cancellationToken), ByondManager.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), CompileJobConsumer.StopAsync(cancellationToken));
559 
561  public async Task SetAutoUpdateInterval(uint newInterval)
562  {
563  Task toWait;
564  lock (this)
565  {
566  if (timerTask != null)
567  {
568  timerCts.Cancel();
569  toWait = timerTask;
570  }
571  else
572  toWait = Task.CompletedTask;
573  }
574  await toWait.ConfigureAwait(false);
575  if (newInterval == 0)
576  return;
577  lock (this)
578  {
579  //race condition, just quit
580  if (timerTask != null)
581  return;
582  timerCts?.Dispose();
583  timerCts = new CancellationTokenSource();
584  timerTask = TimerLoop(newInterval, timerCts.Token);
585  }
586  }
587 
590  {
591  if (!dmbFactory.DmbAvailable)
592  return null;
593  return dmbFactory.LockNextDmb(0)?.CompileJob;
594  }
595  }
596 }
string Name
The name of the User
Definition: User.cs:38
- +
Manages the runtime of Jobs
Definition: IJobManager.cs:12
-
DbSet< Instance > Instances
The Instances in the IDatabaseContext
+
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
@@ -141,9 +141,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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:990
+
async Task StopAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:983
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Instance
Definition: Instance.cs:48
-
DbSet< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the IDatabaseContext
+
DbSet< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the IDatabaseContext
For interacting with the instance services
Definition: IInstance.cs:17
DreamMakerRights
Rights for Models.DreamMaker
@@ -154,17 +154,17 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
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
-
DbSet< CompileJob > CompileJobs
The CompileJobs in the IDatabaseContext
+
DbSet< CompileJob > CompileJobs
The CompileJobs in the IDatabaseContext
-
DbSet< DreamMakerSettings > DreamMakerSettings
The Models.DreamMakerSettings in the IDatabaseContext
+
DbSet< DreamMakerSettings > DreamMakerSettings
The Models.DreamMakerSettings in the IDatabaseContext
Instance Instance
The Models.Instance the RevisionInformation belongs to
-
DbSet< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
+
DbSet< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
Factory for creating and loading IRepositorys
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:956
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:949
async Task StartAsync(CancellationToken cancellationToken)
Definition: Chat.cs:525
@@ -173,7 +173,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
readonly IJobManager jobManager
The IJobManager for the Instance
Definition: Instance.cs:58
RevisionInformation RevisionInformation
See Api.Models.CompileJob.RevisionInformation
Definition: CompileJob.cs:24
-
DbSet< DreamDaemonSettings > DreamDaemonSettings
The Models.DreamDaemonSettings in the IDatabaseContext
+
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
Task StopAsync(CancellationToken cancellationToken)
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 a3055d9d63..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.3")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.3")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.3")]
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 b22daa9bae..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.3")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.3")]
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.3")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("4.0.1.4")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.4")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("4.0.1.4")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html index 76fc39106d..5d18b1948e 100644 --- a/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_console_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Console.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Console.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Console.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html index 37466fca50..53fc29de6a 100644 --- a/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_host_8_console_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html index 3203ae4cf7..22f2c188b8 100644 --- a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html index c53270b03d..38a995518c 100644 --- a/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_razor_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host/obj/Release/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs Source File +tgstation-server: src/Tgstation.Server.Host/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.RazorAssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html index b50f09579f..13e5231737 100644 --- a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs File Reference +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html index 4e22852711..76db250606 100644 --- a/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html +++ b/_tgstation_8_server_8_host_8_tests_8_assembly_info_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Tests.AssemblyInfo.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
@@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
Tgstation.Server.Host.Tests.AssemblyInfo.cs
-Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
+Go to the documentation of this file.
1 //------------------------------------------------------------------------------
2 // <auto-generated>
3 // This code was generated by a tool.
4 // Runtime Version:4.0.30319.42000
5 //
6 // Changes to this file may cause incorrect behavior and will be lost if
7 // the code is regenerated.
8 // </auto-generated>
9 //------------------------------------------------------------------------------
10 
11 using System;
12 using System.Reflection;
13 
14 [assembly: System.Reflection.AssemblyCompanyAttribute("Tgstation.Server.Host.Tests")]
15 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
16 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
18 [assembly: System.Reflection.AssemblyProductAttribute("Tgstation.Server.Host.Tests")]
19 [assembly: System.Reflection.AssemblyTitleAttribute("Tgstation.Server.Host.Tests")]
20 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
21 
22 // Generated by the MSBuild WriteCodeFragment class.
23 
diff --git a/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html b/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html index bd9abbdcce..6f4b8b8668 100644 --- a/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html +++ b/_tgstation_8_server_8_host_8_tests_8_program_8cs_source.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Release/netcoreapp2.1/Tgstation.Server.Host.Tests.Program.cs Source File +tgstation-server: tests/Tgstation.Server.Host.Tests/obj/Debug/netcoreapp2.1/Tgstation.Server.Host.Tests.Program.cs Source File @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html index 33f02c41de..1966731549 100644 --- a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html +++ b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs.html @@ -4,7 +4,7 @@ -tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Release/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs File Reference +tgstation-server: src/Tgstation.Server.Host.Watchdog/obj/Debug/netstandard2.0/Tgstation.Server.Host.Watchdog.AssemblyInfo.cs File Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html b/_tgstation_8_server_8_host_8_watchdog_8_assembly_info_8cs_source.html index 10d748ad08..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.3")]
17 [assembly: System.Reflection.AssemblyInformationalVersionAttribute("4.0.1.3")]
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.3")]
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/_user_controller_8cs_source.html b/_user_controller_8cs_source.html index 5eb9a5a6f1..01dda92f66 100644 --- a/_user_controller_8cs_source.html +++ b/_user_controller_8cs_source.html @@ -92,7 +92,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
UserController.cs
-Go to the documentation of this file.
1 using Microsoft.AspNetCore.Mvc;
2 using Microsoft.EntityFrameworkCore;
3 using Microsoft.Extensions.Logging;
4 using Microsoft.Extensions.Options;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Net;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Tgstation.Server.Api;
18 
19 namespace Tgstation.Server.Host.Controllers
20 {
24  [Route(Routes.User)]
25  public sealed class UserController : ModelController<UserUpdate>
26  {
31 
36 
40  readonly ILogger<UserController> logger;
41 
46 
56  public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger<UserController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false)
57  {
58  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
59  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
60  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
61  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
62  }
63 
65  [TgsAuthorize(AdministrationRights.WriteUsers)]
66  public override async Task<IActionResult> Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
67  {
68  if (model == null)
69  throw new ArgumentNullException(nameof(model));
70 
71  if (!(model.Password == null ^ model.SystemIdentifier == null))
72  return BadRequest(new ErrorMessage { Message = "User must have exactly one of either a password or system identifier!" });
73 
74  model.Name = model.Name?.Trim();
75  if (model.Name?.Length == 0)
76  model.Name = null;
77 
78  if (!(model.Name == null ^ model.SystemIdentifier == null))
79  return BadRequest(new ErrorMessage { Message = "User must have a name if and only if user has no system identifier!" });
80 
81  var dbUser = new Models.User
82  {
83  AdministrationRights = model.AdministrationRights ?? AdministrationRights.None,
84  CreatedAt = DateTimeOffset.Now,
85  CreatedBy = AuthenticationContext.User,
86  Enabled = model.Enabled ?? false,
87  InstanceManagerRights = model.InstanceManagerRights ?? InstanceManagerRights.None,
88  Name = model.Name,
89  SystemIdentifier = model.SystemIdentifier,
90  InstanceUsers = new List<Models.InstanceUser>()
91  };
92 
93  if (model.SystemIdentifier != null)
94  try
95  {
96  using (var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken).ConfigureAwait(false))
97  {
98  if (sysIdentity == null)
99  return StatusCode((int)HttpStatusCode.Gone);
100  dbUser.Name = sysIdentity.Username;
101  dbUser.SystemIdentifier = sysIdentity.Uid;
102  }
103  }
104  catch (NotImplementedException)
105  {
106  return StatusCode((int)HttpStatusCode.NotImplemented);
107  }
108  else
109  {
110  if (model.Password.Length < generalConfiguration.MinimumPasswordLength)
111  return BadRequest(new ErrorMessage { Message = String.Format(CultureInfo.InvariantCulture, "Password must be at least {0} characters long!", generalConfiguration.MinimumPasswordLength) });
112  cryptographySuite.SetUserPassword(dbUser, model.Password, true);
113  }
114 
115  dbUser.CanonicalName = dbUser.Name.ToUpperInvariant();
116 
117  DatabaseContext.Users.Add(dbUser);
118 
119  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
120 
121  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true));
122  }
123 
125  [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
126  public override async Task<IActionResult> Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
127  {
128  if (model == null)
129  throw new ArgumentNullException(nameof(model));
130 
131  var passwordEditOnly = !AuthenticationContext.User.AdministrationRights.Value.HasFlag(AdministrationRights.WriteUsers);
132 
133  var originalUser = passwordEditOnly ? AuthenticationContext.User : await DatabaseContext.Users.Where(x => x.Id == model.Id)
134  .Include(x => x.CreatedBy)
135  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
136  if (originalUser == default)
137  return NotFound();
138 
139  if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.SystemIdentifier != null || model.Name != null))
140  return Forbid();
141 
142  if (model.Password != null)
143  {
144  if (originalUser.PasswordHash == null)
145  return BadRequest(new ErrorMessage { Message = "Cannot convert a system user to a password user!" });
146  cryptographySuite.SetUserPassword(originalUser, model.Password, false);
147  }
148  else if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
149  return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" });
150 
151  if (model.Name != null && model.Name.ToUpperInvariant() != originalUser.CanonicalName)
152  return BadRequest(new ErrorMessage { Message = "Can only change capitalization of a user's name!" });
153 
154  originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights;
155  originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights;
156  originalUser.Enabled = model.Enabled ?? originalUser.Enabled;
157  originalUser.Name = model.Name ?? originalUser.Name;
158 
159  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
160 
161  return Json(model.Id == originalUser.Id || (AuthenticationContext.GetRight(RightsType.Administration) & (ulong)AdministrationRights.ReadUsers) != 0 ? originalUser.ToApi(true) : new Api.Models.User
162  {
163  Id = originalUser.Id
164  });
165  }
166 
168  [TgsAuthorize]
169  public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult<IActionResult>(Json(AuthenticationContext.User.ToApi(true)));
170 
172  [TgsAuthorize(AdministrationRights.ReadUsers)]
173  public override async Task<IActionResult> List(CancellationToken cancellationToken)
174  {
175  var users = await DatabaseContext.Users
176  .Include(x => x.CreatedBy)
177  .ToListAsync(cancellationToken).ConfigureAwait(false);
178  return Json(users.Select(x => x.ToApi(true)));
179  }
180 
182  [TgsAuthorize]
183  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
184  {
185  if (id == AuthenticationContext.User.Id)
186  return await Read(cancellationToken).ConfigureAwait(false);
187 
188  if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
189  return Forbid();
190 
191  var user = await DatabaseContext.Users
192  .Where(x => x.Id == id)
193  .Include(x => x.CreatedBy)
194  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
195  if (user == default)
196  return NotFound();
197  return Json(user.ToApi(true));
198  }
199  }
200 }
+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.Net;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Tgstation.Server.Api;
18 
19 namespace Tgstation.Server.Host.Controllers
20 {
24  [Route(Routes.User)]
25  public sealed class UserController : ModelController<UserUpdate>
26  {
31 
36 
40  readonly ILogger<UserController> logger;
41 
46 
56  public UserController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ISystemIdentityFactory systemIdentityFactory, ICryptographySuite cryptographySuite, ILogger<UserController> logger, IOptions<GeneralConfiguration> generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false)
57  {
58  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
59  this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
60  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
61  generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
62  }
63 
65  [TgsAuthorize(AdministrationRights.WriteUsers)]
66  public override async Task<IActionResult> Create([FromBody] UserUpdate model, CancellationToken cancellationToken)
67  {
68  if (model == null)
69  throw new ArgumentNullException(nameof(model));
70 
71  if (!(model.Password == null ^ model.SystemIdentifier == null))
72  return BadRequest(new ErrorMessage { Message = "User must have exactly one of either a password or system identifier!" });
73 
74  model.Name = model.Name?.Trim();
75  if (model.Name?.Length == 0)
76  model.Name = null;
77 
78  if (!(model.Name == null ^ model.SystemIdentifier == null))
79  return BadRequest(new ErrorMessage { Message = "User must have a name if and only if user has no system identifier!" });
80 
81  var dbUser = new Models.User
82  {
83  AdministrationRights = model.AdministrationRights ?? AdministrationRights.None,
84  CreatedAt = DateTimeOffset.Now,
85  CreatedBy = AuthenticationContext.User,
86  Enabled = model.Enabled ?? false,
87  InstanceManagerRights = model.InstanceManagerRights ?? InstanceManagerRights.None,
88  Name = model.Name,
89  SystemIdentifier = model.SystemIdentifier,
90  InstanceUsers = new List<Models.InstanceUser>()
91  };
92 
93  if (model.SystemIdentifier != null)
94  try
95  {
96  using (var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken).ConfigureAwait(false))
97  {
98  if (sysIdentity == null)
99  return StatusCode((int)HttpStatusCode.Gone);
100  dbUser.Name = sysIdentity.Username;
101  dbUser.SystemIdentifier = sysIdentity.Uid;
102  }
103  }
104  catch (NotImplementedException)
105  {
106  return StatusCode((int)HttpStatusCode.NotImplemented);
107  }
108  else
109  {
110  if (model.Password.Length < generalConfiguration.MinimumPasswordLength)
111  return BadRequest(new ErrorMessage { Message = String.Format(CultureInfo.InvariantCulture, "Password must be at least {0} characters long!", generalConfiguration.MinimumPasswordLength) });
112  cryptographySuite.SetUserPassword(dbUser, model.Password, true);
113  }
114 
115  dbUser.CanonicalName = dbUser.Name.ToUpperInvariant();
116 
117  DatabaseContext.Users.Add(dbUser);
118 
119  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
120 
121  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true));
122  }
123 
125  [TgsAuthorize(AdministrationRights.WriteUsers | AdministrationRights.EditOwnPassword)]
126  public override async Task<IActionResult> Update([FromBody] UserUpdate model, CancellationToken cancellationToken)
127  {
128  if (model == null)
129  throw new ArgumentNullException(nameof(model));
130 
131  var passwordEditOnly = !AuthenticationContext.User.AdministrationRights.Value.HasFlag(AdministrationRights.WriteUsers);
132 
133  var originalUser = passwordEditOnly ? AuthenticationContext.User : await DatabaseContext.Users.Where(x => x.Id == model.Id)
134  .Include(x => x.CreatedBy)
135  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
136  if (originalUser == default)
137  return NotFound();
138 
139  if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.SystemIdentifier != null || model.Name != null))
140  return Forbid();
141 
142  if (model.Password != null)
143  {
144  if (originalUser.PasswordHash == null)
145  return BadRequest(new ErrorMessage { Message = "Cannot convert a system user to a password user!" });
146  cryptographySuite.SetUserPassword(originalUser, model.Password, false);
147  }
148  else if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
149  return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" });
150 
151  if (model.Name != null && model.Name.ToUpperInvariant() != originalUser.CanonicalName)
152  return BadRequest(new ErrorMessage { Message = "Can only change capitalization of a user's name!" });
153 
154  originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights;
155  originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights;
156  originalUser.Enabled = model.Enabled ?? originalUser.Enabled;
157  originalUser.Name = model.Name ?? originalUser.Name;
158 
159  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
160 
161  return Json(model.Id == originalUser.Id || (AuthenticationContext.GetRight(RightsType.Administration) & (ulong)AdministrationRights.ReadUsers) != 0 ? originalUser.ToApi(true) : new Api.Models.User
162  {
163  Id = originalUser.Id
164  });
165  }
166 
168  [TgsAuthorize]
169  public override Task<IActionResult> Read(CancellationToken cancellationToken) => Task.FromResult<IActionResult>(Json(AuthenticationContext.User.ToApi(true)));
170 
172  [TgsAuthorize(AdministrationRights.ReadUsers)]
173  public override async Task<IActionResult> List(CancellationToken cancellationToken)
174  {
175  var users = await DatabaseContext.Users
176  .Include(x => x.CreatedBy)
177  .ToListAsync(cancellationToken).ConfigureAwait(false);
178  return Json(users.Select(x => x.ToApi(true)));
179  }
180 
182  [TgsAuthorize]
183  public override async Task<IActionResult> GetId(long id, CancellationToken cancellationToken)
184  {
185  if (id == AuthenticationContext.User.Id)
186  return await Read(cancellationToken).ConfigureAwait(false);
187 
188  if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
189  return Forbid();
190 
191  var user = await DatabaseContext.Users
192  .Where(x => x.Id == id)
193  .Include(x => x.CreatedBy)
194  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
195  if (user == default)
196  return NotFound();
197  return Json(user.ToApi(true));
198  }
199  }
200 }
const string User
The Models.User controller
Definition: Routes.cs:24
@@ -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/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_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 51628310c5..aac331b237 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().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel-members.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel-members.html index 2d5e378ed5..f9c92868c2 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel-members.html @@ -101,8 +101,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); ConnectionNameTgstation.Server.Host.Components.Chat.Channel FriendlyNameTgstation.Server.Host.Components.Chat.Channel IdTgstation.Server.Host.Components.Chat.Channel - IsAdminTgstation.Server.Host.Components.Chat.Channel - IsPrivateTgstation.Server.Host.Components.Chat.Channel + IsAdminChannelTgstation.Server.Host.Components.Chat.Channel + IsPrivateChannelTgstation.Server.Host.Components.Chat.Channel RealIdTgstation.Server.Host.Components.Chat.Channel TagTgstation.Server.Host.Components.Chat.Channel
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel.html index 70191ffbb6..84177ec693 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_channel.html @@ -116,12 +116,12 @@ Properties string ConnectionName [get, set]  The name of the connection the Channel belongs to More...
  -bool IsAdmin [get, set] - If this is considered a channel for admin commands More...
-  -bool IsPrivate [get, set] - If this is a 1-to-1 chat channel More...
-  +bool IsAdminChannel [get, set] + If this is considered a channel for admin commands More...
+  +bool IsPrivateChannel [get, set] + If this is a 1-to-1 chat channel More...
+  string Tag [get, set]  For user use More...
  @@ -205,7 +205,7 @@ Properties
- +
@@ -213,7 +213,7 @@ Properties @@ -227,11 +227,11 @@ Properties

Definition at line 41 of file Channel.cs.

-

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

+

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

- +
- +
bool Tgstation.Server.Host.Components.Chat.Channel.IsAdminbool Tgstation.Server.Host.Components.Chat.Channel.IsAdminChannel
@@ -239,7 +239,7 @@ Properties @@ -306,7 +306,7 @@ Properties

Definition at line 51 of file Channel.cs.

-

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

+

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

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat.html index 8152420e09..450021ad56 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_chat.html @@ -627,18 +627,17 @@ Private Attributes

Definition at line 191 of file Chat.cs.

-

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

-
192  {
193  //map the channel if it's private and we haven't seen it
194  lock (providers)
195  {
196  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
197  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
198  if (message.User.Channel.IsPrivate)
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.IsAdmin = mapping.Channel.IsAdmin;
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.IsPrivate)
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.IsAdmin)
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  }
+

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

+
192  {
193  //map the channel if it's private and we haven't seen it
194  lock (providers)
195  {
196  var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
197  var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
198  if (message.User.Channel.IsPrivateChannel)
199  lock (mappedChannels)
200  {
201  if (!provider.Connected)
202  return;
203  if (!enumerable.Any())
204  {
205  ulong newId;
206  lock (this)
207  newId = channelIdCounter++;
208  mappedChannels.Add(newId, new ChannelMapping
209  {
210  IsWatchdogChannel = false,
211  ProviderChannelId = message.User.Channel.RealId,
212  ProviderId = providerId,
213  Channel = message.User.Channel
214  });
215  message.User.Channel.RealId = newId;
216  }
217  else
218  message.User.Channel.RealId = enumerable.First().Key;
219  }
220  else
221  {
222  //need to add tag and isAdminChannel
223  var mapping = enumerable.First().Value;
224  message.User.Channel.Id = mapping.Channel.Id;
225  message.User.Channel.Tag = mapping.Channel.Tag;
226  message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
227  }
228  }
229 
230  var splits = new List<string>(message.Content.Trim().Split(' '));
231  var address = splits[0];
232  if (address.Length > 1 && (address[address.Length - 1] == ':' || address[address.Length - 1] == ','))
233  address = address.Substring(0, address.Length - 1);
234 
235  address = address.ToUpperInvariant();
236 
237  var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
238 
239  if (!addressed && !message.User.Channel.IsPrivateChannel)
240  //no mention
241  return;
242 
243  logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
244 
245  if (addressed)
246  splits.RemoveAt(0);
247 
248  if (splits.Count == 0)
249  {
250  //just a mention
251  await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
252  return;
253  }
254 
255  var command = splits[0].ToUpperInvariant();
256  splits.RemoveAt(0);
257  var arguments = String.Join(" ", splits);
258 
259  try
260  {
261  async Task<ICommand> GetCommand(string commandName)
262  {
263  if (!builtinCommands.TryGetValue(commandName, out var handler))
264  {
265  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken));
266  await Task.WhenAll(tasks).ConfigureAwait(false);
267  handler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == commandName).FirstOrDefault();
268  }
269  return handler;
270  };
271 
272  const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
273 
274  if (command == "HELP" || command == "?")
275  {
276  string helpText;
277  if (splits.Count == 0)
278  {
279  var allCommands = builtinCommands.Select(x => x.Value).ToList();
280  var tasks = trackingContexts.Select(x => x.GetCustomCommands(cancellationToken)).ToList();
281  await Task.WhenAll(tasks).ConfigureAwait(false);
282  allCommands.AddRange(tasks.SelectMany(x => x.Result));
283  helpText = String.Format(CultureInfo.InvariantCulture, "Available commands (Type '?' or 'help' and then a command name for more details): {0}", String.Join(", ", allCommands.Select(x => x.Name)));
284  }
285  else
286  {
287  var helpHandler = await GetCommand(splits[0].ToUpperInvariant()).ConfigureAwait(false);
288  if (helpHandler != default)
289  helpText = String.Format(CultureInfo.InvariantCulture, "{0}: {1}{2}", helpHandler.Name, helpHandler.HelpText, helpHandler.AdminOnly ? " - May only be used in admin channels" : String.Empty);
290  else
291  helpText = UnknownCommandMessage;
292  }
293  await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
294  return;
295  }
296 
297  var commandHandler = await GetCommand(command).ConfigureAwait(false);
298 
299  if (commandHandler == default)
300  {
301  await SendMessage(UnknownCommandMessage, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
302  return;
303  }
304 
305  if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
306  {
307  await SendMessage("Use this command in an admin channel!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
308  return;
309  }
310 
311  var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false);
312  if (result != null)
313  await SendMessage(result, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
314  }
315  catch (OperationCanceledException)
316  {
317  throw;
318  }
319  catch (Exception e)
320  {
321  //error bc custom commands should reply about why it failed
322  logger.LogError("Error processing chat command: {0}", e);
323  await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
324  }
325  }
User User
The Components.Chat.User who sent the Message
Definition: Message.cs:16
-
bool IsPrivate
If this is a 1-to-1 chat channel
Definition: Channel.cs:46
Channel Channel
The Components.Chat.Channel the user spoke from
Definition: User.cs:40
readonly Dictionary< long, IProvider > providers
Map of IProviders in use, keyed by ChatBot.Id
Definition: Chat.cs:65
+
bool IsPrivateChannel
If this is a 1-to-1 chat channel
Definition: Channel.cs:46
readonly ILogger< Chat > logger
The ILogger for the Chat
Definition: Chat.cs:55
string Id
Backing field for RealId. Represented as a string to avoid BYOND percision loss
Definition: Channel.cs:15
-
bool IsAdmin
If this is considered a channel for admin commands
Definition: Channel.cs:41
readonly Dictionary< ulong, ChannelMapping > mappedChannels
Map of Channel.RealIds to ChannelMappings
Definition: Chat.cs:70
string BotMention
The string that indicates the IProvider was mentioned
Definition: IProvider.cs:21
@@ -647,6 +646,7 @@ Private Attributes
readonly Dictionary< string, ICommand > builtinCommands
Unchanging ICommands in the Chat mapped by ICommand.Name
Definition: Chat.cs:60
Task SendMessage(string message, IEnumerable< ulong > channelIds, CancellationToken cancellationToken)
Send a chat message to a given set of channelIds
Definition: Chat.cs:483
readonly List< IJsonTrackingContext > trackingContexts
The active IJsonTrackingContexts for the Chat
Definition: Chat.cs:75
+
bool IsAdminChannel
If this is considered a channel for admin commands
Definition: Channel.cs:41
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_byond_command.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_byond_command.html index 8dbdae2899..fe21075d9f 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_byond_command.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_byond_command.html @@ -150,7 +150,7 @@ Additional Inherited Members - +
- +
bool Tgstation.Server.Host.Components.Chat.Channel.IsPrivatebool Tgstation.Server.Host.Components.Chat.Channel.IsPrivateChannel
 The help text to display when queires are made about the command More...
 
bool AdminOnly [get]
 If the command should only be available to Users who's User.Channel has Channel.IsAdmin set More...
 If the command should only be available to Users who's User.Channel has Channel.IsAdminChannel set More...
 

Detailed Description

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_custom_command.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_custom_command.html index 5ecc3f86eb..ea29455862 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_custom_command.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_custom_command.html @@ -138,7 +138,7 @@ Properties  The help text to display when queires are made about the command More...
  bool AdminOnly [get] - If the command should only be available to Users who's User.Channel has Channel.IsAdmin set More...
+ If the command should only be available to Users who's User.Channel has Channel.IsAdminChannel set More...
  - +

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_kek_command.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_kek_command.html index a9bf09dd5a..580dfde3c8 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_kek_command.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_kek_command.html @@ -144,7 +144,7 @@ Additional Inherited Members

 The help text to display when queires are made about the command More...
 
bool AdminOnly [get]
 If the command should only be available to Users who's User.Channel has Channel.IsAdmin set More...
 If the command should only be available to Users who's User.Channel has Channel.IsAdminChannel set More...
 

Detailed Description

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_pull_requests_command.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_pull_requests_command.html index b88a263f8d..eb8c65fe5e 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_pull_requests_command.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_pull_requests_command.html @@ -156,7 +156,7 @@ Additional Inherited Members  The help text to display when queires are made about the command More...
  bool AdminOnly [get] - If the command should only be available to Users who's User.Channel has Channel.IsAdmin set More...
+ If the command should only be available to Users who's User.Channel has Channel.IsAdminChannel set More...
 

Detailed Description

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_revision_command.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_revision_command.html index 5d59ef5bcd..0e7e1103da 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_revision_command.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_revision_command.html @@ -153,7 +153,7 @@ Additional Inherited Members  The help text to display when queires are made about the command More...
  bool AdminOnly [get] - If the command should only be available to Users who's User.Channel has Channel.IsAdmin set More...
+ If the command should only be available to Users who's User.Channel has Channel.IsAdminChannel set More...
 

Detailed Description

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_version_command.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_version_command.html index 92ed575e19..3ae362d0a8 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_version_command.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_commands_1_1_version_command.html @@ -147,7 +147,7 @@ Additional Inherited Members  The help text to display when queires are made about the command More...
  bool AdminOnly [get] - If the command should only be available to Users who's User.Channel has Channel.IsAdmin set More...
+ If the command should only be available to Users who's User.Channel has Channel.IsAdminChannel set More...
 

Detailed Description

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_discord_provider.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_discord_provider.html index 9501f991c9..d4ddaadbaa 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_discord_provider.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_discord_provider.html @@ -286,7 +286,7 @@ Additional Inherited Members

Definition at line 81 of file DiscordProvider.cs.

References Tgstation.Server.Host.Components.Chat.Providers.Message.Content, Tgstation.Server.Host.Components.Chat.Channel.Id, and Tgstation.Server.Host.Components.Chat.User.Id.

-
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  IsPrivate = 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  }
override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
Send a message to the IProvider
+
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  }
override async Task SendMessage(ulong channelId, string message, CancellationToken cancellationToken)
Send a message to the IProvider
static string NormalizeMention(string fromDiscord)
Normalize a discord mention string
readonly DiscordSocketClient client
The DiscordSocketClient for the DiscordProvider
readonly List< ulong > mappedChannels
List<T> of mapped ITextChannel IEntity<TId>.Ids
@@ -433,7 +433,7 @@ Additional Inherited Members

Definition at line 172 of file DiscordProvider.cs.

References Tgstation.Server.Api.Models.ChatChannel.DiscordChannelId, Tgstation.Server.Host.Components.Chat.Channel.Id, Tgstation.Server.Api.Models.ChatChannel.IsAdminChannel, and Tgstation.Server.Api.Models.ChatChannel.Tag.

-
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  IsAdmin = channel.IsAdminChannel == true,
192  ConnectionName = discordChannel.Guild.Name,
193  FriendlyName = discordChannel.Name,
194  IsPrivate = 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  }
Indicates a chat channel
Definition: ChatChannel.cs:8
+
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  }
Indicates a chat channel
Definition: ChatChannel.cs:8
readonly DiscordSocketClient client
The DiscordSocketClient for the DiscordProvider
string Tag
A custom tag users can define to group channels together
Definition: ChatChannel.cs:41
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_irc_provider.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_irc_provider.html index 3b9b39d061..7994f05087 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_irc_provider.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_chat_1_1_providers_1_1_irc_provider.html @@ -479,7 +479,7 @@ Additional Inherited Members

Definition at line 329 of file IrcProvider.cs.

-

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

+

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

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

Definition at line 163 of file IrcProvider.cs.

-
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  IsPrivate = 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  }
ulong channelIdCounter
Id counter for channelIdMap
Definition: IrcProvider.cs:77
+
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  }
ulong channelIdCounter
Id counter for channelIdMap
Definition: IrcProvider.cs:77
readonly Dictionary< ulong, string > queryChannelIdMap
Map of Channel.RealIds to query users
Definition: IrcProvider.cs:72
readonly IrcFeatures client
The IrcFeatures client
Definition: IrcProvider.cs:41
readonly string address
Address of the server to connect to
Definition: IrcProvider.cs:46
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance.html index d1bf037b2a..c199521cf9 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance.html @@ -334,7 +334,7 @@ Private Attributes

References Tgstation.Server.Api.Rights.Configuration.

-

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

+

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

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  }
readonly IEventConsumer eventConsumer
The IEventConsumer for the Instance
Definition: Instance.cs:63
@@ -391,8 +391,8 @@ Private Attributes

Definition at line 136 of file Instance.cs.

-

References Tgstation.Server.Api.Models.Internal.RepositorySettings.AccessToken, Tgstation.Server.Host.Models.RevisionInformation.ActiveTestMerges, Tgstation.Server.Api.Models.TestMergeParameters.Comment, Tgstation.Server.Api.Models.Internal.RevisionInformation.CommitSha, Tgstation.Server.Host.Components.Compiler.DreamMaker.Compile(), Tgstation.Server.Host.Models.IDatabaseContext.CompileJobs, Tgstation.Server.Host.Models.IDatabaseContext.DreamDaemonSettings, Tgstation.Server.Host.Models.IDatabaseContext.DreamMakerSettings, Tgstation.Server.Host.Models.RevisionInformation.Instance, Tgstation.Server.Host.Components.Instance.Instance(), Tgstation.Server.Host.Models.IDatabaseContext.Instances, Tgstation.Server.Host.Models.CompileJob.Job, Tgstation.Server.Host.Components.Repository.RepositoryManager.LoadRepository(), Tgstation.Server.Host.Models.TestMerge.MergedBy, Tgstation.Server.Api.Models.Internal.User.Name, Tgstation.Server.Api.Models.Internal.RevisionInformation.OriginCommitSha, Tgstation.Server.Host.Models.Job.PostComplete, Tgstation.Server.Api.Models.TestMergeParameters.PullRequestRevision, Tgstation.Server.Host.Components.Repository.Repository.Repository(), Tgstation.Server.Host.Models.IDatabaseContext.RepositorySettings, Tgstation.Server.Host.Models.CompileJob.RevisionInformation, Tgstation.Server.Host.Models.IDatabaseContext.RevisionInformations, Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.StartupTimeout, and Tgstation.Server.Api.Models.Internal.Job.StoppedAt.

-
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  }
DbSet< Instance > Instances
The Instances in the IDatabaseContext
+

References Tgstation.Server.Api.Models.Internal.RepositorySettings.AccessToken, Tgstation.Server.Host.Models.RevisionInformation.ActiveTestMerges, Tgstation.Server.Api.Models.TestMergeParameters.Comment, Tgstation.Server.Api.Models.Internal.RevisionInformation.CommitSha, Tgstation.Server.Host.Components.Compiler.DreamMaker.Compile(), Tgstation.Server.Host.Models.IDatabaseContext.CompileJobs, Tgstation.Server.Host.Models.IDatabaseContext.DreamDaemonSettings, Tgstation.Server.Host.Models.IDatabaseContext.DreamMakerSettings, Tgstation.Server.Host.Models.RevisionInformation.Instance, Tgstation.Server.Host.Components.Instance.Instance(), Tgstation.Server.Host.Models.IDatabaseContext.Instances, Tgstation.Server.Host.Models.CompileJob.Job, Tgstation.Server.Host.Components.Repository.RepositoryManager.LoadRepository(), Tgstation.Server.Host.Models.TestMerge.MergedBy, Tgstation.Server.Api.Models.Internal.User.Name, Tgstation.Server.Api.Models.Internal.RevisionInformation.OriginCommitSha, Tgstation.Server.Host.Models.Job.PostComplete, Tgstation.Server.Api.Models.TestMergeParameters.PullRequestRevision, Tgstation.Server.Host.Components.Repository.Repository.Repository(), Tgstation.Server.Host.Models.IDatabaseContext.RepositorySettings, Tgstation.Server.Host.Models.CompileJob.RevisionInformation, Tgstation.Server.Host.Models.IDatabaseContext.RevisionInformations, Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.StartupTimeout, and Tgstation.Server.Api.Models.Internal.Job.StoppedAt.

+
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  }
DbSet< Instance > Instances
The Instances in the IDatabaseContext
Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
Load a new job into the ICompileJobConsumer
CompileJob LatestCompileJob()
The latest staged CompileJob
Definition: Instance.cs:589
@@ -406,21 +406,21 @@ Private Attributes
readonly Api.Models.Instance metadata
The Api.Models.Instance for the Instance
Definition: Instance.cs:78
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
-
DbSet< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the IDatabaseContext
+
DbSet< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the IDatabaseContext
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
readonly ILogger< Instance > logger
The ILogger for the Instance
Definition: Instance.cs:73
IGitHubClient CreateClient()
Create a client with anonymous authentication. Low rate limit
-
DbSet< CompileJob > CompileJobs
The CompileJobs in the IDatabaseContext
+
DbSet< CompileJob > CompileJobs
The CompileJobs in the IDatabaseContext
ICompileJobConsumer CompileJobConsumer
Definition: Instance.cs:43
-
DbSet< DreamMakerSettings > DreamMakerSettings
The Models.DreamMakerSettings in the IDatabaseContext
+
DbSet< DreamMakerSettings > DreamMakerSettings
The Models.DreamMakerSettings in the IDatabaseContext
Instance Instance
The Models.Instance the RevisionInformation belongs to
-
DbSet< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
+
DbSet< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
RevisionInformation RevisionInformation
See Api.Models.CompileJob.RevisionInformation
Definition: CompileJob.cs:24
-
DbSet< DreamDaemonSettings > DreamDaemonSettings
The Models.DreamDaemonSettings in the IDatabaseContext
+
DbSet< DreamDaemonSettings > DreamDaemonSettings
The Models.DreamDaemonSettings in the IDatabaseContext
async Task< IRepository > LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location
string AccessToken
The token/password to access the git repository with
List< RevInfoTestMerge > ActiveTestMerges
See Api.Models.RevisionInformation.ActiveTestMerges
@@ -553,7 +553,7 @@ Private Attributes

Definition at line 542 of file Instance.cs.

-

References Tgstation.Server.Api.Rights.Configuration, Tgstation.Server.Host.Components.Byond.ByondManager.StartAsync(), Tgstation.Server.Host.Components.Chat.Chat.StartAsync(), Tgstation.Server.Host.Components.Watchdog.Watchdog.StartAsync(), Tgstation.Server.Host.Components.Byond.ByondManager.StopAsync(), Tgstation.Server.Host.Components.Chat.Chat.StopAsync(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.StopAsync().

+

References Tgstation.Server.Api.Rights.Configuration, Tgstation.Server.Host.Components.Byond.ByondManager.StartAsync(), Tgstation.Server.Host.Components.Chat.Chat.StartAsync(), Tgstation.Server.Host.Components.Watchdog.Watchdog.StartAsync(), Tgstation.Server.Host.Components.Byond.ByondManager.StopAsync(), Tgstation.Server.Host.Components.Chat.Chat.StopAsync(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.StopAsync().

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  }
async Task SetAutoUpdateInterval(uint newInterval)
Change the Api.Models.Instance.AutoUpdateInterval for the IInstance
Definition: Instance.cs:561
async Task StartAsync(CancellationToken cancellationToken)
@@ -564,7 +564,7 @@ Private Attributes
ICompileJobConsumer CompileJobConsumer
Definition: Instance.cs:43
-
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:956
+
async Task StartAsync(CancellationToken cancellationToken)
Definition: Watchdog.cs:949
async Task StartAsync(CancellationToken cancellationToken)
Definition: Chat.cs:525
readonly IDmbFactory dmbFactory
The IDmbFactory for the Instance
Definition: Instance.cs:53
Task UseContext(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager-members.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager-members.html index b87a2489bd..a14d407723 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager-members.html @@ -102,9 +102,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); databaseContextFactoryTgstation.Server.Host.Components.InstanceManagerprivate Dispose()Tgstation.Server.Host.Components.InstanceManager disposedTgstation.Server.Host.Components.InstanceManagerprivate - GetInstance(Models.Instance metadata)Tgstation.Server.Host.Components.InstanceManager + downgradeVersionTgstation.Server.Host.Components.InstanceManagerprivate + GetInstance(Models.Instance metadata)Tgstation.Server.Host.Components.InstanceManager + HandleRestart(Version updateVersion, CancellationToken cancellationToken)Tgstation.Server.Host.Components.InstanceManager instanceFactoryTgstation.Server.Host.Components.InstanceManagerprivate - InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, ILogger< InstanceManager > logger)Tgstation.Server.Host.Components.InstanceManager + InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger< InstanceManager > logger)Tgstation.Server.Host.Components.InstanceManager instancesTgstation.Server.Host.Components.InstanceManagerprivate ioManagerTgstation.Server.Host.Components.InstanceManagerprivate jobManagerTgstation.Server.Host.Components.InstanceManagerprivate diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager.html index ee9689580e..c23d893882 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager.html @@ -109,16 +109,17 @@ Inheritance diagram for Tgstation.Server.Host.Components.InstanceManager:
Tgstation.Server.Host.Components.IInstanceManager -IHostedService -IDisposable +Tgstation.Server.Host.Core.IRestartHandler +IHostedService +IDisposable
- - - + + + @@ -136,6 +137,9 @@ Public Member Functions + + + @@ -164,6 +168,9 @@ Private Attributes + + + @@ -172,7 +179,7 @@ Private Attributes

Definition at line 15 of file InstanceManager.cs.

Constructor & Destructor Documentation

- +

Public Member Functions

 InstanceManager (IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, ILogger< InstanceManager > logger)
 Construct an InstanceManager More...
 
 InstanceManager (IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger< InstanceManager > logger)
 Construct an InstanceManager More...
 
void Dispose ()
 
IInstance GetInstance (Models.Instance metadata)
 
async Task StopAsync (CancellationToken cancellationToken)
 
Task HandleRestart (Version updateVersion, CancellationToken cancellationToken)
 Handle a restart of the server More...
 
- Public Member Functions inherited from Tgstation.Server.Host.Components.IInstanceManager
Task OfflineInstance (Models.Instance metadata, User user, CancellationToken cancellationToken)
 Offline an IInstance More...
readonly Dictionary< long, IInstanceinstances
 Map of Api.Models.Instance.Ids to respective IInstances More...
 
Version downgradeVersion
 Used in StopAsync(CancellationToken) to determine if database downgrades must be made More...
 
bool disposed
 If the InstanceManager has been Disposed More...
 
@@ -206,6 +213,12 @@ Private Attributes + + + + + + @@ -228,15 +241,19 @@ Private Attributes +
IJobManager  jobManager,
IServerControl serverControl,
databaseContextFactoryThe value of databaseContextFactory
applicationThe value of application
jobManagerThe value of jobManager
serverControlThe IServerControl used to register the InstanceManager as a IRestartHandler
loggerThe value of logger
-

Definition at line 66 of file InstanceManager.cs.

-
67  {
68  this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
69  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
70  this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
71  this.application = application ?? throw new ArgumentNullException(nameof(application));
72  this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
73  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
74 
75  instances = new Dictionary<long, IInstance>();
76  }
readonly ILogger< InstanceManager > logger
The ILogger for the InstanceManager
+

Definition at line 72 of file InstanceManager.cs.

+ +

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

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

-

Definition at line 79 of file InstanceManager.cs.

-
80  {
81  lock (this)
82  {
83  if (disposed)
84  return;
85  disposed = true;
86  }
87  foreach (var I in instances)
88  I.Value.Dispose();
89  }
bool disposed
If the InstanceManager has been Disposed
+

Definition at line 89 of file InstanceManager.cs.

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

Implements Tgstation.Server.Host.Components.IInstanceManager.

-

Definition at line 92 of file InstanceManager.cs.

-
93  {
94  if (metadata == null)
95  throw new ArgumentNullException(nameof(metadata));
96  lock (this)
97  {
98  if (!instances.TryGetValue(metadata.Id, out IInstance instance))
99  throw new InvalidOperationException("Instance not online!");
100  return instance;
101  }
102  }
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
+

Definition at line 102 of file InstanceManager.cs.

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

Handle a restart of the server

+ +

Implements Tgstation.Server.Host.Core.IRestartHandler.

+ +

Definition at line 245 of file InstanceManager.cs.

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

Implements Tgstation.Server.Host.Components.IInstanceManager.

-

Definition at line 105 of file InstanceManager.cs.

-
106  {
107  if (newPath == null)
108  throw new ArgumentNullException(nameof(newPath));
109  if (instance.Online.Value)
110  throw new InvalidOperationException("Cannot move an online instance!");
111  var oldPath = instance.Path;
112  await ioManager.CopyDirectory(oldPath, newPath, null, cancellationToken).ConfigureAwait(false);
114  {
115  var targetInstance = new Models.Instance
116  {
117  Id = instance.Id
118  };
119  db.Instances.Attach(targetInstance);
120  targetInstance.Path = newPath;
121  return db.Save(cancellationToken);
122  }).ConfigureAwait(false);
123  await ioManager.DeleteDirectory(oldPath, cancellationToken).ConfigureAwait(false);
124  }
Task DeleteDirectory(string path, CancellationToken cancellationToken)
Recursively delete a directory
+

Definition at line 115 of file InstanceManager.cs.

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

-

Definition at line 127 of file InstanceManager.cs.

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

Definition at line 137 of file InstanceManager.cs.

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

Implements Tgstation.Server.Host.Components.IInstanceManager.

-

Definition at line 167 of file InstanceManager.cs.

-
168  {
169  if (metadata == null)
170  throw new ArgumentNullException(nameof(metadata));
171  logger.LogInformation("Onlining instance ID {0} ({1}) at {2}", metadata.Id, metadata.Name, metadata.Path);
172  var instance = instanceFactory.CreateInstance(metadata);
173  try
174  {
175  lock (this)
176  {
177  if (instances.ContainsKey(metadata.Id))
178  throw new InvalidOperationException("Instance already online!");
179  instances.Add(metadata.Id, instance);
180  }
181  }
182  catch
183  {
184  instance.Dispose();
185  throw;
186  }
187  await instance.StartAsync(cancellationToken).ConfigureAwait(false);
188  }
IInstance CreateInstance(Models.Instance metadata)
Create an IInstance
+

Definition at line 177 of file InstanceManager.cs.

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

-

Definition at line 223 of file InstanceManager.cs.

-
224  {
225  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
226  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
227  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
228  }
readonly IJobManager jobManager
The IJobManager for the InstanceManager
+

Definition at line 233 of file InstanceManager.cs.

+
234  {
235  await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
236  await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
237  await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
238 
239  //downgrade the db if necessary
240  if (downgradeVersion != null)
241  await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
242  }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the InstanceManager
+
readonly IJobManager jobManager
The IJobManager for the InstanceManager
+
Version downgradeVersion
Used in StopAsync(CancellationToken) to determine if database downgrades must be made ...
readonly IInstanceFactory instanceFactory
The IInstanceFactory for the InstanceManager
readonly Dictionary< long, IInstance > instances
Map of Api.Models.Instance.Ids to respective IInstances
+
Task UseContext(Func< IDatabaseContext, Task > operation)
Run an operation in the scope of an IDatabaseContext
@@ -516,6 +571,30 @@ Private Attributes

If the InstanceManager has been Disposed

+

Definition at line 60 of file InstanceManager.cs.

+ +
+
+ +
+
+ + + + + +
+ + + + +
Version Tgstation.Server.Host.Components.InstanceManager.downgradeVersion
+
+private
+
+ +

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

+

Definition at line 55 of file InstanceManager.cs.

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager.png b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_instance_manager.png index 675c70ede5175adf2ccabce19fec9698cc0d9c80..2f3234b73b7a235d883628cb52bb3a69ba2fbd3d 100644 GIT binary patch literal 2385 zcmcguc~Dd57Do|5!qQqro@{DE!37sUmePo8Z4d(mMI}H)HkCkFL9V(?8x!@67#v=R0%m`JHb$ zzquLigu}Yp>$TO?)O7LR9rRFBTY;dqyOugSKA|6>p&*pL#J97JP4j@Xwc%*+i}bqb-ltL+;=Q)nS4+l^^cvauMX1>7Lv>%tJNV(7G1CULFQn9&N{!>&XPGO-IvDGuD~XInZI8= z-UHiilaq|^Ik33^i_3w{+m}@-fC{?DPJFzj#?1oZQHhB6 z8QRRk{BN;>R_F2Ap5Se)5+y zTw=1II_{ak?S8=?-Io&-3-Y)FePYV^_0Z40XX|kAt^k*4Q+pvbo)qSXujNrEC{tat zp7MsP!IPNB^MT6Q9|PGLdB<%$ci{b+dysH1YtVWmQn1|gUL-AwrkIPKdh1m_4;-zO za_ZMg`(9RagUhd5h=rBT7|@q4AjCMo{2X%1s25PZAtHhF;$+y2mJiRO-L0ezxu}P` zbexum$(+4`Rb@pFNWJz=%*ru=kO&#rA@f=|+M{nlDU=SCsK!HLGy9M_js&vd2mg&l zp6*-g4TZM!-F(Y)JWC7tEY=a?ZmQIuQ7RIK30IsO(xrV0zMSI5?k%1hw%TUv0?>Hp zVx9iF`Xu$@E#~c}J2)UtFfhF^X0jUs@X!lueRZ;(Q zB?cnezX_%jqKoVr`o9$>R-Y#(;cl_OAo9(wHUqA(@(M_<`^LW#ZagVrv=VL@k;Yno zcw-j|5l3>d2_-KP1A4(N>a5i^a{@qX!^TY*yB#SgD2PZ z4j@TEu18J6Yo^zcAn|nd7p8TfO**%varFJhLK4UZmAFO*9M+ik&^D99N(#kKXFC+a z55Gp-Usq4N0Qsy3AXD<>W>J!^RDHI4BfceOR2k@JB#5v=gmVb0VC zi{hkT-ov$8UqPd`OgvU5deGt0gA(W@)c8wTlJM1yk98i|x4&gzMKW6@hdMeh9!S7c zd!sx5JY!;Ndyg;gPGpEZ=NjebBS8L6Hh8G|tM+ln!`qEc8Cx~lWE5kWGOVB&OX(QZnVa|DZ%5Cj+WFF9GT3dJ<;($o8)^Kq&ZK#-UBU+jCb#5nVgqX;&;{z z1@8)r%xskRPJRP&&F8)w{oK~M`OKuP0~;)5)v$AxShivy?y2^V)^R@U%k)UeEr}Tp zk(7>^)_d39?}iLc$f(#Ep)b2DDlTDia0`)*%5Ih-LR9OFo`HQ(7){@%@Ki@y%nRL! zrsR3~?n&r(F%=r0QE6eoFga6~=;mFre(}^e0b(Thj@_xgGT4`X&tJG+_^b`dTWLdE zsDNt{6$v$sAMAZ%>z>8dRkKu?vbdT0iocW7Ir7-MoQM5^6_fOmQs;^1olIr)(P(XGrK3 zF$uwLSxehCTd`nRb|-L=->oWp_?(L!x-)<3O!GIq=5LtxD|+)O^ZNa1y6Vp?`5V#y PA2s|T!ohp{PbdEi(rmW_ literal 1978 zcmbtUc~DbV7k?o@D4Vnbi3Ot$Q3GL#NC5>zWRVvLM!*D&gn)vdib*OE3?N2UsVEXr zl(1<*L0Kd?0w&ZTdsvDfBCB9*P!_2?MbH9YLOb@G`Tpwk&fIs;`^`D`oO^!1ds9hX zd$rWRR|fz<3v_q!1^~E7Zj;pD3Is|Gk}oKdr?2bE%8J~4GyO3#{mEsywK~OOu}bRj z2KldAgtwOuAa_!XK8dd-03bln#mV@-QFiDw|6XdN0Lf+Y@7as7{s(|8h3F+L;1=wy3?#Nk1PE z0xe_SPl@Bu?gRa?X^wW~H}a5O{Y%5!tX(1VHP;5D+bT`Beah|s2{#cH&K*Axwlo)@ z+mO}VA+j4eI8=Ly4B;qx-Fx zH+E+~2spsFXeQQzn#1e#;`WHvhUp8w;$-CNS`|Kvs6D#SH}LxRYoqSS$#(qudis(SEj_rIAV!F=sMPWZUFKIt`hmHn+~7nI?8++_FZi)_D>;A4HoDkk)+)f{cMauUa-&?~=r9^M zdDF52vkr_-@5F%)|3dn49ei)XmZD7Uj3U~fS1WP#Y60f$a50e6sIK&Zt^)I-j|0*7 z;{kaluGAh))*iDb>l7exYAhuUM0bLo@|${fCBuomHaG<9dz_jAu|1arSD>rmK?7gI z*ZSFV(WM)AuOaJqpb!^`|yCg8rz&CAf+-4P^BAo?cp}~ zSp#tVr5ze~`Vp<=GxHOHb^%jPfEjH>(lxa-u@*M2-oTzf6#Jt$&;}KCOVw0W=B7rf zN|&l!;+UgwxyPjCtl?F~Q5i1By7TRqmmq%EuLo=5>JH|=qzWdIph(B4G~!cHdqT)O zs!1;CC>pLj=Vd310XvyHXQq4juSX}QQr;)AyE@e6)@rfkA#O^C&_CH8+d8P*K_#x! zJ{Osqb@Cy7bR;QNBAb8F_vU?Zu`q-xF?pcK-MpV4RkbJdyxXgZ`CM-NA8hlG$N3|h zZ*hh^-Yq2(r zZkAfMXmHfcx{1t&7)=Gi77FT6ZI}LjAte5X3M`raYUu|?{azXL2`ZDK>pMhe`xcV@ z097#%n>#4F*@MP2@x%dNeiUD@?J4`r>r%QTeQASqG*>v7w_3EfOuDapS8v2(cAnOW zDP?6E5xPQBfo_vF_HSzcE_^oZRtLv5e0>W}a<_*}#ZvOQyZdH-_;FXOf(m3oMiPb4 zq0!i*S-(mRR=_g02PkxwsqmCSTy(<_eXri4xlG0(l*{7l5`r7575=irF;)P{=cH* BfXn~@ diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_chat_command-members.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_chat_command-members.html index 1e3699b40a..e53472f1af 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_chat_command-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_chat_command-members.html @@ -99,7 +99,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

This is the complete list of members for Tgstation.Server.Host.Components.Interop.ChatCommand, including all inherited members.

- +
CommandTgstation.Server.Host.Components.Interop.ChatCommand
ParametersTgstation.Server.Host.Components.Interop.ChatCommand
ParamsTgstation.Server.Host.Components.Interop.ChatCommand
UserTgstation.Server.Host.Components.Interop.ChatCommand
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_chat_command.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_chat_command.html index 85e0744e73..7f5c99fcce 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_chat_command.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_chat_command.html @@ -103,8 +103,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Properties string Command [get, set]   -string Parameters [get, set] -  +string Params [get, set]User User [get, set]   @@ -134,7 +134,7 @@ Properties
- +
@@ -142,7 +142,7 @@ Properties diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_command-members.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_command-members.html index f0b9f32179..ffc5dab47a 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_command-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_command-members.html @@ -98,7 +98,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

This is the complete list of members for Tgstation.Server.Host.Components.Interop.CommCommand, including all inherited members.

- +
string Tgstation.Server.Host.Components.Interop.ChatCommand.Parametersstring Tgstation.Server.Host.Components.Interop.ChatCommand.Params
- +
ParametersTgstation.Server.Host.Components.Interop.CommCommand
ParametersTgstation.Server.Host.Components.Interop.CommCommand
RawJsonTgstation.Server.Host.Components.Interop.CommCommand
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_command.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_command.html index 391fd7ddb5..2fef485ee8 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_command.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_interop_1_1_comm_command.html @@ -104,9 +104,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - - - + + + @@ -116,7 +116,7 @@ Properties

Definition at line 8 of file CommCommand.cs.

Property Documentation

- +

Properties

IReadOnlyDictionary< string, string > Parameters [get, set]
 The dictionary of the CommCommand More...
 
IReadOnlyDictionary< string, object > Parameters [get, set]
 The dictionary of the CommCommand More...
 
string RawJson [get, set]
 The raw JSON of the CommCommand More...
 
@@ -124,7 +124,7 @@ Properties @@ -138,7 +138,7 @@ Properties

Definition at line 13 of file CommCommand.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

@@ -164,7 +164,7 @@ Properties

Definition at line 18 of file CommCommand.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

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

Definition at line 98 of file CommContext.cs.

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

Definition at line 22 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

@@ -168,7 +168,7 @@ Public Attributes

Definition at line 26 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

@@ -184,7 +184,7 @@ Public Attributes

Definition at line 25 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

@@ -200,7 +200,7 @@ Public Attributes

Definition at line 21 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

@@ -216,7 +216,7 @@ Public Attributes

Definition at line 23 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

@@ -232,7 +232,7 @@ Public Attributes

Definition at line 24 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

@@ -248,7 +248,7 @@ Public Attributes

Definition at line 11 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.SendCommand().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.SendCommand().

@@ -264,7 +264,7 @@ Public Attributes

Definition at line 28 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop(), and Tgstation.Server.Host.Components.Watchdog.SessionController.SendCommand().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop(), and Tgstation.Server.Host.Components.Watchdog.SessionController.SendCommand().

@@ -280,7 +280,7 @@ Public Attributes

Definition at line 29 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleChatCommand(), Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleEvent(), Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop(), Tgstation.Server.Host.Components.Watchdog.SessionController.SetPort(), and Tgstation.Server.Host.Components.Watchdog.SessionController.SetRebootState().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleChatCommand(), Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleEvent(), Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop(), Tgstation.Server.Host.Components.Watchdog.SessionController.SetPort(), and Tgstation.Server.Host.Components.Watchdog.SessionController.SetRebootState().

@@ -328,7 +328,7 @@ Public Attributes

Definition at line 13 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleEvent(), Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop(), Tgstation.Server.Host.Components.Watchdog.SessionController.SetPort(), and Tgstation.Server.Host.Components.Watchdog.SessionController.SetRebootState().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleEvent(), Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop(), Tgstation.Server.Host.Components.Watchdog.SessionController.SetPort(), and Tgstation.Server.Host.Components.Watchdog.SessionController.SetRebootState().

@@ -344,7 +344,7 @@ Public Attributes

Definition at line 15 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.SetPort().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.SetPort().

@@ -360,7 +360,7 @@ Public Attributes

Definition at line 16 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.SetRebootState().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.SetRebootState().

@@ -376,7 +376,7 @@ Public Attributes

Definition at line 17 of file Constants.cs.

-

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

+

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

@@ -392,7 +392,7 @@ Public Attributes

Definition at line 18 of file Constants.cs.

-

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

+

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

@@ -408,7 +408,7 @@ Public Attributes

Definition at line 19 of file Constants.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.SessionController.HandleInterop().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_monitor_state.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_monitor_state.html index d0a80fdc61..5624b1c6ff 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_monitor_state.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_monitor_state.html @@ -225,7 +225,7 @@ Properties

Definition at line 23 of file MonitorState.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.MonitorLifetimes().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.MonitorLifetimes().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_reattach_information.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_reattach_information.html index add4df45de..f08a571233 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_reattach_information.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_reattach_information.html @@ -260,7 +260,7 @@ Additional Inherited Members

Definition at line 14 of file ReattachInformation.cs.

-

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

+

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

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_session_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_session_controller.html index a341803980..da517b5b7f 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_session_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_session_controller.html @@ -367,8 +367,8 @@ Private Attributes

Definition at line 182 of file SessionController.cs.

-

References Tgstation.Server.Host.Components.Chat.IJsonTrackingContext.Active, Tgstation.Server.Host.Core.IProcessBase.Lifetime, Tgstation.Server.Host.Components.Interop.ICommContext.RegisterHandler(), and Tgstation.Server.Host.Core.IProcess.Startup.

-
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 
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  }
bool disposed
If the SessionController has been disposed
+

References Tgstation.Server.Host.Models.ReattachInformationBase.AccessIdentifier, Tgstation.Server.Host.Components.Chat.IJsonTrackingContext.Active, Tgstation.Server.Host.Core.IProcessBase.Lifetime, Tgstation.Server.Host.Components.Interop.ICommContext.RegisterHandler(), and Tgstation.Server.Host.Core.IProcess.Startup.

+
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 
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  }
bool disposed
If the SessionController has been disposed
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
Definition: IProcessBase.cs:14
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
@@ -380,9 +380,12 @@ Private Attributes
readonly ILogger< SessionController > logger
The ILogger for the SessionController
TaskCompletionSource< object > rebootTcs
The TaskCompletionSource<TResult> that completes when DD tells us about a reboot
+
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController
bool portClosedForReboot
If we know DreamDaemon currently has it&#39;s port closed
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the SessionController
+ +
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance
bool released
If process should be kept alive instead
@@ -437,8 +440,8 @@ Private Attributes

Throws an ObjectDisposedException if Dispose(bool) has been called

-

Definition at line 406 of file SessionController.cs.

-
407  {
408  if (disposed)
409  throw new ObjectDisposedException(nameof(SessionController));
410  }
bool disposed
If the SessionController has been disposed
+

Definition at line 408 of file SessionController.cs.

+
409  {
410  if (disposed)
411  throw new ObjectDisposedException(nameof(SessionController));
412  }
bool disposed
If the SessionController has been disposed
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
@@ -458,8 +461,8 @@ Private Attributes

-

Definition at line 235 of file SessionController.cs.

-
236  {
237  Dispose(true);
238  GC.SuppressFinalize(this);
239  }
+

Definition at line 237 of file SessionController.cs.

+
238  {
239  Dispose(true);
240  GC.SuppressFinalize(this);
241  }
@@ -487,8 +490,8 @@ Private Attributes

-

Definition at line 242 of file SessionController.cs.

-
243  {
244  lock (this)
245  {
246  if (disposed)
247  return;
248  if (disposing)
249  {
250  if (!released)
251  {
252  process.Terminate();
253  byondLock.Dispose();
254  }
255  process.Dispose();
256  interopContext.Dispose();
257  Dmb?.Dispose(); //will be null when released
258  chatJsonTrackingContext.Dispose();
259  disposed = true;
260  }
261  else
262  {
263  if (logger != null)
264  logger.LogError("Being disposed via finalizer!");
265  if (!released)
266  if (process != null)
267  process.Terminate();
268  else if (logger != null)
269  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
270  }
271  }
272  }
bool disposed
If the SessionController has been disposed
+

Definition at line 244 of file SessionController.cs.

+
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  }
bool disposed
If the SessionController has been disposed
readonly ICommContext interopContext
The ICommContext for the SessionController
readonly IProcess process
The IProcess for the SessionController
readonly IJsonTrackingContext chatJsonTrackingContext
The IJsonTrackingContext for the SessionController
@@ -547,13 +550,13 @@ Private Attributes

Implements Tgstation.Server.Host.Components.Interop.ICommHandler.

-

Definition at line 275 of file SessionController.cs.

+

Definition at line 277 of file SessionController.cs.

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

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

Implements Tgstation.Server.Host.Components.Watchdog.ISessionController.

-

Definition at line 416 of file SessionController.cs.

-
417  {
418  CheckDisposed();
419  //we still don't want to dispose the dmb yet, even though we're keeping it alive
420  var tmpProvider = reattachInformation.Dmb;
421  reattachInformation.Dmb = null;
422  released = true;
423  Dispose();
425  tmpProvider.KeepAlive();
426  reattachInformation.Dmb = tmpProvider;
427  return reattachInformation;
428  }
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
- +

Definition at line 418 of file SessionController.cs.

+
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();
427  tmpProvider.KeepAlive();
428  reattachInformation.Dmb = tmpProvider;
429  return reattachInformation;
430  }
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
+
readonly IByondExecutableLock byondLock
The IByondExecutableLock for the SessionController
-
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
+
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon
void DoNotDeleteThisSession()
Call if, during a detach, this version should not be deleted
bool released
If process should be kept alive instead
@@ -635,9 +638,9 @@ Private Attributes

Implements Tgstation.Server.Host.Components.Watchdog.ISessionController.

-

Definition at line 508 of file SessionController.cs.

-
509  {
510  CheckDisposed();
512  }
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
-
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
+

Definition at line 511 of file SessionController.cs.

+
512  {
513  CheckDisposed();
515  }
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
+
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
RebootState RebootState
The current DreamDaemon reboot state
@@ -711,10 +714,10 @@ Private Attributes
- +
IReadOnlyDictionary<string, string> Tgstation.Server.Host.Components.Interop.CommCommand.ParametersIReadOnlyDictionary<string, object> Tgstation.Server.Host.Components.Interop.CommCommand.Parameters
-

Definition at line 433 of file SessionController.cs.

+

Definition at line 435 of file SessionController.cs.

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

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

Implements Tgstation.Server.Host.Components.Watchdog.ISessionController.

-

Definition at line 465 of file SessionController.cs.

+

Definition at line 467 of file SessionController.cs.

References Tgstation.Server.Host.Components.Interop.Constants.DMParameterData, Tgstation.Server.Host.Components.Interop.Constants.DMResponseSuccess, and Tgstation.Server.Host.Components.Interop.Constants.DMTopicChangePort.

-
466  {
467  CheckDisposed();
468 
469  if (port == 0)
470  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
471 
472  async Task<bool> ImmediateTopicPortChange()
473  {
474  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);
475 
476  if (commandResult != Constants.DMResponseSuccess)
477  {
478  logger.LogWarning("Failed port change! DD says: {0}", commandResult);
479  return false;
480  }
481 
482  return true;
483  }
484 
485  lock (this)
487  {
488  if (portAssignmentTcs != null)
489  throw new InvalidOperationException("A port change operation is already in progress!");
490  nextPort = port;
491  portAssignmentTcs = new TaskCompletionSource<bool>();
492  return portAssignmentTcs.Task;
493  }
494  else
495  return ImmediateTopicPortChange();
496  }
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
+
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)
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  }
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
+
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
+
ushort Port
The port DreamDaemon was last listening on
ushort nextPort
The port to assign DreamDaemon when it queries for it
readonly ILogger< SessionController > logger
The ILogger for the SessionController
@@ -818,10 +823,10 @@ Private Attributes

Implements Tgstation.Server.Host.Components.Watchdog.ISessionController.

-

Definition at line 499 of file SessionController.cs.

+

Definition at line 502 of file SessionController.cs.

References Tgstation.Server.Host.Components.Interop.Constants.DMParameterData, Tgstation.Server.Host.Components.Interop.Constants.DMResponseSuccess, and Tgstation.Server.Host.Components.Interop.Constants.DMTopicChangeReboot.

-
500  {
501  if (RebootState == newRebootState)
502  return true;
503  reattachInformation.RebootState = newRebootState;
504  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;
505  }
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
+
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  }
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog.html index 50d5dab46a..3f59b5f3cd 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog.html @@ -454,10 +454,10 @@ Private Attributes

Implements Tgstation.Server.Host.Components.Watchdog.IWatchdog.

-

Definition at line 683 of file Watchdog.cs.

+

Definition at line 676 of file Watchdog.cs.

References Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock(), and Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.Match().

-
684  {
685  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
686  {
687  if (launchParameters.Match(ActiveLaunchParameters))
688  return;
689  ActiveLaunchParameters = launchParameters;
690  if (Running)
691  //queue an update
692  activeParametersUpdated.TrySetResult(null);
693  }
694  }
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
+
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  }
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
DreamDaemonLaunchParameters ActiveLaunchParameters
Definition: Watchdog.cs:40
bool Match(DreamDaemonLaunchParameters otherParameters)
Check if we match a given set of otherParameters
@@ -559,10 +559,10 @@ Private Attributes

-

Definition at line 1052 of file Watchdog.cs.

+

Definition at line 1045 of file Watchdog.cs.

References Tgstation.Server.Host.Components.Interop.Constants.DMParameterData, Tgstation.Server.Host.Components.Interop.Constants.DMTopicChatCommand, Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock(), and Tgstation.Server.Host.Components.Watchdog.ISessionController.SendCommand().

-
1053  {
1054  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1055  {
1056  if (!Running)
1057  return "ERROR: Server offline!";
1058 
1059  var commandObject = new ChatCommand
1060  {
1061  Command = commandName,
1062  Parameters = arguments,
1063  User = sender
1064  };
1065 
1066  var json = JsonConvert.SerializeObject(commandObject, new JsonSerializerSettings
1067  {
1068  ContractResolver = new CamelCasePropertyNamesContractResolver()
1069  });
1070 
1071  var command = String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicChatCommand), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json));
1072 
1073  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1074  return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!";
1075  }
1076  }
+
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  }
Represents a server User
Definition: User.cs:10
@@ -613,10 +613,10 @@ Private Attributes

Implements Tgstation.Server.Host.Components.IEventConsumer.

-

Definition at line 1008 of file Watchdog.cs.

+

Definition at line 1001 of file Watchdog.cs.

References Tgstation.Server.Host.Components.Interop.Constants.DMParameterData, Tgstation.Server.Host.Components.Interop.Constants.DMResponseSuccess, Tgstation.Server.Host.Components.Interop.Constants.DMTopicEvent, Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock(), and Tgstation.Server.Host.Components.Watchdog.ISessionController.SendCommand().

-
1009  {
1010  string results;
1011  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
1012  {
1013  if (!Running)
1014  return true;
1015 
1016  var builder = new StringBuilder(Constants.DMTopicEvent);
1017  builder.Append('&');
1018  var notification = new EventNotification
1019  {
1020  Type = eventType,
1021  Parameters = parameters
1022  };
1023  var json = JsonConvert.SerializeObject(notification);
1024  builder.Append(byondTopicSender.SanitizeString(Constants.DMParameterData));
1025  builder.Append('=');
1026  builder.Append(byondTopicSender.SanitizeString(json));
1027 
1028  var activeServer = AlphaIsActive ? alphaServer : bravoServer;
1029  results = await activeServer.SendCommand(builder.ToString(), cancellationToken).ConfigureAwait(false);
1030  }
1031 
1032  if (results == Constants.DMResponseSuccess)
1033  return true;
1034 
1035  List<Response> responses;
1036  try
1037  {
1038  responses = JsonConvert.DeserializeObject<List<Response>>(results);
1039  }
1040  catch
1041  {
1042  logger.LogInformation("Recieved invalid response from DD when parsing event {0}:{1}{2}", eventType, Environment.NewLine, results);
1043  return true;
1044  }
1045 
1046  await Task.WhenAll(responses.Select(x => chat.SendMessage(x.Message, x.ChannelIds, cancellationToken))).ConfigureAwait(false);
1047 
1048  return true;
1049  }
+
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  }
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:101
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
@@ -664,8 +664,8 @@ Private Attributes

Implements Tgstation.Server.Host.Core.IRestartHandler.

-

Definition at line 1079 of file Watchdog.cs.

-
1080  {
1081  releaseServers = true;
1082  if (Running)
1083  await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false);
1084  }
bool releaseServers
If the servers should be released instead of shutdown
Definition: Watchdog.cs:145
+

Definition at line 1072 of file Watchdog.cs.

+
1073  {
1074  releaseServers = true;
1075  if (Running)
1076  await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false);
1077  }
bool releaseServers
If the servers should be released instead of shutdown
Definition: Watchdog.cs:145
Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
readonly IChat chat
The IChat for the Watchdog
Definition: Watchdog.cs:51
@@ -724,7 +724,7 @@ Private Attributes

Definition at line 256 of file Watchdog.cs.

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

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

Implements Tgstation.Server.Host.Components.Watchdog.IWatchdog.

-

Definition at line 901 of file Watchdog.cs.

+

Definition at line 894 of file Watchdog.cs.

References Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock().

-
902  {
903  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
904  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
905  }
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:704
+
895  {
896  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
897  await LaunchImplNoLock(true, true, null, cancellationToken).ConfigureAwait(false);
898  }
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:697
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
@@ -830,10 +830,10 @@ Private Attributes
Returns
A Task representing the running operation
-

Definition at line 704 of file Watchdog.cs.

+

Definition at line 697 of file Watchdog.cs.

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

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

Definition at line 506 of file Watchdog.cs.

+

Definition at line 499 of file Watchdog.cs.

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

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

Implements Tgstation.Server.Host.Components.Watchdog.IWatchdog.

-

Definition at line 908 of file Watchdog.cs.

+

Definition at line 901 of file Watchdog.cs.

References Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock(), and Tgstation.Server.Host.Components.Watchdog.ISessionController.ResetRebootState().

-
909  {
910  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
911  {
912  if (!Running)
913  return;
914  var toClear = AlphaIsActive ? alphaServer : bravoServer;
915  if (toClear != null)
916  toClear.ResetRebootState();
917  }
918  }
+
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  }
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
ISessionController alphaServer
Server designation alpha
Definition: Watchdog.cs:136
@@ -984,13 +984,13 @@ Private Attributes

Implements Tgstation.Server.Host.Components.Watchdog.IWatchdog.

-

Definition at line 921 of file Watchdog.cs.

+

Definition at line 914 of file Watchdog.cs.

References Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock().

-
922  {
923  logger.LogTrace("Begin Restart. Graceful: {0}", graceful);
924  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
925  {
926  if (!graceful || !Running)
927  {
928  Task chatTask;
929  if (Running)
930  {
931  chatTask = chat.SendWatchdogMessage("Manual restart triggered...", cancellationToken);
932  await TerminateNoLock(false, false, cancellationToken).ConfigureAwait(false);
933  }
934  else
935  chatTask = Task.CompletedTask;
936  await LaunchImplNoLock(true, !Running, null, cancellationToken).ConfigureAwait(false);
937  await chatTask.ConfigureAwait(false);
938  }
939  var toReboot = AlphaIsActive ? alphaServer : bravoServer;
940  if (toReboot != null)
941  {
942  if (!await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false))
943  logger.LogWarning("Unable to send reboot state change event!");
944  }
945  }
946  }
+
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  }
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:101
Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
-
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:704
+
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:697
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
readonly IChat chat
The IChat for the Watchdog
Definition: Watchdog.cs:51
@@ -1018,14 +1018,14 @@ Private Attributes

-

Definition at line 956 of file Watchdog.cs.

+

Definition at line 949 of file Watchdog.cs.

References Tgstation.Server.Host.Components.Instance.Instance(), and Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock().

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

-
957  {
958  var reattachInfo = await reattachInfoHandler.Load(cancellationToken).ConfigureAwait(false);
959  if (!autoStart && reattachInfo == null)
960  return;
961 
962  long? adminUserId = null;
963 
964  await databaseContextFactory.UseContext(async db => adminUserId = await db.Users
965  .Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant())
966  .Select(x => x.Id)
967  .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
968  var job = new Models.Job
969  {
970  StartedBy = new Models.User
971  {
972  Id = adminUserId.Value
973  },
974  Instance = new Models.Instance
975  {
976  Id = instance.Id
977  },
978  Description = "Instance startup watchdog launch",
979  CancelRight = (ulong)DreamDaemonRights.Shutdown,
980  CancelRightsType = RightsType.DreamDaemon
981  };
982  await jobManager.RegisterOperation(job, async (j, databaseContext, progressFunction, ct) =>
983  {
984  using (await SemaphoreSlimContext.Lock(semaphore, ct).ConfigureAwait(false))
985  await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
986  }, cancellationToken).ConfigureAwait(false);
987  }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Watchdog
Definition: Watchdog.cs:71
+
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  }
readonly IDatabaseContextFactory databaseContextFactory
The IDatabaseContextFactory for the Watchdog
Definition: Watchdog.cs:71
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
-
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:704
+
async Task LaunchImplNoLock(bool startMonitor, bool announce, WatchdogReattachInformation reattachInfo, CancellationToken cancellationToken)
Launches the Watchdog
Definition: Watchdog.cs:697
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
Task RegisterOperation(Job job, Func< Job, IDatabaseContext, Action< int >, CancellationToken, Task > operation, CancellationToken cancellationToken)
Registers a given Job and begins running it
readonly IJobManager jobManager
The IJobManager for the Watchdog
Definition: Watchdog.cs:86
@@ -1056,16 +1056,16 @@ Private Attributes

-

Definition at line 990 of file Watchdog.cs.

+

Definition at line 983 of file Watchdog.cs.

References Tgstation.Server.Host.Components.Watchdog.WatchdogReattachInformation.Alpha, and Tgstation.Server.Host.Components.Watchdog.ISessionController.Release().

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

-
991  {
992  if (releaseServers && Running)
993  {
994  await StopMonitor().ConfigureAwait(false);
995 
996  var reattachInformation = new WatchdogReattachInformation
997  {
999  };
1000  reattachInformation.Alpha = alphaServer?.Release();
1001  reattachInformation.Bravo = bravoServer?.Release();
1002  await reattachInfoHandler.Save(reattachInformation, cancellationToken).ConfigureAwait(false);
1003  }
1004  await Terminate(false, cancellationToken).ConfigureAwait(false);
1005  }
-
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Definition: Watchdog.cs:949
+
984  {
985  if (releaseServers && Running)
986  {
987  await StopMonitor().ConfigureAwait(false);
988 
989  var reattachInformation = new WatchdogReattachInformation
990  {
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  }
+
async Task Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog
Definition: Watchdog.cs:942
bool releaseServers
If the servers should be released instead of shutdown
Definition: Watchdog.cs:145
Task Save(WatchdogReattachInformation reattachInformation, CancellationToken cancellationToken)
Save some reattachInformation
-
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
Definition: Watchdog.cs:669
+
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
Definition: Watchdog.cs:662
ReattachInformation Release()
Releases the IProcess without terminating it. Also calls System.IDisposable.Dispose ...
ISessionController alphaServer
Server designation alpha
Definition: Watchdog.cs:136
@@ -1098,8 +1098,8 @@ Private Attributes

Stops MonitorLifetimes(CancellationToken). Doesn't kill the servers

Returns
if the monitor was running, otherwise
-

Definition at line 669 of file Watchdog.cs.

-
670  {
671  logger.LogTrace("StopMonitor");
672  if (monitorTask == null)
673  return false;
674  monitorCts.Cancel();
675  await monitorTask.ConfigureAwait(false);
676  monitorCts.Dispose();
677  monitorTask = null;
678  return true;
679  }
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:101
+

Definition at line 662 of file Watchdog.cs.

+
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  }
readonly ILogger< Watchdog > logger
The ILogger<TCategoryName> for the Watchdog
Definition: Watchdog.cs:101
Task monitorTask
The Task running the monitor loop
Definition: Watchdog.cs:126
CancellationTokenSource monitorCts
The CancellationTokenSource for the monitor loop
Definition: Watchdog.cs:121
@@ -1133,10 +1133,10 @@ Private Attributes

Implements Tgstation.Server.Host.Components.Watchdog.IWatchdog.

-

Definition at line 949 of file Watchdog.cs.

+

Definition at line 942 of file Watchdog.cs.

References Tgstation.Server.Host.Core.SemaphoreSlimContext.Lock().

-
950  {
951  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
952  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
953  }
bool releaseServers
If the servers should be released instead of shutdown
Definition: Watchdog.cs:145
+
943  {
944  using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
945  await TerminateNoLock(graceful, !releaseServers, cancellationToken).ConfigureAwait(false);
946  }
bool releaseServers
If the servers should be released instead of shutdown
Definition: Watchdog.cs:145
readonly SemaphoreSlim semaphore
The SemaphoreSlim for the Watchdog
Definition: Watchdog.cs:106
async Task TerminateNoLock(bool graceful, bool announce, CancellationToken cancellationToken)
Implementation of Terminate(bool, CancellationToken). Does not lock semaphore
Definition: Watchdog.cs:228
static async Task< SemaphoreSlimContext > Lock(SemaphoreSlim semaphore, CancellationToken cancellationToken)
Asyncronously locks a semaphore
@@ -1200,7 +1200,7 @@ Private Attributes
Task SendWatchdogMessage(string message, CancellationToken cancellationToken)
Send a chat message to configured watchdog channels
void DisposeAndNullControllers()
Call IDisposable.Dispose on alphaServer and bravoServer and set them to
Definition: Watchdog.cs:213
DreamDaemonLaunchParameters LastLaunchParameters
Definition: Watchdog.cs:43
-
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
Definition: Watchdog.cs:669
+
async Task< bool > StopMonitor()
Stops MonitorLifetimes(CancellationToken). Doesn&#39;t kill the servers
Definition: Watchdog.cs:662
readonly IChat chat
The IChat for the Watchdog
Definition: Watchdog.cs:51
ISessionController alphaServer
Server designation alpha
Definition: Watchdog.cs:136
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog_reattach_information.html b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog_reattach_information.html index 2f63083123..a796d85e50 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog_reattach_information.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_watchdog_reattach_information.html @@ -259,7 +259,7 @@ Additional Inherited Members

Definition at line 15 of file WatchdogReattachInformation.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.LaunchImplNoLock(), Tgstation.Server.Host.Components.ReattachInfoHandler.ReattachInfoHandler(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.StopAsync().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.LaunchImplNoLock(), Tgstation.Server.Host.Components.ReattachInfoHandler.ReattachInfoHandler(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.StopAsync().

@@ -285,7 +285,7 @@ Additional Inherited Members

Definition at line 20 of file WatchdogReattachInformation.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.LaunchImplNoLock(), and Tgstation.Server.Host.Components.ReattachInfoHandler.ReattachInfoHandler().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.LaunchImplNoLock(), and Tgstation.Server.Host.Components.ReattachInfoHandler.ReattachInfoHandler().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_database_configuration.html b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_database_configuration.html index 971acbad51..cc08f49d69 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_database_configuration.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_configuration_1_1_database_configuration.html @@ -225,7 +225,7 @@ Properties

Definition at line 35 of file DatabaseConfiguration.cs.

-

Referenced by Tgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext >.Initialize().

+

Referenced by Tgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext >.Initialize().

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

Definition at line 25 of file DatabaseConfiguration.cs.

-

Referenced by Tgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext >.Initialize().

+

Referenced by Tgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext >.Initialize().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_api_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_api_controller.html index fb99004edf..774563ef37 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_api_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_api_controller.html @@ -204,7 +204,7 @@ Private Attributes

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

62  {
63  DatabaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
64  if (authenticationContextFactory == null)
65  throw new ArgumentNullException(nameof(authenticationContextFactory));
66  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
67  AuthenticationContext = authenticationContextFactory.CurrentAuthenticationContext;
70  }
- +
ILogger Logger
The ILogger for the ApiController
readonly bool requireInstance
If IAuthenticationContext.InstanceUser permissions are required to access the ApiController ...
Metadata about a server instance
Definition: Instance.cs:9
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_chat_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_chat_controller.html index b6d969e745..6fd71bf311 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_chat_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_chat_controller.html @@ -275,13 +275,13 @@ Private Attributes

Definition at line 62 of file ChatController.cs.

-

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatBots, Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Rights.List, Tgstation.Server.Api.Models.ErrorMessage.Message, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

+

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatBots, Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Rights.List, Tgstation.Server.Api.Models.ErrorMessage.Message, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

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  }
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
- +
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
IChat Chat
The IChat for the IInstance
Definition: IInstance.cs:37
string Message
A human readable description of the error
Definition: ErrorMessage.cs:13
- +
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
Metadata about a server instance
Definition: Instance.cs:9
@@ -330,10 +330,10 @@ Private Attributes

Definition at line 133 of file ChatController.cs.

-

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

+

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

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  }
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
- - + +
Metadata about a server instance
Definition: Instance.cs:9
readonly IInstanceManager instanceManager
The IInstanceManager for the ChatController
@@ -377,12 +377,12 @@ Private Attributes

Definition at line 160 of file ChatController.cs.

-

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatBots, and Tgstation.Server.Host.Security.AuthenticationContext.GetRight().

+

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatBots, and Tgstation.Server.Host.Security.AuthenticationContext.GetRight().

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  }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
- +
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
- +
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
@@ -415,12 +415,12 @@ Private Attributes

Definition at line 143 of file ChatController.cs.

-

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatBots, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), and Tgstation.Server.Api.Models.Instance.Id.

+

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatBots, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), and Tgstation.Server.Api.Models.Instance.Id.

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  }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
- +
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
- +
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Metadata about a server instance
Definition: Instance.cs:9
ChatBotRights
Rights for Models.ChatBot
Definition: ChatBotRights.cs:9
@@ -455,16 +455,16 @@ Private Attributes

Definition at line 178 of file ChatController.cs.

-

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatBots, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatChannels, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

+

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatBots, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ChatChannels, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

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 
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  }
- +
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
- +
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
IChat Chat
The IChat for the IInstance
Definition: IInstance.cs:37
- +
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Metadata about a server instance
Definition: Instance.cs:9
static Models.ChatChannel ConvertApiChatChannel(Api.Models.ChatChannel api)
Converts api to a ChatChannel
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_dream_daemon_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_dream_daemon_controller.html index ecc18fa253..965ea3b9a0 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_dream_daemon_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_dream_daemon_controller.html @@ -367,17 +367,17 @@ Private Attributes

Definition at line 83 of file DreamDaemonController.cs.

-

References Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.AllowWebClient, Tgstation.Server.Api.Models.Internal.DreamDaemonSettings.AutoStart, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.PrimaryPort, Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.SecondaryPort, Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.SecurityLevel, and Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.StartupTimeout.

+

References Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.AllowWebClient, Tgstation.Server.Api.Models.Internal.DreamDaemonSettings.AutoStart, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.PrimaryPort, Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.SecondaryPort, Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.SecurityLevel, and Tgstation.Server.Api.Models.Internal.DreamDaemonLaunchParameters.StartupTimeout.

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  }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
- +
DreamDaemonSecurity SecurityLevel
The DreamDaemonSecurity level of DreamDaemon
bool AutoStart
If DreamDaemon starts when it&#39;s Instance starts
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
DreamDaemonRights
Rights for Models.DreamDaemon
- +
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
IWatchdog Watchdog
The IWatchdog for the IInstance
Definition: IInstance.cs:32
@@ -457,17 +457,17 @@ Private Attributes

Definition at line 142 of file DreamDaemonController.cs.

-

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

+

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

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 
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  }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
- +
DreamDaemonSecurity SecurityLevel
The DreamDaemonSecurity level of DreamDaemon
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
async Task< IActionResult > ReadImpl(DreamDaemonSettings settings, CancellationToken cancellationToken)
Implementation of Read(CancellationToken)
DreamDaemonRights
Rights for Models.DreamDaemon
- +
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
IWatchdog Watchdog
The IWatchdog for the IInstance
Definition: IInstance.cs:32
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_dream_maker_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_dream_maker_controller.html index 3c9c3bd352..03e22d6777 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_dream_maker_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_dream_maker_controller.html @@ -297,11 +297,11 @@ Private Attributes

Definition at line 59 of file DreamMakerController.cs.

-

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.CompileJobs, and Tgstation.Server.Api.Models.Instance.Id.

-
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  }
+

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.CompileJobs, and Tgstation.Server.Api.Models.Instance.Id.

+
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  }
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Metadata about a server instance
Definition: Instance.cs:9
- +
@@ -333,11 +333,11 @@ Private Attributes

Definition at line 74 of file DreamMakerController.cs.

-

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.CompileJobs, and Tgstation.Server.Api.Models.Instance.Id.

-
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  }
+

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.CompileJobs, and Tgstation.Server.Api.Models.Instance.Id.

+
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  }
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Metadata about a server instance
Definition: Instance.cs:9
- +
@@ -369,11 +369,11 @@ Private Attributes

Definition at line 50 of file DreamMakerController.cs.

-

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.DreamMakerSettings, and Tgstation.Server.Api.Models.Instance.Id.

+

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.DreamMakerSettings, and Tgstation.Server.Api.Models.Instance.Id.

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  }
readonly IInstanceManager instanceManager
The IInstanceManager for the DreamMakerController
-
DbSet< DreamMakerSettings > DreamMakerSettings
+
DbSet< DreamMakerSettings > DreamMakerSettings
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
- +
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Metadata about a server instance
Definition: Instance.cs:9
@@ -407,17 +407,17 @@ Private Attributes

Definition at line 101 of file DreamMakerController.cs.

-

References Tgstation.Server.Api.Models.InstanceUser.DreamMakerRights, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.DreamMakerSettings, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Security.AuthenticationContext.InstanceUser, Tgstation.Server.Api.Rights.Read, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

+

References Tgstation.Server.Api.Models.InstanceUser.DreamMakerRights, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.DreamMakerSettings, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Security.AuthenticationContext.InstanceUser, Tgstation.Server.Api.Rights.Read, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

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  }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
-
DbSet< DreamMakerSettings > DreamMakerSettings
+
DbSet< DreamMakerSettings > DreamMakerSettings
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
ushort ApiValidationPort
The port used during compilation to validate the DMAPI
Definition: DreamMaker.cs:19
string ProjectName
The .dme file DreamMaker tries to compile with without the extension
Definition: DreamMaker.cs:13
DreamMakerRights
Rights for Models.DreamMaker
- +
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
DreamMakerRights DreamMakerRights
The Rights.DreamMakerRights of the InstanceUser
Definition: InstanceUser.cs:39
override async Task< IActionResult > Read(CancellationToken cancellationToken)
Attempt to read a TModel
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller.html index fb834c74cb..95e109cf58 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_home_controller.html @@ -278,11 +278,11 @@ Additional Inherited Members

Definition at line 81 of file HomeController.cs.

-

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

+

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

82  {
84  return BadRequest(new Api.Models.ErrorMessage { Message = "Cannot create a token using another token!" });
85 
86  ISystemIdentity identity;
87  try
88  {
89  //trust the system over the database because a user's name can change while still having the same SID
90  identity = await systemIdentityFactory.CreateSystemIdentity(ApiHeaders.Username, ApiHeaders.Password, cancellationToken).ConfigureAwait(false);
91  }
92  catch (NotImplementedException)
93  {
94  identity = null;
95  }
96  using (identity)
97  {
98  IQueryable<User> query;
99  if (identity == null)
100  query = DatabaseContext.Users.Where(x => x.CanonicalName == ApiHeaders.Username.ToUpperInvariant());
101  else
102  query = DatabaseContext.Users.Where(x => x.SystemIdentifier == identity.Uid);
103  var user = await query.Select(x => new User
104  {
105  Id = x.Id,
106  PasswordHash = x.PasswordHash,
107  Enabled = x.Enabled,
108  Name = x.Name
109  }).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
110 
111  if (user == null)
112  return Unauthorized();
113 
114  if (identity == null)
115  {
116  var originalHash = user.PasswordHash;
118  return Unauthorized();
119  if (user.PasswordHash != originalHash)
120  {
121  var updatedUser = new User
122  {
123  Id = user.Id
124  };
125  DatabaseContext.Users.Attach(updatedUser);
126  updatedUser.PasswordHash = user.PasswordHash;
127  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
128  }
129  }
130  //check if the name changed and updoot accordingly
131  else if (identity.Username != user.Name)
132  {
133  DatabaseContext.Users.Attach(user);
134  user.Name = identity.Username;
135  user.CanonicalName = user.Name.ToUpperInvariant();
136  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
137  }
138 
139  if (!user.Enabled.Value)
140  return Forbid();
141 
142  var token = await tokenFactory.CreateToken(user, cancellationToken).ConfigureAwait(false);
143  if (identity != null)
144  {
145  //expire the identity slightly after the auth token in case of lag
146  var identExpiry = token.ExpiresAt.Value;
147  identExpiry += tokenFactory.ValidationParameters.ClockSkew;
148  identExpiry += TimeSpan.FromSeconds(15);
149  identityCache.CacheSystemIdentity(user, identity, identExpiry);
150  }
151 
152  Logger.LogDebug("Successfully logged in user {0}!", user.Id);
153 
154  return Json(token);
155  }
156  }
bool CheckUserPassword(User user, string password)
Checks a given password matches a given user &#39;s User.PasswordHash. This may result in User...
Task< ISystemIdentity > CreateSystemIdentity(User user, CancellationToken cancellationToken)
Create a ISystemIdentity for a given user
Task< Token > CreateToken(Models.User user, CancellationToken cancellationToken)
Create a Token for a given user
- +
Represents a user on the current System.Runtime.InteropServices.OSPlatform
string Username
The client&#39;s username
Definition: ApiHeaders.cs:81
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
@@ -292,7 +292,7 @@ Additional Inherited Members
readonly IIdentityCache identityCache
The IIdentityCache for the HomeController
bool IsTokenAuthentication
If the header uses password or JWT authentication
Definition: ApiHeaders.cs:91
- +
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory
long Id
The ID of the User
Definition: User.cs:15
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_instance_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_instance_controller.html index 0f9ce44a9f..446ca43242 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_instance_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_instance_controller.html @@ -288,7 +288,7 @@ Private Attributes

Definition at line 111 of file InstanceController.cs.

-

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, Tgstation.Server.Api.Rights.List, Tgstation.Server.Api.Models.ErrorMessage.Message, Tgstation.Server.Api.Models.Internal.User.Name, Tgstation.Server.Api.Models.Online, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), Tgstation.Server.Api.Rights.SoftRestart, Tgstation.Server.Api.Rights.SoftShutdown, and Tgstation.Server.Host.Security.AuthenticationContext.User.

+

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, Tgstation.Server.Api.Rights.List, Tgstation.Server.Api.Models.ErrorMessage.Message, Tgstation.Server.Api.Models.Internal.User.Name, Tgstation.Server.Api.Models.Online, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), Tgstation.Server.Api.Rights.SoftRestart, Tgstation.Server.Api.Rights.SoftShutdown, and Tgstation.Server.Host.Security.AuthenticationContext.User.

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  {
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  }
string Name
The name of the User
Definition: User.cs:38
@@ -299,7 +299,7 @@ Private Attributes
readonly IApplication application
The IApplication for the InstanceController
string VersionPrefix
Prefix to VersionString
Definition: IApplication.cs:13
- +
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ConfigurationType
The type of configuration allowed on an Instance
Task CreateDirectory(string path, CancellationToken cancellationToken)
Create a directory at path
@@ -307,7 +307,7 @@ Private Attributes
readonly IIOManager ioManager
The IIOManager for the InstanceController
Task< bool > FileExists(string path, CancellationToken cancellationToken)
Check that the file at path exists
string Message
A human readable description of the error
Definition: ErrorMessage.cs:13
- +
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
DreamDaemonSecurity
DreamDaemon&#39;s security level
Task DeleteFile(string path, CancellationToken cancellationToken)
Deletes a file at path
@@ -359,15 +359,15 @@ Private Attributes

Definition at line 218 of file InstanceController.cs.

-

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ReattachInformations, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.WatchdogReattachInformations.

-
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  }
DbSet< ReattachInformation > ReattachInformations
+

References Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.ReattachInformations, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.WatchdogReattachInformations.

+
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  }
DbSet< ReattachInformation > ReattachInformations
Task WriteAllBytes(string path, byte[] contents, CancellationToken cancellationToken)
Writes some contents to a file at path overwriting previous content
const string InstanceAttachFileName
File name to allow attaching instances
- +
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
readonly IIOManager ioManager
The IIOManager for the InstanceController
- -
DbSet< WatchdogReattachInformation > WatchdogReattachInformations
+ +
DbSet< WatchdogReattachInformation > WatchdogReattachInformations
Represents an error message returned by the server
Definition: ErrorMessage.cs:8
string ConcatPath(params string[] paths)
Combines an array of strings into a path
@@ -411,12 +411,12 @@ Private Attributes

Definition at line 421 of file InstanceController.cs.

-

References Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.Models.Internal.User.InstanceManagerRights, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, and Tgstation.Server.Host.Security.AuthenticationContext.User.

+

References Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.Models.Internal.User.InstanceManagerRights, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, and Tgstation.Server.Host.Security.AuthenticationContext.User.

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  }
InstanceManagerRights InstanceManagerRights
The Rights.InstanceManagerRights for the User
Definition: User.cs:50
- +
InstanceManagerRights
Rights for managing Models.Instances
- +
long Id
The ID of the User
Definition: User.cs:15
@@ -474,12 +474,12 @@ Private Attributes

Definition at line 397 of file InstanceController.cs.

-

References Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.Models.Internal.User.InstanceManagerRights, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, and Tgstation.Server.Host.Security.AuthenticationContext.User.

+

References Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.Models.Internal.User.InstanceManagerRights, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, and Tgstation.Server.Host.Security.AuthenticationContext.User.

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  }
InstanceManagerRights InstanceManagerRights
The Rights.InstanceManagerRights for the User
Definition: User.cs:50
- +
InstanceManagerRights
Rights for managing Models.Instances
- +
long Id
The ID of the User
Definition: User.cs:15
@@ -556,7 +556,7 @@ Private Attributes

Definition at line 252 of file InstanceController.cs.

-

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

+

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

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

Definition at line 51 of file InstanceUserController.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.InstanceUsers, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

+

References Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.InstanceUsers, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

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  }
ConfigurationRights
Rights for Models.ConfigurationFile
ByondRights
Rights for Models.Byond
Definition: ByondRights.cs:9
InstanceUserRights
Rights for an Models.Instance
@@ -236,8 +236,8 @@ Private Member Functions
DreamDaemonRights
Rights for Models.DreamDaemon
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
Metadata about a server instance
Definition: Instance.cs:9
@@ -283,9 +283,9 @@ Private Member Functions

Definition at line 128 of file InstanceUserController.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances.

-
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  }
- +

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances.

+
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  }
+
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Metadata about a server instance
Definition: Instance.cs:9
@@ -329,9 +329,9 @@ Private Member Functions

Definition at line 117 of file InstanceUserController.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances.

-
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  }
- +

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances.

+
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  }
+
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Metadata about a server instance
Definition: Instance.cs:9
@@ -365,9 +365,9 @@ Private Member Functions

Definition at line 109 of file InstanceUserController.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances.

-
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  }
- +

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances.

+
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  }
+
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Metadata about a server instance
Definition: Instance.cs:9
@@ -465,15 +465,15 @@ Private Member Functions

Definition at line 78 of file InstanceUserController.cs.

-

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, Tgstation.Server.Host.Security.AuthenticationContext.InstanceUser, Tgstation.Server.Api.Rights.Read, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), and Tgstation.Server.Host.Security.AuthenticationContext.User.

+

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Instances, Tgstation.Server.Host.Security.AuthenticationContext.InstanceUser, Tgstation.Server.Api.Rights.Read, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), and Tgstation.Server.Host.Security.AuthenticationContext.User.

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  }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
InstanceUserRights
Rights for an Models.Instance
- +
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
BadRequestObjectResult StandardModelChecks(Api.Models.InstanceUser model)
Checks a model for errors
- +
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
long Id
The ID of the User
Definition: User.cs:15
Metadata about a server instance
Definition: Instance.cs:9
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_job_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_job_controller.html index d6e51a6a65..4cc8a8d189 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_job_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_job_controller.html @@ -241,11 +241,11 @@ Private Attributes

Definition at line 61 of file JobController.cs.

-

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Jobs, and Tgstation.Server.Host.Security.AuthenticationContext.User.

+

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Jobs, and Tgstation.Server.Host.Security.AuthenticationContext.User.

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  }
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
- - + +
Task< bool > CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken)
Cancels a give job
readonly IJobManager jobManager
The IJobManager for the JobController
@@ -291,10 +291,10 @@ Private Attributes

Definition at line 80 of file JobController.cs.

-

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

+

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

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  }
int JobProgress(Job job)
Get the Api.Models.Job.Progress for a job
- - + +
readonly IJobManager jobManager
The IJobManager for the JobController
@@ -328,10 +328,10 @@ Private Attributes

Definition at line 49 of file JobController.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Jobs.

-
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  }
+

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Jobs.

+
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  }
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
@@ -364,10 +364,10 @@ Private Attributes

Definition at line 41 of file JobController.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Jobs.

-
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  }
+

References Tgstation.Server.Api.Models.Instance.Id, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Jobs.

+
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  }
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
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_repository_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_repository_controller.html index e2934ecab4..aa3c7b5bd7 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_repository_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_repository_controller.html @@ -277,7 +277,7 @@ Private Attributes

Definition at line 127 of file RepositoryController.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.RepositorySettings, and Tgstation.Server.Host.Security.AuthenticationContext.User.

+

References Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.RepositorySettings, and Tgstation.Server.Host.Security.AuthenticationContext.User.

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 
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  }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
@@ -286,8 +286,8 @@ Private Attributes
readonly IInstanceManager instanceManager
The IInstanceManager for the RepositoryController
string AccessUser
The username to access the git repository with
IRepositoryManager RepositoryManager
The IRepositoryManager for the IInstance
Definition: IInstance.cs:22
- -
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
string Reference
The branch or tag HEAD points to
Definition: Repository.cs:48
@@ -326,15 +326,15 @@ Private Attributes

Definition at line 218 of file RepositoryController.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.RepositorySettings, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), and Tgstation.Server.Host.Security.AuthenticationContext.User.

+

References Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.RepositorySettings, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), and Tgstation.Server.Host.Security.AuthenticationContext.User.

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  }
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
Task RegisterOperation(Job job, Func< Job, IDatabaseContext, Action< int >, CancellationToken, Task > operation, CancellationToken cancellationToken)
Registers a given Job and begins running it
readonly IInstanceManager instanceManager
The IInstanceManager for the RepositoryController
IRepositoryManager RepositoryManager
The IRepositoryManager for the IInstance
Definition: IInstance.cs:22
- -
DbSet< RepositorySettings > RepositorySettings
+ +
DbSet< RepositorySettings > RepositorySettings
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
long Id
The ID of the User
Definition: User.cs:15
Metadata about a server instance
Definition: Instance.cs:9
@@ -401,10 +401,10 @@ Private Attributes

Definition at line 69 of file RepositoryController.cs.

-

References Tgstation.Server.Api.Rights.CompileJobs, Tgstation.Server.Api.Rights.List, and Tgstation.Server.Host.Models.IDatabaseContext.RevisionInformations.

+

References Tgstation.Server.Api.Rights.CompileJobs, Tgstation.Server.Api.Rights.List, and Tgstation.Server.Host.Models.IDatabaseContext.RevisionInformations.

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  }
User may list and read all Models.CompileJobs
Metadata about a server instance
Definition: Instance.cs:9
-
DbSet< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
+
DbSet< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
virtual Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
@@ -499,14 +499,14 @@ Private Attributes

Definition at line 246 of file RepositoryController.cs.

-

References Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.RepositorySettings, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

+

References Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.RepositorySettings, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save().

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();
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  }
IInstance GetInstance(Models.Instance metadata)
Get the IInstance associated with given metadata
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
async Task< bool > PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken)
readonly IInstanceManager instanceManager
The IInstanceManager for the RepositoryController
IRepositoryManager RepositoryManager
The IRepositoryManager for the IInstance
Definition: IInstance.cs:22
- -
DbSet< RepositorySettings > RepositorySettings
+ +
DbSet< RepositorySettings > RepositorySettings
long Id
The id of the Instance. Not modifiable
Definition: Instance.cs:14
Metadata about a server instance
Definition: Instance.cs:9
Represents an error message returned by the server
Definition: ErrorMessage.cs:8
@@ -541,7 +541,7 @@ Private Attributes

Definition at line 282 of file RepositoryController.cs.

-

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.Models.Internal.User.Name, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.RepositorySettings, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), and Tgstation.Server.Host.Security.AuthenticationContext.User.

+

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Instance.Id, Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.Models.Internal.User.Name, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.RepositorySettings, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), and Tgstation.Server.Host.Security.AuthenticationContext.User.

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();
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)
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  }
string Name
The name of the User
Definition: User.cs:38
bool UpdateFromOrigin
Do the equivalent of a git pull. Will attempt to merge unless Reference is also specified in which ca...
Definition: Repository.cs:43
@@ -563,8 +563,8 @@ Private Attributes
string AccessUser
The username to access the git repository with
Represents a git repository
Definition: Repository.cs:8
IRepositoryManager RepositoryManager
The IRepositoryManager for the IInstance
Definition: IInstance.cs:22
- -
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
IGitHubClient CreateClient()
Create a client with anonymous authentication. Low rate limit
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_user_controller.html b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_user_controller.html index 4673c6ea6e..e5f9eff4e4 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_user_controller.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_controllers_1_1_user_controller.html @@ -260,19 +260,19 @@ Private Attributes

Definition at line 66 of file UserController.cs.

-

References Tgstation.Server.Api.Models.Internal.User.Enabled, Tgstation.Server.Api.Rights.List, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), Tgstation.Server.Host.Security.AuthenticationContext.User, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Users.

+

References Tgstation.Server.Api.Models.Internal.User.Enabled, Tgstation.Server.Api.Rights.List, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), Tgstation.Server.Host.Security.AuthenticationContext.User, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Users.

67  {
68  if (model == null)
69  throw new ArgumentNullException(nameof(model));
70 
71  if (!(model.Password == null ^ model.SystemIdentifier == null))
72  return BadRequest(new ErrorMessage { Message = "User must have exactly one of either a password or system identifier!" });
73 
74  model.Name = model.Name?.Trim();
75  if (model.Name?.Length == 0)
76  model.Name = null;
77 
78  if (!(model.Name == null ^ model.SystemIdentifier == null))
79  return BadRequest(new ErrorMessage { Message = "User must have a name if and only if user has no system identifier!" });
80 
81  var dbUser = new Models.User
82  {
84  CreatedAt = DateTimeOffset.Now,
85  CreatedBy = AuthenticationContext.User,
86  Enabled = model.Enabled ?? false,
88  Name = model.Name,
89  SystemIdentifier = model.SystemIdentifier,
90  InstanceUsers = new List<Models.InstanceUser>()
91  };
92 
93  if (model.SystemIdentifier != null)
94  try
95  {
96  using (var sysIdentity = await systemIdentityFactory.CreateSystemIdentity(dbUser, cancellationToken).ConfigureAwait(false))
97  {
98  if (sysIdentity == null)
99  return StatusCode((int)HttpStatusCode.Gone);
100  dbUser.Name = sysIdentity.Username;
101  dbUser.SystemIdentifier = sysIdentity.Uid;
102  }
103  }
104  catch (NotImplementedException)
105  {
106  return StatusCode((int)HttpStatusCode.NotImplemented);
107  }
108  else
109  {
111  return BadRequest(new ErrorMessage { Message = String.Format(CultureInfo.InvariantCulture, "Password must be at least {0} characters long!", generalConfiguration.MinimumPasswordLength) });
112  cryptographySuite.SetUserPassword(dbUser, model.Password, true);
113  }
114 
115  dbUser.CanonicalName = dbUser.Name.ToUpperInvariant();
116 
117  DatabaseContext.Users.Add(dbUser);
118 
119  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
120 
121  return StatusCode((int)HttpStatusCode.Created, dbUser.ToApi(true));
122  }
string Name
The name of the User
Definition: User.cs:38
Task< ISystemIdentity > CreateSystemIdentity(User user, CancellationToken cancellationToken)
Create a ISystemIdentity for a given user
InstanceManagerRights InstanceManagerRights
The Rights.InstanceManagerRights for the User
Definition: User.cs:50
- +
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the UserController
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the UserController
AdministrationRights
Rights for Models.Administration
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
readonly GeneralConfiguration generalConfiguration
The GeneralConfiguration for the UserController
InstanceManagerRights
Rights for managing Models.Instances
- +
string Password
Cleartext password of the User
Definition: UserUpdate.cs:11
override async Task< IActionResult > List(CancellationToken cancellationToken)
Attempt to list entries of the TModel
void SetUserPassword(User user, string newPassword, bool newUser)
Sets a User.PasswordHash for a given user
@@ -323,14 +323,14 @@ Private Attributes

Definition at line 183 of file UserController.cs.

-

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.Rights.Read, Tgstation.Server.Host.Security.AuthenticationContext.User, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Users.

+

References Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Models.Internal.User.Id, Tgstation.Server.Api.Rights.Read, Tgstation.Server.Host.Security.AuthenticationContext.User, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Users.

184  {
185  if (id == AuthenticationContext.User.Id)
186  return await Read(cancellationToken).ConfigureAwait(false);
187 
188  if (!((AdministrationRights)AuthenticationContext.GetRight(RightsType.Administration)).HasFlag(AdministrationRights.ReadUsers))
189  return Forbid();
190 
191  var user = await DatabaseContext.Users
192  .Where(x => x.Id == id)
193  .Include(x => x.CreatedBy)
194  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
195  if (user == default)
196  return NotFound();
197  return Json(user.ToApi(true));
198  }
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
- +
override Task< IActionResult > Read(CancellationToken cancellationToken)
Attempt to read a TModel
AdministrationRights
Rights for Models.Administration
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
- +
long Id
The ID of the User
Definition: User.cs:15
@@ -364,9 +364,9 @@ Private Attributes

Definition at line 173 of file UserController.cs.

-

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

-
174  {
175  var users = await DatabaseContext.Users
176  .Include(x => x.CreatedBy)
177  .ToListAsync(cancellationToken).ConfigureAwait(false);
178  return Json(users.Select(x => x.ToApi(true)));
179  }
- +

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

+
174  {
175  var users = await DatabaseContext.Users
176  .Include(x => x.CreatedBy)
177  .ToListAsync(cancellationToken).ConfigureAwait(false);
178  return Json(users.Select(x => x.ToApi(true)));
179  }
+
@@ -426,17 +426,17 @@ Private Attributes

Definition at line 126 of file UserController.cs.

-

References Tgstation.Server.Api.Models.Internal.User.AdministrationRights, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Rights.Read, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), Tgstation.Server.Host.Security.AuthenticationContext.User, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Users.

+

References Tgstation.Server.Api.Models.Internal.User.AdministrationRights, Tgstation.Server.Host.Security.AuthenticationContext.GetRight(), Tgstation.Server.Api.Rights.Read, Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Save(), Tgstation.Server.Host.Security.AuthenticationContext.User, and Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.Users.

127  {
128  if (model == null)
129  throw new ArgumentNullException(nameof(model));
130 
131  var passwordEditOnly = !AuthenticationContext.User.AdministrationRights.Value.HasFlag(AdministrationRights.WriteUsers);
132 
133  var originalUser = passwordEditOnly ? AuthenticationContext.User : await DatabaseContext.Users.Where(x => x.Id == model.Id)
134  .Include(x => x.CreatedBy)
135  .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
136  if (originalUser == default)
137  return NotFound();
138 
139  if (passwordEditOnly && (model.Id != originalUser.Id || model.InstanceManagerRights.HasValue || model.AdministrationRights.HasValue || model.Enabled.HasValue || model.SystemIdentifier != null || model.Name != null))
140  return Forbid();
141 
142  if (model.Password != null)
143  {
144  if (originalUser.PasswordHash == null)
145  return BadRequest(new ErrorMessage { Message = "Cannot convert a system user to a password user!" });
146  cryptographySuite.SetUserPassword(originalUser, model.Password, false);
147  }
148  else if (model.SystemIdentifier != null && model.SystemIdentifier != originalUser.SystemIdentifier)
149  return BadRequest(new ErrorMessage { Message = "Cannot change a user's system identifier!" });
150 
151  if (model.Name != null && model.Name.ToUpperInvariant() != originalUser.CanonicalName)
152  return BadRequest(new ErrorMessage { Message = "Can only change capitalization of a user's name!" });
153 
154  originalUser.InstanceManagerRights = model.InstanceManagerRights ?? originalUser.InstanceManagerRights;
155  originalUser.AdministrationRights = model.AdministrationRights ?? originalUser.AdministrationRights;
156  originalUser.Enabled = model.Enabled ?? originalUser.Enabled;
157  originalUser.Name = model.Name ?? originalUser.Name;
158 
159  await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
160 
161  return Json(model.Id == originalUser.Id || (AuthenticationContext.GetRight(RightsType.Administration) & (ulong)AdministrationRights.ReadUsers) != 0 ? originalUser.ToApi(true) : new Api.Models.User
162  {
163  Id = originalUser.Id
164  });
165  }
string Name
The name of the User
Definition: User.cs:38
RightsType
The type of rights a model uses
Definition: RightsType.cs:6
InstanceManagerRights InstanceManagerRights
The Rights.InstanceManagerRights for the User
Definition: User.cs:50
- +
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the UserController
AdministrationRights
Rights for Models.Administration
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext
ulong GetRight(RightsType rightsType)
Get the value of a given rightsType
- +
string Password
Cleartext password of the User
Definition: UserUpdate.cs:11
long Id
The ID of the User
Definition: User.cs:15
void SetUserPassword(User user, string newPassword, bool newUser)
Sets a User.PasswordHash for a given user
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application.html index b3a42c2785..749aa08709 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_application.html @@ -294,7 +294,7 @@ Private Attributes

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

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

Definition at line 20 of file DatabaseContextFactory.cs.

-
21  {
22  this.scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
23 
24  using (var scope = scopeFactory.CreateScope())
25  scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
26  }
+
21  {
22  this.scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
23 
24  using (var scope = scopeFactory.CreateScope())
25  scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
26  }
readonly IServiceScopeFactory scopeFactory
The IServiceScopeFactory for the DatabaseContextFactory
@@ -179,7 +179,7 @@ Private Attributes

Implements Tgstation.Server.Host.Core.IDatabaseContextFactory.

Definition at line 29 of file DatabaseContextFactory.cs.

-
30  {
31  if (operation == null)
32  throw new ArgumentNullException(nameof(operation));
33 
34  using (var scope = scopeFactory.CreateScope())
35  await operation(scope.ServiceProvider.GetRequiredService<IDatabaseContext>()).ConfigureAwait(false);
36  }
+
30  {
31  if (operation == null)
32  throw new ArgumentNullException(nameof(operation));
33 
34  using (var scope = scopeFactory.CreateScope())
35  await operation(scope.ServiceProvider.GetRequiredService<IDatabaseContext>()).ConfigureAwait(false);
36  }
readonly IServiceScopeFactory scopeFactory
The IServiceScopeFactory for the DatabaseContextFactory
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_semaphore_slim_context.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_semaphore_slim_context.html index 72db986eed..c54a479088 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_semaphore_slim_context.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_semaphore_slim_context.html @@ -272,7 +272,7 @@ Private Attributes

Definition at line 18 of file SemaphoreSlimContext.cs.

-

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.ChangeSettings(), Tgstation.Server.Host.Components.Byond.ByondManager.ChangeVersion(), Tgstation.Server.Host.Components.Repository.RepositoryManager.CloneRepository(), Tgstation.Server.Host.Components.StaticFiles.Configuration.CopyDMFilesTo(), Tgstation.Server.Host.Components.StaticFiles.Configuration.CreateDirectory(), Tgstation.Server.Host.Components.StaticFiles.Configuration.DeleteDirectory(), Tgstation.Server.Host.Components.Repository.RepositoryManager.DeleteRepository(), Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleChatCommand(), Tgstation.Server.Host.Components.StaticFiles.Configuration.HandleEvent(), Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleEvent(), Tgstation.Server.Host.Components.Byond.WindowsByondInstaller.InstallByond(), Tgstation.Server.Host.Components.Watchdog.Watchdog.Launch(), Tgstation.Server.Host.Components.StaticFiles.Configuration.ListDirectory(), Tgstation.Server.Host.Components.Watchdog.Watchdog.MonitorLifetimes(), Tgstation.Server.Host.Components.StaticFiles.Configuration.Read(), Tgstation.Server.Host.Components.Watchdog.Watchdog.ResetRebootState(), Tgstation.Server.Host.Components.Watchdog.Watchdog.Restart(), Tgstation.Server.Host.Components.Chat.JsonTrackingContext.SetChannels(), Tgstation.Server.Host.Components.Watchdog.Watchdog.StartAsync(), Tgstation.Server.Host.Components.StaticFiles.Configuration.SymlinkStaticFilesTo(), Tgstation.Server.Host.Components.Watchdog.Watchdog.Terminate(), and Tgstation.Server.Host.Components.StaticFiles.Configuration.Write().

+

Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.ChangeSettings(), Tgstation.Server.Host.Components.Byond.ByondManager.ChangeVersion(), Tgstation.Server.Host.Components.Repository.RepositoryManager.CloneRepository(), Tgstation.Server.Host.Components.StaticFiles.Configuration.CopyDMFilesTo(), Tgstation.Server.Host.Components.StaticFiles.Configuration.CreateDirectory(), Tgstation.Server.Host.Components.StaticFiles.Configuration.DeleteDirectory(), Tgstation.Server.Host.Components.Repository.RepositoryManager.DeleteRepository(), Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleChatCommand(), Tgstation.Server.Host.Components.StaticFiles.Configuration.HandleEvent(), Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleEvent(), Tgstation.Server.Host.Components.Byond.WindowsByondInstaller.InstallByond(), Tgstation.Server.Host.Components.Watchdog.Watchdog.Launch(), Tgstation.Server.Host.Components.StaticFiles.Configuration.ListDirectory(), Tgstation.Server.Host.Components.Watchdog.Watchdog.MonitorLifetimes(), Tgstation.Server.Host.Components.StaticFiles.Configuration.Read(), Tgstation.Server.Host.Components.Watchdog.Watchdog.ResetRebootState(), Tgstation.Server.Host.Components.Watchdog.Watchdog.Restart(), Tgstation.Server.Host.Components.Chat.JsonTrackingContext.SetChannels(), Tgstation.Server.Host.Components.Watchdog.Watchdog.StartAsync(), Tgstation.Server.Host.Components.StaticFiles.Configuration.SymlinkStaticFilesTo(), Tgstation.Server.Host.Components.Watchdog.Watchdog.Terminate(), and Tgstation.Server.Host.Components.StaticFiles.Configuration.Write().

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  }
SemaphoreSlimContext(SemaphoreSlim lockedSemaphore)
Construct a SemaphoreSlimContext
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_database_context_factory.html b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_database_context_factory.html index b9166b649e..5a82ff47d5 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_database_context_factory.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_core_1_1_tests_1_1_test_database_context_factory.html @@ -124,7 +124,7 @@ Public Member Functions

Definition at line 14 of file TestDatabaseContextFactory.cs.

-
15  {
16  Assert.ThrowsException<ArgumentNullException>(() => new DatabaseContextFactory(null));
17  var mockProvider = new Mock<IServiceProvider>();
18  mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Verifiable();
19  var mockScope = new Mock<IServiceScope>();
20  mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
21  var mockScopeFactory = new Mock<IServiceScopeFactory>();
22  mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
23  Assert.ThrowsException<InvalidOperationException>(() => new DatabaseContextFactory(mockScopeFactory.Object));
24  mockScopeFactory.VerifyAll();
25  mockScope.VerifyAll();
26  mockProvider.VerifyAll();
27  }
+
15  {
16  Assert.ThrowsException<ArgumentNullException>(() => new DatabaseContextFactory(null));
17  var mockProvider = new Mock<IServiceProvider>();
18  mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Verifiable();
19  var mockScope = new Mock<IServiceScope>();
20  mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
21  var mockScopeFactory = new Mock<IServiceScopeFactory>();
22  mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
23  Assert.ThrowsException<InvalidOperationException>(() => new DatabaseContextFactory(mockScopeFactory.Object));
24  mockScopeFactory.VerifyAll();
25  mockScope.VerifyAll();
26  mockProvider.VerifyAll();
27  }
@@ -142,7 +142,7 @@ Public Member Functions

Definition at line 30 of file TestDatabaseContextFactory.cs.

-
31  {
32  var mockDatabase = new Mock<IDatabaseContext>();
33  var mockProvider = new Mock<IServiceProvider>();
34  var mockDbo = mockDatabase.Object;
35  mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Returns(mockDbo).Verifiable();
36  var mockScope = new Mock<IServiceScope>();
37  mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
38  var mockScopeFactory = new Mock<IServiceScopeFactory>();
39  mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
40 
41  var factory = new DatabaseContextFactory(mockScopeFactory.Object);
42 
43  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => factory.UseContext(null)).ConfigureAwait(false);
44 
45  await factory.UseContext(context =>
46  {
47  Assert.AreSame(mockDbo, context);
48  return Task.CompletedTask;
49  });
50 
51  mockScopeFactory.VerifyAll();
52  mockScope.VerifyAll();
53  mockProvider.VerifyAll();
54  }
+
31  {
32  var mockDatabase = new Mock<IDatabaseContext>();
33  var mockProvider = new Mock<IServiceProvider>();
34  var mockDbo = mockDatabase.Object;
35  mockProvider.Setup(x => x.GetService(typeof(IDatabaseContext))).Returns(mockDbo).Verifiable();
36  var mockScope = new Mock<IServiceScope>();
37  mockScope.SetupGet(x => x.ServiceProvider).Returns(mockProvider.Object).Verifiable();
38  var mockScopeFactory = new Mock<IServiceScopeFactory>();
39  mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object).Verifiable();
40 
41  var factory = new DatabaseContextFactory(mockScopeFactory.Object);
42 
43  await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => factory.UseContext(null)).ConfigureAwait(false);
44 
45  await factory.UseContext(context =>
46  {
47  Assert.AreSame(mockDbo, context);
48  return Task.CompletedTask;
49  });
50 
51  mockScopeFactory.VerifyAll();
52  mockScope.VerifyAll();
53  mockProvider.VerifyAll();
54  }
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context-members.html b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context-members.html index 38d6f86023..3e748d10f2 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context-members.html @@ -117,7 +117,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); RevInfoTestMergesTgstation.Server.Host.Models.DatabaseContext< TParentContext > RevisionInformationsTgstation.Server.Host.Models.DatabaseContext< TParentContext > Save(CancellationToken cancellationToken)Tgstation.Server.Host.Models.DatabaseContext< TParentContext > - TestMergesTgstation.Server.Host.Models.DatabaseContext< TParentContext > + SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)Tgstation.Server.Host.Models.DatabaseContext< TParentContext > + TestMergesTgstation.Server.Host.Models.DatabaseContext< TParentContext > + UseMySQLMigrations()Tgstation.Server.Host.Models.DatabaseContext< TParentContext >protectedpure virtual UsersTgstation.Server.Host.Models.DatabaseContext< TParentContext > WatchdogReattachInformationsTgstation.Server.Host.Models.DatabaseContext< TParentContext >
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context.html b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context.html index a1af99b168..52499ddcad 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_context.html @@ -98,7 +98,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); Private Attributes | List of all members
-
Tgstation.Server.Host.Models.DatabaseContext< TParentContext > Class Template Reference
+
Tgstation.Server.Host.Models.DatabaseContext< TParentContext > Class Template Referenceabstract
@@ -126,11 +126,17 @@ Public Member Functions Task Save (CancellationToken cancellationToken)  Saves changes made to the IDatabaseContext More...
  +async Task SchemaDowngradeForServerVersion (Version version, CancellationToken cancellationToken) + Attempt to downgrade the schema to the migration used for a given server version More...
+  + + +

Protected Member Functions

override void OnModelCreating (ModelBuilder modelBuilder)
 
abstract bool UseMySQLMigrations ()
 If the MY_ class of migrations should be used instead of the MS_ class More...
 
@@ -229,7 +235,7 @@ Private Attributes -

Definition at line 13 of file DatabaseContext.cs.

+

Definition at line 17 of file DatabaseContext.cs.

Constructor & Destructor Documentation

@@ -278,10 +284,10 @@ Private Attributes -

Definition at line 86 of file DatabaseContext.cs.

-
86  : base(dbContextOptions)
87  {
88  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
89  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
90  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
91  }
Configuration options for the Models.DatabaseContext<TParentContext>
-
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
-
ILogger Logger
The ILogger for the DatabaseContext<TParentContext>
+

Definition at line 90 of file DatabaseContext.cs.

+
90  : 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  }
Configuration options for the Models.DatabaseContext<TParentContext>
+
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
+
ILogger Logger
The ILogger for the DatabaseContext<TParentContext>
@@ -304,14 +310,14 @@ Private Attributes

Implements Tgstation.Server.Host.Models.IDatabaseContext.

-

Definition at line 137 of file DatabaseContext.cs.

-
138  {
140  {
141  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
142  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
143  }
144 
145  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
146  var wasEmpty = !migrations.Any();
147 
148  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
149  {
150  Logger.LogInformation("Migrating database...");
151  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
152  }
153  else
154  Logger.LogDebug("No migrations to apply.");
155 
156  wasEmpty |= (await Users.CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
157 
158  if (wasEmpty)
159  {
160  Logger.LogInformation("Seeding database...");
161  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
162  }
164  {
165  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
166  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
167  }
168  }
+

Definition at line 141 of file DatabaseContext.cs.

+
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  }
Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Changes the admin password in IDatabaseContext back to it&#39;s default and enables the account ...
bool ResetAdminPassword
If the admin user should be enabled and have it&#39;s password reset
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>
-
ILogger Logger
The ILogger for the DatabaseContext<TParentContext>
+
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
+
ILogger Logger
The ILogger for the DatabaseContext<TParentContext>
Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken)
Initially seed a given databaseContext
@@ -340,9 +346,9 @@ Private Attributes

-

Definition at line 94 of file DatabaseContext.cs.

-
95  {
96  Logger.LogTrace("Building entity framework context...");
97  base.OnModelCreating(modelBuilder);
98 
99  var userModel = modelBuilder.Entity<User>();
100  userModel.HasIndex(x => x.CanonicalName).IsUnique();
101  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
102 
103  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
104 
105  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.Cascade);
106 
107  var revInfo = modelBuilder.Entity<RevisionInformation>();
108  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
109  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
110  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Restrict);
111  revInfo.HasIndex(x => x.CommitSha).IsUnique();
112 
113  modelBuilder.Entity<CompileJob>().HasIndex(x => x.DirectoryName);
114 
115  modelBuilder.Entity<Job>().HasOne<CompileJob>().WithOne(x => x.Job).OnDelete(DeleteBehavior.Restrict);
116 
117  var chatChannel = modelBuilder.Entity<ChatChannel>();
118  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
119  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
120  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
121 
122  modelBuilder.Entity<ChatBot>().HasIndex(x => x.Name).IsUnique();
123 
124  var instanceModel = modelBuilder.Entity<Instance>();
125  instanceModel.HasIndex(x => x.Path).IsUnique();
126  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
127  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
128  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
129  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
130  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
131  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
132  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
133  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
134  }
-
ILogger Logger
The ILogger for the DatabaseContext<TParentContext>
+

Definition at line 98 of file DatabaseContext.cs.

+
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  }
+
ILogger Logger
The ILogger for the DatabaseContext<TParentContext>
@@ -366,6 +372,69 @@ Private Attributes

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

+ + + +
+
+

Properties

+ + + + + + + + + + + + + + + + + +
async Task Tgstation.Server.Host.Models.DatabaseContext< TParentContext >.SchemaDowngradeForServerVersion (Version version,
CancellationToken cancellationToken 
)
+
+ +

Attempt to downgrade the schema to the migration used for a given server version

+ +

Implements Tgstation.Server.Host.Models.IDatabaseContext.

+ +

Definition at line 184 of file DatabaseContext.cs.

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

If the MY_ class of migrations should be used instead of the MS_ class

+
Returns
if the MY_ class of migrations should be used instead of the MS_ class, otherwise
+ +

Implemented in Tgstation.Server.Host.Models.MySqlDatabaseContext, and Tgstation.Server.Host.Models.SqlServerDatabaseContext.

+

Member Data Documentation

@@ -389,7 +458,7 @@ Private Attributes

The IDatabaseSeeder for the DatabaseContext<TParentContext>

-

Definition at line 77 of file DatabaseContext.cs.

+

Definition at line 81 of file DatabaseContext.cs.

@@ -414,7 +483,7 @@ Private Attributes

-

Definition at line 31 of file DatabaseContext.cs.

+

Definition at line 35 of file DatabaseContext.cs.

Referenced by Tgstation.Server.Host.Controllers.ChatController.Create(), Tgstation.Server.Host.Controllers.ChatController.Delete(), Tgstation.Server.Host.Controllers.ChatController.GetId(), Tgstation.Server.Host.Controllers.ChatController.List(), and Tgstation.Server.Host.Controllers.ChatController.Update().

@@ -440,7 +509,7 @@ Private Attributes

-

Definition at line 43 of file DatabaseContext.cs.

+

Definition at line 47 of file DatabaseContext.cs.

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

@@ -466,7 +535,7 @@ Private Attributes

-

Definition at line 22 of file DatabaseContext.cs.

+

Definition at line 26 of file DatabaseContext.cs.

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

@@ -492,7 +561,7 @@ Private Attributes

The DatabaseConfiguration for the DatabaseContext<TParentContext>

-

Definition at line 72 of file DatabaseContext.cs.

+

Definition at line 76 of file DatabaseContext.cs.

@@ -516,7 +585,7 @@ Private Attributes

-

Definition at line 34 of file DatabaseContext.cs.

+

Definition at line 38 of file DatabaseContext.cs.

@@ -540,7 +609,7 @@ Private Attributes

-

Definition at line 28 of file DatabaseContext.cs.

+

Definition at line 32 of file DatabaseContext.cs.

Referenced by Tgstation.Server.Host.Controllers.DreamMakerController.Read(), and Tgstation.Server.Host.Controllers.DreamMakerController.Update().

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

-

Definition at line 19 of file DatabaseContext.cs.

+

Definition at line 23 of file DatabaseContext.cs.

Referenced by Tgstation.Server.Host.Controllers.InstanceController.Create(), Tgstation.Server.Host.Controllers.InstanceUserController.Delete(), Tgstation.Server.Host.Controllers.InstanceController.Delete(), Tgstation.Server.Host.Controllers.InstanceUserController.GetId(), Tgstation.Server.Host.Controllers.InstanceController.GetId(), Tgstation.Server.Host.Controllers.InstanceUserController.List(), Tgstation.Server.Host.Controllers.InstanceController.List(), Tgstation.Server.Host.Controllers.DreamDaemonController.ReadImpl(), Tgstation.Server.Host.Controllers.InstanceUserController.Update(), Tgstation.Server.Host.Controllers.DreamDaemonController.Update(), and Tgstation.Server.Host.Controllers.InstanceController.Update().

@@ -592,7 +661,7 @@ Private Attributes

-

Definition at line 40 of file DatabaseContext.cs.

+

Definition at line 44 of file DatabaseContext.cs.

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

@@ -618,7 +687,7 @@ Private Attributes

-

Definition at line 46 of file DatabaseContext.cs.

+

Definition at line 50 of file DatabaseContext.cs.

Referenced by Tgstation.Server.Host.Controllers.JobController.Delete(), Tgstation.Server.Host.Controllers.JobController.GetId(), Tgstation.Server.Host.Controllers.JobController.List(), and Tgstation.Server.Host.Controllers.JobController.Read().

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

The ILogger for the DatabaseContext<TParentContext>

-

Definition at line 67 of file DatabaseContext.cs.

+

Definition at line 71 of file DatabaseContext.cs.

@@ -668,7 +737,7 @@ Private Attributes

-

Definition at line 49 of file DatabaseContext.cs.

+

Definition at line 53 of file DatabaseContext.cs.

Referenced by Tgstation.Server.Host.Controllers.InstanceController.Delete().

@@ -694,7 +763,7 @@ Private Attributes

-

Definition at line 37 of file DatabaseContext.cs.

+

Definition at line 41 of file DatabaseContext.cs.

Referenced by Tgstation.Server.Host.Controllers.RepositoryController.Create(), Tgstation.Server.Host.Controllers.RepositoryController.Delete(), Tgstation.Server.Host.Controllers.RepositoryController.Read(), and Tgstation.Server.Host.Controllers.RepositoryController.Update().

@@ -720,7 +789,7 @@ Private Attributes

The RevInfoTestMerges om the DatabaseContext<TParentContext>

-

Definition at line 62 of file DatabaseContext.cs.

+

Definition at line 66 of file DatabaseContext.cs.

@@ -744,7 +813,7 @@ Private Attributes

-

Definition at line 25 of file DatabaseContext.cs.

+

Definition at line 29 of file DatabaseContext.cs.

@@ -768,7 +837,7 @@ Private Attributes

The TestMerges in the DatabaseContext<TParentContext>

-

Definition at line 57 of file DatabaseContext.cs.

+

Definition at line 61 of file DatabaseContext.cs.

@@ -792,7 +861,7 @@ Private Attributes

-

Definition at line 16 of file DatabaseContext.cs.

+

Definition at line 20 of file DatabaseContext.cs.

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

@@ -818,7 +887,7 @@ Private Attributes

-

Definition at line 52 of file DatabaseContext.cs.

+

Definition at line 56 of file DatabaseContext.cs.

Referenced by Tgstation.Server.Host.Controllers.InstanceController.Delete().

diff --git a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_seeder.html b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_seeder.html index b94a1fe1df..e7608790ab 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_seeder.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_database_seeder.html @@ -196,7 +196,7 @@ Private Attributes

Definition at line 52 of file DatabaseSeeder.cs.

-

References Tgstation.Server.Host.Models.IDatabaseContext.Save(), and Tgstation.Server.Host.Models.IDatabaseContext.Users.

+

References Tgstation.Server.Host.Models.IDatabaseContext.Save(), and Tgstation.Server.Host.Models.IDatabaseContext.Users.

53  {
54  var admin = await databaseContext.Users.Where(x => x.CanonicalName == Api.Models.User.AdminName.ToUpperInvariant()).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
55  if (admin == default)
56  SeedAdminUser(databaseContext);
57  else
58  {
59  admin.Enabled = true;
60  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, false);
61  }
62 
63  await databaseContext.Save(cancellationToken).ConfigureAwait(false);
64  }
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the DatabaseContext<TParentContext>
void SeedAdminUser(IDatabaseContext databaseContext)
Add a default admin User to a given databaseContext
void SetUserPassword(User user, string newPassword, bool newUser)
Sets a User.PasswordHash for a given user
@@ -235,7 +235,7 @@ Private Attributes

Definition at line 29 of file DatabaseSeeder.cs.

-

References Tgstation.Server.Host.Models.IDatabaseContext.Users.

+

References Tgstation.Server.Host.Models.IDatabaseContext.Users.

30  {
31  var admin = new User
32  {
34  CreatedAt = DateTimeOffset.Now,
36  Name = Api.Models.User.AdminName,
37  CanonicalName = Api.Models.User.AdminName.ToUpperInvariant(),
38  Enabled = true,
39  };
40  cryptographySuite.SetUserPassword(admin, Api.Models.User.DefaultAdminPassword, true);
41  databaseContext.Users.Add(admin);
42  }
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the DatabaseContext<TParentContext>
AdministrationRights
Rights for Models.Administration
InstanceManagerRights
Rights for managing Models.Instances
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_my_sql_database_context-members.html b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_my_sql_database_context-members.html index f5a73763aa..0eb87031c3 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_my_sql_database_context-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_my_sql_database_context-members.html @@ -118,7 +118,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); RevInfoTestMergesTgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext > RevisionInformationsTgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext > Save(CancellationToken cancellationToken)Tgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext > - TestMergesTgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext > + SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)Tgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext > + TestMergesTgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext > + UseMySQLMigrations()Tgstation.Server.Host.Models.MySqlDatabaseContextprotectedvirtual UsersTgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext > WatchdogReattachInformationsTgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext >
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_my_sql_database_context.html b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_my_sql_database_context.html index c1c4abb8c7..d695430d66 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_my_sql_database_context.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_my_sql_database_context.html @@ -125,11 +125,16 @@ Public Member Functions   Task Save (CancellationToken cancellationToken)   +async Task SchemaDowngradeForServerVersion (Version version, CancellationToken cancellationToken) +  + + + @@ -229,7 +234,7 @@ Additional Inherited Members

Definition at line 23 of file MySqlDatabaseContext.cs.

-
23  : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
24  { }
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
+
23  : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
24  { }
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
@@ -266,8 +271,35 @@ Additional Inherited Members
string MySqlServerVersion
The string form of the System.Version of a target MySQL/MariaDB server
Configuration options for the Models.DatabaseContext<TParentContext>
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
-
ILogger Logger
The ILogger for the DatabaseContext<TParentContext>
+
ILogger Logger
The ILogger for the DatabaseContext<TParentContext>
+ + + +
+
+

Protected Member Functions

override void OnConfiguring (DbContextOptionsBuilder options)
 
override bool UseMySQLMigrations ()
 If the MY_ class of migrations should be used instead of the MS_ class More...
 
- Protected Member Functions inherited from Tgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext >
override void OnModelCreating (ModelBuilder modelBuilder)
 
+ + + + +
+ + + + + + + +
override bool Tgstation.Server.Host.Models.MySqlDatabaseContext.UseMySQLMigrations ()
+
+protectedvirtual
+
+ +

If the MY_ class of migrations should be used instead of the MS_ class

+ +

Implements Tgstation.Server.Host.Models.DatabaseContext< MySqlDatabaseContext >.

+

The documentation for this class was generated from the following file:
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_sql_server_database_context-members.html b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_sql_server_database_context-members.html index cc507c4986..0d1bf5a6fa 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_sql_server_database_context-members.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_sql_server_database_context-members.html @@ -117,8 +117,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); RevInfoTestMergesTgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext > RevisionInformationsTgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext > Save(CancellationToken cancellationToken)Tgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext > - SqlServerDatabaseContext(DbContextOptions< SqlServerDatabaseContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger< SqlServerDatabaseContext > logger)Tgstation.Server.Host.Models.SqlServerDatabaseContext - TestMergesTgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext > + SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)Tgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext > + SqlServerDatabaseContext(DbContextOptions< SqlServerDatabaseContext > dbContextOptions, IOptions< DatabaseConfiguration > databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger< SqlServerDatabaseContext > logger)Tgstation.Server.Host.Models.SqlServerDatabaseContext + TestMergesTgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext > + UseMySQLMigrations()Tgstation.Server.Host.Models.SqlServerDatabaseContextprotectedvirtual UsersTgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext > WatchdogReattachInformationsTgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext >
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_sql_server_database_context.html b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_sql_server_database_context.html index 2d0e512c34..52a1466c15 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_sql_server_database_context.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_models_1_1_sql_server_database_context.html @@ -125,11 +125,16 @@ Public Member Functions   Task Save (CancellationToken cancellationToken)   +async Task SchemaDowngradeForServerVersion (Version version, CancellationToken cancellationToken) +  + + + @@ -229,7 +234,7 @@ Additional Inherited Members

Definition at line 20 of file SqlServerDatabaseContext.cs.

-
20  : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
21  { }
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
+
20  : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
21  { }
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext<TParentContext>
@@ -264,6 +269,33 @@ Additional Inherited Members
25  {
26  base.OnConfiguring(options);
27  options.UseSqlServer(DatabaseConfiguration.ConnectionString);
28  }
string ConnectionString
The connection string for the database
Configuration options for the Models.DatabaseContext<TParentContext>
+ + + +
+
+

Protected Member Functions

override void OnConfiguring (DbContextOptionsBuilder options)
 
override bool UseMySQLMigrations ()
 If the MY_ class of migrations should be used instead of the MS_ class More...
 
- Protected Member Functions inherited from Tgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext >
override void OnModelCreating (ModelBuilder modelBuilder)
 
+ + + + +
+ + + + + + + +
override bool Tgstation.Server.Host.Models.SqlServerDatabaseContext.UseMySQLMigrations ()
+
+protectedvirtual
+
+ +

If the MY_ class of migrations should be used instead of the MS_ class

+ +

Implements Tgstation.Server.Host.Models.DatabaseContext< SqlServerDatabaseContext >.

+

The documentation for this class was generated from the following file:
diff --git a/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_authentication_context_factory.html b/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_authentication_context_factory.html index 218535dcbb..bf02300ea7 100644 --- a/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_authentication_context_factory.html +++ b/class_tgstation_1_1_server_1_1_host_1_1_security_1_1_authentication_context_factory.html @@ -239,12 +239,12 @@ Private Attributes

Definition at line 48 of file AuthenticationContextFactory.cs.

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  }
ISystemIdentity LoadCachedIdentity(User user)
Attempt to load a cached ISystemIdentity
-
DbSet< User > Users
The Users in the IDatabaseContext
+
DbSet< User > Users
The Users in the IDatabaseContext
readonly IIdentityCache identityCache
The IIdentityCache for the AuthenticationContextFactory
readonly IDatabaseContext databaseContext
The IDatabaseContext for the AuthenticationContextFactory
-
DbSet< InstanceUser > InstanceUsers
The InstanceUsers in the IDatabaseContext
+
DbSet< InstanceUser > InstanceUsers
The InstanceUsers in the IDatabaseContext
diff --git a/dir_91e2bbee1291fcb0db7e0417b617d01e.html b/dir_019c6d3af924151b681494cc466be1ac.html similarity index 93% rename from dir_91e2bbee1291fcb0db7e0417b617d01e.html rename to dir_019c6d3af924151b681494cc466be1ac.html index b47601cd5d..f266cfd73a 100644 --- a/dir_91e2bbee1291fcb0db7e0417b617d01e.html +++ b/dir_019c6d3af924151b681494cc466be1ac.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Release Directory Reference +tgstation-server: tests/Tgstation.Server.Client.Tests/obj/Debug Directory Reference @@ -84,18 +84,18 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
-
Release Directory Reference
+
Debug Directory Reference
- +

Directories

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

Directories

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

Directories

directory  Release
directory  Debug
 
diff --git a/dir_b2c89c850c962495793d09ea3a2ee385.html b/dir_1083fcf52a7d94a2efffb6ca88246acf.html similarity index 95% rename from dir_b2c89c850c962495793d09ea3a2ee385.html rename to dir_1083fcf52a7d94a2efffb6ca88246acf.html index 9e631c4d68..e8b31798d9 100644 --- a/dir_b2c89c850c962495793d09ea3a2ee385.html +++ b/dir_1083fcf52a7d94a2efffb6ca88246acf.html @@ -4,7 +4,7 @@ -tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Release/netcoreapp2.1 Directory Reference +tgstation-server: tests/Tgstation.Server.Host.Console.Tests/obj/Debug/netcoreapp2.1 Directory Reference @@ -84,7 +84,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
diff --git a/dir_177c182c6d1f29bec543c3fdad3072d5.html b/dir_177c182c6d1f29bec543c3fdad3072d5.html index c5f1f80c35..de1657aed2 100644 --- a/dir_177c182c6d1f29bec543c3fdad3072d5.html +++ b/dir_177c182c6d1f29bec543c3fdad3072d5.html @@ -95,7 +95,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); - +

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

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

Directories

directory  Release
directory  Debug
 
diff --git a/files.html b/files.html index 2e3265f289..d571cbf37a 100644 --- a/files.html +++ b/files.html @@ -133,8 +133,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  User.cs  UserUpdate.cs   obj -  Release -  netstandard2.0 +  Debug +  netstandard2.0  Tgstation.Server.Api.AssemblyInfo.cs   Rights  AdministrationRights.cs @@ -171,8 +171,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  JobsClient.cs  RepositoryClient.cs   obj -  Release -  netstandard2.0 +  Debug +  netstandard2.0  Tgstation.Server.Client.AssemblyInfo.cs  AdministrationClient.cs  ApiClient.cs @@ -422,8 +422,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  WatchdogReattachInformation.cs  WatchdogReattachInformationBase.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.AssemblyInfo.cs  Tgstation.Server.Host.RazorAssemblyInfo.cs   Security @@ -454,8 +454,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  ServerFactory.cs   Tgstation.Server.Host.Console   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.Console.AssemblyInfo.cs  AssemblyInfo.cs  Program.cs @@ -467,8 +467,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  ServerService.cs   Tgstation.Server.Host.Watchdog   obj -  Release -  netstandard2.0 +  Debug +  netstandard2.0  Tgstation.Server.Host.Watchdog.AssemblyInfo.cs  AssemblyInfo.cs  IWatchdog.cs @@ -480,8 +480,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');   Models  TestIrcConnectionStringBuilder.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Api.Tests.AssemblyInfo.cs  Tgstation.Server.Api.Tests.Program.cs   Rights @@ -491,16 +491,16 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');   Components  TestDreamDaemonClient.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Client.Tests.AssemblyInfo.cs  Tgstation.Server.Client.Tests.Program.cs  TestApiClient.cs  TestServerClientFactory.cs   Tgstation.Server.Host.Console.Tests   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.Console.Tests.AssemblyInfo.cs  Tgstation.Server.Host.Console.Tests.Program.cs  TestProgram.cs @@ -532,8 +532,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  TestPostWriteHandler.cs  TestSymlinkFactory.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.Tests.AssemblyInfo.cs  Tgstation.Server.Host.Tests.Program.cs   Security @@ -541,8 +541,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  TestServerFactory.cs   Tgstation.Server.Host.Watchdog.Tests   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Host.Watchdog.Tests.AssemblyInfo.cs  Tgstation.Server.Host.Watchdog.Tests.Program.cs  TestWatchdog.cs @@ -553,8 +553,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');  ConfigurationTest.cs  InstanceTest.cs   obj -  Release -  netcoreapp2.1 +  Debug +  netcoreapp2.1  Tgstation.Server.Tests.AssemblyInfo.cs  Tgstation.Server.Tests.Program.cs  AdministrationTest.cs diff --git a/functions_d.html b/functions_d.html index ae511fbe89..d8ea0d9f00 100644 --- a/functions_d.html +++ b/functions_d.html @@ -447,6 +447,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); , Tgstation.Server.Host.Models.Migrations.MYNullableAndForeignKeyCleanup , Tgstation.Server.Host.Models.Migrations.MYReattachCompileJobRequired +
  • downgradeVersion +: Tgstation.Server.Host.Components.InstanceManager +
  • DownloadFile() : Tgstation.Server.Host.IO.DefaultIOManager , Tgstation.Server.Host.IO.IIOManager diff --git a/functions_func_h.html b/functions_func_h.html index b18b8643ad..14009c0293 100644 --- a/functions_func_h.html +++ b/functions_func_h.html @@ -146,6 +146,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • HandleRestart() : Tgstation.Server.Host.Components.Chat.Chat +, Tgstation.Server.Host.Components.InstanceManager , Tgstation.Server.Host.Components.Watchdog.Watchdog , Tgstation.Server.Host.Core.IRestartHandler
  • diff --git a/functions_func_i.html b/functions_func_i.html index 433c39f09a..21216af1e0 100644 --- a/functions_func_i.html +++ b/functions_func_i.html @@ -171,7 +171,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Host.Components.InstanceFactory
  • InstanceManager() -: Tgstation.Server.Host.Components.InstanceManager +: Tgstation.Server.Host.Components.InstanceManager
  • InstanceManagerClient() : Tgstation.Server.Client.InstanceManagerClient diff --git a/functions_func_s.html b/functions_func_s.html index 05a341d526..4273064fec 100644 --- a/functions_func_s.html +++ b/functions_func_s.html @@ -139,6 +139,10 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • SaveConfiguration() : Tgstation.Server.Host.Core.SetupWizard
  • +
  • SchemaDowngradeForServerVersion() +: Tgstation.Server.Host.Models.DatabaseContext< TParentContext > +, Tgstation.Server.Host.Models.IDatabaseContext +
  • SecurityWord() : Tgstation.Server.Host.Components.Watchdog.SessionControllerFactory
  • diff --git a/functions_func_u.html b/functions_func_u.html index e1d0662115..117e1c8f41 100644 --- a/functions_func_u.html +++ b/functions_func_u.html @@ -178,7 +178,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • Update< TBody, TResult >() : Tgstation.Server.Client.ApiClient -, Tgstation.Server.Client.IApiClient +, Tgstation.Server.Client.IApiClient
  • Update< TResult >() : Tgstation.Server.Client.ApiClient @@ -198,6 +198,11 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Host.Components.Byond.ByondManager , Tgstation.Server.Host.Components.Byond.IByondManager
  • +
  • UseMySQLMigrations() +: Tgstation.Server.Host.Models.DatabaseContext< TParentContext > +, Tgstation.Server.Host.Models.MySqlDatabaseContext +, Tgstation.Server.Host.Models.SqlServerDatabaseContext +
  • User() : Tgstation.Server.Client.UsersClient
  • diff --git a/functions_h.html b/functions_h.html index 2772fb4552..e1d3584e79 100644 --- a/functions_h.html +++ b/functions_h.html @@ -157,6 +157,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • HandleRestart() : Tgstation.Server.Host.Components.Chat.Chat +, Tgstation.Server.Host.Components.InstanceManager , Tgstation.Server.Host.Components.Watchdog.Watchdog , Tgstation.Server.Host.Core.IRestartHandler
  • diff --git a/functions_i.html b/functions_i.html index c9bfea8fd3..af53fba658 100644 --- a/functions_i.html +++ b/functions_i.html @@ -282,7 +282,7 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
  • InstanceManager : Tgstation.Server.Api.Routes -, Tgstation.Server.Host.Components.InstanceManager +, Tgstation.Server.Host.Components.InstanceManager
  • instanceManager : Tgstation.Server.Host.Controllers.ByondController @@ -389,16 +389,14 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); : Tgstation.Server.Api.Models.ChatChannel
  • IrcConnectionStringBuilder() -: Tgstation.Server.Api.Models.IrcConnectionStringBuilder +: Tgstation.Server.Api.Models.IrcConnectionStringBuilder
  • IrcProvider() : Tgstation.Server.Host.Components.Chat.Providers.IrcProvider
  • -
  • IsAdmin -: Tgstation.Server.Host.Components.Chat.Channel -
  • IsAdminChannel : Tgstation.Server.Api.Models.ChatChannel +, Tgstation.Server.Host.Components.Chat.Channel
  • IsAdministrator() : Tgstation.Server.Host.Service.Program @@ -421,8 +419,8 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search'); , Tgstation.Server.Host.Components.Watchdog.SessionController , Tgstation.Server.Host.Models.ReattachInformationBase
  • -
  • IsPrivate -: Tgstation.Server.Host.Components.Chat.Channel +
  • IsPrivateChannel +: Tgstation.Server.Host.Components.Chat.Channel
  • IsSha() : Tgstation.Server.Host.Components.Repository.IRepository diff --git a/functions_p.html b/functions_p.html index 67836a8194..e41e8f2f5d 100644 --- a/functions_p.html +++ b/functions_p.html @@ -129,10 +129,12 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');

    - p -

  • -

    If the command should only be available to Users who's User.Channel has Channel.IsAdmin set

    +

    If the command should only be available to Users who's User.Channel has Channel.IsAdminChannel set

    Definition at line 24 of file ICommand.cs.

    diff --git a/interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_i_session_controller.html b/interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_i_session_controller.html index ff27d4fbb1..1aa9ee9585 100644 --- a/interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_i_session_controller.html +++ b/interface_tgstation_1_1_server_1_1_host_1_1_components_1_1_watchdog_1_1_i_session_controller.html @@ -196,7 +196,7 @@ Properties

    Implemented in Tgstation.Server.Host.Components.Watchdog.SessionController, and Tgstation.Server.Host.Components.Watchdog.DeadSessionController.

    -

    Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.LaunchImplNoLock().

    +

    Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.LaunchImplNoLock().

    @@ -218,7 +218,7 @@ Properties

    Implemented in Tgstation.Server.Host.Components.Watchdog.SessionController, and Tgstation.Server.Host.Components.Watchdog.DeadSessionController.

    -

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

    +

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

    @@ -239,7 +239,7 @@ Properties

    Implemented in Tgstation.Server.Host.Components.Watchdog.SessionController, and Tgstation.Server.Host.Components.Watchdog.DeadSessionController.

    -

    Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.ResetRebootState().

    +

    Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.ResetRebootState().

    @@ -279,7 +279,7 @@ Properties

    Implemented in Tgstation.Server.Host.Components.Watchdog.SessionController, and Tgstation.Server.Host.Components.Watchdog.DeadSessionController.

    -

    Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleChatCommand(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleEvent().

    +

    Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleChatCommand(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.HandleEvent().

    @@ -488,7 +488,7 @@ Properties

    Definition at line 15 of file ISessionController.cs.

    -

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

    +

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

    diff --git a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_process_base.html b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_process_base.html index 4ec103806d..a0afa740bf 100644 --- a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_process_base.html +++ b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_process_base.html @@ -151,7 +151,7 @@ Properties

    Implemented in Tgstation.Server.Host.Components.Watchdog.SessionController, Tgstation.Server.Host.Core.Process, and Tgstation.Server.Host.Components.Watchdog.DeadSessionController.

    -

    Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.LaunchImplNoLock().

    +

    Referenced by Tgstation.Server.Host.Components.Watchdog.Watchdog.HandlerMonitorWakeup(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.LaunchImplNoLock().

    @@ -178,7 +178,7 @@ Properties

    Definition at line 14 of file IProcessBase.cs.

    -

    Referenced by Tgstation.Server.Host.Components.Byond.WindowsByondInstaller.InstallByond(), Tgstation.Server.Host.Components.Watchdog.Watchdog.MonitorLifetimes(), Tgstation.Server.Host.Components.Watchdog.WindowsNetworkPromptReaper.RegisterProcess(), and Tgstation.Server.Host.Components.Watchdog.SessionController.SessionController().

    +

    Referenced by Tgstation.Server.Host.Components.Byond.WindowsByondInstaller.InstallByond(), Tgstation.Server.Host.Components.Watchdog.Watchdog.MonitorLifetimes(), Tgstation.Server.Host.Components.Watchdog.WindowsNetworkPromptReaper.RegisterProcess(), and Tgstation.Server.Host.Components.Watchdog.SessionController.SessionController().

    diff --git a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_restart_handler.html b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_restart_handler.html index eb0768a8e8..02772797bf 100644 --- a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_restart_handler.html +++ b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_restart_handler.html @@ -108,7 +108,8 @@ Inheritance diagram for Tgstation.Server.Host.Core.IRestartHandler: Tgstation.Server.Host.Components.Chat.Chat -Tgstation.Server.Host.Components.Watchdog.Watchdog +Tgstation.Server.Host.Components.InstanceManager +Tgstation.Server.Host.Components.Watchdog.Watchdog @@ -157,7 +158,7 @@ Public Member Functions
    Returns
    A Task representing the running operation
    -

    Implemented in Tgstation.Server.Host.Components.Watchdog.Watchdog, and Tgstation.Server.Host.Components.Chat.Chat.

    +

    Implemented in Tgstation.Server.Host.Components.Watchdog.Watchdog, Tgstation.Server.Host.Components.Chat.Chat, and Tgstation.Server.Host.Components.InstanceManager.

    diff --git a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_restart_handler.png b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_restart_handler.png index 17cbc48c97c8461dea855c51949e2c1fec2b9a40..3542c1dbe7df84afb80b9f268179bfecd20c7f47 100644 GIT binary patch literal 1982 zcmb_cdsvcZ8-LL}{DvtJf}IbDKpJ9BMV7Q-SU)l&@44PGG{WgoMl>w#{gS1 z(76<*=!ytT3Qy$*3TRI33M4$lrVPt3bT77DyT1RvUDuxL`rYsSKF|BR@8>?frD#-$ zxrwa_008F513}>cpsxgVw6Q)Y!=Lfafe#z>p~&Esl@(BZHT!#Z#iQ$>whAtnYfw54 z1z(M`!ch?b_>pdn^hbIDfYD)O(B8;A$Q*eFyWnEA=H@s3s+C?xMp|sK`{wB&(QaoZ z24XspXSA$mgFjMiRkU_n@eaTY5BYIx>k)kX_aB0q%m1sDq{0ss2F#yr^OtI9-(3VW zwU?_diWkM}wXK}pw^4b8b>f>q-X*d{BGB4jXM2}_3T4!k*9ko)V+y5*I%+#`Xo(Uq zoNSOi5Ej0ED)r!q6$=(|{#21d(cX2RKwHEmP7o0V%9v9_Lqyma7B7NU^WqqfaLP9g znKM1sXF6$~9?T6=H@Ma)R2NDzmOE_g`wG)XF5*D3%KXT(k-~7!n_Ga=vAj9lW z2AT3E_x$MrWtDm1o@(x(^3r0bLp@5`KI@%MIbNU_sp!Wt6*$O)Zj_NH5G}{h#8VU5 zOY15}aXDYy2$ZYgbR5mG1j!hlup|4jf5`J%ZiK(Tf5Rt!te&t>{4z7<#CE2lkTnpO zGc=TM-_?73Ok?NV`3>N|XTELPA14V|im?LpGmfIfudoJsHmgyQ7IxVd&Di$Z zyXt{ex{|u#)}|KYf1n*S-d1ZB_)3op*np<1jqngpu%efBb>#x%M1D^cm*CS9INC#*Jfu0fUuw)#b+%$bGC!so`YnpJq0` zC$o}hf0Hwu?JM{_pW0WH;6PythI5}@4(Ky(twI|Ug8wQ8y^0IpZ|4TMIq4AKFZv*H ztMvvQZb4^^J*`Ylboi+vdyUQ41_nC3;`{2E@rqWV#@Im&llHD+iSh56m$|OFw)4r= zg|9(@*^4PVzttN4#KtI)2-}XdYrN!)+)wbO-y*=R5OIX1^T!Vg0^FKl<~&qQ3p;0y(U-)oi0g?n`pSF#%h?iD` z!JPB^8)>EUB5d9MHW)K`(^OnX>N5}IuF%DjP;t=&lK-Gr^KdSm>ZtjZ93P_E@nK3n zWsX0LyE|W63OhMep4xKTsb=%Enw6X@Qf5Z=kS;jdakxr<|MAQ_35XNWh#BJJr5P&! zg?f+W;7x2*P5DX9mxzIN1LZZ{ce{MLUB1amW)7x-zI`UuDskEtVoD`{$F)-7mL81; z%T?}f>UR4o&fzM23T&iIRKv$8sKL1~udN7)SSGm6bdI=J@6|Q=1Fz$vdtG#T{`#u7 z9931Lw~Bm0f3AN@EBQULSBtg`?TnOa57P=+g8Ugz{q{h)?wBTcAXX@+&jgrA}I{vhwu+;A~i>QkGPz^>Et*@ z*>^{cz5NpOF1s$|e9mG!PsF_WWqd?q;j3k((&eIR?}cOOvp%$y%YKTOJ&)RtpEDTKM}M`> z7bpoQV9k?wp+nyR!3Y0n7XEjHWPIh$C&@`=jFLuAOxe#o8;s@ZE?ta`a`##Tm-gDmf_d6fX`#Xo^;$$N!t|$%wfF#D& z$_)SnX`t@{5dv*W@0tX7D7ZLySo8UO(4G6b7ES3)2fe+`U@$~Ty;QBaw7t1>tU;%K#k!7N17jZlEG=G$vu**ROPhx_|EZc?VTpDn&eu1lL1Y7Mh zuW@hl!#WU_eru(fwGFHmb7y&@CC&b;Lm!qte7gENpObTcv+N>!?TvBVju@hS3YMGV zbCY5^98|tI%A|3rd`5;KZ%R(! z{*(LgDhdngoGoP|AZoVLiaG6ECPhFj<~1 z+l@9}?MC?lM+NpXYQZ|}oAmT{04X|ql!GuR0~i-4i{XIl$xIO5vPZ@Sp#Fg;5Lwg9 zdKA_)N*oY+mmDRmKok<0V;KqV?@AQpO~VEDO_>XPk4OWAA|3(TXd6Nz+Jz8bpw2Ld zYnz+(%ivTF$URc#LiD6X#otl>2i;%h3Pg3hzy>sMQAG=gkg6>;qx66qN~7D>O%&!% z{r-eTHIODFG27k_7)2s8vov0}93k0-w{tTCpZ7&Nu|sw;GU3ZS?#n5^0cYxGeM6le z!nj;fYUbgYU$}`YH8Tb#F&ZHhJNAU^NS;kIuQed5x~QWfhTR$yC5u^(^)<`skLZ!# z2NEQ^k05(O&Jj1UYFo0XgfpEV95^jG+j;f*$M^x}hyw~{mIJjLiLTcdhyruj1Z ze9-|>;{-42O(WUWHubTeb;A4$*VfxLXX#j7#VEzbS~~Uko&rD&Vz0yL8N?4)G~5?Bm~8@GZaNGt@vt5Gw$ggHXqC zK`p(ffD%dswKUJd+YOk5T3Wk=(g;!cS8D0XC`-tzBxwsN5=}J;X8|dBzz5pQNSZ@m SbtJ(d4`8gFtm-UqcmD#4734zz diff --git a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_server_control.html b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_server_control.html index 07e584e093..ed26ff3d1f 100644 --- a/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_server_control.html +++ b/interface_tgstation_1_1_server_1_1_host_1_1_core_1_1_i_server_control.html @@ -205,7 +205,7 @@ Properties

    Implemented in Tgstation.Server.Host.Server.

    -

    Referenced by Tgstation.Server.Host.Components.Chat.Chat.Chat(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.Watchdog().

    +

    Referenced by Tgstation.Server.Host.Components.Chat.Chat.Chat(), Tgstation.Server.Host.Components.InstanceManager.InstanceManager(), and Tgstation.Server.Host.Components.Watchdog.Watchdog.Watchdog().

    diff --git a/interface_tgstation_1_1_server_1_1_host_1_1_models_1_1_i_database_context-members.html b/interface_tgstation_1_1_server_1_1_host_1_1_models_1_1_i_database_context-members.html index c05b428979..e70fad968e 100644 --- a/interface_tgstation_1_1_server_1_1_host_1_1_models_1_1_i_database_context-members.html +++ b/interface_tgstation_1_1_server_1_1_host_1_1_models_1_1_i_database_context-members.html @@ -111,8 +111,9 @@ var searchBox = new SearchBox("searchBox", "search",false,'Search');
    - - + + +
    RepositorySettingsTgstation.Server.Host.Models.IDatabaseContext
    RevisionInformationsTgstation.Server.Host.Models.IDatabaseContext
    Save(CancellationToken cancellationToken)Tgstation.Server.Host.Models.IDatabaseContext
    UsersTgstation.Server.Host.Models.IDatabaseContext
    WatchdogReattachInformationsTgstation.Server.Host.Models.IDatabaseContext
    SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)Tgstation.Server.Host.Models.IDatabaseContext
    UsersTgstation.Server.Host.Models.IDatabaseContext
    WatchdogReattachInformationsTgstation.Server.Host.Models.IDatabaseContext