repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

zmx / src
Eric Bower  ·  2025-11-27

ipc.zig

  1const std = @import("std");
  2const posix = std.posix;
  3
  4pub const Tag = enum(u8) {
  5    Input = 0,
  6    Output = 1,
  7    Resize = 2,
  8    Detach = 3,
  9    DetachAll = 4,
 10    Kill = 5,
 11    Info = 6,
 12    Init = 7,
 13};
 14
 15pub const Header = packed struct {
 16    tag: Tag,
 17    len: u32,
 18};
 19
 20pub const Resize = packed struct {
 21    rows: u16,
 22    cols: u16,
 23};
 24
 25pub const Info = packed struct {
 26    clients_len: usize,
 27    pid: i32,
 28};
 29
 30pub fn expectedLength(data: []const u8) ?usize {
 31    if (data.len < @sizeOf(Header)) return null;
 32    const header = std.mem.bytesToValue(Header, data[0..@sizeOf(Header)]);
 33    return @sizeOf(Header) + header.len;
 34}
 35
 36pub fn send(fd: i32, tag: Tag, data: []const u8) !void {
 37    const header = Header{
 38        .tag = tag,
 39        .len = @intCast(data.len),
 40    };
 41    const header_bytes = std.mem.asBytes(&header);
 42    try writeAll(fd, header_bytes);
 43    if (data.len > 0) {
 44        try writeAll(fd, data);
 45    }
 46}
 47
 48pub fn appendMessage(alloc: std.mem.Allocator, list: *std.ArrayList(u8), tag: Tag, data: []const u8) !void {
 49    const header = Header{
 50        .tag = tag,
 51        .len = @intCast(data.len),
 52    };
 53    try list.appendSlice(alloc, std.mem.asBytes(&header));
 54    if (data.len > 0) {
 55        try list.appendSlice(alloc, data);
 56    }
 57}
 58
 59fn writeAll(fd: i32, data: []const u8) !void {
 60    var index: usize = 0;
 61    while (index < data.len) {
 62        const n = try posix.write(fd, data[index..]);
 63        if (n == 0) return error.DiskQuota;
 64        index += n;
 65    }
 66}
 67
 68pub const Message = struct {
 69    tag: Tag,
 70    data: []u8,
 71
 72    pub fn deinit(self: Message, alloc: std.mem.Allocator) void {
 73        if (self.data.len > 0) {
 74            alloc.free(self.data);
 75        }
 76    }
 77};
 78
 79pub const SocketMsg = struct {
 80    header: Header,
 81    payload: []const u8,
 82};
 83
 84pub const SocketBuffer = struct {
 85    buf: std.ArrayList(u8),
 86    alloc: std.mem.Allocator,
 87    head: usize,
 88
 89    pub fn init(alloc: std.mem.Allocator) !SocketBuffer {
 90        return .{
 91            .buf = try std.ArrayList(u8).initCapacity(alloc, 4096),
 92            .alloc = alloc,
 93            .head = 0,
 94        };
 95    }
 96
 97    pub fn deinit(self: *SocketBuffer) void {
 98        self.buf.deinit(self.alloc);
 99    }
100
101    /// Reads from fd into buffer.
102    /// Returns number of bytes read.
103    /// Propagates error.WouldBlock and other errors to caller.
104    /// Returns 0 on EOF.
105    pub fn read(self: *SocketBuffer, fd: i32) !usize {
106        if (self.head > 0) {
107            const remaining = self.buf.items.len - self.head;
108            if (remaining > 0) {
109                std.mem.copyForwards(u8, self.buf.items[0..remaining], self.buf.items[self.head..]);
110                self.buf.items.len = remaining;
111            } else {
112                self.buf.clearRetainingCapacity();
113            }
114            self.head = 0;
115        }
116
117        var tmp: [4096]u8 = undefined;
118        const n = try posix.read(fd, &tmp);
119        if (n > 0) {
120            try self.buf.appendSlice(self.alloc, tmp[0..n]);
121        }
122        return n;
123    }
124
125    /// Returns the next complete message or `null` when none available.
126    /// `buf` is advanced automatically; caller keeps the returned slices
127    /// valid until the following `next()` (or `deinit`).
128    pub fn next(self: *SocketBuffer) ?SocketMsg {
129        const available = self.buf.items[self.head..];
130        const total = expectedLength(available) orelse return null;
131        if (available.len < total) return null;
132
133        const hdr = std.mem.bytesToValue(Header, available[0..@sizeOf(Header)]);
134        const pay = available[@sizeOf(Header)..total];
135
136        self.head += total;
137        return .{ .header = hdr, .payload = pay };
138    }
139};