diff --git a/src/comm.zig b/src/comm.zig index 987dfed..c8b8bbb 100644 --- a/src/comm.zig +++ b/src/comm.zig @@ -1,14 +1,48 @@ //! 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; +const types = @import("types.zig"); 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. pub const Error = error{ CommReadInvalidTag, @@ -210,7 +244,7 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage { /// outputs the message msg using writer. /// all allocated resources are freed upon return. 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(); switch (msg) { .ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload diff --git a/src/nd.zig b/src/nd.zig index ab7f467..befdf26 100644 --- a/src/nd.zig +++ b/src/nd.zig @@ -171,8 +171,10 @@ pub fn main() !void { // note: read(2) indicates file destriptor i/o is atomic linux since 3.14. const uireader = ngui.stdout.?.reader(); 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. - comm.write(gpa, uiwriter, .ping) catch |err| { + comm.pipeWrite(.ping) catch |err| { logger.err("comm.write ping: {any}", .{err}); return err; }; diff --git a/src/ngui.zig b/src/ngui.zig index 01682e2..bfce8a5 100644 --- a/src/ngui.zig +++ b/src/ngui.zig @@ -13,8 +13,6 @@ const symbol = @import("ui/symbol.zig"); const logger = std.log.scoped(.ngui); // 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(); 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. export fn nm_sys_shutdown() void { 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 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 { logger.info("starting wifi scan", .{}); 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 { t.destroy(); 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. @@ -154,9 +152,7 @@ export fn nm_wifi_start_connect(ssid: [*:0]const u8, password: [*:0]const u8) vo .password = std.mem.span(password), } }; logger.info("connect to wifi [{s}]", .{msg.wifi_connect.ssid}); - comm.write(gpa, stdout, msg) catch |err| { - logger.err("comm.write: {any}", .{err}); - }; + comm.pipeWrite(msg) catch |err| logger.err("nm_wifi_start_connect: {any}", .{err}); } /// callers must hold ui mutex for the whole duration. @@ -225,12 +221,12 @@ fn commThreadLoop() void { /// the UI accordingly. /// holds ui mutex for most of the duration. 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 defer ui_mutex.unlock(); switch (state) { .standby => switch (msg.value) { - .ping => try comm.write(gpa, stdout, comm.Message.pong), + .ping => try comm.pipeWrite(comm.Message.pong), .network_report, .bitcoind_report, .lightning_report, @@ -238,7 +234,7 @@ fn commThreadLoopCycle() !void { else => logger.debug("ignoring {s}: in standby", .{@tagName(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| { ui.poweroff.updateStatus(rep) catch |err| logger.err("poweroff.updateStatus: {any}", .{err}); msg.deinit(); @@ -275,9 +271,7 @@ fn uiThreadLoop() void { .standby => { // go into a screen sleep mode due to no user activity wakeup.reset(); - comm.write(gpa, stdout, comm.Message.standby) catch |err| { - logger.err("comm.write standby: {any}", .{err}); - }; + comm.pipeWrite(comm.Message.standby) catch |err| logger.err("standby: {any}", .{err}); screen.sleep(&ui_mutex, &wakeup); // blocking // wake up due to touch screen activity or wakeup event is set @@ -286,9 +280,7 @@ fn uiThreadLoop() void { defer ui_mutex.unlock(); if (state == .standby) { state = .active; - comm.write(gpa, stdout, comm.Message.wakeup) catch |err| { - logger.err("comm.write wakeup: {any}", .{err}); - }; + comm.pipeWrite(comm.Message.wakeup) catch |err| logger.err("wakeup: {any}", .{err}); lvgl.resetIdle(); last_report.mu.lock(); @@ -369,6 +361,9 @@ pub fn main() anyerror!void { // the UI is unusable otherwise. 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. ui.init() catch |err| { logger.err("ui.init: {any}", .{err}); diff --git a/src/test.zig b/src/test.zig index b5744c5..f158045 100644 --- a/src/test.zig +++ b/src/test.zig @@ -2,6 +2,9 @@ const std = @import("std"); const builtin = @import("builtin"); const nif = @import("nif"); +const comm = @import("comm.zig"); +const types = @import("types.zig"); + comptime { if (!builtin.is_test) @compileError("test-only module"); } @@ -19,6 +22,27 @@ export fn lv_disp_get_inactive_time(disp: *opaque {}) u32 { 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. pub const TestTimer = struct { value: u64,