1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
6 using System.Collections.Generic;
11 using System.Threading.Tasks;
35 if (!Lifetime.IsCompleted)
36 throw new InvalidOperationException(
"ApiValidated cannot be checked while Lifetime is incomplete!");
37 return apiValidationStatus;
47 return reattachInformation.Dmb;
57 if (portClosedForReboot)
59 return reattachInformation.Port;
69 return reattachInformation.RebootState;
74 public Version DMApiVersion {
get;
private set; }
77 public bool ClosePortOnReboot {
get;
set; }
80 public bool TerminationWasRequested {
get;
private set; }
86 public Task<int> Lifetime => process.Lifetime;
89 public Task OnReboot => rebootTcs.Task;
92 public Task OnPrime => primeTcs.Task;
95 public bool DMApiAvailable => reattachInformation.Dmb.CompileJob.DMApiVersion?.Major ==
DMApiConstants.
Version.Major;
145 readonly ILogger<SessionController>
logger;
210 Api.Models.Instance metadata,
213 ITopicClient byondTopicSender,
218 ILogger<SessionController> logger,
219 uint? startupTimeout,
223 this.reattachInformation = reattachInformation ??
throw new ArgumentNullException(nameof(reattachInformation));
224 this.metadata = metadata ??
throw new ArgumentNullException(nameof(metadata));
225 this.process = process ??
throw new ArgumentNullException(nameof(process));
226 this.byondLock = byondLock ??
throw new ArgumentNullException(nameof(byondLock));
227 this.byondTopicSender = byondTopicSender ??
throw new ArgumentNullException(nameof(byondTopicSender));
228 this.chatTrackingContext = chatTrackingContext ??
throw new ArgumentNullException(nameof(chatTrackingContext));
229 if (bridgeRegistrar == null)
230 throw new ArgumentNullException(nameof(bridgeRegistrar));
231 this.chat = chat ??
throw new ArgumentNullException(nameof(chat));
232 this.logger = logger ??
throw new ArgumentNullException(nameof(logger));
234 if (apiValidate || DMApiAvailable)
241 "Not registering session with {0} DMAPI version for interop!",
244 : $
"incompatible ({reattachInformation.Dmb.CompileJob.DMApiVersion})");
246 portClosedForReboot =
false;
251 rebootTcs =
new TaskCompletionSource<object>();
252 primeTcs =
new TaskCompletionSource<object>();
253 reattachTopicCts =
new CancellationTokenSource();
254 synchronizationLock =
new object();
259 lock (synchronizationLock)
261 reattachTopicCts.Cancel();
262 chatTrackingContext.
Active =
false;
264 TaskScheduler.Current);
267 assemblyInformationProvider,
271 logger.LogDebug(
"Created session controller. CommsKey: {0}, Port: {1}", reattachInformation.
AccessIdentifier, Port);
278 #pragma warning disable CA1821 // Remove empty Finalizers TODO: remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed 280 #pragma warning restore CA1821 // Remove empty Finalizers 286 GC.SuppressFinalize(
this);
295 lock (synchronizationLock)
300 logger.LogTrace(
"Disposing...");
310 bridgeRegistration?.Dispose();
311 reattachInformation.Dmb?.Dispose();
312 chatTrackingContext.Dispose();
313 reattachTopicCts.Dispose();
318 logger.LogError(
"Being disposed via finalizer!");
322 else if (logger != null)
323 logger.LogCritical(
"Unable to terminate active DreamDaemon session due to finalizer ordering!");
337 uint? startupTimeout,
340 var startTime = DateTimeOffset.Now;
341 Task toAwait = process.Startup;
343 if (startupTimeout.HasValue)
344 toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
346 await toAwait.ConfigureAwait(
false);
350 ExitCode = process.Lifetime.IsCompleted ? (
int?)await process.Lifetime.ConfigureAwait(
false) : null,
351 StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
354 logger.LogTrace(
"Launch result: {0}", result);
356 if (!result.ExitCode.HasValue && reattached && !disposed)
358 var reattachResponse = await SendCommand(
360 assemblyInformationProvider.
Version,
361 reattachInformation.RuntimeInformation.ServerPort),
362 reattachTopicCts.Token)
363 .ConfigureAwait(
false);
365 if (reattachResponse.InteropResponse?.CustomCommands != null)
366 chatTrackingContext.CustomCommands = reattachResponse.InteropResponse.CustomCommands;
367 else if (reattachResponse.InteropResponse != null)
369 "DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
370 Dmb.CompileJob.DMApiVersion.Semver());
379 if (parameters == null)
380 throw new ArgumentNullException(nameof(parameters));
382 using (LogContext.PushProperty(
"Instance", metadata.Id))
384 logger.LogTrace(
"Handling bridge request...");
398 ErrorMessage =
"Missing channelIds field in chatMessage!" 401 if (parameters.
ChatMessage.
ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _)))
413 await chat.SendMessage(
416 cancellationToken).ConfigureAwait(
false);
419 var oldPrimeTcs = primeTcs;
420 primeTcs =
new TaskCompletionSource<object>();
421 oldPrimeTcs.SetResult(null);
424 logger.LogInformation(
"Bridge requested process termination!");
425 TerminationWasRequested =
true;
429 lock (synchronizationLock)
434 logger.LogWarning(
"DreamDaemon sent new port command without providing it's own!");
437 ErrorMessage =
"Missing stringified port as data parameter!" 442 if (!nextPort.HasValue)
443 reattachInformation.Port = parameters.
CurrentPort.Value;
448 response.NewPort = nextPort.Value;
449 reattachInformation.Port = nextPort.Value;
453 var tmpTcs = portAssignmentTcs;
454 portAssignmentTcs = null;
455 tmpTcs.SetResult(
true);
458 portClosedForReboot =
false;
464 if (parameters.
Version == null)
470 DMApiVersion = parameters.
Version;
503 response.RuntimeInformation = reattachInformation.RuntimeInformation;
509 if (ClosePortOnReboot)
511 chatTrackingContext.Active =
false;
512 response.NewPort = 0;
513 portClosedForReboot =
true;
516 var oldRebootTcs = rebootTcs;
517 rebootTcs =
new TaskCompletionSource<object>();
518 oldRebootTcs.SetResult(null);
521 response.ErrorMessage =
"Missing commandType!";
524 response.ErrorMessage =
"Requested commandType not supported!";
542 public void EnableCustomChatCommands() => chatTrackingContext.Active = DMApiAvailable;
550 var tmpProvider = reattachInformation.Dmb;
551 reattachInformation.Dmb = null;
554 byondLock.DoNotDeleteThisSession();
555 tmpProvider.KeepAlive();
556 reattachInformation.Dmb = tmpProvider;
557 return reattachInformation;
563 if (parameters == null)
564 throw new ArgumentNullException(nameof(parameters));
566 if (Lifetime.IsCompleted)
569 "Attempted to send a command to an inactive SessionController: {0}",
576 logger.LogTrace(
"Not sending topic request {0} to server without/with incompatible DMAPI!", parameters.
CommandType);
583 logger.LogTrace(
"Topic request: {0}", json);
586 var commandString = String.Format(CultureInfo.InvariantCulture,
589 byondTopicSender.SanitizeString(json));
591 var targetPort = reattachInformation.Port;
593 var topicResponse = await byondTopicSender.SendTopic(
594 new IPEndPoint(IPAddress.Loopback, targetPort),
596 cancellationToken).ConfigureAwait(
false);
598 var topicReturn = topicResponse.StringData;
600 Interop.Topic.TopicResponse interopResponse = null;
601 if (topicReturn != null)
605 if (interopResponse.ErrorMessage != null)
607 logger.LogWarning(
"Errored topic response for command {0}: {1}", parameters.
CommandType, interopResponse.ErrorMessage);
610 logger.LogTrace(
"Interop response: {0}", topicReturn);
614 logger.LogWarning(
"Invalid interop response: {0}", topicReturn);
619 catch (OperationCanceledException)
622 "Topic request {0}!",
623 cancellationToken.IsCancellationRequested
626 cancellationToken.ThrowIfCancellationRequested();
630 logger.LogWarning(
"Send command exception:{0}{1}", Environment.NewLine, e);
637 public Task<bool>
SetPort(ushort port, CancellationToken cancellationToken)
642 throw new ArgumentOutOfRangeException(nameof(port), port,
"port must not be zero!");
644 async Task<bool> ImmediateTopicPortChange()
646 var commandResult = await SendCommand(
649 .ConfigureAwait(
false);
651 if (commandResult.InteropResponse?.ErrorMessage != null)
654 reattachInformation.Port = port;
658 lock (synchronizationLock)
659 if (portClosedForReboot)
661 if (portAssignmentTcs != null)
662 throw new InvalidOperationException(
"A port change operation is already in progress!");
664 portAssignmentTcs =
new TaskCompletionSource<bool>();
665 return portAssignmentTcs.Task;
668 return ImmediateTopicPortChange();
677 logger.LogTrace(
"Changing reboot state to {0}", newRebootState);
679 reattachInformation.RebootState = newRebootState;
680 var result = await SendCommand(
683 .ConfigureAwait(
false);
685 return result?.InteropResponse != null && result.InteropResponse?.ErrorMessage == null;
692 logger.LogTrace(
"Resetting reboot state...");
693 reattachInformation.RebootState =
RebootState.Normal;
697 public void SetHighPriority() => process.SetHighPriority();
700 public void Suspend() => process.Suspend();
703 public void Resume() => process.Resume();
708 var oldDmb = reattachInformation.Dmb;
709 reattachInformation.Dmb = dmbProvider ??
throw new ArgumentNullException(nameof(dmbProvider));
714 public Task
InstanceRenamed(
string newInstanceName, CancellationToken cancellationToken)
715 => SendCommand(
new TopicParameters(newInstanceName), cancellationToken);
718 public Task UpdateChannels(IEnumerable<ChannelRepresentation> newChannels, CancellationToken cancellationToken)
725 public Task CreateDump(
string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken);
CompileJob CompileJob
The CompileJob of the .dmb
readonly CancellationTokenSource reattachTopicCts
A CancellationTokenSource used for the topic send operation made on reattaching.
SessionController(ReattachInformation reattachInformation, Api.Models.Instance metadata, IProcess process, IByondExecutableLock byondLock, ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, ILogger< SessionController > logger, uint?startupTimeout, bool reattached, bool apiValidate)
Construct a SessionController
ApiValidationStatus
Status of DMAPI validation
readonly IChatManager chat
The IChatManager for the SessionController
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
readonly object synchronizationLock
object for port updates and disposed.
ushort nextPort
The port to assign DreamDaemon when it queries for it
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController
Version Version
The DMAPI global::System.Version for BridgeCommandType.Startup requests.
BridgeCommandType CommandType
The BridgeCommandType.
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
Use server authentication
Parameters for a topic request.
readonly IProcess process
The IProcess for the SessionController
readonly Api.Models.Instance metadata
The Instance metadata.
ICollection< CustomCommand > CustomCommands
The DMAPI CustomCommands for BridgeCommandType.Startup requests.
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
TaskCompletionSource< bool > portAssignmentTcs
The TaskCompletionSource<TResult> SetPort(ushort, CancellationToken) waits on when DreamDaemon curren...
ChatMessage ChatMessage
The Interop.ChatMessage for BridgeCommandType.ChatSend requests.
void ReplaceDmbProvider(IDmbProvider dmbProvider)
Replace Dmb with a given newProvider , disposing the old one.
async Task< CombinedTopicResponse > SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
TopicCommandType CommandType
The TopicCommandType.
bool portClosedForReboot
If we know DreamDaemon currently has it's port closed
virtual Version DMApiVersion
The DMAPI Version.
const string TopicData
Parameter json is encoded in for topic requests.
DreamDaemonSecurity MinimumSecurityLevel
The minimum required DreamDaemonSecurity level for BridgeCommandType.Startup requests.
Notifyee of when ChannelRepresentations in a IChatTrackingContext are updated.
Constants used for communication with the DMAPI
Represents the result of trying to start a DD process
readonly IBridgeRegistration bridgeRegistration
The IBridgeRegistration for the SessionController
bool released
If process should be kept alive instead
For managing connected chat services
The owning instance was renamed.
ushort CurrentPort
The current port for BridgeCommandType.PortUpdate requests.
Task< bool > SetPort(ushort port, CancellationToken cancellationToken)
Causes the world to start listening on a newPort
void Dispose(bool disposing)
Implements the IDisposable pattern
RebootState
Represents the action to take when /world/Reboot() is called
Common base for interop parameters.
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
readonly IByondExecutableLock byondLock
The IByondExecutableLock for the SessionController
static readonly JsonSerializerSettings SerializerSettings
JsonSerializerSettings for use when communicating with the DMAPI.
bool Active
If the CustomCommands should be used.
ReattachInformation Release()
Releases the IProcess without terminating it. Also calls IDisposable.Dispose
static readonly Version Version
The DMAPI Version being used.
string AccessIdentifier
Used to identify and authenticate the DreamDaemon instance
Abstraction over a global::System.Diagnostics.Process
string Text
The message string.
Parameters for a bridge request.
readonly ILogger< SessionController > logger
The ILogger for the SessionController
Represents usage of the two primary BYOND server executables
Handles communication with a DreamDaemon IProcess
ICollection< string > ChannelIds
The ICollection<T> of Chat.ChannelRepresentation.Ids to sent the Text to. Must be safe to parse as ul...
async Task< bool > SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
Attempts to change the current RebootState to newRebootState
BridgeCommandType
Represents the BridgeParameters.CommandType.
Represents a BYOND installation. RawData.Content is used to upload custom BYOND version zip files...
readonly ITopicClient byondTopicSender
The ITopicClient for the SessionController
Represents a tracking of dynamic chat json files
DreamDaemonSecurity
DreamDaemon's security level
Provides absolute paths to the latest compiled .dmbs
readonly IChatTrackingContext chatTrackingContext
The IChatTrackingContext for the SessionController
Registers IBridgeHandlers.
void SetChannelSink(IChannelSink channelSink)
Sets the channelSink for the IChatTrackingContext.
Represents an error message returned by the server
Represents an update of ChannelRepresentations.
async Task< LaunchResult > GetLaunchResult(IAssemblyInformationProvider assemblyInformationProvider, uint?startupTimeout, bool reattached)
The Task<TResult> for LaunchResult.
Represents a registration of an interop session.
bool disposed
If the SessionController has been disposed
TaskCompletionSource< object > primeTcs
The TaskCompletionSource<TResult> that completes when DD tells us it's primed.
A response to a bridge request.
Combines a global::Byond.TopicSender.TopicResponse with a TopicResponse.
TaskCompletionSource< object > rebootTcs
The TaskCompletionSource<TResult> that completes when DD tells us about a reboot
void ResetRebootState()
Changes RebootState to RebootState.Normal without telling the DMAPI