/// <inheritdoc />
        public Task Save(ReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async(db) =>
        {
            if (reattachInformation == null)
            {
                throw new ArgumentNullException(nameof(reattachInformation));
            }

            logger.LogDebug("Saving reattach information: {0}...", reattachInformation);

            await db
            .ReattachInformations
            .AsQueryable()
            .Where(x => x.CompileJob.Job.Instance.Id == metadata.Id)
            .DeleteAsync(cancellationToken)
            .ConfigureAwait(false);

            var dbReattachInfo = new Models.ReattachInformation
            {
                AccessIdentifier    = reattachInformation.AccessIdentifier,
                CompileJobId        = reattachInformation.Dmb.CompileJob.Id,
                Port                = reattachInformation.Port,
                ProcessId           = reattachInformation.ProcessId,
                RebootState         = reattachInformation.RebootState,
                LaunchSecurityLevel = reattachInformation.LaunchSecurityLevel
            };

            db.ReattachInformations.Add(dbReattachInfo);
            await db.Save(cancellationToken).ConfigureAwait(false);
        });
        /// <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);
        }
Esempio n. 3
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 <ReattachInformation> Load(CancellationToken cancellationToken)
        {
            Models.ReattachInformation result = null;
            TimeSpan?topicTimeout             = null;
            await databaseContextFactory.UseContext(async (db) =>
            {
                var dbReattachInfos = await db
                                      .ReattachInformations
                                      .AsQueryable()
                                      .Where(x => x.CompileJob.Job.Instance.Id == metadata.Id)
                                      .Include(x => x.CompileJob)
                                      .ToListAsync(cancellationToken).ConfigureAwait(false);
                result = dbReattachInfos.FirstOrDefault();
                if (result == default)
                {
                    return;
                }

                var timeoutMilliseconds = await db
                                          .Instances
                                          .AsQueryable()
                                          .Where(x => x.Id == metadata.Id)
                                          .Select(x => x.DreamDaemonSettings.TopicRequestTimeout)
                                          .FirstOrDefaultAsync(cancellationToken)
                                          .ConfigureAwait(false);

                if (timeoutMilliseconds == default)
                {
                    logger.LogCritical("Missing TopicRequestTimeout!");
                    return;
                }

                topicTimeout = TimeSpan.FromMilliseconds(timeoutMilliseconds.Value);

                bool first = true;
                foreach (var reattachInfo in dbReattachInfos)
                {
                    if (!first)
                    {
                        logger.LogWarning("Killing PID {0} associated with extra reattach information...", reattachInfo.ProcessId);
                        try
                        {
                            using var process = processExecutor.GetProcess(reattachInfo.ProcessId);
                            process.Terminate();
                            await process.Lifetime.ConfigureAwait(false);
                        }
                        catch (Exception ex)
                        {
                            logger.LogWarning(ex, "Failed to kill process!");
                        }
                    }

                    db.ReattachInformations.Remove(reattachInfo);
                    first = false;
                }

                await db.Save(cancellationToken).ConfigureAwait(false);
            }).ConfigureAwait(false);

            if (!topicTimeout.HasValue)
            {
                logger.LogDebug("Reattach information not found!");
                return(null);
            }

            var dmb = await dmbFactory.FromCompileJob(result.CompileJob, cancellationToken).ConfigureAwait(false);

            if (dmb == null)
            {
                logger.LogError("Unable to reattach! Could not load .dmb!");
                return(null);
            }

            var info = new ReattachInformation(
                result,
                dmb,
                topicTimeout.Value);

            logger.LogDebug("Reattach information loaded: {0}", info);

            return(info);
        }