tgstation-server  4.4.0
The /tg/station 13 server suite
SessionController.cs
Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using Newtonsoft.Json;
4 using Serilog.Context;
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.Linq;
9 using System.Net;
10 using System.Threading;
11 using System.Threading.Tasks;
12 using Tgstation.Server.Api;
21 
22 namespace Tgstation.Server.Host.Components.Session
23 {
26  {
28  public DMApiParameters DMApiParameters => reattachInformation;
29 
32  {
33  get
34  {
35  if (!Lifetime.IsCompleted)
36  throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!");
37  return apiValidationStatus;
38  }
39  }
40 
42  public IDmbProvider Dmb
43  {
44  get
45  {
46  CheckDisposed();
47  return reattachInformation.Dmb;
48  }
49  }
50 
52  public ushort? Port
53  {
54  get
55  {
56  CheckDisposed();
57  if (portClosedForReboot)
58  return null;
59  return reattachInformation.Port;
60  }
61  }
62 
65  {
66  get
67  {
68  CheckDisposed();
69  return reattachInformation.RebootState;
70  }
71  }
72 
74  public Version DMApiVersion { get; private set; }
75 
77  public bool ClosePortOnReboot { get; set; }
78 
80  public bool TerminationWasRequested { get; private set; }
81 
83  public Task<LaunchResult> LaunchResult { get; }
84 
86  public Task<int> Lifetime => process.Lifetime;
87 
89  public Task OnReboot => rebootTcs.Task;
90 
92  public Task OnPrime => primeTcs.Task;
93 
95  public bool DMApiAvailable => reattachInformation.Dmb.CompileJob.DMApiVersion?.Major == DMApiConstants.Version.Major;
96 
101 
105  readonly Api.Models.Instance metadata;
106 
110  readonly CancellationTokenSource reattachTopicCts;
111 
115  readonly ITopicClient byondTopicSender;
116 
121 
125  readonly IProcess process;
126 
131 
136 
140  readonly IChatManager chat;
141 
145  readonly ILogger<SessionController> logger;
146 
150  readonly object synchronizationLock;
151 
155  TaskCompletionSource<bool> portAssignmentTcs;
156 
160  ushort? nextPort;
161 
165  TaskCompletionSource<object> rebootTcs;
166 
170  TaskCompletionSource<object> primeTcs;
171 
176 
180  bool disposed;
181 
186 
190  bool released;
191 
209  ReattachInformation reattachInformation,
210  Api.Models.Instance metadata,
211  IProcess process,
212  IByondExecutableLock byondLock,
213  ITopicClient byondTopicSender,
214  IChatTrackingContext chatTrackingContext,
215  IBridgeRegistrar bridgeRegistrar,
216  IChatManager chat,
217  IAssemblyInformationProvider assemblyInformationProvider,
218  ILogger<SessionController> logger,
219  uint? startupTimeout,
220  bool reattached,
221  bool apiValidate)
222  {
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));
233 
234  if (apiValidate || DMApiAvailable)
235  {
236  bridgeRegistration = bridgeRegistrar.RegisterHandler(this);
237  this.chatTrackingContext.SetChannelSink(this);
238  }
239  else
240  logger.LogTrace(
241  "Not registering session with {0} DMAPI version for interop!",
242  reattachInformation.Dmb.CompileJob.DMApiVersion == null
243  ? "no"
244  : $"incompatible ({reattachInformation.Dmb.CompileJob.DMApiVersion})");
245 
246  portClosedForReboot = false;
247  disposed = false;
248  apiValidationStatus = ApiValidationStatus.NeverValidated;
249  released = false;
250 
251  rebootTcs = new TaskCompletionSource<object>();
252  primeTcs = new TaskCompletionSource<object>();
253  reattachTopicCts = new CancellationTokenSource();
254  synchronizationLock = new object();
255 
256  _ = process.Lifetime.ContinueWith(
257  x =>
258  {
259  lock (synchronizationLock)
260  if (!disposed)
261  reattachTopicCts.Cancel();
262  chatTrackingContext.Active = false;
263  },
264  TaskScheduler.Current);
265 
266  LaunchResult = GetLaunchResult(
267  assemblyInformationProvider,
268  startupTimeout,
269  reattached);
270 
271  logger.LogDebug("Created session controller. CommsKey: {0}, Port: {1}", reattachInformation.AccessIdentifier, Port);
272  }
273 
278 #pragma warning disable CA1821 // Remove empty Finalizers TODO: remove this when https://github.com/dotnet/roslyn-analyzers/issues/1241 is fixed
279  ~SessionController() => Dispose(false);
280 #pragma warning restore CA1821 // Remove empty Finalizers
281 
283  public void Dispose()
284  {
285  Dispose(true);
286  GC.SuppressFinalize(this);
287  }
288 
293  void Dispose(bool disposing)
294  {
295  lock (synchronizationLock)
296  {
297  if (disposed)
298  return;
299  disposed = true;
300  logger.LogTrace("Disposing...");
301  if (disposing)
302  {
303  if (!released)
304  {
305  process.Terminate();
306  byondLock.Dispose();
307  }
308 
309  process.Dispose();
310  bridgeRegistration?.Dispose();
311  reattachInformation.Dmb?.Dispose(); // will be null when released
312  chatTrackingContext.Dispose();
313  reattachTopicCts.Dispose();
314  }
315  else
316  {
317  if (logger != null)
318  logger.LogError("Being disposed via finalizer!");
319  if (!released)
320  if (process != null)
321  process.Terminate();
322  else if (logger != null)
323  logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
324  }
325  }
326  }
327 
335  async Task<LaunchResult> GetLaunchResult(
336  IAssemblyInformationProvider assemblyInformationProvider,
337  uint? startupTimeout,
338  bool reattached)
339  {
340  var startTime = DateTimeOffset.Now;
341  Task toAwait = process.Startup;
342 
343  if (startupTimeout.HasValue)
344  toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime));
345 
346  await toAwait.ConfigureAwait(false);
347 
348  var result = new LaunchResult
349  {
350  ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
351  StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null
352  };
353 
354  logger.LogTrace("Launch result: {0}", result);
355 
356  if (!result.ExitCode.HasValue && reattached && !disposed)
357  {
358  var reattachResponse = await SendCommand(
359  new TopicParameters(
360  assemblyInformationProvider.Version,
361  reattachInformation.RuntimeInformation.ServerPort),
362  reattachTopicCts.Token)
363  .ConfigureAwait(false);
364 
365  if (reattachResponse.InteropResponse?.CustomCommands != null)
366  chatTrackingContext.CustomCommands = reattachResponse.InteropResponse.CustomCommands;
367  else if (reattachResponse.InteropResponse != null)
368  logger.LogWarning(
369  "DMAPI v{0} isn't returning the TGS custom commands list. Functionality added in v5.2.0.",
370  Dmb.CompileJob.DMApiVersion.Semver());
371  }
372 
373  return result;
374  }
375 
377  public async Task<BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
378  {
379  if (parameters == null)
380  throw new ArgumentNullException(nameof(parameters));
381 
382  using (LogContext.PushProperty("Instance", metadata.Id))
383  {
384  logger.LogTrace("Handling bridge request...");
385  var response = new BridgeResponse();
386  switch (parameters.CommandType)
387  {
388  case BridgeCommandType.ChatSend:
389  if (parameters.ChatMessage == null)
390  return new BridgeResponse
391  {
392  ErrorMessage = "Missing chatMessage field!"
393  };
394 
395  if (parameters.ChatMessage.ChannelIds == null)
396  return new BridgeResponse
397  {
398  ErrorMessage = "Missing channelIds field in chatMessage!"
399  };
400 
401  if (parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _)))
402  return new BridgeResponse
403  {
404  ErrorMessage = "Invalid channelIds in chatMessage!"
405  };
406 
407  if (parameters.ChatMessage.Text == null)
408  return new BridgeResponse
409  {
410  ErrorMessage = "Missing message field in chatMessage!"
411  };
412 
413  await chat.SendMessage(
414  parameters.ChatMessage.Text,
415  parameters.ChatMessage.ChannelIds.Select(UInt64.Parse),
416  cancellationToken).ConfigureAwait(false);
417  break;
418  case BridgeCommandType.Prime:
419  var oldPrimeTcs = primeTcs;
420  primeTcs = new TaskCompletionSource<object>();
421  oldPrimeTcs.SetResult(null);
422  break;
423  case BridgeCommandType.Kill:
424  logger.LogInformation("Bridge requested process termination!");
425  TerminationWasRequested = true;
426  process.Terminate();
427  break;
428  case BridgeCommandType.PortUpdate:
429  lock (synchronizationLock)
430  {
431  if (!parameters.CurrentPort.HasValue)
432  {
434  logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
435  return new BridgeResponse
436  {
437  ErrorMessage = "Missing stringified port as data parameter!"
438  };
439  }
440 
441  var currentPort = parameters.CurrentPort.Value;
442  if (!nextPort.HasValue)
443  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
444  else
445  {
446  // nextPort is ready, tell DD to switch to that
447  // if it fails it'll kill itself
448  response.NewPort = nextPort.Value;
449  reattachInformation.Port = nextPort.Value;
450  nextPort = null;
451 
452  // we'll also get here from SetPort so complete that task
453  var tmpTcs = portAssignmentTcs;
454  portAssignmentTcs = null;
455  tmpTcs.SetResult(true);
456  }
457 
458  portClosedForReboot = false;
459  }
460 
461  break;
462  case BridgeCommandType.Startup:
463  apiValidationStatus = ApiValidationStatus.BadValidationRequest;
464  if (parameters.Version == null)
465  return new BridgeResponse
466  {
467  ErrorMessage = "Missing dmApiVersion field!"
468  };
469 
470  DMApiVersion = parameters.Version;
471  if (DMApiVersion.Major != DMApiConstants.Version.Major)
472  {
473  apiValidationStatus = ApiValidationStatus.Incompatible;
474  return new BridgeResponse
475  {
476  ErrorMessage = "Incompatible dmApiVersion!"
477  };
478  }
479 
480  switch (parameters.MinimumSecurityLevel)
481  {
482  case DreamDaemonSecurity.Ultrasafe:
483  apiValidationStatus = ApiValidationStatus.RequiresUltrasafe;
484  break;
485  case DreamDaemonSecurity.Safe:
486  apiValidationStatus = ApiValidationStatus.RequiresSafe;
487  break;
488  case DreamDaemonSecurity.Trusted:
489  apiValidationStatus = ApiValidationStatus.RequiresTrusted;
490  break;
491  case null:
492  return new BridgeResponse
493  {
494  ErrorMessage = "Missing minimumSecurityLevel field!"
495  };
496  default:
497  return new BridgeResponse
498  {
499  ErrorMessage = "Invalid minimumSecurityLevel!"
500  };
501  }
502 
503  response.RuntimeInformation = reattachInformation.RuntimeInformation;
504 
505  // Load custom commands
506  chatTrackingContext.CustomCommands = parameters.CustomCommands;
507  break;
508  case BridgeCommandType.Reboot:
509  if (ClosePortOnReboot)
510  {
511  chatTrackingContext.Active = false;
512  response.NewPort = 0;
513  portClosedForReboot = true;
514  }
515 
516  var oldRebootTcs = rebootTcs;
517  rebootTcs = new TaskCompletionSource<object>();
518  oldRebootTcs.SetResult(null);
519  break;
520  case null:
521  response.ErrorMessage = "Missing commandType!";
522  break;
523  default:
524  response.ErrorMessage = "Requested commandType not supported!";
525  break;
526  }
527 
528  return response;
529  }
530  }
531 
536  {
537  if (disposed)
538  throw new ObjectDisposedException(nameof(SessionController));
539  }
540 
542  public void EnableCustomChatCommands() => chatTrackingContext.Active = DMApiAvailable;
543 
546  {
547  CheckDisposed();
548 
549  // we still don't want to dispose the dmb yet, even though we're keeping it alive
550  var tmpProvider = reattachInformation.Dmb;
551  reattachInformation.Dmb = null;
552  released = true;
553  Dispose();
554  byondLock.DoNotDeleteThisSession();
555  tmpProvider.KeepAlive();
556  reattachInformation.Dmb = tmpProvider;
557  return reattachInformation;
558  }
559 
561  public async Task<CombinedTopicResponse> SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
562  {
563  if (parameters == null)
564  throw new ArgumentNullException(nameof(parameters));
565 
566  if (Lifetime.IsCompleted)
567  {
568  logger.LogWarning(
569  "Attempted to send a command to an inactive SessionController: {0}",
570  parameters.CommandType);
571  return null;
572  }
573 
574  if (!DMApiAvailable)
575  {
576  logger.LogTrace("Not sending topic request {0} to server without/with incompatible DMAPI!", parameters.CommandType);
577  return null;
578  }
579 
580  parameters.AccessIdentifier = reattachInformation.AccessIdentifier;
581 
582  var json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings);
583  logger.LogTrace("Topic request: {0}", json);
584  try
585  {
586  var commandString = String.Format(CultureInfo.InvariantCulture,
587  "?{0}={1}",
588  byondTopicSender.SanitizeString(DMApiConstants.TopicData),
589  byondTopicSender.SanitizeString(json));
590 
591  var targetPort = reattachInformation.Port;
592 
593  var topicResponse = await byondTopicSender.SendTopic(
594  new IPEndPoint(IPAddress.Loopback, targetPort),
595  commandString,
596  cancellationToken).ConfigureAwait(false);
597 
598  var topicReturn = topicResponse.StringData;
599 
600  Interop.Topic.TopicResponse interopResponse = null;
601  if (topicReturn != null)
602  try
603  {
604  interopResponse = JsonConvert.DeserializeObject<Interop.Topic.TopicResponse>(topicReturn, DMApiConstants.SerializerSettings);
605  if (interopResponse.ErrorMessage != null)
606  {
607  logger.LogWarning("Errored topic response for command {0}: {1}", parameters.CommandType, interopResponse.ErrorMessage);
608  }
609 
610  logger.LogTrace("Interop response: {0}", topicReturn);
611  }
612  catch
613  {
614  logger.LogWarning("Invalid interop response: {0}", topicReturn);
615  }
616 
617  return new CombinedTopicResponse(topicResponse, interopResponse);
618  }
619  catch (OperationCanceledException)
620  {
621  logger.LogTrace(
622  "Topic request {0}!",
623  cancellationToken.IsCancellationRequested
624  ? "aborted"
625  : "timed out");
626  cancellationToken.ThrowIfCancellationRequested();
627  }
628  catch (Exception e)
629  {
630  logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, e);
631  }
632 
633  return null;
634  }
635 
637  public Task<bool> SetPort(ushort port, CancellationToken cancellationToken)
638  {
639  CheckDisposed();
640 
641  if (port == 0)
642  throw new ArgumentOutOfRangeException(nameof(port), port, "port must not be zero!");
643 
644  async Task<bool> ImmediateTopicPortChange()
645  {
646  var commandResult = await SendCommand(
647  new TopicParameters(port),
648  cancellationToken)
649  .ConfigureAwait(false);
650 
651  if (commandResult.InteropResponse?.ErrorMessage != null)
652  return false;
653 
654  reattachInformation.Port = port;
655  return true;
656  }
657 
658  lock (synchronizationLock)
659  if (portClosedForReboot)
660  {
661  if (portAssignmentTcs != null)
662  throw new InvalidOperationException("A port change operation is already in progress!");
663  nextPort = port;
664  portAssignmentTcs = new TaskCompletionSource<bool>();
665  return portAssignmentTcs.Task;
666  }
667  else
668  return ImmediateTopicPortChange();
669  }
670 
672  public async Task<bool> SetRebootState(RebootState newRebootState, CancellationToken cancellationToken)
673  {
674  if (RebootState == newRebootState)
675  return true;
676 
677  logger.LogTrace("Changing reboot state to {0}", newRebootState);
678 
679  reattachInformation.RebootState = newRebootState;
680  var result = await SendCommand(
681  new TopicParameters(newRebootState),
682  cancellationToken)
683  .ConfigureAwait(false);
684 
685  return result?.InteropResponse != null && result.InteropResponse?.ErrorMessage == null;
686  }
687 
689  public void ResetRebootState()
690  {
691  CheckDisposed();
692  logger.LogTrace("Resetting reboot state...");
693  reattachInformation.RebootState = RebootState.Normal;
694  }
695 
697  public void SetHighPriority() => process.SetHighPriority();
698 
700  public void Suspend() => process.Suspend();
701 
703  public void Resume() => process.Resume();
704 
706  public void ReplaceDmbProvider(IDmbProvider dmbProvider)
707  {
708  var oldDmb = reattachInformation.Dmb;
709  reattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
710  oldDmb.Dispose();
711  }
712 
714  public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken)
715  => SendCommand(new TopicParameters(newInstanceName), cancellationToken);
716 
718  public Task UpdateChannels(IEnumerable<ChannelRepresentation> newChannels, CancellationToken cancellationToken)
719  => SendCommand(
720  new TopicParameters(
721  new ChatUpdate(newChannels)),
722  cancellationToken);
723 
725  public Task CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken);
726  }
727 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:24
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
Definition: IProcessBase.cs:15
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.
void CheckDisposed()
Throws an ObjectDisposedException if Dispose(bool) has been called
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.
Parameters necessary for duplicating a ISessionController session
async Task< CombinedTopicResponse > SendCommand(TopicParameters parameters, CancellationToken cancellationToken)
Sends a command to DreamDaemon through /world/Topic()
IBridgeRegistration RegisterHandler(IBridgeHandler bridgeHandler)
Register a given bridgeHandler .
bool portClosedForReboot
If we know DreamDaemon currently has it&#39;s port closed
virtual Version DMApiVersion
The DMAPI Version.
Definition: CompileJob.cs:39
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.
Definition: IChannelSink.cs:10
Constants used for communication with the DMAPI
Represents the result of trying to start a DD process
Definition: LaunchResult.cs:9
readonly IBridgeRegistration bridgeRegistration
The IBridgeRegistration for the SessionController
bool released
If process should be kept alive instead
For managing connected chat services
Definition: IChatManager.cs:13
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
Definition: RebootState.cs:6
async Task< BridgeResponse > ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken)
Handle a set of bridge parameters .
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon
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
Definition: IProcess.cs:9
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...
Definition: ChatMessage.cs:18
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...
Definition: Byond.cs:9
readonly ITopicClient byondTopicSender
The ITopicClient for the SessionController
Represents a tracking of dynamic chat json files
DreamDaemonSecurity
DreamDaemon&#39;s security level
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
readonly IChatTrackingContext chatTrackingContext
The IChatTrackingContext for the SessionController
void SetChannelSink(IChannelSink channelSink)
Sets the channelSink for the IChatTrackingContext.
Represents an error message returned by the server
Definition: ErrorMessage.cs:9
Represents an update of ChannelRepresentations.
Definition: ChatUpdate.cs:11
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&#39;s primed.
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