/// <summary> /// Construct a <see cref="SessionController"/> /// </summary> /// <param name="reattachInformation">The value of <see cref="reattachInformation"/></param> /// <param name="process">The value of <see cref="process"/></param> /// <param name="byondLock">The value of <see cref="byondLock"/></param> /// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param> /// <param name="interopContext">The value of <see cref="interopContext"/></param> /// <param name="chat">The value of <see cref="chat"/></param> /// <param name="chatJsonTrackingContext">The value of <see cref="chatJsonTrackingContext"/></param> /// <param name="logger">The value of <see cref="logger"/></param> /// <param name="launchSecurityLevel">The value of <see cref="launchSecurityLevel"/></param> /// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/></param> public SessionController( ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger <SessionController> logger, DreamDaemonSecurity?launchSecurityLevel, uint?startupTimeout) { this.chatJsonTrackingContext = chatJsonTrackingContext; // null valid this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); this.interopContext = interopContext ?? throw new ArgumentNullException(nameof(interopContext)); this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.launchSecurityLevel = launchSecurityLevel; interopContext.RegisterHandler(this); portClosedForReboot = false; disposed = false; apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; rebootTcs = new TaskCompletionSource <object>(); process.Lifetime.ContinueWith(x => chatJsonTrackingContext.Active = false, TaskScheduler.Current); async Task <LaunchResult> GetLaunchResult() { var startTime = DateTimeOffset.Now; Task toAwait = process.Startup; if (startupTimeout.HasValue) { toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime)); } await toAwait.ConfigureAwait(false); var result = new LaunchResult { ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null, StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null }; return(result); } LaunchResult = GetLaunchResult(); logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port); }
/// <summary> /// Construct a <see cref="SessionController"/> /// </summary> /// <param name="reattachInformation">The value of <see cref="reattachInformation"/></param> /// <param name="process">The value of <see cref="process"/></param> /// <param name="byondLock">The value of <see cref="byondLock"/></param> /// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param> /// <param name="bridgeRegistrar">The <see cref="IBridgeRegistrar"/> used to populate <see cref="bridgeRegistration"/>.</param> /// <param name="chat">The value of <see cref="chat"/></param> /// <param name="chatTrackingContext">The value of <see cref="chatTrackingContext"/></param> /// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="SessionController"/>.</param> /// <param name="logger">The value of <see cref="logger"/></param> /// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/></param> /// <param name="reattached">If this is a reattached session.</param> public SessionController( ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, ILogger <SessionController> logger, uint?startupTimeout, bool reattached) { this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext)); bridgeRegistration = bridgeRegistrar?.RegisterHandler(this) ?? throw new ArgumentNullException(nameof(bridgeRegistrar)); this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.chatTrackingContext.SetChannelSink(this); portClosedForReboot = false; disposed = false; apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; rebootTcs = new TaskCompletionSource <object>(); primeTcs = new TaskCompletionSource <object>(); reattachTopicCts = new CancellationTokenSource(); synchronizationLock = new object(); _ = process.Lifetime.ContinueWith( x => { if (!disposed) { reattachTopicCts.Cancel(); } chatTrackingContext.Active = false; }, TaskScheduler.Current); LaunchResult = GetLaunchResult( assemblyInformationProvider, startupTimeout, reattached); logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port); }
/// <inheritdoc /> #pragma warning disable CA1502 // TODO: Decomplexify public async Task HandleInterop(CommCommand command, CancellationToken cancellationToken) { if (command == null) { throw new ArgumentNullException(nameof(command)); } var query = command.Parameters; object content; Action postRespond = null; ushort?overrideResponsePort = null; if (query.TryGetValue(Constants.DMParameterCommand, out var method)) { content = new object(); switch (method) { case Constants.DMCommandChat: try { var message = JsonConvert.DeserializeObject <Response>(command.RawJson, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); if (message.ChannelIds == null) { throw new InvalidOperationException("Missing ChannelIds field!"); } if (message.Message == null) { throw new InvalidOperationException("Missing Message field!"); } await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false); } catch (Exception e) { logger.LogDebug("Exception while decoding chat message! Exception: {0}", e); goto default; } break; case Constants.DMCommandServerPrimed: // currently unused, maybe in the future break; case Constants.DMCommandEndProcess: TerminationWasRequested = true; process.Terminate(); return; case Constants.DMCommandNewPort: lock (this) { if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort)) { /////UHHHH logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); content = new ErrorMessage { Message = "Missing stringified port as data parameter!" }; break; } if (!nextPort.HasValue) { reattachInformation.Port = currentPort; // 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 } else { // nextPort is ready, tell DD to switch to that // if it fails it'll kill itself content = new Dictionary <string, ushort> { { Constants.DMParameterData, nextPort.Value } }; reattachInformation.Port = nextPort.Value; overrideResponsePort = currentPort; nextPort = null; // we'll also get here from SetPort so complete that task var tmpTcs = portAssignmentTcs; portAssignmentTcs = null; if (tmpTcs != null) { postRespond = () => tmpTcs.SetResult(true); } } portClosedForReboot = false; } break; case Constants.DMCommandApiValidate: if (!launchSecurityLevel.HasValue) { logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!"); apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest; content = new ErrorMessage { Message = "Invalid API validation request!" }; break; } if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse <DreamDaemonSecurity>(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel)) { apiValidationStatus = ApiValidationStatus.BadValidationRequest; } else { switch (minimumSecurityLevel) { case DreamDaemonSecurity.Safe: apiValidationStatus = ApiValidationStatus.RequiresSafe; break; case DreamDaemonSecurity.Ultrasafe: apiValidationStatus = ApiValidationStatus.RequiresUltrasafe; break; case DreamDaemonSecurity.Trusted: apiValidationStatus = ApiValidationStatus.RequiresTrusted; break; default: throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!"); } } break; case Constants.DMCommandWorldReboot: if (ClosePortOnReboot) { chatJsonTrackingContext.Active = false; content = new Dictionary <string, int> { { Constants.DMParameterData, 0 } }; portClosedForReboot = true; } var oldTcs = rebootTcs; rebootTcs = new TaskCompletionSource <object>(); postRespond = () => oldTcs.SetResult(null); break; default: content = new ErrorMessage { Message = "Requested command not supported!" }; break; } } else { content = new ErrorMessage { Message = "Missing command parameter!" } }; var json = JsonConvert.SerializeObject(content); 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); if (response != Constants.DMResponseSuccess) { logger.LogWarning("Received error response while responding to interop: {0}", response); } postRespond?.Invoke(); } #pragma warning restore CA1502 /// <summary> /// Throws an <see cref="ObjectDisposedException"/> if <see cref="Dispose(bool)"/> has been called /// </summary> void CheckDisposed() { if (disposed) { throw new ObjectDisposedException(nameof(SessionController)); } }
/// <summary> /// Construct a <see cref="SessionController"/> /// </summary> /// <param name="reattachInformation">The value of <see cref="reattachInformation"/></param> /// <param name="metadata">The owning <see cref="Instance"/>.</param> /// <param name="process">The value of <see cref="process"/></param> /// <param name="byondLock">The value of <see cref="byondLock"/></param> /// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param> /// <param name="bridgeRegistrar">The <see cref="IBridgeRegistrar"/> used to populate <see cref="bridgeRegistration"/>.</param> /// <param name="chat">The value of <see cref="chat"/></param> /// <param name="chatTrackingContext">The value of <see cref="chatTrackingContext"/></param> /// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="SessionController"/>.</param> /// <param name="logger">The value of <see cref="logger"/></param> /// <param name="postLifetimeCallback">The <see cref="Func{TResult}"/> returning a <see cref="Task"/> to be run after the <paramref name="process"/> ends.</param> /// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/></param> /// <param name="reattached">If this is a reattached session.</param> /// <param name="apiValidate">If this is a DMAPI validation session.</param> public SessionController( ReattachInformation reattachInformation, Api.Models.Instance metadata, IProcess process, IByondExecutableLock byondLock, ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, ILogger <SessionController> logger, Func <Task> postLifetimeCallback, uint?startupTimeout, bool reattached, bool apiValidate) { this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext)); if (bridgeRegistrar == null) { throw new ArgumentNullException(nameof(bridgeRegistrar)); } this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); portClosedForReboot = false; disposed = false; apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; rebootTcs = new TaskCompletionSource <object>(); primeTcs = new TaskCompletionSource <object>(); initialBridgeRequestTcs = new TaskCompletionSource <object>(); reattachTopicCts = new CancellationTokenSource(); synchronizationLock = new object(); if (apiValidate || DMApiAvailable) { bridgeRegistration = bridgeRegistrar.RegisterHandler(this); this.chatTrackingContext.SetChannelSink(this); } else { logger.LogTrace( "Not registering session with {0} DMAPI version for interop!", reattachInformation.Dmb.CompileJob.DMApiVersion == null ? "no" : $"incompatible ({reattachInformation.Dmb.CompileJob.DMApiVersion})"); } async Task <int> WrapLifetime() { var exitCode = await process.Lifetime.ConfigureAwait(false); await postLifetimeCallback().ConfigureAwait(false); return(exitCode); } Lifetime = WrapLifetime(); LaunchResult = GetLaunchResult( assemblyInformationProvider, startupTimeout, reattached, apiValidate); logger.LogDebug( "Created session controller. CommsKey: {0}, Port: {1}", reattachInformation.AccessIdentifier, reattachInformation.Port); }
/// <inheritdoc /> public async Task <BridgeResponse> ProcessBridgeRequest(BridgeParameters parameters, CancellationToken cancellationToken) { if (parameters == null) { throw new ArgumentNullException(nameof(parameters)); } var response = new BridgeResponse(); switch (parameters.CommandType) { case BridgeCommandType.ChatSend: if (parameters.ChatMessage == null) { return new BridgeResponse { ErrorMessage = "Missing chatMessage field!" } } ; if (parameters.ChatMessage.ChannelIds == null) { return new BridgeResponse { ErrorMessage = "Missing channelIds field in chatMessage!" } } ; if (parameters.ChatMessage.ChannelIds.Any(channelIdString => !UInt64.TryParse(channelIdString, out var _))) { return new BridgeResponse { ErrorMessage = "Invalid channelIds in chatMessage!" } } ; if (parameters.ChatMessage.Text == null) { return new BridgeResponse { ErrorMessage = "Missing message field in chatMessage!" } } ; await chat.SendMessage( parameters.ChatMessage.Text, parameters.ChatMessage.ChannelIds.Select(UInt64.Parse), cancellationToken).ConfigureAwait(false); break; case BridgeCommandType.Prime: var oldPrimeTcs = primeTcs; primeTcs = new TaskCompletionSource <object>(); oldPrimeTcs.SetResult(null); break; case BridgeCommandType.Kill: logger.LogInformation("Bridge requested process termination!"); TerminationWasRequested = true; process.Terminate(); break; case BridgeCommandType.PortUpdate: lock (synchronizationLock) { if (!parameters.CurrentPort.HasValue) { /////UHHHH logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); return(new BridgeResponse { ErrorMessage = "Missing stringified port as data parameter!" }); } var currentPort = parameters.CurrentPort.Value; if (!nextPort.HasValue) { 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 } else { // nextPort is ready, tell DD to switch to that // if it fails it'll kill itself response.NewPort = nextPort.Value; reattachInformation.Port = nextPort.Value; nextPort = null; // we'll also get here from SetPort so complete that task var tmpTcs = portAssignmentTcs; portAssignmentTcs = null; tmpTcs.SetResult(true); } portClosedForReboot = false; } break; case BridgeCommandType.Startup: apiValidationStatus = ApiValidationStatus.BadValidationRequest; if (parameters.Version == null) { return new BridgeResponse { ErrorMessage = "Missing dmApiVersion field!" } } ; DMApiVersion = parameters.Version; switch (parameters.MinimumSecurityLevel) { case DreamDaemonSecurity.Ultrasafe: apiValidationStatus = ApiValidationStatus.RequiresUltrasafe; break; case DreamDaemonSecurity.Safe: apiValidationStatus = ApiValidationStatus.RequiresSafe; break; case DreamDaemonSecurity.Trusted: apiValidationStatus = ApiValidationStatus.RequiresTrusted; break; case null: return(new BridgeResponse { ErrorMessage = "Missing minimumSecurityLevel field!" }); default: return(new BridgeResponse { ErrorMessage = "Invalid minimumSecurityLevel!" }); } response.RuntimeInformation = reattachInformation.RuntimeInformation; // Load custom commands chatTrackingContext.CustomCommands = parameters.CustomCommands; break; case BridgeCommandType.Reboot: if (ClosePortOnReboot) { chatTrackingContext.Active = false; response.NewPort = 0; portClosedForReboot = true; } var oldRebootTcs = rebootTcs; rebootTcs = new TaskCompletionSource <object>(); oldRebootTcs.SetResult(null); break; case null: response.ErrorMessage = "Missing commandType!"; break; default: response.ErrorMessage = "Requested commandType not supported!"; break; } return(response); }