report system shutdown progress during poweroff #22
17
build.zig
17
build.zig
|
@ -104,7 +104,7 @@ pub fn build(b: *std.build.Builder) void {
|
||||||
nd_build_step.dependOn(&b.addInstallArtifact(nd).step);
|
nd_build_step.dependOn(&b.addInstallArtifact(nd).step);
|
||||||
|
|
||||||
// default build
|
// default build
|
||||||
const build_all_step = b.step("all", "build everything");
|
const build_all_step = b.step("all", "build nd and ngui");
|
||||||
build_all_step.dependOn(ngui_build_step);
|
build_all_step.dependOn(ngui_build_step);
|
||||||
build_all_step.dependOn(nd_build_step);
|
build_all_step.dependOn(nd_build_step);
|
||||||
b.default_step.dependOn(build_all_step);
|
b.default_step.dependOn(build_all_step);
|
||||||
|
@ -114,12 +114,27 @@ pub fn build(b: *std.build.Builder) void {
|
||||||
tests.setTarget(target);
|
tests.setTarget(target);
|
||||||
tests.setBuildMode(mode);
|
tests.setBuildMode(mode);
|
||||||
tests.linkLibC();
|
tests.linkLibC();
|
||||||
|
tests.addPackage(buildopts.getPackage("build_options"));
|
||||||
|
nifbuild.addPkg(b, tests, "lib/nif");
|
||||||
|
|
||||||
const f = b.option([]const u8, "test-filter", "run tests matching the filter");
|
const f = b.option([]const u8, "test-filter", "run tests matching the filter");
|
||||||
tests.setFilter(f);
|
tests.setFilter(f);
|
||||||
|
|
||||||
const test_step = b.step("test", "run tests");
|
const test_step = b.step("test", "run tests");
|
||||||
test_step.dependOn(&tests.step);
|
test_step.dependOn(&tests.step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const guiplay = b.addExecutable("guiplay", "src/test/guiplay.zig");
|
||||||
|
guiplay.setTarget(target);
|
||||||
|
guiplay.setBuildMode(mode);
|
||||||
|
guiplay.step.dependOn(semver_step);
|
||||||
|
guiplay.addPackagePath("comm", "src/comm.zig");
|
||||||
|
|
||||||
|
const guiplay_build_step = b.step("guiplay", "build GUI playground");
|
||||||
|
guiplay_build_step.dependOn(&b.addInstallArtifact(guiplay).step);
|
||||||
|
guiplay_build_step.dependOn(ngui_build_step);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DriverTarget = enum {
|
const DriverTarget = enum {
|
||||||
|
|
|
@ -3,11 +3,11 @@ const mem = std.mem;
|
||||||
const Thread = std.Thread;
|
const Thread = std.Thread;
|
||||||
|
|
||||||
const WPACtrl = opaque {};
|
const WPACtrl = opaque {};
|
||||||
const WPAReqCallback = *const fn ([*:0]const u8, usize) callconv(.C) void;
|
pub const ReqCallback = *const fn ([*:0]const u8, usize) callconv(.C) void;
|
||||||
|
|
||||||
extern fn wpa_ctrl_open(ctrl_path: [*:0]const u8) ?*WPACtrl;
|
extern fn wpa_ctrl_open(ctrl_path: [*:0]const u8) ?*WPACtrl;
|
||||||
extern fn wpa_ctrl_close(ctrl: *WPACtrl) void;
|
extern fn wpa_ctrl_close(ctrl: *WPACtrl) void;
|
||||||
extern fn wpa_ctrl_request(ctrl: *WPACtrl, cmd: [*:0]const u8, clen: usize, reply: [*:0]u8, rlen: *usize, cb: ?WPAReqCallback) c_int;
|
extern fn wpa_ctrl_request(ctrl: *WPACtrl, cmd: [*:0]const u8, clen: usize, reply: [*:0]u8, rlen: *usize, cb: ?ReqCallback) c_int;
|
||||||
extern fn wpa_ctrl_pending(ctrl: *WPACtrl) c_int;
|
extern fn wpa_ctrl_pending(ctrl: *WPACtrl) c_int;
|
||||||
extern fn wpa_ctrl_recv(ctrl: *WPACtrl, reply: [*:0]u8, reply_len: *usize) c_int;
|
extern fn wpa_ctrl_recv(ctrl: *WPACtrl, reply: [*:0]u8, reply_len: *usize) c_int;
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ pub const Control = struct {
|
||||||
|
|
||||||
/// send a command to the control interface, returning a response owned by buf.
|
/// send a command to the control interface, returning a response owned by buf.
|
||||||
/// callback receives a message from the same buf.
|
/// callback receives a message from the same buf.
|
||||||
pub fn request(self: Self, cmd: [:0]const u8, buf: [:0]u8, callback: ?WPAReqCallback) Error![]const u8 {
|
pub fn request(self: Self, cmd: [:0]const u8, buf: [:0]u8, callback: ?ReqCallback) Error![]const u8 {
|
||||||
//self.mu.lock();
|
//self.mu.lock();
|
||||||
//defer self.mu.unlock();
|
//defer self.mu.unlock();
|
||||||
var n: usize = buf.len;
|
var n: usize = buf.len;
|
||||||
|
|
35
src/comm.zig
35
src/comm.zig
|
@ -24,6 +24,7 @@ pub const Message = union(MessageTag) {
|
||||||
wifi_connect: WifiConnect,
|
wifi_connect: WifiConnect,
|
||||||
network_report: NetworkReport,
|
network_report: NetworkReport,
|
||||||
get_network_report: GetNetworkReport,
|
get_network_report: GetNetworkReport,
|
||||||
|
poweroff_progress: PoweroffProgress,
|
||||||
|
|
||||||
pub const WifiConnect = struct {
|
pub const WifiConnect = struct {
|
||||||
ssid: []const u8,
|
ssid: []const u8,
|
||||||
|
@ -39,6 +40,16 @@ pub const Message = union(MessageTag) {
|
||||||
pub const GetNetworkReport = struct {
|
pub const GetNetworkReport = struct {
|
||||||
scan: bool, // true starts a wifi scan and send NetworkReport only after completion
|
scan: bool, // true starts a wifi scan and send NetworkReport only after completion
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const PoweroffProgress = struct {
|
||||||
|
services: []const Service,
|
||||||
|
|
||||||
|
pub const Service = struct {
|
||||||
|
name: []const u8,
|
||||||
|
stopped: bool,
|
||||||
|
err: ?[]const u8,
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// it is important to preserve ordinal values for future compatiblity,
|
/// it is important to preserve ordinal values for future compatiblity,
|
||||||
|
@ -54,7 +65,9 @@ pub const MessageTag = enum(u16) {
|
||||||
standby = 0x07,
|
standby = 0x07,
|
||||||
// ngui -> nd: resume screen due to user touch; no reply
|
// ngui -> nd: resume screen due to user touch; no reply
|
||||||
wakeup = 0x08,
|
wakeup = 0x08,
|
||||||
// next: 0x09
|
// nd -> ngui: reports poweroff progress
|
||||||
|
poweroff_progress = 0x09,
|
||||||
|
// next: 0x0a
|
||||||
};
|
};
|
||||||
|
|
||||||
/// reads and parses a single message from the input stream reader.
|
/// reads and parses a single message from the input stream reader.
|
||||||
|
@ -92,6 +105,9 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
|
||||||
.get_network_report => Message{
|
.get_network_report => Message{
|
||||||
.get_network_report = try json.parse(Message.GetNetworkReport, &jstream, jopt),
|
.get_network_report = try json.parse(Message.GetNetworkReport, &jstream, jopt),
|
||||||
},
|
},
|
||||||
|
.poweroff_progress => Message{
|
||||||
|
.poweroff_progress = try json.parse(Message.PoweroffProgress, &jstream, jopt),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +122,7 @@ pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
|
||||||
.wifi_connect => try json.stringify(msg.wifi_connect, jopt, data.writer()),
|
.wifi_connect => try json.stringify(msg.wifi_connect, jopt, data.writer()),
|
||||||
.network_report => try json.stringify(msg.network_report, 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()),
|
.get_network_report => try json.stringify(msg.get_network_report, jopt, data.writer()),
|
||||||
|
.poweroff_progress => try json.stringify(msg.poweroff_progress, jopt, data.writer()),
|
||||||
}
|
}
|
||||||
if (data.items.len > std.math.maxInt(u64)) {
|
if (data.items.len > std.math.maxInt(u64)) {
|
||||||
return Error.CommWriteTooLarge;
|
return Error.CommWriteTooLarge;
|
||||||
|
@ -125,6 +142,22 @@ pub fn free(allocator: mem.Allocator, m: Message) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use fifo
|
||||||
|
//
|
||||||
|
// var buf = std.fifo.LinearFifo(u8, .Dynamic).init(t.allocator);
|
||||||
|
// defer buf.deinit();
|
||||||
|
// const w = buf.writer();
|
||||||
|
// const r = buf.reader();
|
||||||
|
// try w.writeAll("hello there");
|
||||||
|
//
|
||||||
|
// var b: [100]u8 = undefined;
|
||||||
|
// var n = try r.readAll(&b);
|
||||||
|
// try t.expectEqualStrings("hello there", b[0..n]);
|
||||||
|
//
|
||||||
|
// try w.writeAll("once more");
|
||||||
|
// n = try r.readAll(&b);
|
||||||
|
// try t.expectEqualStrings("once more2", b[0..n]);
|
||||||
|
|
||||||
test "read" {
|
test "read" {
|
||||||
const t = std.testing;
|
const t = std.testing;
|
||||||
|
|
||||||
|
|
89
src/nd.zig
89
src/nd.zig
|
@ -111,13 +111,17 @@ fn parseArgs(gpa: std.mem.Allocator) !NdArgs {
|
||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// quit signals nd to exit.
|
/// sigquit signals nd main loop to exit.
|
||||||
/// TODO: thread-safety?
|
/// since both the loop and sighandler are on the same thread, it must
|
||||||
var quit = false;
|
/// not be guarded by a mutex which otherwise leads to a dealock.
|
||||||
|
var sigquit = false;
|
||||||
|
|
||||||
fn sighandler(sig: c_int) callconv(.C) void {
|
fn sighandler(sig: c_int) callconv(.C) void {
|
||||||
logger.info("got signal {}; exiting...\n", .{sig});
|
logger.info("received signal {}", .{sig});
|
||||||
quit = true;
|
switch (sig) {
|
||||||
|
os.SIG.INT, os.SIG.TERM => sigquit = true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
|
@ -134,9 +138,7 @@ pub fn main() !void {
|
||||||
|
|
||||||
// reset the screen backlight to normal power regardless
|
// reset the screen backlight to normal power regardless
|
||||||
// of its previous state.
|
// of its previous state.
|
||||||
screen.backlight(.on) catch |err| {
|
screen.backlight(.on) catch |err| logger.err("backlight: {any}", .{err});
|
||||||
logger.err("backlight: {any}", .{err});
|
|
||||||
};
|
|
||||||
|
|
||||||
// start ngui, unless -nogui mode
|
// start ngui, unless -nogui mode
|
||||||
var ngui = std.ChildProcess.init(&.{args.gui.?}, gpa);
|
var ngui = std.ChildProcess.init(&.{args.gui.?}, gpa);
|
||||||
|
@ -156,9 +158,8 @@ pub fn main() !void {
|
||||||
//ngui.uid = uiuser.uid;
|
//ngui.uid = uiuser.uid;
|
||||||
//ngui.gid = uiuser.gid;
|
//ngui.gid = uiuser.gid;
|
||||||
// ngui.env_map = ...
|
// ngui.env_map = ...
|
||||||
ngui.spawn() catch |err| {
|
ngui.spawn() catch |err| fatal("unable to start ngui: {any}", .{err});
|
||||||
fatal("unable to start ngui: {any}", .{err});
|
|
||||||
};
|
|
||||||
// TODO: thread-safety, esp. uiwriter
|
// TODO: thread-safety, esp. uiwriter
|
||||||
const uireader = ngui.stdout.?.reader();
|
const uireader = ngui.stdout.?.reader();
|
||||||
const uiwriter = ngui.stdin.?.writer();
|
const uiwriter = ngui.stdin.?.writer();
|
||||||
|
@ -175,24 +176,25 @@ pub fn main() !void {
|
||||||
.flags = 0,
|
.flags = 0,
|
||||||
};
|
};
|
||||||
try os.sigaction(os.SIG.INT, &sa, null);
|
try os.sigaction(os.SIG.INT, &sa, null);
|
||||||
//TODO: try os.sigaction(os.SIG.TERM, &sa, null);
|
try os.sigaction(os.SIG.TERM, &sa, null);
|
||||||
|
|
||||||
// start network monitor
|
var nd = try Daemon.init(gpa, uiwriter, args.wpa.?);
|
||||||
var ctrl = try nif.wpa.Control.open(args.wpa.?);
|
defer nd.deinit();
|
||||||
defer ctrl.close() catch {};
|
|
||||||
var nd: Daemon = .{
|
|
||||||
.allocator = gpa,
|
|
||||||
.uiwriter = uiwriter,
|
|
||||||
.wpa_ctrl = ctrl,
|
|
||||||
};
|
|
||||||
try nd.start();
|
try nd.start();
|
||||||
// send the UI network report right away, without scanning wifi
|
// send the UI network report right away, without scanning wifi
|
||||||
nd.reportNetworkStatus(.{ .scan = false });
|
nd.reportNetworkStatus(.{ .scan = false });
|
||||||
|
|
||||||
// comm with ui loop; run until exit is requested
|
|
||||||
var poweroff = false;
|
var poweroff = false;
|
||||||
while (!quit) {
|
// ngui -> nd comm loop; run until exit is requested
|
||||||
|
// TODO: move this loop to Daemon.zig? but what about quit and keep ngui running
|
||||||
|
while (!sigquit) {
|
||||||
time.sleep(100 * time.ns_per_ms);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// note: uireader.read is blocking
|
// note: uireader.read is blocking
|
||||||
// TODO: handle error.EndOfStream - ngui exited
|
// TODO: handle error.EndOfStream - ngui exited
|
||||||
const msg = comm.read(gpa, uireader) catch |err| {
|
const msg = comm.read(gpa, uireader) catch |err| {
|
||||||
|
@ -205,9 +207,11 @@ pub fn main() !void {
|
||||||
logger.info("received pong from ngui", .{});
|
logger.info("received pong from ngui", .{});
|
||||||
},
|
},
|
||||||
.poweroff => {
|
.poweroff => {
|
||||||
logger.info("poweroff requested; terminating", .{});
|
|
||||||
quit = true;
|
|
||||||
poweroff = true;
|
poweroff = true;
|
||||||
|
nd.beginPoweroff() catch |err| {
|
||||||
|
logger.err("beginPoweroff: {any}", .{err});
|
||||||
|
poweroff = false;
|
||||||
|
};
|
||||||
},
|
},
|
||||||
.get_network_report => |req| {
|
.get_network_report => |req| {
|
||||||
nd.reportNetworkStatus(.{ .scan = req.scan });
|
nd.reportNetworkStatus(.{ .scan = req.scan });
|
||||||
|
@ -234,39 +238,10 @@ pub fn main() !void {
|
||||||
comm.free(gpa, msg);
|
comm.free(gpa, msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// shutdown
|
// reached here due to sig TERM or INT;
|
||||||
|
// note: poweroff does not terminate the loop and instead initiates
|
||||||
|
// a system shutdown which in turn should terminate this process via a SIGTERM.
|
||||||
|
// so, there's no difference whether we're exiting due to poweroff of a SIGTERM here.
|
||||||
_ = ngui.kill() catch |err| logger.err("ngui.kill: {any}", .{err});
|
_ = ngui.kill() catch |err| logger.err("ngui.kill: {any}", .{err});
|
||||||
nd.stop();
|
nd.stop();
|
||||||
if (poweroff) {
|
|
||||||
svShutdown(gpa);
|
|
||||||
var off = std.ChildProcess.init(&.{"poweroff"}, gpa);
|
|
||||||
_ = try off.spawnAndWait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// shut down important services manually.
|
|
||||||
/// TODO: make this OS-agnostic
|
|
||||||
fn svShutdown(allocator: std.mem.Allocator) void {
|
|
||||||
// sv waits 7sec by default but bitcoind and lnd need more
|
|
||||||
// http://smarden.org/runit/
|
|
||||||
const Argv = []const []const u8;
|
|
||||||
const cmds: []const Argv = &.{
|
|
||||||
&.{ "sv", "-w", "600", "stop", "lnd" },
|
|
||||||
&.{ "sv", "-w", "600", "stop", "bitcoind" },
|
|
||||||
};
|
|
||||||
var procs: [cmds.len]?std.ChildProcess = undefined;
|
|
||||||
for (cmds) |argv, i| {
|
|
||||||
var p = std.ChildProcess.init(argv, allocator);
|
|
||||||
if (p.spawn()) {
|
|
||||||
procs[i] = p;
|
|
||||||
} else |err| {
|
|
||||||
logger.err("{s}: {any}", .{ argv, err });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (procs) |_, i| {
|
|
||||||
var p = procs[i];
|
|
||||||
if (p != null) {
|
|
||||||
_ = p.?.wait() catch |err| logger.err("{any}", .{err});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
///! daemon watches network status and communicates updates to the gui
|
//! daemon watches network status and communicates updates to the GUI using uiwriter.
|
||||||
///! using uiwriter
|
//! public fields are allocator
|
||||||
|
//! usage example:
|
||||||
|
//!
|
||||||
|
//! var ctrl = try nif.wpa.Control.open("/run/wpa_supplicant/wlan0");
|
||||||
|
//! defer ctrl.close() catch {};
|
||||||
|
//! var nd: Daemon = .{
|
||||||
|
//! .allocator = gpa,
|
||||||
|
//! .uiwriter = ngui_stdio_writer,
|
||||||
|
//! .wpa_ctrl = ctrl,
|
||||||
|
//! };
|
||||||
|
//! try nd.start();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
const time = std.time;
|
const time = std.time;
|
||||||
|
@ -8,41 +19,137 @@ const nif = @import("nif");
|
||||||
|
|
||||||
const comm = @import("../comm.zig");
|
const comm = @import("../comm.zig");
|
||||||
const screen = @import("../ui/screen.zig");
|
const screen = @import("../ui/screen.zig");
|
||||||
|
const types = @import("../types.zig");
|
||||||
|
const SysService = @import("SysService.zig");
|
||||||
|
|
||||||
const logger = std.log.scoped(.netmon);
|
const logger = std.log.scoped(.netmon);
|
||||||
|
|
||||||
// pub fields
|
|
||||||
allocator: mem.Allocator,
|
allocator: mem.Allocator,
|
||||||
uiwriter: std.fs.File.Writer, // ngui stdin
|
uiwriter: std.fs.File.Writer, // ngui stdin
|
||||||
wpa_ctrl: nif.wpa.Control, // guarded by mu once start'ed
|
wpa_ctrl: types.WpaControl, // guarded by mu once start'ed
|
||||||
|
|
||||||
// private fields
|
/// guards all the fields below to sync between pub fns and main/poweroff threads.
|
||||||
mu: std.Thread.Mutex = .{},
|
mu: std.Thread.Mutex = .{},
|
||||||
quit: bool = false, // tells daemon to quit
|
|
||||||
main_thread: ?std.Thread = null, // non-nill if started
|
/// daemon state
|
||||||
want_report: bool = false,
|
state: enum {
|
||||||
|
stopped,
|
||||||
|
running,
|
||||||
|
poweroff,
|
||||||
|
},
|
||||||
|
|
||||||
|
main_thread: ?std.Thread = null,
|
||||||
|
poweroff_thread: ?std.Thread = null,
|
||||||
|
|
||||||
|
want_stop: bool = false, // tells daemon main loop to quit
|
||||||
|
want_network_report: bool = false,
|
||||||
want_wifi_scan: bool = false,
|
want_wifi_scan: bool = false,
|
||||||
wifi_scan_in_progress: bool = false,
|
wifi_scan_in_progress: bool = false,
|
||||||
report_ready: bool = true, // no need to scan for an immediate report
|
network_report_ready: bool = true, // no need to scan for an immediate report
|
||||||
wpa_save_config_on_connected: bool = false,
|
wpa_save_config_on_connected: bool = false,
|
||||||
|
|
||||||
|
/// system services actively managed by the daemon.
|
||||||
|
/// these are stop'ed during poweroff and their shutdown progress sent to ngui.
|
||||||
|
/// initialized in start and never modified again: ok to access without holding self.mu.
|
||||||
|
services: []SysService = &.{},
|
||||||
|
|
||||||
const Daemon = @This();
|
const Daemon = @This();
|
||||||
|
|
||||||
pub fn start(self: *Daemon) !void {
|
/// callers must deinit when done.
|
||||||
// TODO: return error if already started
|
pub fn init(a: std.mem.Allocator, iogui: std.fs.File.Writer, wpa_path: [:0]const u8) !Daemon {
|
||||||
self.main_thread = try std.Thread.spawn(.{}, mainThreadLoop, .{self});
|
var svlist = std.ArrayList(SysService).init(a);
|
||||||
|
errdefer {
|
||||||
|
for (svlist.items) |*sv| sv.deinit();
|
||||||
|
svlist.deinit();
|
||||||
|
}
|
||||||
|
// the order is important. when powering off, the services are shut down
|
||||||
|
// in the same order appended here.
|
||||||
|
try svlist.append(SysService.init(a, "lnd", .{ .stop_wait_sec = 600 }));
|
||||||
|
try svlist.append(SysService.init(a, "bitcoind", .{ .stop_wait_sec = 600 }));
|
||||||
|
return .{
|
||||||
|
.allocator = a,
|
||||||
|
.uiwriter = iogui,
|
||||||
|
.wpa_ctrl = try types.WpaControl.open(wpa_path),
|
||||||
|
.state = .stopped,
|
||||||
|
.services = svlist.toOwnedSlice(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// releases all associated resources.
|
||||||
|
/// if the daemon is not in a stopped or poweroff mode, deinit panics.
|
||||||
|
pub fn deinit(self: *Daemon) void {
|
||||||
|
self.mu.lock();
|
||||||
|
defer self.mu.unlock();
|
||||||
|
switch (self.state) {
|
||||||
|
.stopped, .poweroff => if (self.want_stop) {
|
||||||
|
@panic("deinit while stopping");
|
||||||
|
},
|
||||||
|
else => @panic("deinit while running"),
|
||||||
|
}
|
||||||
|
self.wpa_ctrl.close() catch |err| logger.err("deinit: wpa_ctrl.close: {any}", .{err});
|
||||||
|
for (self.services) |*sv| {
|
||||||
|
sv.deinit();
|
||||||
|
}
|
||||||
|
self.allocator.free(self.services);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// start launches a main thread and returns immediately.
|
||||||
|
/// once started, the daemon must be eventually stop'ed to clean up resources
|
||||||
|
/// even if a poweroff sequence is launched with beginPoweroff. however, in the latter
|
||||||
|
/// case the daemon cannot be start'ed again after stop.
|
||||||
|
pub fn start(self: *Daemon) !void {
|
||||||
|
self.mu.lock();
|
||||||
|
defer self.mu.unlock();
|
||||||
|
switch (self.state) {
|
||||||
|
.running => return error.AlreadyStarted,
|
||||||
|
.poweroff => return error.InPoweroffState,
|
||||||
|
.stopped => {}, // continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.wpa_ctrl.attach();
|
||||||
|
self.main_thread = try std.Thread.spawn(.{}, mainThreadLoop, .{self});
|
||||||
|
self.state = .running;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// stop blocks until all daemon threads exit, including poweroff if any.
|
||||||
|
/// once stopped, the daemon can be start'ed again unless a poweroff was initiated.
|
||||||
|
///
|
||||||
|
/// note: stop leaves system services like lnd and bitcoind running.
|
||||||
pub fn stop(self: *Daemon) void {
|
pub fn stop(self: *Daemon) void {
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
self.quit = true;
|
if (self.want_stop or self.state == .stopped) {
|
||||||
self.mu.unlock();
|
self.mu.unlock();
|
||||||
|
return; // already in progress or stopped
|
||||||
|
}
|
||||||
|
self.want_stop = true;
|
||||||
|
self.mu.unlock(); // avoid threads deadlock
|
||||||
|
|
||||||
if (self.main_thread) |th| {
|
if (self.main_thread) |th| {
|
||||||
th.join();
|
th.join();
|
||||||
|
self.main_thread = null;
|
||||||
}
|
}
|
||||||
|
// must be the last one to join because it sends a final poweroff report.
|
||||||
|
if (self.poweroff_thread) |th| {
|
||||||
|
th.join();
|
||||||
|
self.poweroff_thread = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mu.lock();
|
||||||
|
defer self.mu.unlock();
|
||||||
|
self.want_stop = false;
|
||||||
|
if (self.state != .poweroff) { // keep poweroff to prevent start'ing again
|
||||||
|
self.state = .stopped;
|
||||||
|
}
|
||||||
|
self.wpa_ctrl.detach() catch |err| logger.err("stop: wpa_ctrl.detach: {any}", .{err});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn standby(_: *Daemon) !void {
|
pub fn standby(self: *Daemon) !void {
|
||||||
|
self.mu.lock();
|
||||||
|
defer self.mu.unlock();
|
||||||
|
switch (self.state) {
|
||||||
|
.poweroff => return error.InPoweroffState,
|
||||||
|
.running, .stopped => {}, // continue
|
||||||
|
}
|
||||||
try screen.backlight(.off);
|
try screen.backlight(.off);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,27 +157,72 @@ pub fn wakeup(_: *Daemon) !void {
|
||||||
try screen.backlight(.on);
|
try screen.backlight(.on);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// initiates system poweroff sequence in a separate thread: shut down select
|
||||||
|
/// system services such as lnd and bitcoind, and issue "poweroff" command.
|
||||||
|
///
|
||||||
|
/// in the poweroff mode, the daemon is still running as usual and must be stop'ed.
|
||||||
|
/// however, in poweroff mode regular functionalities are disabled, such as
|
||||||
|
/// wifi scan and standby.
|
||||||
|
pub fn beginPoweroff(self: *Daemon) !void {
|
||||||
|
self.mu.lock();
|
||||||
|
defer self.mu.unlock();
|
||||||
|
if (self.state == .poweroff) {
|
||||||
|
return; // already in poweroff state
|
||||||
|
}
|
||||||
|
|
||||||
|
self.poweroff_thread = try std.Thread.spawn(.{}, poweroffThread, .{self});
|
||||||
|
self.state = .poweroff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// stops all monitored services and issue poweroff command while reporting
|
||||||
|
// the progress to ngui.
|
||||||
|
fn poweroffThread(self: *Daemon) !void {
|
||||||
|
logger.info("begin powering off", .{});
|
||||||
|
screen.backlight(.on) catch |err| {
|
||||||
|
logger.err("screen.backlight(.on) during poweroff: {any}", .{err});
|
||||||
|
};
|
||||||
|
self.wpa_ctrl.detach() catch {}; // don't care because powering off anyway
|
||||||
|
|
||||||
|
// initiate shutdown of all services concurrently.
|
||||||
|
for (self.services) |*sv| {
|
||||||
|
sv.stop() catch |err| logger.err("sv stop '{s}': {any}", .{ sv.name, err });
|
||||||
|
}
|
||||||
|
self.sendPoweroffReport() catch |err| logger.err("sendPoweroffReport: {any}", .{err});
|
||||||
|
|
||||||
|
// wait each service until stopped or error.
|
||||||
|
for (self.services) |*sv| {
|
||||||
|
_ = sv.stopWait() catch {};
|
||||||
|
logger.info("{s} sv is now stopped; err={any}", .{ sv.name, sv.lastStopError() });
|
||||||
|
self.sendPoweroffReport() catch |err| logger.err("sendPoweroffReport: {any}", .{err});
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, initiate system shutdown and power it off.
|
||||||
|
var off = types.ChildProcess.init(&.{"poweroff"}, self.allocator);
|
||||||
|
const res = off.spawnAndWait();
|
||||||
|
logger.info("poweroff: {any}", .{res});
|
||||||
|
}
|
||||||
|
|
||||||
/// main thread entry point.
|
/// main thread entry point.
|
||||||
fn mainThreadLoop(self: *Daemon) !void {
|
fn mainThreadLoop(self: *Daemon) !void {
|
||||||
try self.wpa_ctrl.attach();
|
var quit = false;
|
||||||
defer self.wpa_ctrl.detach() catch |err| logger.err("wpa_ctrl.detach failed on exit: {any}", .{err});
|
while (!quit) {
|
||||||
|
self.mainThreadLoopCycle() catch |err| logger.err("main thread loop: {any}", .{err});
|
||||||
while (true) {
|
|
||||||
time.sleep(1 * time.ns_per_s);
|
time.sleep(1 * time.ns_per_s);
|
||||||
self.mainThreadLoopCycle();
|
|
||||||
|
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
const do_quit = self.quit;
|
quit = self.want_stop;
|
||||||
self.mu.unlock();
|
self.mu.unlock();
|
||||||
if (do_quit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// run one cycle of the main thread loop iteration.
|
/// run one cycle of the main thread loop iteration.
|
||||||
/// holds self.mu for the whole duration.
|
/// unless in poweroff mode, the cycle holds self.mu for the whole duration.
|
||||||
fn mainThreadLoopCycle(self: *Daemon) void {
|
fn mainThreadLoopCycle(self: *Daemon) !void {
|
||||||
|
switch (self.state) {
|
||||||
|
// poweroff mode: do nothing; handled by poweroffThread
|
||||||
|
.poweroff => {},
|
||||||
|
// normal state: running or standby
|
||||||
|
else => {
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
|
self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
|
||||||
|
@ -81,27 +233,43 @@ fn mainThreadLoopCycle(self: *Daemon) void {
|
||||||
logger.err("startWifiScan: {any}", .{err});
|
logger.err("startWifiScan: {any}", .{err});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (self.want_report and self.report_ready) {
|
if (self.want_network_report and self.network_report_ready) {
|
||||||
if (self.sendNetworkReport()) {
|
if (self.sendNetworkReport()) {
|
||||||
self.want_report = false;
|
self.want_network_report = false;
|
||||||
} else |err| {
|
} else |err| {
|
||||||
logger.err("sendNetworkReport: {any}", .{err});
|
logger.err("sendNetworkReport: {any}", .{err});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sendPoweroffReport(self: *Daemon) !void {
|
||||||
|
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.len);
|
||||||
|
defer self.allocator.free(svstat);
|
||||||
|
for (self.services) |*sv, i| {
|
||||||
|
svstat[i] = .{
|
||||||
|
.name = sv.name,
|
||||||
|
.stopped = sv.status() == .stopped,
|
||||||
|
.err = if (sv.lastStopError()) |err| @errorName(err) else null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const report = comm.Message{ .poweroff_progress = .{ .services = svstat } };
|
||||||
|
try comm.write(self.allocator, self.uiwriter, report);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// caller must hold self.mu.
|
/// caller must hold self.mu.
|
||||||
fn startWifiScan(self: *Daemon) !void {
|
fn startWifiScan(self: *Daemon) !void {
|
||||||
try self.wpa_ctrl.scan();
|
try self.wpa_ctrl.scan();
|
||||||
self.wifi_scan_in_progress = true;
|
self.wifi_scan_in_progress = true;
|
||||||
self.report_ready = false;
|
self.network_report_ready = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// invoked when CTRL-EVENT-SCAN-RESULTS event is seen.
|
/// invoked when CTRL-EVENT-SCAN-RESULTS event is seen.
|
||||||
/// caller must hold self.mu.
|
/// caller must hold self.mu.
|
||||||
fn wifiScanComplete(self: *Daemon) void {
|
fn wifiScanComplete(self: *Daemon) void {
|
||||||
self.wifi_scan_in_progress = false;
|
self.wifi_scan_in_progress = false;
|
||||||
self.report_ready = true;
|
self.network_report_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// invoked when CTRL-EVENT-CONNECTED event is seen.
|
/// invoked when CTRL-EVENT-CONNECTED event is seen.
|
||||||
|
@ -117,15 +285,15 @@ fn wifiConnected(self: *Daemon) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// always send a network report when connected
|
// always send a network report when connected
|
||||||
self.want_report = true;
|
self.want_network_report = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// invoked when CTRL-EVENT-SSID-TEMP-DISABLED event with authentication failures is seen.
|
/// invoked when CTRL-EVENT-SSID-TEMP-DISABLED event with authentication failures is seen.
|
||||||
/// caller must hold self.mu.
|
/// caller must hold self.mu.
|
||||||
fn wifiInvalidKey(self: *Daemon) void {
|
fn wifiInvalidKey(self: *Daemon) void {
|
||||||
self.wpa_save_config_on_connected = false;
|
self.wpa_save_config_on_connected = false;
|
||||||
self.want_report = true;
|
self.want_network_report = true;
|
||||||
self.report_ready = true;
|
self.network_report_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ReportNetworkStatusOpt = struct {
|
pub const ReportNetworkStatusOpt = struct {
|
||||||
|
@ -135,10 +303,10 @@ pub const ReportNetworkStatusOpt = struct {
|
||||||
pub fn reportNetworkStatus(self: *Daemon, opt: ReportNetworkStatusOpt) void {
|
pub fn reportNetworkStatus(self: *Daemon, opt: ReportNetworkStatusOpt) void {
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
self.want_report = true;
|
self.want_network_report = true;
|
||||||
self.want_wifi_scan = opt.scan and !self.wifi_scan_in_progress;
|
self.want_wifi_scan = opt.scan and !self.wifi_scan_in_progress;
|
||||||
if (self.want_wifi_scan and self.report_ready) {
|
if (self.want_wifi_scan and self.network_report_ready) {
|
||||||
self.report_ready = false;
|
self.network_report_ready = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +328,7 @@ fn connectWifiThread(self: *Daemon, ssid: []const u8, password: []const u8) void
|
||||||
// https://hostap.epitest.fi/wpa_supplicant/devel/ctrl_iface_page.html
|
// https://hostap.epitest.fi/wpa_supplicant/devel/ctrl_iface_page.html
|
||||||
// https://wiki.archlinux.org/title/WPA_supplicant
|
// https://wiki.archlinux.org/title/WPA_supplicant
|
||||||
|
|
||||||
// unfortunately, this prevents main thread from looping until released.
|
// this prevents main thread from looping until released,
|
||||||
// but the following commands and expected to be pretty quick.
|
// but the following commands and expected to be pretty quick.
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
|
@ -305,6 +473,7 @@ fn sendNetworkReport(self: *Daemon) !void {
|
||||||
return comm.write(self.allocator, self.uiwriter, comm.Message{ .network_report = report });
|
return comm.write(self.allocator, self.uiwriter, comm.Message{ .network_report = report });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// caller must hold self.mu.
|
||||||
fn queryWifiSSID(self: *Daemon) !?[]const u8 {
|
fn queryWifiSSID(self: *Daemon) !?[]const u8 {
|
||||||
var buf: [512:0]u8 = undefined;
|
var buf: [512:0]u8 = undefined;
|
||||||
const resp = try self.wpa_ctrl.request("STATUS", &buf, null);
|
const resp = try self.wpa_ctrl.request("STATUS", &buf, null);
|
||||||
|
@ -320,7 +489,8 @@ fn queryWifiSSID(self: *Daemon) !?[]const u8 {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// callers must free with StringList.deinit.
|
/// caller must hold self.mu.
|
||||||
|
/// the retuned value must free'd with StringList.deinit.
|
||||||
fn queryWifiScanResults(self: *Daemon) !StringList {
|
fn queryWifiScanResults(self: *Daemon) !StringList {
|
||||||
var buf: [8192:0]u8 = undefined; // TODO: what if isn't enough?
|
var buf: [8192:0]u8 = undefined; // TODO: what if isn't enough?
|
||||||
// first line is banner: "bssid / frequency / signal level / flags / ssid"
|
// first line is banner: "bssid / frequency / signal level / flags / ssid"
|
||||||
|
@ -352,7 +522,8 @@ const WifiNetworksListFilter = struct {
|
||||||
ssid: ?[]const u8, // ignore networks whose ssid doesn't match
|
ssid: ?[]const u8, // ignore networks whose ssid doesn't match
|
||||||
};
|
};
|
||||||
|
|
||||||
/// caller must release results with allocator.free.
|
/// caller must hold self.mu.
|
||||||
|
/// the returned value must be free'd with self.allocator.
|
||||||
fn queryWifiNetworksList(self: *Daemon, filter: WifiNetworksListFilter) ![]u32 {
|
fn queryWifiNetworksList(self: *Daemon, filter: WifiNetworksListFilter) ![]u32 {
|
||||||
var buf: [8192:0]u8 = undefined; // TODO: is this enough?
|
var buf: [8192:0]u8 = undefined; // TODO: is this enough?
|
||||||
// first line is banner: "network id / ssid / bssid / flags"
|
// first line is banner: "network id / ssid / bssid / flags"
|
||||||
|
@ -407,3 +578,83 @@ const StringList = struct {
|
||||||
return self.l.items;
|
return self.l.items;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test "start-stop" {
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
const pipe = try types.IoPipe.create();
|
||||||
|
defer pipe.close();
|
||||||
|
var daemon = try Daemon.init(t.allocator, pipe.writer(), "/dev/null");
|
||||||
|
|
||||||
|
try t.expect(daemon.state == .stopped);
|
||||||
|
try daemon.start();
|
||||||
|
try t.expect(daemon.state == .running);
|
||||||
|
try t.expect(daemon.wpa_ctrl.opened);
|
||||||
|
try t.expect(daemon.wpa_ctrl.attached);
|
||||||
|
|
||||||
|
daemon.stop();
|
||||||
|
try t.expect(daemon.state == .stopped);
|
||||||
|
try t.expect(!daemon.want_stop);
|
||||||
|
try t.expect(!daemon.wpa_ctrl.attached);
|
||||||
|
try t.expect(daemon.wpa_ctrl.opened);
|
||||||
|
try t.expect(daemon.main_thread == null);
|
||||||
|
try t.expect(daemon.poweroff_thread == null);
|
||||||
|
|
||||||
|
try t.expect(daemon.services.len > 0);
|
||||||
|
for (daemon.services) |*sv| {
|
||||||
|
try t.expect(!sv.stop_proc.spawned);
|
||||||
|
try t.expectEqual(SysService.Status.initial, sv.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
daemon.deinit();
|
||||||
|
try t.expect(!daemon.wpa_ctrl.opened);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "start-poweroff-stop" {
|
||||||
|
const t = std.testing;
|
||||||
|
const tt = @import("../test.zig");
|
||||||
|
|
||||||
|
var arena_alloc = std.heap.ArenaAllocator.init(t.allocator);
|
||||||
|
defer arena_alloc.deinit();
|
||||||
|
const arena = arena_alloc.allocator();
|
||||||
|
|
||||||
|
const pipe = try types.IoPipe.create();
|
||||||
|
var daemon = try Daemon.init(arena, pipe.writer(), "/dev/null");
|
||||||
|
defer {
|
||||||
|
daemon.deinit();
|
||||||
|
pipe.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
try daemon.start();
|
||||||
|
try daemon.beginPoweroff();
|
||||||
|
daemon.stop();
|
||||||
|
try t.expect(daemon.state == .poweroff);
|
||||||
|
for (daemon.services) |*sv| {
|
||||||
|
try t.expect(sv.stop_proc.spawned);
|
||||||
|
try t.expect(sv.stop_proc.waited);
|
||||||
|
try t.expectEqual(SysService.Status.stopped, sv.status());
|
||||||
|
}
|
||||||
|
|
||||||
|
const pipe_reader = pipe.reader();
|
||||||
|
const msg1 = try comm.read(arena, pipe_reader);
|
||||||
|
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
|
||||||
|
.{ .name = "lnd", .stopped = false, .err = null },
|
||||||
|
.{ .name = "bitcoind", .stopped = false, .err = null },
|
||||||
|
} } }, msg1);
|
||||||
|
|
||||||
|
const msg2 = try comm.read(arena, pipe_reader);
|
||||||
|
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
|
||||||
|
.{ .name = "lnd", .stopped = true, .err = null },
|
||||||
|
.{ .name = "bitcoind", .stopped = false, .err = null },
|
||||||
|
} } }, msg2);
|
||||||
|
|
||||||
|
const msg3 = try comm.read(arena, pipe_reader);
|
||||||
|
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
|
||||||
|
.{ .name = "lnd", .stopped = true, .err = null },
|
||||||
|
.{ .name = "bitcoind", .stopped = true, .err = null },
|
||||||
|
} } }, msg3);
|
||||||
|
|
||||||
|
// TODO: ensure "poweroff" was executed once custom runner is in a zig release;
|
||||||
|
// need custom runner to set up a global registry for child processes.
|
||||||
|
// https://github.com/ziglang/zig/pull/13411
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
///! an interface to programmatically manage a system service.
|
||||||
|
///! safe for concurrent use.
|
||||||
|
const std = @import("std");
|
||||||
|
const types = @import("../types.zig");
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
name: []const u8,
|
||||||
|
stop_wait_sec: ?u32 = null,
|
||||||
|
|
||||||
|
/// mutex guards all fields below.
|
||||||
|
mu: std.Thread.Mutex = .{},
|
||||||
|
stat: State,
|
||||||
|
stop_proc: types.ChildProcess = undefined,
|
||||||
|
stop_err: ?anyerror = null,
|
||||||
|
|
||||||
|
/// service current state.
|
||||||
|
/// the .initial value is a temporary solution until service watcher and start
|
||||||
|
/// are implemnted: at the moment, SysService can only stop services, nothing else.
|
||||||
|
pub const Status = enum(u8) {
|
||||||
|
initial, // TODO: add .running
|
||||||
|
stopping,
|
||||||
|
stopped,
|
||||||
|
};
|
||||||
|
|
||||||
|
const State = union(Status) {
|
||||||
|
initial: void,
|
||||||
|
stopping: void,
|
||||||
|
stopped: std.ChildProcess.Term,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SysService = @This();
|
||||||
|
|
||||||
|
pub const InitOpts = struct {
|
||||||
|
/// how long to wait for the service to stop before SIGKILL.
|
||||||
|
/// if unspecified, default for sv is 7.
|
||||||
|
stop_wait_sec: ?u32 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// must deinit when done.
|
||||||
|
pub fn init(a: std.mem.Allocator, name: []const u8, opts: InitOpts) SysService {
|
||||||
|
return .{
|
||||||
|
.allocator = a,
|
||||||
|
.name = name,
|
||||||
|
.stop_wait_sec = opts.stop_wait_sec,
|
||||||
|
.stat = .initial,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(_: *SysService) void {}
|
||||||
|
|
||||||
|
/// reports current state of the service.
|
||||||
|
/// note that it may incorrectly reflect the actual running service state
|
||||||
|
/// since SysService has no process watcher implementation yet.
|
||||||
|
pub fn status(self: *SysService) Status {
|
||||||
|
self.mu.lock();
|
||||||
|
defer self.mu.unlock();
|
||||||
|
return self.stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns an error during last stop call, if any.
|
||||||
|
pub fn lastStopError(self: *SysService) ?anyerror {
|
||||||
|
self.mu.lock();
|
||||||
|
defer self.mu.unlock();
|
||||||
|
return self.stop_err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// launches a service stop procedure and returns immediately.
|
||||||
|
/// callers must invoke stopWait to release all resources used by the stop.
|
||||||
|
pub fn stop(self: *SysService) !void {
|
||||||
|
self.mu.lock();
|
||||||
|
defer self.mu.unlock();
|
||||||
|
|
||||||
|
self.stop_err = null;
|
||||||
|
self.spawnStop() catch |err| {
|
||||||
|
self.stop_err = err;
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// blocks until the service stopping procedure terminates.
|
||||||
|
/// an error is returned also in the case where stopping a service failed.
|
||||||
|
pub fn stopWait(self: *SysService) !void {
|
||||||
|
self.mu.lock();
|
||||||
|
defer self.mu.unlock();
|
||||||
|
|
||||||
|
self.stop_err = null;
|
||||||
|
self.spawnStop() catch |err| {
|
||||||
|
self.stop_err = err;
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
|
const term = self.stop_proc.wait() catch |err| {
|
||||||
|
self.stop_err = err;
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
self.stat = .{ .stopped = term };
|
||||||
|
switch (term) {
|
||||||
|
.Exited => |code| if (code != 0) {
|
||||||
|
self.stop_err = error.SysServiceBadStopCode;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
self.stop_err = error.SysServiceBadStopTerm;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (self.stop_err) |err| {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// actual internal body of SysService.stop: stopWait also uses this.
|
||||||
|
/// callers must hold self.mu.
|
||||||
|
fn spawnStop(self: *SysService) !void {
|
||||||
|
switch (self.stat) {
|
||||||
|
.stopping => return, // already in progress
|
||||||
|
// intentionally let .stopped state pass through: can't see any downsides.
|
||||||
|
.initial, .stopped => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// use arena to simplify stop proc args construction.
|
||||||
|
var arena_alloc = std.heap.ArenaAllocator.init(self.allocator);
|
||||||
|
defer arena_alloc.deinit();
|
||||||
|
const arena = arena_alloc.allocator();
|
||||||
|
|
||||||
|
var argv = std.ArrayList([]const u8).init(arena);
|
||||||
|
try argv.append("sv");
|
||||||
|
if (self.stop_wait_sec) |sec| {
|
||||||
|
const s = try std.fmt.allocPrint(arena, "{d}", .{sec});
|
||||||
|
try argv.appendSlice(&.{ "-w", s });
|
||||||
|
}
|
||||||
|
try argv.appendSlice(&.{ "stop", self.name });
|
||||||
|
// can't use arena alloc since it's deinited upon return but proc needs alloc until wait'ed.
|
||||||
|
// child process dup's argv when spawned and auto-frees all resources when done (wait'ed).
|
||||||
|
self.stop_proc = types.ChildProcess.init(argv.items, self.allocator);
|
||||||
|
try self.stop_proc.spawn();
|
||||||
|
self.stat = .stopping;
|
||||||
|
}
|
||||||
|
|
||||||
|
test "stop then stopWait" {
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
var sv = SysService.init(t.allocator, "testsv1", .{ .stop_wait_sec = 13 });
|
||||||
|
try t.expectEqual(Status.initial, sv.status());
|
||||||
|
|
||||||
|
try sv.stop();
|
||||||
|
try t.expectEqual(Status.stopping, sv.status());
|
||||||
|
defer sv.stop_proc.deinit(); // TestChildProcess
|
||||||
|
|
||||||
|
try t.expect(sv.stop_proc.spawned);
|
||||||
|
try t.expect(!sv.stop_proc.waited);
|
||||||
|
try t.expect(!sv.stop_proc.killed);
|
||||||
|
const cmd = try std.mem.join(t.allocator, " ", sv.stop_proc.argv);
|
||||||
|
defer t.allocator.free(cmd);
|
||||||
|
try t.expectEqualStrings("sv -w 13 stop testsv1", cmd);
|
||||||
|
|
||||||
|
try sv.stopWait();
|
||||||
|
try t.expect(sv.stop_proc.waited);
|
||||||
|
try t.expect(!sv.stop_proc.killed);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "stopWait" {
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
var sv = SysService.init(t.allocator, "testsv2", .{ .stop_wait_sec = 14 });
|
||||||
|
try sv.stopWait();
|
||||||
|
defer sv.stop_proc.deinit(); // TestChildProcess
|
||||||
|
|
||||||
|
try t.expect(sv.stop_proc.spawned);
|
||||||
|
try t.expect(sv.stop_proc.waited);
|
||||||
|
try t.expect(!sv.stop_proc.killed);
|
||||||
|
const cmd = try std.mem.join(t.allocator, " ", sv.stop_proc.argv);
|
||||||
|
defer t.allocator.free(cmd);
|
||||||
|
try t.expectEqualStrings("sv -w 14 stop testsv2", cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "stop with default wait" {
|
||||||
|
const t = std.testing;
|
||||||
|
|
||||||
|
var sv = SysService.init(t.allocator, "testsv3", .{});
|
||||||
|
try sv.stopWait();
|
||||||
|
defer sv.stop_proc.deinit(); // TestChildProcess
|
||||||
|
|
||||||
|
const cmd = try std.mem.join(t.allocator, " ", sv.stop_proc.argv);
|
||||||
|
defer t.allocator.free(cmd);
|
||||||
|
try t.expectEqualStrings("sv stop testsv3", cmd);
|
||||||
|
}
|
89
src/ngui.zig
89
src/ngui.zig
|
@ -1,5 +1,6 @@
|
||||||
const buildopts = @import("build_options");
|
const buildopts = @import("build_options");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const os = std.os;
|
||||||
const time = std.time;
|
const time = std.time;
|
||||||
|
|
||||||
const comm = @import("comm.zig");
|
const comm = @import("comm.zig");
|
||||||
|
@ -22,25 +23,26 @@ const logger = std.log.scoped(.ngui);
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
/// global heap allocator used throughout the gui program.
|
/// global heap allocator used throughout the GUI program.
|
||||||
/// TODO: thread-safety?
|
/// TODO: thread-safety?
|
||||||
var gpa: std.mem.Allocator = undefined;
|
var gpa: std.mem.Allocator = undefined;
|
||||||
|
|
||||||
/// the mutex must be held before any call reaching into lv_xxx functions.
|
/// the mutex must be held before any call reaching into lv_xxx functions.
|
||||||
/// all nm_xxx functions assume it is the case since they are invoked from lvgl c code.
|
/// all nm_xxx functions assume it is the case since they are invoked from lvgl c code.
|
||||||
var ui_mutex: std.Thread.Mutex = .{};
|
var ui_mutex: std.Thread.Mutex = .{};
|
||||||
|
|
||||||
/// the program runs until quit is true.
|
/// the program runs until quit is true.
|
||||||
var quit: bool = false;
|
/// set from sighandler or on unrecoverable comm failure with the daemon.
|
||||||
|
var want_quit: bool = false;
|
||||||
|
|
||||||
var state: enum {
|
var state: enum {
|
||||||
active, // normal operational mode
|
active, // normal operational mode
|
||||||
standby, // idling
|
standby, // idling
|
||||||
alert, // draw user attention; never go standby
|
alert, // draw user attention; never go standby
|
||||||
} = .active;
|
} = .active;
|
||||||
|
|
||||||
/// setting wakeup brings the screen back from sleep()'ing without waiting
|
/// by setting wakeup brings the screen back from sleep()'ing without waiting for user action.
|
||||||
/// for user action.
|
/// can be used by comms when an alert is received from the daemon, to draw user attention.
|
||||||
/// can be used by comms when an alert is received from the daemon, to draw
|
|
||||||
/// user attention.
|
|
||||||
/// safe for concurrent use except wakeup.reset() is UB during another thread
|
/// safe for concurrent use except wakeup.reset() is UB during another thread
|
||||||
/// wakeup.wait()'ing or timedWait'ing.
|
/// wakeup.wait()'ing or timedWait'ing.
|
||||||
var wakeup = std.Thread.ResetEvent{};
|
var wakeup = std.Thread.ResetEvent{};
|
||||||
|
@ -68,12 +70,13 @@ export fn nm_check_idle_time(_: *lvgl.LvTimer) void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// initiate system shutdown leading to power off.
|
/// tells the daemon to initiate system shutdown leading to power off.
|
||||||
|
/// 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 {
|
||||||
logger.info("initiating system shutdown", .{});
|
|
||||||
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.write(gpa, stdout, msg) catch |err| logger.err("nm_sys_shutdown: {any}", .{err});
|
||||||
quit = true;
|
state = .alert; // prevent screen sleep
|
||||||
|
wakeup.set(); // wake up from standby, if any
|
||||||
}
|
}
|
||||||
|
|
||||||
export fn nm_tab_settings_active() void {
|
export fn nm_tab_settings_active() void {
|
||||||
|
@ -144,17 +147,16 @@ fn updateNetworkStatus(report: comm.Message.NetworkReport) !void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// reads messages from nd; loops indefinitely until program exit
|
/// reads messages from nd.
|
||||||
fn commThread() void {
|
/// loops indefinitely until program exit or comm returns EOS.
|
||||||
|
fn commThreadLoop() void {
|
||||||
while (true) {
|
while (true) {
|
||||||
commThreadLoopCycle() catch |err| logger.err("commThreadLoopCycle: {any}", .{err});
|
commThreadLoopCycle() catch |err| logger.err("commThreadLoopCycle: {any}", .{err});
|
||||||
ui_mutex.lock();
|
|
||||||
const do_quit = quit;
|
std.atomic.spinLoopHint();
|
||||||
ui_mutex.unlock();
|
time.sleep(1 * time.ns_per_ms);
|
||||||
if (do_quit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
logger.info("exiting commThreadLoop", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commThreadLoopCycle() !void {
|
fn commThreadLoopCycle() !void {
|
||||||
|
@ -162,8 +164,9 @@ fn commThreadLoopCycle() !void {
|
||||||
if (err == error.EndOfStream) {
|
if (err == error.EndOfStream) {
|
||||||
// pointless to continue running if comms is broken.
|
// pointless to continue running if comms is broken.
|
||||||
// a parent/supervisor is expected to restart ngui.
|
// a parent/supervisor is expected to restart ngui.
|
||||||
|
logger.err("comm.read: EOS", .{});
|
||||||
ui_mutex.lock();
|
ui_mutex.lock();
|
||||||
quit = true;
|
want_quit = true;
|
||||||
ui_mutex.unlock();
|
ui_mutex.unlock();
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
|
@ -175,6 +178,11 @@ fn commThreadLoopCycle() !void {
|
||||||
.network_report => |report| {
|
.network_report => |report| {
|
||||||
updateNetworkStatus(report) catch |err| logger.err("updateNetworkStatus: {any}", .{err});
|
updateNetworkStatus(report) catch |err| logger.err("updateNetworkStatus: {any}", .{err});
|
||||||
},
|
},
|
||||||
|
.poweroff_progress => |report| {
|
||||||
|
ui_mutex.lock();
|
||||||
|
defer ui_mutex.unlock();
|
||||||
|
ui.poweroff.updateStatus(report) catch |err| logger.err("poweroff.updateStatus: {any}", .{err});
|
||||||
|
},
|
||||||
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
|
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,6 +226,20 @@ fn usage(prog: []const u8) !void {
|
||||||
, .{prog});
|
, .{prog});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// handles sig TERM and INT: makes the program exit.
|
||||||
|
///
|
||||||
|
/// note: must avoid locking ui_mutex within the handler since it may lead to
|
||||||
|
/// a race and a deadlock where the sighandler is invoked while the mutex is held
|
||||||
|
/// by the UI loop because a sighandler invocation interrupts main execution flow,
|
||||||
|
/// and so the mutex would then remain locked indefinitely.
|
||||||
|
fn sighandler(sig: c_int) callconv(.C) void {
|
||||||
|
logger.info("received signal {}", .{sig});
|
||||||
|
switch (sig) {
|
||||||
|
os.SIG.INT, os.SIG.TERM => want_quit = true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// nakamochi UI program entry point.
|
/// nakamochi UI program entry point.
|
||||||
pub fn main() anyerror!void {
|
pub fn main() anyerror!void {
|
||||||
// main heap allocator used through the lifetime of nd
|
// main heap allocator used through the lifetime of nd
|
||||||
|
@ -240,8 +262,16 @@ pub fn main() anyerror!void {
|
||||||
};
|
};
|
||||||
|
|
||||||
// start comms with daemon in a seaparate thread.
|
// start comms with daemon in a seaparate thread.
|
||||||
const th = try std.Thread.spawn(.{}, commThread, .{});
|
_ = try std.Thread.spawn(.{}, commThreadLoop, .{});
|
||||||
th.detach();
|
|
||||||
|
// set up a sigterm handler for clean exit.
|
||||||
|
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);
|
||||||
|
|
||||||
// run idle timer indefinitely
|
// run idle timer indefinitely
|
||||||
_ = lvgl.createTimer(nm_check_idle_time, 2000, null) catch |err| {
|
_ = lvgl.createTimer(nm_check_idle_time, 2000, null) catch |err| {
|
||||||
|
@ -249,16 +279,12 @@ pub fn main() anyerror!void {
|
||||||
};
|
};
|
||||||
|
|
||||||
// main UI thread; must never block unless in idle/sleep mode
|
// main UI thread; must never block unless in idle/sleep mode
|
||||||
// TODO: handle sigterm
|
while (!want_quit) {
|
||||||
while (true) {
|
|
||||||
ui_mutex.lock();
|
ui_mutex.lock();
|
||||||
var till_next_ms = lvgl.loopCycle();
|
var till_next_ms = lvgl.loopCycle(); // UI loop
|
||||||
const do_quit = quit;
|
|
||||||
const do_state = state;
|
const do_state = state;
|
||||||
ui_mutex.unlock();
|
ui_mutex.unlock();
|
||||||
if (do_quit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (do_state == .standby) {
|
if (do_state == .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();
|
||||||
|
@ -266,6 +292,7 @@ pub fn main() anyerror!void {
|
||||||
logger.err("comm.write standby: {any}", .{err});
|
logger.err("comm.write standby: {any}", .{err});
|
||||||
};
|
};
|
||||||
screen.sleep(&wakeup);
|
screen.sleep(&wakeup);
|
||||||
|
|
||||||
// wake up due to touch screen activity or wakeup event is set
|
// wake up due to touch screen activity or wakeup event is set
|
||||||
logger.info("waking up from sleep", .{});
|
logger.info("waking up from sleep", .{});
|
||||||
ui_mutex.lock();
|
ui_mutex.lock();
|
||||||
|
@ -279,10 +306,14 @@ pub fn main() anyerror!void {
|
||||||
ui_mutex.unlock();
|
ui_mutex.unlock();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
std.atomic.spinLoopHint();
|
std.atomic.spinLoopHint();
|
||||||
// sleep at least 1ms
|
time.sleep(@max(1, till_next_ms) * time.ns_per_ms); // sleep at least 1ms
|
||||||
time.sleep(@max(1, till_next_ms) * time.ns_per_ms);
|
|
||||||
}
|
}
|
||||||
|
logger.info("main UI loop terminated", .{});
|
||||||
|
|
||||||
|
// not waiting for comm thread because it is terminated at program exit here
|
||||||
|
// anyway.
|
||||||
}
|
}
|
||||||
|
|
||||||
test "tick" {
|
test "tick" {
|
||||||
|
|
222
src/test.zig
222
src/test.zig
|
@ -1,4 +1,10 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
const nif = @import("nif");
|
||||||
|
|
||||||
|
comptime {
|
||||||
|
if (!builtin.is_test) @compileError("test-only module");
|
||||||
|
}
|
||||||
|
|
||||||
export fn wifi_ssid_add_network(name: [*:0]const u8) void {
|
export fn wifi_ssid_add_network(name: [*:0]const u8) void {
|
||||||
_ = name;
|
_ = name;
|
||||||
|
@ -13,8 +19,222 @@ export fn lv_disp_get_inactive_time(disp: *opaque {}) u32 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TestTimer always reports the same fixed value.
|
||||||
|
pub const TestTimer = struct {
|
||||||
|
value: u64,
|
||||||
|
started: bool = false, // true if called start
|
||||||
|
resetted: bool = false, // true if called reset
|
||||||
|
|
||||||
|
pub fn start() std.time.Timer.Error!TestTimer {
|
||||||
|
return .{ .value = 42 };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *TestTimer) void {
|
||||||
|
self.resetted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: *TestTimer) u64 {
|
||||||
|
return self.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// args in init are dup'ed using an allocator.
|
||||||
|
/// the caller must deinit in the end.
|
||||||
|
pub const TestChildProcess = struct {
|
||||||
|
// test hooks
|
||||||
|
spawn_callback: ?*const fn (*TestChildProcess) std.ChildProcess.SpawnError!void = null,
|
||||||
|
wait_callback: ?*const fn (*TestChildProcess) anyerror!std.ChildProcess.Term = null,
|
||||||
|
kill_callback: ?*const fn (*TestChildProcess) anyerror!std.ChildProcess.Term = null,
|
||||||
|
spawned: bool = false,
|
||||||
|
waited: bool = false,
|
||||||
|
killed: bool = false,
|
||||||
|
|
||||||
|
// original std ChildProcess init args
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
argv: []const []const u8,
|
||||||
|
|
||||||
|
pub fn init(argv: []const []const u8, allocator: std.mem.Allocator) TestChildProcess {
|
||||||
|
var adup = allocator.alloc([]u8, argv.len) catch unreachable;
|
||||||
|
for (argv) |v, i| {
|
||||||
|
adup[i] = allocator.dupe(u8, v) catch unreachable;
|
||||||
|
}
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.argv = adup,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *TestChildProcess) void {
|
||||||
|
for (self.argv) |v| self.allocator.free(v);
|
||||||
|
self.allocator.free(self.argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn(self: *TestChildProcess) std.ChildProcess.SpawnError!void {
|
||||||
|
defer self.spawned = true;
|
||||||
|
if (self.spawn_callback) |cb| {
|
||||||
|
return cb(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wait(self: *TestChildProcess) anyerror!std.ChildProcess.Term {
|
||||||
|
defer self.waited = true;
|
||||||
|
if (self.wait_callback) |cb| {
|
||||||
|
return cb(self);
|
||||||
|
}
|
||||||
|
return .{ .Exited = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawnAndWait(self: *TestChildProcess) !std.ChildProcess.Term {
|
||||||
|
try self.spawn();
|
||||||
|
return self.wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kill(self: *TestChildProcess) !std.ChildProcess.Term {
|
||||||
|
defer self.killed = true;
|
||||||
|
if (self.kill_callback) |cb| {
|
||||||
|
return cb(self);
|
||||||
|
}
|
||||||
|
return .{ .Exited = 0 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// a nif.wpa.Control stub for tests.
|
||||||
|
pub const TestWpaControl = struct {
|
||||||
|
ctrl_path: []const u8,
|
||||||
|
opened: bool,
|
||||||
|
attached: bool = false,
|
||||||
|
scanned: bool = false,
|
||||||
|
saved: bool = false,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn open(path: [:0]const u8) !Self {
|
||||||
|
return .{ .ctrl_path = path, .opened = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(self: *Self) !void {
|
||||||
|
self.opened = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attach(self: *Self) !void {
|
||||||
|
self.attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detach(self: *Self) !void {
|
||||||
|
self.attached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pending(_: Self) !bool {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receive(_: Self, _: [:0]u8) ![]const u8 {
|
||||||
|
return &.{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scan(self: *Self) !void {
|
||||||
|
self.scanned = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn saveConfig(self: *Self) !void {
|
||||||
|
self.saved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request(_: Self, _: [:0]const u8, _: [:0]u8, _: ?nif.wpa.ReqCallback) ![]const u8 {
|
||||||
|
return &.{};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// similar to std.testing.expectEqual but compares slices with expectEqualSlices
|
||||||
|
/// or expectEqualStrings where slice element is a u8.
|
||||||
|
pub fn expectDeepEqual(expected: anytype, actual: @TypeOf(expected)) !void {
|
||||||
|
const t = std.testing;
|
||||||
|
switch (@typeInfo(@TypeOf(actual))) {
|
||||||
|
.Pointer => |p| {
|
||||||
|
switch (p.size) {
|
||||||
|
.One => try expectDeepEqual(expected.*, actual.*),
|
||||||
|
.Slice => {
|
||||||
|
switch (@typeInfo(p.child)) {
|
||||||
|
.Pointer, .Struct, .Optional, .Union => {
|
||||||
|
var err: ?anyerror = blk: {
|
||||||
|
if (expected.len != actual.len) {
|
||||||
|
std.debug.print("expected.len = {d}, actual.len = {d}\n", .{ expected.len, actual.len });
|
||||||
|
break :blk error.ExpectDeepEqual;
|
||||||
|
}
|
||||||
|
break :blk null;
|
||||||
|
};
|
||||||
|
const n = std.math.min(expected.len, actual.len);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < n) : (i += 1) {
|
||||||
|
expectDeepEqual(expected[i], actual[i]) catch |e| {
|
||||||
|
std.debug.print("unequal slice elements at index {d}\n", .{i});
|
||||||
|
return e;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (err) |e| {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
if (p.child == u8) {
|
||||||
|
try t.expectEqualStrings(expected, actual);
|
||||||
|
} else {
|
||||||
|
try t.expectEqualSlices(p.child, expected, actual);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
else => try t.expectEqual(expected, actual),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Struct => |st| {
|
||||||
|
inline for (st.fields) |f| {
|
||||||
|
expectDeepEqual(@field(expected, f.name), @field(actual, f.name)) catch |err| {
|
||||||
|
std.debug.print("unequal field '{s}' of struct {any}\n", .{ f.name, @TypeOf(actual) });
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Optional => {
|
||||||
|
if (expected) |x| {
|
||||||
|
if (actual) |v| {
|
||||||
|
try expectDeepEqual(x, v);
|
||||||
|
} else {
|
||||||
|
std.debug.print("expected {any}, found null\n", .{x});
|
||||||
|
return error.TestExpectDeepEqual;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (actual) |v| {
|
||||||
|
std.debug.print("expected null, found {any}\n", .{v});
|
||||||
|
return error.TestExpectDeepEqual;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Union => |u| {
|
||||||
|
if (u.tag_type == null) {
|
||||||
|
@compileError("unable to compare untagged union values");
|
||||||
|
}
|
||||||
|
const Tag = std.meta.Tag(@TypeOf(expected));
|
||||||
|
const atag = @as(Tag, actual);
|
||||||
|
try t.expectEqual(@as(Tag, expected), atag);
|
||||||
|
inline for (u.fields) |f| {
|
||||||
|
if (std.mem.eql(u8, f.name, @tagName(atag))) {
|
||||||
|
try expectDeepEqual(@field(expected, f.name), @field(actual, f.name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unreachable;
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
try t.expectEqual(expected, actual);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = @import("comm.zig");
|
_ = @import("nd.zig");
|
||||||
|
_ = @import("nd/Daemon.zig");
|
||||||
|
_ = @import("nd/SysService.zig");
|
||||||
_ = @import("ngui.zig");
|
_ = @import("ngui.zig");
|
||||||
|
|
||||||
std.testing.refAllDecls(@This());
|
std.testing.refAllDecls(@This());
|
||||||
|
|
|
@ -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});
|
||||||
|
}
|
|
@ -1,17 +1,49 @@
|
||||||
const std = @import("std");
|
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
|
const std = @import("std");
|
||||||
|
const nif = @import("nif");
|
||||||
|
|
||||||
|
const tt = @import("test.zig");
|
||||||
|
|
||||||
|
pub usingnamespace if (builtin.is_test) struct {
|
||||||
|
// stubs, mocks, overrides for testing.
|
||||||
|
pub const Timer = tt.TestTimer;
|
||||||
|
pub const ChildProcess = tt.TestChildProcess;
|
||||||
|
pub const WpaControl = tt.TestWpaControl;
|
||||||
|
} else struct {
|
||||||
|
// regular types for production code.
|
||||||
|
pub const Timer = std.time.Timer;
|
||||||
|
pub const ChildProcess = std.ChildProcess;
|
||||||
|
pub const WpaControl = nif.wpa.Control;
|
||||||
|
};
|
||||||
|
|
||||||
/// prefer this type over the std.ArrayList(u8) just to ensure consistency
|
/// prefer this type over the std.ArrayList(u8) just to ensure consistency
|
||||||
/// and potential regressions. For example, comm module uses it for read/write.
|
/// and potential regressions. For example, comm module uses it for read/write.
|
||||||
pub const ByteArrayList = std.ArrayList(u8);
|
pub const ByteArrayList = std.ArrayList(u8);
|
||||||
|
|
||||||
pub const Timer = if (builtin.is_test) TestTimer else std.time.Timer;
|
/// an OS-based I/O pipe; see man(2) pipe.
|
||||||
|
pub const IoPipe = struct {
|
||||||
|
r: std.fs.File,
|
||||||
|
w: std.fs.File,
|
||||||
|
|
||||||
/// TestTimer always reports the same fixed value.
|
/// a pipe must be close'ed when done.
|
||||||
pub const TestTimer = if (!builtin.is_test) @compileError("TestTimer is for tests only") else struct {
|
pub fn create() std.os.PipeError!IoPipe {
|
||||||
value: u64,
|
const fds = try std.os.pipe();
|
||||||
|
return .{
|
||||||
|
.r = std.fs.File{ .handle = fds[0] },
|
||||||
|
.w = std.fs.File{ .handle = fds[1] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read(self: *Timer) u64 {
|
pub fn close(self: IoPipe) void {
|
||||||
return self.value;
|
self.w.close();
|
||||||
|
self.r.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reader(self: IoPipe) std.fs.File.Reader {
|
||||||
|
return self.r.reader();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writer(self: IoPipe) std.fs.File.Writer {
|
||||||
|
return self.w.writer();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -329,12 +329,17 @@ pub inline fn paletteDarken(p: Palette, l: PaletteModLevel) Color {
|
||||||
|
|
||||||
/// represents lv_obj_t type in C.
|
/// represents lv_obj_t type in C.
|
||||||
pub const LvObj = opaque {
|
pub const LvObj = opaque {
|
||||||
/// deallocates all the resources used by the object, including its children.
|
/// deallocates all resources used by the object, including its children.
|
||||||
/// user data pointers are untouched.
|
/// user data pointers are untouched.
|
||||||
pub fn destroy(self: *LvObj) void {
|
pub fn destroy(self: *LvObj) void {
|
||||||
lv_obj_del(self);
|
lv_obj_del(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// deallocates all resources used by the object's children.
|
||||||
|
pub fn deleteChildren(self: *LvObj) void {
|
||||||
|
lv_obj_clean(self);
|
||||||
|
}
|
||||||
|
|
||||||
/// creates a new event handler where cb is called upon event with the filter code.
|
/// creates a new event handler where cb is called upon event with the filter code.
|
||||||
/// to make cb called on any event, use EventCode.all filter.
|
/// to make cb called on any event, use EventCode.all filter.
|
||||||
/// multiple event handlers are called in the same order as they were added.
|
/// multiple event handlers are called in the same order as they were added.
|
||||||
|
@ -343,6 +348,11 @@ pub const LvObj = opaque {
|
||||||
return lv_obj_add_event_cb(self, cb, filter, udata);
|
return lv_obj_add_event_cb(self, cb, filter, udata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// sets label text to a new value.
|
||||||
|
pub fn setLabelText(self: *LvObj, text: [*:0]const u8) void {
|
||||||
|
lv_label_set_text(self, text);
|
||||||
|
}
|
||||||
|
|
||||||
/// sets or clears an object flag.
|
/// sets or clears an object flag.
|
||||||
pub fn setFlag(self: *LvObj, onoff: enum { on, off }, v: ObjFlag) void {
|
pub fn setFlag(self: *LvObj, onoff: enum { on, off }, v: ObjFlag) void {
|
||||||
switch (onoff) {
|
switch (onoff) {
|
||||||
|
@ -388,11 +398,18 @@ pub const LvObj = opaque {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// selects which side to pad in setPad func.
|
/// selects which side to pad in setPad func.
|
||||||
pub const PadSelector = enum { left, right, top, bottom, row, column };
|
pub const PadSelector = enum { all, left, right, top, bottom, row, column };
|
||||||
|
|
||||||
/// adds a padding style to the object.
|
/// adds a padding style to the object.
|
||||||
pub fn setPad(self: *LvObj, v: Coord, p: PadSelector, sel: StyleSelector) void {
|
pub fn setPad(self: *LvObj, v: Coord, p: PadSelector, sel: StyleSelector) void {
|
||||||
switch (p) {
|
switch (p) {
|
||||||
|
.all => {
|
||||||
|
const vsel = sel.value();
|
||||||
|
lv_obj_set_style_pad_left(self, v, vsel);
|
||||||
|
lv_obj_set_style_pad_right(self, v, vsel);
|
||||||
|
lv_obj_set_style_pad_top(self, v, vsel);
|
||||||
|
lv_obj_set_style_pad_bottom(self, v, vsel);
|
||||||
|
},
|
||||||
.left => lv_obj_set_style_pad_left(self, v, sel.value()),
|
.left => lv_obj_set_style_pad_left(self, v, sel.value()),
|
||||||
.right => lv_obj_set_style_pad_right(self, v, sel.value()),
|
.right => lv_obj_set_style_pad_right(self, v, sel.value()),
|
||||||
.top => lv_obj_set_style_pad_top(self, v, sel.value()),
|
.top => lv_obj_set_style_pad_top(self, v, sel.value()),
|
||||||
|
@ -453,6 +470,11 @@ pub const LvObj = opaque {
|
||||||
pub fn setBackgroundColor(self: *LvObj, v: Color, sel: StyleSelector) void {
|
pub fn setBackgroundColor(self: *LvObj, v: Color, sel: StyleSelector) void {
|
||||||
lv_obj_set_style_bg_color(self, v, sel.value());
|
lv_obj_set_style_bg_color(self, v, sel.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// sets the color of a text, typically a label object.
|
||||||
|
pub fn setTextColor(self: *LvObj, v: Color, sel: StyleSelector) void {
|
||||||
|
lv_obj_set_style_text_color(self, v, sel.value());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn createObject(parent: *LvObj) !*LvObj {
|
pub fn createObject(parent: *LvObj) !*LvObj {
|
||||||
|
@ -631,7 +653,7 @@ pub fn createWindow(parent: ?*LvObj, header_height: i16, title: [*:0]const u8) !
|
||||||
return .{ .winobj = winobj };
|
return .{ .winobj = winobj };
|
||||||
}
|
}
|
||||||
|
|
||||||
const Window = struct {
|
pub const Window = struct {
|
||||||
winobj: *LvObj,
|
winobj: *LvObj,
|
||||||
|
|
||||||
pub fn content(self: Window) *LvObj {
|
pub fn content(self: Window) *LvObj {
|
||||||
|
@ -640,38 +662,58 @@ const Window = struct {
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const CreateLabelOpt = struct {
|
pub const CreateLabelOpt = struct {
|
||||||
long_mode: enum(c.lv_label_long_mode_t) {
|
// LVGL defaults to .wrap
|
||||||
|
long_mode: ?enum(c.lv_label_long_mode_t) {
|
||||||
wrap = c.LV_LABEL_LONG_WRAP, // keep the object width, wrap the too long lines and expand the object height
|
wrap = c.LV_LABEL_LONG_WRAP, // keep the object width, wrap the too long lines and expand the object height
|
||||||
dot = c.LV_LABEL_LONG_DOT, // keep the size and write dots at the end if the text is too long
|
dot = c.LV_LABEL_LONG_DOT, // keep the size and write dots at the end if the text is too long
|
||||||
scroll = c.LV_LABEL_LONG_SCROLL, // keep the size and roll the text back and forth
|
scroll = c.LV_LABEL_LONG_SCROLL, // keep the size and roll the text back and forth
|
||||||
scroll_circular = c.LV_LABEL_LONG_SCROLL_CIRCULAR, // keep the size and roll the text circularly
|
scroll_circular = c.LV_LABEL_LONG_SCROLL_CIRCULAR, // keep the size and roll the text circularly
|
||||||
clip = c.LV_LABEL_LONG_CLIP, // keep the size and clip the text out of it
|
clip = c.LV_LABEL_LONG_CLIP, // keep the size and clip the text out of it
|
||||||
},
|
} = null,
|
||||||
pos: enum {
|
pos: ?PosAlign = null,
|
||||||
none,
|
|
||||||
centered,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// creates a new label object.
|
||||||
|
/// the text is heap-duplicated for the lifetime of the object and free'ed automatically.
|
||||||
pub fn createLabel(parent: *LvObj, text: [*:0]const u8, opt: CreateLabelOpt) !*LvObj {
|
pub fn createLabel(parent: *LvObj, text: [*:0]const u8, opt: CreateLabelOpt) !*LvObj {
|
||||||
var lb = lv_label_create(parent) orelse return error.OutOfMemory;
|
var lb = lv_label_create(parent) orelse return error.OutOfMemory;
|
||||||
//lv_label_set_text_static(lb, text); // static doesn't work with .dot
|
//lv_label_set_text_static(lb, text); // static doesn't work with .dot
|
||||||
lv_label_set_text(lb, text);
|
lv_label_set_text(lb, text);
|
||||||
lv_label_set_recolor(lb, true);
|
lv_label_set_recolor(lb, true);
|
||||||
//lv_obj_set_height(lb, sizeContent); // default
|
//lv_obj_set_height(lb, sizeContent); // default
|
||||||
lv_label_set_long_mode(lb, @enumToInt(opt.long_mode));
|
if (opt.long_mode) |m| {
|
||||||
if (opt.pos == .centered) {
|
lv_label_set_long_mode(lb, @enumToInt(m));
|
||||||
lb.center();
|
}
|
||||||
|
if (opt.pos) |p| {
|
||||||
|
lb.posAlign(p, 0, 0);
|
||||||
}
|
}
|
||||||
return lb;
|
return lb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// formats label text using std.fmt.format and the provided buffer.
|
||||||
|
/// a label object is then created with the resulting text using createLabel.
|
||||||
|
/// the text is heap-dup'ed so no need to retain buf. see createLabel.
|
||||||
|
pub fn createLabelFmt(parent: *LvObj, buf: []u8, comptime format: []const u8, args: anytype, opt: CreateLabelOpt) !*LvObj {
|
||||||
|
const text = try std.fmt.bufPrintZ(buf, format, args);
|
||||||
|
return createLabel(parent, text, opt);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn createButton(parent: *LvObj, label: [*:0]const u8) !*LvObj {
|
pub fn createButton(parent: *LvObj, label: [*:0]const u8) !*LvObj {
|
||||||
const btn = lv_btn_create(parent) orelse return error.OutOfMemory;
|
const btn = lv_btn_create(parent) orelse return error.OutOfMemory;
|
||||||
_ = try createLabel(btn, label, .{ .long_mode = .dot, .pos = .centered });
|
_ = try createLabel(btn, label, .{ .long_mode = .dot, .pos = .center });
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// creates a spinner object with hardcoded dimensions and animation speed
|
||||||
|
/// used througout the GUI.
|
||||||
|
pub fn createSpinner(parent: *LvObj) !*LvObj {
|
||||||
|
const spin = lv_spinner_create(parent, 1000, 60) orelse return error.OutOfMemory;
|
||||||
|
lv_obj_set_size(spin, 20, 20);
|
||||||
|
const ind: StyleSelector = .{ .part = .indicator };
|
||||||
|
lv_obj_set_style_arc_width(spin, 4, ind.value());
|
||||||
|
return spin;
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// imports from nakamochi custom C code that extends LVGL
|
// imports from nakamochi custom C code that extends LVGL
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
@ -748,12 +790,14 @@ extern fn lv_obj_add_style(obj: *LvObj, style: *LvStyle, sel: c.lv_style_selecto
|
||||||
extern fn lv_obj_remove_style(obj: *LvObj, style: ?*LvStyle, sel: c.lv_style_selector_t) void;
|
extern fn lv_obj_remove_style(obj: *LvObj, style: ?*LvStyle, sel: c.lv_style_selector_t) void;
|
||||||
extern fn lv_obj_remove_style_all(obj: *LvObj) void;
|
extern fn lv_obj_remove_style_all(obj: *LvObj) void;
|
||||||
extern fn lv_obj_set_style_bg_color(obj: *LvObj, val: Color, sel: c.lv_style_selector_t) void;
|
extern fn lv_obj_set_style_bg_color(obj: *LvObj, val: Color, sel: c.lv_style_selector_t) void;
|
||||||
|
extern fn lv_obj_set_style_text_color(obj: *LvObj, val: Color, sel: c.lv_style_selector_t) void;
|
||||||
extern fn lv_obj_set_style_pad_left(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
extern fn lv_obj_set_style_pad_left(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
||||||
extern fn lv_obj_set_style_pad_right(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
extern fn lv_obj_set_style_pad_right(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
||||||
extern fn lv_obj_set_style_pad_top(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
extern fn lv_obj_set_style_pad_top(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
||||||
extern fn lv_obj_set_style_pad_bottom(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
extern fn lv_obj_set_style_pad_bottom(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
||||||
extern fn lv_obj_set_style_pad_row(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
extern fn lv_obj_set_style_pad_row(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
||||||
extern fn lv_obj_set_style_pad_column(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
extern fn lv_obj_set_style_pad_column(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
||||||
|
extern fn lv_obj_set_style_arc_width(obj: *LvObj, val: c.lv_coord_t, sel: c.lv_style_selector_t) void;
|
||||||
|
|
||||||
// TODO: port these to zig
|
// TODO: port these to zig
|
||||||
extern fn lv_palette_main(c.lv_palette_t) Color;
|
extern fn lv_palette_main(c.lv_palette_t) Color;
|
||||||
|
@ -766,6 +810,8 @@ extern fn lv_palette_darken(c.lv_palette_t, level: u8) Color;
|
||||||
extern fn lv_obj_create(parent: ?*LvObj) ?*LvObj;
|
extern fn lv_obj_create(parent: ?*LvObj) ?*LvObj;
|
||||||
/// deletes and deallocates an object and all its children from UI tree.
|
/// deletes and deallocates an object and all its children from UI tree.
|
||||||
extern fn lv_obj_del(obj: *LvObj) void;
|
extern fn lv_obj_del(obj: *LvObj) void;
|
||||||
|
/// deletes children of the obj.
|
||||||
|
extern fn lv_obj_clean(obj: *LvObj) void;
|
||||||
|
|
||||||
extern fn lv_obj_add_flag(obj: *LvObj, v: c.lv_obj_flag_t) void;
|
extern fn lv_obj_add_flag(obj: *LvObj, v: c.lv_obj_flag_t) void;
|
||||||
extern fn lv_obj_clear_flag(obj: *LvObj, v: c.lv_obj_flag_t) void;
|
extern fn lv_obj_clear_flag(obj: *LvObj, v: c.lv_obj_flag_t) void;
|
||||||
|
@ -794,6 +840,8 @@ extern fn lv_label_set_text_static(label: *LvObj, text: [*:0]const u8) void;
|
||||||
extern fn lv_label_set_long_mode(label: *LvObj, mode: c.lv_label_long_mode_t) void;
|
extern fn lv_label_set_long_mode(label: *LvObj, mode: c.lv_label_long_mode_t) void;
|
||||||
extern fn lv_label_set_recolor(label: *LvObj, enable: bool) void;
|
extern fn lv_label_set_recolor(label: *LvObj, enable: bool) void;
|
||||||
|
|
||||||
|
extern fn lv_spinner_create(parent: *LvObj, speed_ms: u32, arc_deg: u32) ?*LvObj;
|
||||||
|
|
||||||
extern fn lv_win_create(parent: *LvObj, header_height: c.lv_coord_t) ?*LvObj;
|
extern fn lv_win_create(parent: *LvObj, header_height: c.lv_coord_t) ?*LvObj;
|
||||||
extern fn lv_win_add_title(win: *LvObj, title: [*:0]const u8) ?*LvObj;
|
extern fn lv_win_add_title(win: *LvObj, title: [*:0]const u8) ?*LvObj;
|
||||||
extern fn lv_win_get_content(win: *LvObj) *LvObj;
|
extern fn lv_win_get_content(win: *LvObj) *LvObj;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,4 +1,5 @@
|
||||||
///! display and touch screen helper functions.
|
///! display and touch screen helper functions.
|
||||||
|
const builtin = @import("builtin");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Thread = std.Thread;
|
const Thread = std.Thread;
|
||||||
|
|
||||||
|
@ -36,7 +37,7 @@ pub fn sleep(wake: *const Thread.ResetEvent) void {
|
||||||
|
|
||||||
/// turn on or off display backlight.
|
/// turn on or off display backlight.
|
||||||
pub fn backlight(onoff: enum { on, off }) !void {
|
pub fn backlight(onoff: enum { on, off }) !void {
|
||||||
const blpath = "/sys/class/backlight/rpi_backlight/bl_power";
|
const blpath = if (builtin.is_test) "/dev/null" else "/sys/class/backlight/rpi_backlight/bl_power";
|
||||||
const f = try std.fs.openFileAbsolute(blpath, .{ .mode = .write_only });
|
const f = try std.fs.openFileAbsolute(blpath, .{ .mode = .write_only });
|
||||||
defer f.close();
|
defer f.close();
|
||||||
const v = if (onoff == .on) "0" else "1";
|
const v = if (onoff == .on) "0" else "1";
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
const buildopts = @import("build_options");
|
const buildopts = @import("build_options");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
const comm = @import("../comm.zig");
|
||||||
const lvgl = @import("lvgl.zig");
|
const lvgl = @import("lvgl.zig");
|
||||||
const drv = @import("drv.zig");
|
const drv = @import("drv.zig");
|
||||||
const symbol = @import("symbol.zig");
|
const symbol = @import("symbol.zig");
|
||||||
const widget = @import("widget.zig");
|
const widget = @import("widget.zig");
|
||||||
|
pub const poweroff = @import("poweroff.zig");
|
||||||
|
|
||||||
const logger = std.log.scoped(.ui);
|
const logger = std.log.scoped(.ui);
|
||||||
|
|
||||||
extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int;
|
extern "c" fn nm_ui_init(disp: *lvgl.LvDisp) c_int;
|
||||||
extern fn nm_sys_shutdown() void;
|
|
||||||
|
|
||||||
pub fn init() !void {
|
pub fn init() !void {
|
||||||
lvgl.init();
|
lvgl.init();
|
||||||
|
@ -25,33 +26,6 @@ pub fn init() !void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// called when "power off" button is pressed.
|
|
||||||
export fn nm_poweroff_btn_callback(e: *lvgl.LvEvent) void {
|
|
||||||
_ = e;
|
|
||||||
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});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poweroffModalCallback(btn_idx: usize) void {
|
|
||||||
// proceed = 0, cancel = 1
|
|
||||||
if (btn_idx != 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// proceed with shutdown
|
|
||||||
nm_sys_shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
export fn nm_create_info_panel(parent: *lvgl.LvObj) c_int {
|
export fn nm_create_info_panel(parent: *lvgl.LvObj) c_int {
|
||||||
createInfoPanel(parent) catch |err| {
|
createInfoPanel(parent) catch |err| {
|
||||||
logger.err("createInfoPanel: {any}", .{err});
|
logger.err("createInfoPanel: {any}", .{err});
|
||||||
|
@ -66,5 +40,5 @@ fn createInfoPanel(parent: *lvgl.LvObj) !void {
|
||||||
|
|
||||||
var buf: [100]u8 = undefined;
|
var buf: [100]u8 = undefined;
|
||||||
const sver = try std.fmt.bufPrintZ(&buf, "GUI version: {any}", .{buildopts.semver});
|
const sver = try std.fmt.bufPrintZ(&buf, "GUI version: {any}", .{buildopts.semver});
|
||||||
_ = try lvgl.createLabel(parent, sver, .{ .long_mode = .wrap, .pos = .none });
|
_ = try lvgl.createLabel(parent, sver, .{});
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ pub fn modal(title: [*:0]const u8, text: [*:0]const u8, btns: []const [*:0]const
|
||||||
const wincont = win.content();
|
const wincont = win.content();
|
||||||
wincont.flexFlow(.column);
|
wincont.flexFlow(.column);
|
||||||
wincont.flexAlign(.start, .center, .center);
|
wincont.flexAlign(.start, .center, .center);
|
||||||
const msg = try lvgl.createLabel(wincont, text, .{ .long_mode = .wrap, .pos = .centered });
|
const msg = try lvgl.createLabel(wincont, text, .{ .pos = .center });
|
||||||
msg.setWidth(lvgl.displayHoriz() - 100);
|
msg.setWidth(lvgl.displayHoriz() - 100);
|
||||||
msg.flexGrow(1);
|
msg.flexGrow(1);
|
||||||
|
|
||||||
|
|
Reference in New Issue