File: src\Aspire.Tray\Common\TrayLaunchCommand.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.Diagnostics;
using System.Text;
 
namespace Aspire.Tray;
 
internal static class TrayLaunchCommand
{
    public static ProcessStartInfo CreateWindowsStartInfo(TrayOptions options)
    {
        if (options.SmokeSeconds is not null || options.BundleRoot is null)
        {
            throw new ArgumentException("Starting the packaged tray requires --bundle-root and does not support smoke mode.");
        }
        if (!Path.IsPathFullyQualified(options.BundleRoot) || !Path.IsPathFullyQualified(options.CliPath))
        {
            throw new ArgumentException("Absolute bundle and CLI paths are required.");
        }
 
        var executable = Path.Combine(options.BundleRoot, "tray", "aspire-tray.exe");
        if (!File.Exists(executable))
        {
            throw new FileNotFoundException("The bundle does not contain the Windows Aspire Tray executable.", executable);
        }
        if (!File.Exists(options.CliPath))
        {
            throw new FileNotFoundException("The Aspire CLI executable was not found.", options.CliPath);
        }
 
        var startInfo = new ProcessStartInfo(executable)
        {
            UseShellExecute = false,
            CreateNoWindow = true,
            WorkingDirectory = Path.GetDirectoryName(executable)!
        };
        foreach (var argument in new[] { "--cli", options.CliPath, "--bundle-root", options.BundleRoot })
        {
            startInfo.ArgumentList.Add(argument);
        }
        AddStartupCli(startInfo, options.StartupCliPath);
 
        return startInfo;
    }
 
    internal static string BuildWindowsCommandLine(ProcessStartInfo startInfo)
    {
        var command = new StringBuilder();
        AppendQuoted(startInfo.FileName);
        foreach (var argument in startInfo.ArgumentList)
        {
            command.Append(' ');
            AppendQuoted(argument);
        }
 
        return command.ToString();
 
        void AppendQuoted(string argument)
        {
            // CreateProcess consumes a command line, not argv. Match the CRT/.NET rules:
            // a literal quote needs 2n+1 preceding backslashes; a closing quote needs 2n.
            // For example, the argument C:\a b\ becomes "C:\a b\\".
            // https://learn.microsoft.com/cpp/c-language/parsing-c-command-line-arguments
            // This is the same algorithm used by the CLI's WindowsProcessInterop.
            command.Append('"');
            var slashes = 0;
            foreach (var character in argument)
            {
                if (character == '\\')
                {
                    slashes++;
                    continue;
                }
                command.Append('\\', character == '"' ? (slashes * 2) + 1 : slashes);
                command.Append(character);
                slashes = 0;
            }
            command.Append('\\', slashes * 2);
            command.Append('"');
        }
    }
 
    public static ProcessStartInfo CreateStartInfo(TrayOptions options)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(options.BundleRoot);
 
        var appPath = Path.Combine(options.BundleRoot, "tray", "Aspire Tray.app");
        var executable = Path.Combine(appPath, "Contents", "MacOS", "aspire-tray");
        if (!File.Exists(executable) || !File.Exists(Path.Combine(appPath, "Contents", "Info.plist")))
        {
            throw new FileNotFoundException("The bundle does not contain the macOS Aspire Tray app.", appPath);
        }
 
        var startInfo = new ProcessStartInfo(executable)
        {
            UseShellExecute = false,
            WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
        };
        foreach (var argument in new[]
        {
            "--cli", options.CliPath, "--bundle-root", options.BundleRoot
        })
        {
            startInfo.ArgumentList.Add(argument);
        }
        AddStartupCli(startInfo, options.StartupCliPath);
 
        return startInfo;
    }
 
    private static void AddStartupCli(ProcessStartInfo startInfo, string? startupCli)
    {
        if (startupCli is not null)
        {
            startInfo.ArgumentList.Add("--startup-cli");
            startInfo.ArgumentList.Add(startupCli);
        }
    }
}