/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Shared/E2ETesting/SauceConnectServer.cs
262 строки
9 KB
William Godbe
Rename Microsoft.Aspnetcore.Testing to Microsoft.AspNetCore.InternalTesting (#51713)
02 ноя 2023, 03:15
Не верифицирован
02 ноя 2023, 03:15
fc8deca
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.E2ETesting; using Microsoft.Extensions.Internal; using Xunit.Abstractions; using Xunit.Sdk; namespace Microsoft.AspNetCore.E2ETesting; public class SauceConnectServer : IDisposable { private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); private Process _process; private string _sentinelPath; private Process _sentinelProcess; private static IMessageSink _diagnosticsMessageSink; // 2h private const int SauceConnectProcessTimeout = 7200; public SauceConnectServer(IMessageSink diagnosticsMessageSink) { if (Instance != null || _diagnosticsMessageSink != null) { throw new InvalidOperationException("Sauce connect singleton already created."); } // The assembly level attribute AssemblyFixture takes care of this being being instantiated before tests run // and disposed after tests are run, gracefully shutting down the server when possible by calling Dispose on // the singleton. Instance = this; _diagnosticsMessageSink = diagnosticsMessageSink; } private void Initialize( Process process, string sentinelPath, Process sentinelProcess) { _process = process; _sentinelPath = sentinelPath; _sentinelProcess = sentinelProcess; } internal static SauceConnectServer Instance { get; private set; } public static async Task StartAsync(ITestOutputHelper output) { try { await _semaphore.WaitAsync(); if (Instance._process == null) { // No process was started, meaning the instance wasn't initialized. await InitializeInstance(output); } } finally { _semaphore.Release(); } } private static async Task InitializeInstance(ITestOutputHelper output) { var psi = new ProcessStartInfo { FileName = "npm", Arguments = "run sauce --" + $" --sauce-user {E2ETestOptions.Instance.Sauce.Username}" + $" --sauce-key {E2ETestOptions.Instance.Sauce.AccessKey}" + $" --sauce-tunnel {E2ETestOptions.Instance.Sauce.TunnelIdentifier}" + $" --use-hostname {E2ETestOptions.Instance.Sauce.HostName}", RedirectStandardOutput = true, RedirectStandardError = true, }; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { psi.FileName = "cmd"; psi.Arguments = $"/c npm {psi.Arguments}"; } // It's important that we get the folder value before we start the process to prevent // untracked processes when the tracking folder is not correctly configure. var trackingFolder = GetProcessTrackingFolder(); if (!Directory.Exists(trackingFolder)) { throw new InvalidOperationException($"Invalid tracking folder. Set the 'SauceConnectProcessTrackingFolder' MSBuild property to a valid folder."); } Process process = null; Process sentinel = null; string pidFilePath = null; try { process = Process.Start(psi); pidFilePath = await WriteTrackingFileAsync(output, trackingFolder, process); sentinel = StartSentinelProcess(process, pidFilePath, SauceConnectProcessTimeout); } catch { ProcessCleanup(process, pidFilePath); ProcessCleanup(sentinel, pidFilePath: null); throw; } // Log output for sauce connect process. // This is for the case where the server fails to launch. var logOutput = new BlockingCollection<string>(); process.OutputDataReceived += LogOutput; process.ErrorDataReceived += LogOutput; process.BeginOutputReadLine(); process.BeginErrorReadLine(); // The Sauce connect server has to be up for the entirety of the tests and is only shutdown when the application (i.e. the test) exits. AppDomain.CurrentDomain.ProcessExit += (sender, args) => ProcessCleanup(process, pidFilePath); // Log void LogOutput(object sender, DataReceivedEventArgs e) { logOutput.TryAdd(e.Data); // We avoid logging on the output here because it is unreliable. We can only log in the diagnostics sink. lock (_diagnosticsMessageSink) { _diagnosticsMessageSink.OnMessage(new DiagnosticMessage(e.Data)); } } var uri = new UriBuilder("http", E2ETestOptions.Instance.Sauce.HostName, 4445).Uri; var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(1), }; var retries = 0; do { await Task.Delay(1000); try { var response = await httpClient.GetAsync(uri); if (response.StatusCode == HttpStatusCode.OK) { output = null; Instance.Initialize(process, pidFilePath, sentinel); return; } } catch (OperationCanceledException) { } catch (HttpRequestException) { } retries++; } while (retries < 30); // Make output null so that we stop logging to it. output = null; logOutput.CompleteAdding(); var exitCodeString = process.HasExited ? process.ExitCode.ToString(CultureInfo.InvariantCulture) : "Process has not yet exited."; var message = $@"Failed to launch the server. ExitCode: {exitCodeString} Captured output lines: {string.Join(Environment.NewLine, logOutput.GetConsumingEnumerable())}."; // If we got here, we couldn't launch Sauce connect or get it to respond. So shut it down. ProcessCleanup(process, pidFilePath); throw new InvalidOperationException(message); } private static Process StartSentinelProcess(Process process, string sentinelFile, int timeout) { // This sentinel process will start and will kill any rouge sauce connect server that wasn't torn down via normal means. var psi = new ProcessStartInfo { FileName = "powershell", Arguments = $"-NoProfile -NonInteractive -Command \"Start-Sleep {timeout}; " + $"if(Test-Path {sentinelFile}){{ " + $"Write-Output 'Stopping process {process.Id}'; Stop-Process {process.Id}; }}" + $"else{{ Write-Output 'Sentinel file {sentinelFile} not found.'}}", }; return Process.Start(psi); } private static void ProcessCleanup(Process process, string pidFilePath) { try { if (process?.HasExited == false) { try { process?.KillTree(TimeSpan.FromSeconds(10)); process?.Dispose(); } catch { // Ignore errors here since we can't do anything } } if (pidFilePath != null && File.Exists(pidFilePath)) { File.Delete(pidFilePath); } } catch { // Ignore errors here since we can't do anything } } private static async Task<string> WriteTrackingFileAsync(ITestOutputHelper output, string trackingFolder, Process process) { var pidFile = Path.Combine(trackingFolder, $"{process.Id}.{Guid.NewGuid()}.pid"); for (var i = 0; i < 3; i++) { try { await File.WriteAllTextAsync(pidFile, process.Id.ToString(CultureInfo.InvariantCulture)); return pidFile; } catch { output.WriteLine($"Can't write file to process tracking folder: {trackingFolder}"); } } throw new InvalidOperationException($"Failed to write file for process {process.Id}"); } private static string GetProcessTrackingFolder() => typeof(SauceConnectServer).Assembly .GetCustomAttributes<AssemblyMetadataAttribute>() .Single(a => a.Key == "Microsoft.AspNetCore.InternalTesting.SauceConnect.ProcessTracking").Value; public void Dispose() { ProcessCleanup(_process, _sentinelPath); ProcessCleanup(_sentinelProcess, pidFilePath: null); } }