tgstation-server 6.1.2
The /tg/station 13 server suite
Loading...
Searching...
No Matches
SessionController.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.Linq;
5using System.Text;
6using System.Threading;
7using System.Threading.Tasks;
8
9using Microsoft.Extensions.Logging;
10
11using Newtonsoft.Json;
12
13using Serilog.Context;
14
28
30{
33 {
37 internal static bool LogTopicRequests { get; set; } = true;
38
41
44 {
45 get
46 {
47 if (!Lifetime.IsCompleted)
48 throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
50 }
51 }
52
55
58
61
63 public Version? DMApiVersion { get; private set; }
64
66 public bool TerminationWasRequested { get; private set; }
67
69 public Task<LaunchResult> LaunchResult { get; }
70
72 public Task<int?> Lifetime { get; }
73
75 public Task OnStartup => startupTcs.Task;
76
78 public Task OnReboot => rebootTcs.Task;
79
81 public Task RebootGate
82 {
83 get => rebootGate;
84 set
85 {
86 var tcs = new TaskCompletionSource<Task>();
87 async Task Wrap()
88 {
89 var toAwait = await tcs.Task;
90 await toAwait;
91 await value;
92 }
93
94 tcs.SetResult(Interlocked.Exchange(ref rebootGate, Wrap()));
95 }
96 }
97
99 public Task OnPrime => primeTcs.Task;
100
103
106
111
116
120 readonly Byond.TopicSender.ITopicClient byondTopicSender;
121
126
131
136
141
146
151
155 readonly TaskCompletionSource initialBridgeRequestTcs;
156
161
165 readonly CancellationTokenSource reattachTopicCts;
166
170 readonly object synchronizationLock;
171
175 readonly bool apiValidationSession;
176
180 volatile TaskCompletionSource startupTcs;
181
185 volatile TaskCompletionSource rebootTcs;
186
190 volatile TaskCompletionSource primeTcs;
191
195 volatile Task rebootGate;
196
201
206
211
216
221
241 ReattachInformation reattachInformation,
242 Api.Models.Instance metadata,
245 Byond.TopicSender.ITopicClient byondTopicSender,
247 IBridgeRegistrar bridgeRegistrar,
249 IAssemblyInformationProvider assemblyInformationProvider,
251 ILogger<SessionController> logger,
252 Func<ValueTask> postLifetimeCallback,
253 uint? startupTimeout,
254 bool reattached,
255 bool apiValidate)
256 : base(logger)
257 {
258 ReattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
259 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
260 this.process = process ?? throw new ArgumentNullException(nameof(process));
261 this.engineLock = engineLock ?? throw new ArgumentNullException(nameof(engineLock));
262 this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
263 this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext));
264 ArgumentNullException.ThrowIfNull(bridgeRegistrar);
265
266 this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
267 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
268
269 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
270
271 apiValidationSession = apiValidate;
272
273 disposed = false;
275 released = false;
276
277 startupTcs = new TaskCompletionSource();
278 rebootTcs = new TaskCompletionSource();
279 primeTcs = new TaskCompletionSource();
280
281 rebootGate = Task.CompletedTask;
282
283 // Run this asynchronously because we want to try to avoid any effects sending topics to the server while the initial bridge request is processing
284 // It MAY be the source of a DD crash. See this gist https://gist.github.com/Cyberboss/7776bbeff3a957d76affe0eae95c9f14
285 // Worth further investigation as to if that sequence of events is a reliable crash vector and opening a BYOND bug if it is
286 initialBridgeRequestTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
287 reattachTopicCts = new CancellationTokenSource();
288
290 synchronizationLock = new object();
291
293 {
294 bridgeRegistration = bridgeRegistrar.RegisterHandler(this);
295 this.chatTrackingContext.SetChannelSink(this);
296 }
297 else
298 logger.LogTrace(
299 "Not registering session with {reasonWhyDmApiIsBad} DMAPI version for interop!",
300 reattachInformation.Dmb.CompileJob.DMApiVersion == null
301 ? "no"
302 : $"incompatible ({reattachInformation.Dmb.CompileJob.DMApiVersion})");
303
304 async Task<int?> WrapLifetime()
305 {
306 var exitCode = await process.Lifetime;
307 await postLifetimeCallback();
308 if (postValidationShutdownTask != null)
310
311 return exitCode;
312 }
313
314 Lifetime = WrapLifetime();
315
317 assemblyInformationProvider,
319 startupTimeout,
320 reattached,
321 apiValidate);
322
323 logger.LogDebug(
324 "Created session controller. CommsKey: {accessIdentifier}, Port: {port}",
325 reattachInformation.AccessIdentifier,
326 reattachInformation.Port);
327 }
328
330 public async ValueTask DisposeAsync()
331 {
333 {
334 if (disposed)
335 return;
336 disposed = true;
337 }
338
339 Logger.LogTrace("Disposing...");
340
341 reattachTopicCts.Cancel();
342 var cancellationToken = CancellationToken.None; // DCT: None available
343 var semaphoreLockTask = TopicSendSemaphore.Lock(cancellationToken);
344
345 if (!released)
346 {
348 Logger,
349 process,
352 cancellationToken);
353 }
354
355 await process.DisposeAsync();
356 engineLock.Dispose();
357 bridgeRegistration?.Dispose();
358 var regularDmbDisposeTask = ReattachInformation.Dmb.DisposeAsync();
359 var initialDmb = ReattachInformation.InitialDmb;
360 if (initialDmb != null)
361 await initialDmb.DisposeAsync();
362
363 await regularDmbDisposeTask;
364
365 chatTrackingContext.Dispose();
366 reattachTopicCts.Dispose();
367
368 if (!released)
369 await Lifetime; // finish the async callback
370
371 (await semaphoreLockTask).Dispose();
373 }
374
376 public async ValueTask<BridgeResponse?> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
377 {
378 ArgumentNullException.ThrowIfNull(parameters);
379
380 using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id))
381 {
382 Logger.LogTrace("Handling bridge request...");
383
384 try
385 {
386 return await ProcessBridgeCommand(parameters, cancellationToken);
387 }
388 finally
389 {
390 initialBridgeRequestTcs.TrySetResult();
391 }
392 }
393 }
394
396 public ValueTask Release()
397 {
399
403 released = true;
404 return DisposeAsync();
405 }
406
408 public ValueTask<TopicResponse?> SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
409 => SendCommand(parameters, false, cancellationToken);
410
412 public async ValueTask<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
413 {
414 if (RebootState == newRebootState)
415 return true;
416
417 Logger.LogTrace("Changing reboot state to {newRebootState}", newRebootState);
418
419 ReattachInformation.RebootState = newRebootState;
420 var result = await SendCommand(
421 new TopicParameters(newRebootState),
422 cancellationToken);
423
424 return result != null && result.ErrorMessage == null;
425 }
426
428 public void ResetRebootState()
429 {
431 Logger.LogTrace("Resetting reboot state...");
432 ReattachInformation.RebootState = RebootState.Normal;
433 }
434
436 public void AdjustPriority(bool higher) => process.AdjustPriority(higher);
437
440
443
446 {
447 var oldDmb = ReattachInformation.Dmb;
448 ReattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
449 return oldDmb;
450 }
451
453 public async ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
454 {
455 var runtimeInformation = ReattachInformation.RuntimeInformation;
456 if (runtimeInformation != null)
457 runtimeInformation.InstanceName = newInstanceName;
458
459 await SendCommand(
461 cancellationToken);
462 }
463
465 public async ValueTask UpdateChannels(IEnumerable<ChannelRepresentation> newChannels, CancellationToken cancellationToken)
466 => await SendCommand(
467 new TopicParameters(
468 new ChatUpdate(newChannels)),
469 cancellationToken);
470
472 public ValueTask CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken);
473
483 async Task<LaunchResult> GetLaunchResult(
484 IAssemblyInformationProvider assemblyInformationProvider,
486 uint? startupTimeout,
487 bool reattached,
488 bool apiValidate)
489 {
490 var startTime = DateTimeOffset.UtcNow;
491 var useBridgeRequestForLaunchResult = !reattached && (apiValidate || DMApiAvailable);
492 var startupTask = useBridgeRequestForLaunchResult
493 ? initialBridgeRequestTcs.Task
495 var toAwait = Task.WhenAny(startupTask, process.Lifetime);
496
497 if (startupTimeout.HasValue)
498 toAwait = Task.WhenAny(
499 toAwait,
501 TimeSpan.FromSeconds(startupTimeout.Value),
502 CancellationToken.None)); // DCT: None available, task will clean up after delay
503
504 Logger.LogTrace(
505 "Waiting for LaunchResult based on {launchResultCompletionCause}{possibleTimeout}...",
506 useBridgeRequestForLaunchResult ? "initial bridge request" : "process startup",
507 startupTimeout.HasValue ? $" with a timeout of {startupTimeout.Value}s" : String.Empty);
508
509 await toAwait;
510
511 var result = new LaunchResult
512 {
513 ExitCode = process.Lifetime.IsCompleted ? await process.Lifetime : null,
514 StartupTime = startupTask.IsCompleted ? (DateTimeOffset.UtcNow - startTime) : null,
515 };
516
517 Logger.LogTrace("Launch result: {launchResult}", result);
518
519 if (!result.ExitCode.HasValue && reattached && !disposed)
520 {
521 var reattachResponse = await SendCommand(
522 new TopicParameters(
523 assemblyInformationProvider.Version,
525 true,
526 reattachTopicCts.Token);
527
528 if (reattachResponse != null)
529 {
530 if (reattachResponse?.CustomCommands != null)
531 chatTrackingContext.CustomCommands = reattachResponse.CustomCommands;
532 else if (reattachResponse != null)
533 Logger.Log(
534 CompileJob.DMApiVersion >= new Version(5, 2, 0)
535 ? LogLevel.Warning
536 : LogLevel.Debug,
537 "DMAPI Interop v{interopVersion} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
538 CompileJob.DMApiVersion!.Semver());
539 }
540 }
541
542 return result;
543 }
544
548 void CheckDisposed() => ObjectDisposedException.ThrowIf(disposed, this);
549
555 async Task PostValidationShutdown(Task<bool> proceedTask)
556 {
557 Logger.LogTrace("Entered post validation terminate task.");
558 if (!await proceedTask)
559 {
560 Logger.LogTrace("Not running post validation terminate task for repeated bridge request.");
561 return;
562 }
563
564 const int GracePeriodSeconds = 30;
565 Logger.LogDebug("Server will terminated in {gracePeriodSeconds}s if it does not exit...", GracePeriodSeconds);
566 var delayTask = asyncDelayer.Delay(TimeSpan.FromSeconds(GracePeriodSeconds), CancellationToken.None); // DCT: None available
567 await Task.WhenAny(process.Lifetime, delayTask);
568
569 if (!process.Lifetime.IsCompleted)
570 {
571 Logger.LogWarning("DMAPI took too long to shutdown server after validation request!");
573 apiValidationStatus = ApiValidationStatus.BadValidationRequest;
574 }
575 else
576 Logger.LogTrace("Server exited properly post validation.");
577 }
578
585#pragma warning disable CA1502 // TODO: Decomplexify
586 async ValueTask<BridgeResponse?> ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken)
587 {
588 var response = new BridgeResponse();
589 switch (parameters.CommandType)
590 {
591 case BridgeCommandType.ChatSend:
592 if (parameters.ChatMessage == null)
593 return BridgeError("Missing chatMessage field!");
594
595 if (parameters.ChatMessage.ChannelIds == null)
596 return BridgeError("Missing channelIds field in chatMessage!");
597
598 if (parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _)))
599 return BridgeError("Invalid channelIds in chatMessage!");
600
601 if (parameters.ChatMessage.Text == null)
602 return BridgeError("Missing message field in chatMessage!");
603
604 var anyFailed = false;
605 var parsedChannels = parameters.ChatMessage.ChannelIds.Select(
606 channelString =>
607 {
608 anyFailed |= !UInt64.TryParse(channelString, out var channelId);
609 return channelId;
610 });
611
612 if (anyFailed)
613 return BridgeError("Failed to parse channelIds as U64!");
614
616 parameters.ChatMessage,
617 parsedChannels);
618 break;
619 case BridgeCommandType.Prime:
620 Interlocked.Exchange(ref primeTcs, new TaskCompletionSource()).SetResult();
621 break;
622 case BridgeCommandType.Kill:
623 Logger.LogInformation("Bridge requested process termination!");
624 chatTrackingContext.Active = false;
627 break;
628 case BridgeCommandType.DeprecatedPortUpdate:
629 return BridgeError("Port switching is no longer supported!");
630 case BridgeCommandType.Startup:
631 apiValidationStatus = ApiValidationStatus.BadValidationRequest;
632
634 {
635 var proceedTcs = new TaskCompletionSource<bool>();
636 var firstValidationRequest = Interlocked.CompareExchange(ref postValidationShutdownTask, PostValidationShutdown(proceedTcs.Task), null) == null;
637 proceedTcs.SetResult(firstValidationRequest);
638
639 if (!firstValidationRequest)
640 return BridgeError("Startup bridge request was repeated!");
641 }
642
643 if (parameters.Version == null)
644 return BridgeError("Missing dmApiVersion field!");
645
646 DMApiVersion = parameters.Version;
647
648 // TODO: When OD figures out how to unite port and topic_port, set an upper version bound on OD for this check
650 || (EngineVersion.Engine == EngineType.OpenDream && DMApiVersion < new Version(5, 7)))
651 {
653 return BridgeError("Incompatible dmApiVersion!");
654 }
655
656 switch (parameters.MinimumSecurityLevel)
657 {
658 case DreamDaemonSecurity.Ultrasafe:
659 apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
660 break;
661 case DreamDaemonSecurity.Safe:
663 break;
664 case DreamDaemonSecurity.Trusted:
666 break;
667 case null:
668 return BridgeError("Missing minimumSecurityLevel field!");
669 default:
670 return BridgeError("Invalid minimumSecurityLevel!");
671 }
672
673 Logger.LogTrace("ApiValidationStatus set to {apiValidationStatus}", apiValidationStatus);
674
675 // we create new runtime info here because of potential .Dmb changes (i think. i forget...)
676 response.RuntimeInformation = new RuntimeInformation(
685
686 if (parameters.TopicPort.HasValue)
687 {
688 var newTopicPort = parameters.TopicPort.Value;
689 Logger.LogInformation("Server is requesting use of port {topicPort} for topic communications", newTopicPort);
690 ReattachInformation.TopicPort = newTopicPort;
691 }
692
693 // Load custom commands
694 chatTrackingContext.CustomCommands = parameters.CustomCommands ?? Array.Empty<CustomCommand>();
695 chatTrackingContext.Active = true;
696 Interlocked.Exchange(ref startupTcs, new TaskCompletionSource()).SetResult();
697 break;
698 case BridgeCommandType.Reboot:
699 Interlocked.Increment(ref rebootBridgeRequestsProcessing);
700 try
701 {
702 chatTrackingContext.Active = false;
703 Interlocked.Exchange(ref rebootTcs, new TaskCompletionSource()).SetResult();
704 await RebootGate.WaitAsync(cancellationToken);
705 }
706 finally
707 {
708 Interlocked.Decrement(ref rebootBridgeRequestsProcessing);
709 }
710
711 break;
712 case BridgeCommandType.Chunk:
713 return await ProcessChunk<BridgeParameters, BridgeResponse>(ProcessBridgeCommand, BridgeError, parameters.Chunk, cancellationToken);
714 case null:
715 return BridgeError("Missing commandType!");
716 default:
717 return BridgeError($"commandType {parameters.CommandType} not supported!");
718 }
719
720 return response;
721 }
722#pragma warning restore CA1502
723
730 {
731 Logger.LogWarning("Bridge request error: {message}", message);
732 return new BridgeResponse
733 {
734 ErrorMessage = message,
735 };
736 }
737
744 async ValueTask<CombinedTopicResponse?> SendTopicRequest(TopicParameters parameters, CancellationToken cancellationToken)
745 {
746 parameters.AccessIdentifier = ReattachInformation.AccessIdentifier;
747
748 var fullCommandString = GenerateQueryString(parameters, out var json);
749 if (LogTopicRequests)
750 Logger.LogTrace("Topic request: {json}", json);
751 var fullCommandByteCount = Encoding.UTF8.GetByteCount(fullCommandString);
752 var topicPriority = parameters.IsPriority;
753 if (fullCommandByteCount <= DMApiConstants.MaximumTopicRequestLength)
754 return await SendRawTopic(fullCommandString, topicPriority, cancellationToken);
755
756 var interopChunkingVersion = new Version(5, 6, 0);
757 if (ReattachInformation.Dmb.CompileJob.DMApiVersion < interopChunkingVersion)
758 {
759 Logger.LogWarning(
760 "Cannot send topic request as it is exceeds the single request limit of {limitBytes}B ({actualBytes}B) and requires chunking and the current compile job's interop version must be at least {chunkingVersionRequired}!",
762 fullCommandByteCount,
763 interopChunkingVersion);
764 return null;
765 }
766
767 var payloadId = NextPayloadId;
768
769 // AccessIdentifer is just noise in a chunked request
770 parameters.AccessIdentifier = null!;
771 GenerateQueryString(parameters, out json);
772
773 // yes, this straight up ignores unicode, precalculating it is useless when we don't
774 // even know if the UTF8 bytes of the url encoded chunk will fit the window until we do said encoding
775 var fullPayloadSize = (uint)json.Length;
776
777 List<string>? chunkQueryStrings = null;
778 for (var chunkCount = 2; chunkQueryStrings == null; ++chunkCount)
779 {
780 var standardChunkSize = fullPayloadSize / chunkCount;
781 var bigChunkSize = standardChunkSize + (fullPayloadSize % chunkCount);
782 if (bigChunkSize > DMApiConstants.MaximumTopicRequestLength)
783 continue;
784
785 chunkQueryStrings = new List<string>();
786 for (var i = 0U; i < chunkCount; ++i)
787 {
788 var startIndex = i * standardChunkSize;
789 var subStringLength = Math.Min(
790 fullPayloadSize - startIndex,
791 i == chunkCount - 1
792 ? bigChunkSize
793 : standardChunkSize);
794 var chunkPayload = json.Substring((int)startIndex, (int)subStringLength);
795
796 var chunk = new ChunkData
797 {
798 Payload = chunkPayload,
799 PayloadId = payloadId,
800 SequenceId = i,
801 TotalChunks = (uint)chunkCount,
802 };
803
804 var chunkParameters = new TopicParameters(chunk)
805 {
806 AccessIdentifier = ReattachInformation.AccessIdentifier,
807 };
808
809 var chunkCommandString = GenerateQueryString(chunkParameters, out _);
810 if (Encoding.UTF8.GetByteCount(chunkCommandString) > DMApiConstants.MaximumTopicRequestLength)
811 {
812 // too long when encoded, need more chunks
813 chunkQueryStrings = null;
814 break;
815 }
816
817 chunkQueryStrings.Add(chunkCommandString);
818 }
819 }
820
821 Logger.LogTrace("Chunking topic request ({totalChunks} total)...", chunkQueryStrings.Count);
822
823 CombinedTopicResponse? combinedResponse = null;
824 bool LogRequestIssue(bool possiblyFromCompletedRequest)
825 {
826 if (combinedResponse?.InteropResponse == null || combinedResponse.InteropResponse.ErrorMessage != null)
827 {
828 Logger.LogWarning(
829 "Topic request {chunkingStatus} failed!{potentialRequestError}",
830 possiblyFromCompletedRequest ? "final chunk" : "chunking",
831 combinedResponse?.InteropResponse?.ErrorMessage != null
832 ? $" Request error: {combinedResponse.InteropResponse.ErrorMessage}"
833 : String.Empty);
834 return true;
835 }
836
837 return false;
838 }
839
840 foreach (var chunkCommandString in chunkQueryStrings)
841 {
842 combinedResponse = await SendRawTopic(chunkCommandString, topicPriority, cancellationToken);
843 if (LogRequestIssue(chunkCommandString == chunkQueryStrings.Last()))
844 return null;
845 }
846
847 while ((combinedResponse?.InteropResponse?.MissingChunks?.Count ?? 0) > 0)
848 {
849 Logger.LogWarning("DD is still missing some chunks of topic request P{payloadId}! Sending missing chunks...", payloadId);
850 var missingChunks = combinedResponse!.InteropResponse!.MissingChunks!;
851 var lastIndex = missingChunks.Last();
852 foreach (var missingChunkIndex in missingChunks)
853 {
854 var chunkCommandString = chunkQueryStrings[(int)missingChunkIndex];
855 combinedResponse = await SendRawTopic(chunkCommandString, topicPriority, cancellationToken);
856 if (LogRequestIssue(missingChunkIndex == lastIndex))
857 return null;
858 }
859 }
860
861 return combinedResponse;
862 }
863
870 string GenerateQueryString(TopicParameters parameters, out string json)
871 {
872 json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings);
873 var commandString = String.Format(
874 CultureInfo.InvariantCulture,
875 "?{0}={1}",
877 byondTopicSender.SanitizeString(json));
878 return commandString;
879 }
880
888 async ValueTask<CombinedTopicResponse?> SendRawTopic(string queryString, bool priority, CancellationToken cancellationToken)
889 {
890 if (disposed)
891 {
892 Logger.LogWarning(
893 "Attempted to send a topic on a disposed SessionController");
894 return null;
895 }
896
897 var targetPort = ReattachInformation.TopicPort ?? ReattachInformation.Port;
898 Byond.TopicSender.TopicResponse? byondResponse;
899 using (await TopicSendSemaphore.Lock(cancellationToken))
900 byondResponse = await byondTopicSender.SendWithOptionalPriority(
902 Logger,
903 queryString,
904 targetPort,
905 priority,
906 cancellationToken);
907
908 if (byondResponse == null)
909 {
910 if (priority)
911 Logger.LogError(
912 "Unable to send priority topic \"{queryString}\"!",
913 queryString);
914
915 return null;
916 }
917
918 var topicReturn = byondResponse.StringData;
919
920 TopicResponse? interopResponse = null;
921 if (topicReturn != null)
922 try
923 {
924 interopResponse = JsonConvert.DeserializeObject<TopicResponse>(topicReturn, DMApiConstants.SerializerSettings);
925 }
926 catch (Exception ex)
927 {
928 Logger.LogWarning(ex, "Invalid interop response: {topicReturnString}", topicReturn);
929 }
930
931 return new CombinedTopicResponse(byondResponse, interopResponse);
932 }
933
941 async ValueTask<TopicResponse?> SendCommand(TopicParameters parameters, bool bypassLaunchResult, CancellationToken cancellationToken)
942 {
943 ArgumentNullException.ThrowIfNull(parameters);
944
945 if (Lifetime.IsCompleted || disposed)
946 {
947 Logger.LogWarning(
948 "Attempted to send a command to an inactive SessionController: {commandType}",
949 parameters.CommandType);
950 return null;
951 }
952
953 if (!DMApiAvailable)
954 {
955 Logger.LogTrace("Not sending topic request {commandType} to server without/with incompatible DMAPI!", parameters.CommandType);
956 return null;
957 }
958
959 var reboot = OnReboot;
960 if (!bypassLaunchResult)
961 {
962 var launchResult = await LaunchResult.WaitAsync(cancellationToken);
963 if (launchResult.ExitCode.HasValue)
964 {
965 Logger.LogDebug("Not sending topic request {commandType} to server that failed to launch!", parameters.CommandType);
966 return null;
967 }
968 }
969
970 // meh, this is kind of a hack, but it works
972 {
973 Logger.LogDebug("Not sending topic request {commandType} to server that is rebooting/starting.", parameters.CommandType);
974 return null;
975 }
976
977 using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
978 var combinedCancellationToken = cts.Token;
979 async ValueTask CancelIfLifetimeElapses()
980 {
981 try
982 {
983 var completed = await Task.WhenAny(Lifetime, reboot).WaitAsync(combinedCancellationToken);
984
985 Logger.LogDebug(
986 "Server {action}, cancelling pending command: {commandType}",
987 completed != reboot
988 ? "process ended"
989 : "rebooting",
990 parameters.CommandType);
991 cts.Cancel();
992 }
993 catch (OperationCanceledException)
994 {
995 // expected, not even worth tracing
996 }
997 catch (Exception ex)
998 {
999 Logger.LogError(ex, "Error in CancelIfLifetimeElapses!");
1000 }
1001 }
1002
1003 TopicResponse? fullResponse = null;
1004 var lifetimeWatchingTask = CancelIfLifetimeElapses();
1005 try
1006 {
1007 var combinedResponse = await SendTopicRequest(parameters, combinedCancellationToken);
1008
1009 void LogCombinedResponse()
1010 {
1011 if (LogTopicRequests && combinedResponse != null)
1012 Logger.LogTrace("Topic response: {topicString}", combinedResponse.ByondTopicResponse.StringData ?? "(NO STRING DATA)");
1013 }
1014
1015 LogCombinedResponse();
1016
1017 if (combinedResponse?.InteropResponse?.Chunk != null)
1018 {
1019 Logger.LogTrace("Topic response is chunked...");
1020
1021 ChunkData? nextChunk = combinedResponse.InteropResponse.Chunk;
1022 do
1023 {
1024 var nextRequest = await ProcessChunk<TopicResponse, ChunkedTopicParameters>(
1025 (completedResponse, _) =>
1026 {
1027 fullResponse = completedResponse;
1028 return ValueTask.FromResult<ChunkedTopicParameters?>(null);
1029 },
1030 error =>
1031 {
1032 Logger.LogWarning("Topic response chunking error: {message}", error);
1033 return null;
1034 },
1035 combinedResponse?.InteropResponse?.Chunk,
1036 combinedCancellationToken);
1037
1038 if (nextRequest != null)
1039 {
1040 nextRequest.PayloadId = nextChunk.PayloadId;
1041 combinedResponse = await SendTopicRequest(nextRequest, combinedCancellationToken);
1042 LogCombinedResponse();
1043 nextChunk = combinedResponse?.InteropResponse?.Chunk;
1044 }
1045 else
1046 nextChunk = null;
1047 }
1048 while (nextChunk != null);
1049 }
1050 else
1051 fullResponse = combinedResponse?.InteropResponse;
1052 }
1053 catch (OperationCanceledException ex)
1054 {
1055 Logger.LogDebug(
1056 ex,
1057 "Topic request {cancellationType}!",
1058 combinedCancellationToken.IsCancellationRequested
1059 ? cancellationToken.IsCancellationRequested
1060 ? "cancelled"
1061 : "aborted"
1062 : "timed out");
1063
1064 // throw only if the original token was the trigger
1065 cancellationToken.ThrowIfCancellationRequested();
1066 }
1067 finally
1068 {
1069 cts.Cancel();
1070 await lifetimeWatchingTask;
1071 }
1072
1073 if (fullResponse?.ErrorMessage != null)
1074 Logger.LogWarning(
1075 "Errored topic response for command {commandType}: {errorMessage}",
1076 parameters.CommandType,
1077 fullResponse.ErrorMessage);
1078
1079 return fullResponse;
1080 }
1081 }
1082}
Information about an engine installation.
EngineType? Engine
The EngineType.
Metadata about a server instance.
Definition: Instance.cs:9
virtual ? Version DMApiVersion
The DMAPI Version.
Definition: CompileJob.cs:41
ChatMessage? ChatMessage
The Interop.ChatMessage for BridgeCommandType.ChatSend requests.
ushort? TopicPort
The port that should be used to send world topics, if not the default.
DreamDaemonSecurity? MinimumSecurityLevel
The minimum required DreamDaemonSecurity level for BridgeCommandType.Startup requests.
ChunkData? Chunk
The ChunkData for BridgeCommandType.Chunk requests.
Version? Version
The DMAPI global::System.Version for BridgeCommandType.Startup requests.
Representation of the initial data passed as part of a BridgeCommandType.Startup request.
ushort ServerPort
The port the HTTP server is running on.
Version ServerVersion
The IAssemblyInformationProvider.Version.
DreamDaemonVisibility Visibility
The DreamDaemonSecurity level of the launch.
string InstanceName
The NamedEntity.Name of the owner at the time of launch.
bool ApiValidateOnly
If DD should just respond if it's API is working and then exit.
DreamDaemonSecurity SecurityLevel
The DreamDaemonSecurity level of the launch.
ICollection< string >? ChannelIds
The ICollection<T> of Chat.ChannelRepresentation.Ids to sent the MessageContent to....
Definition: ChatMessage.cs:13
Represents an update of ChannelRepresentations.
Definition: ChatUpdate.cs:13
A packet of a split serialized set of data.
Definition: ChunkData.cs:7
uint? PayloadId
The ID of the full request to differentiate different chunkings. Nullable to prevent default value om...
Definition: ChunkSetInfo.cs:9
Class that deserializes chunked interop payloads.
Definition: Chunker.cs:16
ILogger< Chunker > Logger
The ILogger for the Chunker.
Definition: Chunker.cs:20
uint NextPayloadId
Gets a payload ID for use in a new ChunkSetInfo.
Definition: Chunker.cs:26
Constants used for communication with the DMAPI.
static readonly JsonSerializerSettings SerializerSettings
JsonSerializerSettings for use when communicating with the DMAPI.
const uint MaximumTopicRequestLength
The maximum length in bytes of a Byond.TopicSender.ITopicClient payload.
static readonly Version InteropVersion
The DMAPI InteropVersion being used.
const string TopicData
Parameter json is encoded in for topic requests.
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance.
string? ErrorMessage
Any errors in the client's parameters.
static TopicParameters CreateInstanceRenamedTopicParameters(string newInstanceName)
Initializes a new instance of the TopicParameters class.
bool IsPriority
Whether or not the TopicParameters constitute a priority request.
Combines a Byond.TopicSender.TopicResponse with a TopicResponse.
TopicResponse? InteropResponse
The interop TopicResponse, if any.
Represents the result of trying to start a DD process.
Definition: LaunchResult.cs:10
Parameters necessary for duplicating a ISessionController session.
RuntimeInformation? RuntimeInformation
The Interop.Bridge.RuntimeInformation for the DMAPI.
IDmbProvider? InitialDmb
The IDmbProvider initially used to launch DreamDaemon. Should be a different IDmbProvider than Dmb....
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon.
async ValueTask InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
Called when the owning Instance is renamed. A ValueTask representing the running operation.
ValueTask< TopicResponse?> SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic(). A ValueTask<TResult> resulting in the TopicRes...
async ValueTask< BridgeResponse?> ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
readonly Byond.TopicSender.ITopicClient byondTopicSender
The Byond.TopicSender.ITopicClient for the SessionController.
SessionController(ReattachInformation reattachInformation, Api.Models.Instance metadata, IProcess process, IEngineExecutableLock engineLock, Byond.TopicSender.ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, ILogger< SessionController > logger, Func< ValueTask > postLifetimeCallback, uint? startupTimeout, bool reattached, bool apiValidate)
Initializes a new instance of the SessionController class.
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController.
readonly object synchronizationLock
lock object for port updates and disposed.
void AdjustPriority(bool higher)
Set's the owned global::System.Diagnostics.Process.PriorityClass to a non-normal value.
async ValueTask< TopicResponse?> SendCommand(TopicParameters parameters, bool bypassLaunchResult, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic().
volatile uint rebootBridgeRequestsProcessing
The number of currently active calls to ProcessBridgeRequest(BridgeParameters, CancellationToken) fro...
readonly Api.Models.Instance metadata
The Instance metadata.
Task OnReboot
A Task that completes when the server calls /world/TgsReboot().
bool DMApiAvailable
If the DMAPI may be used this session.
readonly TaskCompletionSource initialBridgeRequestTcs
The TaskCompletionSource that completes when DD makes it's first bridge request.
bool TerminationWasRequested
If the DreamDaemon instance sent a.
bool disposed
If the SessionController has been disposed.
void ResetRebootState()
Changes RebootState to RebootState.Normal without telling the DMAPI.
readonly IEngineExecutableLock engineLock
The IEngineExecutableLock for the SessionController.
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the SessionController.
async ValueTask< BridgeResponse?> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters . A ValueTask<TResult> resulting in the BridgeResponse for the requ...
ReattachInformation ReattachInformation
The up to date Session.ReattachInformation.
readonly CancellationTokenSource reattachTopicCts
A CancellationTokenSource used for the topic send operation made on reattaching.
volatile Task rebootGate
Backing field for RebootGate.
BridgeResponse BridgeError(string message)
Log and return a BridgeResponse for a given message .
bool released
If process should be kept alive instead.
readonly IChatManager chat
The IChatManager for the SessionController.
readonly IChatTrackingContext chatTrackingContext
The IChatTrackingContext for the SessionController.
Task< int?> Lifetime
The Task<TResult> resulting in the exit code of the process or null if the process was detached.
volatile TaskCompletionSource startupTcs
The TaskCompletionSource that completes when DD sends a valid startup bridge request.
async Task PostValidationShutdown(Task< bool > proceedTask)
Terminates the server after ten seconds if it does not exit.
readonly bool apiValidationSession
If this session is meant to validate the presence of the DMAPI.
FifoSemaphore TopicSendSemaphore
The FifoSemaphore used to prevent concurrent calls into /world/Topic().
async ValueTask UpdateChannels(IEnumerable< ChannelRepresentation > newChannels, CancellationToken cancellationToken)
Called when newChannels are set. A ValueTask representing the running operation.
string GenerateQueryString(TopicParameters parameters, out string json)
Generates a Byond.TopicSender.ITopicClient query string for a given set of parameters .
ValueTask CreateDump(string outputFile, CancellationToken cancellationToken)
Create a dump file of the process. A ValueTask representing the running operation.
void CheckDisposed()
Throws an ObjectDisposedException if DisposeAsync has been called.
readonly? IBridgeRegistration bridgeRegistration
The IBridgeRegistration for the SessionController.
async Task< LaunchResult > GetLaunchResult(IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, uint? startupTimeout, bool reattached, bool apiValidate)
The Task<TResult> for LaunchResult.
IAsyncDisposable ReplaceDmbProvider(IDmbProvider dmbProvider)
Replace the IDmbProvider in use with a given newProvider , disposing the old one. An IAsyncDisposable...
Task OnStartup
A Task that completes when the server calls /world/TgsNew().
bool ProcessingRebootBridgeRequest
If the ISessionController is currently processing a bridge request from TgsReboot().
ValueTask Release()
Releases the IProcess without terminating it. Also calls IDisposable.Dispose. A ValueTask representin...
volatile? Task postValidationShutdownTask
Task for shutting down the server if it is taking too long after validation.
Task OnPrime
A Task that completes when the server calls /world/TgsInitializationComplete().
async ValueTask< CombinedTopicResponse?> SendTopicRequest(TopicParameters parameters, CancellationToken cancellationToken)
Send a topic request for given parameters to DreamDaemon, chunking it if necessary.
volatile TaskCompletionSource rebootTcs
The TaskCompletionSource that completes when DD tells us about a reboot.
async ValueTask< CombinedTopicResponse?> SendRawTopic(string queryString, bool priority, CancellationToken cancellationToken)
Send a given queryString to DreamDaemon's /world/Topic.
volatile TaskCompletionSource primeTcs
The TaskCompletionSource that completes when DD tells us it's primed.
async ValueTask< bool > SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
Attempts to change the current RebootState to newRebootState . A ValueTask<TResult> resulting in true...
Task RebootGate
A Task that must complete before a TgsReboot() bridge request can complete.
readonly IProcess process
The IProcess for the SessionController.
RebootState RebootState
The current DreamDaemon reboot state.
ushort Port
The port the game server was last listening on.
A first-in first-out async semaphore.
async ValueTask< SemaphoreSlimContext > Lock(CancellationToken cancellationToken)
Locks the FifoSemaphore.
Helpers for manipulating the Serilog.Context.LogContext.
const string InstanceIdContextProperty
The Serilog.Context.LogContext property name for Models.Instance Api.Models.EntityId....
Notifyee of when ChannelRepresentations in a IChatTrackingContext are updated.
Definition: IChannelSink.cs:11
For managing connected chat services.
Definition: IChatManager.cs:15
void QueueMessage(MessageContent message, IEnumerable< ulong > channelIds)
Queue a chat message to a given set of channelIds .
Represents a tracking of dynamic chat json files.
void SetChannelSink(IChannelSink channelSink)
Sets the channelSink for the IChatTrackingContext.
Provides absolute paths to the latest compiled .dmbs.
Definition: IDmbProvider.cs:11
void KeepAlive()
Disposing the IDmbProvider won't cause a cleanup of the working directory.
EngineVersion EngineVersion
The Api.Models.EngineVersion used to build the .dmb.
Definition: IDmbProvider.cs:30
Models.CompileJob CompileJob
The CompileJob of the .dmb.
Definition: IDmbProvider.cs:25
Represents usage of the two primary BYOND server executables.
void DoNotDeleteThisSession()
Call if, during a detach, this version should not be deleted.
ValueTask StopServerProcess(ILogger logger, IProcess process, string accessIdentifier, ushort port, CancellationToken cancellationToken)
Kills a given engine server process .
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
Handles communication with a DreamDaemon IProcess.
ValueTask CreateDump(string outputFile, CancellationToken cancellationToken)
Create a dump file of the process.
void SuspendProcess()
Suspends the process.
void ResumeProcess()
Resumes the process.
void AdjustPriority(bool higher)
Set's the owned global::System.Diagnostics.Process.PriorityClass to a non-normal value.
Task< int?> Lifetime
The Task<TResult> resulting in the exit code of the process or null if the process was detached.
Definition: IProcessBase.cs:14
Abstraction over a global::System.Diagnostics.Process.
Definition: IProcess.cs:11
Task Startup
The Task representing the time until the IProcess becomes "idle".
Definition: IProcess.cs:20
void Terminate()
Asycnhronously terminates the process.
Task Delay(TimeSpan timeSpan, CancellationToken cancellationToken)
Create a Task that completes after a given timeSpan .
DreamDaemonSecurity
DreamDaemon's security level.
EngineType
The type of engine the codebase is using.
Definition: EngineType.cs:7
BridgeCommandType
Represents the BridgeParameters.CommandType.
RebootState
Represents the action to take when /world/Reboot() is called.
Definition: RebootState.cs:7
ApiValidationStatus
Status of DMAPI validation.