File: src\Aspire.Tray\Common\TrayStartupEntry.cs
Web Access
Project: src\src\Aspire.Tray\Windows\Aspire.Tray.Windows.csproj (aspire-tray)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers.Binary;
using System.Reflection.PortableExecutable;
 
namespace Aspire.Tray;
 
/// <summary>
/// Validates the installed CLI entry point separately from the current backend.
/// </summary>
internal static class TrayStartupEntry
{
    internal const string Unavailable = "Launch at sign-in requires a verified stable native CLI installation. Start the tray from a script/PR-installed Aspire CLI; package-manager, unmanaged, and development paths cannot be registered automatically.";
 
    internal static string? GetUnavailableReason(TrayOptions options, bool nativeFrontend, bool windows)
    {
        if (!nativeFrontend || options.SmokeSeconds is not null || options.StartupCliPath is not { } startup || options.BundleRoot is null
            || !Path.IsPathFullyQualified(startup) || !File.Exists(startup))
        {
            return Unavailable;
        }
        try
        {
            var resolved = Resolve(startup);
            var root = Resolve(options.BundleRoot);
            if (IsPackageStorePath(startup) || IsPackageStorePath(resolved))
            {
                return Unavailable;
            }
            if (IsInside(startup, options.BundleRoot, windows) || IsInside(resolved, root, windows))
            {
                return "Launch at sign-in cannot use a CLI inside an extracted version bundle.";
            }
            if (!string.Equals(resolved, Resolve(options.CliPath), windows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)
                || !IsNativeExecutable(resolved, windows, requireGui: false))
            {
                return Unavailable;
            }
            return null;
        }
        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or BadImageFormatException)
        {
            return "The installed native CLI could not be verified for launch at sign-in.";
        }
    }
 
    internal static bool IsNativeExecutable(string path, bool windows, bool requireGui)
    {
        // Managed apphosts are native PE/Mach-O loaders, but depend on an adjacent runtime
        // config. Do not mistake a development apphost for the self-contained native CLI.
        if (File.Exists(Path.ChangeExtension(path, ".runtimeconfig.json")))
        {
            return false;
        }
        using var stream = File.OpenRead(path);
        if (windows)
        {
            using var reader = new PEReader(stream);
            var headers = reader.PEHeaders;
            return headers.CoffHeader.Machine is Machine.Amd64 or Machine.Arm64
                && headers.PEHeader?.Magic == PEMagic.PE32Plus && headers.CorHeader is null
                && (headers.CoffHeader.Characteristics & Characteristics.Dll) == 0
                && headers.PEHeader.Subsystem == (requireGui ? Subsystem.WindowsGui : Subsystem.WindowsCui);
        }
 
        Span<byte> header = stackalloc byte[16];
        if (stream.Read(header) != header.Length)
        {
            return false;
        }
        // Mach-O 64-bit little-endian MH_EXECUTE, or a universal container whose first
        // slice has that executable header. Never accept a dylib or a shell script.
        // https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h
        if (BinaryPrimitives.ReadUInt32BigEndian(header) == 0xcafebabe)
        {
            stream.Position = 16; // fat_header (8) + cputype/cpusubtype (8)
            Span<byte> offset = stackalloc byte[4];
            stream.ReadExactly(offset);
            stream.Position = BinaryPrimitives.ReadUInt32BigEndian(offset);
            stream.ReadExactly(header);
        }
        return BinaryPrimitives.ReadUInt32LittleEndian(header) == 0xfeedfacf
            && BinaryPrimitives.ReadUInt32LittleEndian(header[12..]) == 2;
    }
 
    private static bool IsInside(string path, string directory, bool windows)
    {
        var comparison = windows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
        var root = Path.TrimEndingDirectorySeparator(Path.GetFullPath(directory));
        var full = Path.GetFullPath(path);
        return full.Equals(root, comparison) || full.StartsWith(root + Path.DirectorySeparatorChar, comparison);
    }
 
    private static bool IsPackageStorePath(string path)
        => Path.GetFullPath(path).Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar],
            StringSplitOptions.RemoveEmptyEntries).Any(component =>
                component.Equals("node_modules", StringComparison.OrdinalIgnoreCase)
                || component.Equals(".store", StringComparison.OrdinalIgnoreCase));
 
    private static string Resolve(string path)
        => ResolveCore(path, 32);
 
    private static string ResolveCore(string path, int remainingLinks)
    {
        if (remainingLinks == 0)
        {
            throw new IOException("The installed CLI contains too many symbolic links.");
        }
        var full = Path.GetFullPath(path);
        var current = Path.GetPathRoot(full)!;
        foreach (var component in full[current.Length..].Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries))
        {
            current = Path.Combine(current, component);
            FileSystemInfo entry = Directory.Exists(current) ? new DirectoryInfo(current) : new FileInfo(current);
            if (entry.ResolveLinkTarget(returnFinalTarget: true) is { } target)
            {
                // A link target can itself contain linked ancestors (for example macOS
                // /var -> /private/var). Normalize those too before comparing installations.
                current = ResolveCore(target.FullName, remainingLinks - 1);
            }
        }
        return Path.GetFullPath(current);
    }
}