feat: add NTP; fix networking bugs/regressions

This commit is contained in:
2026-07-29 16:32:18 +01:00
parent a288dee7df
commit d99dab45e5
26 changed files with 809 additions and 47 deletions
+1
View File
@@ -163,6 +163,7 @@ extern "C" void _start() {
// ---- Stage 1: Network configuration (non-blocking) ----
run_service("0:/os/dhcp.elf", "dhcp", false);
run_service("0:/os/ntp.elf", "network time", false);
// Printing is an optional install component; skip quietly when absent.
if (service_installed("0:/os/printd.elf"))
+81
View File
@@ -0,0 +1,81 @@
/*
* main.cpp
* MontaukOS Network Time Protocol service
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/config.h>
#include <montauk/ntp.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
static constexpr uint64_t RETRY_INTERVAL_MS =
montauk::ntp::MIN_QUERY_INTERVAL_MS;
static constexpr uint64_t SYNC_INTERVAL_MS = 60ULL * 60ULL * 1000ULL;
static constexpr uint64_t CONFIG_POLL_MS = 5000;
static void log(const char* message) {
montauk::print("ntp: ");
montauk::print(message);
montauk::print("\n");
}
static bool load_settings(char* server, int server_cap) {
auto cfg = montauk::config::load("ntp");
bool enabled = cfg.get_bool("ntp.enabled", true);
montauk::strncpy(server, cfg.get_string("ntp.server", "pool.ntp.org"),
server_cap);
cfg.destroy();
if (!server[0])
montauk::strncpy(server, "pool.ntp.org", server_cap);
return enabled;
}
extern "C" void _start() {
log("service started");
char active_server[128] = {};
bool was_enabled = false;
uint64_t next_attempt = 0;
for (;;) {
char server[128];
bool enabled = load_settings(server, sizeof(server));
bool server_changed = !montauk::streq(server, active_server);
if (server_changed) {
montauk::strncpy(active_server, server, sizeof(active_server));
next_attempt = 0;
}
if (!enabled) {
was_enabled = false;
next_attempt = 0;
montauk::sleep_ms(CONFIG_POLL_MS);
continue;
}
if (!was_enabled) {
was_enabled = true;
next_attempt = 0;
}
uint64_t now = montauk::get_milliseconds();
if (next_attempt != 0 && now < next_attempt) {
montauk::sleep_ms(CONFIG_POLL_MS);
continue;
}
montauk::abi::NetCfg net = {};
montauk::get_netcfg(&net);
if (net.ipAddress == 0 || net.dnsServer == 0) {
next_attempt = now + RETRY_INTERVAL_MS;
montauk::sleep_ms(CONFIG_POLL_MS);
continue;
}
int result = montauk::ntp::synchronize(server);
log(montauk::ntp::result_string(result));
next_attempt = montauk::get_milliseconds() +
(result == montauk::ntp::OK ? SYNC_INTERVAL_MS : RETRY_INTERVAL_MS);
montauk::sleep_ms(CONFIG_POLL_MS);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
# Makefile for timezone (standalone Time Zone configuration app) on MontaukOS
# Makefile for timezone (standalone Time configuration app) on MontaukOS
# Copyright (c) 2026 Daniel Hammer
MAKEFLAGS += -rR
+271 -36
View File
@@ -1,6 +1,6 @@
/*
* main.cpp
* MontaukOS Time Zone configuration app
* MontaukOS Time configuration app
* Copyright (c) 2026 Daniel Hammer
*/
@@ -8,6 +8,7 @@
#include <montauk/heap.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
#include <montauk/ntp.h>
#include <gui/gui.hpp>
#include <gui/canvas.hpp>
#include <gui/mtk.hpp>
@@ -23,13 +24,25 @@ using namespace gui;
static constexpr int INIT_W = 640;
static constexpr int INIT_H = 460;
static constexpr int HEADER_H = 14;
static constexpr int TAB_H = 36;
static constexpr int FOOTER_H = 58;
static constexpr int PAD = 16;
static constexpr int GAP = 12;
static constexpr int LABEL_H = 24;
static constexpr int ROW_H = 30;
static constexpr int MAX_COUNTRIES = 320;
static constexpr int NTP_SERVER_CAP = 128;
enum Tab {
TAB_TIME_ZONES = 0,
TAB_NTP = 1,
TAB_COUNT = 2,
};
static const char* const kTabLabels[TAB_COUNT] = {
"Time Zones",
"NTP",
};
struct CountryEntry {
char key[64]; // Full TOML table key, e.g. countries.NO
@@ -43,6 +56,7 @@ struct CountryEntry {
};
static WsWindow g_win;
static Tab g_tab = TAB_TIME_ZONES;
static montauk::toml::Doc g_data;
static bool g_data_loaded = false;
static CountryEntry g_countries[MAX_COUNTRIES];
@@ -60,10 +74,20 @@ static int g_focus_pane = 0; // 0=country list, 1=zone list
static int g_mouse_x = -1;
static int g_mouse_y = -1;
static bool g_dirty = false;
static char g_ntp_server[NTP_SERVER_CAP] = "pool.ntp.org";
static char g_saved_ntp_server[NTP_SERVER_CAP] = "pool.ntp.org";
static mtk::TextInputState g_ntp_input = {};
static bool g_ntp_enabled = true;
static bool g_saved_ntp_enabled = true;
static bool g_ntp_dirty = false;
static bool g_ntp_syncing = false;
static uint64_t g_last_clock_render = 0;
static char g_status[160] = {};
static uint64_t g_status_time = 0;
static Color g_accent = colors::ACCENT;
static void render();
static void safe_copy(char* dst, int cap, const char* src) {
montauk::strncpy(dst, src ? src : "", cap);
}
@@ -217,8 +241,8 @@ static void clamp_scrolls() {
Rect country_list = {};
Rect zone_list = {};
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
int list_y = HEADER_H + LABEL_H;
int list_h = gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H);
int list_y = TAB_H + LABEL_H;
int list_h = gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H);
country_list = {PAD, list_y, left_w, list_h};
zone_list = {PAD + left_w + GAP, list_y,
g_win.width - PAD * 2 - GAP - left_w, list_h};
@@ -235,8 +259,8 @@ static void clamp_scrolls() {
}
static void ensure_country_visible() {
Rect list = {PAD, HEADER_H + LABEL_H, 250,
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
Rect list = {PAD, TAB_H + LABEL_H, 250,
gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
int rows = visible_rows(list);
if (g_selected_country < g_country_scroll)
g_country_scroll = g_selected_country;
@@ -246,9 +270,9 @@ static void ensure_country_visible() {
static void ensure_zone_visible() {
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
Rect list = {PAD + left_w + GAP, HEADER_H + LABEL_H,
Rect list = {PAD + left_w + GAP, TAB_H + LABEL_H,
g_win.width - PAD * 2 - GAP - left_w,
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
int rows = visible_rows(list);
if (g_selected_zone < g_zone_scroll)
g_zone_scroll = g_selected_zone;
@@ -357,6 +381,73 @@ static void load_saved_selection() {
clamp_scrolls();
}
static void load_ntp_settings() {
auto cfg = montauk::config::load("ntp");
g_ntp_enabled = cfg.get_bool("ntp.enabled", true);
safe_copy(g_ntp_server, sizeof(g_ntp_server),
cfg.get_string("ntp.server", "pool.ntp.org"));
cfg.destroy();
if (!g_ntp_server[0])
safe_copy(g_ntp_server, sizeof(g_ntp_server), "pool.ntp.org");
safe_copy(g_saved_ntp_server, sizeof(g_saved_ntp_server), g_ntp_server);
g_saved_ntp_enabled = g_ntp_enabled;
g_ntp_dirty = false;
mtk::text_input_reset(g_ntp_input, montauk::slen(g_ntp_server));
}
static bool ntp_settings_changed() {
return g_ntp_enabled != g_saved_ntp_enabled ||
!montauk::streq(g_ntp_server, g_saved_ntp_server);
}
static void save_ntp_settings() {
if (!g_ntp_server[0]) {
set_status("Enter an NTP server");
return;
}
auto cfg = montauk::config::load("ntp");
montauk::config::set_bool(&cfg, "ntp.enabled", g_ntp_enabled);
montauk::config::set_string(&cfg, "ntp.server", g_ntp_server);
int result = montauk::config::save("ntp", &cfg);
cfg.destroy();
if (result < 0) {
set_status("Could not save NTP settings");
return;
}
safe_copy(g_saved_ntp_server, sizeof(g_saved_ntp_server), g_ntp_server);
g_saved_ntp_enabled = g_ntp_enabled;
g_ntp_dirty = false;
set_status("NTP settings saved");
}
static void revert_ntp_settings() {
safe_copy(g_ntp_server, sizeof(g_ntp_server), g_saved_ntp_server);
g_ntp_enabled = g_saved_ntp_enabled;
g_ntp_dirty = false;
mtk::text_input_reset(g_ntp_input, montauk::slen(g_ntp_server));
set_status("NTP changes reverted");
}
static void ntp_progress(const char* message) {
set_status(message);
render();
}
static void synchronize_ntp() {
if (g_ntp_syncing) return;
if (!g_ntp_server[0]) {
set_status("Enter an NTP server");
return;
}
g_ntp_syncing = true;
int result = montauk::ntp::synchronize(g_ntp_server, 5000, nullptr,
ntp_progress);
g_ntp_syncing = false;
set_status(montauk::ntp::result_string(result));
}
static void fit_text(char* out, int cap, const char* text, int max_w) {
safe_copy(out, cap, text);
if (text_width(out) <= max_w) return;
@@ -482,8 +573,8 @@ static void revert_selection() {
static Rect country_list_rect() {
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
return {PAD, HEADER_H + LABEL_H, left_w,
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
return {PAD, TAB_H + LABEL_H, left_w,
gui_max(g_win.height - TAB_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
}
static Rect zone_list_rect() {
@@ -500,6 +591,30 @@ static Rect revert_button_rect() {
return {g_win.width - PAD - 104 - GAP - 92, g_win.height - FOOTER_H + 14, 92, 30};
}
static Rect tab_bar_rect() {
return {0, 0, g_win.width, TAB_H};
}
static int ntp_server_field_y() {
return TAB_H + 24;
}
static Rect ntp_server_input_rect() {
return mtk::labeled_text_input_rect(PAD, ntp_server_field_y(),
g_win.width - PAD * 2,
app_theme(), 34);
}
static Rect ntp_enabled_rect() {
Rect input = ntp_server_input_rect();
return {PAD, input.y + input.h + 18, g_win.width - PAD * 2, 28};
}
static Rect ntp_sync_button_rect() {
Rect revert = revert_button_rect();
return {revert.x - GAP - 112, revert.y, 112, revert.h};
}
static bool mouse_in_rect(const Rect& rect) {
return rect.contains(g_mouse_x, g_mouse_y);
}
@@ -644,50 +759,92 @@ static void draw_details(Canvas& canvas, const mtk::Theme& theme) {
}
}
static void draw_ntp_tab(Canvas& canvas, const mtk::Theme& theme) {
int field_y = ntp_server_field_y();
mtk::draw_labeled_text_field(canvas, PAD, field_y, g_win.width - PAD * 2,
"NTP Server", g_ntp_server,
g_ntp_input.cursor, true, false, theme, 34,
g_ntp_input.selection_anchor);
Rect enabled = ntp_enabled_rect();
mtk::draw_checkbox(canvas, enabled, "Synchronize time automatically",
mtk::check_state(g_ntp_enabled), theme, true,
enabled.contains(g_mouse_x, g_mouse_y));
canvas.text(PAD, enabled.y + enabled.h + 10,
"MontaukOS will synchronize at startup and periodically while enabled.",
theme.text_subtle);
montauk::abi::DateTime now = {};
montauk::gettime(&now);
char current[96];
snprintf(current, sizeof(current), "Current system time: %04u-%02u-%02u %02u:%02u:%02u",
(unsigned)now.Year, (unsigned)now.Month, (unsigned)now.Day,
(unsigned)now.Hour, (unsigned)now.Minute, (unsigned)now.Second);
int explanation_y = enabled.y + enabled.h + 10;
canvas.text(PAD, explanation_y + system_font_height() + 24,
current, theme.text);
}
static void render() {
mtk::StandaloneHost host(&g_win);
Canvas canvas = host.canvas();
mtk::Theme theme = app_theme();
canvas.fill(theme.window_bg);
mtk::draw_tab_bar(canvas, tab_bar_rect(), kTabLabels, TAB_COUNT,
(int)g_tab, theme);
if (g_country_count <= 0) {
canvas.text(PAD, HEADER_H + 24,
"No time zone data was found in 0:/config/timezonedata.toml",
theme.danger);
host.present();
return;
if (g_tab == TAB_TIME_ZONES) {
if (g_country_count <= 0) {
canvas.text(PAD, TAB_H + 24,
"No time zone data was found in 0:/config/timezonedata.toml",
theme.danger);
} else {
Rect country = country_list_rect();
Rect zone = zone_list_rect();
canvas.text(country.x, TAB_H + 6, "Country", theme.text_subtle);
canvas.text(zone.x, TAB_H + 6, "Time Zone", theme.text_subtle);
draw_country_list(canvas, country, theme);
draw_zone_list(canvas, zone, theme);
draw_details(canvas, theme);
}
} else {
draw_ntp_tab(canvas, theme);
}
Rect country = country_list_rect();
Rect zone = zone_list_rect();
canvas.text(country.x, HEADER_H + 6, "Country", theme.text_subtle);
canvas.text(zone.x, HEADER_H + 6, "Time Zone", theme.text_subtle);
draw_country_list(canvas, country, theme);
draw_zone_list(canvas, zone, theme);
draw_details(canvas, theme);
Rect footer = {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H};
canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface);
mtk::draw_separator(canvas, 0, footer.y, g_win.width, theme);
const char* footer_text = status_visible()
? g_status
: (g_dirty ? "Unsaved time zone selection" : "Selection saved");
int max_footer_text = revert_button_rect().x - PAD - GAP;
bool dirty = g_tab == TAB_TIME_ZONES ? g_dirty : g_ntp_dirty;
const char* footer_text = status_visible() ? g_status :
(g_tab == TAB_TIME_ZONES
? (g_dirty ? "Unsaved time zone selection" : "Selection saved")
: (g_ntp_dirty ? "Unsaved NTP changes" : "NTP settings ready"));
int max_footer_text = (g_tab == TAB_NTP ? ntp_sync_button_rect().x
: revert_button_rect().x) - PAD - GAP;
char footer_label[160];
fit_text(footer_label, sizeof(footer_label), footer_text, max_footer_text);
canvas.text(PAD, footer.y + (FOOTER_H - system_font_height()) / 2,
footer_label, g_dirty ? theme.text : theme.text_subtle);
footer_label, dirty ? theme.text : theme.text_subtle);
Rect revert = revert_button_rect();
Rect apply = apply_button_rect();
mtk::draw_button(canvas, revert, "Revert", mtk::BUTTON_SECONDARY,
button_state(revert, g_dirty), theme);
button_state(revert, dirty), theme);
mtk::draw_button(canvas, apply, "Apply", mtk::BUTTON_PRIMARY,
button_state(apply, g_dirty), theme);
button_state(apply, dirty), theme);
if (g_tab == TAB_NTP) {
Rect sync = ntp_sync_button_rect();
mtk::draw_button(canvas, sync, g_ntp_syncing ? "Syncing..." : "Sync Now",
mtk::BUTTON_PRIMARY,
button_state(sync, !g_ntp_syncing), theme);
mtk::draw_text_input_context_menu(
canvas, g_ntp_input, theme,
mtk::text_input_has_selection(g_ntp_input, g_ntp_server,
(int)sizeof(g_ntp_server)));
}
host.present();
}
@@ -698,6 +855,55 @@ static bool handle_mouse(const montauk::abi::WinEvent& ev) {
g_mouse_x = ev.mouse.x;
g_mouse_y = ev.mouse.y;
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
bool right_clicked = (ev.mouse.buttons & 2) && !(ev.mouse.prev_buttons & 2);
if (clicked) {
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT,
g_mouse_x, g_mouse_y);
if (tab >= 0) {
g_tab = (Tab)tab;
mtk::context_menu_close(g_ntp_input.context);
return true;
}
}
if (g_tab == TAB_NTP) {
Rect input = ntp_server_input_rect();
if (g_ntp_input.context.open || g_ntp_input.dragging ||
input.contains(g_mouse_x, g_mouse_y)) {
int result = mtk::text_input_handle_mouse(
g_ntp_input, input, g_ntp_server, (int)sizeof(g_ntp_server),
g_mouse_x, g_mouse_y, ev.mouse.buttons, ev.mouse.prev_buttons,
g_win.width, g_win.height, true);
if (result & mtk::TEXT_INPUT_CHANGED) {
g_ntp_dirty = ntp_settings_changed();
}
if (result != mtk::TEXT_INPUT_NONE) return true;
}
if (!clicked && !right_clicked)
return true;
if (clicked && ntp_enabled_rect().contains(g_mouse_x, g_mouse_y)) {
g_ntp_enabled = !g_ntp_enabled;
g_ntp_dirty = ntp_settings_changed();
return true;
}
if (clicked && !g_ntp_syncing &&
ntp_sync_button_rect().contains(g_mouse_x, g_mouse_y)) {
synchronize_ntp();
return true;
}
if (clicked && revert_button_rect().contains(g_mouse_x, g_mouse_y)) {
if (g_ntp_dirty) revert_ntp_settings();
return true;
}
if (clicked && apply_button_rect().contains(g_mouse_x, g_mouse_y)) {
if (g_ntp_dirty) save_ntp_settings();
return true;
}
return true;
}
Rect country = country_list_rect();
Rect zone = zone_list_rect();
Rect revert = revert_button_rect();
@@ -745,7 +951,6 @@ static bool handle_mouse(const montauk::abi::WinEvent& ev) {
}
}
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
if (!clicked) return redraw;
if (!csb_track.empty() && csb_track.contains(g_mouse_x, g_mouse_y)) {
@@ -804,7 +1009,31 @@ static bool handle_mouse(const montauk::abi::WinEvent& ev) {
}
static bool handle_key(const montauk::abi::KeyEvent& key) {
if (!key.pressed || g_country_count <= 0) return false;
if (!key.pressed) return false;
if (key.ascii == '\t') {
g_tab = g_tab == TAB_TIME_ZONES ? TAB_NTP : TAB_TIME_ZONES;
mtk::context_menu_close(g_ntp_input.context);
return true;
}
if (g_tab == TAB_NTP) {
if (key.ascii == '\n' || key.ascii == '\r') {
synchronize_ntp();
return true;
}
if (key.ascii == '\033') {
if (g_ntp_dirty) revert_ntp_settings();
return true;
}
int result = mtk::text_input_key(g_ntp_input, g_ntp_server,
(int)sizeof(g_ntp_server), key);
if (result & mtk::TEXT_INPUT_CHANGED)
g_ntp_dirty = ntp_settings_changed();
return (result & mtk::TEXT_INPUT_CONSUMED) != 0;
}
if (g_country_count <= 0) return false;
if (key.ascii == '\n' || key.ascii == '\r') {
if (g_dirty) save_selection();
@@ -851,10 +1080,11 @@ extern "C" void _start() {
load_accent();
load_timezone_data();
if (!g_win.create("Time Zone", INIT_W, INIT_H))
if (!g_win.create("Time", INIT_W, INIT_H))
montauk::exit(1);
load_saved_selection();
load_ntp_settings();
render();
while (g_win.id >= 0 && !g_win.closed) {
@@ -864,6 +1094,11 @@ extern "C" void _start() {
if (r < 0) break;
if (r == 0) {
uint64_t now = montauk::get_milliseconds();
if (g_tab == TAB_NTP && now - g_last_clock_render >= 1000) {
g_last_clock_render = now;
render();
}
montauk::sleep_ms(16);
continue;
}
+1 -1
View File
@@ -1,6 +1,6 @@
[app]
id = "timezone"
name = "Time Zone"
name = "Time"
binary = "timezone.elf"
icon = "preferences-system-time.svg"