Step 4 — Refactor HermesServiceEngine (Server)
Sekarang kita ganti HermesServiceEngine/Conn/IpcComService.cs dengan implementasi StreamJsonRpc.
4.1 Implementasikan IHermesRpc
Buat file baru:
HermesServices/HermesServiceEngine/Rpc/HermesRpcServer.cs
using System.Threading;
using System.Threading.Tasks;
using HermesIpc.Contracts;
using HermesServiceEngine.Log;
using HermesServiceEngine.Modules;
namespace HermesServiceEngine.Rpc;
/// <summary>
/// Implementasi server-side IHermesRpc. Method ini di-invoke oleh StreamJsonRpc
/// saat client call. Wrap domain modules existing (ServiceXdr, ServiceRmm, ServiceSase).
/// </summary>
public sealed class HermesRpcServer : IHermesRpc
{
private readonly IClientNotifier _notifier;
public HermesRpcServer(IClientNotifier notifier)
{
_notifier = notifier;
}
public Task<RpcResult> SetLogPathAsync(string path, CancellationToken ct)
{
HelpReport.ChangeLogPath(path);
return Task.FromResult(RpcResult.Ok($"Log path set to {path}"));
}
public async Task<RpcResult> StartXdrAsync(string config, CancellationToken ct)
{
var ok = await ServiceXdr.StartAgentXdrService(config).ConfigureAwait(false);
await _notifier.OnXdrStatusChangedAsync(ok).ConfigureAwait(false);
return ok ? RpcResult.Ok("XDR started") : RpcResult.Fail("Failed to start XDR");
}
public async Task<RpcResult> StopXdrAsync(CancellationToken ct)
{
var ok = await ServiceXdr.StopAgentXdrService().ConfigureAwait(false);
await _notifier.OnXdrStatusChangedAsync(false).ConfigureAwait(false);
return ok ? RpcResult.Ok("XDR stopped") : RpcResult.Fail("Failed to stop XDR");
}
public async Task<RpcResult> InstallRmmAsync(CancellationToken ct)
{
var (ok, err) = await ServiceRmm.InstallingRmmModule().ConfigureAwait(false);
return ok ? RpcResult.Ok("Installation success") : RpcResult.Fail($"Installation failed: {err}");
}
public async Task<RpcResult> ActivateRmmAsync(string config, CancellationToken ct)
{
var (ok, err) = await ServiceRmm.ActivatingRmmModule(config).ConfigureAwait(false);
return ok ? RpcResult.Ok("Activation success") : RpcResult.Fail($"Activation failed: {err}");
}
public async Task<RpcResult> StartRmmAsync(CancellationToken ct)
{
var ok = await ServiceRmm.StartServiceMeshAgent().ConfigureAwait(false);
return ok ? RpcResult.Ok("RMM started") : RpcResult.Fail("Failed to start RMM");
}
public async Task<RpcResult> StopRmmAsync(CancellationToken ct)
{
var ok = await ServiceRmm.StopServiceMeshAgent().ConfigureAwait(false);
return ok ? RpcResult.Ok("RMM stopped") : RpcResult.Fail("Failed to stop RMM");
}
public async Task<RpcResult> StartSaseAsync(string wireGuardConfig, CancellationToken ct)
{
// WgRename + ServiceSase.CreateSaseConfiguration + ServiceSase.StartSaseService
// tetap dipanggil sama persis seperti dispatcher lama (IpcComService.cs:257-281).
await _notifier.OnSaseStatusChangedAsync(SaseStatus.Connecting).ConfigureAwait(false);
WgFileManager.Rename(toWireguardDll: true); // lihat catatan di bawah
var (cfgOk, cfgErr) = ServiceSase.CreateSaseConfiguration(wireGuardConfig);
if (!cfgOk)
{
await _notifier.OnSaseStatusChangedAsync(SaseStatus.Failed).ConfigureAwait(false);
return RpcResult.Fail($"Failed to create SASE config: {cfgErr}");
}
await Task.Delay(1000, ct).ConfigureAwait(false);
var (startOk, startErr) = ServiceSase.StartSaseService();
if (!startOk)
{
await _notifier.OnSaseStatusChangedAsync(SaseStatus.Failed).ConfigureAwait(false);
return RpcResult.Fail($"Failed to start SASE: {startErr}");
}
await _notifier.OnSaseStatusChangedAsync(SaseStatus.Connected).ConfigureAwait(false);
return RpcResult.Ok("SASE started");
}
public async Task<RpcResult> StopSaseAsync(CancellationToken ct)
{
var (ok, err) = ServiceSase.StopSaseService();
WgFileManager.Rename(toWireguardDll: false);
await _notifier.OnSaseStatusChangedAsync(SaseStatus.Disconnected).ConfigureAwait(false);
return ok ? RpcResult.Ok("SASE stopped") : RpcResult.Fail($"Failed to stop SASE: {err}");
}
public Task<HealthReport> GetHealthAsync(CancellationToken ct)
{
var report = new HealthReport(
ServiceRunning: true,
XdrRunning: ServiceXdr.IsRunning,
RmmRunning: ServiceRmm.IsRunning,
SaseRunning: ServiceSase.IsRunning,
Version: typeof(HermesRpcServer).Assembly.GetName().Version?.ToString() ?? "0.0.0");
return Task.FromResult(report);
}
}
Catatan
WgFileManager: EkstrakWgRenamedariIpcComService.cs:411-443ke kelas terpisah dan wrap denganlockuntuk fix race condition yang sudah diidentifikasi di analisis sebelumnya.
4.2 Notifier interface
Server butuh cara push event ke client. Kita injeksi via interface:
HermesServices/HermesServiceEngine/Rpc/IClientNotifier.cs
using System.Threading.Tasks;
using HermesIpc.Contracts;
namespace HermesServiceEngine.Rpc;
public interface IClientNotifier
{
Task OnSaseStatusChangedAsync(SaseStatus status);
Task OnKcptunLogAsync(string line);
Task OnXdrStatusChangedAsync(bool running);
}
4.3 Host loop — accept clients, attach JSON-RPC
HermesServices/HermesServiceEngine/Rpc/HermesRpcHost.cs
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using HermesIpc.Contracts;
using HermesIpc.Contracts.Transports;
using HermesServiceEngine.Log;
using Nerdbank.Streams;
using StreamJsonRpc;
namespace HermesServiceEngine.Rpc;
public sealed class HermesRpcHost : IAsyncDisposable
{
private readonly CancellationTokenSource _cts = new();
private Task? _runLoop;
public Task StartAsync()
{
_runLoop = Task.Run(() => RunAsync(_cts.Token));
return Task.CompletedTask;
}
private async Task RunAsync(CancellationToken ct)
{
var transport = IpcTransportFactory.CreateServer();
HelpReport.LogInfo($"[RPC] Host listening...");
while (!ct.IsCancellationRequested)
{
try
{
var stream = await transport.AcceptAsync(ct).ConfigureAwait(false);
_ = Task.Run(() => HandleClientAsync(stream, ct), ct);
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
HelpReport.LogInfo($"[RPC] Accept error: {ex.Message}");
await Task.Delay(500, ct).ConfigureAwait(false);
}
}
}
private async Task HandleClientAsync(Stream stream, CancellationToken ct)
{
try
{
// Verifikasi peer (Step 7) — diisi nanti
// PeerVerifier.Verify(stream);
var formatter = new SystemTextJsonFormatter();
var handler = new LengthHeaderMessageHandler(stream, stream, formatter);
using var rpc = new JsonRpc(handler);
// Notifier yang membungkus rpc.NotifyAsync
var notifier = new JsonRpcClientNotifier(rpc);
var target = new HermesRpcServer(notifier);
rpc.AddLocalRpcTarget<IHermesRpc>(target, new JsonRpcTargetOptions
{
AllowNonPublicInvocation = false,
NotifyClientOfEvents = true,
});
rpc.StartListening();
await rpc.Completion.ConfigureAwait(false);
}
catch (Exception ex)
{
HelpReport.LogInfo($"[RPC] Client session error: {ex.Message}");
}
finally
{
try { stream.Dispose(); } catch { }
}
}
public async ValueTask DisposeAsync()
{
_cts.Cancel();
if (_runLoop is not null)
{
try { await _runLoop.ConfigureAwait(false); } catch { }
}
_cts.Dispose();
}
}
/// <summary>
/// Notifier implementasi yang me-forward ke JsonRpc.NotifyAsync.
/// Method name harus match IHermesRpcClient di sisi UI.
/// </summary>
internal sealed class JsonRpcClientNotifier : IClientNotifier
{
private readonly JsonRpc _rpc;
public JsonRpcClientNotifier(JsonRpc rpc) => _rpc = rpc;
public Task OnSaseStatusChangedAsync(SaseStatus status) =>
_rpc.NotifyAsync(nameof(IHermesRpcClient.OnSaseStatusChangedAsync), status);
public Task OnKcptunLogAsync(string line) =>
_rpc.NotifyAsync(nameof(IHermesRpcClient.OnKcptunLogAsync), line);
public Task OnXdrStatusChangedAsync(bool running) =>
_rpc.NotifyAsync(nameof(IHermesRpcClient.OnXdrStatusChangedAsync), running);
}
4.4 Wiring di service entry point
Cari file entry point Hermes service (cari static class Program atau ServiceBase.Run di HermesServiceEngine). Tambahkan startup:
HermesServices/HermesServiceEngine/Program.cs (atau HermesService.cs)
// di startup, sebelum service main loop:
var rpcHost = new HermesRpcHost();
await rpcHost.StartAsync();
// ... existing service loop ...
// pada shutdown:
await rpcHost.DisposeAsync();
4.5 Hapus kode lama (HATI-HATI)
JANGAN hapus dulu — sampai sisi client juga migrasi (Step 5) dan sudah ditest. Gunakan feature flag atau pertahankan paralel sementara:
// HermesService.cs
if (FeatureFlags.UseStreamJsonRpc)
await rpcHost.StartAsync();
else
_ = oldIpcComService.StartIpcLoopAsync(); // legacy
Setelah semua green di staging (Step 8), baru hapus:
HermesServiceEngine/Conn/IpcComService.cs(456 baris)- Switch dispatcher
ProcessMessage(~150 baris)
Checklist
-
HermesRpcServer.csmengimplementasikanIHermesRpc -
IClientNotifier.csinterface -
HermesRpcHost.csaccept loop + per-client task -
JsonRpcClientNotifierforward kerpc.NotifyAsync - Wiring di service startup
- Feature flag untuk paralel run dengan kode lama
-
WgRenamediekstrak keWgFileManagerdenganlock
Lanjut: Step 5: Client.