| File: TerminalHostProcessRunner.cs | Web Access |
| Project: src\src\Aspire.TerminalHost\Aspire.TerminalHost.csproj (Aspire.TerminalHost) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; using Aspire.Hosting; using Aspire.Shared; namespace Aspire.TerminalHost; /// <summary> /// Runs a terminal host with process-level graceful shutdown handling. /// </summary> public static class TerminalHostProcessRunner { /// <summary> /// Runs a terminal host until it exits, cancellation is requested, or the process receives /// SIGINT or SIGTERM. /// </summary> public static async Task<int> RunAsync(string[] args, CancellationToken cancellationToken = default) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); using var forceExitCts = new CancellationTokenSource(); var parentWatchdog = ParentProcessWatchdog.Start( cts, KnownConfigNames.TerminalHostParentProcessId, KnownConfigNames.TerminalHostParentProcessStartedStable, legacyStartVariable: null); var terminationSignalReceived = 0; bool TryRequestGracefulShutdown() { if (Interlocked.Exchange(ref terminationSignalReceived, 1) != 0) { return false; } _ = ParentProcessWatchdog.CancelAndForceExitAsync( cts, forceExitCts.Token, ParentProcessWatchdog.ForceExitGracePeriod, Environment.Exit); return true; } void OnPosixSignal(PosixSignalContext context) { // The first signal grants TerminalHostApp a bounded window to unlink its sockets. // A second signal retains the platform default so operators can terminate immediately. context.Cancel = TryRequestGracefulShutdown(); } PosixSignalRegistration? sigIntRegistration = null; PosixSignalRegistration? sigTermRegistration = null; PosixSignalRegistration? sigQuitRegistration = null; ConsoleCancelEventHandler? cancelKeyPressHandler = null; try { // PosixSignalRegistration also maps Windows console control events. Win32 // TerminateProcess is not interceptable, so forced DCP termination on Windows can // bypass this cleanup; AppHost exact-path cleanup and the next startup sweep are the // backstops for that platform. if (!OperatingSystem.IsBrowser() && !OperatingSystem.IsIOS() && !OperatingSystem.IsTvOS() && !OperatingSystem.IsAndroid()) { sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, OnPosixSignal); sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, OnPosixSignal); // SIGQUIT maps to Ctrl+Break on Windows. Preserve the previous // Console.CancelKeyPress behavior without intercepting Unix core dumps. if (OperatingSystem.IsWindows()) { sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, OnPosixSignal); } } else { // Terminal hosts are desktop processes, but retain Ctrl+C behavior if this // executable is ever used on a platform without PosixSignalRegistration. cancelKeyPressHandler = (_, eventArgs) => { eventArgs.Cancel = TryRequestGracefulShutdown(); }; Console.CancelKeyPress += cancelKeyPressHandler; } return await TerminalHostApp.RunAsync(args, cts.Token).ConfigureAwait(false); } finally { forceExitCts.Cancel(); sigIntRegistration?.Dispose(); sigTermRegistration?.Dispose(); sigQuitRegistration?.Dispose(); if (cancelKeyPressHandler is not null) { Console.CancelKeyPress -= cancelKeyPressHandler; } if (parentWatchdog is not null) { await parentWatchdog.DisposeAsync().ConfigureAwait(false); } } } }