ngui: display poweroff progress during system shutdown
this greatly improves UX during shutdown: no need to guess at which point it's ok to unplug the power cord. the commit also includes a GUI playground. a sort of a fake daemon which sends hardcoded messages to ngui. useful for manual debugging and trying different UX scenarious.pull/22/head
parent
78df4ad7ee
commit
532581a246
@ -0,0 +1,158 @@
|
||||
const std = @import("std");
|
||||
const time = std.time;
|
||||
const os = std.os;
|
||||
|
||||
const comm = @import("comm");
|
||||
|
||||
const logger = std.log.scoped(.play);
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
|
||||
var ngui_proc: std.ChildProcess = undefined;
|
||||
var sigquit = false;
|
||||
|
||||
fn sighandler(sig: c_int) callconv(.C) void {
|
||||
logger.info("received signal {} (TERM={} INT={})", .{ sig, os.SIG.TERM, os.SIG.INT });
|
||||
switch (sig) {
|
||||
os.SIG.INT, os.SIG.TERM => sigquit = true,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
|
||||
stderr.print(fmt, args) catch {};
|
||||
if (fmt[fmt.len - 1] != '\n') {
|
||||
stderr.writeByte('\n') catch {};
|
||||
}
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
const Flags = struct {
|
||||
ngui_path: ?[:0]const u8 = null,
|
||||
|
||||
fn deinit(self: @This(), allocator: std.mem.Allocator) void {
|
||||
if (self.ngui_path) |p| allocator.free(p);
|
||||
}
|
||||
};
|
||||
|
||||
fn parseArgs(gpa: std.mem.Allocator) !Flags {
|
||||
var flags: Flags = .{};
|
||||
|
||||
var args = try std.process.ArgIterator.initWithAllocator(gpa);
|
||||
defer args.deinit();
|
||||
const prog = args.next() orelse return error.NoProgName;
|
||||
|
||||
var lastarg: enum {
|
||||
none,
|
||||
ngui_path,
|
||||
} = .none;
|
||||
while (args.next()) |a| {
|
||||
switch (lastarg) {
|
||||
.none => {},
|
||||
.ngui_path => {
|
||||
flags.ngui_path = try gpa.dupeZ(u8, a);
|
||||
lastarg = .none;
|
||||
continue;
|
||||
},
|
||||
}
|
||||
if (std.mem.eql(u8, a, "-ngui")) {
|
||||
lastarg = .ngui_path;
|
||||
} else {
|
||||
fatal("unknown arg name {s}", .{a});
|
||||
}
|
||||
}
|
||||
if (lastarg != .none) {
|
||||
fatal("invalid arg: {s} requires a value", .{@tagName(lastarg)});
|
||||
}
|
||||
|
||||
if (flags.ngui_path == null) {
|
||||
const dir = std.fs.path.dirname(prog) orelse "/";
|
||||
flags.ngui_path = try std.fs.path.joinZ(gpa, &.{ dir, "ngui" });
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer if (gpa_state.deinit()) {
|
||||
logger.err("memory leaks detected", .{});
|
||||
};
|
||||
const gpa = gpa_state.allocator();
|
||||
const flags = try parseArgs(gpa);
|
||||
defer flags.deinit(gpa);
|
||||
|
||||
ngui_proc = std.ChildProcess.init(&.{flags.ngui_path.?}, gpa);
|
||||
ngui_proc.stdin_behavior = .Pipe;
|
||||
ngui_proc.stdout_behavior = .Pipe;
|
||||
ngui_proc.stderr_behavior = .Inherit;
|
||||
ngui_proc.spawn() catch |err| {
|
||||
fatal("unable to start ngui: {any}", .{err});
|
||||
};
|
||||
|
||||
const sa = os.Sigaction{
|
||||
.handler = .{ .handler = sighandler },
|
||||
.mask = os.empty_sigset,
|
||||
.flags = 0,
|
||||
};
|
||||
try os.sigaction(os.SIG.INT, &sa, null);
|
||||
try os.sigaction(os.SIG.TERM, &sa, null);
|
||||
|
||||
const uireader = ngui_proc.stdout.?.reader();
|
||||
const uiwriter = ngui_proc.stdin.?.writer();
|
||||
comm.write(gpa, uiwriter, .ping) catch |err| {
|
||||
logger.err("comm.write ping: {any}", .{err});
|
||||
};
|
||||
|
||||
var poweroff = false;
|
||||
while (!sigquit) {
|
||||
std.atomic.spinLoopHint();
|
||||
time.sleep(100 * time.ns_per_ms);
|
||||
if (poweroff) {
|
||||
// GUI is not expected to send anything back at this point,
|
||||
// so just loop until we're terminated by a SIGTERM (sigquit).
|
||||
continue;
|
||||
}
|
||||
|
||||
const msg = comm.read(gpa, uireader) catch |err| {
|
||||
logger.err("comm.read: {any}", .{err});
|
||||
continue;
|
||||
};
|
||||
logger.debug("got ui msg tagged {s}", .{@tagName(msg)});
|
||||
switch (msg) {
|
||||
.pong => {
|
||||
logger.info("received pong from ngui", .{});
|
||||
},
|
||||
.poweroff => {
|
||||
poweroff = true;
|
||||
|
||||
logger.info("sending poweroff status1", .{});
|
||||
var s1: comm.Message.PoweroffProgress = .{ .services = &.{
|
||||
.{ .name = "lnd", .stopped = false, .err = null },
|
||||
.{ .name = "bitcoind", .stopped = false, .err = null },
|
||||
} };
|
||||
comm.write(gpa, uiwriter, .{ .poweroff_progress = s1 }) catch |err| logger.err("comm.write: {any}", .{err});
|
||||
|
||||
time.sleep(2 * time.ns_per_s);
|
||||
logger.info("sending poweroff status2", .{});
|
||||
var s2: comm.Message.PoweroffProgress = .{ .services = &.{
|
||||
.{ .name = "lnd", .stopped = true, .err = null },
|
||||
.{ .name = "bitcoind", .stopped = false, .err = null },
|
||||
} };
|
||||
comm.write(gpa, uiwriter, .{ .poweroff_progress = s2 }) catch |err| logger.err("comm.write: {any}", .{err});
|
||||
|
||||
time.sleep(3 * time.ns_per_s);
|
||||
logger.info("sending poweroff status3", .{});
|
||||
var s3: comm.Message.PoweroffProgress = .{ .services = &.{
|
||||
.{ .name = "lnd", .stopped = true, .err = null },
|
||||
.{ .name = "bitcoind", .stopped = true, .err = null },
|
||||
} };
|
||||
comm.write(gpa, uiwriter, .{ .poweroff_progress = s3 }) catch |err| logger.err("comm.write: {any}", .{err});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("killing ngui", .{});
|
||||
const term = ngui_proc.kill();
|
||||
logger.info("ngui_proc.kill term: {any}", .{term});
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
//! poweroff workflow.
|
||||
//! all functions assume ui_mutex is always locked.
|
||||
const std = @import("std");
|
||||
|
||||
const comm = @import("../comm.zig");
|
||||
const lvgl = @import("lvgl.zig");
|
||||
const symbol = @import("symbol.zig");
|
||||
const widget = @import("widget.zig");
|
||||
|
||||
const logger = std.log.scoped(.ui);
|
||||
|
||||
/// initiates system shutdown leading to power off.
|
||||
/// defined in ngui.zig.
|
||||
extern fn nm_sys_shutdown() void;
|
||||
|
||||
/// called when "power off" button is pressed.
|
||||
export fn nm_poweroff_btn_callback(_: *lvgl.LvEvent) void {
|
||||
const proceed: [*:0]const u8 = "PROCEED";
|
||||
const abort: [*:0]const u8 = "CANCEL";
|
||||
const title = " " ++ symbol.Power ++ " SHUTDOWN";
|
||||
const text =
|
||||
\\ARE YOU SURE?
|
||||
\\
|
||||
\\once shut down,
|
||||
\\payments cannot go through via bitcoin or lightning networks
|
||||
\\until the node is powered back on.
|
||||
;
|
||||
widget.modal(title, text, &.{ proceed, abort }, poweroffModalCallback) catch |err| {
|
||||
logger.err("shutdown btn: modal: {any}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// poweroff confirmation screen callback.
|
||||
fn poweroffModalCallback(btn_idx: usize) void {
|
||||
// proceed = 0, cancel = 1
|
||||
if (btn_idx != 0) {
|
||||
return;
|
||||
}
|
||||
defer nm_sys_shutdown(); // initiate shutdown even if next lines fail
|
||||
global_progress_win = ProgressWin.create() catch |err| {
|
||||
logger.err("ProgressWin.create: {any}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
var global_progress_win: ?ProgressWin = null;
|
||||
|
||||
/// updates the global poweroff process window with the status report.
|
||||
/// the report is normally sent to GUI by the daemon.
|
||||
pub fn updateStatus(report: comm.Message.PoweroffProgress) !void {
|
||||
if (global_progress_win) |win| {
|
||||
var all_stopped = true;
|
||||
win.resetSvContainer();
|
||||
for (report.services) |sv| {
|
||||
try win.addServiceStatus(sv.name, sv.stopped, sv.err);
|
||||
all_stopped = all_stopped and sv.stopped;
|
||||
}
|
||||
if (all_stopped) {
|
||||
win.status.setLabelText("powering off ...");
|
||||
}
|
||||
} else {
|
||||
return error.NoProgressWindow;
|
||||
}
|
||||
}
|
||||
|
||||
/// represents a modal window in which the poweroff progress is reported until
|
||||
/// the device turns off.
|
||||
const ProgressWin = struct {
|
||||
win: lvgl.Window,
|
||||
status: *lvgl.LvObj, // text status label
|
||||
svcont: *lvgl.LvObj, // services container
|
||||
|
||||
/// symbol width next to the service name. this aligns all service names vertically.
|
||||
/// has to be wide enough to accomodate the spinner, but not too wide
|
||||
/// so that the service name is still close to the symbol.
|
||||
const sym_width = 20;
|
||||
|
||||
fn create() !ProgressWin {
|
||||
const win = try lvgl.createWindow(null, 60, " " ++ symbol.Power ++ " SHUTDOWN");
|
||||
errdefer win.winobj.destroy(); // also deletes all children created below
|
||||
const wincont = win.content();
|
||||
wincont.flexFlow(.column);
|
||||
|
||||
// initial status message
|
||||
const status = try lvgl.createLabel(wincont, "shutting down services. it may take up to a few minutes.", .{});
|
||||
status.setWidth(lvgl.sizePercent(100));
|
||||
// prepare a container for services status
|
||||
const svcont = try lvgl.createObject(wincont);
|
||||
svcont.removeBackgroundStyle();
|
||||
svcont.flexFlow(.column);
|
||||
svcont.flexGrow(1);
|
||||
svcont.padColumnDefault();
|
||||
svcont.setWidth(lvgl.sizePercent(100));
|
||||
|
||||
return .{
|
||||
.win = win,
|
||||
.status = status,
|
||||
.svcont = svcont,
|
||||
};
|
||||
}
|
||||
|
||||
fn resetSvContainer(self: ProgressWin) void {
|
||||
self.svcont.deleteChildren();
|
||||
}
|
||||
|
||||
fn addServiceStatus(self: ProgressWin, name: []const u8, stopped: bool, err: ?[]const u8) !void {
|
||||
const row = try lvgl.createObject(self.svcont);
|
||||
row.removeBackgroundStyle();
|
||||
row.flexFlow(.row);
|
||||
row.flexAlign(.center, .center, .center);
|
||||
row.padColumnDefault();
|
||||
row.setPad(10, .all, .{});
|
||||
row.setWidth(lvgl.sizePercent(100));
|
||||
row.setHeightToContent();
|
||||
|
||||
var buf: [100]u8 = undefined;
|
||||
if (err) |e| {
|
||||
const sym = try lvgl.createLabelFmt(row, &buf, symbol.Warning, .{}, .{ .long_mode = .clip });
|
||||
sym.setWidth(sym_width);
|
||||
sym.setTextColor(lvgl.paletteMain(.red), .{});
|
||||
const lb = try lvgl.createLabelFmt(row, &buf, "{s}: {s}", .{ name, e }, .{ .long_mode = .dot });
|
||||
lb.setTextColor(lvgl.paletteMain(.red), .{});
|
||||
lb.flexGrow(1);
|
||||
} else if (stopped) {
|
||||
const sym = try lvgl.createLabelFmt(row, &buf, symbol.Ok, .{}, .{ .long_mode = .clip });
|
||||
sym.setWidth(sym_width);
|
||||
const lb = try lvgl.createLabelFmt(row, &buf, "{s}", .{name}, .{ .long_mode = .dot });
|
||||
lb.flexGrow(1);
|
||||
} else {
|
||||
const spin = try lvgl.createSpinner(row);
|
||||
spin.setWidth(sym_width);
|
||||
const lb = try lvgl.createLabelFmt(row, &buf, "{s}", .{name}, .{ .long_mode = .dot });
|
||||
lb.flexGrow(1);
|
||||
}
|
||||
}
|
||||
};
|
Reference in New Issue