//! IPC Client for TUI -> Daemon communication. //! Wraps control.zig types with deep-copying logic for memory safety. const std = @import("std"); const control = @import("control"); pub const NodeStatus = control.NodeStatus; pub const SlashEvent = control.SlashEvent; pub const TopologyInfo = control.TopologyInfo; pub const GraphNode = control.GraphNode; pub const GraphEdge = control.GraphEdge; pub const Client = struct { allocator: std.mem.Allocator, stream: ?std.net.Stream = null, pub fn init(allocator: std.mem.Allocator) !Client { return .{ .allocator = allocator, }; } pub fn deinit(self: *Client) void { if (self.stream) |s| s.close(); } pub fn connect(self: *Client, socket_path: []const u8) !void { self.stream = std.net.connectUnixSocket(socket_path) catch |err| { std.log.err("Failed to connect to daemon at {s}: {}. Is it running?", .{ socket_path, err }); return err; }; } pub fn getStatus(self: *Client) !NodeStatus { var parsed = try self.request(.Status); defer parsed.deinit(); switch (parsed.value) { .NodeStatus => |s| return try self.deepCopyStatus(s), else => return error.UnexpectedResponse, } } pub fn getSlashLog(self: *Client, limit: usize) ![]SlashEvent { var parsed = try self.request(.{ .SlashLog = .{ .limit = limit } }); defer parsed.deinit(); switch (parsed.value) { .SlashLogResult => |l| return try self.deepCopySlashLog(l), else => return error.UnexpectedResponse, } } pub fn getTopology(self: *Client) !TopologyInfo { var parsed = try self.request(.Topology); defer parsed.deinit(); switch (parsed.value) { .TopologyInfo => |t| return try self.deepCopyTopology(t), else => return error.UnexpectedResponse, } } pub fn request(self: *Client, cmd: control.Command) !std.json.Parsed(control.Response) { if (self.stream == null) return error.NotConnected; const stream = self.stream.?; const json_bytes = try std.json.Stringify.valueAlloc(self.allocator, cmd, .{}); defer self.allocator.free(json_bytes); try stream.writeAll(json_bytes); var resp_buf: [32768]u8 = undefined; const bytes = try stream.read(&resp_buf); if (bytes == 0) return error.ConnectionClosed; return try std.json.parseFromSlice(control.Response, self.allocator, resp_buf[0..bytes], .{ .ignore_unknown_fields = true }); } fn deepCopyStatus(self: *Client, s: NodeStatus) !NodeStatus { return .{ .node_id = try self.allocator.dupe(u8, s.node_id), .state = try self.allocator.dupe(u8, s.state), .peers_count = s.peers_count, .uptime_seconds = s.uptime_seconds, .version = try self.allocator.dupe(u8, s.version), }; } fn deepCopySlashLog(self: *Client, events: []const SlashEvent) ![]SlashEvent { const list = try self.allocator.alloc(SlashEvent, events.len); for (events, 0..) |ev, i| { list[i] = .{ .timestamp = ev.timestamp, .target_did = try self.allocator.dupe(u8, ev.target_did), .reason = try self.allocator.dupe(u8, ev.reason), .severity = try self.allocator.dupe(u8, ev.severity), .evidence_hash = try self.allocator.dupe(u8, ev.evidence_hash), }; } return list; } fn deepCopyTopology(self: *Client, topo: TopologyInfo) !TopologyInfo { const nodes = try self.allocator.alloc(control.GraphNode, topo.nodes.len); for (topo.nodes, 0..) |n, i| { nodes[i] = .{ .id = try self.allocator.dupe(u8, n.id), .trust_score = n.trust_score, .status = try self.allocator.dupe(u8, n.status), .role = try self.allocator.dupe(u8, n.role), }; } const edges = try self.allocator.alloc(control.GraphEdge, topo.edges.len); for (topo.edges, 0..) |e, i| { edges[i] = .{ .source = try self.allocator.dupe(u8, e.source), .target = try self.allocator.dupe(u8, e.target), .weight = e.weight, }; } return TopologyInfo{ .nodes = nodes, .edges = edges, }; } pub fn freeStatus(self: *Client, s: NodeStatus) void { self.allocator.free(s.node_id); self.allocator.free(s.state); self.allocator.free(s.version); } pub fn freeSlashLog(self: *Client, events: []SlashEvent) void { for (events) |ev| { self.allocator.free(ev.target_did); self.allocator.free(ev.reason); self.allocator.free(ev.severity); self.allocator.free(ev.evidence_hash); } self.allocator.free(events); } pub fn freeTopology(self: *Client, topo: TopologyInfo) void { for (topo.nodes) |n| { self.allocator.free(n.id); self.allocator.free(n.status); self.allocator.free(n.role); } self.allocator.free(topo.nodes); for (topo.edges) |e| { self.allocator.free(e.source); self.allocator.free(e.target); } self.allocator.free(topo.edges); } };