using System;
using System.IO;
using System.IO.Pipes;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
namespace CodexProviderSync.App;
///
/// Cross-process focus broker: the first instance of CodexProviderSync runs
/// this server on a per-user named pipe. Subsequent instances connect, send
/// the "FOCUS" command, and exit; the server invokes the registered handler
/// on the main thread, which is expected to bring the existing window to
/// the foreground.
///
public sealed class FocusRequestServer : IDisposable
{
private const string PipeNamePrefix = "CodexProviderSync.Focus";
private readonly Action _onFocusRequested;
private readonly CancellationTokenSource _shutdown = new();
private Task? _loop;
private bool _disposed;
public FocusRequestServer(Action onFocusRequested)
{
_onFocusRequested = onFocusRequested ?? throw new ArgumentNullException(nameof(onFocusRequested));
}
public string PipeName { get; } = BuildPipeName();
public void Start()
{
if (_loop is not null)
{
return;
}
_loop = Task.Run(RunAsync);
}
public async Task SendFocusRequestAsync(TimeSpan timeout)
{
try
{
using NamedPipeClientStream client = new(".", PipeName, PipeDirection.Out, PipeOptions.None);
await client.ConnectAsync((int)timeout.TotalMilliseconds).ConfigureAwait(false);
using StreamWriter writer = new(client) { AutoFlush = true };
await writer.WriteLineAsync("FOCUS").ConfigureAwait(false);
return true;
}
catch (TimeoutException)
{
return false;
}
catch (IOException)
{
return false;
}
}
private async Task RunAsync()
{
while (!_shutdown.IsCancellationRequested)
{
try
{
using NamedPipeServerStream server = new(
PipeName,
PipeDirection.In,
NamedPipeServerStream.MaxAllowedServerInstances,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
await server.WaitForConnectionAsync(_shutdown.Token).ConfigureAwait(false);
using StreamReader reader = new(server);
string? line = await reader.ReadLineAsync().ConfigureAwait(false);
if (string.Equals(line, "FOCUS", StringComparison.Ordinal))
{
_onFocusRequested();
}
}
catch (OperationCanceledException)
{
return;
}
catch (IOException)
{
// Pipe was broken mid-handshake; loop and create a fresh one.
}
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_shutdown.Cancel();
try
{
_loop?.Wait(TimeSpan.FromSeconds(2));
}
catch (AggregateException)
{
// The loop is allowed to fault on cancellation; nothing actionable.
}
_shutdown.Dispose();
}
private static string BuildPipeName()
{
// Per-user scope so two users on the same Windows machine do not
// collide. Identity.Name is "DOMAIN\User" on Windows; the backslash
// is illegal in pipe names so we swap it out.
string identity = WindowsIdentity.GetCurrent().Name ?? "default";
string sanitized = identity.Replace('\\', '_').Replace('/', '_').Replace(' ', '_');
return $"{PipeNamePrefix}.{sanitized}";
}
}