Exemple #1
0
        /// <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);
        }
Exemple #3
0
        /// <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));
            }
        }
Exemple #4
0
        /// <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);
        }