File: src\sdk\src\Dotnet.Watch\HotReloadAgent.Host\StartupHook.cs
Web Access
Project: src\sdk\src\Dotnet.Watch\DotNetDeltaApplier\Microsoft.Extensions.DotNetDeltaApplier.csproj (Microsoft.Extensions.DotNetDeltaApplier)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.HotReload;

/// <summary>
/// The runtime startup hook looks for top-level type named "StartupHook".
/// </summary>
internal sealed class StartupHook
{
    private static readonly string? s_standardOutputLogPrefix = Environment.GetEnvironmentVariable(AgentEnvironmentVariables.HotReloadDeltaClientLogMessages);
    private static readonly bool s_supportsConsoleColor = !OperatingSystem.IsAndroid()
                                                       && !OperatingSystem.IsIOS()
                                                       && !OperatingSystem.IsTvOS()
                                                       && !OperatingSystem.IsBrowser();
    private static readonly bool s_supportsPosixSignals = s_supportsConsoleColor;

#if NET10_0_OR_GREATER
    private static PosixSignalRegistration? s_signalRegistration;
#endif

    /// <summary>
    /// Invoked by the runtime when the containing assembly is listed in DOTNET_STARTUP_HOOKS.
    /// </summary>
    public static void Initialize()
    {
        var processPath = Environment.GetCommandLineArgs().FirstOrDefault();
        var processDir = Path.GetDirectoryName(processPath)!;

        Log($"Loaded into process: {processPath} ({typeof(StartupHook).Assembly.Location})");

        var transport = Transport.TryCreate(Log);

        HotReloadAgent.ClearHotReloadEnvironmentVariables(typeof(StartupHook));

        if (transport == null)
        {
            Log($"No hot reload endpoint configured. Set {AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName} or {AgentEnvironmentVariables.DotNetWatchHotReloadWebSocketEndpoint}");
            return;
        }

        RegisterSignalHandlers();

        Listener? listener = null;

        var agent = new HotReloadAgent(
            assemblyResolvingHandler: (_, args) =>
            {
                Log($"Resolving '{args.Name}, Version={args.Version}'");
                var path = Path.Combine(processDir, args.Name + ".dll");
                return File.Exists(path) ? AssemblyLoadContext.Default.LoadFromAssemblyPath(path) : null;
            },
            hotReloadExceptionCreateHandler: (code, message) =>
            {
                // Continue executing the code if the debugger is attached.
                // It will throw the exception and the debugger will handle it.
                if (Debugger.IsAttached)
                {
                    return;
                }

                Debug.Assert(listener != null);
                Log($"Runtime rude edit detected: '{message}'");

                SendAndForgetAsync().Wait();

                // Handle Ctrl+C to terminate gracefully:
                Console.CancelKeyPress += (_, _) => Environment.Exit(0);

                // wait for the process to be terminated by the Hot Reload client (other threads might still execute):
                Thread.Sleep(Timeout.Infinite);

                async Task SendAndForgetAsync()
                {
                    try
                    {
                        await listener.SendResponseAsync(new HotReloadExceptionCreatedNotification(code, message), CancellationToken.None).ConfigureAwait(false);
                    }
                    catch
                    {
                        // do not crash the app
                    }
                }
            });

        listener = new Listener(transport, agent, Log);

        // fire and forget:
        _ = listener.Listen(CancellationToken.None);
    }

    private static void RegisterSignalHandlers()
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            // Enables handling of Ctrl+C in a process where it was disabled.
            // 
            // If a process is launched with CREATE_NEW_PROCESS_GROUP flag
            // it allows the parent process to send Ctrl+C event to the child process,
            // but also disables Ctrl+C handlers.
            // 
            // "If the HandlerRoutine parameter is NULL, a TRUE value causes the calling process to ignore CTRL+C input,
            // and a FALSE value restores normal processing of CTRL+C input.
            // This attribute of ignoring or processing CTRL+C is inherited by child processes."

            if (SetConsoleCtrlHandler(null, false))
            {
                Log("Windows Ctrl+C handling enabled.");
            }
            else
            {
                Log($"Failed to enable Ctrl+C handling: {GetLastPInvokeErrorMessage()}");
            }

            [DllImport("kernel32.dll", SetLastError = true)]
            static extern bool SetConsoleCtrlHandler(Delegate? handler, bool add);
        }
        else if (s_supportsPosixSignals)
        {
#if NET10_0_OR_GREATER
            // Register a handler for SIGTERM to allow graceful shutdown of the application on Unix.
            // See https://github.com/dotnet/docs/issues/46226.

            // Note: registered handlers are executed in reverse order of their registration.
            // Since the startup hook is executed before any code of the application, it is the first handler registered and thus the last to run.

            s_signalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, context =>
            {
                Log($"SIGTERM received. Cancel={context.Cancel}");

                if (!context.Cancel)
                {
                    Environment.Exit(0);
                }
            });

            Log("Posix signal handlers registered.");
#endif
        }
    }

    private static string GetLastPInvokeErrorMessage()
    {
        var error = Marshal.GetLastPInvokeError();
#if NET10_0_OR_GREATER
        return $"{Marshal.GetPInvokeErrorMessage(error)} (code {error})";
#else
        return $"error code {error}";
#endif
    }

    private static void Log(string message)
    {
        var prefix = s_standardOutputLogPrefix;
        if (!string.IsNullOrEmpty(prefix))
        {
            if (s_supportsConsoleColor)
            {
                Console.ForegroundColor = ConsoleColor.DarkGray;
            }

            Console.Error.WriteLine($"{prefix} {message}");

            if (s_supportsConsoleColor)
            {
                Console.ResetColor();
            }
        }
    }
}