📄 OutGridTree.Window/RpcService.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Management.Automation;
using System.Text;
using System.Threading;

namespace OutGridTree.Window;

internal sealed class RpcService
{
    public event Action<IEnumerable<string>>? HeadersReceived;
    public event Action<object?>? RecordReceived;

    private NamedPipeClientStream? pipe;
    private readonly CancellationTokenSource cts = new();
    private Thread? thread;

    public void Start(string pipeName)
    {
        pipe = new(".", pipeName, PipeDirection.In);
        thread = new(Run);
        thread.Start();
    }

    private void Run()
    {
        if (pipe is null)
        {
            return;
        }

        pipe.Connect();
        var reader = new StreamReader(pipe);
        while (reader.ReadLine(cts.Token) is string line)
        {
            if (line.Split(": ", 2) is not [var header, var body])
            {
                continue;
            }
            if (header is "HEADERS")
            {
                HeadersReceived?.Invoke(body.Split(','));
            }
            else if (header is "RECORD")
            {
                var record = PSSerializer.Deserialize(Encoding.UTF8.GetString(Convert.FromBase64String(body)));
                RecordReceived?.Invoke(record);
            }
        }
    }

    public void Stop()
    {
        cts.Cancel();
        thread?.Join();
        pipe?.Dispose();
    }
}

file static class StreamReaderExtensions
{
    public static string? ReadLine(this StreamReader reader, CancellationToken cancellationToken)
    {
        var valueTask = reader.ReadLineAsync(cancellationToken);
        if (valueTask.IsCompletedSuccessfully)
        {
            return valueTask.Result;
        }
        var task = valueTask.AsTask();
        task.Wait(cancellationToken);
        return task.Result;
    }
}