SystemTrayMenu/Business/IpcPipe.cs
Peter Kirmeier 0e812d207b Allow waking up another process when started a second time.
This was not possible before and was solved by sending hotkey in version 1.
In v2 it will use named pipes for IPC communication, so it don't rely on hotkeys any more.
2023-06-03 20:18:10 +02:00

161 lines
5.2 KiB
C#

// <copyright file="IpcPipe.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
//
// Copyright (c) 2023-2023 Peter Kirmeier
// Based on example: https://learn.microsoft.com/en-us/dotnet/standard/io/how-to-use-named-pipes-for-network-interprocess-communication
namespace SystemTrayMenu.Business
{
using System;
using System.IO;
using System.IO.Pipes;
using System.Reflection;
using System.Text;
using System.Threading;
using SystemTrayMenu.Utilities;
internal class IpcPipe : IDisposable
{
private readonly string pipeName;
private readonly string pipeHello;
private NamedPipeServerStream? serverPipe;
private Thread? serverThread;
private Func<string, string>? serverHandler;
private bool isStopped;
internal IpcPipe(byte version, string name)
{
pipeName = version.ToString() + "." + name + "." + (Assembly.GetExecutingAssembly().GetName().Name ?? "local");
pipeHello = "Hello " + nameof(IpcPipe) + " from " + pipeName + "!";
}
public void Dispose()
{
isStopped = true;
serverPipe?.Disconnect();
serverThread?.Join(250);
serverPipe?.Dispose();
serverThread = null;
}
internal void StartServer(Func<string, string> handler)
{
serverHandler = handler;
serverThread = new(ServerThread);
serverThread?.Start(this);
}
internal string? SendToServer(string request)
{
string? response = null;
NamedPipeClientStream pipeClient = new(pipeName);
// Wait a bit in case server is restarting
pipeClient.Connect(500);
IpcPipeStream stream = new (pipeClient);
string hello = stream.ReadString();
// Check who there is, just for sanity
if (hello == pipeHello)
{
stream.WriteString(request);
response = stream.ReadString();
}
else
{
Log.Info($"IPC client pipe error: Invalid hello: \"" + hello + "\"");
}
pipeClient.Dispose();
return response;
}
private static void ServerThread(object? data)
{
IpcPipe ipc = (IpcPipe)data!;
Func<string, string> handler = ipc.serverHandler ?? (_ => string.Empty);
while (!ipc.isStopped)
{
NamedPipeServerStream pipe = ipc.serverPipe = new(ipc.pipeName);
try
{
pipe.WaitForConnection();
IpcPipeStream stream = new(pipe);
// Send indicator who we are, just for sanity checking
stream.WriteString(ipc.pipeHello);
string request = stream.ReadString();
string respone = handler.Invoke(request);
stream.WriteString(respone);
}
catch (Exception ex)
{
Log.Warn($"IPC server pipe error: ", ex);
}
ipc.serverPipe = null;
pipe.Dispose();
}
}
// Defines the data protocol for reading and writing strings on our stream
private class IpcPipeStream
{
private readonly Stream ioStream;
private readonly UnicodeEncoding streamEncoding = new();
internal IpcPipeStream(Stream ioStream)
{
this.ioStream = ioStream;
}
internal string ReadString()
{
int len = 0;
// Receive 32 bit message size
len += ioStream.ReadByte() >> 24;
len += ioStream.ReadByte() >> 16;
len += ioStream.ReadByte() >> 8;
len += ioStream.ReadByte() >> 0;
// Receive message
byte[] inBuffer = new byte[len];
ioStream.Read(inBuffer, 0, len);
return streamEncoding.GetString(inBuffer);
}
internal int WriteString(string outString)
{
byte[] outBuffer = streamEncoding.GetBytes(outString);
int len = outBuffer.Length;
if (len > int.MaxValue)
{
throw new InternalBufferOverflowException("More data than available space withing an IPC message");
}
// Send 32 bit message size
ioStream.WriteByte((byte)((len >> 24) & 0xFF));
ioStream.WriteByte((byte)((len >> 16) & 0xFF));
ioStream.WriteByte((byte)((len >> 8) & 0xFF));
ioStream.WriteByte((byte)((len >> 0) & 0xFF));
// Send message
ioStream.Write(outBuffer, 0, len);
ioStream.Flush();
return outBuffer.Length + 2;
}
}
}
}