Beispiel #1
0
        public void CopyFolder()
        {
            using (var tempFolder = new TempFolder())
            {
                var sourceFolder    = Path.Combine(tempFolder.Path, "source");
                var sourceSubFolder = Path.Combine(sourceFolder, "subfolder");

                Directory.CreateDirectory(sourceFolder);
                File.WriteAllText(Path.Combine(sourceFolder, "test1.txt"), "test1");
                File.WriteAllText(Path.Combine(sourceFolder, "test2.txt"), "test2");

                Directory.CreateDirectory(sourceSubFolder);
                File.WriteAllText(Path.Combine(sourceSubFolder, "test3.txt"), "test3");

                var targetFolder = Path.Combine(tempFolder.Path, "target");

                NeonHelper.CopyFolder(sourceFolder, targetFolder);

                Assert.True(Directory.Exists(targetFolder));
                Assert.True(Directory.Exists(Path.Combine(targetFolder, "subfolder")));

                Assert.Equal("test1", File.ReadAllText(Path.Combine(targetFolder, "test1.txt")));
                Assert.Equal("test2", File.ReadAllText(Path.Combine(targetFolder, "test2.txt")));
                Assert.Equal("test3", File.ReadAllText(Path.Combine(targetFolder, "subfolder", "test3.txt")));
            }
        }
Beispiel #2
0
        /// <summary>
        /// Implements the <b>publish-folder</b> command.
        /// </summary>
        /// <param name="commandLine">The command line.</param>
        public static void PublishFolder(CommandLine commandLine)
        {
            commandLine = commandLine.Shift(1);

            if (commandLine.Arguments.Length != 2)
            {
                Console.WriteLine(usage);
                Program.Exit(1);
            }

            var sourceFolder = commandLine.Arguments[0];
            var targetFolder = commandLine.Arguments[1];

            Console.WriteLine($"neon-build publish-folder: {sourceFolder} --> {targetFolder}");

            if (!Directory.Exists(sourceFolder))
            {
                Console.Error.WriteLine($"*** ERROR: [SOURCE-FOLDER={sourceFolder}] does not exist!");
                Program.Exit(1);
            }

            if (Directory.Exists(targetFolder))
            {
                Directory.Delete(targetFolder, recursive: true);
            }

            NeonHelper.CopyFolder(sourceFolder, targetFolder);
        }
Beispiel #3
0
        /// <inheritdoc/>
        public override async Task RunAsync(CommandLine commandLine)
        {
            if (commandLine.HasHelpOption || commandLine.Arguments.Length == 0)
            {
                Console.WriteLine(usage);
                Program.Exit(0);
            }

            var sourceFolder = commandLine.Arguments.ElementAtOrDefault(0);
            var outputPath   = commandLine.Arguments.ElementAtOrDefault(1);
            var sbGccArgs    = new StringBuilder();

            if (string.IsNullOrEmpty(sourceFolder) || string.IsNullOrEmpty(outputPath))
            {
                Console.WriteLine(usage);
                Program.Exit(1);
            }

            foreach (var arg in commandLine.Arguments.Skip(2))
            {
                sbGccArgs.AppendWithSeparator(arg);
            }

            // We're going to build this within the distro at [/tmp/wsl-util/GUID] by
            // recursively copying the contents of SOURCE-FOLDER to this directory,
            // running GCC to build the thing, passing [*.c] to include all of the C
            // source files and generating the binary as [output.bin] with the folder.
            //
            // We're also going to clear the [/tmp/wsl-util] folder first to ensure that we don't
            // accumulate any old build files over time and we'll also ensure that
            // [gcc] is installed.

            var defaultDistro = Wsl2Proxy.GetDefault();

            if (string.IsNullOrEmpty(defaultDistro))
            {
                Console.Error.WriteLine("*** ERROR: There is no default WSL2 distro.");
                Program.Exit(1);
            }

            var distro = new Wsl2Proxy(defaultDistro);

            if (!distro.IsDebian)
            {
                Console.Error.WriteLine($"*** ERROR: Your default WSL2 distro [{distro.Name}] is running: {distro.OSRelease["ID"]}/{distro.OSRelease["ID_LIKE"]}");
                Console.Error.WriteLine($"           The CRI-O build requires an Debian/Ubuntu based distribution.");
                Program.Exit(1);
            }

            var linuxUtilFolder    = LinuxPath.Combine("/", "tmp", "wsl-util");
            var linuxBuildFolder   = LinuxPath.Combine(linuxUtilFolder, Guid.NewGuid().ToString("d"));
            var linuxOutputPath    = LinuxPath.Combine(linuxBuildFolder, "output.bin");
            var windowsUtilFolder  = distro.ToWindowsPath(linuxUtilFolder);
            var windowsBuildFolder = distro.ToWindowsPath(linuxBuildFolder);

            try
            {
                // Delete the [/tmp/wsl-util] folder on Linux and the copy the
                // source from the Windows side into a fresh distro folder.

                NeonHelper.DeleteFolder(windowsUtilFolder);
                NeonHelper.CopyFolder(sourceFolder, windowsBuildFolder);

                // Install [safe-apt-get] if it's not already present.  We're using this
                // because it's possible that projects build in parallel and it's possible
                // that multiple GCC commands could also be running in parallel.

                var linuxSafeAptGetPath   = "/usr/bin/safe-apt-get";
                var windowsSafeAptGetPath = distro.ToWindowsPath(linuxSafeAptGetPath);

                if (!File.Exists(windowsSafeAptGetPath))
                {
                    var resources  = Assembly.GetExecutingAssembly().GetResourceFileSystem("WslUtil.Resources");
                    var toolScript = resources.GetFile("/safe-apt-get.sh").ReadAllText();

                    // Note that we need to escape all "$" characters in the script
                    // so the upload script won't attempt to replace any variables
                    // (with blanks).

                    toolScript = toolScript.Replace("$", "\\$");

                    var uploadScript =
                        $@"
cat <<EOF > {linuxSafeAptGetPath}
{toolScript}
EOF

chmod 754 {linuxSafeAptGetPath}
";
                    distro.SudoExecuteScript(uploadScript).EnsureSuccess();
                }

                // Perform the build.

                var buildScript =
                    $@"
set -euo pipefail

safe-apt-get install -yq gcc

cd {linuxBuildFolder}
gcc *.c -o {linuxOutputPath} {sbGccArgs}
";
                distro.SudoExecuteScript(buildScript).EnsureSuccess();

                // Copy the build output to the Windows output path.

                Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
                NeonHelper.DeleteFile(outputPath);
                File.Copy(distro.ToWindowsPath(linuxOutputPath), outputPath);
            }
            finally
            {
                // Remove the temporary distro folder.

                NeonHelper.DeleteFolder(windowsBuildFolder);
            }

            await Task.CompletedTask;
        }
Beispiel #4
0
        /// <summary>
        /// Configures Varnish based on the current traffic manager configuration.
        /// </summary>
        /// <remarks>
        /// This method will terminate the service if Varnish could not be started
        /// for the first call.
        /// </remarks>
        public async static Task ConfigureVarnish()
        {
            try
            {
                // Retrieve the configuration HASH and compare that with what
                // we have already deployed.

                log.LogInfo(() => $"VARNISH-SHIM: Retrieving configuration HASH from Consul path [{configHashKey}].");

                string configHash;

                try
                {
                    configHash = await consul.KV.GetString(configHashKey, terminator.CancellationToken);
                }
                catch (OperationCanceledException)
                {
                    return;
                }
                catch (Exception e)
                {
                    SetErrorTime();
                    log.LogError($"VARNISH-SHIM: Cannot retrieve [{configHashKey}] from Consul.", e);
                    return;
                }

                if (configHash == deployedHash)
                {
                    log.LogInfo(() => $"VARNISH-SHIM: Configuration with [hash={configHash}] is already deployed.");
                    return;
                }
                else
                {
                    log.LogInfo(() => $"VARNISH-SHIM: Configuration hash has changed from [{deployedHash}] to [{configHash}].");
                }

                // Download the configuration archive from Consul and extract it to
                // the new configuration directory (after ensuring that the directory
                // has been cleared).

                log.LogInfo(() => $"VARNISH-SHIM: Retrieving configuration ZIP archive from Consul path [{configKey}].");

                byte[] zipBytes;

                try
                {
                    zipBytes = await consul.KV.GetBytes(configKey, terminator.CancellationToken);
                }
                catch (OperationCanceledException)
                {
                    return;
                }
                catch (Exception e)
                {
                    SetErrorTime();
                    log.LogError($"VARNISH-SHIM: Cannot retrieve [{configKey}] from Consul.", e);
                    return;
                }

                if (configHash == deployedHash)
                {
                    log.LogInfo(() => $"VARNISH-SHIM: Configuration with [hash={configHash}] is already deployed.");
                    return;
                }

                var zipPath = Path.Combine(configUpdateFolder, "haproxy.zip");

                log.LogInfo(() => $"VARNISH-SHIM: Extracting ZIP archive to [{configUpdateFolder}].");

                // Ensure that we have a fresh update folder.

                NeonHelper.DeleteFolder(configUpdateFolder);
                Directory.CreateDirectory(configUpdateFolder);

                // Unzip the configuration archive to the update folder.

                File.WriteAllBytes(zipPath, zipBytes);

                var response = NeonHelper.ExecuteCapture("unzip",
                                                         new object[]
                {
                    "-o", zipPath,
                    "-d", configUpdateFolder
                });

                response.EnsureSuccess();

                // It's possible that very old versions of [neon-proxy-manager] haven't
                // included a generated [varnish.vcl] file within the ZIP archive.  We'll
                // create a stub (do-nothing) file in this case to make Varnish happy.

                if (!File.Exists(configUpdatePath))
                {
                    const string stubVcl =
                        @"vcl 4.0;

# The proxy configuration archive did not include a [varnish.vcl] file so
# we'll use this stub VCL file that doesn't do anything.

backend stub {
    .host = ""localhost"";
    .port = ""8080"";
}
";
                    File.WriteAllText(configUpdatePath, NeonHelper.ToLinuxLineEndings(stubVcl));
                }

                // Compare the VCL just downloaded with that we saved from the last update
                // (if this isn't the first).  If the two programs are the same then we
                // don't need to do anything.  This can happen if the HAProxy config changed
                // but the Varnish config didn't.

                var newVCL = File.ReadAllText(configUpdatePath);

                if (lastVcl != null)
                {
                    if (lastVcl == newVCL)
                    {
                        log.LogInfo(() => "VARNISH-SHIM: VCL is unchanged.  No need to update Varnish.");
                        return;
                    }
                }

                // Verify the configuration.

                log.LogInfo(() => "VARNISH-SHIM: Verifying Varnish configuration.");

                var verifyWorkDir = "/tmp/verify";

                response = NeonHelper.ExecuteCapture("varnishd",
                                                     new object[]
                {
                    "-C",
                    "-f", configUpdatePath,
                    "-n", verifyWorkDir,
                    "-a", "127.0.0.2:9090"          // Avoid conflicting with the production instance via an internal address/port
                });

                NeonHelper.DeleteFolder(verifyWorkDir);

                if (response.ExitCode == 0)
                {
                    log.LogInfo(() => "VARNISH-SHIM: Configuration is OK.");
                }
                else
                {
                    SetErrorTime();

                    // If Varnish is running then we'll let it continue using
                    // the out-of-date configuration as a fail-safe.  If it's not
                    // running, we're going to terminate the service.

                    if (!GetVarnishProcessIds().IsEmpty())
                    {
                        log.LogError(() => $"VARNISH-SHIM: Invalid Varnish configuration: {response.AllText}.");
                        log.LogError(() => $"VARNISH-SHIM: Using out-of-date configuration as a fail-safe.");
                    }
                    else
                    {
                        log.LogCritical(() => $"VARNISH-SHIM: Invalid Varnish configuration: {response.AllText}.");
                        log.LogCritical(() => "VARNISH-SHIM: Terminating service.");
                        Program.Exit(1);
                        return;
                    }
                }

                // Purge the contents of the [configFolder] and copy the contents
                // of [configUpdateFolder] into it.

                NeonHelper.DeleteFolder(configFolder);
                Directory.CreateDirectory(configFolder);
                NeonHelper.CopyFolder(configUpdateFolder, configFolder);

                // Start Varnish if it's not already running.

                if (GetVarnishProcessIds().IsEmpty())
                {
                    log.LogInfo(() => $"VARNISH-SHIM: Starting Vanish.");

                    response = NeonHelper.ExecuteCapture("varnishd",
                                                         new object[]
                    {
                        "-f", configPath,
                        "-s", $"malloc,{memoryLimit}",
                        "-T", AdminInterface,
                        "-a", "0.0.0.0:80",
                        "-n", workDir
                    });

                    if (response.ExitCode == 0)
                    {
                        log.LogInfo(() => $"VARNISH-SHIM: Varnish has started.");

                        // Update the deployed hash so we won't try to update the same
                        // configuration again.

                        deployedHash = configHash;
                    }
                    else
                    {
                        log.LogCritical(() => $"VARNISH-SHIM: Cannot start Varnish: {response.ErrorText}");
                        Program.Exit(1);
                        return;
                    }
                }

                // Update the Varnish config.

                log.LogInfo(() => $"VARNISH-SHIM: Updating Varnish.");

                var oldVclProgram = vclVersion == 0 ? "boot" : $"main-{vclVersion}";
                var newVclProgram = $"main-{++vclVersion}";

                try
                {
                    log.LogInfo(() => $"VARNISH-SHIM: varnishadm -n {workDir} vcl.load {newVclProgram} {configPath}");

                    response = NeonHelper.ExecuteCapture("varnishadm",
                                                         new object[]
                    {
                        "-n", workDir,
                        "vcl.load", newVclProgram, configPath
                    });

                    response.EnsureSuccess();

                    // Activate the new VCL.

                    log.LogInfo(() => $"VARNISH-SHIM: varnishadm -n {workDir} vcl.use {newVclProgram}");

                    response = NeonHelper.ExecuteCapture("varnishadm",
                                                         new object[]
                    {
                        "-n", workDir,
                        "vcl.use", newVclProgram
                    });

                    response.EnsureSuccess();

                    // Remove the previous VCL program.

                    log.LogInfo(() => $"VARNISH-SHIM: varnishadm -n {workDir} vcl.discard {newVclProgram}");

                    response = NeonHelper.ExecuteCapture("varnishadm",
                                                         new object[]
                    {
                        "-n", workDir,
                        "vcl.discard", oldVclProgram
                    });

                    response.EnsureSuccess();

                    // Update the deployed hash so we won't try to update the same
                    // configuration again.

                    deployedHash = configHash;
                    lastVcl      = newVCL;

                    log.LogInfo(() => $"VARNISH-SHIM: Varnish is up-to-date.");

                    // Read the cache settings and update the cache warmer.

                    var cacheSettingsJson = File.ReadAllText(Path.Combine(configFolder, "cache-settings.json"));
                    var cacheSettings     = NeonHelper.JsonDeserialize <TrafficCacheSettings>(cacheSettingsJson);

                    UpdateCacheWarmer(cacheSettings);
                }
                catch (ExecuteException e)
                {
                    SetErrorTime();
                    log.LogError(() => $"VARNISH-SHIM: Cannot update Varnish: {e.Message}");
                    return;
                }

                // Varnish was updated successfully so we can reset the error time
                // so to ensure that periodic error reporting will stop.

                ResetErrorTime();
            }
            catch (OperationCanceledException)
            {
                log.LogInfo(() => "VARNISH-SHIM: Terminating");
                throw;
            }
            finally
            {
                // When DEBUG mode is not enabled, we're going to clear the
                // both the old and new configuration folders so we don't leave
                // secrets like TLS private keys lying around in a file system.
                //
                // We'll leave these intact for DEBUG mode so we can manually
                // poke around the config.

                if (!debugMode)
                {
                    NeonHelper.DeleteFolder(configFolder);
                    NeonHelper.DeleteFolder(configUpdateFolder);
                }
            }
        }
Beispiel #5
0
        /// <summary>
        /// Configures HAProxy based on the current traffic manager configuration.
        /// </summary>
        /// <remarks>
        /// This method will terminate the service if HAProxy could not be started
        /// for the first call.
        /// </remarks>
        public async static Task ConfigureHAProxy()
        {
            try
            {
                // Retrieve the configuration HASH and compare that with what
                // we have already deployed.

                log.LogInfo(() => $"HAPROXY-SHIM: Retrieving configuration HASH from Consul path [{configHashKey}].");

                string configHash;

                try
                {
                    configHash = await consul.KV.GetString(configHashKey, terminator.CancellationToken);
                }
                catch (OperationCanceledException)
                {
                    return;
                }
                catch (Exception e)
                {
                    SetErrorTime();
                    log.LogError($"HAPROXY-SHIM: Cannot retrieve [{configHashKey}] from Consul.", e);
                    return;
                }

                if (configHash == deployedHash)
                {
                    log.LogInfo(() => $"HAPROXY-SHIM: Configuration with [hash={configHash}] is already deployed.");
                    return;
                }
                else
                {
                    log.LogInfo(() => $"HAPROXY-SHIM: Configuration hash has changed from [{deployedHash}] to [{configHash}].");
                }

                // Download the configuration archive from Consul and extract it to
                // the new configuration directory (after ensuring that the directory
                // has been cleared).

                log.LogInfo(() => $"HAPROXY-SHIM: Retrieving configuration ZIP archive from Consul path [{configKey}].");

                byte[] zipBytes;

                try
                {
                    zipBytes = await consul.KV.GetBytes(configKey, terminator.CancellationToken);
                }
                catch (OperationCanceledException)
                {
                    return;
                }
                catch (Exception e)
                {
                    SetErrorTime();
                    log.LogError($"HAPROXY-SHIM: Cannot retrieve [{configKey}] from Consul.", e);
                    return;
                }

                if (configHash == deployedHash)
                {
                    log.LogInfo(() => $"HAPROXY-SHIM: Configuration with [hash={configHash}] is already deployed.");
                    return;
                }

                var zipPath = Path.Combine(configUpdateFolder, "haproxy.zip");

                log.LogInfo(() => $"HAPROXY-SHIM: Extracting ZIP archive to [{configUpdateFolder}].");

                // Ensure that we have a fresh update folder.

                NeonHelper.DeleteFolder(configUpdateFolder);

                Directory.CreateDirectory(configUpdateFolder);

                // Unzip the configuration archive to the update folder.

                File.WriteAllBytes(zipPath, zipBytes);

                var response = NeonHelper.ExecuteCapture("unzip",
                                                         new object[]
                {
                    "-o", zipPath,
                    "-d", configUpdateFolder
                });

                response.EnsureSuccess();

                // The [certs.list] file (if present) describes the certificates
                // to be downloaded from Vault.
                //
                // Each line contains three fields separated by a space:
                // the Vault object path, the relative destination folder
                // path and the file name.
                //
                // Note that certificates are stored in Vault as JSON using
                // the [TlsCertificate] schema, so we'll need to extract and
                // combine the [cert] and [key] properties.

                var certsPath = Path.Combine(configUpdateFolder, "certs.list");

                if (File.Exists(certsPath))
                {
                    using (var reader = new StreamReader(certsPath))
                    {
                        foreach (var line in reader.Lines())
                        {
                            if (string.IsNullOrWhiteSpace(line))
                            {
                                continue;   // Ignore blank lines
                            }

                            if (isBridge)
                            {
                                log.LogWarn(() => $"HAPROXY-SHIM: Bridge cannot process unexpected TLS certificate reference: {line}");
                                return;
                            }

                            var fields   = line.Split(' ');
                            var certKey  = fields[0];
                            var certDir  = Path.Combine(configUpdateFolder, fields[1]);
                            var certFile = fields[2];

                            Directory.CreateDirectory(certDir);

                            var cert = await vault.ReadJsonAsync <TlsCertificate>(certKey, terminator.CancellationToken);

                            File.WriteAllText(Path.Combine(certDir, certFile), cert.CombinedPemNormalized);
                        }
                    }
                }

                // Verify the configuration.  Note that HAProxy will return a
                // 0 error code if the configuration is OK and specifies at
                // least one route.  It will return 2 if the configuration is
                // OK but there are no routes.  In this case, HAProxy won't
                // actually launch.  Any other exit code indicates that the
                // configuration is not valid.

                log.LogInfo(() => "Verifying HAProxy configuration.");

                Environment.SetEnvironmentVariable("HAPROXY_CONFIG_FOLDER", configUpdateFolder);

                response = NeonHelper.ExecuteCapture("haproxy",
                                                     new object[]
                {
                    "-c",
                    "-q",
                    "-f", configUpdateFolder
                });

                switch (response.ExitCode)
                {
                case 0:

                    log.LogInfo(() => "HAPROXY-SHIM: Configuration is OK.");
                    break;

                case 2:

                    log.LogInfo(() => "HAPROXY-SHIM: Configuration is valid but specifies no routes.");

                    // Ensure that any existing HAProxy instances are stopped and that
                    // the configuration folders are cleared (for non-DEBUG mode) and
                    // then return so we won't try to spin up another HAProxy.

                    foreach (var processId in GetHAProxyProcessIds())
                    {
                        KillProcess(processId);
                    }

                    if (!debugMode)
                    {
                        NeonHelper.DeleteFolder(configFolder);
                        NeonHelper.DeleteFolder(configUpdateFolder);
                    }
                    return;

                default:

                    SetErrorTime();

                    log.LogError(() => $"HAPROXY-SHIM: Invalid HAProxy configuration: {response.AllText}.");

                    // If HAProxy is running then we'll let it continue using
                    // the out-of-date configuration as a fail-safe.  If it's not
                    // running, we're going to terminate service.

                    if (!GetHAProxyProcessIds().IsEmpty())
                    {
                        log.LogWarn(() => "HAPROXY-SHIM: Continuining to use the previous configuration as a fail-safe.");
                    }
                    else
                    {
                        log.LogCritical(() => "HAPROXY-SHIM: Terminating service because there is no valid configuration to fall back to.");
                        Program.Exit(1);
                        return;
                    }
                    break;
                }

                // Purge the contents of the [configFolder] and copy the contents
                // of [configUpdateolder] into it.

                NeonHelper.DeleteFolder(configFolder);
                Directory.CreateDirectory(configFolder);
                NeonHelper.CopyFolder(configUpdateFolder, configFolder);

                // Start HAProxy if it's not already running.
                //
                // ...or we'll generally do a soft stop when HAProxy is already running,
                // which means that HAProxy will try hard to maintain existing connections
                // as it reloads its config.  The presence of a [.hardstop] file in the
                // configuration folder will enable a hard stop.
                //
                // Note that there may actually be more then one HAProxy process running.
                // One will be actively handling new connections and the rest will be
                // waiting gracefully for existing connections to be closed before they
                // terminate themselves.

                // $todo(jeff.lill):
                //
                // I don't believe that [neon-proxy-manager] ever generates a
                // [.hardstop] file.  This was an idea for the future.  This would
                // probably be better replaced by a boolean [HardStop] oroperty
                // passed with the notification message.

                var haProxyProcessIds = GetHAProxyProcessIds();
                var stopType          = string.Empty;
                var stopOptions       = new List <string>();
                var restart           = false;

                if (haProxyProcessIds.Count > 0)
                {
                    restart = true;

                    if (File.Exists(Path.Combine(configFolder, ".hardstop")))
                    {
                        stopType = "(hard stop)";

                        stopOptions.Add("-st");
                        foreach (var processId in haProxyProcessIds)
                        {
                            stopOptions.Add(processId.ToString());
                        }
                    }
                    else
                    {
                        stopType = "(soft stop)";

                        stopOptions.Add("-sf");
                        foreach (var processId in haProxyProcessIds)
                        {
                            stopOptions.Add(processId.ToString());
                        }
                    }

                    log.LogInfo(() => $"HAPROXY-SHIM: Restarting HAProxy {stopType}.");
                }
                else
                {
                    restart = false;
                    log.LogInfo(() => $"HAPROXY-SHIM: Starting HAProxy.");
                }

                // Enable HAProxy debugging mode to get a better idea of why health
                // checks are failing.

                var debugOption = string.Empty;

                if (debugMode)
                {
                    debugOption = "-d";
                }

                // Execute HAProxy.  If we're not running in DEBUG mode then HAProxy
                // will fork another HAProxy process that will handle the traffic and
                // return.  For DEBUG mode, this HAProxy call will ignore daemon mode
                // and run in the forground.  In that case, we need to fork HAProxy
                // so we won't block here forever.

                Environment.SetEnvironmentVariable("HAPROXY_CONFIG_FOLDER", configFolder);

                if (!debugMode)
                {
                    // Regular mode.

                    response = NeonHelper.ExecuteCapture("haproxy",
                                                         new object[]
                    {
                        "-f", configPath,
                        stopOptions,
                        debugOption,
                        "-V"
                    });

                    if (response.ExitCode != 0)
                    {
                        SetErrorTime();
                        log.LogError(() => $"HAPROXY-SHIM: HAProxy failure: {response.ErrorText}");
                        return;
                    }
                }
                else
                {
                    // DEBUG mode steps:
                    //
                    //      1: Kill any existing HAProxy processes.
                    //      2: Fork the new one to pick up the latest config.

                    foreach (var processId in haProxyProcessIds)
                    {
                        KillProcess(processId);
                    }

                    NeonHelper.Fork("haproxy",
                                    new object[]
                    {
                        "-f", configPath,
                        stopOptions,
                        debugOption,
                        "-V"
                    });
                }

                // Give HAProxy a chance to start/restart cleanly.

                await Task.Delay(startDelay, terminator.CancellationToken);

                if (restart)
                {
                    log.LogInfo(() => "HAPROXY-SHIM: HAProxy has been updated.");
                }
                else
                {
                    log.LogInfo(() => "HAPROXY-SHIM: HAProxy has started.");
                }

                // Update the deployed hash so we won't try to update the same
                // configuration again.

                deployedHash = configHash;

                // Ensure that we're not exceeding the limit for HAProxy processes.

                if (maxHAProxyCount > 0)
                {
                    var newHaProxyProcessIds = GetHAProxyProcessIds();

                    if (newHaProxyProcessIds.Count > maxHAProxyCount)
                    {
                        log.LogWarn(() => $"HAProxy process count [{newHaProxyProcessIds}] exceeds [MAX_HAPROXY_COUNT={maxHAProxyCount}] so we're killing the oldest inactive instance.");
                        KillOldestProcess(haProxyProcessIds);
                    }
                }

                // HAProxy was updated successfully so we can reset the error time
                // so to ensure that periodic error reporting will stop.

                ResetErrorTime();
            }
            catch (OperationCanceledException)
            {
                log.LogInfo(() => "HAPROXY-SHIM: Terminating");
                throw;
            }
            catch (Exception e)
            {
                if (GetHAProxyProcessIds().Count == 0)
                {
                    log.LogCritical("HAPROXY-SHIM: Terminating because we cannot launch HAProxy.", e);
                    Program.Exit(1);
                    return;
                }
                else
                {
                    log.LogError("HAPROXY-SHIM: Unable to reconfigure HAProxy.  Using the old configuration as a fail-safe.", e);
                }

                SetErrorTime();
            }
            finally
            {
                // When DEBUG mode is not enabled, we're going to clear the
                // both the old and new configuration folders so we don't leave
                // secrets like TLS private keys lying around in a file system.
                //
                // We'll leave these intact for DEBUG mode so we can manually
                // poke around the config.

                if (!debugMode)
                {
                    NeonHelper.DeleteFolder(configFolder);
                    NeonHelper.DeleteFolder(configUpdateFolder);
                }
            }
        }