nd,ngui: make program termination reliable on SIGTERM #23
17
src/comm.zig
17
src/comm.zig
|
@ -1,12 +1,14 @@
|
||||||
///! daemon/gui communication.
|
//! daemon/gui communication.
|
||||||
///! the protocol is a simple TLV construct: MessageTag(u16), length(u64), json-marshalled Message;
|
//! the protocol is a simple TLV construct: MessageTag(u16), length(u64), json-marshalled Message;
|
||||||
///! little endian.
|
//! little endian.
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const json = std.json;
|
const json = std.json;
|
||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
|
|
||||||
const ByteArrayList = @import("types.zig").ByteArrayList;
|
const ByteArrayList = @import("types.zig").ByteArrayList;
|
||||||
|
|
||||||
|
const logger = std.log.scoped(.comm);
|
||||||
|
|
||||||
/// common errors returned by read/write functions.
|
/// common errors returned by read/write functions.
|
||||||
pub const Error = error{
|
pub const Error = error{
|
||||||
CommReadInvalidTag,
|
CommReadInvalidTag,
|
||||||
|
@ -71,12 +73,13 @@ pub const MessageTag = enum(u16) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// reads and parses a single message from the input stream reader.
|
/// 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 {
|
pub fn read(allocator: mem.Allocator, reader: anytype) !Message {
|
||||||
// alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic.
|
// alternative is @intToEnum(reader.ReadIntLittle(u16)) but it may panic.
|
||||||
const tag = reader.readEnum(MessageTag, .Little) catch {
|
const tag = try reader.readEnum(MessageTag, .Little);
|
||||||
return Error.CommReadInvalidTag;
|
|
||||||
};
|
|
||||||
const len = try reader.readIntLittle(u64);
|
const len = try reader.readIntLittle(u64);
|
||||||
if (len == 0) {
|
if (len == 0) {
|
||||||
return switch (tag) {
|
return switch (tag) {
|
||||||
|
|
142
src/nd.zig
142
src/nd.zig
|
@ -28,21 +28,11 @@ fn usage(prog: []const u8) !void {
|
||||||
, .{prog});
|
, .{prog});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// prints messages in the same way std.fmt.format does and exits the process
|
/// nd program flags. see usage.
|
||||||
/// 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.
|
|
||||||
const NdArgs = struct {
|
const NdArgs = struct {
|
||||||
gui: ?[:0]const u8 = null, // = "ngui",
|
gui: ?[:0]const u8 = null,
|
||||||
gui_user: ?[:0]const u8 = null, // u8 = "uiuser",
|
gui_user: ?[:0]const u8 = null,
|
||||||
wpa: ?[:0]const u8 = null, // = "/var/run/wpa_supplicant/wlan0",
|
wpa: ?[:0]const u8 = null,
|
||||||
|
|
||||||
fn deinit(self: @This(), allocator: std.mem.Allocator) void {
|
fn deinit(self: @This(), allocator: std.mem.Allocator) void {
|
||||||
if (self.gui) |p| allocator.free(p);
|
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")) {
|
} else if (std.mem.eql(u8, a, "-wpa")) {
|
||||||
lastarg = .wpa;
|
lastarg = .wpa;
|
||||||
} else {
|
} else {
|
||||||
fatal("unknown arg name {s}", .{a});
|
logger.err("unknown arg name {s}", .{a});
|
||||||
|
return error.UnknownArgName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastarg != .none) {
|
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;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// sigquit signals nd main loop to exit.
|
/// sigquit tells nd to exit.
|
||||||
/// since both the loop and sighandler are on the same thread, it must
|
var sigquit: std.Thread.ResetEvent = .{};
|
||||||
/// 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("received signal {}", .{sig});
|
logger.info("received signal {}", .{sig});
|
||||||
switch (sig) {
|
switch (sig) {
|
||||||
os.SIG.INT, os.SIG.TERM => sigquit = true,
|
os.SIG.INT, os.SIG.TERM => sigquit.set(),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,7 +140,8 @@ pub fn main() !void {
|
||||||
screen.backlight(.on) catch |err| logger.err("backlight: {any}", .{err});
|
screen.backlight(.on) catch |err| logger.err("backlight: {any}", .{err});
|
||||||
|
|
||||||
// start ngui, unless -nogui mode
|
// 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.stdin_behavior = .Pipe;
|
||||||
ngui.stdout_behavior = .Pipe;
|
ngui.stdout_behavior = .Pipe;
|
||||||
ngui.stderr_behavior = .Inherit;
|
ngui.stderr_behavior = .Inherit;
|
||||||
|
@ -158,17 +158,29 @@ 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| 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 uireader = ngui.stdout.?.reader();
|
||||||
const uiwriter = ngui.stdin.?.writer();
|
const uiwriter = ngui.stdin.?.writer();
|
||||||
// send UI a ping as the first thing to make sure pipes are working.
|
// send UI a ping right away to make sure pipes are working, crash otherwise.
|
||||||
// https://git.qcode.ch/nakamochi/ndg/issues/16
|
|
||||||
comm.write(gpa, uiwriter, .ping) catch |err| {
|
comm.write(gpa, uiwriter, .ping) catch |err| {
|
||||||
logger.err("comm.write ping: {any}", .{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)
|
// graceful shutdown; see sigaction(2)
|
||||||
const sa = os.Sigaction{
|
const sa = os.Sigaction{
|
||||||
.handler = .{ .handler = sighandler },
|
.handler = .{ .handler = sighandler },
|
||||||
|
@ -177,71 +189,13 @@ pub fn main() !void {
|
||||||
};
|
};
|
||||||
try os.sigaction(os.SIG.INT, &sa, null);
|
try os.sigaction(os.SIG.INT, &sa, null);
|
||||||
try os.sigaction(os.SIG.TERM, &sa, null);
|
try os.sigaction(os.SIG.TERM, &sa, null);
|
||||||
|
sigquit.wait();
|
||||||
|
|
||||||
var nd = try Daemon.init(gpa, uiwriter, args.wpa.?);
|
// reached here due to sig TERM or INT.
|
||||||
defer nd.deinit();
|
// tell deamon to terminate threads.
|
||||||
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});
|
|
||||||
nd.stop();
|
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.
|
//! daemon watches network status and communicates updates to the GUI using uiwriter.
|
||||||
//! public fields are allocator
|
|
||||||
//! usage example:
|
//! usage example:
|
||||||
//!
|
//!
|
||||||
//! var ctrl = try nif.wpa.Control.open("/run/wpa_supplicant/wlan0");
|
//! var nd = Daemon.init(gpa, ngui_io_reader, ngui_io_writer, "/run/wpa_suppl/wlan0");
|
||||||
//! defer ctrl.close() catch {};
|
//! defer nd.deinit();
|
||||||
//! var nd: Daemon = .{
|
|
||||||
//! .allocator = gpa,
|
|
||||||
//! .uiwriter = ngui_stdio_writer,
|
|
||||||
//! .wpa_ctrl = ctrl,
|
|
||||||
//! };
|
|
||||||
//! try nd.start();
|
//! try nd.start();
|
||||||
|
//! // wait for sigterm...
|
||||||
|
//! nd.stop();
|
||||||
|
//! // terminate ngui proc...
|
||||||
|
//! nd.wait();
|
||||||
|
//!
|
||||||
|
|
||||||
|
const builtin = @import("builtin");
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const mem = std.mem;
|
const mem = std.mem;
|
||||||
const time = std.time;
|
const time = std.time;
|
||||||
|
|
||||||
const nif = @import("nif");
|
|
||||||
|
|
||||||
const comm = @import("../comm.zig");
|
const comm = @import("../comm.zig");
|
||||||
|
const network = @import("network.zig");
|
||||||
const screen = @import("../ui/screen.zig");
|
const screen = @import("../ui/screen.zig");
|
||||||
const types = @import("../types.zig");
|
const types = @import("../types.zig");
|
||||||
const SysService = @import("SysService.zig");
|
const SysService = @import("SysService.zig");
|
||||||
|
|
||||||
const logger = std.log.scoped(.netmon);
|
const logger = std.log.scoped(.daemon);
|
||||||
|
|
||||||
allocator: mem.Allocator,
|
allocator: mem.Allocator,
|
||||||
|
uireader: std.fs.File.Reader, // ngui stdout
|
||||||
uiwriter: std.fs.File.Writer, // ngui stdin
|
uiwriter: std.fs.File.Writer, // ngui stdin
|
||||||
wpa_ctrl: types.WpaControl, // guarded by mu once start'ed
|
wpa_ctrl: types.WpaControl, // guarded by mu once start'ed
|
||||||
|
|
||||||
|
@ -35,17 +35,20 @@ mu: std.Thread.Mutex = .{},
|
||||||
state: enum {
|
state: enum {
|
||||||
stopped,
|
stopped,
|
||||||
running,
|
running,
|
||||||
|
standby,
|
||||||
poweroff,
|
poweroff,
|
||||||
},
|
},
|
||||||
|
|
||||||
main_thread: ?std.Thread = null,
|
main_thread: ?std.Thread = null,
|
||||||
|
comm_thread: ?std.Thread = null,
|
||||||
poweroff_thread: ?std.Thread = null,
|
poweroff_thread: ?std.Thread = null,
|
||||||
|
|
||||||
want_stop: bool = false, // tells daemon main loop to quit
|
want_stop: bool = false, // tells daemon main loop to quit
|
||||||
want_network_report: bool = false,
|
// network flags
|
||||||
want_wifi_scan: bool = false,
|
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,
|
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,
|
wpa_save_config_on_connected: bool = false,
|
||||||
|
|
||||||
/// system services actively managed by the daemon.
|
/// system services actively managed by the daemon.
|
||||||
|
@ -55,8 +58,10 @@ services: []SysService = &.{},
|
||||||
|
|
||||||
const Daemon = @This();
|
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.
|
/// 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);
|
var svlist = std.ArrayList(SysService).init(a);
|
||||||
errdefer {
|
errdefer {
|
||||||
for (svlist.items) |*sv| sv.deinit();
|
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 }));
|
try svlist.append(SysService.init(a, "bitcoind", .{ .stop_wait_sec = 600 }));
|
||||||
return .{
|
return .{
|
||||||
.allocator = a,
|
.allocator = a,
|
||||||
.uiwriter = iogui,
|
.uireader = r,
|
||||||
.wpa_ctrl = try types.WpaControl.open(wpa_path),
|
.uiwriter = w,
|
||||||
|
.wpa_ctrl = try types.WpaControl.open(wpa),
|
||||||
.state = .stopped,
|
.state = .stopped,
|
||||||
.services = svlist.toOwnedSlice(),
|
.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.
|
/// 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 {
|
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});
|
self.wpa_ctrl.close() catch |err| logger.err("deinit: wpa_ctrl.close: {any}", .{err});
|
||||||
for (self.services) |*sv| {
|
for (self.services) |*sv| {
|
||||||
sv.deinit();
|
sv.deinit();
|
||||||
|
@ -93,95 +95,124 @@ pub fn deinit(self: *Daemon) void {
|
||||||
self.allocator.free(self.services);
|
self.allocator.free(self.services);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// start launches a main thread and returns immediately.
|
/// start launches daemon threads and returns immediately.
|
||||||
/// once started, the daemon must be eventually stop'ed to clean up resources
|
/// once started, the daemon must be eventually stop'ed and wait'ed to clean up
|
||||||
/// even if a poweroff sequence is launched with beginPoweroff. however, in the latter
|
/// resources even if a poweroff sequence is initiated with beginPoweroff.
|
||||||
/// case the daemon cannot be start'ed again after stop.
|
|
||||||
pub fn start(self: *Daemon) !void {
|
pub fn start(self: *Daemon) !void {
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
switch (self.state) {
|
switch (self.state) {
|
||||||
.running => return error.AlreadyStarted,
|
|
||||||
.poweroff => return error.InPoweroffState,
|
|
||||||
.stopped => {}, // continue
|
.stopped => {}, // continue
|
||||||
|
.poweroff => return error.InPoweroffState,
|
||||||
|
else => return error.AlreadyStarted,
|
||||||
}
|
}
|
||||||
|
|
||||||
try self.wpa_ctrl.attach();
|
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.main_thread = try std.Thread.spawn(.{}, mainThreadLoop, .{self});
|
||||||
|
self.comm_thread = try std.Thread.spawn(.{}, commThreadLoop, .{self});
|
||||||
self.state = .running;
|
self.state = .running;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// stop blocks until all daemon threads exit, including poweroff if any.
|
/// tells the daemon to stop threads to prepare for termination.
|
||||||
/// once stopped, the daemon can be start'ed again unless a poweroff was initiated.
|
/// stop returns immediately.
|
||||||
///
|
/// callers must `wait` to release all resources.
|
||||||
/// 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();
|
||||||
if (self.want_stop or self.state == .stopped) {
|
defer self.mu.unlock();
|
||||||
self.mu.unlock();
|
|
||||||
return; // already in progress or stopped
|
|
||||||
}
|
|
||||||
self.want_stop = true;
|
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| {
|
if (self.main_thread) |th| {
|
||||||
th.join();
|
th.join();
|
||||||
self.main_thread = null;
|
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.
|
// must be the last one to join because it sends a final poweroff report.
|
||||||
if (self.poweroff_thread) |th| {
|
if (self.poweroff_thread) |th| {
|
||||||
th.join();
|
th.join();
|
||||||
self.poweroff_thread = null;
|
self.poweroff_thread = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.mu.lock();
|
self.wpa_ctrl.detach() catch |err| logger.err("wait: wpa_ctrl.detach: {any}", .{err});
|
||||||
defer self.mu.unlock();
|
|
||||||
self.want_stop = false;
|
self.want_stop = false;
|
||||||
if (self.state != .poweroff) { // keep poweroff to prevent start'ing again
|
self.state = .stopped;
|
||||||
self.state = .stopped;
|
|
||||||
}
|
|
||||||
self.wpa_ctrl.detach() catch |err| logger.err("stop: wpa_ctrl.detach: {any}", .{err});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
self.mu.lock();
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
switch (self.state) {
|
switch (self.state) {
|
||||||
.poweroff => return error.InPoweroffState,
|
.standby => {},
|
||||||
.running, .stopped => {}, // continue
|
.stopped, .poweroff => return error.InvalidState,
|
||||||
|
.running => {
|
||||||
|
try screen.backlight(.off);
|
||||||
|
self.state = .standby;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
try screen.backlight(.off);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wakeup(_: *Daemon) !void {
|
/// tells the daemon to return from standby, typically due to user interaction.
|
||||||
try screen.backlight(.on);
|
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
|
/// initiates system poweroff sequence in a separate thread: shut down select
|
||||||
/// system services such as lnd and bitcoind, and issue "poweroff" command.
|
/// 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.
|
/// beingPoweroff also makes other threads exit but callers must still call `wait`
|
||||||
/// however, in poweroff mode regular functionalities are disabled, such as
|
/// to make sure poweroff sequence is complete.
|
||||||
/// wifi scan and standby.
|
fn beginPoweroff(self: *Daemon) !void {
|
||||||
pub fn beginPoweroff(self: *Daemon) !void {
|
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
if (self.state == .poweroff) {
|
switch (self.state) {
|
||||||
return; // already in poweroff 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
|
/// set when poweroff_thread starts. available in tests only.
|
||||||
// the progress to ngui.
|
var test_poweroff_started = if (builtin.is_test) std.Thread.ResetEvent{} else {};
|
||||||
fn poweroffThread(self: *Daemon) !void {
|
|
||||||
|
/// 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", .{});
|
logger.info("begin powering off", .{});
|
||||||
screen.backlight(.on) catch |err| {
|
screen.backlight(.on) catch |err| {
|
||||||
logger.err("screen.backlight(.on) during poweroff: {any}", .{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.
|
// initiate shutdown of all services concurrently.
|
||||||
for (self.services) |*sv| {
|
for (self.services) |*sv| {
|
||||||
|
@ -202,48 +233,110 @@ fn poweroffThread(self: *Daemon) !void {
|
||||||
logger.info("poweroff: {any}", .{res});
|
logger.info("poweroff: {any}", .{res});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// main thread entry point.
|
/// main thread entry point: watches for want_xxx flags and monitors network.
|
||||||
fn mainThreadLoop(self: *Daemon) !void {
|
/// exits when want_stop is true.
|
||||||
|
fn mainThreadLoop(self: *Daemon) void {
|
||||||
var quit = false;
|
var quit = false;
|
||||||
while (!quit) {
|
while (!quit) {
|
||||||
self.mainThreadLoopCycle() catch |err| logger.err("main thread loop: {any}", .{err});
|
self.mainThreadLoopCycle() catch |err| logger.err("main thread loop: {any}", .{err});
|
||||||
|
std.atomic.spinLoopHint();
|
||||||
time.sleep(1 * time.ns_per_s);
|
time.sleep(1 * time.ns_per_s);
|
||||||
|
|
||||||
self.mu.lock();
|
self.mu.lock();
|
||||||
quit = self.want_stop;
|
quit = self.want_stop;
|
||||||
self.mu.unlock();
|
self.mu.unlock();
|
||||||
}
|
}
|
||||||
|
logger.info("exiting main thread loop", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// run one cycle of the main thread loop iteration.
|
/// runs one cycle of the main thread loop iteration.
|
||||||
/// unless in poweroff mode, the cycle holds self.mu for the whole duration.
|
/// the cycle holds self.mu for the whole duration.
|
||||||
fn mainThreadLoopCycle(self: *Daemon) !void {
|
fn mainThreadLoopCycle(self: *Daemon) !void {
|
||||||
switch (self.state) {
|
self.mu.lock();
|
||||||
// poweroff mode: do nothing; handled by poweroffThread
|
defer self.mu.unlock();
|
||||||
.poweroff => {},
|
self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
|
||||||
// normal state: running or standby
|
if (self.want_wifi_scan) {
|
||||||
else => {
|
if (self.startWifiScan()) {
|
||||||
self.mu.lock();
|
self.want_wifi_scan = false;
|
||||||
defer self.mu.unlock();
|
} else |err| {
|
||||||
self.readWPACtrlMsg() catch |err| logger.err("readWPACtrlMsg: {any}", .{err});
|
logger.err("startWifiScan: {any}", .{err});
|
||||||
if (self.want_wifi_scan) {
|
}
|
||||||
if (self.startWifiScan()) {
|
}
|
||||||
self.want_wifi_scan = false;
|
if (self.want_network_report and self.network_report_ready) {
|
||||||
} else |err| {
|
if (network.sendReport(self.allocator, &self.wpa_ctrl, self.uiwriter)) {
|
||||||
logger.err("startWifiScan: {any}", .{err});
|
self.want_network_report = false;
|
||||||
}
|
} else |err| {
|
||||||
}
|
logger.err("network.sendReport: {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});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
fn sendPoweroffReport(self: *Daemon) !void {
|
||||||
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.len);
|
var svstat = try self.allocator.alloc(comm.Message.PoweroffProgress.Service, self.services.len);
|
||||||
defer self.allocator.free(svstat);
|
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.
|
/// 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 {
|
fn wifiInvalidKey(self: *Daemon) void {
|
||||||
self.wpa_save_config_on_connected = false;
|
self.wpa_save_config_on_connected = false;
|
||||||
self.want_network_report = true;
|
self.want_network_report = true;
|
||||||
self.network_report_ready = true;
|
self.network_report_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const ReportNetworkStatusOpt = struct {
|
const ReportNetworkStatusOpt = struct {
|
||||||
scan: bool,
|
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();
|
self.mu.lock();
|
||||||
defer self.mu.unlock();
|
defer self.mu.unlock();
|
||||||
self.want_network_report = true;
|
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) {
|
if (ssid.len == 0) {
|
||||||
return error.ConnectWifiEmptySSID;
|
return error.ConnectWifiEmptySSID;
|
||||||
}
|
}
|
||||||
|
@ -320,21 +416,24 @@ pub fn startConnectWifi(self: *Daemon, ssid: []const u8, password: []const u8) !
|
||||||
th.detach();
|
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 {
|
fn connectWifiThread(self: *Daemon, ssid: []const u8, password: []const u8) void {
|
||||||
|
self.mu.lock();
|
||||||
defer {
|
defer {
|
||||||
|
self.mu.unlock();
|
||||||
self.allocator.free(ssid);
|
self.allocator.free(ssid);
|
||||||
self.allocator.free(password);
|
self.allocator.free(password);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
// this prevents main thread from looping until released,
|
const id = network.addWifi(self.allocator, &self.wpa_ctrl, ssid, password) catch |err| {
|
||||||
// but the following commands and expected to be pretty quick.
|
logger.err("addWifi: {any}; exiting", .{err});
|
||||||
self.mu.lock();
|
|
||||||
defer self.mu.unlock();
|
|
||||||
|
|
||||||
const id = self.addWifiNetwork(ssid, password) catch |err| {
|
|
||||||
logger.err("addWifiNetwork: {any}; exiting", .{err});
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// SELECT_NETWORK <id> - this disables others
|
// 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;
|
self.wpa_save_config_on_connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// adds a new network and configures its parameters.
|
/// reads all available messages from self.wpa_ctrl and acts accordingly.
|
||||||
/// caller must hold self.mu.
|
/// callers 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.
|
|
||||||
fn readWPACtrlMsg(self: *Daemon) !void {
|
fn readWPACtrlMsg(self: *Daemon) !void {
|
||||||
var buf: [512:0]u8 = undefined;
|
var buf: [512:0]u8 = undefined;
|
||||||
while (try self.wpa_ctrl.pending()) {
|
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" {
|
test "start-stop" {
|
||||||
const t = std.testing;
|
const t = std.testing;
|
||||||
|
|
||||||
const pipe = try types.IoPipe.create();
|
const pipe = try types.IoPipe.create();
|
||||||
defer pipe.close();
|
var daemon = try Daemon.init(t.allocator, pipe.reader(), pipe.writer(), "/dev/null");
|
||||||
var daemon = try Daemon.init(t.allocator, pipe.writer(), "/dev/null");
|
daemon.want_network_report = false;
|
||||||
|
|
||||||
try t.expect(daemon.state == .stopped);
|
try t.expect(daemon.state == .stopped);
|
||||||
try daemon.start();
|
try daemon.start();
|
||||||
try t.expect(daemon.state == .running);
|
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.opened);
|
||||||
try t.expect(daemon.wpa_ctrl.attached);
|
try t.expect(daemon.wpa_ctrl.attached);
|
||||||
|
|
||||||
daemon.stop();
|
daemon.stop();
|
||||||
|
pipe.close();
|
||||||
|
daemon.wait();
|
||||||
try t.expect(daemon.state == .stopped);
|
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.attached);
|
||||||
try t.expect(daemon.wpa_ctrl.opened);
|
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);
|
try t.expect(daemon.services.len > 0);
|
||||||
for (daemon.services) |*sv| {
|
for (daemon.services) |*sv| {
|
||||||
|
@ -610,7 +520,7 @@ test "start-stop" {
|
||||||
try t.expect(!daemon.wpa_ctrl.opened);
|
try t.expect(!daemon.wpa_ctrl.opened);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "start-poweroff-stop" {
|
test "start-poweroff" {
|
||||||
const t = std.testing;
|
const t = std.testing;
|
||||||
const tt = @import("../test.zig");
|
const tt = @import("../test.zig");
|
||||||
|
|
||||||
|
@ -618,37 +528,44 @@ test "start-poweroff-stop" {
|
||||||
defer arena_alloc.deinit();
|
defer arena_alloc.deinit();
|
||||||
const arena = arena_alloc.allocator();
|
const arena = arena_alloc.allocator();
|
||||||
|
|
||||||
const pipe = try types.IoPipe.create();
|
const gui_stdin = try types.IoPipe.create();
|
||||||
var daemon = try Daemon.init(arena, pipe.writer(), "/dev/null");
|
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 {
|
defer {
|
||||||
daemon.deinit();
|
daemon.deinit();
|
||||||
pipe.close();
|
gui_stdin.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
try daemon.start();
|
try daemon.start();
|
||||||
try daemon.beginPoweroff();
|
try comm.write(arena, gui_stdout.writer(), comm.Message.poweroff);
|
||||||
daemon.stop();
|
try test_poweroff_started.timedWait(2 * time.ns_per_s);
|
||||||
try t.expect(daemon.state == .poweroff);
|
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| {
|
for (daemon.services) |*sv| {
|
||||||
try t.expect(sv.stop_proc.spawned);
|
try t.expect(sv.stop_proc.spawned);
|
||||||
try t.expect(sv.stop_proc.waited);
|
try t.expect(sv.stop_proc.waited);
|
||||||
try t.expectEqual(SysService.Status.stopped, sv.status());
|
try t.expectEqual(SysService.Status.stopped, sv.status());
|
||||||
}
|
}
|
||||||
|
|
||||||
const pipe_reader = pipe.reader();
|
const msg1 = try comm.read(arena, gui_reader);
|
||||||
const msg1 = try comm.read(arena, pipe_reader);
|
|
||||||
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
|
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
|
||||||
.{ .name = "lnd", .stopped = false, .err = null },
|
.{ .name = "lnd", .stopped = false, .err = null },
|
||||||
.{ .name = "bitcoind", .stopped = false, .err = null },
|
.{ .name = "bitcoind", .stopped = false, .err = null },
|
||||||
} } }, msg1);
|
} } }, 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 = &.{
|
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
|
||||||
.{ .name = "lnd", .stopped = true, .err = null },
|
.{ .name = "lnd", .stopped = true, .err = null },
|
||||||
.{ .name = "bitcoind", .stopped = false, .err = null },
|
.{ .name = "bitcoind", .stopped = false, .err = null },
|
||||||
} } }, msg2);
|
} } }, 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 = &.{
|
try tt.expectDeepEqual(comm.Message{ .poweroff_progress = .{ .services = &.{
|
||||||
.{ .name = "lnd", .stopped = true, .err = null },
|
.{ .name = "lnd", .stopped = true, .err = null },
|
||||||
.{ .name = "bitcoind", .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.
|
/// the program can handle it.
|
||||||
pub const keep_sigpipe = true;
|
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 stdin = std.io.getStdIn().reader();
|
||||||
const stdout = std.io.getStdOut().writer();
|
const stdout = std.io.getStdOut().writer();
|
||||||
const stderr = std.io.getStdErr().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;
|
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.
|
/// 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.
|
|
||||||
/// 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;
|
||||||
|
|
||||||
|
/// 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.
|
/// 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.
|
/// 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
|
||||||
|
@ -151,28 +153,28 @@ fn updateNetworkStatus(report: comm.Message.NetworkReport) !void {
|
||||||
/// loops indefinitely until program exit or comm returns EOS.
|
/// loops indefinitely until program exit or comm returns EOS.
|
||||||
fn commThreadLoop() void {
|
fn commThreadLoop() void {
|
||||||
while (true) {
|
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();
|
std.atomic.spinLoopHint();
|
||||||
time.sleep(1 * time.ns_per_ms);
|
time.sleep(10 * time.ns_per_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("exiting commThreadLoop", .{});
|
logger.info("exiting commThreadLoop", .{});
|
||||||
|
sigquit.set();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// runs one cycle of the commThreadLoop: read messages from stdin and update
|
||||||
|
/// the UI accordingly.
|
||||||
fn commThreadLoopCycle() !void {
|
fn commThreadLoopCycle() !void {
|
||||||
const msg = comm.read(gpa, stdin) catch |err| {
|
const msg = try comm.read(gpa, stdin);
|
||||||
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;
|
|
||||||
};
|
|
||||||
defer comm.free(gpa, msg);
|
defer comm.free(gpa, msg);
|
||||||
logger.debug("got msg tagged {s}", .{@tagName(msg)});
|
logger.debug("got msg: {s}", .{@tagName(msg)});
|
||||||
switch (msg) {
|
switch (msg) {
|
||||||
.ping => try comm.write(gpa, stdout, comm.Message.pong),
|
.ping => try comm.write(gpa, stdout, comm.Message.pong),
|
||||||
.network_report => |report| {
|
.network_report => |report| {
|
||||||
|
@ -187,14 +189,46 @@ fn commThreadLoopCycle() !void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// prints messages in the same way std.fmt.format does and exits the process
|
/// UI thread: LVGL loop runs here.
|
||||||
/// with a non-zero code.
|
/// must never block unless in idle/sleep mode.
|
||||||
fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
|
fn uiThreadLoop() void {
|
||||||
stderr.print(fmt, args) catch {};
|
while (true) {
|
||||||
if (fmt[fmt.len - 1] != '\n') {
|
ui_mutex.lock();
|
||||||
stderr.writeByte('\n') catch {};
|
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 {
|
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});
|
try stderr.print("{any}\n", .{buildopts.semver});
|
||||||
std.process.exit(0);
|
std.process.exit(0);
|
||||||
} else {
|
} 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.
|
/// 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 {
|
fn sighandler(sig: c_int) callconv(.C) void {
|
||||||
logger.info("received signal {}", .{sig});
|
logger.info("received signal {}", .{sig});
|
||||||
switch (sig) {
|
switch (sig) {
|
||||||
os.SIG.INT, os.SIG.TERM => want_quit = true,
|
os.SIG.INT, os.SIG.TERM => sigquit.set(),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,11 +288,25 @@ pub fn main() anyerror!void {
|
||||||
// initalizes display, input driver and finally creates the user interface.
|
// initalizes display, input driver and finally creates the user interface.
|
||||||
ui.init() catch |err| {
|
ui.init() catch |err| {
|
||||||
logger.err("ui.init: {any}", .{err});
|
logger.err("ui.init: {any}", .{err});
|
||||||
std.process.exit(1);
|
return err;
|
||||||
};
|
};
|
||||||
|
|
||||||
// start comms with daemon in a seaparate thread.
|
// run idle timer indefinitely.
|
||||||
_ = try std.Thread.spawn(.{}, commThreadLoop, .{});
|
// 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.
|
// set up a sigterm handler for clean exit.
|
||||||
const sa = os.Sigaction{
|
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.INT, &sa, null);
|
||||||
try os.sigaction(os.SIG.TERM, &sa, null);
|
try os.sigaction(os.SIG.TERM, &sa, null);
|
||||||
|
sigquit.wait();
|
||||||
|
|
||||||
// run idle timer indefinitely
|
logger.info("main terminated", .{});
|
||||||
_ = 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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "tick" {
|
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 {
|
pub fn request(_: Self, _: [:0]const u8, _: [:0]u8, _: ?nif.wpa.ReqCallback) ![]const u8 {
|
||||||
return &.{};
|
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
|
/// 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();
|
const stderr = std.io.getStdErr().writer();
|
||||||
|
|
||||||
var ngui_proc: std.ChildProcess = undefined;
|
var ngui_proc: std.ChildProcess = undefined;
|
||||||
var sigquit = false;
|
var sigquit: std.Thread.ResetEvent = .{};
|
||||||
|
|
||||||
fn sighandler(sig: c_int) callconv(.C) void {
|
fn sighandler(sig: c_int) callconv(.C) void {
|
||||||
logger.info("received signal {} (TERM={} INT={})", .{ sig, os.SIG.TERM, os.SIG.INT });
|
logger.info("received signal {} (TERM={} INT={})", .{ sig, os.SIG.TERM, os.SIG.INT });
|
||||||
switch (sig) {
|
switch (sig) {
|
||||||
os.SIG.INT, os.SIG.TERM => sigquit = true,
|
os.SIG.INT, os.SIG.TERM => sigquit.set(),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,59 @@ fn parseArgs(gpa: std.mem.Allocator) !Flags {
|
||||||
return 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 {
|
pub fn main() !void {
|
||||||
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa_state = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer if (gpa_state.deinit()) {
|
defer if (gpa_state.deinit()) {
|
||||||
|
@ -89,6 +142,11 @@ pub fn main() !void {
|
||||||
fatal("unable to start ngui: {any}", .{err});
|
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{
|
const sa = os.Sigaction{
|
||||||
.handler = .{ .handler = sighandler },
|
.handler = .{ .handler = sighandler },
|
||||||
.mask = os.empty_sigset,
|
.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.INT, &sa, null);
|
||||||
try os.sigaction(os.SIG.TERM, &sa, null);
|
try os.sigaction(os.SIG.TERM, &sa, null);
|
||||||
|
sigquit.wait();
|
||||||
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", .{});
|
logger.info("killing ngui", .{});
|
||||||
const term = ngui_proc.kill();
|
const term = ngui_proc.kill();
|
||||||
|
|
|
@ -47,3 +47,35 @@ pub const IoPipe = struct {
|
||||||
return self.w.writer();
|
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