File: TrayActivationTests.cs
Web Access
Project: src\tests\Aspire.Tray.Tests\Aspire.Tray.Tests.csproj (Aspire.Tray.Tests)
// 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.Diagnostics;
using System.IO.Pipes;
 
namespace Aspire.Tray.Tests;
 
public class TrayActivationTests
{
    [Fact]
    public async Task RepeatedStartsRestoreTheExistingInstanceAndWaitForUiAcknowledgement()
    {
        var name = PipeName();
        var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var restored = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var calls = 0;
        var server = new TrayActivation(name, async cancellationToken =>
        {
            Interlocked.Increment(ref calls);
            entered.TrySetResult();
            await restored.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
        }, _ => Task.CompletedTask, UnexpectedStop);
        await using var lifetime = server.ConfigureAwait(true);
 
        var first = TrayActivation.ShowExistingAsync(name, TestContext.Current.CancellationToken);
        await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        Assert.False(first.IsCompleted);
        restored.SetResult();
        await first.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        await TrayActivation.ShowExistingAsync(name, TestContext.Current.CancellationToken);
        Assert.Equal(2, Volatile.Read(ref calls));
    }
 
    [Fact]
    public async Task FailedNativeRestorationIsNotReportedAsSuccess()
    {
        var name = PipeName();
        var server = new TrayActivation(name, _ => throw new InvalidOperationException("Native operation failed."),
            _ => Task.CompletedTask, UnexpectedStop);
        await using var lifetime = server.ConfigureAwait(true);
 
        var error = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            TrayActivation.ShowExistingAsync(name, TestContext.Current.CancellationToken));
        Assert.Equal("The running tray could not restore its icon.", error.Message);
    }
 
    [Fact]
    public async Task UnsupportedRequestsAreRejectedWithoutTouchingTheUi()
    {
        var name = PipeName();
        var calls = 0;
        var server = new TrayActivation(name, _ =>
        {
            Interlocked.Increment(ref calls);
            return Task.CompletedTask;
        }, _ => Task.CompletedTask, UnexpectedStop);
        await using var lifetime = server.ConfigureAwait(true);
        using (var client = ConnectClient(name))
        {
            await client.ConnectAsync(TestContext.Current.CancellationToken);
            await client.WriteAsync(new byte[] { 99 }, TestContext.Current.CancellationToken);
            var response = new byte[13];
            await client.ReadExactlyAsync(response, TestContext.Current.CancellationToken);
            Assert.Equal(0, response[0]);
            using var process = Process.GetCurrentProcess();
            Assert.Equal(process.Id, BinaryPrimitives.ReadInt32LittleEndian(response.AsSpan(1)));
            Assert.Equal(process.StartTime.ToUniversalTime().Ticks, BinaryPrimitives.ReadInt64LittleEndian(response.AsSpan(5)));
            if (OperatingSystem.IsWindows())
            {
                await client.WriteAsync(new byte[] { 1 }, TestContext.Current.CancellationToken);
            }
        }
        Assert.Equal(0, Volatile.Read(ref calls));
        await TrayActivation.ShowExistingAsync(name, TestContext.Current.CancellationToken);
        Assert.Equal(1, Volatile.Read(ref calls));
    }
 
    [Fact]
    public async Task AClientThatDisconnectsWithoutARequestDoesNotBreakTheListener()
    {
        var name = PipeName();
        var server = new TrayActivation(name, _ => Task.CompletedTask, _ => Task.CompletedTask, UnexpectedStop);
        await using var lifetime = server.ConfigureAwait(true);
        using (var client = ConnectClient(name))
        {
            await client.ConnectAsync(TestContext.Current.CancellationToken);
        }
 
        await TrayActivation.ShowExistingAsync(name, TestContext.Current.CancellationToken);
    }
 
    [Fact]
    public async Task ASlowClientCannotKeepTheOnlyConnectionForever()
    {
        var name = PipeName();
        var calls = 0;
        var server = new TrayActivation(name, _ =>
        {
            Interlocked.Increment(ref calls);
            return Task.CompletedTask;
        }, _ => Task.CompletedTask, UnexpectedStop, TimeSpan.FromSeconds(1));
        await using var lifetime = server.ConfigureAwait(true);
        using var stalled = ConnectClient(name);
        await stalled.ConnectAsync(TestContext.Current.CancellationToken);
        var buffer = new byte[1];
        Assert.Equal(0, await stalled.ReadAsync(buffer, TestContext.Current.CancellationToken));
        Assert.Equal(0, Volatile.Read(ref calls));
 
        await TrayActivation.ShowExistingAsync(name, TestContext.Current.CancellationToken);
        Assert.Equal(1, Volatile.Read(ref calls));
    }
 
    [Fact]
    public async Task ShutdownCancelsAnActivationWaitingForTheUi()
    {
        var name = PipeName();
        var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var cancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var server = new TrayActivation(name, async cancellationToken =>
        {
            entered.TrySetResult();
            try
            {
                await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false);
            }
            finally
            {
                cancelled.TrySetResult();
            }
        }, _ => Task.CompletedTask, UnexpectedStop);
        var request = TrayActivation.ShowExistingAsync(name, TestContext.Current.CancellationToken);
        try
        {
            await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        }
        finally
        {
            await server.DisposeAsync();
        }
        await cancelled.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        await Assert.ThrowsAnyAsync<IOException>(() => request);
    }
 
    [Fact]
    public async Task MissingListenerHonorsCancellationInsteadOfStartingAnotherTray()
    {
        using var cancellation = new CancellationTokenSource();
        var request = TrayActivation.ShowExistingAsync(PipeName(), cancellation.Token);
        await cancellation.CancelAsync();
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => request);
    }
 
    [Fact]
    public async Task ListenerCanShutDownWithoutAnyClients()
    {
        var server = new TrayActivation(PipeName(), _ => Task.CompletedTask, _ => Task.CompletedTask, UnexpectedStop);
        await server.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
    }
 
    [Fact]
    public async Task ReadinessWaitsForTheUiWithoutRestoringPlacement()
    {
        var name = PipeName();
        var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var restored = false;
        var server = new TrayActivation(name, _ =>
        {
            restored = true;
            return Task.CompletedTask;
        }, token =>
        {
            entered.TrySetResult();
            return ready.Task.WaitAsync(token);
        }, UnexpectedStop);
        await using var lifetime = server.ConfigureAwait(true);
 
        var request = TrayActivation.WaitUntilReadyAsync(name, TestContext.Current.CancellationToken);
        await entered.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        Assert.False(request.IsCompleted);
        ready.SetResult();
        var identity = await request.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        using var process = Process.GetCurrentProcess();
        Assert.Equal(new(process.Id, process.StartTime.ToUniversalTime().Ticks), identity);
        Assert.False(restored);
    }
 
    [Fact]
    public async Task StopReturnsTheExactTrayIdentityAndRequestsQuit()
    {
        var name = PipeName();
        var stopped = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var restored = false;
        var server = new TrayActivation(name, _ =>
        {
            restored = true;
            return Task.CompletedTask;
        }, _ => Task.CompletedTask, () => stopped.TrySetResult());
        await using var lifetime = server.ConfigureAwait(true);
 
        var identity = await TrayActivation.StopExistingAsync(name, TestContext.Current.CancellationToken);
        await stopped.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        using var process = Process.GetCurrentProcess();
        Assert.Equal(new(process.Id, process.StartTime.ToUniversalTime().Ticks), identity);
        Assert.False(restored);
    }
 
    [Fact]
    public async Task AFailedReadinessCheckDoesNotRequestQuit()
    {
        var name = PipeName();
        var stopped = false;
        var server = new TrayActivation(name, _ => Task.CompletedTask,
            _ => throw new InvalidOperationException("UI unavailable."), () => stopped = true);
        await using var lifetime = server.ConfigureAwait(true);
 
        var error = await Assert.ThrowsAsync<InvalidOperationException>(() =>
            TrayActivation.StopExistingAsync(name, TestContext.Current.CancellationToken));
        Assert.Equal("The running tray could not accept the stop request.", error.Message);
        Assert.False(stopped);
    }
 
    [Fact]
    public async Task StopAcknowledgementSurvivesImmediateListenerShutdown()
    {
        var name = PipeName();
        var quit = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var server = new TrayActivation(name, _ => Task.CompletedTask,
            _ => Task.CompletedTask, () => quit.TrySetResult());
        var shutdown = ShutdownAsync();
        try
        {
            var identity = await TrayActivation.StopExistingAsync(name, TestContext.Current.CancellationToken);
            Assert.Equal(Environment.ProcessId, identity.ProcessId);
        }
        finally
        {
            quit.TrySetResult();
            await shutdown.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        }
 
        async Task ShutdownAsync()
        {
            await quit.Task.ConfigureAwait(false);
            await server.DisposeAsync().ConfigureAwait(false);
        }
    }
 
    [Fact]
    public async Task AnInvalidProcessIdentityIsRejected()
    {
        var name = PipeName();
        using var server = new NamedPipeServerStream(name, PipeDirection.InOut, 1,
            PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
        var request = TrayActivation.StopExistingAsync(name, TestContext.Current.CancellationToken);
        await server.WaitForConnectionAsync(TestContext.Current.CancellationToken);
        var command = new byte[1];
        await server.ReadExactlyAsync(command, TestContext.Current.CancellationToken);
        Assert.Equal(2, command[0]);
        var response = new byte[13];
        response[0] = 1;
        await server.WriteAsync(response, TestContext.Current.CancellationToken);
        if (OperatingSystem.IsWindows())
        {
            // The Windows control protocol requires the peer to consume the [1] reply
            // acknowledgement; otherwise the client can block before validating the identity.
            var acknowledgement = new byte[1];
            await server.ReadExactlyAsync(acknowledgement, TestContext.Current.CancellationToken);
            Assert.Equal(1, acknowledgement[0]);
        }
 
        await Assert.ThrowsAsync<InvalidDataException>(() => request);
    }
 
    [Fact]
    public async Task ExitWaitNeverTargetsAReusedProcessId()
    {
        using var process = Process.GetCurrentProcess();
        var identity = new TrayProcessIdentity(process.Id, process.StartTime.ToUniversalTime().Ticks + 1);
        await identity.WaitForExitAsync(TestContext.Current.CancellationToken)
            .WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
        Assert.False(process.HasExited);
    }
 
    private static void UnexpectedStop() => throw new InvalidOperationException("Unexpected stop request.");
 
    // macOS includes its long per-user temporary directory in the 104-byte socket limit.
    private static string PipeName() => $"at-{Guid.NewGuid():N}";
 
    private static NamedPipeClientStream ConnectClient(string name)
        => new(".", name, PipeDirection.InOut, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
}