You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
ndg/src/comm.zig

209 lines
6.6 KiB
Zig

///! daemon/gui communication.
///! the protocol is a simple TLV construct: MessageTag(u16), length(u64), json-marshalled Message;
///! little endian.
const std = @import("std");
const json = std.json;
const mem = std.mem;
const ByteArrayList = @import("types.zig").ByteArrayList;
/// common errors returned by read/write functions.
pub const Error = error{
CommReadInvalidTag,
CommReadZeroLenInNonVoidTag,
CommWriteTooLarge,
};
/// daemon and gui exchange messages of this type.
pub const Message = union(MessageTag) {
ping: void,
pong: void,
poweroff: void,
wifi_connect: WifiConnect,
network_report: NetworkReport,
get_network_report: GetNetworkReport,
pub const WifiConnect = struct {
ssid: []const u8,
password: []const u8,
};
pub const NetworkReport = struct {
ipaddrs: []const []const u8,
wifi_ssid: ?[]const u8, // null indicates disconnected from wifi
wifi_scan_networks: []const []const u8,
};
pub const GetNetworkReport = struct {
scan: bool, // true starts a wifi scan and send NetworkReport only after completion
};
};
/// it is important to preserve ordinal values for future compatiblity,
/// especially when nd and gui may temporary diverge in their implementations.
pub const MessageTag = enum(u16) {
ping = 0x01,
pong = 0x02,
poweroff = 0x03,
wifi_connect = 0x04,
network_report = 0x05,
get_network_report = 0x06,
// next: 0x07
};
/// reads and parses a single message from the input stream reader.
/// callers must deallocate resources with free when done.
pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
// alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic.
const tag = reader.readEnum(MessageTag, .Little) catch {
return Error.CommReadInvalidTag;
};
const len = try reader.readIntLittle(u64);
if (len == 0) {
return switch (tag) {
.ping => Message{ .ping = {} },
.pong => Message{ .pong = {} },
.poweroff => Message{ .poweroff = {} },
else => Error.CommReadZeroLenInNonVoidTag,
};
}
var bytes = try allocator.alloc(u8, len);
defer allocator.free(bytes);
try reader.readNoEof(bytes);
const jopt = json.ParseOptions{ .allocator = allocator, .ignore_unknown_fields = true };
var jstream = json.TokenStream.init(bytes);
return switch (tag) {
.ping, .pong, .poweroff => unreachable, // handled above
.wifi_connect => Message{
.wifi_connect = try json.parse(Message.WifiConnect, &jstream, jopt),
},
.network_report => Message{
.network_report = try json.parse(Message.NetworkReport, &jstream, jopt),
},
.get_network_report => Message{
.get_network_report = try json.parse(Message.GetNetworkReport, &jstream, jopt),
},
};
}
/// outputs the message msg using writer.
/// all allocated resources are freed upon return.
pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
const jopt = .{ .whitespace = null };
var data = ByteArrayList.init(allocator);
defer data.deinit();
switch (msg) {
.ping, .pong, .poweroff => {}, // zero length payload
.wifi_connect => try json.stringify(msg.wifi_connect, jopt, data.writer()),
.network_report => try json.stringify(msg.network_report, jopt, data.writer()),
.get_network_report => try json.stringify(msg.get_network_report, jopt, data.writer()),
}
if (data.items.len > std.math.maxInt(u64)) {
return Error.CommWriteTooLarge;
}
try writer.writeIntLittle(u16, @enumToInt(msg));
try writer.writeIntLittle(u64, data.items.len);
try writer.writeAll(data.items);
}
pub fn free(allocator: mem.Allocator, m: Message) void {
switch (m) {
.ping, .pong, .poweroff => {}, // zero length payload
else => |v| {
json.parseFree(@TypeOf(v), v, .{ .allocator = allocator });
},
}
}
test "read" {
const t = std.testing;
var data = std.ArrayList(u8).init(t.allocator);
defer data.deinit();
const msg = Message{ .wifi_connect = .{ .ssid = "hello", .password = "world" } };
try json.stringify(msg.wifi_connect, .{}, data.writer());
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
try buf.writer().writeIntLittle(u16, @enumToInt(msg));
try buf.writer().writeIntLittle(u64, data.items.len);
try buf.writer().writeAll(data.items);
var bs = std.io.fixedBufferStream(buf.items);
const res = try read(t.allocator, bs.reader());
defer free(t.allocator, res);
try t.expectEqualStrings(msg.wifi_connect.ssid, res.wifi_connect.ssid);
try t.expectEqualStrings(msg.wifi_connect.password, res.wifi_connect.password);
}
test "write" {
const t = std.testing;
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
const msg = Message{ .wifi_connect = .{ .ssid = "wlan", .password = "secret" } };
try write(t.allocator, buf.writer(), msg);
const payload = "{\"ssid\":\"wlan\",\"password\":\"secret\"}";
var js = std.ArrayList(u8).init(t.allocator);
defer js.deinit();
try js.writer().writeIntLittle(u16, @enumToInt(msg));
try js.writer().writeIntLittle(u64, payload.len);
try js.appendSlice(payload);
try t.expectEqualStrings(js.items, buf.items);
}
test "write/read void tags" {
const t = std.testing;
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
const msg = [_]Message{
Message.ping,
Message.pong,
Message.poweroff,
};
for (msg) |m| {
buf.clearAndFree();
try write(t.allocator, buf.writer(), m);
var bs = std.io.fixedBufferStream(buf.items);
const res = try read(t.allocator, bs.reader());
free(t.allocator, res); // noop
try t.expectEqual(m, res);
}
}
test "msg sequence" {
const t = std.testing;
var buf = std.ArrayList(u8).init(t.allocator);
defer buf.deinit();
const msgs = [_]Message{
Message.ping,
Message{ .wifi_connect = .{ .ssid = "wlan", .password = "secret" } },
Message.pong,
Message{ .network_report = .{
.ipaddrs = &.{},
.wifi_ssid = null,
.wifi_scan_networks = &.{ "foo", "bar" },
} },
};
for (msgs) |m| {
try write(t.allocator, buf.writer(), m);
}
var bs = std.io.fixedBufferStream(buf.items);
for (msgs) |m| {
const res = try read(t.allocator, bs.reader());
defer free(t.allocator, res);
try t.expectEqual(@as(MessageTag, m), @as(MessageTag, res));
}
}