Beispiel #1
0
        internal void DistributedService
        (
            [Description("Cache name")] string cacheName,
            [Description("Cache root path")] string cachePath,
            [DefaultValue(ServiceConfiguration.GrpcDisabledPort), Description(GrpcPortDescription)] int grpcPort,
            [Description("Name of the memory mapped file used to share GRPC port. 'CASaaS GRPC port' if not specified.")] string grpcPortFileName,
            [DefaultValue(null), Description("Writable directory for service operations (use CWD if null)")] string dataRootPath,
            [DefaultValue(null), Description("Identifier for the stamp this service will run as")] string stampId,
            [DefaultValue(null), Description("Identifier for the ring this service will run as")] string ringId,
            [DefaultValue(Constants.OneMB), Description("Max size quota in MB")] int maxSizeQuotaMB,
            [DefaultValue(false), Description("Whether or not GRPC is used for file copies")] bool useDistributedGrpc,
            [DefaultValue(false), Description("Whether or not GZip is used for GRPC file copies")] bool useCompressionForCopies,
            [DefaultValue(null), Description("Buffer size for streaming GRPC copies")] int?bufferSizeForGrpcCopies
        )
        {
            Initialize();

            try
            {
                var cancellationTokenSource = new CancellationTokenSource();
                Console.CancelKeyPress += (sender, args) =>
                {
                    cancellationTokenSource.Cancel();
                };

                var host = new HostInfo(stampId, ringId, new List <string>());

                if (grpcPort == 0)
                {
                    grpcPort = Helpers.GetGrpcPortFromFile(_logger, grpcPortFileName);
                }

                var arguments = CreateDistributedCacheServiceArguments(
                    copier: useDistributedGrpc ? new GrpcFileCopier(new Interfaces.Tracing.Context(_logger), grpcPort, useCompressionForCopies) : (IAbsolutePathFileCopier) new DistributedCopier(),
                    pathTransformer: useDistributedGrpc ? new GrpcDistributedPathTransformer() : (IAbsolutePathTransformer) new DistributedPathTransformer(),
                    host: host,
                    cacheName: cacheName,
                    cacheRootPath: cachePath,
                    grpcPort: (uint)grpcPort,
                    maxSizeQuotaMB: maxSizeQuotaMB,
                    dataRootPath: dataRootPath,
                    ct: cancellationTokenSource.Token,
                    bufferSizeForGrpcCopies: bufferSizeForGrpcCopies);

                DistributedCacheServiceFacade.RunAsync(arguments).GetAwaiter().GetResult();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
        internal void DistributedService
        (
            [Description("Cache name")] string cacheName,
            [Description("Cache root path")] string cachePath,
            [DefaultValue(ServiceConfiguration.GrpcDisabledPort), Description(GrpcPortDescription)] uint grpcPort,
            [DefaultValue(null), Description("Writable directory for service operations (use CWD if null)")] string dataRootPath,
            [DefaultValue(null), Description("Identifier for the stamp this service will run as")] string stampId,
            [DefaultValue(null), Description("Identifier for the ring this service will run as")] string ringId,
            [DefaultValue(Constants.OneMB), Description("Max size quota in MB")] int maxSizeQuotaMB
        )
        {
            Initialize();

            try
            {
                var cancellationTokenSource = new CancellationTokenSource();
                Console.CancelKeyPress += (sender, args) =>
                {
                    cancellationTokenSource.Cancel();
                };

                var copier          = new DistributedCopier();
                var pathTransformer = new DistributedPathTransformer();
                var host            = new HostInfo(stampId, ringId, new List <string>());

                var arguments = CreateDistributedCacheServiceArguments(
                    copier: copier,
                    pathTransformer: pathTransformer,
                    host: host,
                    cacheName: cacheName,
                    cacheRootPath: cachePath,
                    grpcPort: grpcPort,
                    maxSizeQuotaMB: maxSizeQuotaMB,
                    dataRootPath: dataRootPath,
                    ct: cancellationTokenSource.Token);

                DistributedCacheServiceFacade.RunAsync(arguments).GetAwaiter().GetResult();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
            public TestCacheServerWrapper(TestHost testHost, DistributedCacheServiceArguments arguments)
            {
                string[] commandLineArguments = CreateCommandLine(arguments);

                var hostParameters = HostParameters.FromEnvironment();
                var hostInfo       = new HostInfo(hostParameters.Stamp, hostParameters.Ring, new List <string>());

                var serviceHost = new CacheServiceStartup.ServiceHost(
                    new string[0],
                    arguments.Configuration,
                    hostParameters,
                    new Context(TestGlobal.Logger));

                Host = new TestServiceHost(testHost, serviceHost);

                var _ = arguments.Cancellation.Register(() => Host.StartupCompletedSignal.TrySetCanceled());

                RunInBackground("RunCacheService", async context =>
                {
                    using (var cts = CancellationTokenSource.CreateLinkedTokenSource(context.Token, arguments.Cancellation))
                    {
                        arguments = arguments with
                        {
                            Host         = Host,
                            Cancellation = cts.Token
                        };

                        await DistributedCacheServiceFacade.RunAsync(arguments);

                        Assert.True(cts.IsCancellationRequested, "Cache service task shutdown prematurely");
                    }

                    return(BoolResult.Success);
                },
                                fireAndForget: false);
            }
Beispiel #4
0
        internal void DistributedService
            (
            [Description("Path to DistributedContentSettings file")] string settingsPath,
            [Description("Cache name")] string cacheName,
            [Description("Cache root path")] string cachePath,
            [DefaultValue((int)ServiceConfiguration.GrpcDisabledPort), Description(GrpcPortDescription)] int grpcPort,
            [Description("Name of the memory mapped file used to share GRPC port. 'CASaaS GRPC port' if not specified.")] string grpcPortFileName,
            [DefaultValue(null), Description("Writable directory for service operations (use CWD if null)")] string dataRootPath,
            [DefaultValue(null), Description("Identifier for the stamp this service will run as")] string stampId,
            [DefaultValue(null), Description("Identifier for the ring this service will run as")] string ringId,
            [DefaultValue(Constants.OneMB), Description("Max size quota in MB")] int maxSizeQuotaMB,
            [DefaultValue(false)] bool debug,
            [DefaultValue(false), Description("Whether or not GRPC is used for file copies")] bool useDistributedGrpc,
            [DefaultValue(false), Description("Whether or not GZip is used for GRPC file copies")] bool useCompressionForCopies,
            [DefaultValue(null), Description("Buffer size for streaming GRPC copies")] int? bufferSizeForGrpcCopies,
            [DefaultValue(null), Description("Files greater than this size are compressed if compression is used")] int? gzipBarrierSizeForGrpcCopies
            )
        {
            Initialize();

            if (debug)
            {
                System.Diagnostics.Debugger.Launch();
            }

            try
            {
                Validate();

                var dcs = JsonConvert.DeserializeObject<DistributedContentSettings>(File.ReadAllText(settingsPath));

                var host = new HostInfo(stampId, ringId, new List<string>());

                if (grpcPort == 0)
                {
                    grpcPort = Helpers.GetGrpcPortFromFile(_logger, grpcPortFileName);
                }

                var arguments = CreateDistributedCacheServiceArguments(
                    copier: useDistributedGrpc
                        ? new GrpcFileCopier(
                            context: new Interfaces.Tracing.Context(_logger),
                            grpcPort: grpcPort,
                            maxGrpcClientCount: dcs.MaxGrpcClientCount,
                            maxGrpcClientAgeMinutes: dcs.MaxGrpcClientAgeMinutes,
                            grpcClientCleanupDelayMinutes: dcs.GrpcClientCleanupDelayMinutes,
                            useCompression: useCompressionForCopies,
                            bufferSize: bufferSizeForGrpcCopies)
                        : (IAbsolutePathFileCopier)new DistributedCopier(),
                    pathTransformer: useDistributedGrpc ? new GrpcDistributedPathTransformer() : (IAbsolutePathTransformer)new DistributedPathTransformer(),
                    dcs: dcs,
                    host: host,
                    cacheName: cacheName,
                    cacheRootPath: cachePath,
                    grpcPort: (uint)grpcPort,
                    maxSizeQuotaMB: maxSizeQuotaMB,
                    dataRootPath: dataRootPath,
                    ct: _cancellationToken,
                    bufferSizeForGrpcCopies: bufferSizeForGrpcCopies,
                    gzipBarrierSizeForGrpcCopies: gzipBarrierSizeForGrpcCopies);

                DistributedCacheServiceFacade.RunAsync(arguments).GetAwaiter().GetResult();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
Beispiel #5
0
        internal void DistributedService
        (
            [Description("Path to DistributedContentSettings file")] string settingsPath,
            [Description("Cache root path")] string cachePath,
            [DefaultValue((int)ServiceConfiguration.GrpcDisabledPort), Description(GrpcPortDescription)] int grpcPort,
            [Description("Name of the memory mapped file used to share GRPC port. 'CASaaS GRPC port' if not specified.")] string grpcPortFileName,
            [DefaultValue(null), Description("Writable directory for service operations (use CWD if null)")] string dataRootPath,
            [DefaultValue(null), Description("Identifier for the stamp this service will run as")] string stampId,
            [DefaultValue(null), Description("Identifier for the ring this service will run as")] string ringId,
            [DefaultValue(Constants.OneMB), Description("Max size quota in MB")] int maxSizeQuotaMB,
            [DefaultValue(false)] bool debug,
            [DefaultValue(false), Description("Whether or not GRPC is used for file copies")] bool useDistributedGrpc,
            [DefaultValue(false), Description("Whether or not GZip is used for GRPC file copies")] bool useCompressionForCopies,
            [DefaultValue(null), Description("Buffer size for streaming GRPC copies")] int?bufferSizeForGrpcCopies,
            [DefaultValue(null), Description("Files greater than this size are compressed if compression is used")] int?gzipBarrierSizeForGrpcCopies,
            [DefaultValue(null), Description("nLog configuration path. If empty, it is disabled")] string nLogConfigurationPath,
            [DefaultValue(null), Description("Whether to use Azure Blob logging or not")] string nLogToBlobStorageSecretName,
            [DefaultValue(null), Description("If using Azure Blob logging, where to temporarily store logs")] string nLogToBlobStorageWorkspacePath
        )
        {
            // We don't actually support the cache name being anything different than this, so there is no point in
            // allowing it.
            var cacheName = "Default";

            Initialize();

            if (debug)
            {
                System.Diagnostics.Debugger.Launch();
            }

            try
            {
                Validate();

                var dcs = JsonConvert.DeserializeObject <DistributedContentSettings>(File.ReadAllText(settingsPath));

                var host = new HostInfo(stampId, ringId, new List <string>());

                if (grpcPort == 0)
                {
                    grpcPort = Helpers.GetGrpcPortFromFile(_logger, grpcPortFileName);
                }

                // We don't have to dispose the copier here. RunAsync will take care of that.
                var grpcCopier = new GrpcFileCopier(
                    context: new Interfaces.Tracing.Context(_logger),
                    grpcPort: grpcPort,
                    maxGrpcClientCount: dcs.MaxGrpcClientCount,
                    maxGrpcClientAgeMinutes: dcs.MaxGrpcClientAgeMinutes,
                    useCompression: useCompressionForCopies);

                var copier = useDistributedGrpc
                        ? grpcCopier
                        : (IAbsolutePathFileCopier) new DistributedCopier();

                LoggingSettings loggingSettings = null;
                if (!string.IsNullOrEmpty(nLogConfigurationPath))
                {
                    loggingSettings = new LoggingSettings()
                    {
                        NLogConfigurationPath = nLogConfigurationPath,
                        Configuration         = new AzureBlobStorageLogPublicConfiguration()
                        {
                            SecretName          = nLogToBlobStorageSecretName,
                            WorkspaceFolderPath = nLogToBlobStorageWorkspacePath,
                        }
                    };
                }

                var arguments = CreateDistributedCacheServiceArguments(
                    copier: copier,
                    pathTransformer: useDistributedGrpc ? new GrpcDistributedPathTransformer(_logger) : (IAbsolutePathTransformer) new DistributedPathTransformer(),
                    copyRequester: grpcCopier,
                    dcs: dcs,
                    host: host,
                    cacheName: cacheName,
                    cacheRootPath: cachePath,
                    grpcPort: (uint)grpcPort,
                    maxSizeQuotaMB: maxSizeQuotaMB,
                    dataRootPath: dataRootPath,
                    ct: _cancellationToken,
                    bufferSizeForGrpcCopies: bufferSizeForGrpcCopies,
                    gzipBarrierSizeForGrpcCopies: gzipBarrierSizeForGrpcCopies,
                    loggingSettings: loggingSettings,
                    telemetryFieldsProvider: new TelemetryFieldsProvider(ringId, stampId));

                DistributedCacheServiceFacade.RunAsync(arguments).GetAwaiter().GetResult();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }
Beispiel #6
0
        internal void Service
        (
            [Description("Cache names")] string[] names,
            [Description("Cache root paths")] string[] paths,
            [DefaultValue(DefaultMaxConnections), Description(MaxConnectionsDescription)] uint maxConnections,
            [DefaultValue(DefaultGracefulShutdownSeconds), Description(GracefulShutdownSecondsDescription)] uint gracefulShutdownSeconds,
            [DefaultValue(ServiceConfiguration.GrpcDisabledPort), Description(GrpcPortDescription)] int grpcPort,
            [Description("Name of the memory mapped file used to share GRPC port. 'CASaaS GRPC port' if not specified.")] string grpcPortFileName,
            [DefaultValue(null), Description("Writable directory for service operations (use CWD if null)")] string dataRootPath,
            [DefaultValue(null), Description("Duration of inactivity after which a session will be timed out.")] double?unusedSessionTimeoutSeconds,
            [DefaultValue(null), Description("Duration of inactivity after which a session with a heartbeat will be timed out.")] double?unusedSessionHeartbeatTimeoutSeconds,
            [DefaultValue(false), Description("Stop running service")] bool stop,
            [DefaultValue(Constants.OneMB), Description("Max size quota in MB")] int maxSizeQuotaMB
        )
        {
            Initialize();

            if (stop)
            {
                IpcUtilities.SetShutdown(_scenario);

                return;
            }

            if (names == null || paths == null)
            {
                throw new CacheException("At least one cache name/path is required.");
            }

            if (names.Length != paths.Length)
            {
                throw new CacheException("Mismatching lengths of names/paths arguments.");
            }

            var caches = new Dictionary <string, string>();

            for (var i = 0; i < names.Length; i++)
            {
                caches.Add(names[i], paths[i]);
            }

            var serverDataRootPath = !string.IsNullOrWhiteSpace(dataRootPath)
                ? new AbsolutePath(dataRootPath)
                : new AbsolutePath(Environment.CurrentDirectory);

            var cancellationTokenSource = new CancellationTokenSource();

#if !FEATURE_CORECLR
            var configuration = new ServiceConfiguration(caches, serverDataRootPath, maxConnections, gracefulShutdownSeconds, grpcPort, grpcPortFileName);
            if (!configuration.IsValid)
            {
                throw new CacheException($"Invalid service configuration, error=[{configuration.Error}]");
            }

            var localContentServerConfiguration = new LocalServerConfiguration(configuration);
            if (unusedSessionTimeoutSeconds != null)
            {
                localContentServerConfiguration.UnusedSessionTimeout = TimeSpan.FromSeconds(unusedSessionTimeoutSeconds.Value);
            }

            if (unusedSessionHeartbeatTimeoutSeconds != null)
            {
                localContentServerConfiguration.UnusedSessionHeartbeatTimeout = TimeSpan.FromSeconds(unusedSessionHeartbeatTimeoutSeconds.Value);
            }

            if (_scenario != null)
            {
                _logger.Debug($"scenario=[{_scenario}]");
            }

            var exitSignal = new ManualResetEvent(false);
            Console.CancelKeyPress += (sender, args) =>
            {
                exitSignal.Set();
                args.Cancel = true;
            };

            using (var exitEvent = IpcUtilities.GetShutdownWaitHandle(_scenario))
            {
                var server = new LocalContentServer(
                    _fileSystem,
                    _logger,
                    _scenario,
                    path =>
                    new FileSystemContentStore(
                        _fileSystem,
                        SystemClock.Instance,
                        path,
                        new ConfigurationModel(inProcessConfiguration: ContentStoreConfiguration.CreateWithMaxSizeQuotaMB((uint)maxSizeQuotaMB))),
                    localContentServerConfiguration);

                using (server)
                {
                    var context = new Context(_logger);
                    try
                    {
                        var result = server.StartupAsync(context).Result;
                        if (!result.Succeeded)
                        {
                            throw new CacheException(result.ErrorMessage);
                        }

                        int completedIndex = WaitHandle.WaitAny(new WaitHandle[] { exitSignal, exitEvent });
                        var source         = completedIndex == 0 ? "control-C" : "exit event";
                        _tracer.Always(context, $"Shutdown by {source}.");
                    }
                    finally
                    {
                        var result = server.ShutdownAsync(context).Result;
                        if (!result.Succeeded)
                        {
                            _tracer.Warning(context, $"Failed to shutdown store: {result.ErrorMessage}");
                        }
                    }
                }
            }
#else
            Console.CancelKeyPress += (sender, args) =>
            {
                cancellationTokenSource.Cancel();
                args.Cancel = true;
            };

            var localCasSettings = LocalCasSettings.Default(maxSizeQuotaMB, serverDataRootPath.Path, names[0], (uint)grpcPort);

            var distributedContentSettings = DistributedContentSettings.CreateDisabled();

            var distributedCacheServiceConfiguration = new DistributedCacheServiceConfiguration(localCasSettings, distributedContentSettings);

            // Ensure the computed keyspace is computed based on the hostInfo's StampId
            distributedCacheServiceConfiguration.UseStampBasedIsolation = false;

            var distributedCacheServiceArguments = new DistributedCacheServiceArguments(
                logger: _logger,
                copier: null,
                pathTransformer: null,
                host: new EnvironmentVariableHost(),
                hostInfo: new HostInfo(null, null, new List <string>()),
                cancellation: cancellationTokenSource.Token,
                dataRootPath: serverDataRootPath.Path,
                configuration: distributedCacheServiceConfiguration,
                keyspace: null);

            DistributedCacheServiceFacade.RunAsync(distributedCacheServiceArguments).GetAwaiter().GetResult();

            // Because the facade completes immediately and named wait handles don't exist in CORECLR,
            // completion here is gated on Control+C. In the future, this can be redone with another option,
            // such as a MemoryMappedFile or GRPC heartbeat. This is just intended to be functional.
            cancellationTokenSource.Token.WaitHandle.WaitOne();
#endif
        }
Beispiel #7
0
        internal void Service
        (
            [Description("Cache names")] string[] names,
            [Description("Cache root paths")] string[] paths,
            [DefaultValue(DefaultMaxConnections), Description(MaxConnectionsDescription)] uint maxConnections,
            [DefaultValue(DefaultGracefulShutdownSeconds), Description(GracefulShutdownSecondsDescription)] uint gracefulShutdownSeconds,
            [DefaultValue(ServiceConfiguration.GrpcDisabledPort), Description(GrpcPortDescription)] uint grpcPort,
            [Description("Name of the memory mapped file used to share GRPC port. 'CASaaS GRPC port' if not specified.")] string grpcPortFileName,
            [DefaultValue(null), Description("Writable directory for service operations (use CWD if null)")] string dataRootPath,
            [DefaultValue(null), Description("Duration of inactivity after which a session will be timed out.")] double?unusedSessionTimeoutSeconds,
            [DefaultValue(null), Description("Duration of inactivity after which a session with a heartbeat will be timed out.")] double?unusedSessionHeartbeatTimeoutSeconds,
            [DefaultValue(false), Description("Stop running service")] bool stop,
            [DefaultValue(Constants.OneGBInMB), Description("Max size quota in MB")] int maxSizeQuotaMB,
            [DefaultValue(ServiceConfiguration.GrpcDisabledPort), Description(RemoteGrpcPortDescription)] uint backingGrpcPort,
            [DefaultValue(null), Description("Name of scenario for backing CAS service")] string backingScenario,
            [DefaultValue("None"), Description("Ring Id. Used only for telemetry.")] string ringId,
            [DefaultValue("None"), Description("Stamp Id. Used only for telemetry.")] string stampId,
            [DefaultValue(null), Description("nLog configuration path. If empty, it is disabled")] string nLogConfigurationPath,
            [DefaultValue(null), Description("Whether to use Azure Blob logging or not")] string nLogToBlobStorageSecretName,
            [DefaultValue(null), Description("If using Azure Blob logging, where to temporarily store logs")] string nLogToBlobStorageWorkspacePath,
            [DefaultValue(false), Description("Enable metadata")] bool enableMetadata
        )
        {
            Initialize();

            if (stop)
            {
                IpcUtilities.SetShutdown(_scenario);
                return;
            }

            if (names == null || paths == null)
            {
                throw new CacheException("At least one cache name/path is required.");
            }

            if (names.Length != paths.Length)
            {
                throw new CacheException("Mismatching lengths of names/paths arguments.");
            }

            var serverDataRootPath = !string.IsNullOrWhiteSpace(dataRootPath)
                ? new AbsolutePath(dataRootPath)
                : new AbsolutePath(Environment.CurrentDirectory);

            var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken);

            if (_scenario != null)
            {
                _logger.Debug($"scenario=[{_scenario}]");
            }

            using var exitEvent = GetExitEvent();

            var localCasSettings = LocalCasSettings.Default(maxSizeQuotaMB, paths[0], names[0], grpcPort, grpcPortFileName);

            for (int i = 1; i < names.Length; i++)
            {
                localCasSettings.AddNamedCache(names[i], paths[i]);
            }

            localCasSettings.ServiceSettings.ScenarioName = _scenario;

            if (unusedSessionTimeoutSeconds != null)
            {
                localCasSettings.ServiceSettings.UnusedSessionTimeoutMinutes = TimeSpan.FromSeconds(unusedSessionTimeoutSeconds.Value).TotalMinutes;
            }

            if (unusedSessionHeartbeatTimeoutSeconds != null)
            {
                localCasSettings.ServiceSettings.UnusedSessionHeartbeatTimeoutMinutes = TimeSpan.FromSeconds(unusedSessionHeartbeatTimeoutSeconds.Value).TotalMinutes;
            }

            var distributedContentSettings = DistributedContentSettings.CreateDisabled();

            if (backingGrpcPort != ServiceConfiguration.GrpcDisabledPort)
            {
                distributedContentSettings.BackingGrpcPort = (int)backingGrpcPort;
                distributedContentSettings.BackingScenario = backingScenario;
            }

            if (enableMetadata)
            {
                distributedContentSettings.EnableMetadataStore = true;
            }

            LoggingSettings loggingSettings = null;

            if (!string.IsNullOrEmpty(nLogConfigurationPath))
            {
                loggingSettings = new LoggingSettings()
                {
                    NLogConfigurationPath = nLogConfigurationPath,
                    Configuration         = new AzureBlobStorageLogPublicConfiguration()
                    {
                        SecretName          = nLogToBlobStorageSecretName,
                        WorkspaceFolderPath = nLogToBlobStorageWorkspacePath,
                    }
                };
            }

            var distributedCacheServiceConfiguration = new DistributedCacheServiceConfiguration(localCasSettings, distributedContentSettings, loggingSettings);

            // Ensure the computed keyspace is computed based on the hostInfo's StampId
            distributedCacheServiceConfiguration.UseStampBasedIsolation = false;

            var distributedCacheServiceArguments = new DistributedCacheServiceArguments(
                logger: _logger,
                copier: new DistributedCopier(),
                copyRequester: null,
                host: new EnvironmentVariableHost(),
                hostInfo: new HostInfo(null, null, new List <string>()),
                cancellation: cancellationTokenSource.Token,
                dataRootPath: serverDataRootPath.Path,
                configuration: distributedCacheServiceConfiguration,
                keyspace: null)
            {
                TelemetryFieldsProvider = new TelemetryFieldsProvider(ringId, stampId, serviceName: "Service"),
            };

            var runTask = Task.Run(() => DistributedCacheServiceFacade.RunAsync(distributedCacheServiceArguments));

            // Because the facade completes immediately and named wait handles don't exist in CORECLR,
            // completion here is gated on Control+C. In the future, this can be redone with another option,
            // such as a MemoryMappedFile or GRPC heartbeat. This is just intended to be functional.
            int completedIndex = WaitHandle.WaitAny(new WaitHandle[] { cancellationTokenSource.Token.WaitHandle, exitEvent });

            var source = completedIndex == 0 ? "control-C" : "exit event";

            _logger.Always($"Shutdown by {source}.");

            if (completedIndex == 1)
            {
                cancellationTokenSource.Cancel();
            }

            runTask.GetAwaiter().GetResult();
        }