File: Program.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.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using Aspire.Shared;
using Microsoft.Win32;
 
namespace Aspire.Tray;
 
internal static class Program
{
    [STAThread]
    private static int Main(string[] args)
    {
        if (args is ["--help"])
        {
            Console.WriteLine(TrayOptions.Usage);
            return 0;
        }
        if (!OperatingSystem.IsWindows())
        {
            Console.Error.WriteLine("This tray frontend requires Windows.");
            return 1;
        }
 
        return RunWindows(args);
    }
 
    [SupportedOSPlatform("windows")]
    private static int RunWindows(string[] args)
    {
        var smoke = args.Contains("--smoke-seconds", StringComparer.Ordinal);
        var helper = args.FirstOrDefault() is "start" or "stop" or "login-start";
        WindowsTrayLog? log = null;
        try
        {
            // This mode also runs from the stable copied WinExe. Keep it ahead of UI,
            // icon, singleton, bundle, and startup-settings initialization.
            if (args is ["login-start", "--cli", var startupCli])
            {
                log = new WindowsTrayLog();
                WindowsLoginBootstrap.RunAsync(startupCli).GetAwaiter().GetResult();
                Log("Sign-in tray start completed.");
                return 0;
            }
            if (args.FirstOrDefault() == "login-start")
            {
                throw new ArgumentException("Usage: aspire-tray-login.exe login-start --cli <absolute native Aspire CLI path>");
            }
            if (args is ["stop"])
            {
                WindowsTrayLauncher.StopAsync().GetAwaiter().GetResult();
                return 0;
            }
            if (args is ["start", .. var startArgs])
            {
                WindowsTrayLauncher.StartAsync(TrayOptions.Parse(startArgs)).GetAwaiter().GetResult();
                return 0;
            }
            if (!smoke && !helper)
            {
                log = new WindowsTrayLog();
            }
            var options = TrayOptions.Parse(args);
            if (options.SmokeSeconds is int seconds)
            {
                return NativeSmokeHarness.Run(options.CliPath, seconds, options.InteractiveSmoke);
            }
 
            // Run, acquire, and release the named mutex on the initial STA thread.
            using var instance = WindowsSingleInstance.TryAcquire();
            if (instance is null)
            {
                TrayActivation.ShowExistingAsync(WindowsSingleInstance.ActivationPipeName, CancellationToken.None).GetAwaiter().GetResult();
                Log("Restored the running Aspire tray icon.");
                return 0;
            }
 
            using var lease = options.BundleRoot is null
                ? null : BundleVersionLease.Acquire(options.BundleRoot, "tray", "tray");
            var controller = new TrayController(new CliAppHostClient(options.CliPath),
                FileTraySavedStateStore.CreateWithLegacyMigration(
                    TrayConfiguration.GetSavedStatePath(options.CliPath), WindowsSingleInstance.DirectoryPath),
                TrayConfiguration.LoadRecentAppHostLimit(TrayConfiguration.GetSettingsPath(options.CliPath)));
            TrayApplication? tray = null;
            TrayActivation? activation = null;
            try
            {
                var startupSettings = new WindowsTrayStartupSettings(options, !RuntimeFeature.IsDynamicCodeSupported,
                    Environment.ProcessPath ?? string.Empty,
                    Path.Combine(WindowsSingleInstance.DirectoryPath, "Startup", "aspire-tray-login.exe"),
                    new WindowsRunStartupRegistrationStore(Registry.CurrentUser,
                        WindowsRunStartupRegistrationStore.RunKeyPath, WindowsRunStartupRegistrationStore.RunValueName));
                tray = new TrayApplication(controller, smokeSeconds: null, startupSettings);
                activation = new TrayActivation(WindowsSingleInstance.ActivationPipeName,
                    tray.RestoreIconAsync, tray.WaitUntilReadyAsync, tray.RequestQuit);
                Log("Starting the Windows Aspire tray.");
                controller.Start();
                tray.Run();
            }
            finally
            {
                // Join IPC and controller producers while their native dispatcher is still
                // alive. Keep the bundle lease, diagnostic streams, and mutex through cleanup.
                try
                {
                    activation?.DisposeAsync().AsTask().GetAwaiter().GetResult();
                }
                finally
                {
                    try
                    {
                        controller.DisposeAsync().AsTask().GetAwaiter().GetResult();
                    }
                    finally
                    {
                        tray?.Dispose();
                    }
                }
            }
            Log($"The Windows Aspire tray stopped (exit {tray.ExitCode}).");
            return tray.ExitCode;
        }
        catch (Exception ex)
        {
            var message = ex switch
            {
                NativeCallException => ex.Message,
                ArgumentException or TimeoutException or InvalidOperationException or FileNotFoundException => ex.Message,
                Win32Exception native => $"Windows startup failed (native error {native.NativeErrorCode}). {native.Message}",
                _ => $"The tray failed ({ex.GetType().Name})."
            };
            // Helpers and malformed smoke invocations must never block automation on a modal dialog.
            Log(message);
            return 1;
        }
        finally
        {
            log?.Dispose();
        }
    }
 
    [SupportedOSPlatform("windows")]
    internal static void Log(string message)
    {
        try
        {
            Console.Error.WriteLine(message);
        }
        catch (IOException ex)
        {
            // Never let a closed console or failing disk throw through a native callback.
            // Preserve the failure in the debugger channel instead of silently swallowing it.
            NativeMethods.OutputDebugString($"Tray diagnostic write failed ({ex.GetType().Name}).{Environment.NewLine}");
        }
        NativeMethods.OutputDebugString(message + Environment.NewLine);
    }
}