Files
LlamaCasty/ytLive.Tests/AudioPipeInstanceTests.cs
T
gramps dbcb69278f fix(audio): a DEBUG build never shares the pipe name with Release
Creator was recording on the Release build and hit "Couldn't start the audio — another copy of
LlamaCasty is already using the audio pipe" when going live on Debug. The guard did its job (no
crash), but the collision should not have been possible: the pipe name only suffixed when
`YTLIVE_INSTANCE` was set, and a Debug launched straight from the IDE has no id — so it asked for
the same `ytllive_audio` the Release build was serving.

DEBUG now always suffixes: the claimed instance id when there is one, otherwise the process id. Any
number of dev builds can run beside the Release app, and each Debug process is distinct from every
other. Release is unchanged — it resolves to the plain name, and the whole mechanism disappears in
`#if DEBUG` as before.

`TryStartAudioPipe` now logs the resolved name (`Audio pipe: …`), so the next report of this kind is
one grep instead of a code hunt.

Tests: AudioPipeInstanceTests pins that an id-less DEBUG process never resolves to the bare name;
572 green, 0 warnings.
2026-11-06 12:30:48 -08:00

90 lines
3.5 KiB
C#

using System;
using System.IO;
using System.IO.Pipes;
using Xunit;
using ytLive.Helpers;
using ytLive.Services.Audio;
namespace ytLive.Tests;
/// <summary>
/// 2026-11-06 crash: two LlamaCasty instances on one machine, the second one died on the Go Live click with
/// <c>IOException: All pipe instances are busy</c>. The audio pipe server allows exactly ONE instance and the
/// name was a shared constant, so this pins both halves of the fix — the collision is real, and each dev
/// instance now gets its own name.
/// </summary>
public sealed class AudioPipeInstanceTests : IDisposable
{
private readonly string? _original = Environment.GetEnvironmentVariable(InstanceProfile.InstanceVariable);
public void Dispose() => Environment.SetEnvironmentVariable(InstanceProfile.InstanceVariable, _original);
[Fact]
public void ASecondServerOnTheSameName_CannotStart_WhichIsWhatKilledTheSecondInstance()
{
var name = $"ytllive_test_{Guid.NewGuid():N}";
using var first = new NamedPipeServerStream(name, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
var error = Record.Exception(() =>
new NamedPipeServerStream(name, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous));
Assert.NotNull(error);
Assert.IsType<IOException>(error);
}
[Fact]
public void ADevInstanceServesItsOwnPipeName()
{
Environment.SetEnvironmentVariable(InstanceProfile.InstanceVariable, null);
#if DEBUG
// No claimed id (launched from the IDE) must still never collide with the Release build's pipe.
var unnamed = InstanceProfile.AudioPipeName("ytllive_audio");
Assert.StartsWith("ytllive_audio_pid", unnamed);
Assert.Equal(unnamed, InstanceProfile.AudioPipeName("ytllive_audio"));
#else
Assert.Equal("ytllive_audio", InstanceProfile.AudioPipeName("ytllive_audio"));
#endif
Environment.SetEnvironmentVariable(InstanceProfile.InstanceVariable, "2");
Assert.Equal("ytllive_audio_2", InstanceProfile.AudioPipeName("ytllive_audio"));
Environment.SetEnvironmentVariable(InstanceProfile.InstanceVariable, "qa");
Assert.Equal("ytllive_audio_qa", InstanceProfile.AudioPipeName("ytllive_audio"));
Assert.NotEqual(InstanceProfile.AudioPipeName("ytllive_audio"),
WithInstance("3", () => InstanceProfile.AudioPipeName("ytllive_audio")));
}
private static string WithInstance(string id, Func<string> read)
{
var original = Environment.GetEnvironmentVariable(InstanceProfile.InstanceVariable);
try
{
Environment.SetEnvironmentVariable(InstanceProfile.InstanceVariable, id);
return read();
}
finally
{
Environment.SetEnvironmentVariable(InstanceProfile.InstanceVariable, original);
}
}
[Fact]
public void TheMixerRefusesAStartedSecondTime_ButAWriterOnATakenNameThrows()
{
// The mixer's own guard is idempotent (StartLive twice is a no-op); the exception came from the
// pipe itself, which the VM now catches and reports instead of letting it end the process.
var name = $"ytllive_test_{Guid.NewGuid():N}";
var writer = new NamedPipeAudioWriter();
writer.Start(name);
try
{
var second = Record.Exception(() => new NamedPipeAudioWriter().Start(name));
Assert.IsType<IOException>(second);
}
finally
{
writer.Stop();
}
}
}