Step 5 — Refactor HermesNetwork (Client UI)
Ganti HermesNetwork/Conn/IpcComService.cs dengan proxy StreamJsonRpc.
5.1 Implementasikan IHermesRpcClient (target untuk event server)
HermesNetwork/Rpc/HermesRpcClientTarget.cs
using System.Threading.Tasks;
using HermesIpc.Contracts;
using HermesNetwork.Bus;
using HermesNetwork.Models;
namespace HermesNetwork.Rpc;
/// <summary>
/// Implementasi sisi UI dari event push server.
/// Forward semua event ke MessageBusProvider.IpcMessageBus
/// supaya kode konsumer existing (ViewModel dst.) tidak perlu diubah.
/// </summary>
public sealed class HermesRpcClientTarget : IHermesRpcClient
{
public Task OnSaseStatusChangedAsync(SaseStatus status)
{
MessageBusProvider.IpcMessageBus.SendMessage(new IpcMessageResult
{
Service = "Sase",
Arg = status.ToString(),
Status = status == SaseStatus.Connected,
Message = $"SASE status: {status}"
});
return Task.CompletedTask;
}
public Task OnKcptunLogAsync(string line)
{
MessageBusProvider.IpcMessageBus.SendMessage(new IpcMessageResult
{
Service = "Kcptun",
Arg = "Log",
Status = true,
Message = line
});
return Task.CompletedTask;
}
public Task OnXdrStatusChangedAsync(bool running)
{
MessageBusProvider.IpcMessageBus.SendMessage(new IpcMessageResult
{
Service = "Xdr",
Arg = running ? "Started" : "Stopped",
Status = running
});
return Task.CompletedTask;
}
public Task OnLogEntryAsync(LogLevel level, string source, string message)
{
MessageBusProvider.IpcMessageBus.SendMessage(new IpcMessageResult
{
Service = source,
Arg = level.ToString(),
Message = message
});
return Task.CompletedTask;
}
}
5.2 RPC connection manager dengan auto-reconnect
HermesNetwork/Rpc/HermesRpcClient.cs
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using HermesIpc.Contracts;
using HermesIpc.Contracts.Transports;
using HermesNetwork.Log;
using StreamJsonRpc;
namespace HermesNetwork.Rpc;
public sealed class HermesRpcClient : IAsyncDisposable
{
private static HermesRpcClient? _instance;
private static readonly object _lock = new();
public static HermesRpcClient Instance
{
get { lock (_lock) return _instance ??= new HermesRpcClient(); }
}
private readonly SemaphoreSlim _connectGate = new(1, 1);
private readonly CancellationTokenSource _cts = new();
private JsonRpc? _rpc;
private IHermesRpc? _proxy;
private Stream? _stream;
private Task? _reconnectLoop;
private HermesRpcClient() { }
public IHermesRpc Proxy
{
get => _proxy ?? throw new InvalidOperationException("Not connected. Call StartAsync first.");
}
public bool IsConnected => _rpc is { IsDisposed: false };
public async Task StartAsync()
{
await ConnectOnceAsync(_cts.Token).ConfigureAwait(false);
_reconnectLoop = Task.Run(async () =>
{
while (!_cts.IsCancellationRequested)
{
try
{
if (_rpc is not null) await _rpc.Completion.ConfigureAwait(false);
}
catch { /* connection dropped */ }
if (_cts.IsCancellationRequested) break;
HelpReport.LogInfo("[RPC] Disconnected. Reconnecting in 2s...");
await Task.Delay(2000, _cts.Token).ConfigureAwait(false);
try { await ConnectOnceAsync(_cts.Token).ConfigureAwait(false); }
catch (Exception ex) { HelpReport.LogInfo($"[RPC] Reconnect failed: {ex.Message}"); }
}
});
}
private async Task ConnectOnceAsync(CancellationToken ct)
{
await _connectGate.WaitAsync(ct).ConfigureAwait(false);
try
{
DisposeRpc();
var transport = IpcTransportFactory.CreateClient();
using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
connectCts.CancelAfter(TimeSpan.FromSeconds(5));
_stream = await transport.ConnectAsync(connectCts.Token).ConfigureAwait(false);
var formatter = new SystemTextJsonFormatter();
var handler = new LengthHeaderMessageHandler(_stream, _stream, formatter);
_rpc = new JsonRpc(handler);
// Daftarkan client target untuk terima event push dari server
_rpc.AddLocalRpcTarget<IHermesRpcClient>(
new HermesRpcClientTarget(),
new JsonRpcTargetOptions { AllowNonPublicInvocation = false });
// Buat proxy bertipe untuk panggil method server
_proxy = _rpc.Attach<IHermesRpc>();
_rpc.StartListening();
HelpReport.LogInfo("[RPC] Connected to HermesServiceEngine");
}
finally { _connectGate.Release(); }
}
private void DisposeRpc()
{
try { _rpc?.Dispose(); } catch { }
try { _stream?.Dispose(); } catch { }
_rpc = null;
_stream = null;
_proxy = null;
}
public async ValueTask DisposeAsync()
{
_cts.Cancel();
if (_reconnectLoop is not null) try { await _reconnectLoop.ConfigureAwait(false); } catch { }
DisposeRpc();
_cts.Dispose();
}
}
5.3 Pemanggilan dari ViewModel
// Sebelum:
// await IpcComService.SendMessage(new IpcMessageCommand {
// Code = StringHelper.GetRandomString(6),
// Service = IpcConst.Services.Xdr,
// Arg = IpcConst.Arguments.Start,
// Config = "..."
// });
// Sesudah:
var result = await HermesRpcClient.Instance.Proxy
.StartXdrAsync(config: "...", cancellationToken);
if (!result.Success)
ShowError(result.Message);
Atau untuk SASE:
var sw = Stopwatch.StartNew();
try
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var result = await HermesRpcClient.Instance.Proxy
.StartSaseAsync(wireGuardConfig, cts.Token);
Log.Info($"SASE start: {result.Success} in {sw.ElapsedMilliseconds}ms");
}
catch (TaskCanceledException)
{
Log.Warn("SASE start timed out after 30s");
}
catch (RemoteInvocationException ex)
{
// Exception yang di-throw dari sisi server
Log.Error($"Server error: {ex.Message}");
}
catch (ConnectionLostException)
{
Log.Error("Lost connection to service");
// HermesRpcClient akan auto-reconnect
}
5.4 Lifecycle wiring di App
HermesNetwork/App.axaml.cs
public override async void OnFrameworkInitializationCompleted()
{
// ... existing init ...
await HermesRpcClient.Instance.StartAsync();
}
protected override async void OnExiting()
{
await HermesRpcClient.Instance.DisposeAsync();
base.OnExiting();
}
5.5 Hapus pipe receiver lama
Setelah event handler berjalan via HermesRpcClientTarget, server pipe lama HermesNetwork360Guard di HermesNetwork/Conn/IpcComService.cs:50-67 tidak lagi diperlukan. Hapus saat rollout final.
Checklist
-
HermesRpcClientTarget.csforward event ke MessageBus -
HermesRpcClient.csdengan singleton + auto-reconnect - ViewModel migrasi dari
SendMessage(IpcMessageCommand)keProxy.XxxAsync(...) - App lifecycle wired (
StartAsyncdi init,DisposeAsyncdi exit) - Pipe receiver lama dihapus setelah verified
Lanjut: Step 6: Push Events.