nd,ngui: make program termination reliable on SIGTERM #23
17
src/comm.zig
17
src/comm.zig
|
@ -1,12 +1,14 @@
|
|||
///! daemon/gui communication.
|
||||
///! the protocol is a simple TLV construct: MessageTag(u16), length(u64), json-marshalled Message;
|
||||
///! little endian.
|
||||
//! daemon/gui communication.
|
||||
//! the protocol is a simple TLV construct: MessageTag(u16), length(u64), json-marshalled Message;
|
||||
//! little endian.
|
||||
const std = @import("std");
|
||||
const json = std.json;
|
||||
const mem = std.mem;
|
||||
|
||||
const ByteArrayList = @import("types.zig").ByteArrayList;
|
||||
|
||||
const logger = std.log.scoped(.comm);
|
||||
|
||||
/// common errors returned by read/write functions.
|
||||
pub const Error = error{
|
||||
CommReadInvalidTag,
|
||||
|
@ -71,12 +73,13 @@ pub const MessageTag = enum(u16) {
|
|||
};
|
||||
|
||||
/// reads and parses a single message from the input stream reader.
|
||||
/// callers must deallocate resources with free when done.
|
||||
/// propagates reader errors as is. for example, a closed reader returns
|
||||
/// error.EndOfStream.
|
||||
///
|
||||
/// callers must deallocate resources with Message.free when done.
|
||||
pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
|
||||
// alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic.
|
||||
const tag = reader.readEnum(MessageTag, .Little) catch {
|
||||
return Error.CommReadInvalidTag;
|
||||
};
|
||||
const tag = try reader.readEnum(MessageTag, .Little);
|
||||
const len = try reader.readIntLittle(u64);
|
||||
if (len == 0) {
|
||||
return switch (tag) {
|
||||
|
|
142
src/nd.zig
142
src/nd.zig
|
@ -28,21 +28,11 @@ fn usage(prog: []const u8) !void {
|
|||
, .{prog});
|
||||
}
|
||||
|
||||
/// prints messages in the same way std.fmt.format does and exits the process
|
||||
/// with a non-zero code.
|
||||
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);
|
||||
}
|
||||
|
||||
/// nd program args. see usage.
|
||||
/// nd program flags. see usage.
|
||||
const NdArgs = struct {
|
||||
gui: ?[:0]const u8 = null, // = "ngui",
|
||||
gui_user: ?[:0]const u8 = null, // u8 = "uiuser",
|
||||
wpa: ?[:0]const u8 = null, // = "/var/run/wpa_supplicant/wlan0",
|
||||
gui: ?[:0]const u8 = null,
|
||||
gui_user: ?[:0]const u8 = null,
|
||||
wpa: ?[:0]const u8 = null,
|
||||
|
||||
fn deinit(self: @This(), allocator: std.mem.Allocator) void {
|
||||
if (self.gui) |p| allocator.free(p);
|
||||
|
@ -97,29 +87,38 @@ fn parseArgs(gpa: std.mem.Allocator) !NdArgs {
|
|||
} else if (std.mem.eql(u8, a, "-wpa")) {
|
||||
lastarg = .wpa;
|
||||
} else {
|
||||
fatal("unknown arg name {s}", .{a});
|
||||
logger.err("unknown arg name {s}", .{a});
|
||||
return error.UnknownArgName;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastarg != .none) {
|
||||
fatal("invalid arg: {s} requires a value", .{@tagName(lastarg)});
|
||||
logger.err("invalid arg: {s} requires a value", .{@tagName(lastarg)});
|
||||
return error.MissinArgValue;
|
||||
}
|
||||
if (flags.gui == null) {
|
||||
logger.err("missing -gui arg", .{});
|
||||
return error.MissingGuiFlag;
|
||||
}
|
||||
if (flags.gui_user == null) {
|
||||
logger.err("missing -gui-user arg", .{});
|
||||
return error.MissinGuiUserFlag;
|
||||
}
|
||||
if (flags.wpa == null) {
|
||||
logger.err("missing -wpa arg", .{});
|
||||
return error.MissingWpaFlag;
|
||||
}
|
||||
if (flags.gui == null) fatal("missing -gui arg", .{});
|
||||
if (flags.gui_user == null) fatal("missing -gui-user arg", .{});
|
||||
if (flags.wpa == null) fatal("missing -wpa arg", .{});
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
/// sigquit signals nd main loop to exit.
|
||||
/// since both the loop and sighandler are on the same thread, it must
|
||||
/// not be guarded by a mutex which otherwise leads to a dealock.
|
||||
var sigquit = false;
|
||||
/// sigquit tells nd to exit.
|
||||
var sigquit: std.Thread.ResetEvent = .{};
|
||||
|
||||
fn sighandler(sig: c_int) callconv(.C) void {
|
||||
logger.info("received signal {}", .{sig});
|
||||
switch (sig) {
|
||||
os.SIG.INT, os.SIG.TERM => sigquit = true,
|
||||
os.SIG.INT, os.SIG.TERM => sigquit.set(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
@ -141,7 +140,8 @@ pub fn main() !void {
|
|||
screen.backlight(.on) catch |err| logger.err("backlight: {any}", .{err});
|
||||
|
||||
// start ngui, unless -nogui mode
|
||||
var ngui = std.ChildProcess.init(&.{args.gui.?}, gpa);
|
||||
const gui_path = args.gui.?; // guaranteed to be non-null
|
||||
var ngui = std.ChildProcess.init(&.{gui_path}, gpa);
|
||||
ngui.stdin_behavior = .Pipe;
|
||||
ngui.stdout_behavior = .Pipe;
|
||||
ngui.stderr_behavior = .Inherit;
|
||||
|
@ -158,17 +158,29 @@ pub fn main() !void {
|
|||
//ngui.uid = uiuser.uid;
|
||||
//ngui.gid = uiuser.gid;
|
||||
// ngui.env_map = ...
|
||||
ngui.spawn() catch |err| fatal("unable to start ngui: {any}", .{err});
|
||||
ngui.spawn() catch |err| {
|
||||
logger.err("unable to start ngui at path {s}", .{gui_path});
|
||||
return err;
|
||||
};
|
||||
// if the daemon fails to start and its process exits, ngui may hang forever
|
||||
// preventing system services monitoring to detect a failure and restart nd.
|
||||
// so, make sure to kill the ngui child process on fatal failures.
|
||||
errdefer _ = ngui.kill() catch {};
|
||||
|
||||
// TODO: thread-safety, esp. uiwriter
|
||||
// the i/o is closed as soon as ngui child process terminates.
|
||||
// note: read(2) indicates file destriptor i/o is atomic linux since 3.14.
|
||||
const uireader = ngui.stdout.?.reader();
|
||||
const uiwriter = ngui.stdin.?.writer();
|
||||
// send UI a ping as the first thing to make sure pipes are working.
|
||||
// https://git.qcode.ch/nakamochi/ndg/issues/16
|
||||
// send UI a ping right away to make sure pipes are working, crash otherwise.
|
||||
comm.write(gpa, uiwriter, .ping) catch |err| {
|
||||
logger.err("comm.write ping: {any}", .{err});
|
||||
return err;
|
||||
};
|
||||
|
||||
var nd = try Daemon.init(gpa, uireader, uiwriter, args.wpa.?);
|
||||
defer nd.deinit();
|
||||
try nd.start();
|
||||
|
||||
// graceful shutdown; see sigaction(2)
|
||||
const sa = os.Sigaction{
|
||||
.handler = .{ .handler = sighandler },
|
||||
|
@ -177,71 +189,13 @@ pub fn main() !void {
|
|||
};
|
||||
try os.sigaction(os.SIG.INT, &sa, null);
|
||||
try os.sigaction(os.SIG.TERM, &sa, null);
|
||||
sigquit.wait();
|
||||
|
||||
var nd = try Daemon.init(gpa, uiwriter, args.wpa.?);
|
||||
defer nd.deinit();
|
||||
try nd.start();
|
||||
// send the UI network report right away, without scanning wifi
|
||||
nd.reportNetworkStatus(.{ .scan = false });
|
||||
|
||||
var poweroff = false;
|
||||
// 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);
|
||||
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
|
||||
// TODO: handle error.EndOfStream - ngui exited
|
||||
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;
|
||||
nd.beginPoweroff() catch |err| {
|
||||
logger.err("beginPoweroff: {any}", .{err});
|
||||
poweroff = false;
|
||||
};
|
||||
},
|
||||
.get_network_report => |req| {
|
||||
nd.reportNetworkStatus(.{ .scan = req.scan });
|
||||
},
|
||||
.wifi_connect => |req| {
|
||||
nd.startConnectWifi(req.ssid, req.password) catch |err| {
|
||||
logger.err("startConnectWifi: {any}", .{err});
|
||||
};
|
||||
},
|
||||
.standby => {
|
||||
logger.info("entering standby mode", .{});
|
||||
nd.standby() catch |err| {
|
||||
logger.err("nd.standby: {any}", .{err});
|
||||
};
|
||||
},
|
||||
.wakeup => {
|
||||
logger.info("wakeup from standby", .{});
|
||||
nd.wakeup() catch |err| {
|
||||
logger.err("nd.wakeup: {any}", .{err});
|
||||
};
|
||||
},
|
||||
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
|
||||
}
|
||||
comm.free(gpa, msg);
|
||||
}
|
||||
|
||||
// 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});
|
||||
// reached here due to sig TERM or INT.
|
||||
// tell deamon to terminate threads.
|
||||
nd.stop();
|
||||
// once ngui exits, it'll close uireader/writer i/o from child proc
|
||||
// which lets the daemon's wait() to return.
|
||||
_ = ngui.kill() catch |err| logger.err("ngui.kill: {any}", .{err});
|
||||
nd.wait();
|
||||
}
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
//! daemon watches network status and communicates updates to the GUI 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,
|
||||
//! };
|
||||
//! var nd = Daemon.init(gpa, ngui_io_reader, ngui_io_writer, "/run/wpa_suppl/wlan0");
|
||||
//! defer nd.deinit();
|
||||
//! try nd.start();
|
||||
//! // wait for sigterm...
|
||||
//! nd.stop();
|
||||
//! // terminate ngui proc...
|
||||
//! nd.wait();
|
||||
//!
|
||||
|
||||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const time = std.time;
|
||||
|
||||
const nif = @import("nif");
|
||||
|
||||
const comm = @import("../comm.zig");
|
||||
const network = @import("network.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(.daemon);
|
||||
|
||||
allocator: mem.Allocator,
|
||||
uireader: std.fs.File.Reader, // ngui stdout
|
||||
uiwriter: std.fs.File.Writer, // ngui stdin
|
||||
wpa_ctrl: types.WpaControl, // guarded by mu once start'ed
|
||||
|
||||
|
@ -35,17 +35,20 @@ mu: std.Thread.Mutex = .{},
|
|||
state: enum {
|
||||
stopped,
|
||||
running,
|
||||
standby,
|
||||
poweroff,
|
||||
},
|
||||
|
||||
main_thread: ?std.Thread = null,
|
||||
comm_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,
|
||||
// network flags
|
||||
want_network_report: bool, // start gathering network status and send out as soon as ready
|
||||
want_wifi_scan: bool, // initiate wifi scan at the next loop cycle
|
||||
network_report_ready: bool, // indicates whether the network status is ready to be sent
|
||||
wifi_scan_in_progress: bool = false,
|
||||
network_report_ready: bool = true, // no need to scan for an immediate report
|
||||
wpa_save_config_on_connected: bool = false,
|
||||
|
||||
/// system services actively managed by the daemon.
|
||||
|
@ -55,8 +58,10 @@ services: []SysService = &.{},
|
|||
|
||||
const Daemon = @This();
|
||||
|
||||
/// initializes a daemon instance using the provided GUI stdout reader and stdin writer,
|
||||
/// and a filesystem path to WPA control socket.
|
||||
/// callers must deinit when done.
|
||||
pub fn init(a: std.mem.Allocator, iogui: std.fs.File.Writer, wpa_path: [:0]const u8) !Daemon {
|
||||
pub fn init(a: std.mem.Allocator, r: std.fs.File.Reader, w: std.fs.File.Writer, wpa: [:0]const u8) !Daemon {
|
||||
var svlist = std.ArrayList(SysService).init(a);
|
||||
errdefer {
|
||||
for (svlist.items) |*sv| sv.deinit();
|
||||
|
@ -68,24 +73,21 @@ pub fn init(a: std.mem.Allocator, iogui: std.fs.File.Writer, wpa_path: [:0]const
|
|||
try svlist.append(SysService.init(a, "bitcoind", .{ .stop_wait_sec = 600 }));
|
||||
return .{
|
||||
.allocator = a,
|
||||
.uiwriter = iogui,
|
||||
.wpa_ctrl = try types.WpaControl.open(wpa_path),
|
||||
.uireader = r,
|
||||
.uiwriter = w,
|
||||
.wpa_ctrl = try types.WpaControl.open(wpa),
|
||||
.state = .stopped,
|
||||
.services = svlist.toOwnedSlice(),
|
||||
// send a network report right at start without wifi scan to make it faster.
|
||||
.want_network_report = true,
|
||||
.want_wifi_scan = false,
|
||||
.network_report_ready = true,
|
||||
};
|
||||
}
|
||||
|
||||
/// releases all associated resources.
|
||||
/// if the daemon is not in a stopped or poweroff mode, deinit panics.
|
||||
/// the daemon must be stop'ed and wait'ed before deiniting.
|
||||
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();
|
||||
|
@ -93,95 +95,124 @@ pub fn deinit(self: *Daemon) void {
|
|||
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.
|
||||
/// start launches daemon threads and returns immediately.
|
||||
/// once started, the daemon must be eventually stop'ed and wait'ed to clean up
|
||||
/// resources even if a poweroff sequence is initiated with beginPoweroff.
|
||||
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
|
||||
.poweroff => return error.InPoweroffState,
|
||||
else => return error.AlreadyStarted,
|
||||
}
|
||||
|
||||
try self.wpa_ctrl.attach();
|
||||
errdefer {
|
||||
self.wpa_ctrl.detach() catch {};
|
||||
self.want_stop = true;
|
||||
}
|
||||
|
||||
self.main_thread = try std.Thread.spawn(.{}, mainThreadLoop, .{self});
|
||||
self.comm_thread = try std.Thread.spawn(.{}, commThreadLoop, .{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.
|
||||
/// tells the daemon to stop threads to prepare for termination.
|
||||
/// stop returns immediately.
|
||||
/// callers must `wait` to release all resources.
|
||||
pub fn stop(self: *Daemon) void {
|
||||
self.mu.lock();
|
||||
if (self.want_stop or self.state == .stopped) {
|
||||
self.mu.unlock();
|
||||
return; // already in progress or stopped
|
||||
}
|
||||
defer self.mu.unlock();
|
||||
self.want_stop = true;
|
||||
self.mu.unlock(); // avoid threads deadlock
|
||||
}
|
||||
|
||||
/// blocks and waits for all threads to terminate. the daemon instance cannot
|
||||
/// be start'ed afterwards.
|
||||
///
|
||||
/// note that in order for wait to return, the GUI I/O reader provided at init
|
||||
/// must be closed.
|
||||
pub fn wait(self: *Daemon) void {
|
||||
if (self.main_thread) |th| {
|
||||
th.join();
|
||||
self.main_thread = null;
|
||||
}
|
||||
if (self.comm_thread) |th| {
|
||||
th.join();
|
||||
self.comm_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.wpa_ctrl.detach() catch |err| logger.err("wait: wpa_ctrl.detach: {any}", .{err});
|
||||
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});
|
||||
self.state = .stopped;
|
||||
}
|
||||
|
||||
pub fn standby(self: *Daemon) !void {
|
||||
/// tells the daemon to go into a standby mode, typically due to user inactivity.
|
||||
fn standby(self: *Daemon) !void {
|
||||
self.mu.lock();
|
||||
defer self.mu.unlock();
|
||||
switch (self.state) {
|
||||
.poweroff => return error.InPoweroffState,
|
||||
.running, .stopped => {}, // continue
|
||||
.standby => {},
|
||||
.stopped, .poweroff => return error.InvalidState,
|
||||
.running => {
|
||||
try screen.backlight(.off);
|
||||
self.state = .standby;
|
||||
},
|
||||
}
|
||||
try screen.backlight(.off);
|
||||
}
|
||||
|
||||
pub fn wakeup(_: *Daemon) !void {
|
||||
try screen.backlight(.on);
|
||||
/// tells the daemon to return from standby, typically due to user interaction.
|
||||
fn wakeup(self: *Daemon) !void {
|
||||
self.mu.lock();
|
||||
defer self.mu.unlock();
|
||||
switch (self.state) {
|
||||
.running => {},
|
||||
.stopped, .poweroff => return error.InvalidState,
|
||||
.standby => {
|
||||
try screen.backlight(.on);
|
||||
self.state = .running;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// beingPoweroff also makes other threads exit but callers must still call `wait`
|
||||
/// to make sure poweroff sequence is complete.
|
||||
fn beginPoweroff(self: *Daemon) !void {
|
||||
self.mu.lock();
|
||||
defer self.mu.unlock();
|
||||
if (self.state == .poweroff) {
|
||||
return; // already in poweroff state
|
||||
switch (self.state) {
|
||||
.poweroff => {}, // already in poweroff mode
|
||||
.stopped => return error.InvalidState,
|
||||
.running, .standby => {
|
||||
self.poweroff_thread = try std.Thread.spawn(.{}, poweroffThread, .{self});
|
||||
self.state = .poweroff;
|
||||
self.want_stop = true;
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
/// set when poweroff_thread starts. available in tests only.
|
||||
var test_poweroff_started = if (builtin.is_test) std.Thread.ResetEvent{} else {};
|
||||
|
||||
/// the poweroff thread entry point: stops all monitored services and issues poweroff
|
||||
/// command while reporting the progress to ngui.
|
||||
/// exits after issuing "poweroff" command.
|
||||
fn poweroffThread(self: *Daemon) void {
|
||||
if (builtin.is_test) {
|
||||
test_poweroff_started.set();
|
||||
}
|
||||
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| {
|
||||
|
@ -202,48 +233,110 @@ fn poweroffThread(self: *Daemon) !void {
|
|||
logger.info("poweroff: {any}", .{res});
|
||||
}
|
||||
|
||||
/// main thread entry point.
|
||||
fn mainThreadLoop(self: *Daemon) !void {
|
||||
/// main thread entry point: watches for want_xxx flags and monitors network.
|
||||
/// exits when want_stop is true.
|
||||
fn mainThreadLoop(self: *Daemon) void {
|
||||
var quit = false;
|
||||
while (!quit) {
|
||||
self.mainThreadLoopCycle() catch |err| logger.err("main thread loop: {any}", .{err});
|
||||
std.atomic.spinLoopHint();
|
||||
time.sleep(1 * time.ns_per_s);
|
||||
|
||||
self.mu.lock();
|
||||
quit = self.want_stop;
|
||||
self.mu.unlock();
|
||||
}
|
||||
logger.info("exiting main thread loop", .{});
|
||||
}
|
||||
|
||||
/// run one cycle of the main thread loop iteration.
|
||||
/// unless in poweroff mode, the cycle holds self.mu for the whole duration.
|
||||
/// runs one cycle of the main thread loop iteration.
|
||||
/// the cycle holds self.mu for the whole duration.
|
||||
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();
|
||||
defer self.mu.unlock();
|
||||
self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
|
||||
if (self.want_wifi_scan) {
|
||||
if (self.startWifiScan()) {
|
||||
self.want_wifi_scan = false;
|
||||
} else |err| {
|
||||
logger.err("startWifiScan: {any}", .{err});
|
||||
}
|
||||
}
|
||||
if (self.want_network_report and self.network_report_ready) {
|
||||
if (self.sendNetworkReport()) {
|
||||
self.want_network_report = false;
|
||||
} else |err| {
|
||||
logger.err("sendNetworkReport: {any}", .{err});
|
||||
}
|
||||
}
|
||||
},
|
||||
self.mu.lock();
|
||||
defer self.mu.unlock();
|
||||
self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
|
||||
if (self.want_wifi_scan) {
|
||||
if (self.startWifiScan()) {
|
||||
self.want_wifi_scan = false;
|
||||
} else |err| {
|
||||
logger.err("startWifiScan: {any}", .{err});
|
||||
}
|
||||
}
|
||||
if (self.want_network_report and self.network_report_ready) {
|
||||
if (network.sendReport(self.allocator, &self.wpa_ctrl, self.uiwriter)) {
|
||||
self.want_network_report = false;
|
||||
} else |err| {
|
||||
logger.err("network.sendReport: {any}", .{err});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// comm thread entry point: reads messages sent from ngui and acts accordinly.
|
||||
/// exits when want_stop is true or comm reader is closed.
|
||||
/// note: the thread might not exit immediately on want_stop because comm.read
|
||||
/// is blocking.
|
||||
fn commThreadLoop(self: *Daemon) void {
|
||||
var quit = false;
|
||||
loop: while (!quit) {
|
||||
std.atomic.spinLoopHint();
|
||||
time.sleep(100 * time.ns_per_ms);
|
||||
|
||||
const msg = comm.read(self.allocator, self.uireader) catch |err| {
|
||||
self.mu.lock();
|
||||
defer self.mu.unlock();
|
||||
if (self.want_stop) {
|
||||
break :loop; // pipe is most likely already closed
|
||||
}
|
||||
switch (self.state) {
|
||||
.stopped, .poweroff => break :loop,
|
||||
.running, .standby => {
|
||||
logger.err("commThreadLoop: {any}", .{err});
|
||||
if (err == error.EndOfStream) {
|
||||
// pointless to continue running if comms I/O is broken.
|
||||
self.want_stop = true;
|
||||
break :loop;
|
||||
}
|
||||
continue;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
logger.debug("got msg: {s}", .{@tagName(msg)});
|
||||
switch (msg) {
|
||||
.pong => {
|
||||
logger.info("received pong from ngui", .{});
|
||||
},
|
||||
.poweroff => {
|
||||
self.beginPoweroff() catch |err| logger.err("beginPoweroff: {any}", .{err});
|
||||
},
|
||||
.get_network_report => |req| {
|
||||
self.reportNetworkStatus(.{ .scan = req.scan });
|
||||
},
|
||||
.wifi_connect => |req| {
|
||||
self.startConnectWifi(req.ssid, req.password) catch |err| {
|
||||
logger.err("startConnectWifi: {any}", .{err});
|
||||
};
|
||||
},
|
||||
.standby => {
|
||||
logger.info("entering standby mode", .{});
|
||||
self.standby() catch |err| logger.err("nd.standby: {any}", .{err});
|
||||
},
|
||||
.wakeup => {
|
||||
logger.info("wakeup from standby", .{});
|
||||
self.wakeup() catch |err| logger.err("nd.wakeup: {any}", .{err});
|
||||
},
|
||||
else => logger.warn("unhandled msg tag {s}", .{@tagName(msg)}),
|
||||
}
|
||||
comm.free(self.allocator, msg);
|
||||
|
||||
self.mu.lock();
|
||||
quit = self.want_stop;
|
||||
self.mu.unlock();
|
||||
}
|
||||
logger.info("exiting comm thread loop", .{});
|
||||
}
|
||||
|
||||
/// sends poweroff progress to uiwriter in comm.Message.PoweroffProgress format.
|
||||
fn sendPoweroffReport(self: *Daemon) !void {
|
||||
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.len);
|
||||
defer self.allocator.free(svstat);
|
||||
|
@ -289,18 +382,20 @@ fn wifiConnected(self: *Daemon) void {
|
|||
}
|
||||
|
||||
/// invoked when CTRL-EVENT-SSID-TEMP-DISABLED event with authentication failures is seen.
|
||||
/// caller must hold self.mu.
|
||||
/// callers must hold self.mu.
|
||||
fn wifiInvalidKey(self: *Daemon) void {
|
||||
self.wpa_save_config_on_connected = false;
|
||||
self.want_network_report = true;
|
||||
self.network_report_ready = true;
|
||||
}
|
||||
|
||||
pub const ReportNetworkStatusOpt = struct {
|
||||
const ReportNetworkStatusOpt = struct {
|
||||
scan: bool,
|
||||
};
|
||||
|
||||
pub fn reportNetworkStatus(self: *Daemon, opt: ReportNetworkStatusOpt) void {
|
||||
/// tells the daemon to start preparing network status report, including a wifi
|
||||
/// scan as an option.
|
||||
fn reportNetworkStatus(self: *Daemon, opt: ReportNetworkStatusOpt) void {
|
||||
self.mu.lock();
|
||||
defer self.mu.unlock();
|
||||
self.want_network_report = true;
|
||||
|
@ -310,7 +405,8 @@ pub fn reportNetworkStatus(self: *Daemon, opt: ReportNetworkStatusOpt) void {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn startConnectWifi(self: *Daemon, ssid: []const u8, password: []const u8) !void {
|
||||
/// initiates wifi connection procedure in a separate thread
|
||||
fn startConnectWifi(self: *Daemon, ssid: []const u8, password: []const u8) !void {
|
||||
if (ssid.len == 0) {
|
||||
return error.ConnectWifiEmptySSID;
|
||||
}
|
||||
|
@ -320,21 +416,24 @@ pub fn startConnectWifi(self: *Daemon, ssid: []const u8, password: []const u8) !
|
|||
th.detach();
|
||||
}
|
||||
|
||||
/// the wifi connection procedure thread entry point.
|
||||
/// holds self.mu for the whole duration. however the thread lifetime is expected
|
||||
/// to be short since all it does is issuing commands to self.wpa_ctrl.
|
||||
///
|
||||
/// the thread owns ssid and password args, and frees them at exit.
|
||||
fn connectWifiThread(self: *Daemon, ssid: []const u8, password: []const u8) void {
|
||||
self.mu.lock();
|
||||
defer {
|
||||
self.mu.unlock();
|
||||
self.allocator.free(ssid);
|
||||
self.allocator.free(password);
|
||||
}
|
||||
|
||||
// https://hostap.epitest.fi/wpa_supplicant/devel/ctrl_iface_page.html
|
||||
// https://wiki.archlinux.org/title/WPA_supplicant
|
||||
|
||||
// this prevents main thread from looping until released,
|
||||
// but the following commands and expected to be pretty quick.
|
||||
self.mu.lock();
|
||||
defer self.mu.unlock();
|
||||
|
||||
const id = self.addWifiNetwork(ssid, password) catch |err| {
|
||||
logger.err("addWifiNetwork: {any}; exiting", .{err});
|
||||
const id = network.addWifi(self.allocator, &self.wpa_ctrl, ssid, password) catch |err| {
|
||||
logger.err("addWifi: {any}; exiting", .{err});
|
||||
return;
|
||||
};
|
||||
// SELECT_NETWORK <id> - this disables others
|
||||
|
@ -353,51 +452,8 @@ fn connectWifiThread(self: *Daemon, ssid: []const u8, password: []const u8) void
|
|||
self.wpa_save_config_on_connected = true;
|
||||
}
|
||||
|
||||
/// adds a new network and configures its parameters.
|
||||
/// caller must hold self.mu.
|
||||
fn addWifiNetwork(self: *Daemon, ssid: []const u8, password: []const u8) !u32 {
|
||||
// - ADD_NETWORK -> get id and set parameters
|
||||
// - SET_NETWORK <id> ssid "ssid"
|
||||
// - if password:
|
||||
// SET_NETWORK <id> psk "password"
|
||||
// else:
|
||||
// SET_NETWORK <id> key_mgmt NONE
|
||||
const newWifiId = try self.wpa_ctrl.addNetwork();
|
||||
errdefer self.wpa_ctrl.removeNetwork(newWifiId) catch |err| {
|
||||
logger.err("addWifiNetwork cleanup: {any}", .{err});
|
||||
};
|
||||
var buf: [128:0]u8 = undefined;
|
||||
// TODO: convert ssid to hex string, to support special characters
|
||||
const ssidZ = try std.fmt.bufPrintZ(&buf, "\"{s}\"", .{ssid});
|
||||
try self.wpa_ctrl.setNetworkParam(newWifiId, "ssid", ssidZ);
|
||||
if (password.len > 0) {
|
||||
// TODO: switch to wpa_passphrase
|
||||
const v = try std.fmt.bufPrintZ(&buf, "\"{s}\"", .{password});
|
||||
try self.wpa_ctrl.setNetworkParam(newWifiId, "psk", v);
|
||||
} else {
|
||||
try self.wpa_ctrl.setNetworkParam(newWifiId, "key_mgmt", "NONE");
|
||||
}
|
||||
|
||||
// - LIST_NETWORKS: network id / ssid / bssid / flags
|
||||
// - for each matching ssid unless it's newly created: REMOVE_NETWORK <id>
|
||||
if (self.queryWifiNetworksList(.{ .ssid = ssid })) |res| {
|
||||
defer self.allocator.free(res);
|
||||
for (res) |id| {
|
||||
if (id == newWifiId) {
|
||||
continue;
|
||||
}
|
||||
self.wpa_ctrl.removeNetwork(id) catch |err| {
|
||||
logger.err("wpa_ctrl.removeNetwork({}): {any}", .{ id, err });
|
||||
};
|
||||
}
|
||||
} else |err| {
|
||||
logger.err("queryWifiNetworksList({s}): {any}; won't remove existing, if any", .{ ssid, err });
|
||||
}
|
||||
|
||||
return newWifiId;
|
||||
}
|
||||
|
||||
/// caller must hold self.mu.
|
||||
/// reads all available messages from self.wpa_ctrl and acts accordingly.
|
||||
/// callers must hold self.mu.
|
||||
fn readWPACtrlMsg(self: *Daemon) !void {
|
||||
var buf: [512:0]u8 = undefined;
|
||||
while (try self.wpa_ctrl.pending()) {
|
||||
|
@ -428,177 +484,31 @@ fn readWPACtrlMsg(self: *Daemon) !void {
|
|||
}
|
||||
}
|
||||
|
||||
/// report network status to ngui.
|
||||
/// caller must hold self.mu.
|
||||
fn sendNetworkReport(self: *Daemon) !void {
|
||||
var report = comm.Message.NetworkReport{
|
||||
.ipaddrs = undefined,
|
||||
.wifi_ssid = null,
|
||||
.wifi_scan_networks = undefined,
|
||||
};
|
||||
|
||||
// fetch all public IP addresses using getifaddrs
|
||||
const pubaddr = try nif.pubAddresses(self.allocator, null);
|
||||
defer self.allocator.free(pubaddr);
|
||||
//var addrs = std.ArrayList([]).init(t.allocator);
|
||||
var ipaddrs = try self.allocator.alloc([]const u8, pubaddr.len);
|
||||
for (pubaddr) |a, i| {
|
||||
ipaddrs[i] = try std.fmt.allocPrint(self.allocator, "{s}", .{a});
|
||||
}
|
||||
defer {
|
||||
for (ipaddrs) |a| self.allocator.free(a);
|
||||
self.allocator.free(ipaddrs);
|
||||
}
|
||||
report.ipaddrs = ipaddrs;
|
||||
|
||||
// get currently connected SSID, if any, from WPA ctrl
|
||||
const ssid = self.queryWifiSSID() catch |err| blk: {
|
||||
logger.err("queryWifiSsid: {any}", .{err});
|
||||
break :blk null;
|
||||
};
|
||||
defer if (ssid) |v| self.allocator.free(v);
|
||||
report.wifi_ssid = ssid;
|
||||
|
||||
// fetch available wifi networks from scan results using WPA ctrl
|
||||
var wifi_networks: ?StringList = if (self.queryWifiScanResults()) |v| v else |err| blk: {
|
||||
logger.err("queryWifiScanResults: {any}", .{err});
|
||||
break :blk null;
|
||||
};
|
||||
defer if (wifi_networks) |*list| list.deinit();
|
||||
if (wifi_networks) |list| {
|
||||
report.wifi_scan_networks = list.items();
|
||||
}
|
||||
|
||||
// report everything back to ngui
|
||||
return comm.write(self.allocator, self.uiwriter, comm.Message{ .network_report = report });
|
||||
}
|
||||
|
||||
/// caller must hold self.mu.
|
||||
fn queryWifiSSID(self: *Daemon) !?[]const u8 {
|
||||
var buf: [512:0]u8 = undefined;
|
||||
const resp = try self.wpa_ctrl.request("STATUS", &buf, null);
|
||||
const ssid = "ssid=";
|
||||
var it = mem.tokenize(u8, resp, "\n");
|
||||
while (it.next()) |line| {
|
||||
if (mem.startsWith(u8, line, ssid)) {
|
||||
// TODO: check line.len vs ssid.len
|
||||
const v = try self.allocator.dupe(u8, line[ssid.len..]);
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// caller must hold self.mu.
|
||||
/// the retuned value must free'd with StringList.deinit.
|
||||
fn queryWifiScanResults(self: *Daemon) !StringList {
|
||||
var buf: [8192:0]u8 = undefined; // TODO: what if isn't enough?
|
||||
// first line is banner: "bssid / frequency / signal level / flags / ssid"
|
||||
const resp = try self.wpa_ctrl.request("SCAN_RESULTS", &buf, null);
|
||||
var it = mem.tokenize(u8, resp, "\n");
|
||||
if (it.next() == null) {
|
||||
return error.MissingWifiScanHeader;
|
||||
}
|
||||
|
||||
var seen = std.BufSet.init(self.allocator);
|
||||
defer seen.deinit();
|
||||
var list = StringList.init(self.allocator);
|
||||
errdefer list.deinit();
|
||||
while (it.next()) |line| {
|
||||
// TODO: wpactrl's text protocol won't work for names with control characters
|
||||
if (mem.lastIndexOfScalar(u8, line, '\t')) |i| {
|
||||
const s = mem.trim(u8, line[i..], "\t\n");
|
||||
if (s.len == 0 or seen.contains(s)) {
|
||||
continue;
|
||||
}
|
||||
try seen.insert(s);
|
||||
try list.append(s);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
const WifiNetworksListFilter = struct {
|
||||
ssid: ?[]const u8, // ignore networks whose ssid doesn't match
|
||||
};
|
||||
|
||||
/// caller must hold self.mu.
|
||||
/// the returned value must be free'd with self.allocator.
|
||||
fn queryWifiNetworksList(self: *Daemon, filter: WifiNetworksListFilter) ![]u32 {
|
||||
var buf: [8192:0]u8 = undefined; // TODO: is this enough?
|
||||
// first line is banner: "network id / ssid / bssid / flags"
|
||||
const resp = try self.wpa_ctrl.request("LIST_NETWORKS", &buf, null);
|
||||
var it = mem.tokenize(u8, resp, "\n");
|
||||
if (it.next() == null) {
|
||||
return error.MissingWifiNetworksListHeader;
|
||||
}
|
||||
|
||||
var list = std.ArrayList(u32).init(self.allocator);
|
||||
while (it.next()) |line| {
|
||||
var cols = mem.tokenize(u8, line, "\t");
|
||||
const id_str = cols.next() orelse continue; // bad line format?
|
||||
const ssid = cols.next() orelse continue; // bad line format?
|
||||
const id = std.fmt.parseUnsigned(u32, id_str, 10) catch continue; // skip bad line
|
||||
if (filter.ssid != null and !mem.eql(u8, filter.ssid.?, ssid)) {
|
||||
continue;
|
||||
}
|
||||
list.append(id) catch {}; // grab anything we can
|
||||
}
|
||||
return list.toOwnedSlice();
|
||||
}
|
||||
|
||||
// TODO: turns this into a UniqStringList backed by StringArrayHashMap; also see std.BufSet
|
||||
const StringList = struct {
|
||||
l: std.ArrayList([]const u8),
|
||||
allocator: mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: mem.Allocator) Self {
|
||||
return Self{
|
||||
.l = std.ArrayList([]const u8).init(allocator),
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.l.items) |a| {
|
||||
self.allocator.free(a);
|
||||
}
|
||||
self.l.deinit();
|
||||
}
|
||||
|
||||
pub fn append(self: *Self, s: []const u8) !void {
|
||||
const item = try self.allocator.dupe(u8, s);
|
||||
errdefer self.allocator.free(item);
|
||||
try self.l.append(item);
|
||||
}
|
||||
|
||||
pub fn items(self: Self) []const []const u8 {
|
||||
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");
|
||||
var daemon = try Daemon.init(t.allocator, pipe.reader(), pipe.writer(), "/dev/null");
|
||||
daemon.want_network_report = false;
|
||||
|
||||
try t.expect(daemon.state == .stopped);
|
||||
try daemon.start();
|
||||
try t.expect(daemon.state == .running);
|
||||
try t.expect(daemon.main_thread != null);
|
||||
try t.expect(daemon.comm_thread != null);
|
||||
try t.expect(daemon.poweroff_thread == null);
|
||||
try t.expect(daemon.wpa_ctrl.opened);
|
||||
try t.expect(daemon.wpa_ctrl.attached);
|
||||
|
||||
daemon.stop();
|
||||
pipe.close();
|
||||
daemon.wait();
|
||||
try t.expect(daemon.state == .stopped);
|
||||
try t.expect(!daemon.want_stop);
|
||||
try t.expect(daemon.main_thread == null);
|
||||
try t.expect(daemon.comm_thread == null);
|
||||
try t.expect(daemon.poweroff_thread == null);
|
||||
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| {
|
||||
|
@ -610,7 +520,7 @@ test "start-stop" {
|
|||
try t.expect(!daemon.wpa_ctrl.opened);
|
||||
}
|
||||
|
||||
test "start-poweroff-stop" {
|
||||
test "start-poweroff" {
|
||||
const t = std.testing;
|
||||
const tt = @import("../test.zig");
|
||||
|
||||
|
@ -618,37 +528,44 @@ test "start-poweroff-stop" {
|
|||
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");
|
||||
const gui_stdin = try types.IoPipe.create();
|
||||
const gui_stdout = try types.IoPipe.create();
|
||||
const gui_reader = gui_stdin.reader();
|
||||
var daemon = try Daemon.init(arena, gui_stdout.reader(), gui_stdin.writer(), "/dev/null");
|
||||
daemon.want_network_report = false;
|
||||
defer {
|
||||
daemon.deinit();
|
||||
pipe.close();
|
||||
gui_stdin.close();
|
||||
}
|
||||
|
||||
try daemon.start();
|
||||
try daemon.beginPoweroff();
|
||||
daemon.stop();
|
||||
try comm.write(arena, gui_stdout.writer(), comm.Message.poweroff);
|
||||
try test_poweroff_started.timedWait(2 * time.ns_per_s);
|
||||
try t.expect(daemon.state == .poweroff);
|
||||
|
||||
gui_stdout.close();
|
||||
daemon.wait();
|
||||
try t.expect(daemon.state == .stopped);
|
||||
try t.expect(daemon.poweroff_thread == null);
|
||||
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);
|
||||
const msg1 = try comm.read(arena, gui_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);
|
||||
const msg2 = try comm.read(arena, gui_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);
|
||||
const msg3 = try comm.read(arena, gui_reader);
|
||||
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
|
||||
.{ .name = "lnd", .stopped = true, .err = null },
|
||||
.{ .name = "bitcoind", .stopped = true, .err = null },
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
//! network utility functions.
|
||||
//! unsafe for concurrent use: callers must implement a fence mechanism
|
||||
//! to allow only a single function execution concurrently when called with
|
||||
//! the same WPA control socket, and possibly i/o writer or allocator unless
|
||||
//! those already provide synchronization.
|
||||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
|
||||
const nif = @import("nif");
|
||||
const comm = @import("../comm.zig");
|
||||
const types = @import("../types.zig");
|
||||
|
||||
const logger = std.log.scoped(.network);
|
||||
|
||||
/// creates a new network using wpa_ctrl and configures its parameters.
|
||||
/// returns an ID of the new wifi.
|
||||
///
|
||||
/// if password is blank, the key management config is set to NONE.
|
||||
/// note: only cleartext passwords are supported at the moment.
|
||||
pub fn addWifi(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, ssid: []const u8, password: []const u8) !u32 {
|
||||
// - ADD_NETWORK -> get id and set parameters
|
||||
// - SET_NETWORK <id> ssid "ssid"
|
||||
// - if password:
|
||||
// SET_NETWORK <id> psk "password"
|
||||
// else:
|
||||
// SET_NETWORK <id> key_mgmt NONE
|
||||
const new_wifi_id = try wpa_ctrl.addNetwork();
|
||||
errdefer wpa_ctrl.removeNetwork(new_wifi_id) catch |err| {
|
||||
logger.err("addWifiNetwork err cleanup: {any}", .{err});
|
||||
};
|
||||
var buf: [128:0]u8 = undefined;
|
||||
// TODO: convert ssid to hex string, to support special characters
|
||||
const ssidZ = try std.fmt.bufPrintZ(&buf, "\"{s}\"", .{ssid});
|
||||
try wpa_ctrl.setNetworkParam(new_wifi_id, "ssid", ssidZ);
|
||||
if (password.len > 0) {
|
||||
// TODO: switch to wpa_passphrase
|
||||
const v = try std.fmt.bufPrintZ(&buf, "\"{s}\"", .{password});
|
||||
try wpa_ctrl.setNetworkParam(new_wifi_id, "psk", v);
|
||||
} else {
|
||||
try wpa_ctrl.setNetworkParam(new_wifi_id, "key_mgmt", "NONE");
|
||||
}
|
||||
|
||||
// - LIST_NETWORKS: network id / ssid / bssid / flags
|
||||
// - for each matching ssid unless it's newly created: REMOVE_NETWORK <id>
|
||||
if (queryWifiNetworksList(gpa, wpa_ctrl, .{ .ssid = ssid })) |res| {
|
||||
defer gpa.free(res);
|
||||
for (res) |id| {
|
||||
if (id == new_wifi_id) {
|
||||
continue;
|
||||
}
|
||||
wpa_ctrl.removeNetwork(id) catch |err| {
|
||||
logger.err("wpa_ctrl.removeNetwork({}): {any}", .{ id, err });
|
||||
};
|
||||
}
|
||||
} else |err| {
|
||||
logger.err("queryWifiNetworksList({s}): {any}; won't remove existing, if any", .{ ssid, err });
|
||||
}
|
||||
|
||||
return new_wifi_id;
|
||||
}
|
||||
|
||||
/// reports network status to the writer w in `comm.Message.NetworkReport` format.
|
||||
pub fn sendReport(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, w: anytype) !void {
|
||||
var report = comm.Message.NetworkReport{
|
||||
.ipaddrs = undefined,
|
||||
.wifi_ssid = null,
|
||||
.wifi_scan_networks = undefined,
|
||||
};
|
||||
|
||||
// fetch all public IP addresses using getifaddrs
|
||||
const pubaddr = try nif.pubAddresses(gpa, null);
|
||||
defer gpa.free(pubaddr);
|
||||
//var addrs = std.ArrayList([]).init(t.allocator);
|
||||
var ipaddrs = try gpa.alloc([]const u8, pubaddr.len);
|
||||
for (pubaddr) |a, i| {
|
||||
ipaddrs[i] = try std.fmt.allocPrint(gpa, "{s}", .{a});
|
||||
}
|
||||
defer {
|
||||
for (ipaddrs) |a| gpa.free(a);
|
||||
gpa.free(ipaddrs);
|
||||
}
|
||||
report.ipaddrs = ipaddrs;
|
||||
|
||||
// get currently connected SSID, if any, from WPA ctrl
|
||||
const ssid = queryWifiSSID(gpa, wpa_ctrl) catch |err| blk: {
|
||||
logger.err("queryWifiSsid: {any}", .{err});
|
||||
break :blk null;
|
||||
};
|
||||
defer if (ssid) |v| gpa.free(v);
|
||||
report.wifi_ssid = ssid;
|
||||
|
||||
// fetch available wifi networks from scan results using WPA ctrl
|
||||
var wifi_networks: ?types.StringList = if (queryWifiScanResults(gpa, wpa_ctrl)) |v| v else |err| blk: {
|
||||
logger.err("queryWifiScanResults: {any}", .{err});
|
||||
break :blk null;
|
||||
};
|
||||
defer if (wifi_networks) |*list| list.deinit();
|
||||
if (wifi_networks) |list| {
|
||||
report.wifi_scan_networks = list.items();
|
||||
}
|
||||
|
||||
// report everything back to ngui
|
||||
return comm.write(gpa, w, comm.Message{ .network_report = report });
|
||||
}
|
||||
|
||||
/// returns SSID of the currenly connected wifi, if any.
|
||||
/// callers must free returned value with the same allocator.
|
||||
fn queryWifiSSID(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl) !?[]const u8 {
|
||||
var buf: [512:0]u8 = undefined;
|
||||
const resp = try wpa_ctrl.request("STATUS", &buf, null);
|
||||
const ssid = "ssid=";
|
||||
var it = mem.tokenize(u8, resp, "\n");
|
||||
while (it.next()) |line| {
|
||||
if (mem.startsWith(u8, line, ssid)) {
|
||||
// TODO: check line.len vs ssid.len
|
||||
const v = try gpa.dupe(u8, line[ssid.len..]);
|
||||
return v;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// returns a list of all available wifi networks once a scan is complete.
|
||||
/// the scan is initiated with wpa_ctrl.scan() and it is ready when CTRL-EVENT-SCAN-RESULTS
|
||||
/// header is present on wpa_ctrl.
|
||||
///
|
||||
/// the retuned value must be free'd with StringList.deinit.
|
||||
fn queryWifiScanResults(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl) !types.StringList {
|
||||
var buf: [8192:0]u8 = undefined; // TODO: what if isn't enough?
|
||||
// first line is banner: "bssid / frequency / signal level / flags / ssid"
|
||||
const resp = try wpa_ctrl.request("SCAN_RESULTS", &buf, null);
|
||||
var it = mem.tokenize(u8, resp, "\n");
|
||||
if (it.next() == null) {
|
||||
return error.MissingWifiScanHeader;
|
||||
}
|
||||
|
||||
var seen = std.BufSet.init(gpa);
|
||||
defer seen.deinit();
|
||||
var list = types.StringList.init(gpa);
|
||||
errdefer list.deinit();
|
||||
while (it.next()) |line| {
|
||||
// TODO: wpactrl's text protocol won't work for names with control characters
|
||||
if (mem.lastIndexOfScalar(u8, line, '\t')) |i| {
|
||||
const s = mem.trim(u8, line[i..], "\t\n");
|
||||
if (s.len == 0 or seen.contains(s)) {
|
||||
continue;
|
||||
}
|
||||
try seen.insert(s);
|
||||
try list.append(s);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
const WifiNetworksListFilter = struct {
|
||||
ssid: ?[]const u8, // ignore networks whose ssid doesn't match
|
||||
};
|
||||
|
||||
/// returns a list of all configured network IDs or only those matching the filter.
|
||||
/// the returned value must be free'd with the same allocator.
|
||||
fn queryWifiNetworksList(gpa: mem.Allocator, wpa_ctrl: *types.WpaControl, filter: WifiNetworksListFilter) ![]u32 {
|
||||
var buf: [8192:0]u8 = undefined; // TODO: is this enough?
|
||||
// first line is banner: "network id / ssid / bssid / flags"
|
||||
const resp = try wpa_ctrl.request("LIST_NETWORKS", &buf, null);
|
||||
var it = mem.tokenize(u8, resp, "\n");
|
||||
if (it.next() == null) {
|
||||
return error.MissingWifiNetworksListHeader;
|
||||
}
|
||||
|
||||
var list = std.ArrayList(u32).init(gpa);
|
||||
while (it.next()) |line| {
|
||||
var cols = mem.tokenize(u8, line, "\t");
|
||||
const id_str = cols.next() orelse continue; // bad line format?
|
||||
const ssid = cols.next() orelse continue; // bad line format?
|
||||
const id = std.fmt.parseUnsigned(u32, id_str, 10) catch continue; // skip bad line
|
||||
if (filter.ssid != null and !mem.eql(u8, filter.ssid.?, ssid)) {
|
||||
continue;
|
||||
}
|
||||
list.append(id) catch {}; // grab anything we can
|
||||
}
|
||||
return list.toOwnedSlice();
|
||||
}
|
161
src/ngui.zig
161
src/ngui.zig
|
@ -16,10 +16,12 @@ const symbol = @import("ui/symbol.zig");
|
|||
/// the program can handle it.
|
||||
pub const keep_sigpipe = true;
|
||||
|
||||
const logger = std.log.scoped(.ngui);
|
||||
|
||||
// these are auto-closed as soon as main fn terminates.
|
||||
const stdin = std.io.getStdIn().reader();
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
const logger = std.log.scoped(.ngui);
|
||||
|
||||
extern "c" fn ui_update_network_status(text: [*:0]const u8, wifi_list: ?[*:0]const u8) void;
|
||||
|
||||
|
@ -31,16 +33,16 @@ var gpa: std.mem.Allocator = undefined;
|
|||
/// all nm_xxx functions assume it is the case since they are invoked from lvgl c code.
|
||||
var ui_mutex: std.Thread.Mutex = .{};
|
||||
|
||||
/// the program runs until quit is true.
|
||||
/// set from sighandler or on unrecoverable comm failure with the daemon.
|
||||
var want_quit: bool = false;
|
||||
|
||||
var state: enum {
|
||||
active, // normal operational mode
|
||||
standby, // idling
|
||||
alert, // draw user attention; never go standby
|
||||
} = .active;
|
||||
|
||||
/// the program runs until sigquit is true.
|
||||
/// set from sighandler or on unrecoverable comm failure with the daemon.
|
||||
var sigquit: std.Thread.ResetEvent = .{};
|
||||
|
||||
/// by setting wakeup brings the screen back from sleep()'ing without waiting for user action.
|
||||
/// 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
|
||||
|
@ -151,28 +153,28 @@ fn updateNetworkStatus(report: comm.Message.NetworkReport) !void {
|
|||
/// loops indefinitely until program exit or comm returns EOS.
|
||||
fn commThreadLoop() void {
|
||||
while (true) {
|
||||
commThreadLoopCycle() catch |err| logger.err("commThreadLoopCycle: {any}", .{err});
|
||||
|
||||
commThreadLoopCycle() catch |err| {
|
||||
logger.err("commThreadLoopCycle: {any}", .{err});
|
||||
if (err == error.EndOfStream) {
|
||||
// pointless to continue running if comms is broken.
|
||||
// a parent/supervisor is expected to restart ngui.
|
||||
break;
|
||||
}
|
||||
};
|
||||
std.atomic.spinLoopHint();
|
||||
time.sleep(1 * time.ns_per_ms);
|
||||
time.sleep(10 * time.ns_per_ms);
|
||||
}
|
||||
|
||||
logger.info("exiting commThreadLoop", .{});
|
||||
sigquit.set();
|
||||
}
|
||||
|
||||
/// runs one cycle of the commThreadLoop: read messages from stdin and update
|
||||
/// the UI accordingly.
|
||||
fn commThreadLoopCycle() !void {
|
||||
const msg = comm.read(gpa, stdin) catch |err| {
|
||||
if (err == error.EndOfStream) {
|
||||
// pointless to continue running if comms is broken.
|
||||
// a parent/supervisor is expected to restart ngui.
|
||||
logger.err("comm.read: EOS", .{});
|
||||
ui_mutex.lock();
|
||||
want_quit = true;
|
||||
ui_mutex.unlock();
|
||||
}
|
||||
return err;
|
||||
};
|
||||
const msg = try comm.read(gpa, stdin);
|
||||
defer comm.free(gpa, msg);
|
||||
logger.debug("got msg tagged {s}", .{@tagName(msg)});
|
||||
logger.debug("got msg: {s}", .{@tagName(msg)});
|
||||
switch (msg) {
|
||||
.ping => try comm.write(gpa, stdout, comm.Message.pong),
|
||||
.network_report => |report| {
|
||||
|
@ -187,14 +189,46 @@ fn commThreadLoopCycle() !void {
|
|||
}
|
||||
}
|
||||
|
||||
/// prints messages in the same way std.fmt.format does and exits the process
|
||||
/// with a non-zero code.
|
||||
fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
|
||||
stderr.print(fmt, args) catch {};
|
||||
if (fmt[fmt.len - 1] != '\n') {
|
||||
stderr.writeByte('\n') catch {};
|
||||
/// UI thread: LVGL loop runs here.
|
||||
/// must never block unless in idle/sleep mode.
|
||||
fn uiThreadLoop() void {
|
||||
while (true) {
|
||||
ui_mutex.lock();
|
||||
var till_next_ms = lvgl.loopCycle(); // UI loop
|
||||
const do_state = state;
|
||||
ui_mutex.unlock();
|
||||
|
||||
switch (do_state) {
|
||||
.active => {},
|
||||
.alert => {},
|
||||
.standby => {
|
||||
// go into a screen sleep mode due to no user activity
|
||||
wakeup.reset();
|
||||
comm.write(gpa, stdout, comm.Message.standby) catch |err| {
|
||||
logger.err("comm.write standby: {any}", .{err});
|
||||
};
|
||||
screen.sleep(&wakeup); // blocking
|
||||
|
||||
// wake up due to touch screen activity or wakeup event is set
|
||||
logger.info("waking up from sleep", .{});
|
||||
ui_mutex.lock();
|
||||
if (state == .standby) {
|
||||
state = .active;
|
||||
comm.write(gpa, stdout, comm.Message.wakeup) catch |err| {
|
||||
logger.err("comm.write wakeup: {any}", .{err});
|
||||
};
|
||||
lvgl.resetIdle();
|
||||
}
|
||||
ui_mutex.unlock();
|
||||
continue;
|
||||
},
|
||||
}
|
||||
|
||||
std.atomic.spinLoopHint();
|
||||
time.sleep(@max(1, till_next_ms) * time.ns_per_ms); // sleep at least 1ms
|
||||
}
|
||||
std.process.exit(1);
|
||||
|
||||
logger.info("exiting UI thread loop", .{});
|
||||
}
|
||||
|
||||
fn parseArgs(alloc: std.mem.Allocator) !void {
|
||||
|
@ -210,7 +244,8 @@ fn parseArgs(alloc: std.mem.Allocator) !void {
|
|||
try stderr.print("{any}\n", .{buildopts.semver});
|
||||
std.process.exit(0);
|
||||
} else {
|
||||
fatal("unknown arg name {s}", .{a});
|
||||
logger.err("unknown arg name {s}", .{a});
|
||||
return error.UnknownArgName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -227,15 +262,10 @@ fn usage(prog: []const u8) !void {
|
|||
}
|
||||
|
||||
/// 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,
|
||||
os.SIG.INT, os.SIG.TERM => sigquit.set(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
@ -258,11 +288,25 @@ pub fn main() anyerror!void {
|
|||
// initalizes display, input driver and finally creates the user interface.
|
||||
ui.init() catch |err| {
|
||||
logger.err("ui.init: {any}", .{err});
|
||||
std.process.exit(1);
|
||||
return err;
|
||||
};
|
||||
|
||||
// start comms with daemon in a seaparate thread.
|
||||
_ = try std.Thread.spawn(.{}, commThreadLoop, .{});
|
||||
// run idle timer indefinitely.
|
||||
// continue on failure: screen standby won't work at the worst.
|
||||
_ = lvgl.createTimer(nm_check_idle_time, 2000, null) catch |err| {
|
||||
logger.err("lvgl.CreateTimer(idle check): {any}", .{err});
|
||||
};
|
||||
|
||||
{
|
||||
// start the main UI thread.
|
||||
const th = try std.Thread.spawn(.{}, uiThreadLoop, .{});
|
||||
th.detach();
|
||||
}
|
||||
{
|
||||
// start comms with daemon in a seaparate thread.
|
||||
const th = try std.Thread.spawn(.{}, commThreadLoop, .{});
|
||||
th.detach();
|
||||
}
|
||||
|
||||
// set up a sigterm handler for clean exit.
|
||||
const sa = os.Sigaction{
|
||||
|
@ -272,48 +316,9 @@ pub fn main() anyerror!void {
|
|||
};
|
||||
try os.sigaction(os.SIG.INT, &sa, null);
|
||||
try os.sigaction(os.SIG.TERM, &sa, null);
|
||||
sigquit.wait();
|
||||
|
||||
// run idle timer indefinitely
|
||||
_ = lvgl.createTimer(nm_check_idle_time, 2000, null) catch |err| {
|
||||
logger.err("idle timer: lvgl.CreateTimer failed: {any}", .{err});
|
||||
};
|
||||
|
||||
// main UI thread; must never block unless in idle/sleep mode
|
||||
while (!want_quit) {
|
||||
ui_mutex.lock();
|
||||
var till_next_ms = lvgl.loopCycle(); // UI loop
|
||||
const do_state = state;
|
||||
ui_mutex.unlock();
|
||||
|
||||
if (do_state == .standby) {
|
||||
// go into a screen sleep mode due to no user activity
|
||||
wakeup.reset();
|
||||
comm.write(gpa, stdout, comm.Message.standby) catch |err| {
|
||||
logger.err("comm.write standby: {any}", .{err});
|
||||
};
|
||||
screen.sleep(&wakeup);
|
||||
|
||||
// wake up due to touch screen activity or wakeup event is set
|
||||
logger.info("waking up from sleep", .{});
|
||||
ui_mutex.lock();
|
||||
if (state == .standby) {
|
||||
state = .active;
|
||||
comm.write(gpa, stdout, comm.Message.wakeup) catch |err| {
|
||||
logger.err("comm.write wakeup: {any}", .{err});
|
||||
};
|
||||
lvgl.resetIdle();
|
||||
}
|
||||
ui_mutex.unlock();
|
||||
continue;
|
||||
}
|
||||
|
||||
std.atomic.spinLoopHint();
|
||||
time.sleep(@max(1, till_next_ms) * time.ns_per_ms); // sleep at least 1ms
|
||||
}
|
||||
logger.info("main UI loop terminated", .{});
|
||||
|
||||
// not waiting for comm thread because it is terminated at program exit here
|
||||
// anyway.
|
||||
logger.info("main terminated", .{});
|
||||
}
|
||||
|
||||
test "tick" {
|
||||
|
|
22
src/test.zig
22
src/test.zig
|
@ -143,6 +143,28 @@ pub const TestWpaControl = struct {
|
|||
pub fn request(_: Self, _: [:0]const u8, _: [:0]u8, _: ?nif.wpa.ReqCallback) ![]const u8 {
|
||||
return &.{};
|
||||
}
|
||||
|
||||
pub fn addNetwork(_: *Self) !u32 {
|
||||
return 12345;
|
||||
}
|
||||
|
||||
pub fn removeNetwork(_: *Self, id: u32) !void {
|
||||
_ = id;
|
||||
}
|
||||
|
||||
pub fn setNetworkParam(_: *Self, id: u32, name: []const u8, val: []const u8) !void {
|
||||
_ = id;
|
||||
_ = name;
|
||||
_ = val;
|
||||
}
|
||||
|
||||
pub fn selectNetwork(_: *Self, id: u32) !void {
|
||||
_ = id;
|
||||
}
|
||||
|
||||
pub fn enableNetwork(_: *Self, id: u32) !void {
|
||||
_ = id;
|
||||
}
|
||||
};
|
||||
|
||||
/// similar to std.testing.expectEqual but compares slices with expectEqualSlices
|
||||
|
|
|
@ -8,12 +8,12 @@ const logger = std.log.scoped(.play);
|
|||
const stderr = std.io.getStdErr().writer();
|
||||
|
||||
var ngui_proc: std.ChildProcess = undefined;
|
||||
var sigquit = false;
|
||||
var sigquit: std.Thread.ResetEvent = .{};
|
||||
|
||||
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,
|
||||
os.SIG.INT, os.SIG.TERM => sigquit.set(),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +72,59 @@ fn parseArgs(gpa: std.mem.Allocator) !Flags {
|
|||
return flags;
|
||||
}
|
||||
|
||||
fn commThread(gpa: std.mem.Allocator, r: anytype, w: anytype) void {
|
||||
comm.write(gpa, w, .ping) catch |err| logger.err("comm.write ping: {any}", .{err});
|
||||
|
||||
while (true) {
|
||||
std.atomic.spinLoopHint();
|
||||
time.sleep(100 * time.ns_per_ms);
|
||||
|
||||
const msg = comm.read(gpa, r) catch |err| {
|
||||
if (err == error.EndOfStream) {
|
||||
sigquit.set();
|
||||
break;
|
||||
}
|
||||
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 => {
|
||||
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, w, .{ .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, w, .{ .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, w, .{ .poweroff_progress = s3 }) catch |err| logger.err("comm.write: {any}", .{err});
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("exiting comm thread loop", .{});
|
||||
sigquit.set();
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer if (gpa_state.deinit()) {
|
||||
|
@ -89,6 +142,11 @@ pub fn main() !void {
|
|||
fatal("unable to start ngui: {any}", .{err});
|
||||
};
|
||||
|
||||
// ngui proc stdio is auto-closed as soon as its main process terminates.
|
||||
const uireader = ngui_proc.stdout.?.reader();
|
||||
const uiwriter = ngui_proc.stdin.?.writer();
|
||||
_ = try std.Thread.spawn(.{}, commThread, .{ gpa, uireader, uiwriter });
|
||||
|
||||
const sa = os.Sigaction{
|
||||
.handler = .{ .handler = sighandler },
|
||||
.mask = os.empty_sigset,
|
||||
|
@ -96,61 +154,7 @@ pub fn main() !void {
|
|||
};
|
||||
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 => {},
|
||||
}
|
||||
}
|
||||
sigquit.wait();
|
||||
|
||||
logger.info("killing ngui", .{});
|
||||
const term = ngui_proc.kill();
|
||||
|
|
|
@ -47,3 +47,35 @@ pub const IoPipe = struct {
|
|||
return self.w.writer();
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: turns this into a UniqStringList backed by StringArrayHashMap; also see std.BufSet
|
||||
pub const StringList = struct {
|
||||
l: std.ArrayList([]const u8),
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
return Self{
|
||||
.l = std.ArrayList([]const u8).init(allocator),
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
for (self.l.items) |a| {
|
||||
self.allocator.free(a);
|
||||
}
|
||||
self.l.deinit();
|
||||
}
|
||||
|
||||
pub fn append(self: *Self, s: []const u8) !void {
|
||||
const item = try self.allocator.dupe(u8, s);
|
||||
errdefer self.allocator.free(item);
|
||||
try self.l.append(item);
|
||||
}
|
||||
|
||||
pub fn items(self: Self) []const []const u8 {
|
||||
return self.l.items;
|
||||
}
|
||||
};
|
||||
|
|
Reference in New Issue