tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
Instance.cs
Go to the documentation of this file.
1using System;
3using System.Linq;
6
9
10using NCrontab;
11
12using Serilog.Context;
13
26
28{
30#pragma warning disable CA1506 // TODO: Decomplexify
31 sealed class Instance : IInstance
32 {
36 public const string DifferentCoreExceptionMessage = "Job started on different instance core!";
37
40
43
45 public IWatchdog Watchdog { get; }
46
48 public IChatManager Chat { get; }
49
52
54 public IDreamMaker DreamMaker { get; }
55
60
65
70
75
80
85
90
95
100
105
110
115
120
125
142 public Instance(
143 Api.Models.Instance metadata,
144 IRepositoryManager repositoryManager,
145 IEngineManager engineManager,
147 IWatchdog watchdog,
148 IChatManager chat,
149 StaticFiles.IConfiguration
150 configuration,
157 {
159 RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
160 EngineManager = engineManager ?? throw new ArgumentNullException(nameof(engineManager));
162 Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
163 Chat = chat ?? throw new ArgumentNullException(nameof(chat));
164 Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
171
172 timerLock = new object();
173 }
174
177 {
179 {
180 var chatDispose = Chat.DisposeAsync();
181 var watchdogDispose = Watchdog.DisposeAsync();
182 autoUpdateCts?.Dispose();
183 autoStartCts?.Dispose();
184 autoStopCts?.Dispose();
185 Configuration.Dispose();
186 dmbFactory.Dispose();
191 }
192 }
193
195 public ValueTask InstanceRenamed(string newName, CancellationToken cancellationToken)
196 {
197 ArgumentNullException.ThrowIfNull(newName);
198 if (String.IsNullOrWhiteSpace(newName))
199 throw new ArgumentException("newName cannot be whitespace!", nameof(newName));
200
203 }
204
206 public async Task StartAsync(CancellationToken cancellationToken)
207 {
209 {
210 await Task.WhenAll(
211 ScheduleAutoUpdate(metadata.Require(x => x.AutoUpdateInterval), metadata.AutoUpdateCron).AsTask(),
212 ScheduleServerStart(null).AsTask(),
213 ScheduleServerStop(null).AsTask(),
216 Chat.StartAsync(cancellationToken),
217 dmbFactory.StartAsync(cancellationToken));
218
219 // dependent on so many things, its just safer this way
220 await Watchdog.StartAsync(cancellationToken);
221
223 }
224 }
225
227 public async Task StopAsync(CancellationToken cancellationToken)
228 {
230 {
231 logger.LogDebug("Stopping instance...");
232 await ScheduleAutoUpdate(0, null);
234 await Task.WhenAll(
237 Chat.StopAsync(cancellationToken),
238 dmbFactory.StopAsync(cancellationToken));
239 }
240 }
241
244 {
245 if (newInterval > 0 && !String.IsNullOrWhiteSpace(newCron))
246 throw new ArgumentException("Only one of newInterval and newCron may be set!");
247
248 Task toWait;
250 if (autoUpdateTask != null)
251 {
252 logger.LogTrace("Cancelling auto-update task");
253 autoUpdateCts!.Cancel();
254 autoUpdateCts.Dispose();
256 autoUpdateTask = null;
257 autoUpdateCts = null;
258 }
259 else
260 toWait = Task.CompletedTask;
261
263 if (newInterval == 0 && String.IsNullOrWhiteSpace(newCron))
264 {
265 logger.LogTrace("Auto-update disabled 0. Not starting task.");
266 return;
267 }
268
270 {
271 // race condition, just quit
272 if (autoUpdateTask != null)
273 {
274 logger.LogWarning("Aborting auto-update scheduling change due to race condition!");
275 return;
276 }
277
280 }
281 }
282
285 {
286 Task toWait;
288 if (autoStartTask != null)
289 {
290 logger.LogTrace("Cancelling auto-start task");
291 autoStartCts!.Cancel();
292 autoStartCts.Dispose();
294 autoStartTask = null;
295 autoStartCts = null;
296 }
297 else
298 toWait = Task.CompletedTask;
299
301 if (String.IsNullOrWhiteSpace(newCron))
302 {
303 logger.LogTrace("Auto-start disabled. Not starting task.");
304 return;
305 }
306
308 {
309 // race condition, just quit
310 if (autoStartTask != null)
311 {
312 logger.LogWarning("Aborting auto-start scheduling change due to race condition!");
313 return;
314 }
315
317 autoStartTask = TimerLoop(Watchdog.Launch, "auto-start", 0, newCron, autoStartCts.Token);
318 }
319 }
320
323 {
324 Task toWait;
326 if (autoStopTask != null)
327 {
328 logger.LogTrace("Cancelling auto-stop task");
329 autoStopCts!.Cancel();
330 autoStopCts.Dispose();
332 autoStopTask = null;
333 autoStopCts = null;
334 }
335 else
336 toWait = Task.CompletedTask;
337
339 if (String.IsNullOrWhiteSpace(newCron))
340 {
341 logger.LogTrace("Auto-stop disabled. Not stoping task.");
342 return;
343 }
344
346 {
347 // race condition, just quit
348 if (autoStopTask != null)
349 {
350 logger.LogWarning("Aborting auto-stop scheduling change due to race condition!");
351 return;
352 }
353
357 "auto-stop",
358 0,
359 newCron,
360 autoStopCts.Token);
361 }
362 }
363
366
376#pragma warning disable CA1502 // Cyclomatic complexity
379 IDatabaseContextFactory databaseContextFactory,
380 Job job,
382 CancellationToken cancellationToken)
383 => databaseContextFactory.UseContext(
384 async databaseContext =>
385 {
386 if (core != this)
388
389 // assume 5 steps with synchronize
390 var repositorySettingsTask = databaseContext
391 .RepositorySettings
392 .AsQueryable()
393 .Where(x => x.InstanceId == metadata.Id)
394 .FirstAsync(cancellationToken);
395
396 const int ProgressSections = 7;
398 {
400 }
401
403 if (repo == null)
404 {
405 logger.LogTrace("Aborting repo update, no repository!");
406 return;
407 }
408
409 var startSha = repo.Head;
410 if (!repo.Tracking)
411 {
412 logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
413 return;
414 }
415
417
418 // the main point of auto update is to pull the remote
419 await repo.FetchOrigin(
420 NextProgressReporter("Fetch Origin"),
421 repositorySettings.AccessUser,
422 repositorySettings.AccessToken,
423 true,
425
426 var hasDbChanges = false;
428 Models.Instance? attachedInstance = null;
430 {
431 if (currentRevInfo == null)
432 {
433 logger.LogTrace("Loading revision info for commit {sha}...", startSha[..7]);
434 currentRevInfo = await databaseContext
436 .AsQueryable()
437 .Where(x => x.CommitSha == startSha && x.InstanceId == metadata.Id)
438 .Include(x => x.ActiveTestMerges!)
439 .ThenInclude(x => x.TestMerge)
440 .FirstOrDefaultAsync(cancellationToken);
441 }
442
443 if (currentRevInfo == default)
444 {
445 logger.LogInformation(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
446 onOrigin = true;
447 }
448 else if (currentRevInfo.CommitSha == currentHead)
449 {
450 logger.LogTrace("Not updating rev-info, already in DB.");
451 return;
452 }
453
454 if (attachedInstance == null)
455 {
457 {
458 Id = metadata.Id,
459 };
460 databaseContext.Instances.Attach(attachedInstance);
461 }
462
465 {
466 CommitSha = currentHead,
467 Timestamp = await repo.TimestampCommit(currentHead, cancellationToken),
468 OriginCommitSha = onOrigin
470 : await repo.GetOriginSha(cancellationToken),
472 };
473
474 if (!onOrigin)
475 {
476 var testMerges = updatedTestMerges ?? oldRevInfo!.ActiveTestMerges!.Select(x => x.TestMerge);
479 .ToList();
480
482 }
483
484 databaseContext.RevisionInformations.Add(currentRevInfo);
485 hasDbChanges = true;
486 }
487
488 // build current commit data if it's missing
489 await UpdateRevInfo(repo.Head, false, null);
490
491 var preserveTestMerges = repositorySettings.AutoUpdatesKeepTestMerges!.Value;
493 metadata,
494 repo.RemoteGitProvider!.Value);
495
497 repo,
501
502 var result = await repo.MergeOrigin(
503 NextProgressReporter("Merge Origin"),
504 repositorySettings.CommitterName!,
505 repositorySettings.CommitterEmail!,
506 true,
508
509 // take appropriate auto update actions
510 var shouldSyncTracked = false;
511 if (result.HasValue)
512 {
513 if (updatedTestMerges.Count == 0)
514 {
515 logger.LogTrace("All test merges have been merged on remote");
516 preserveTestMerges = false;
517 }
518 else
519 {
521 currentRevInfo == default
522 || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
524
525 var currentHead = repo.Head;
526 if (currentHead != startSha)
527 {
530 }
531 }
532 }
533 else if (preserveTestMerges)
534 throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict);
535
537 {
538 const string StageName = "Resetting to origin...";
539 logger.LogTrace(StageName);
540 await repo.ResetToOrigin(
541 NextProgressReporter(StageName),
542 repositorySettings.AccessUser,
543 repositorySettings.AccessToken,
544 repositorySettings.UpdateSubmodules!.Value,
545 true,
547
548 var currentHead = repo.Head;
549
551 .AsQueryable()
552 .Where(x => x.CommitSha == currentHead && x.InstanceId == metadata.Id)
553 .FirstOrDefaultAsync(cancellationToken);
554
555 if (currentHead != startSha && currentRevInfo == default)
556 await UpdateRevInfo(currentHead, true, null);
557
558 shouldSyncTracked = true;
559 }
560
561 // synch if necessary
562 if (repositorySettings.AutoUpdatesSynchronize!.Value && startSha != repo.Head && (shouldSyncTracked || repositorySettings.PushTestMergeCommits!.Value))
563 {
564 var pushedOrigin = await repo.Synchronize(
565 NextProgressReporter("Synchronize"),
566 repositorySettings.AccessUser,
567 repositorySettings.AccessToken,
568 repositorySettings.CommitterName!,
569 repositorySettings.CommitterEmail!,
571 true,
573 var currentHead = repo.Head;
574 if (currentHead != currentRevInfo!.CommitSha)
576 }
577
578 if (hasDbChanges)
579 try
580 {
581 await databaseContext.Save(cancellationToken);
582 }
583 catch
584 {
585 // DCT: Cancellation token is for job, operation must run regardless
586 await repo.ResetToSha(startSha, progressReporter, CancellationToken.None);
587 throw;
588 }
589 });
590#pragma warning restore CA1502 // Cyclomatic complexity
591
602 {
603 logger.LogDebug("Entering auto-update loop");
604 while (true)
605 try
606 {
608 if (!String.IsNullOrWhiteSpace(cron))
609 {
610 logger.LogTrace("Using cron schedule: {cron}", cron);
612 cron,
613 new CrontabSchedule.ParseOptions
614 {
615 IncludingSeconds = true,
616 });
617 var now = DateTime.UtcNow;
618 var nextOccurrence = schedule.GetNextOccurrence(now);
620 }
621 else
622 {
623 logger.LogTrace("Using interval: {interval}m", minutes);
624
625 delay = TimeSpan.FromMinutes(minutes);
626 }
627
628 logger.LogInformation("Next {desc} will occur at {time}", description, DateTimeOffset.UtcNow + delay);
629
631
633 }
635 {
636 logger.LogDebug("Cancelled {desc} loop!", description);
637 break;
638 }
639 catch (Exception e)
640 {
641 logger.LogError(e, "Error in {desc} loop!", description);
642 continue;
643 }
644
645 logger.LogTrace("Leaving {desc} loop...", description);
646 }
647
654 {
655 logger.LogInformation("Beginning auto update...");
656 await eventConsumer.HandleEvent(EventType.InstanceAutoUpdateStart, Enumerable.Empty<string>(), true, cancellationToken);
657
658 var repositoryUpdateJob = Job.Create(Api.Models.JobCode.RepositoryAutoUpdate, null, metadata, RepositoryRights.CancelPendingChanges);
663
665 if (repoUpdateJobResult == false)
666 {
667 logger.LogWarning("Aborting auto-update due to repository update error!");
668 return;
669 }
670
673 {
674 if (repo == null)
675 throw new JobException(Api.Models.ErrorCode.RepoMissing);
676
677 var deploySha = repo.Head;
678 if (deploySha == null)
679 {
680 logger.LogTrace("Aborting auto update, repository error!");
681 return;
682 }
683
685 {
686 logger.LogTrace("Aborting auto update, same revision as latest CompileJob");
687 return;
688 }
689
690 // finally set up the job
691 compileProcessJob = Job.Create(Api.Models.JobCode.AutomaticDeployment, null, metadata, DreamMakerRights.CancelCompile);
694 (core, databaseContextFactory, job, progressReporter, jobCancellationToken) =>
695 {
696 if (core != this)
699 job,
700 databaseContextFactory,
703 },
705 }
706
708 }
709 }
710}
Metadata about a server instance.
Definition Instance.cs:9
async ValueTask DeploymentProcess(Models.Job job, IDatabaseContextFactory databaseContextFactory, JobProgressReporter progressReporter, CancellationToken cancellationToken)
Create and a compile job and insert it into the database. Meant to be called by a IJobManager....
Task StopAsync(CancellationToken cancellationToken)
async Task StartAsync(CancellationToken cancellationToken)
ValueTask< CompileJob?> LatestCompileJob()
Gets the latest CompileJob.A ValueTask<TResult> resulting in the latest CompileJob or null if none ar...
async Task StartAsync(CancellationToken cancellationToken)
Definition Instance.cs:206
Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IEngineManager engineManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChatManager chat, StaticFiles.IConfiguration configuration, IDmbFactory dmbFactory, IJobManager jobManager, IEventConsumer eventConsumer, IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory, IAsyncDelayer asyncDelayer, ILogger< Instance > logger)
Initializes a new instance of the Instance class.
Definition Instance.cs:142
readonly IEventConsumer eventConsumer
The IEventConsumer for the Instance.
Definition Instance.cs:69
Task? autoStartTask
The auto-start Task.
Definition Instance.cs:109
async ValueTask ScheduleServerStart(string? newCron)
Change the server auto-start timing for the IInstanceCore.A ValueTask representing the running operat...
Definition Instance.cs:284
IChatManager Chat
The IChatManager for the IInstanceCore.
Definition Instance.cs:48
StaticFiles.IConfiguration Configuration
The IConfiguration for the IInstanceCore.
Definition Instance.cs:51
readonly IDmbFactory dmbFactory
The IDmbFactory for the Instance.
Definition Instance.cs:59
CancellationTokenSource? autoUpdateCts
CancellationTokenSource for autoUpdateTask.
Definition Instance.cs:104
async ValueTask ScheduleAutoUpdate(uint newInterval, string? newCron)
Change the auto-update timing for the IInstanceCore.A ValueTask representing the running operation.
Definition Instance.cs:243
readonly IJobManager jobManager
The IJobManager for the Instance.
Definition Instance.cs:64
CancellationTokenSource? autoStartCts
CancellationTokenSource for autoStartTask.
Definition Instance.cs:114
CancellationTokenSource? autoStopCts
CancellationTokenSource for autoStopTask.
Definition Instance.cs:124
IWatchdog Watchdog
The IWatchdog for the IInstanceCore.
Definition Instance.cs:45
async ValueTask AutoUpdateAction(CancellationToken cancellationToken)
Pulls the repository and compiles.
Definition Instance.cs:653
async ValueTask ScheduleServerStop(string? newCron)
Change the server auto-stop timing for the IInstanceCore.A ValueTask representing the running operati...
Definition Instance.cs:322
readonly IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory
The IRemoteDeploymentManagerFactory for the Instance.
Definition Instance.cs:74
Task? autoUpdateTask
The auto-update Task.
Definition Instance.cs:99
readonly ILogger< Instance > logger
The ILogger for the Instance.
Definition Instance.cs:84
const string DifferentCoreExceptionMessage
Message for the InvalidOperationException if ever a job starts on a different IInstanceCore than the ...
Definition Instance.cs:36
readonly Api.Models.Instance metadata
The Api.Models.Instance for the Instance.
Definition Instance.cs:89
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the Instance.
Definition Instance.cs:79
async Task TimerLoop(Func< CancellationToken, ValueTask > timerAction, string description, uint minutes, string? cron, CancellationToken cancellationToken)
Runs a timerAction every set of given minutes or on a given cron schedule.
Definition Instance.cs:601
ValueTask RepositoryAutoUpdateJob(IInstanceCore? core, IDatabaseContextFactory databaseContextFactory, Job job, JobProgressReporter progressReporter, CancellationToken cancellationToken)
The JobEntrypoint for updating the repository.
readonly object timerLock
lock object for autoUpdateCts and autoUpdateTask.
Definition Instance.cs:94
ValueTask InstanceRenamed(string newName, CancellationToken cancellationToken)
Called when the owning Instance is renamed.A ValueTask representing the running operation.
Definition Instance.cs:195
async Task StopAsync(CancellationToken cancellationToken)
Definition Instance.cs:227
Task? autoStopTask
The auto-stop Task.
Definition Instance.cs:119
Repository(LibGit2Sharp.IRepository libGitRepo, ILibGit2Commands commands, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, IPostWriteHandler postWriteHandler, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILibGit2RepositoryFactory submoduleFactory, ILogger< Repository > logger, GeneralConfiguration generalConfiguration, Action disposeAction)
Initializes a new instance of the Repository class.
async ValueTask< IRepository?> LoadRepository(CancellationToken cancellationToken)
Attempt to load the IRepository from the default location.A ValueTask<TResult> resulting in the loade...
Operation exceptions thrown from the context of a Models.Job.
JobProgressReporter CreateSection(string? newStageName, double percentage)
Create a subsection of the JobProgressReporter with its optional own stage name.
Represents an Api.Models.Instance in the database.
Definition Instance.cs:11
ICollection< RevisionInformation > RevisionInformations
The RevisionInformations in the Instance.
Definition Instance.cs:50
static Job Create(JobCode code, User? startedBy, Api.Models.Instance instance)
Creates a new job for registering in the Jobs.IJobService.
Many to many relationship for Models.RevisionInformation and Models.TestMerge.
Instance? Instance
The Models.Instance the RevisionInformation belongs to.
Helpers for manipulating the Serilog.Context.LogContext.
const string InstanceIdContextProperty
The Serilog.Context.LogContext property name for Models.Instance Api.Models.EntityId....
For managing connected chat services.
ValueTask CleanUnusedCompileJobs(CancellationToken cancellationToken)
Deletes all compile jobs that are inactive in the Game folder.
ValueTask< CompileJob?> LatestCompileJob()
Gets the latest CompileJob.
IRemoteDeploymentManager CreateRemoteDeploymentManager(Api.Models.Instance metadata, RemoteGitProvider remoteGitProvider)
Creates a IRemoteDeploymentManager for a given remoteGitProvider .
ValueTask< IReadOnlyCollection< TestMerge > > RemoveMergedTestMerges(IRepository repository, RepositorySettings repositorySettings, RevisionInformation revisionInformation, CancellationToken cancellationToken)
Get the updated list of TestMerges for an origin merge.
Consumes EventTypes and takes the appropriate actions.
ValueTask HandleEvent(EventType eventType, IEnumerable< string?> parameters, bool deploymentPipeline, CancellationToken cancellationToken)
Handle a given eventType .
For interacting with the instance services.
Component version of IInstanceCore.
Definition IInstance.cs:9
ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
Called when the owning Instance is renamed.
Runs and monitors the twin server controllers.
Definition IWatchdog.cs:16
ValueTask Launch(CancellationToken cancellationToken)
Start the IWatchdog.
ValueTask Terminate(bool graceful, CancellationToken cancellationToken)
Stops the watchdog.
Factory for scoping usage of IDatabaseContexts. Meant for use by Components.
Manages the runtime of Jobs.
ValueTask< bool?> WaitForJobCompletion(Job job, User? canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken)
Wait for a given job to complete.
ValueTask RegisterOperation(Job job, JobEntrypoint operation, CancellationToken cancellationToken)
Registers a given Job and begins running it.
ValueTask Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .
@ List
User may list files if the Models.Instance allows it.
DreamMakerRights
Rights for deployment.
RepositoryRights
Rights for the git repository.
EventType
Types of events. Mirror in tgs.dm. Prefer last listed name for script.
Definition EventType.cs:7