File: BackEnd\Handshake.cs
Web Access
Project: ..\..\..\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// 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.Diagnostics;
using System.Reflection;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Internal;
 
internal class Handshake
{
    /// <summary>
    /// Marker indicating that the next integer in the child handshake response is the PacketVersion.
    /// </summary>
    public const int PacketVersionFromChildMarker = -1;
 
    // The number is selected as an arbitrary value that is unlikely to conflict with any future sdk version.
    public const int NetTaskHostHandshakeVersion = 99;
 
    protected readonly HandshakeComponents _handshakeComponents;
 
    /// <summary>
    ///  Initializes a new instance of the <see cref="Handshake"/> class with the specified node type.
    /// </summary>
    /// <param name="nodeType">
    ///  The <see cref="HandshakeOptions"/> that specifies the type of node and configuration options for the handshake operation.
    /// </param>
    public Handshake(HandshakeOptions nodeType)
        : this(nodeType, includeSessionId: true, toolsDirectory: null)
    {
    }
 
    /// <summary>
    ///  Initializes a new instance of the <see cref="Handshake"/> class with the specified node type
    ///  and optional predefined tools directory.
    /// </summary>
    /// <param name="nodeType">
    ///  The <see cref="HandshakeOptions"/> that specifies the type of node and configuration options for the handshake operation.
    /// </param>
    /// <param name="toolsDirectory">
    ///  The directory path to use for handshake salt calculation. For some task hosts, notably the .NET TaskHost (on .NET Framework)
    ///  and the CLR2 TaskHost, this is needed to ensure the child process connects with the expected tools directory context.
    /// </param>
    public Handshake(HandshakeOptions nodeType, string toolsDirectory)
        : this(nodeType, includeSessionId: true, toolsDirectory)
    {
    }
 
    // Helper method to validate handshake option presence
    internal static bool IsHandshakeOptionEnabled(HandshakeOptions hostContext, HandshakeOptions option)
        => (hostContext & option) == option;
 
    // Source options of the handshake.
    internal HandshakeOptions HandshakeOptions { get; }
 
    protected Handshake(HandshakeOptions nodeType, bool includeSessionId, string? toolsDirectory)
    {
        HandshakeOptions = nodeType;
 
#if NETFRAMEWORK
        FrameworkErrorUtilities.VerifyThrow(
            toolsDirectory is null || IsNetTaskHost || IsClr2TaskHost,
            $"{toolsDirectory} should only be provided for .NET or CLR2 TaskHost nodes.");
#else
        // IsNetTaskHost covers the case when NET process spawns NET TaskHost.
        FrameworkErrorUtilities.VerifyThrow(
            toolsDirectory is null || IsNetTaskHost,
            $"{toolsDirectory} should not have been provided.");
#endif
 
        toolsDirectory ??= BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryRoot;
 
        // Build handshake options with version in upper bits
        const int handshakeVersion = (int)CommunicationsUtilities.handshakeVersion;
        var options = (int)nodeType | (handshakeVersion << 24);
        CommunicationsUtilities.Trace($"Building handshake for node type {nodeType}, (version {handshakeVersion}): options {options}.");
 
        // Calculate salt from environment and tools directory
        string handshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT") ?? "";
 
        int salt = CommunicationsUtilities.GetHashCode($"{handshakeSalt}{toolsDirectory}");
 
        CommunicationsUtilities.Trace($"Handshake salt is {handshakeSalt}");
        CommunicationsUtilities.Trace($"Tools directory root is {toolsDirectory}");
 
        // Get session ID if needed (expensive call)
        int sessionId = 0;
        if (includeSessionId && NativeMethods.IsWindows)
        {
            // On Windows, SessionId differentiates RDP sessions.
            // On Unix, getsid() returns the session leader PID which differs per terminal,
            // preventing cross-terminal node reuse. Use 0 since Unix doesn't need
            // RDP-style session isolation.
            using var currentProcess = Process.GetCurrentProcess();
            sessionId = currentProcess.SessionId;
        }
 
        _handshakeComponents = IsNetTaskHost
            ? CreateNetTaskHostComponents(options, salt, sessionId)
            : CreateStandardComponents(options, salt, sessionId);
    }
 
    private bool IsNetTaskHost
        => IsHandshakeOptionEnabled(HandshakeOptions, HandshakeOptions.NET | HandshakeOptions.TaskHost);
 
#if NETFRAMEWORK
    private bool IsClr2TaskHost
        => IsHandshakeOptionEnabled(HandshakeOptions, HandshakeOptions.CLR2 | HandshakeOptions.TaskHost);
#endif
 
    private static HandshakeComponents CreateNetTaskHostComponents(int options, int salt, int sessionId) => new(
        options,
        salt,
        NetTaskHostHandshakeVersion,
        NetTaskHostHandshakeVersion,
        NetTaskHostHandshakeVersion,
        NetTaskHostHandshakeVersion,
        sessionId);
 
    private static HandshakeComponents CreateStandardComponents(int options, int salt, int sessionId)
    {
        var fileVersion = new Version(FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion!);
 
        return new(
            options,
            salt,
            fileVersion.Major,
            fileVersion.Minor,
            fileVersion.Build,
            fileVersion.Revision,
            sessionId);
    }
 
    public virtual HandshakeComponents RetrieveHandshakeComponents() => new(
        CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.Options),
        CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.Salt),
        CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.FileVersionMajor),
        CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.FileVersionMinor),
        CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.FileVersionBuild),
        CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.FileVersionPrivate),
        CommunicationsUtilities.AvoidEndOfHandshakeSignal(_handshakeComponents.SessionId));
 
    public virtual string GetKey()
        => $"{_handshakeComponents.Options} {_handshakeComponents.Salt} {_handshakeComponents.FileVersionMajor} {_handshakeComponents.FileVersionMinor} {_handshakeComponents.FileVersionBuild} {_handshakeComponents.FileVersionPrivate} {_handshakeComponents.SessionId}";
 
    public virtual byte? ExpectedVersionInFirstByte
        => CommunicationsUtilities.handshakeVersion;
}