comm: introduce a simpler way to read/write

this builds on top of the main read and write fn, setting up a global
structure to allow module users imply comm.pipeWrite(msg)
and comm.pipeRead() without providing an allocator or reader/writer
on each call.

the advantage is simplification in the gui functions because they don't
have access to an allocator or the nd process read/write pipe.

disadvantage is in testing because it requires a global,
"before all tests" setup.

at the moment, only ngui is modified to use the new pipeRead/Write. the
daemon would suffer too many changes, especially in tests, due to the
global state.
pull/28/head
alex 1 year ago
parent a18a2a5435
commit aca7eb1165
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -1,14 +1,48 @@
//! daemon/gui communication. //! daemon/gui communication.
//! the protocol is a simple TLV construct: MessageTag(u16), length(u64), json-marshalled Message; //! the protocol is a simple TLV construct: MessageTag(u16), length(u64), json-marshalled Message;
//! little endian. //! little endian.
const std = @import("std"); const std = @import("std");
const json = std.json; const json = std.json;
const mem = std.mem; const mem = std.mem;
const ByteArrayList = @import("types.zig").ByteArrayList; const types = @import("types.zig");
const logger = std.log.scoped(.comm); const logger = std.log.scoped(.comm);
var plumb: struct {
a: std.mem.Allocator,
r: std.fs.File.Reader,
w: std.fs.File.Writer,
fn pipeRead(self: @This()) !ParsedMessage {
return read(self.a, self.r);
}
fn pipeWrite(self: @This(), m: Message) !void {
return write(self.a, self.w, m);
}
} = undefined;
/// initializes a global comm pipe, making `pipeRead` and `pipeWrite` ready to use from any module.
/// a message sent with `pipeWrite` can be subsequently read with `pipeRead`.
pub fn initPipe(a: std.mem.Allocator, p: types.IoPipe) void {
plumb = .{ .a = a, .r = p.r.reader(), .w = p.w.writer() };
}
/// similar to `read` but uses a global pipe initialized with `initPipe`.
/// blocking call.
pub fn pipeRead() !ParsedMessage {
return plumb.pipeRead();
}
/// similar to `write` but uses a global pipe initialized with `initPipe`.
/// blocking but normally buffered.
/// callers must deallocate resources with ParsedMessage.deinit when done.
pub fn pipeWrite(m: Message) !void {
return plumb.pipeWrite(m);
}
/// common errors returned by read/write functions. /// common errors returned by read/write functions.
pub const Error = error{ pub const Error = error{
CommReadInvalidTag, CommReadInvalidTag,
@ -210,7 +244,7 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
/// outputs the message msg using writer. /// outputs the message msg using writer.
/// all allocated resources are freed upon return. /// all allocated resources are freed upon return.
pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void { pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
var data = ByteArrayList.init(allocator); var data = types.ByteArrayList.init(allocator);
defer data.deinit(); defer data.deinit();
switch (msg) { switch (msg) {
.ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload .ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload

@ -171,8 +171,10 @@ pub fn main() !void {
// note: read(2) indicates file destriptor i/o is atomic linux since 3.14. // note: read(2) indicates file destriptor i/o is atomic linux since 3.14.
const uireader = ngui.stdout.?.reader(); const uireader = ngui.stdout.?.reader();
const uiwriter = ngui.stdin.?.writer(); const uiwriter = ngui.stdin.?.writer();
comm.initPipe(gpa, .{ .r = ngui.stdout.?, .w = ngui.stdin.? });
// send UI a ping right away to make sure pipes are working, crash otherwise. // send UI a ping right away to make sure pipes are working, crash otherwise.
comm.write(gpa, uiwriter, .ping) catch |err| { comm.pipeWrite(.ping) catch |err| {
logger.err("comm.write ping: {any}", .{err}); logger.err("comm.write ping: {any}", .{err});
return err; return err;
}; };

@ -13,8 +13,6 @@ const symbol = @import("ui/symbol.zig");
const logger = std.log.scoped(.ngui); const logger = std.log.scoped(.ngui);
// these are auto-closed as soon as main fn terminates. // these are auto-closed as soon as main fn terminates.
const stdin = std.io.getStdIn().reader();
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer(); const stderr = std.io.getStdErr().writer();
extern "c" fn ui_update_network_status(text: [*:0]const u8, wifi_list: ?[*:0]const u8) void; extern "c" fn ui_update_network_status(text: [*:0]const u8, wifi_list: ?[*:0]const u8) void;
@ -130,7 +128,7 @@ export fn nm_check_idle_time(_: *lvgl.LvTimer) void {
/// once all's done, the daemon will send a SIGTERM back to ngui. /// once all's done, the daemon will send a SIGTERM back to ngui.
export fn nm_sys_shutdown() void { export fn nm_sys_shutdown() void {
const msg = comm.Message.poweroff; const msg = comm.Message.poweroff;
comm.write(gpa, stdout, msg) catch |err| logger.err("nm_sys_shutdown: {any}", .{err}); comm.pipeWrite(msg) catch |err| logger.err("nm_sys_shutdown: {any}", .{err});
state = .alert; // prevent screen sleep state = .alert; // prevent screen sleep
wakeup.set(); // wake up from standby, if any wakeup.set(); // wake up from standby, if any
} }
@ -138,13 +136,13 @@ export fn nm_sys_shutdown() void {
export fn nm_tab_settings_active() void { export fn nm_tab_settings_active() void {
logger.info("starting wifi scan", .{}); logger.info("starting wifi scan", .{});
const msg = comm.Message{ .get_network_report = .{ .scan = true } }; const msg = comm.Message{ .get_network_report = .{ .scan = true } };
comm.write(gpa, stdout, msg) catch |err| logger.err("nm_tab_settings_active: {any}", .{err}); comm.pipeWrite(msg) catch |err| logger.err("nm_tab_settings_active: {any}", .{err});
} }
export fn nm_request_network_status(t: *lvgl.LvTimer) void { export fn nm_request_network_status(t: *lvgl.LvTimer) void {
t.destroy(); t.destroy();
const msg: comm.Message = .{ .get_network_report = .{ .scan = false } }; const msg: comm.Message = .{ .get_network_report = .{ .scan = false } };
comm.write(gpa, stdout, msg) catch |err| logger.err("nm_request_network_status: {any}", .{err}); comm.pipeWrite(msg) catch |err| logger.err("nm_request_network_status: {any}", .{err});
} }
/// ssid and password args must not outlive this function. /// ssid and password args must not outlive this function.
@ -154,9 +152,7 @@ export fn nm_wifi_start_connect(ssid: [*:0]const u8, password: [*:0]const u8) vo
.password = std.mem.span(password), .password = std.mem.span(password),
} }; } };
logger.info("connect to wifi [{s}]", .{msg.wifi_connect.ssid}); logger.info("connect to wifi [{s}]", .{msg.wifi_connect.ssid});
comm.write(gpa, stdout, msg) catch |err| { comm.pipeWrite(msg) catch |err| logger.err("nm_wifi_start_connect: {any}", .{err});
logger.err("comm.write: {any}", .{err});
};
} }
/// callers must hold ui mutex for the whole duration. /// callers must hold ui mutex for the whole duration.
@ -225,12 +221,12 @@ fn commThreadLoop() void {
/// the UI accordingly. /// the UI accordingly.
/// holds ui mutex for most of the duration. /// holds ui mutex for most of the duration.
fn commThreadLoopCycle() !void { fn commThreadLoopCycle() !void {
const msg = try comm.read(gpa, stdin); // blocking const msg = try comm.pipeRead(); // blocking
ui_mutex.lock(); // guards the state and all UI calls below ui_mutex.lock(); // guards the state and all UI calls below
defer ui_mutex.unlock(); defer ui_mutex.unlock();
switch (state) { switch (state) {
.standby => switch (msg.value) { .standby => switch (msg.value) {
.ping => try comm.write(gpa, stdout, comm.Message.pong), .ping => try comm.pipeWrite(comm.Message.pong),
.network_report, .network_report,
.bitcoind_report, .bitcoind_report,
.lightning_report, .lightning_report,
@ -238,7 +234,7 @@ fn commThreadLoopCycle() !void {
else => logger.debug("ignoring {s}: in standby", .{@tagName(msg.value)}), else => logger.debug("ignoring {s}: in standby", .{@tagName(msg.value)}),
}, },
.active, .alert => switch (msg.value) { .active, .alert => switch (msg.value) {
.ping => try comm.write(gpa, stdout, comm.Message.pong), .ping => try comm.pipeWrite(comm.Message.pong),
.poweroff_progress => |rep| { .poweroff_progress => |rep| {
ui.poweroff.updateStatus(rep) catch |err| logger.err("poweroff.updateStatus: {any}", .{err}); ui.poweroff.updateStatus(rep) catch |err| logger.err("poweroff.updateStatus: {any}", .{err});
msg.deinit(); msg.deinit();
@ -275,9 +271,7 @@ fn uiThreadLoop() void {
.standby => { .standby => {
// go into a screen sleep mode due to no user activity // go into a screen sleep mode due to no user activity
wakeup.reset(); wakeup.reset();
comm.write(gpa, stdout, comm.Message.standby) catch |err| { comm.pipeWrite(comm.Message.standby) catch |err| logger.err("standby: {any}", .{err});
logger.err("comm.write standby: {any}", .{err});
};
screen.sleep(&ui_mutex, &wakeup); // blocking screen.sleep(&ui_mutex, &wakeup); // blocking
// wake up due to touch screen activity or wakeup event is set // wake up due to touch screen activity or wakeup event is set
@ -286,9 +280,7 @@ fn uiThreadLoop() void {
defer ui_mutex.unlock(); defer ui_mutex.unlock();
if (state == .standby) { if (state == .standby) {
state = .active; state = .active;
comm.write(gpa, stdout, comm.Message.wakeup) catch |err| { comm.pipeWrite(comm.Message.wakeup) catch |err| logger.err("wakeup: {any}", .{err});
logger.err("comm.write wakeup: {any}", .{err});
};
lvgl.resetIdle(); lvgl.resetIdle();
last_report.mu.lock(); last_report.mu.lock();
@ -369,6 +361,9 @@ pub fn main() anyerror!void {
// the UI is unusable otherwise. // the UI is unusable otherwise.
tick_timer = try time.Timer.start(); tick_timer = try time.Timer.start();
// initialize global nd/ngui pipe plumbing.
comm.initPipe(gpa, .{ .r = std.io.getStdIn(), .w = std.io.getStdOut() });
// initalizes display, input driver and finally creates the user interface. // initalizes display, input driver and finally creates the user interface.
ui.init() catch |err| { ui.init() catch |err| {
logger.err("ui.init: {any}", .{err}); logger.err("ui.init: {any}", .{err});

@ -2,6 +2,9 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const nif = @import("nif"); const nif = @import("nif");
const comm = @import("comm.zig");
const types = @import("types.zig");
comptime { comptime {
if (!builtin.is_test) @compileError("test-only module"); if (!builtin.is_test) @compileError("test-only module");
} }
@ -19,6 +22,27 @@ export fn lv_disp_get_inactive_time(disp: *opaque {}) u32 {
return 0; return 0;
} }
var global_gpa_state: std.heap.GeneralPurposeAllocator(.{}) = undefined;
var global_gpa: std.mem.Allocator = undefined;
var initGlobalOnce = std.once(initGlobalFn);
/// initializes globals like the `comm.initPipe`.
/// can be called from any test, multiple times: the initialization is enforced to happen only once.
/// safe for concurrent use. needs not deinit'ing: resources are released by the OS
/// when the test binary terminates.
pub fn initGlobal() void {
initGlobalOnce.call();
}
fn initGlobalFn() void {
global_gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
global_gpa = global_gpa_state.allocator();
var pipe = types.IoPipe.create() catch |err| {
std.debug.panic("IoPipe.create: {any}", .{err});
};
comm.initPipe(global_gpa, pipe);
}
/// TestTimer always reports the same fixed value. /// TestTimer always reports the same fixed value.
pub const TestTimer = struct { pub const TestTimer = struct {
value: u64, value: u64,