Step 3 — Per-OS Transport Factory
StreamJsonRpc agnostic terhadap transport — ia butuh Stream duplex. Kita buat factory yang return Stream berbeda per OS.
3.1 Interface abstraksi
HermesServices/HermesIpc.Contracts/IIpcTransport.cs
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace HermesIpc.Contracts;
/// <summary>
/// Abstraksi transport. Server: AcceptAsync(). Client: ConnectAsync().
/// </summary>
public interface IIpcTransport
{
/// <summary>Server side: tunggu koneksi client, kembalikan duplex Stream.</summary>
Task<Stream> AcceptAsync(CancellationToken ct);
/// <summary>Client side: connect ke server, kembalikan duplex Stream.</summary>
Task<Stream> ConnectAsync(CancellationToken ct);
}
3.2 Konfigurasi endpoint
HermesServices/HermesIpc.Contracts/IpcEndpoint.cs
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace HermesIpc.Contracts;
public static class IpcEndpoint
{
public const string PipeName = "HermesRpc.v1";
/// <summary>Path Unix Domain Socket pada macOS/Linux.</summary>
public static string UnixSocketPath
{
get
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return "/var/run/hermes-rpc.sock";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return "/run/hermes-rpc.sock";
// fallback dev environment
return Path.Combine(Path.GetTempPath(), "hermes-rpc.sock");
}
}
}
3.3 Implementasi Windows — Named Pipe
HermesServices/HermesIpc.Contracts/Transports/WindowsPipeTransport.cs
using System;
using System.IO;
using System.IO.Pipes;
using System.Runtime.Versioning;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
namespace HermesIpc.Contracts.Transports;
[SupportedOSPlatform("windows")]
public sealed class WindowsPipeTransport : IIpcTransport
{
private readonly string _pipeName;
private readonly bool _isServer;
private readonly PipeSecurity? _serverSecurity;
public WindowsPipeTransport(string pipeName, bool isServer, PipeSecurity? security = null)
{
_pipeName = pipeName;
_isServer = isServer;
_serverSecurity = security;
}
public async Task<Stream> AcceptAsync(CancellationToken ct)
{
if (!_isServer) throw new InvalidOperationException("Configured as client");
var server = NamedPipeServerStreamAcl.Create(
pipeName: _pipeName,
direction: PipeDirection.InOut,
maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances,
transmissionMode: PipeTransmissionMode.Byte,
options: PipeOptions.Asynchronous | PipeOptions.WriteThrough,
inBufferSize: 64 * 1024,
outBufferSize: 64 * 1024,
pipeSecurity: _serverSecurity ?? DefaultSecurity());
await server.WaitForConnectionAsync(ct).ConfigureAwait(false);
return server;
}
public async Task<Stream> ConnectAsync(CancellationToken ct)
{
if (_isServer) throw new InvalidOperationException("Configured as server");
var client = new NamedPipeClientStream(
serverName: ".",
pipeName: _pipeName,
direction: PipeDirection.InOut,
options: PipeOptions.Asynchronous | PipeOptions.WriteThrough);
await client.ConnectAsync(ct).ConfigureAwait(false);
return client;
}
/// <summary>
/// ACL default: hanya user yang sedang interactive logon + LocalSystem.
/// MENGGANTIKAN WorldSid (rawan) dari implementasi lama.
/// </summary>
private static PipeSecurity DefaultSecurity()
{
var sec = new PipeSecurity();
// LocalSystem (untuk service helper itu sendiri)
sec.AddAccessRule(new PipeAccessRule(
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
PipeAccessRights.FullControl,
AccessControlType.Allow));
// Interactive users (user yang punya UI session)
sec.AddAccessRule(new PipeAccessRule(
new SecurityIdentifier(WellKnownSidType.InteractiveSid, null),
PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance,
AccessControlType.Allow));
return sec;
}
}
3.4 Implementasi macOS/Linux — Unix Domain Socket
HermesServices/HermesIpc.Contracts/Transports/UnixSocketTransport.cs
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace HermesIpc.Contracts.Transports;
public sealed class UnixSocketTransport : IIpcTransport, IDisposable
{
private readonly string _path;
private readonly bool _isServer;
private Socket? _listener;
public UnixSocketTransport(string socketPath, bool isServer)
{
_path = socketPath;
_isServer = isServer;
}
public async Task<Stream> AcceptAsync(CancellationToken ct)
{
if (!_isServer) throw new InvalidOperationException("Configured as client");
if (_listener is null)
{
// Hapus socket file lama kalau ada
if (File.Exists(_path)) File.Delete(_path);
_listener = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
_listener.Bind(new UnixDomainSocketEndPoint(_path));
_listener.Listen(backlog: 16);
// Set mode 0600: owner-only RW. Hindari WorldRW di filesystem.
// Pada .NET 7+: File.SetUnixFileMode
try
{
File.SetUnixFileMode(_path,
UnixFileMode.UserRead | UnixFileMode.UserWrite);
}
catch (PlatformNotSupportedException) { /* Windows fallback */ }
}
var clientSocket = await _listener.AcceptAsync(ct).ConfigureAwait(false);
return new NetworkStream(clientSocket, ownsSocket: true);
}
public async Task<Stream> ConnectAsync(CancellationToken ct)
{
if (_isServer) throw new InvalidOperationException("Configured as server");
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
await socket.ConnectAsync(new UnixDomainSocketEndPoint(_path), ct).ConfigureAwait(false);
return new NetworkStream(socket, ownsSocket: true);
}
public void Dispose()
{
_listener?.Dispose();
try { if (File.Exists(_path)) File.Delete(_path); } catch { /* ignore */ }
}
}
3.5 Factory aggregator
HermesServices/HermesIpc.Contracts/Transports/IpcTransportFactory.cs
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace HermesIpc.Contracts.Transports;
public static class IpcTransportFactory
{
public static IIpcTransport CreateServer()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return CreateWindowsServer();
return new UnixSocketTransport(IpcEndpoint.UnixSocketPath, isServer: true);
}
public static IIpcTransport CreateClient()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
return CreateWindowsClient();
return new UnixSocketTransport(IpcEndpoint.UnixSocketPath, isServer: false);
}
[SupportedOSPlatform("windows")]
private static IIpcTransport CreateWindowsServer() =>
new WindowsPipeTransport(IpcEndpoint.PipeName, isServer: true);
[SupportedOSPlatform("windows")]
private static IIpcTransport CreateWindowsClient() =>
new WindowsPipeTransport(IpcEndpoint.PipeName, isServer: false);
}
3.6 Sanity build
dotnet build HermesServices/HermesIpc.Contracts/HermesIpc.Contracts.csproj
Catatan: NamedPipeServerStreamAcl ada di package System.IO.Pipes.AccessControl (sudah otomatis di Windows .NET 8 SDK).
Checklist
-
IIpcTransport.cs -
IpcEndpoint.cs -
WindowsPipeTransport.csdengan ACL ketat (noWorldSid!) -
UnixSocketTransport.csdengan mode 0600 -
IpcTransportFactory.cs - Build sukses di Windows
- (Optional) Build cross-compile cek di Linux runner CI
Lanjut: Step 4: Server.