1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
4 using Newtonsoft.Json.Serialization;
6 using System.Collections.Generic;
10 using System.Threading.Tasks;
28 return reattachInformation.IsPrimary;
37 if (!Lifetime.IsCompleted)
38 throw new InvalidOperationException(
"ApiValidated cannot be checked while Lifetime is incomplete!");
39 return apiValidationStatus;
49 return reattachInformation.Dmb;
59 if (portClosedForReboot)
61 return reattachInformation.Port;
71 return reattachInformation.RebootState;
76 public bool ClosePortOnReboot {
get;
set; }
79 public bool TerminationWasRequested {
get;
private set; }
85 public Task<int> Lifetime => process.Lifetime;
88 public Task OnReboot => rebootTcs.Task;
128 readonly ILogger<SessionController>
logger;
184 this.chatJsonTrackingContext = chatJsonTrackingContext;
185 this.reattachInformation = reattachInformation ??
throw new ArgumentNullException(nameof(reattachInformation));
186 this.byondTopicSender = byondTopicSender ??
throw new ArgumentNullException(nameof(byondTopicSender));
187 this.process = process ??
throw new ArgumentNullException(nameof(process));
188 this.byondLock = byondLock ??
throw new ArgumentNullException(nameof(byondLock));
189 this.interopContext = interopContext ??
throw new ArgumentNullException(nameof(interopContext));
190 this.chat = chat ??
throw new ArgumentNullException(nameof(chat));
191 this.logger = logger ??
throw new ArgumentNullException(nameof(logger));
193 this.launchSecurityLevel = launchSecurityLevel;
197 portClosedForReboot =
false;
202 rebootTcs =
new TaskCompletionSource<object>();
204 process.
Lifetime.ContinueWith(x => chatJsonTrackingContext.
Active =
false, TaskScheduler.Current);
206 async Task<LaunchResult> GetLaunchResult()
208 var startTime = DateTimeOffset.Now;
209 Task toAwait = process.
Startup;
211 if (startupTimeout.HasValue)
212 toAwait = Task.WhenAny(process.
Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
214 await toAwait.ConfigureAwait(
false);
218 ExitCode = process.
Lifetime.IsCompleted ? (
int?)await process.
Lifetime.ConfigureAwait(
false) : null,
219 StartupTime = process.
Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
225 logger.LogDebug(
"Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.
AccessIdentifier, Port);
232 #pragma warning disable CA1821 // Remove empty Finalizers //TODO remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed 234 #pragma warning restore CA1821 // Remove empty Finalizers 240 GC.SuppressFinalize(
this);
258 interopContext.Dispose();
260 chatJsonTrackingContext.Dispose();
266 logger.LogError(
"Being disposed via finalizer!");
270 else if (logger != null)
271 logger.LogCritical(
"Unable to terminate active DreamDaemon session due to finalizer ordering!");
280 throw new ArgumentNullException(nameof(command));
285 Action postRespond = null;
286 ushort? overrideResponsePort = null;
289 content =
new object();
295 var message = JsonConvert.DeserializeObject<
Response>(command.
RawJson,
new JsonSerializerSettings
297 ContractResolver =
new CamelCasePropertyNamesContractResolver()
299 if (message.ChannelIds == null)
300 throw new InvalidOperationException(
"Missing ChannelIds field!");
301 if (message.Message == null)
302 throw new InvalidOperationException(
"Missing Message field!");
303 await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(
false);
307 logger.LogDebug(
"Exception while decoding chat message! Exception: {0}", e);
315 TerminationWasRequested =
true;
321 if (!query.TryGetValue(
Constants.
DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as
string, out var currentPort))
324 logger.LogWarning(
"DreamDaemon sent new port command without providing it's own!");
325 content =
new ErrorMessage { Message =
"Missing stringified port as data parameter!" };
329 if (!nextPort.HasValue)
331 reattachInformation.Port = currentPort;
337 reattachInformation.Port = nextPort.Value;
338 overrideResponsePort = currentPort;
342 var tmpTcs = portAssignmentTcs;
343 portAssignmentTcs = null;
345 postRespond = () => tmpTcs.SetResult(
true);
348 portClosedForReboot =
false;
352 if (!launchSecurityLevel.HasValue)
354 logger.LogWarning(
"DreamDaemon requested API validation but no intial security level was passed to the session controller!");
356 content =
new ErrorMessage { Message =
"Invalid API validation request!" };
362 switch (minimumSecurityLevel)
374 throw new InvalidOperationException(
"Enum.TryParse failed to validate the DreamDaemonSecurity range!");
378 if (ClosePortOnReboot)
380 chatJsonTrackingContext.Active =
false;
382 portClosedForReboot =
true;
384 var oldTcs = rebootTcs;
385 rebootTcs =
new TaskCompletionSource<object>();
386 postRespond = () => oldTcs.SetResult(null);
389 content =
new ErrorMessage { Message =
"Requested command not supported!" };
394 content =
new ErrorMessage { Message =
"Missing command parameter!" };
396 var json = JsonConvert.SerializeObject(content);
397 var response = await SendCommand(String.Format(CultureInfo.InvariantCulture,
"{0}&{1}={2}", byondTopicSender.SanitizeString(
Constants.
DMTopicInteropResponse), byondTopicSender.SanitizeString(
Constants.
DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(
false);
400 logger.LogWarning(
"Received error response while responding to interop: {0}", response);
402 postRespond?.Invoke();
415 public void EnableCustomChatCommands() => chatJsonTrackingContext.Active =
true;
422 var tmpProvider = reattachInformation.Dmb;
423 reattachInformation.Dmb = null;
426 byondLock.DoNotDeleteThisSession();
427 tmpProvider.KeepAlive();
428 reattachInformation.Dmb = tmpProvider;
429 return reattachInformation;
433 public Task<string> SendCommand(
string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken);
435 async Task<string>
SendCommand(
string command, ushort? overridePort, CancellationToken cancellationToken)
439 var commandString = String.Format(CultureInfo.InvariantCulture,
442 byondTopicSender.SanitizeString(reattachInformation.AccessIdentifier),
447 var targetPort = overridePort ?? reattachInformation.Port;
448 logger.LogTrace(
"Export to :{0}. Query: {1}", targetPort, commandString);
450 return await byondTopicSender.SendTopic(
451 new IPEndPoint(IPAddress.Loopback, targetPort),
453 cancellationToken).ConfigureAwait(
false);
455 catch (OperationCanceledException)
461 logger.LogInformation(
"Send command exception:{0}{1}", Environment.NewLine, e.Message);
467 public Task<bool>
SetPort(ushort port, CancellationToken cancellationToken)
472 throw new ArgumentOutOfRangeException(nameof(port), port,
"port must not be zero!");
474 async Task<bool> ImmediateTopicPortChange()
476 var commandResult = await SendCommand(String.Format(CultureInfo.InvariantCulture,
"{0}&{1}={2}", byondTopicSender.SanitizeString(
Constants.
DMTopicChangePort), byondTopicSender.SanitizeString(
Constants.
DMParameterData), byondTopicSender.SanitizeString(port.ToString(CultureInfo.InvariantCulture))), cancellationToken).ConfigureAwait(
false);
480 logger.LogWarning(
"Failed port change! DD says: {0}", commandResult);
484 reattachInformation.Port = port;
489 if (portClosedForReboot)
491 if (portAssignmentTcs != null)
492 throw new InvalidOperationException(
"A port change operation is already in progress!");
494 portAssignmentTcs =
new TaskCompletionSource<bool>();
495 return portAssignmentTcs.Task;
498 return ImmediateTopicPortChange();
506 reattachInformation.RebootState = newRebootState;
514 reattachInformation.RebootState =
RebootState.Normal;
518 public void SetHighPriority() => process.SetHighPriority();
bool disposed
If the SessionController has been disposed
const string DMCommandWorldReboot
readonly ICommContext interopContext
The ICommContext for the SessionController
Task< int > Lifetime
The Task<TResult> resulting in the exit code of the process
IReadOnlyDictionary< string, object > Parameters
The dictionary of the CommCommand
Represents the result of trying to start a DD process
readonly ReattachInformation reattachInformation
The up to date ReattachInformation
Task< bool > SetPort(ushort port, CancellationToken cancellationToken)
Causes the world to start listening on a newPort
Use server authentication
readonly IProcess process
The IProcess for the SessionController
ReattachInformation Release()
Releases the IProcess without terminating it. Also calls System.IDisposable.Dispose ...
Represents a registration of an interop session
readonly IJsonTrackingContext chatJsonTrackingContext
The IJsonTrackingContext for the SessionController
async Task< bool > SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
Attempts to change the current RebootState to newRebootState
bool Active
If the IJsonTrackingContext should be used for GetCustomCommands(CancellationToken) ...
readonly IByondExecutableLock byondLock
The IByondExecutableLock for the SessionController
void Dispose(bool disposing)
readonly IChat chat
The IChat for the SessionController
Task Startup
The Task representing the time until the IProcess becomes "idle"
For managing connected chat services
void RegisterHandler(ICommHandler handler)
Register a handler with the ICommContext
SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger< SessionController > logger, DreamDaemonSecurity?launchSecurityLevel, uint?startupTimeout)
Construct a SessionController
Handles communication with a DreamDaemon IProcess
string RawJson
The raw JSON of the CommCommand
readonly DreamDaemonSecurity launchSecurityLevel
The DreamDaemonSecurity level the process was launched with
TaskCompletionSource< bool > portAssignmentTcs
The TaskCompletionSource<TResult> SetPort(ushort, CancellationToken) waits on when DreamDaemon curren...
const string DMCommandEndProcess
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
const string DMCommandApiValidate
async Task< string > SendCommand(string command, ushort?overridePort, CancellationToken cancellationToken)
const string DMTopicChangePort
const string DMTopicInteropResponse
ushort nextPort
The port to assign DreamDaemon when it queries for it
Represents a command from DD
readonly ILogger< SessionController > logger
The ILogger for the SessionController
TaskCompletionSource< object > rebootTcs
The TaskCompletionSource<TResult> that completes when DD tells us about a reboot
const string DMCommandServerPrimed
Abstraction over a System.Diagnostics.Process
Represents a tracking of dynamic chat json files
const string DMParameterData
const string DMResponseSuccess
Represents usage of the two primary BYOND server executables
const string DMParameterCommand
ApiValidationStatus apiValidationStatus
The ApiValidationStatus for the SessionController
const string DMCommandNewPort
void ResetRebootState()
Changes RebootState to Components.Watchdog.RebootState.Normal without telling the DMAPI ...
Represents a BYOND installation
const string DMInteropAccessIdentifier
DreamDaemonSecurity
DreamDaemon's security level
Provides absolute paths to the latest compiled .dmbs
bool portClosedForReboot
If we know DreamDaemon currently has it's port closed
const string DMTopicChangeReboot
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the SessionController
const string DMCommandChat
async Task HandleInterop(CommCommand command, CancellationToken cancellationToken)
Handle a command
Represents an error message returned by the server
RebootState
Represents the action to take when /world/Reboot() is called
ApiValidationStatus
Status of DMAPI validation
bool released
If process should be kept alive instead
Represents a chat message requested by DD