Skip to content

Commit 5c65333

Browse files
committed
Display current git pulled sysupdates HEAD in UI
1 parent 897f5c4 commit 5c65333

File tree

6 files changed

+170
-15
lines changed

6 files changed

+170
-15
lines changed

src/comm.zig

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ pub const MessageTag = enum(u16) {
9595
screen_unlock_result = 0x18,
9696
// ngui -> nd: set or disable screenlock pin code
9797
slock_set_pincode = 0x19,
98-
// next: 0x1a
98+
// ngui -> nd: request current settings
99+
get_settings = 0x1a,
100+
// next: 0x1b
99101
};
100102

101103
/// daemon and gui exchange messages of this type.
@@ -124,6 +126,7 @@ pub const Message = union(MessageTag) {
124126
unlock_screen: []const u8, // pincode
125127
screen_unlock_result: ScreenUnlockResult,
126128
slock_set_pincode: ?[]const u8,
129+
get_settings: void,
127130

128131
pub const WifiConnect = struct {
129132
ssid: []const u8,
@@ -266,6 +269,7 @@ pub const Message = union(MessageTag) {
266269
sysupdates: struct {
267270
url: []const u8,
268271
channel: SysupdatesChan,
272+
head: []const u8 = "", // git commit hash
269273
},
270274
};
271275

@@ -308,6 +312,7 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
308312
.poweroff => .{ .value = .{ .poweroff = {} } },
309313
.standby => .{ .value = .{ .standby = {} } },
310314
.wakeup => .{ .value = .{ .wakeup = {} } },
315+
.get_settings => .{ .value = .{ .get_settings = {} } },
311316
else => Error.CommReadZeroLenInNonVoidTag,
312317
};
313318
}
@@ -319,6 +324,7 @@ pub fn read(allocator: mem.Allocator, reader: anytype) !ParsedMessage {
319324
.poweroff,
320325
.standby,
321326
.wakeup,
327+
.get_settings,
322328
=> unreachable, // handled above
323329
inline else => |t| {
324330
const bytes = try allocator.alloc(u8, len);
@@ -348,7 +354,7 @@ pub fn write(allocator: mem.Allocator, writer: anytype, msg: Message) !void {
348354
var data = types.ByteArrayList.init(allocator);
349355
defer data.deinit();
350356
switch (msg) {
351-
.ping, .pong, .poweroff, .standby, .wakeup => {}, // zero length payload
357+
.ping, .pong, .poweroff, .standby, .wakeup, .get_settings => {}, // zero length payload
352358
.wifi_connect => try json.stringify(msg.wifi_connect, .{}, data.writer()),
353359
.network_report => try json.stringify(msg.network_report, .{}, data.writer()),
354360
.get_network_report => try json.stringify(msg.get_network_report, .{}, data.writer()),
@@ -466,6 +472,7 @@ test "write/read void tags" {
466472
Message.poweroff,
467473
Message.standby,
468474
Message.wakeup,
475+
Message.get_settings,
469476
};
470477

471478
for (msg) |m| {

src/nd/Config.zig

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const logger = std.log.scoped(.config);
1212
const SYSUPDATES_CRON_SCRIPT_PATH = "/etc/cron.hourly/sysupdate";
1313
/// must be the same as https://github.com/nakamochi/sysupdates/blob/master/update.sh
1414
const SYSUPDATES_RUN_SCRIPT_NAME = "update.sh";
15-
const SYSUPDATES_RUN_SCRIPT_PATH = "/ssd/sysupdates/" ++ SYSUPDATES_RUN_SCRIPT_NAME;
15+
pub const SYSUPDATES_LOCAL_REPO_PATH = "/ssd/sysupdates";
16+
const SYSUPDATES_RUN_SCRIPT_PATH = SYSUPDATES_LOCAL_REPO_PATH ++ "/" ++ SYSUPDATES_RUN_SCRIPT_NAME;
1617
const SYSUPDATES_DEFAULT_URL = "https://github.com/nakamochi/sysupdates.git";
1718

1819
/// must be the same as https://github.com/nakamochi/sysupdates/tree/master/lnd
@@ -52,6 +53,7 @@ pub const Data = struct {
5253
syschannel: SysupdatesChannel,
5354
syscronscript: []const u8,
5455
sysrunscript: []const u8,
56+
head: []const u8 = "", // current HEAD commit hash of local sysupdates repo
5557
};
5658

5759
/// static data is interred at init and never changes except for hostname - see `setHostname`.
@@ -109,6 +111,8 @@ fn initData(allocator: std.mem.Allocator, filepath: []const u8) !Data {
109111

110112
fn inferData(allocator: std.mem.Allocator) Data {
111113
const sysupdates_channel_data = inferSysupdatesChannel(allocator, SYSUPDATES_CRON_SCRIPT_PATH);
114+
const sysupdates_current_state = inferCurrentUpdateState(allocator, SYSUPDATES_LOCAL_REPO_PATH);
115+
logger.info("inferred sysupdates current HEAD: {s}", .{sysupdates_current_state});
112116
if (sysupdates_channel_data.channel == .disabled) {
113117
logger.info("sysupdates are disabled", .{});
114118
} else {
@@ -120,6 +124,7 @@ fn inferData(allocator: std.mem.Allocator) Data {
120124
.syschannel = sysupdates_channel_data.channel,
121125
.syscronscript = SYSUPDATES_CRON_SCRIPT_PATH,
122126
.sysrunscript = SYSUPDATES_RUN_SCRIPT_PATH,
127+
.head = sysupdates_current_state,
123128
};
124129
}
125130

@@ -161,6 +166,46 @@ fn inferSysupdatesChannel(allocator: std.mem.Allocator, cron_script_path: []cons
161166
return .{ .url = SYSUPDATES_DEFAULT_URL, .channel = .master };
162167
}
163168

169+
fn inferCurrentUpdateState(allocator: std.mem.Allocator, local_repo_path: []const u8) []const u8 {
170+
// In tests (or some deployments) the repo path may not exist.
171+
// Treat that as "unknown" without logging an error.
172+
{
173+
var d = std.fs.cwd().openDir(local_repo_path, .{}) catch |err| switch (err) {
174+
error.FileNotFound, error.NotDir => return "",
175+
else => {
176+
logger.debug("inferCurrentUpdateState: openDir({s}) failed: {any}", .{ local_repo_path, err });
177+
return "";
178+
},
179+
};
180+
d.close();
181+
}
182+
183+
const res = std.process.Child.run(.{
184+
.allocator = allocator,
185+
.argv = &.{ "git", "-C", local_repo_path, "rev-parse", "--short", "HEAD" },
186+
}) catch |err| {
187+
// git missing or spawn error; keep it non-fatal/noisy.
188+
logger.debug("inferCurrentUpdateState: failed to run git: {any}", .{err});
189+
return "";
190+
};
191+
defer {
192+
allocator.free(res.stdout);
193+
allocator.free(res.stderr);
194+
}
195+
196+
if (res.term != .Exited or res.term.Exited != 0) {
197+
// Common cases: not a git repo, cannot chdir, etc. Avoid error logs in tests.
198+
logger.debug(
199+
"inferCurrentUpdateState: git rev-parse failed; term={any}; stderr: {s}",
200+
.{ res.term, res.stderr },
201+
);
202+
return "";
203+
}
204+
205+
const head = std.mem.trim(u8, res.stdout, &std.ascii.whitespace);
206+
return allocator.dupe(u8, head) catch "";
207+
}
208+
164209
fn inferStaticData(allocator: std.mem.Allocator) !StaticData {
165210
const hostname = try sys.hostname(allocator);
166211
const lnduser: ?std.process.UserInfo = blk: {
@@ -567,6 +612,7 @@ test "ndconfig: init existing" {
567612
try t.expectEqual(SysupdatesChannel.dev, conf.data.syschannel);
568613
try t.expectEqualStrings("/cron/sysupdates.sh", conf.data.syscronscript);
569614
try t.expectEqualStrings("/sysupdates/run.sh", conf.data.sysrunscript);
615+
try t.expectEqualStrings("", conf.data.head);
570616
}
571617

572618
test "ndconfig: init null" {
@@ -578,6 +624,7 @@ test "ndconfig: init null" {
578624
try t.expectEqual(SysupdatesChannel.disabled, conf.data.syschannel);
579625
try t.expectEqualStrings(SYSUPDATES_CRON_SCRIPT_PATH, conf.data.syscronscript);
580626
try t.expectEqualStrings(SYSUPDATES_RUN_SCRIPT_PATH, conf.data.sysrunscript);
627+
try t.expectEqualStrings("", conf.data.head);
581628
}
582629

583630
test "ndconfig: dump" {
@@ -611,6 +658,7 @@ test "ndconfig: dump" {
611658
try t.expectEqual(SysupdatesChannel.master, parsed.value.syschannel);
612659
try t.expectEqualStrings("cronscript.sh", parsed.value.syscronscript);
613660
try t.expectEqualStrings("runscript.sh", parsed.value.sysrunscript);
661+
try t.expectEqualStrings("", parsed.value.head);
614662
}
615663

616664
test "ndconfig: switch sysupdates and infer" {

src/nd/Daemon.zig

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ want_lnd_report: bool,
6969
lnd_timer: time.Timer,
7070
lnd_report_interval: u64 = 1 * time.ns_per_min,
7171
lnd_tls_reset_count: usize = 0,
72+
sysupdates_head: ?[]u8 = null,
73+
next_head_refresh_ns: i128 = 0,
7274

7375
// TODO: move this to a sys.ServiceList
7476
/// system services actively managed by the daemon.
@@ -143,7 +145,7 @@ pub fn init(opt: InitOpt) !Daemon {
143145

144146
logger.debug("conf = {any}", .{opt.conf});
145147

146-
return .{
148+
var d: Daemon = .{
147149
.allocator = opt.allocator,
148150
.conf = opt.conf,
149151
.uireader = opt.uir,
@@ -164,14 +166,22 @@ pub fn init(opt: InitOpt) !Daemon {
164166
// report lightning status immediately on start
165167
.want_lnd_report = true,
166168
.lnd_timer = try time.Timer.start(),
169+
.next_head_refresh_ns = 0,
170+
.sysupdates_head = null,
167171
};
172+
173+
d.refreshSysupdatesHead();
174+
return d;
168175
}
169176

170177
/// releases all associated resources.
171178
/// the daemon must be stop'ed and wait'ed before deiniting.
172179
pub fn deinit(self: *Daemon) void {
173180
self.wpa_ctrl.close() catch |err| logger.err("deinit: wpa_ctrl.close: {any}", .{err});
174181
self.services.deinit(self.allocator);
182+
if (self.sysupdates_head) |head| {
183+
self.allocator.free(head);
184+
}
175185
}
176186

177187
/// start launches daemon threads and returns immediately.
@@ -338,16 +348,32 @@ fn mainThreadLoop(self: *Daemon) void {
338348
logger.info("exiting main thread loop", .{});
339349
}
340350

351+
const HEAD_REFRESH_PERIOD_NS: i128 = 60 * std.time.ns_per_s;
352+
341353
/// runs one cycle of the main thread loop iteration.
342354
/// the cycle holds self.mu for the whole duration.
343355
fn mainThreadLoopCycle(self: *Daemon) !void {
356+
// decide whether to refresh (fast)
357+
var do_refresh = false;
358+
const now = std.time.nanoTimestamp();
359+
self.mu.lock();
360+
if (now >= self.next_head_refresh_ns) {
361+
self.next_head_refresh_ns = now + HEAD_REFRESH_PERIOD_NS;
362+
do_refresh = true;
363+
}
364+
self.mu.unlock();
365+
366+
if (do_refresh) self.refreshSysupdatesHead();
367+
344368
self.mu.lock();
345369
defer self.mu.unlock();
346370

347371
if (self.want_settings) {
348-
const ok = self.conf.safeReadOnly(struct {
349-
fn f(conf: Config.Data, static: Config.StaticData) bool {
350-
const msg: comm.Message.Settings = .{
372+
const head_override: ?[]const u8 = self.sysupdates_head;
373+
374+
const msg_opt = self.conf.safeReadOnly(struct {
375+
fn f(conf: Config.Data, static: Config.StaticData) ?comm.Message.Settings {
376+
return .{
351377
.hostname = static.hostname,
352378
.slock_enabled = conf.slock != null,
353379
.sysupdates = .{
@@ -357,16 +383,23 @@ fn mainThreadLoopCycle(self: *Daemon) !void {
357383
.master => .stable,
358384
.disabled => .disabled,
359385
},
386+
.head = conf.head, // temporary; caller may override
360387
},
361388
};
362-
comm.pipeWrite(.{ .settings = msg }) catch |err| {
363-
logger.err("{}", .{err});
364-
return false;
365-
};
366-
return true;
367389
}
368390
}.f);
369-
self.want_settings = !ok;
391+
392+
if (msg_opt) |msg_const| {
393+
var msg = msg_const;
394+
msg.sysupdates.head = if (head_override) |h| h else msg.sysupdates.head;
395+
396+
var ok = true;
397+
comm.pipeWrite(.{ .settings = msg }) catch |err| {
398+
logger.err("{}", .{err});
399+
ok = false;
400+
};
401+
self.want_settings = !ok;
402+
}
370403
}
371404

372405
// network stats
@@ -539,6 +572,12 @@ fn commThreadLoop(self: *Daemon) void {
539572
.unlock_screen => |pincode| {
540573
self.unlockScreen(pincode) catch |err| logger.err("unlockScreen: {!}", .{err});
541574
},
575+
.get_settings => {
576+
self.refreshSysupdatesHead();
577+
self.mu.lock();
578+
self.want_settings = true;
579+
self.mu.unlock();
580+
},
542581
else => |v| logger.warn("unhandled msg tag {s}", .{@tagName(v)}),
543582
}
544583

@@ -1292,6 +1331,56 @@ fn allocSanitizeNodename(allocator: std.mem.Allocator, name: []const u8) ![]cons
12921331
return allocator.dupe(u8, trimmed);
12931332
}
12941333

1334+
fn readGitHeadOwned(self: *Daemon) ?[]u8 {
1335+
std.fs.accessAbsolute(Config.SYSUPDATES_LOCAL_REPO_PATH, .{}) catch |err| switch (err) {
1336+
error.FileNotFound => return null,
1337+
else => {
1338+
logger.debug("readGitHeadOwned: access check failed: {any}", .{err});
1339+
// Proceed anyway; git command will provide a clearer error if needed
1340+
},
1341+
};
1342+
1343+
const res = std.process.Child.run(.{
1344+
.allocator = self.allocator,
1345+
.argv = &.{ "git", "-C", Config.SYSUPDATES_LOCAL_REPO_PATH, "rev-parse", "--short", "HEAD" },
1346+
}) catch |err| {
1347+
// In tests, git may not exist; keep this non-fatal.
1348+
logger.debug("readGitHeadOwned: git failed: {any}", .{err});
1349+
return null;
1350+
};
1351+
defer {
1352+
self.allocator.free(res.stdout);
1353+
self.allocator.free(res.stderr);
1354+
}
1355+
1356+
if (res.term != .Exited or res.term.Exited != 0) {
1357+
// If it's missing/not a repo, be quiet (or debug), not error.
1358+
logger.debug("readGitHeadOwned: git exit {any}; stderr={s}", .{ res.term, res.stderr });
1359+
return null;
1360+
}
1361+
1362+
const head = std.mem.trim(u8, res.stdout, &std.ascii.whitespace);
1363+
return self.allocator.dupe(u8, head) catch null;
1364+
}
1365+
1366+
fn refreshSysupdatesHead(self: *Daemon) void {
1367+
const new = self.readGitHeadOwned() orelse return;
1368+
1369+
self.mu.lock();
1370+
defer self.mu.unlock();
1371+
1372+
if (self.sysupdates_head) |old| {
1373+
if (std.mem.eql(u8, old, new)) {
1374+
self.allocator.free(new);
1375+
return;
1376+
}
1377+
logger.info("sysupdates head changed: {s} -> {s}", .{ old, new });
1378+
self.allocator.free(old);
1379+
}
1380+
self.sysupdates_head = new;
1381+
self.want_settings = true;
1382+
}
1383+
12951384
test "daemon: start-stop" {
12961385
const t = std.testing;
12971386

src/ngui.zig

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,12 @@ export fn nm_sys_shutdown() void {
141141

142142
export fn nm_tab_settings_active() void {
143143
logger.info("starting wifi scan", .{});
144+
// Force nd to refresh settings (incl sysupdates HEAD) on entering the settings tab.
145+
comm.pipeWrite(comm.Message{ .get_settings = {} }) catch |err|
146+
logger.err("nm_tab_settings_active: get_settings: {any}", .{err});
144147
const msg = comm.Message{ .get_network_report = .{ .scan = true } };
145-
comm.pipeWrite(msg) catch |err| logger.err("nm_tab_settings_active: {any}", .{err});
148+
comm.pipeWrite(msg) catch |err|
149+
logger.err("nm_tab_settings_active: {any}", .{err});
146150
}
147151

148152
export fn nm_request_network_status(t: *lvgl.LvTimer) void {
@@ -415,6 +419,9 @@ pub fn main() anyerror!void {
415419
// initialize global nd/ngui pipe plumbing.
416420
comm.initPipe(gpa, .{ .r = std.io.getStdIn(), .w = std.io.getStdOut() });
417421

422+
comm.pipeWrite(comm.Message{ .get_settings = {} }) catch |err|
423+
logger.err("startup: get_settings: {any}", .{err});
424+
418425
// initalizes display, input driver and finally creates the user interface.
419426
ui.init(.{ .allocator = gpa, .slock = flags.slock }) catch |err| {
420427
logger.err("ui.init: {any}", .{err});

src/test/guiplay.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ fn commWriteThread(gpa: std.mem.Allocator, w: anytype) !void {
218218
const sett: comm.Message.Settings = .{
219219
.slock_enabled = state.slock_pincode != null,
220220
.hostname = state.nodename.val(),
221-
.sysupdates = .{ .url = "", .channel = .edge },
221+
.sysupdates = .{ .url = "", .channel = .edge, .head = "" },
222222
};
223223
comm.write(gpa, w, .{ .settings = sett }) catch |err| {
224224
logger.err("{}", .{err});

0 commit comments

Comments
 (0)