| File: tests\Shared\TestProcesses.cs | Web Access |
| Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; // Global namespace (no wrapper) so both Aspire.Cli.Tests and Aspire.Hosting.Tests can link this // shared helper without a project-specific using, matching the sibling TempDirectory.cs. internal static class TestProcesses { // A leaked long-running child lingers on a CI agent until the box is recycled. The process-lifecycle // tests kill it explicitly within seconds, but if a test aborts before its `using` disposes (or the // test host is SIGKILLed for a hangdump), the child can be orphaned. Two backstops guard against that: // // 1. Every started process is tracked and reaped on a graceful test-host shutdown (ProcessExit). // 2. The process self-terminates after a generous ceiling, so even an ungraceful SIGKILL (where // ProcessExit never runs) cannot leave it running forever. // // The ceiling only matters when normal cleanup was skipped; healthy tests never come close to it. private static readonly TimeSpan s_maxLifetime = TimeSpan.FromMinutes(5); private static readonly ConcurrentDictionary<int, Process> s_tracked = new(); private static int s_exitHookInstalled; /// <summary> /// Starts a cross-platform, long-running child process that stands in for a launcher/parent process /// in liveness tests. The caller owns the returned process and should kill and dispose it; the helper /// tracks it and reaps it on a graceful test-host shutdown, and the process self-terminates after a /// bounded lifetime as a last-resort backstop against leaks. /// </summary> public static Process StartLongRunning() { EnsureExitHookInstalled(); // Bounded stand-in for the previous indefinite `ping -t` / `tail -f /dev/null`: a trivially // available command that blocks for a fixed, generous duration and then exits on its own. // Windows: `ping -n <seconds> 127.0.0.1` sends one echo per ~second, so the count ≈ seconds. // Unix: `sleep <seconds>`. var seconds = ((int)s_maxLifetime.TotalSeconds).ToString(CultureInfo.InvariantCulture); var psi = OperatingSystem.IsWindows() ? new ProcessStartInfo("ping", $"-n {seconds} 127.0.0.1") { CreateNoWindow = true } : new ProcessStartInfo("sleep", seconds) { CreateNoWindow = true }; var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start long-running test process."); s_tracked[process.Id] = process; return process; } private static void EnsureExitHookInstalled() { if (Interlocked.Exchange(ref s_exitHookInstalled, 1) != 0) { return; } // Best-effort reaping when the test host shuts down cleanly. ProcessExit does not run on SIGKILL, // which is why StartLongRunning also caps the child's lifetime. AppDomain.CurrentDomain.ProcessExit += static (_, _) => { foreach (var process in s_tracked.Values) { try { if (!process.HasExited) { process.Kill(entireProcessTree: true); } } catch { // The process may already be gone or disposed by its owning test; nothing to do. } } }; } }