<commit_before>// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.SpaProxy
{
    internal class SpaProxyLaunchManager : IDisposable
    {
        private readonly SpaDevelopmentServerOptions _options;
        private readonly ILogger<SpaProxyLaunchManager> _logger;
        private readonly object _lock = new object();

        private Process? _spaProcess;
        private bool _disposedValue;
        private Task? _launchTask;

        public SpaProxyLaunchManager(
            ILogger<SpaProxyLaunchManager> logger,
            IHostApplicationLifetime appLifetime,
            IOptions<SpaDevelopmentServerOptions> options)
        {
            _options = options.Value;
            _logger = logger;
            appLifetime.ApplicationStopping.Register(() => Dispose(true));
        }

        public void StartInBackground(CancellationToken cancellationToken)
        {
            // We are not waiting for the SPA proxy to launch, instead we are going to rely on a piece of
            // middleware to display an HTML document while the SPA proxy is not ready, refresh every three
            // seconds and redirect to the SPA proxy url once it is ready.
            // Being ready in this context means that we were able to receive a 200 from the proxy or that
            // we gave up waiting.
            // We do this to ensure Visual Studio can work correctly with IIS and when running without debugging.
            lock (_lock)
            {
                if (_launchTask == null)
                {
                    _logger.LogInformation($"No SPA development server running at {_options.ServerUrl} found.");
                    _launchTask = UpdateStatus(StartSpaProcessAndProbeForLiveness(cancellationToken));
                }
            }

            async Task UpdateStatus(Task launchTask)
            {
                try
                {
                    await launchTask;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "There was an error trying to launch the SPA proxy.");
                }
                finally
                {
                    lock (_lock)
                    {
                        _launchTask = null;
                    }
                }
            }
        }

        public async Task<bool> IsSpaProxyRunning(CancellationToken cancellationToken)
        {
            var httpClient = CreateHttpClient();

            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
            using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
            try
            {
                var response = await httpClient.GetAsync(_options.ServerUrl, cancellationTokenSource.Token);
                var running = response.IsSuccessStatusCode;
                return running;
            }
            catch (Exception exception) when (exception is HttpRequestException ||
                  exception is TaskCanceledException ||
                  exception is OperationCanceledException)
            {
                _logger.LogDebug(exception, "Failed to connect to the SPA Development proxy.");
                return false;
            }
        }

        private static HttpClient CreateHttpClient()
        {
            var httpClient = new HttpClient(new HttpClientHandler()
            {
                // It's ok for us to do this here since this service is only plugged in during development.
                ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
            });
            // We don't care about the returned content type as long as the server is able to answer with 2XX
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*", 0.1));
            return httpClient;
        }

        private async Task<bool> ProbeSpaDevelopmentServerUrl(HttpClient httpClient, CancellationToken cancellationToken)
        {
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
            using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
            try
            {
                var response = await httpClient.GetAsync(_options.ServerUrl, cancellationTokenSource.Token);
                var running = response.IsSuccessStatusCode;
                return running;
            }
            catch (Exception exception) when (exception is HttpRequestException ||
                  exception is TaskCanceledException ||
                  exception is OperationCanceledException)
            {
                _logger.LogDebug(exception, "Failed to connect to the SPA Development proxy.");
                return false;
            }
        }

        private async Task StartSpaProcessAndProbeForLiveness(CancellationToken cancellationToken)
        {
            LaunchDevelopmentProxy();
            var sw = Stopwatch.StartNew();
            var livenessProbeSucceeded = false;
            var maxTimeoutReached = false;
            var httpClient = CreateHttpClient();
            while (_spaProcess != null && !_spaProcess.HasExited && !maxTimeoutReached)
            {
                livenessProbeSucceeded = await ProbeSpaDevelopmentServerUrl(httpClient, cancellationToken);
                if (livenessProbeSucceeded)
                {
                    break;
                }

                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                maxTimeoutReached = sw.Elapsed >= _options.MaxTimeout;
                await Task.Delay(1000, cancellationToken);
            }

            if (_spaProcess == null || _spaProcess.HasExited)
            {
                _logger.LogError($"Couldn't start the SPA development server with command '{_options.LaunchCommand}'.");
            }
            else if (!livenessProbeSucceeded)
            {
                _logger.LogError($"Unable to connect to the SPA development server at '{_options.ServerUrl}'.");
            }
            else
            {
                _logger.LogInformation($"SPA development server running at '{_options.ServerUrl}'");
            }
        }

        private void LaunchDevelopmentProxy()
        {
            try
            {
                // Launch command is going to be something like `npm/yarn <<verb>> <<options>>`
                // We split it into two to separate the tool (command) from the verb and the rest of the arguments.
                var space = _options.LaunchCommand.IndexOf(' ');
                var command = _options.LaunchCommand[0..space];
                var arguments = _options.LaunchCommand[++space..];
                if (OperatingSystem.IsWindows() && !Path.HasExtension(command))
                {
                    // On windows we transform npm/yarn to npm.cmd/yarn.cmd so that the command
                    // can actually be found when we start the process. This is overridable if
                    // necessary by explicitly setting up the extension on the command.
                    command = $"{command}.cmd";
                }

                var info = new ProcessStartInfo(command, arguments)
                {
                    // Linux and Mac OS don't have the concept of launching a terminal process in a new window.
                    // On those cases the process will be launched in the same terminal window and will just print
                    // some output during the start phase of the app.
                    // This is not a problem since users don't need to interact with the proxy other than to stop it
                    // and this is only an optimization to keep the current experience. We can always tell them to
                    // run the proxy manually.
                    CreateNoWindow = false,
                    UseShellExecute = true,
                    WindowStyle = ProcessWindowStyle.Normal,
                    WorkingDirectory = Path.Combine(AppContext.BaseDirectory, _options.WorkingDirectory)
                };
                _spaProcess = Process.Start(info);
                if (OperatingSystem.IsWindows() && _spaProcess != null)
                {
                    var stopScript = $@"do{{
  try
  {{
    $processId = Get-Process -PID {Environment.ProcessId} -ErrorAction Stop;
  }}catch
  {{
    $processId = $null;
  }}
  Start-Sleep -Seconds 1;
}}while($processId -ne $null);

try
{{
  taskkill /T /F /PID {_spaProcess.Id};
}}
catch
{{
}}";
                    var stopScriptInfo = new ProcessStartInfo(
                        "powershell.exe",
                        string.Join(" ", "-NoProfile", "-C", stopScript))
                    {
                        CreateNoWindow = true,
                        WorkingDirectory = Path.Combine(AppContext.BaseDirectory, _options.WorkingDirectory)
                    };

                    var stopProcess = Process.Start(stopScriptInfo);
                    if (stopProcess == null || stopProcess.HasExited)
                    {
                        _logger.LogWarning($"SPA process shutdown script '{stopProcess?.Id}' failed to start. The SPA proxy might" +
                            $" remain open if the dotnet process is terminated abruptly. Use the operating system command to kill" +
                            $"the process tree for {_spaProcess.Id}");
                    }
                    else
                    {
                        _logger.LogDebug($"Watch process '{stopProcess}' started.");
                    }
                }
            }
            catch (Exception exception)
            {
                _logger.LogError(exception, $"Failed to launch the SPA development server '{_options.LaunchCommand}'.");<commit_msg>[SPA] Ensure spawned NPM processes are terminated when the dotnet process is ungracefully terminated (#35591)<commit_after>// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.SpaProxy
{
    internal class SpaProxyLaunchManager : IDisposable
    {
        private readonly SpaDevelopmentServerOptions _options;
        private readonly ILogger<SpaProxyLaunchManager> _logger;
        private readonly object _lock = new object();

        private Process? _spaProcess;
        private bool _disposedValue;
        private Task? _launchTask;

        public SpaProxyLaunchManager(
            ILogger<SpaProxyLaunchManager> logger,
            IHostApplicationLifetime appLifetime,
            IOptions<SpaDevelopmentServerOptions> options)
        {
            _options = options.Value;
            _logger = logger;
            appLifetime.ApplicationStopping.Register(() => Dispose(true));
        }

        public void StartInBackground(CancellationToken cancellationToken)
        {
            // We are not waiting for the SPA proxy to launch, instead we are going to rely on a piece of
            // middleware to display an HTML document while the SPA proxy is not ready, refresh every three
            // seconds and redirect to the SPA proxy url once it is ready.
            // Being ready in this context means that we were able to receive a 200 from the proxy or that
            // we gave up waiting.
            // We do this to ensure Visual Studio can work correctly with IIS and when running without debugging.
            lock (_lock)
            {
                if (_launchTask == null)
                {
                    _logger.LogInformation($"No SPA development server running at {_options.ServerUrl} found.");
                    _launchTask = UpdateStatus(StartSpaProcessAndProbeForLiveness(cancellationToken));
                }
            }

            async Task UpdateStatus(Task launchTask)
            {
                try
                {
                    await launchTask;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "There was an error trying to launch the SPA proxy.");
                }
                finally
                {
                    lock (_lock)
                    {
                        _launchTask = null;
                    }
                }
            }
        }

        public async Task<bool> IsSpaProxyRunning(CancellationToken cancellationToken)
        {
            var httpClient = CreateHttpClient();

            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
            using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
            try
            {
                var response = await httpClient.GetAsync(_options.ServerUrl, cancellationTokenSource.Token);
                var running = response.IsSuccessStatusCode;
                return running;
            }
            catch (Exception exception) when (exception is HttpRequestException ||
                  exception is TaskCanceledException ||
                  exception is OperationCanceledException)
            {
                _logger.LogDebug(exception, "Failed to connect to the SPA Development proxy.");
                return false;
            }
        }

        private static HttpClient CreateHttpClient()
        {
            var httpClient = new HttpClient(new HttpClientHandler()
            {
                // It's ok for us to do this here since this service is only plugged in during development.
                ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
            });
            // We don't care about the returned content type as long as the server is able to answer with 2XX
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*", 0.1));
            return httpClient;
        }

        private async Task<bool> ProbeSpaDevelopmentServerUrl(HttpClient httpClient, CancellationToken cancellationToken)
        {
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
            using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
            try
            {
                var response = await httpClient.GetAsync(_options.ServerUrl, cancellationTokenSource.Token);
                var running = response.IsSuccessStatusCode;
                return running;
            }
            catch (Exception exception) when (exception is HttpRequestException ||
                  exception is TaskCanceledException ||
                  exception is OperationCanceledException)
            {
                _logger.LogDebug(exception, "Failed to connect to the SPA Development proxy.");
                return false;
            }
        }

        private async Task StartSpaProcessAndProbeForLiveness(CancellationToken cancellationToken)
        {
            LaunchDevelopmentProxy();
            var sw = Stopwatch.StartNew();
            var livenessProbeSucceeded = false;
            var maxTimeoutReached = false;
            var httpClient = CreateHttpClient();
            while (_spaProcess != null && !_spaProcess.HasExited && !maxTimeoutReached)
            {
                livenessProbeSucceeded = await ProbeSpaDevelopmentServerUrl(httpClient, cancellationToken);
                if (livenessProbeSucceeded)
                {
                    break;
                }

                if (cancellationToken.IsCancellationRequested)
                {
                    return;
                }

                maxTimeoutReached = sw.Elapsed >= _options.MaxTimeout;
                await Task.Delay(1000, cancellationToken);
            }

            if (_spaProcess == null || _spaProcess.HasExited)
            {
                _logger.LogError($"Couldn't start the SPA development server with command '{_options.LaunchCommand}'.");
            }
            else if (!livenessProbeSucceeded)
            {
                _logger.LogError($"Unable to connect to the SPA development server at '{_options.ServerUrl}'.");
            }
            else
            {
                _logger.LogInformation($"SPA development server running at '{_options.ServerUrl}'");
            }
        }

        private void LaunchDevelopmentProxy()
        {
            try
            {
                // Launch command is going to be something like `npm/yarn <<verb>> <<options>>`
                // We split it into two to separate the tool (command) from the verb and the rest of the arguments.
                var space = _options.LaunchCommand.IndexOf(' ');
                var command = _options.LaunchCommand[0..space];
                var arguments = _options.LaunchCommand[++space..];
                if (OperatingSystem.IsWindows() && !Path.HasExtension(command))
                {
                    // On windows we transform npm/yarn to npm.cmd/yarn.cmd so that the command
                    // can actually be found when we start the process. This is overridable if
                    // necessary by explicitly setting up the extension on the command.
                    command = $"{command}.cmd";
                }

                var info = new ProcessStartInfo(command, arguments)
                {
                    // Linux and Mac OS don't have the concept of launching a terminal process in a new window.
                    // On those cases the process will be launched in the same terminal window and will just print
                    // some output during the start phase of the app.
                    // This is not a problem since users don't need to interact with the proxy other than to stop it
                    // and this is only an optimization to keep the current experience. We can always tell them to
                    // run the proxy manually.
                    CreateNoWindow = false,
                    UseShellExecute = true,
                    WindowStyle = ProcessWindowStyle.Normal,
                    WorkingDirectory = Path.Combine(AppContext.BaseDirectory, _options.WorkingDirectory)
                };
                _spaProcess = Process.Start(info);
                if (_spaProcess != null && !_spaProcess.HasExited)
                {
                    if (OperatingSystem.IsWindows())
                    {
                        LaunchStopScriptWindows(_spaProcess.Id);
                    }
                    else if (OperatingSystem.IsMacOS())
                    {
                        LaunchStopScriptMacOS(_spaProcess.Id);
                    }
                }
            }
            catch (Exception exception)
            {
                _logger.LogError(exception, $"Failed to launch the SPA development server '{_options.LaunchCommand}'.");
            }
        }

        private void LaunchStopScriptWindows(int spaProcessId)
        {
            var stopScript = $@"do{{
  try
  {{
    $processId = Get-Process -PID {Environment.ProcessId} -ErrorAction Stop;
  }}catch
  {{
    $processId = $null;
  }}
  Start-Sleep -Seconds 1;
}}while($processId -ne $null);

try
{{
  taskkill /T /F /PID {spaProcessId};
}}
catch
{{
}}";
            var stopScriptInfo = new ProcessStartInfo(
                "powershell.exe",
                string.Join(" ", "-NoProfile", "-C", stopScript))
            {
                CreateNoWindow = true,
                WorkingDirectory = Path.Combine(AppContext.BaseDirectory, _options.WorkingDirectory)
            };

            var stopProcess = Process.Start(stopScriptInfo);
            if (stopProcess == null || stopProcess.HasExited)
            {
                _logger.LogWarning($"The SPA process shutdown script '{stopProcess?.Id}' failed to start. The SPA proxy might" +
                    $" remain open if the dotnet process is terminated ungracefully. Use the operating system commands to kill" +
                    $" the process tree for {spaProcessId}");
            }
            else
            {
                _logger.LogDebug($"Watch process '{stopProcess}' started.");
            }
        }

        private void LaunchStopScriptMacOS(int spaProcessId)
        {
            var fileName = Guid.NewGuid().ToString("N") + ".sh";
            var scriptPath = Path.Combine(AppContext.BaseDirectory, fileName);
            var stopScript = @$"function list_child_processes(){{
    local ppid=$1;
    local current_children=$(pgrep -P $ppid);
    local local_child;
    if [ $? -eq 0 ];
    then
        for current_child in $current_children
        do
          local_child=$current_child;
          list_child_processes $local_child;
          echo $local_child;
        done;
    else
      return 0;
    fi;
}}

ps {Environment.ProcessId};
while [ $? -eq 0 ];
do
  sleep 1;
  ps {Environment.ProcessId} > /dev/null;
done;

for child in $(list_child_processes {spaProcessId});
do
  echo killing $child;
  kill -s KILL $child;
done;
rm {scriptPath};
";
            File.WriteAllText(scriptPath, stopScript);

            var stopScriptInfo = new ProcessStartInfo("/bin/bash", scriptPath)
            {
                CreateNoWindow = true,
                WorkingDirectory = Path.Combine(AppContext.BaseDirectory, _options.WorkingDirectory)
            };

            var stopProcess = Process.Start(stopScriptInfo);
            if (stopProcess == null || stopProcess.HasExited)
            {
                _logger.LogWarning($"The SPA process shutdown script '{stopProcess?.Id}' failed to start. The SPA proxy might" +
                    $" remain open if the dotnet process is terminated ungracefully. Use the operating system commands to kill" +
                    $" the process tree for {spaProcessId}");
{'fraction_non_alphanumeric': 0.07110815012214079, 'fraction_numerical': 0.001865423051299134, 'mean_word_length': 1.7023523763802209, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 26, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '11188', 'n_tokens_mistral': 5362, 'n_tokens_neox': 5041, 'n_words': 1593}