tgstation-server 5.12.7
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.Net;
6using System.Text;
7using System.Threading;
8using System.Threading.Tasks;
9
10using Microsoft.Extensions.Logging;
11
12using Newtonsoft.Json;
13
14using Serilog.Context;
15
27
29{
32 {
36 internal static bool LogTopicRequests { get; set; } = true;
37
40
43 {
44 get
45 {
46 if (!Lifetime.IsCompleted)
47 throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
49 }
50 }
51
54
57
59 public Version DMApiVersion { get; private set; }
60
62 public bool ClosePortOnReboot { get; set; }
63
65 public bool TerminationWasRequested { get; private set; }
66
68 public Task<LaunchResult> LaunchResult { get; }
69
71 public Task<int> Lifetime { get; }
72
74 public Task OnStartup => startupTcs.Task;
75
77 public Task OnReboot => rebootTcs.Task;
78
80 public Task RebootGate
81 {
82 get => rebootGate;
83 set
84 {
85 var tcs = new TaskCompletionSource();
86 Task toAwait = null;
87 async Task Wrap()
88 {
89 await tcs.Task;
90 await toAwait;
91 await value;
92 }
93
94 toAwait = Interlocked.Exchange(ref rebootGate, Wrap());
95 tcs.SetResult();
96 }
97 }
98
100 public Task OnPrime => primeTcs.Task;
101
104
107
112
116 readonly TaskCompletionSource initialBridgeRequestTcs;
117
122
126 readonly CancellationTokenSource reattachTopicCts;
127
131 readonly global::Byond.TopicSender.ITopicClient byondTopicSender;
132
137
142
147
152
157
162
166 readonly object synchronizationLock;
167
171 TaskCompletionSource<bool> portAssignmentTcs;
172
176 volatile TaskCompletionSource startupTcs;
177
181 volatile TaskCompletionSource rebootTcs;
182
186 volatile TaskCompletionSource primeTcs;
187
191 volatile Task rebootGate;
192
197
201 ushort? nextPort;
202
207
212
217
222
242 ReattachInformation reattachInformation,
243 Api.Models.Instance metadata,
246 global::Byond.TopicSender.ITopicClient byondTopicSender,
248 IBridgeRegistrar bridgeRegistrar,
250 IAssemblyInformationProvider assemblyInformationProvider,
252 ILogger<SessionController> logger,
253 Func<Task> postLifetimeCallback,
254 uint? startupTimeout,
255 bool reattached,
256 bool apiValidate)
257 : base(logger)
258 {
259 ReattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation));
260 this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata));
261 this.process = process ?? throw new ArgumentNullException(nameof(process));
262 this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
263 this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
264 this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext));
265 ArgumentNullException.ThrowIfNull(bridgeRegistrar);
266
267 this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
268 ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
269
270 this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
271
272 portClosedForReboot = false;
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 synchronizationLock = new object();
289
290 if (apiValidate || DMApiAvailable)
291 {
292 bridgeRegistration = bridgeRegistrar.RegisterHandler(this);
293 this.chatTrackingContext.SetChannelSink(this);
294 }
295 else
296 logger.LogTrace(
297 "Not registering session with {reasonWhyDmApiIsBad} DMAPI version for interop!",
298 reattachInformation.Dmb.CompileJob.DMApiVersion == null
299 ? "no"
300 : $"incompatible ({reattachInformation.Dmb.CompileJob.DMApiVersion})");
301
302 async Task<int> WrapLifetime()
303 {
304 var exitCode = await process.Lifetime;
305 await postLifetimeCallback();
306 return exitCode;
307 }
308
309 Lifetime = WrapLifetime();
310
312 assemblyInformationProvider,
314 startupTimeout,
315 reattached,
316 apiValidate);
317
318 logger.LogDebug(
319 "Created session controller. CommsKey: {accessIdentifier}, Port: {port}",
320 reattachInformation.AccessIdentifier,
321 reattachInformation.Port);
322 }
323
325 public async ValueTask DisposeAsync()
326 {
328 {
329 if (disposed)
330 return;
331 disposed = true;
332 }
333
334 Logger.LogTrace("Disposing...");
335 if (!released)
336 {
338 await process.Lifetime;
339 }
340
341 await process.DisposeAsync();
342 byondLock.Dispose();
343 bridgeRegistration?.Dispose();
344 ReattachInformation.Dmb.Dispose();
346 chatTrackingContext.Dispose();
347 reattachTopicCts.Dispose();
348
349 if (!released)
350 await Lifetime; // finish the async callback
351 }
352
354 public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
355 {
356 ArgumentNullException.ThrowIfNull(parameters);
357
358 using (LogContext.PushProperty(SerilogContextHelper.InstanceIdContextProperty, metadata.Id))
359 {
360 Logger.LogTrace("Handling bridge request...");
361
362 try
363 {
364 return await ProcessBridgeCommand(parameters, cancellationToken);
365 }
366 finally
367 {
368 initialBridgeRequestTcs.TrySetResult();
369 }
370 }
371 }
372
374 public void EnableCustomChatCommands() => chatTrackingContext.Active = DMApiAvailable;
375
377 public async Task Release()
378 {
380
384 released = true;
385 await DisposeAsync();
386 }
387
389 public async Task<TopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
390 {
391 ArgumentNullException.ThrowIfNull(parameters);
392
393 if (Lifetime.IsCompleted)
394 {
395 Logger.LogWarning(
396 "Attempted to send a command to an inactive SessionController: {commandType}",
397 parameters.CommandType);
398 return null;
399 }
400
401 if (!DMApiAvailable)
402 {
403 Logger.LogTrace("Not sending topic request {commandType} to server without/with incompatible DMAPI!", parameters.CommandType);
404 return null;
405 }
406
407 TopicResponse fullResponse = null;
408 try
409 {
410 var combinedResponse = await SendTopicRequest(parameters, cancellationToken);
411
412 void LogCombinedResponse()
413 {
414 if (LogTopicRequests && combinedResponse != null)
415 Logger.LogTrace("Topic response: {topicString}", combinedResponse.ByondTopicResponse.StringData ?? "(NO STRING DATA)");
416 }
417
418 LogCombinedResponse();
419
420 if (combinedResponse?.InteropResponse?.Chunk != null)
421 {
422 Logger.LogTrace("Topic response is chunked...");
423
424 ChunkData nextChunk = combinedResponse.InteropResponse.Chunk;
425 do
426 {
427 var nextRequest = await ProcessChunk<TopicResponse, ChunkedTopicParameters>(
428 (completedResponse, cancellationToken) =>
429 {
430 fullResponse = completedResponse;
431 return Task.FromResult<ChunkedTopicParameters>(null);
432 },
433 error =>
434 {
435 Logger.LogWarning("Topic response chunking error: {message}", error);
436 return null;
437 },
438 combinedResponse?.InteropResponse?.Chunk,
439 cancellationToken);
440
441 if (nextRequest != null)
442 {
443 nextRequest.PayloadId = nextChunk.PayloadId;
444 combinedResponse = await SendTopicRequest(nextRequest, cancellationToken);
445 LogCombinedResponse();
446 nextChunk = combinedResponse?.InteropResponse?.Chunk;
447 }
448 else
449 nextChunk = null;
450 }
451 while (nextChunk != null);
452 }
453 else
454 fullResponse = combinedResponse?.InteropResponse;
455 }
456 catch (OperationCanceledException ex)
457 {
458 Logger.LogDebug(
459 ex,
460 "Topic request {cancellationType}!",
461 cancellationToken.IsCancellationRequested
462 ? "aborted"
463 : "timed out");
464 cancellationToken.ThrowIfCancellationRequested();
465 }
466
467 if (fullResponse?.ErrorMessage != null)
468 Logger.LogWarning(
469 "Errored topic response for command {commandType}: {errorMessage}",
470 parameters.CommandType,
471 fullResponse.ErrorMessage);
472
473 return fullResponse;
474 }
475
477 public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
478 {
480
481 if (port == 0)
482 throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
483
484 async Task<bool> ImmediateTopicPortChange()
485 {
486 var commandResult = await SendCommand(
487 new TopicParameters(port),
488 cancellationToken);
489
490 if (commandResult?.ErrorMessage != null)
491 return false;
492
493 ReattachInformation.Port = port;
494 return true;
495 }
496
499 {
500 if (portAssignmentTcs != null)
501 throw new InvalidOperationException("A port change operation is already in progress!");
502 nextPort = port;
503 portAssignmentTcs = new TaskCompletionSource<bool>();
504 return portAssignmentTcs.Task;
505 }
506 else
507 return ImmediateTopicPortChange();
508 }
509
511 public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
512 {
513 if (RebootState == newRebootState)
514 return true;
515
516 Logger.LogTrace("Changing reboot state to {newRebootState}", newRebootState);
517
518 ReattachInformation.RebootState = newRebootState;
519 var result = await SendCommand(
520 new TopicParameters(newRebootState),
521 cancellationToken);
522
523 return result?.ErrorMessage == null;
524 }
525
527 public void ResetRebootState()
528 {
530 Logger.LogTrace("Resetting reboot state...");
531 ReattachInformation.RebootState = RebootState.Normal;
532 }
533
535 public void AdjustPriority(bool higher) => process.AdjustPriority(higher);
536
538 public void Suspend() => process.Suspend();
539
541 public void Resume() => process.Resume();
542
545 {
546 var oldDmb = ReattachInformation.Dmb;
547 ReattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
548 return oldDmb;
549 }
550
552 public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
553 {
554 ReattachInformation.RuntimeInformation.InstanceName = newInstanceName;
555 return SendCommand(new TopicParameters(newInstanceName), cancellationToken);
556 }
557
559 public Task UpdateChannels(IEnumerable<ChannelRepresentation> newChannels, CancellationToken cancellationToken)
560 => SendCommand(
561 new TopicParameters(
562 new ChatUpdate(newChannels)),
563 cancellationToken);
564
566 public Task CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken);
567
577 async Task<LaunchResult> GetLaunchResult(
578 IAssemblyInformationProvider assemblyInformationProvider,
580 uint? startupTimeout,
581 bool reattached,
582 bool apiValidate)
583 {
584 var startTime = DateTimeOffset.UtcNow;
585 var useBridgeRequestForLaunchResult = !reattached && (apiValidate || DMApiAvailable);
586 var startupTask = useBridgeRequestForLaunchResult
587 ? initialBridgeRequestTcs.Task
589 var toAwait = Task.WhenAny(startupTask, process.Lifetime);
590
591 if (startupTimeout.HasValue)
592 toAwait = Task.WhenAny(
593 toAwait,
595 TimeSpan.FromSeconds(startupTimeout.Value),
596 CancellationToken.None)); // DCT: None available, task will clean up after delay
597
598 Logger.LogTrace(
599 "Waiting for LaunchResult based on {launchResultCompletionCause}{possibleTimeout}...",
600 useBridgeRequestForLaunchResult ? "initial bridge request" : "process startup",
601 startupTimeout.HasValue ? $" with a timeout of {startupTimeout.Value}s" : String.Empty);
602
603 await toAwait;
604
605 var result = new LaunchResult
606 {
607 ExitCode = process.Lifetime.IsCompleted ? await process.Lifetime : null,
608 StartupTime = startupTask.IsCompleted ? (DateTimeOffset.UtcNow - startTime) : null,
609 };
610
611 Logger.LogTrace("Launch result: {launchResult}", result);
612
613 if (!result.ExitCode.HasValue && reattached && !disposed)
614 {
615 var reattachResponse = await SendCommand(
616 new TopicParameters(
617 assemblyInformationProvider.Version,
619 reattachTopicCts.Token);
620
621 if (reattachResponse != null)
622 {
623 if (reattachResponse?.CustomCommands != null)
624 chatTrackingContext.CustomCommands = reattachResponse.CustomCommands;
625 else if (reattachResponse != null)
626 Logger.Log(
627 CompileJob.DMApiVersion >= new Version(5, 2, 0)
628 ? LogLevel.Warning
629 : LogLevel.Debug,
630 "DMAPI Interop v{interopVersion} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
631 CompileJob.DMApiVersion.Semver());
632 }
633 }
634
635 return result;
636 }
637
642 {
643 if (disposed)
644 throw new ObjectDisposedException(nameof(SessionController));
645 }
646
653 async Task<BridgeResponse> ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken)
654 {
655 var response = new BridgeResponse();
656 switch (parameters.CommandType)
657 {
658 case BridgeCommandType.ChatSend:
659 if (parameters.ChatMessage == null)
660 return BridgeError("Missing chatMessage field!");
661
662 if (parameters.ChatMessage.ChannelIds == null)
663 return BridgeError("Missing channelIds field in chatMessage!");
664
665 if (parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _)))
666 return BridgeError("Invalid channelIds in chatMessage!");
667
668 if (parameters.ChatMessage.Text == null)
669 return BridgeError("Missing message field in chatMessage!");
670
671 var anyFailed = false;
672 var parsedChannels = parameters.ChatMessage.ChannelIds.Select(
673 channelString =>
674 {
675 anyFailed |= !UInt64.TryParse(channelString, out var channelId);
676 return channelId;
677 });
678
679 if (anyFailed)
680 return BridgeError("Failed to parse channelIds as U64!");
681
683 parameters.ChatMessage,
684 parsedChannels);
685 break;
686 case BridgeCommandType.Prime:
687 Interlocked.Exchange(ref primeTcs, new TaskCompletionSource()).SetResult();
688 break;
689 case BridgeCommandType.Kill:
690 Logger.LogInformation("Bridge requested process termination!");
693 break;
694 case BridgeCommandType.PortUpdate:
696 {
697 if (!parameters.CurrentPort.HasValue)
698 {
700 Logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
701 return BridgeError("Missing stringified port as data parameter!");
702 }
703
704 var currentPort = parameters.CurrentPort.Value;
705 if (!nextPort.HasValue)
706 ReattachInformation.Port = parameters.CurrentPort.Value; // not ready yet, so what we'll do is accept the random port DD opened on for now and change it later when we decide to
707 else
708 {
709 // nextPort is ready, tell DD to switch to that
710 // if it fails it'll kill itself
711 response.NewPort = nextPort.Value;
712 ReattachInformation.Port = nextPort.Value;
713 nextPort = null;
714
715 // we'll also get here from SetPort so complete that task
716 var tmpTcs = portAssignmentTcs;
717 portAssignmentTcs = null;
718 tmpTcs.SetResult(true);
719 }
720
721 portClosedForReboot = false;
722 }
723
724 break;
725 case BridgeCommandType.Startup:
726 apiValidationStatus = ApiValidationStatus.BadValidationRequest;
727 if (parameters.Version == null)
728 return BridgeError("Missing dmApiVersion field!");
729
730 DMApiVersion = parameters.Version;
731 if (DMApiVersion.Major != DMApiConstants.InteropVersion.Major)
732 {
734 return BridgeError("Incompatible dmApiVersion!");
735 }
736
737 switch (parameters.MinimumSecurityLevel)
738 {
739 case DreamDaemonSecurity.Ultrasafe:
740 apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
741 break;
742 case DreamDaemonSecurity.Safe:
744 break;
745 case DreamDaemonSecurity.Trusted:
747 break;
748 case null:
749 return BridgeError("Missing minimumSecurityLevel field!");
750 default:
751 return BridgeError("Invalid minimumSecurityLevel!");
752 }
753
754 Logger.LogTrace("ApiValidationStatus set to {apiValidationStatus}", apiValidationStatus);
755
756 response.RuntimeInformation = new RuntimeInformation(
765
766 // Load custom commands
767 chatTrackingContext.CustomCommands = parameters.CustomCommands;
768 Interlocked.Exchange(ref startupTcs, new TaskCompletionSource()).SetResult();
769 break;
770 case BridgeCommandType.Reboot:
771 Interlocked.Increment(ref rebootBridgeRequestsProcessing);
772 try
773 {
775 {
776 chatTrackingContext.Active = false;
777 response.NewPort = 0;
778 portClosedForReboot = true;
779 }
780
781 Interlocked.Exchange(ref rebootTcs, new TaskCompletionSource()).SetResult();
782 await RebootGate.WithToken(cancellationToken);
783 }
784 finally
785 {
786 Interlocked.Decrement(ref rebootBridgeRequestsProcessing);
787 }
788
789 break;
790 case BridgeCommandType.Chunk:
791 return await ProcessChunk<BridgeParameters, BridgeResponse>(ProcessBridgeCommand, BridgeError, parameters.Chunk, cancellationToken);
792 case null:
793 return BridgeError("Missing commandType!");
794 default:
795 return BridgeError($"commandType {parameters.CommandType} not supported!");
796 }
797
798 return response;
799 }
800
807 {
808 Logger.LogWarning("Bridge request chunking error: {message}", message);
809 return new BridgeResponse
810 {
811 ErrorMessage = message,
812 };
813 }
814
821 async Task<CombinedTopicResponse> SendTopicRequest(TopicParameters parameters, CancellationToken cancellationToken)
822 {
823 parameters.AccessIdentifier = ReattachInformation.AccessIdentifier;
824
825 var fullCommandString = GenerateQueryString(parameters, out var json);
826 if (LogTopicRequests)
827 Logger.LogTrace("Topic request: {json}", json);
828 var fullCommandByteCount = Encoding.UTF8.GetByteCount(fullCommandString);
829 var topicPriority = parameters.IsPriority;
830 if (fullCommandByteCount <= DMApiConstants.MaximumTopicRequestLength)
831 return await SendRawTopic(fullCommandString, topicPriority, cancellationToken);
832
833 var interopChunkingVersion = new Version(5, 6, 0);
834 if (ReattachInformation.Dmb.CompileJob.DMApiVersion < interopChunkingVersion)
835 {
836 Logger.LogWarning(
837 "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}!",
839 fullCommandByteCount,
840 interopChunkingVersion);
841 return null;
842 }
843
844 var payloadId = NextPayloadId;
845
846 // AccessIdentifer is just noise in a chunked request
847 parameters.AccessIdentifier = null;
848 GenerateQueryString(parameters, out json);
849
850 // yes, this straight up ignores unicode, precalculating it is useless when we don't
851 // even know if the UTF8 bytes of the url encoded chunk will fit the window until we do said encoding
852 var fullPayloadSize = (uint)json.Length;
853
854 List<string> chunkQueryStrings = null;
855 for (var chunkCount = 2; chunkQueryStrings == null; ++chunkCount)
856 {
857 var standardChunkSize = fullPayloadSize / chunkCount;
858 var bigChunkSize = standardChunkSize + (fullPayloadSize % chunkCount);
859 if (bigChunkSize > DMApiConstants.MaximumTopicRequestLength)
860 continue;
861
862 chunkQueryStrings = new List<string>();
863 for (var i = 0U; i < chunkCount; ++i)
864 {
865 var startIndex = i * standardChunkSize;
866 var subStringLength = Math.Min(
867 fullPayloadSize - startIndex,
868 i == chunkCount - 1
869 ? bigChunkSize
870 : standardChunkSize);
871 var chunkPayload = json.Substring((int)startIndex, (int)subStringLength);
872
873 var chunk = new ChunkData
874 {
875 Payload = chunkPayload,
876 PayloadId = payloadId,
877 SequenceId = i,
878 TotalChunks = (uint)chunkCount,
879 };
880
881 var chunkParameters = new TopicParameters(chunk)
882 {
883 AccessIdentifier = ReattachInformation.AccessIdentifier,
884 };
885
886 var chunkCommandString = GenerateQueryString(chunkParameters, out _);
887 if (Encoding.UTF8.GetByteCount(chunkCommandString) > DMApiConstants.MaximumTopicRequestLength)
888 {
889 // too long when encoded, need more chunks
890 chunkQueryStrings = null;
891 break;
892 }
893
894 chunkQueryStrings.Add(chunkCommandString);
895 }
896 }
897
898 Logger.LogTrace("Chunking topic request ({totalChunks} total)...", chunkQueryStrings.Count);
899
900 CombinedTopicResponse combinedResponse = null;
901 bool LogRequestIssue(bool possiblyFromCompletedRequest)
902 {
903 if (combinedResponse?.InteropResponse == null || combinedResponse.InteropResponse.ErrorMessage != null)
904 {
905 Logger.LogWarning(
906 "Topic request {chunkingStatus} failed!{potentialRequestError}",
907 possiblyFromCompletedRequest ? "final chunk" : "chunking",
908 combinedResponse?.InteropResponse?.ErrorMessage != null
909 ? $" Request error: {combinedResponse.InteropResponse.ErrorMessage}"
910 : String.Empty);
911 return true;
912 }
913
914 return false;
915 }
916
917 foreach (var chunkCommandString in chunkQueryStrings)
918 {
919 combinedResponse = await SendRawTopic(chunkCommandString, topicPriority, cancellationToken);
920 if (LogRequestIssue(chunkCommandString == chunkQueryStrings.Last()))
921 return null;
922 }
923
924 while ((combinedResponse.InteropResponse.MissingChunks?.Count ?? 0) > 0)
925 {
926 Logger.LogWarning("DD is still missing some chunks of topic request P{payloadId}! Sending missing chunks...", payloadId);
927 var lastIndex = combinedResponse.InteropResponse.MissingChunks.Last();
928 foreach (var missingChunkIndex in combinedResponse.InteropResponse.MissingChunks)
929 {
930 var chunkCommandString = chunkQueryStrings[(int)missingChunkIndex];
931 combinedResponse = await SendRawTopic(chunkCommandString, topicPriority, cancellationToken);
932 if (LogRequestIssue(missingChunkIndex == lastIndex))
933 return null;
934 }
935 }
936
937 return combinedResponse;
938 }
939
946 string GenerateQueryString(TopicParameters parameters, out string json)
947 {
948 json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings);
949 var commandString = String.Format(
950 CultureInfo.InvariantCulture,
951 "?{0}={1}",
953 byondTopicSender.SanitizeString(json));
954 return commandString;
955 }
956
964 async Task<CombinedTopicResponse> SendRawTopic(string queryString, bool priority, CancellationToken cancellationToken)
965 {
966 var targetPort = ReattachInformation.Port;
967 var killedOrRebootedTask = Task.WhenAny(Lifetime, OnReboot);
968 global::Byond.TopicSender.TopicResponse byondResponse = null;
969 var firstSend = true;
970
971 const int PrioritySendAttempts = 5;
972 for (var i = PrioritySendAttempts - 1; i >= 0 && (priority || firstSend); --i)
973 try
974 {
975 firstSend = false;
976 if (!killedOrRebootedTask.IsCompleted)
977 byondResponse = await byondTopicSender.SendTopic(
978 new IPEndPoint(IPAddress.Loopback, targetPort),
979 queryString,
980 cancellationToken);
981
982 break;
983 }
984 catch (Exception ex)
985 {
986 Logger.LogWarning(ex, "SendTopic exception!{retryDetails}", priority ? $" {i} attempts remaining." : String.Empty);
987
988 if (priority && i > 0)
989 {
990 var delayTask = asyncDelayer.Delay(TimeSpan.FromSeconds(2), cancellationToken);
991 await Task.WhenAny(killedOrRebootedTask, delayTask);
992 }
993 }
994
995 if (byondResponse == null)
996 {
997 if (priority)
998 if (killedOrRebootedTask.IsCompleted)
999 Logger.LogWarning(
1000 "Unable to send priority topic \"{queryString}\" DreamDaemon {stateClearAction}!",
1001 queryString,
1002 Lifetime.IsCompleted ? "process ended" : "rebooted");
1003 else
1004 Logger.LogError(
1005 "Unable to send priority topic \"{queryString}\"!",
1006 queryString);
1007
1008 return null;
1009 }
1010
1011 var topicReturn = byondResponse.StringData;
1012
1013 TopicResponse interopResponse = null;
1014 if (topicReturn != null)
1015 try
1016 {
1017 interopResponse = JsonConvert.DeserializeObject<TopicResponse>(topicReturn, DMApiConstants.SerializerSettings);
1018 }
1019 catch (Exception ex)
1020 {
1021 Logger.LogWarning(ex, "Invalid interop response: {topicReturnString}", topicReturn);
1022 }
1023
1024 return new CombinedTopicResponse(byondResponse, interopResponse);
1025 }
1026 }
1027}
Metadata about a server instance.
Definition: Instance.cs:9
Version Version
The DMAPI global::System.Version for BridgeCommandType.Startup requests.
ChatMessage ChatMessage
The Interop.ChatMessage for BridgeCommandType.ChatSend requests.
ushort? CurrentPort
The current port for BridgeCommandType.PortUpdate requests.
ICollection< CustomCommand > CustomCommands
The DMAPI CustomCommands for BridgeCommandType.Startup requests.
ChunkData Chunk
The ChunkData for BridgeCommandType.Chunk requests.
DreamDaemonSecurity? MinimumSecurityLevel
The minimum required DreamDaemonSecurity level 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.
DreamDaemonSecurity? SecurityLevel
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.
DreamDaemonVisibility? Visibility
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:45
uint NextPayloadId
Gets a payload ID for use in a new ChunkSetInfo.
Definition: Chunker.cs:21
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 global::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.
bool IsPriority
Whether or not the TopicParameters constitute a priority request.
Combines a global::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.
IDmbProvider InitialDmb
The IDmbProvider initially used to launch DreamDaemon. Should be a different IDmbProvider than Dmb....
RuntimeInformation RuntimeInformation
The Interop.Bridge.RuntimeInformation for the DMAPI.
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon.
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController.
Models.CompileJob CompileJob
Gets the CompileJob associated with the ISessionController.
readonly object synchronizationLock
lock object for port updates and disposed.
Task UpdateChannels(IEnumerable< ChannelRepresentation > newChannels, CancellationToken cancellationToken)
Called when newChannels are set. A Task representing the running operation.
void AdjustPriority(bool higher)
Set's the owned global::System.Diagnostics.Process.PriorityClass to a non-normal value.
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().
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process.
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.
Task CreateDump(string outputFile, CancellationToken cancellationToken)
Create a dump file of the process. A Task representing the running operation.
bool disposed
If the SessionController has been disposed.
void ResetRebootState()
Changes RebootState to RebootState.Normal without telling the DMAPI.
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the SessionController.
async Task< CombinedTopicResponse > SendRawTopic(string queryString, bool priority, CancellationToken cancellationToken)
Send a given queryString to DreamDaemon's /world/Topic.
async Task Release()
Releases the IProcess without terminating it. Also calls IDisposable.Dispose. A Task representing the...
Task< bool > SetPort(ushort port, CancellationToken cancellationToken)
Causes the world to start listening on a newPort . A Task<TResult> resulting in true if the operation...
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.
async Task< BridgeResponse > ProcessBridgeCommand(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
readonly IChatManager chat
The IChatManager for the SessionController.
async Task< bool > SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
Attempts to change the current RebootState to newRebootState . A Task<TResult> resulting in true if t...
readonly IChatTrackingContext chatTrackingContext
The IChatTrackingContext for the SessionController.
bool ClosePortOnReboot
If the port should be rotated off when the world reboots.
Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
Called when the owning Instance is renamed. A Task representing the running operation.
volatile TaskCompletionSource startupTcs
The TaskCompletionSource that completes when DD sends a valid startup bridge request.
readonly IByondExecutableLock byondLock
The IByondExecutableLock for the SessionController.
async Task< CombinedTopicResponse > SendTopicRequest(TopicParameters parameters, CancellationToken cancellationToken)
Send a topic request for given parameters to DreamDaemon, chunking it if necessary.
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters . A Task<TResult> resulting in the BridgeResponse for the request o...
SessionController(ReattachInformation reattachInformation, Api.Models.Instance metadata, IProcess process, IByondExecutableLock byondLock, global::Byond.TopicSender.ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, ILogger< SessionController > logger, Func< Task > postLifetimeCallback, uint? startupTimeout, bool reattached, bool apiValidate)
Initializes a new instance of the SessionController class.
IDisposable ReplaceDmbProvider(IDmbProvider dmbProvider)
Replace the IDmbProvider in use with a given newProvider , disposing the old one. An IDisposable to b...
string GenerateQueryString(TopicParameters parameters, out string json)
Generates a global::Byond.TopicSender.ITopicClient query string for a given set of parameters .
readonly global::Byond.TopicSender.ITopicClient byondTopicSender
The global::Byond.TopicSender.ITopicClient for the SessionController.
bool portClosedForReboot
If we know DreamDaemon currently has it's port closed.
void CheckDisposed()
Throws an ObjectDisposedException if DisposeAsync has been called.
void EnableCustomChatCommands()
Enables the reading of custom chat commands from the ISessionController.
async Task< LaunchResult > GetLaunchResult(IAssemblyInformationProvider assemblyInformationProvider, IAsyncDelayer asyncDelayer, uint? startupTimeout, bool reattached, bool apiValidate)
The Task<TResult> for LaunchResult.
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().
ushort? nextPort
The port to assign DreamDaemon when it queries for it.
Task OnPrime
A Task that completes when the server calls /world/TgsInitializationComplete().
TaskCompletionSource< bool > portAssignmentTcs
The TaskCompletionSource<TResult> SetPort(ushort, CancellationToken) waits on when DreamDaemon curren...
readonly IBridgeRegistration bridgeRegistration
The IBridgeRegistration for the SessionController.
volatile TaskCompletionSource rebootTcs
The TaskCompletionSource that completes when DD tells us about a reboot.
volatile TaskCompletionSource primeTcs
The TaskCompletionSource that completes when DD tells us it's primed.
Task RebootGate
A Task that must complete before a TgsReboot() bridge request can complete.
readonly IProcess process
The IProcess for the SessionController.
async Task< TopicResponse > SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic(). A Task<TResult> resulting in the TopicResponse...
RebootState RebootState
The current DreamDaemon reboot state.
ushort Port
The port DreamDaemon was last listening on.
Helpers for manipulating the Serilog.Context.LogContext.
const string InstanceIdContextProperty
The Serilog.Context.LogContext property name for Models.Instance Api.Models.EntityId....
Represents usage of the two primary BYOND server executables.
void DoNotDeleteThisSession()
Call if, during a detach, this version should not be deleted.
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.
CompileJob CompileJob
The CompileJob of the .dmb.
Definition: IDmbProvider.cs:25
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
Handles communication with a DreamDaemon IProcess.
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process.
Definition: IProcessBase.cs:14
void Suspend()
Suspends the process.
void Resume()
Resumes the process.
void AdjustPriority(bool higher)
Set's the owned global::System.Diagnostics.Process.PriorityClass to a non-normal value.
Task CreateDump(string outputFile, CancellationToken cancellationToken)
Create a dump file of the process.
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.
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.