File: StartupHook.cs
Web Access
Project: ..\..\..\src\BuiltInTools\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.
 
using System.Diagnostics;
using System.IO.Pipes;
using System.Reflection;
using System.Runtime.Loader;
using Microsoft.DotNet.HotReload;
using Microsoft.DotNet.Watch;
 
/// <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 string? s_namedPipeName = Environment.GetEnvironmentVariable(AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName);
 
#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})");
 
        HotReloadAgent.ClearHotReloadEnvironmentVariables(typeof(StartupHook));
 
        if (string.IsNullOrEmpty(s_namedPipeName))
        {
            Log($"Environment variable {AgentEnvironmentVariables.DotNetWatchHotReloadNamedPipeName} has no value");
            return;
        }
 
        RegisterSignalHandlers();
 
        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;
        });
 
        var listener = new PipeListener(s_namedPipeName, agent, Log);
 
        // fire and forget:
        _ = listener.Listen(CancellationToken.None);
    }
 
    private static void RegisterSignalHandlers()
    {
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            ProcessUtilities.EnableWindowsCtrlCHandling(Log);
        }
        else
        {
#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 void Log(string message)
    {
        var prefix = s_standardOutputLogPrefix;
        if (!string.IsNullOrEmpty(prefix))
        {
            Console.ForegroundColor = ConsoleColor.DarkGray;
            Console.WriteLine($"{prefix} {message}");
            Console.ResetColor();
        }
    }
}