1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
6 using System.Collections.Generic;
11 using System.Threading.Tasks;
36 return reattachInformation.IsPrimary;
45 if (!Lifetime.IsCompleted)
46 throw new InvalidOperationException(
"ApiValidated cannot be checked while Lifetime is incomplete!");
47 return apiValidationStatus;
57 return reattachInformation.Dmb;
67 if (portClosedForReboot)
69 return reattachInformation.Port;
79 return reattachInformation.RebootState;
84 public Version DMApiVersion {
get;
private set; }
87 public bool ClosePortOnReboot {
get;
set; }
90 public bool TerminationWasRequested {
get;
private set; }
96 public Task<int> Lifetime => process.Lifetime;
99 public Task OnReboot => rebootTcs.Task;
102 public Task OnPrime => primeTcs.Task;
152 readonly ILogger<SessionController>
logger;
216 Api.Models.Instance metadata,
219 ITopicClient byondTopicSender,
224 ILogger<SessionController> logger,
225 uint? startupTimeout,
228 this.reattachInformation = reattachInformation ??
throw new ArgumentNullException(nameof(reattachInformation));
229 this.metadata = metadata ??
throw new ArgumentNullException(nameof(metadata));
230 this.process = process ??
throw new ArgumentNullException(nameof(process));
231 this.byondLock = byondLock ??
throw new ArgumentNullException(nameof(byondLock));
232 this.byondTopicSender = byondTopicSender ??
throw new ArgumentNullException(nameof(byondTopicSender));
233 this.chatTrackingContext = chatTrackingContext ??
throw new ArgumentNullException(nameof(chatTrackingContext));
234 bridgeRegistration = bridgeRegistrar?.
RegisterHandler(
this) ??
throw new ArgumentNullException(nameof(bridgeRegistrar));
235 this.chat = chat ??
throw new ArgumentNullException(nameof(chat));
236 this.logger = logger ??
throw new ArgumentNullException(nameof(logger));
240 portClosedForReboot =
false;
245 rebootTcs =
new TaskCompletionSource<object>();
246 primeTcs =
new TaskCompletionSource<object>();
247 reattachTopicCts =
new CancellationTokenSource();
248 synchronizationLock =
new object();
253 lock (synchronizationLock)
255 reattachTopicCts.Cancel();
256 chatTrackingContext.
Active =
false;
258 TaskScheduler.Current);
261 assemblyInformationProvider,
265 logger.LogDebug(
"Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.
AccessIdentifier, Port);
272 #pragma warning disable CA1821 // Remove empty Finalizers TODO: remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed 274 #pragma warning restore CA1821 // Remove empty Finalizers 280 GC.SuppressFinalize(
this);
289 lock (synchronizationLock)
294 logger.LogTrace(
"Disposing...");
304 bridgeRegistration.Dispose();
305 reattachInformation.Dmb?.Dispose();
306 chatTrackingContext.Dispose();
307 reattachTopicCts.Dispose();
312 logger.LogError(
"Being disposed via finalizer!");
316 else if (logger != null)
317 logger.LogCritical(
"Unable to terminate active DreamDaemon session due to finalizer ordering!");
331 uint? startupTimeout,
334 var startTime = DateTimeOffset.Now;
335 Task toAwait = process.Startup;
337 if (startupTimeout.HasValue)
338 toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
340 await toAwait.ConfigureAwait(
false);
344 ExitCode = process.Lifetime.IsCompleted ? (
int?)await process.Lifetime.ConfigureAwait(
false) : null,
345 StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
348 logger.LogTrace(
"Launch result: {0}", result);
350 if (!result.ExitCode.HasValue && reattached && !disposed)
352 var reattachResponse = await SendCommand(
354 assemblyInformationProvider.
Version,
355 reattachInformation.RuntimeInformation.ServerPort),
356 reattachTopicCts.Token)
357 .ConfigureAwait(
false);
359 if (reattachResponse.InteropResponse?.CustomCommands != null)
360 chatTrackingContext.CustomCommands = reattachResponse.InteropResponse.CustomCommands;
361 else if (reattachResponse.InteropResponse != null)
363 "DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
364 Dmb.CompileJob.DMApiVersion.Semver());
373 if (parameters == null)
374 throw new ArgumentNullException(nameof(parameters));
376 using (LogContext.PushProperty(
"Instance", metadata.Id))
378 logger.LogTrace(
"Handling bridge request...");
392 ErrorMessage =
"Missing channelIds field in chatMessage!" 395 if (parameters.
ChatMessage.
ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _)))
407 await chat.SendMessage(
410 cancellationToken).ConfigureAwait(
false);
413 var oldPrimeTcs = primeTcs;
414 primeTcs =
new TaskCompletionSource<object>();
415 oldPrimeTcs.SetResult(null);
418 logger.LogInformation(
"Bridge requested process termination!");
419 TerminationWasRequested =
true;
423 lock (synchronizationLock)
428 logger.LogWarning(
"DreamDaemon sent new port command without providing it's own!");
431 ErrorMessage =
"Missing stringified port as data parameter!" 436 if (!nextPort.HasValue)
437 reattachInformation.Port = parameters.
CurrentPort.Value;
442 response.NewPort = nextPort.Value;
443 reattachInformation.Port = nextPort.Value;
447 var tmpTcs = portAssignmentTcs;
448 portAssignmentTcs = null;
449 tmpTcs.SetResult(
true);
452 portClosedForReboot =
false;
458 if (parameters.
Version == null)
464 DMApiVersion = parameters.
Version;
488 response.RuntimeInformation = reattachInformation.RuntimeInformation;
494 if (ClosePortOnReboot)
496 chatTrackingContext.Active =
false;
497 response.NewPort = 0;
498 portClosedForReboot =
true;
501 var oldRebootTcs = rebootTcs;
502 rebootTcs =
new TaskCompletionSource<object>();
503 oldRebootTcs.SetResult(null);
506 response.ErrorMessage =
"Missing commandType!";
509 response.ErrorMessage =
"Requested commandType not supported!";
527 public void EnableCustomChatCommands() => chatTrackingContext.Active =
true;
535 var tmpProvider = reattachInformation.Dmb;
536 reattachInformation.Dmb = null;
539 byondLock.DoNotDeleteThisSession();
540 tmpProvider.KeepAlive();
541 reattachInformation.Dmb = tmpProvider;
542 return reattachInformation;
548 if (Lifetime.IsCompleted)
551 "Attempted to send a command to an inactive SessionController: {0}",
559 logger.LogTrace(
"Topic request: {0}", json);
562 var commandString = String.Format(CultureInfo.InvariantCulture,
565 byondTopicSender.SanitizeString(json));
567 var targetPort = reattachInformation.Port;
569 var topicResponse = await byondTopicSender.SendTopic(
570 new IPEndPoint(IPAddress.Loopback, targetPort),
572 cancellationToken).ConfigureAwait(
false);
574 var topicReturn = topicResponse.StringData;
576 Interop.Topic.TopicResponse interopResponse = null;
577 if (topicReturn != null)
581 if (interopResponse.ErrorMessage != null)
583 logger.LogWarning(
"Errored topic response for command {0}: {1}", parameters.
CommandType, interopResponse.ErrorMessage);
586 logger.LogTrace(
"Interop response: {0}", topicReturn);
590 logger.LogWarning(
"Invalid interop response: {0}", topicReturn);
595 catch (OperationCanceledException)
598 "Topic request {0}!",
599 cancellationToken.IsCancellationRequested
602 cancellationToken.ThrowIfCancellationRequested();
606 logger.LogWarning(
"Send command exception:{0}{1}", Environment.NewLine, e);
613 public Task<bool>
SetPort(ushort port, CancellationToken cancellationToken)
618 throw new ArgumentOutOfRangeException(nameof(port), port,
"port must not be zero!");
620 async Task<bool> ImmediateTopicPortChange()
622 var commandResult = await SendCommand(
625 .ConfigureAwait(
false);
627 if (commandResult.InteropResponse?.ErrorMessage != null)
630 reattachInformation.Port = port;
634 lock (synchronizationLock)
635 if (portClosedForReboot)
637 if (portAssignmentTcs != null)
638 throw new InvalidOperationException(
"A port change operation is already in progress!");
640 portAssignmentTcs =
new TaskCompletionSource<bool>();
641 return portAssignmentTcs.Task;
644 return ImmediateTopicPortChange();
653 logger.LogTrace(
"Changing reboot state to {0}", newRebootState);
655 reattachInformation.RebootState = newRebootState;
656 var result = await SendCommand(
659 .ConfigureAwait(
false);
661 return result?.InteropResponse != null && result.InteropResponse?.ErrorMessage == null;
668 logger.LogTrace(
"Resetting reboot state...");
669 reattachInformation.RebootState =
RebootState.Normal;
673 public void SetHighPriority() => process.SetHighPriority();
676 public void Suspend() => process.Suspend();
679 public void Resume() => process.Resume();
684 var oldDmb = reattachInformation.Dmb;
685 reattachInformation.Dmb = dmbProvider ??
throw new ArgumentNullException(nameof(dmbProvider));
690 public Task
InstanceRenamed(
string newInstanceName, CancellationToken cancellationToken)
691 => SendCommand(
new TopicParameters(newInstanceName), cancellationToken);
694 public Task UpdateChannels(IEnumerable<ChannelRepresentation> newChannels, CancellationToken cancellationToken)
readonly CancellationTokenSource reattachTopicCts
A CancellationTokenSource used for the topic send operation made on reattaching.
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
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
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
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.
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)
Construct a SessionController
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