From 3d5dd856a9b443078c203615bd66b4576d86ee6f Mon Sep 17 00:00:00 2001 From: "Daniel K. O" Date: Fri, 20 Oct 2023 15:02:58 -0300 Subject: [PATCH] Timezone detection, Y2k36 handling, code refactoring. (#18) * created upstream branch --HG-- branch : upstream * backported changes to upstream --HG-- branch : upstream * fixed a typo, now the sync option should work properly --HG-- branch : upstream * backported changes to upstream --HG-- branch : upstream * Backported to upstream. --HG-- branch : upstream * Backported to upstream. --HG-- branch : upstream --------- Co-authored-by: Daniel K. O. (dkosmari) --- Makefile | 24 +- source/cfg.cpp | 64 +++ source/cfg.hpp | 38 ++ source/config_screen.cpp | 87 ++++ source/config_screen.hpp | 15 + source/core.cpp | 354 ++++++++++++++ source/core.hpp | 23 + source/http_client.cpp | 164 +++++++ source/http_client.hpp | 15 + source/limited_async.hpp | 76 +++ source/log.cpp | 62 +++ source/log.hpp | 20 + source/main.cpp | 933 +----------------------------------- source/nintendo_glyphs.hpp | 159 ++++++ source/ntp.cpp | 152 ++++++ source/ntp.hpp | 69 ++- source/preview_screen.cpp | 192 ++++++++ source/preview_screen.hpp | 33 ++ source/utc.cpp | 30 ++ source/utc.hpp | 21 + source/utils.cpp | 290 +++++++++++ source/utils.hpp | 108 +++++ source/wupsxx/bool_item.cpp | 6 +- source/wupsxx/int_item.cpp | 26 +- source/wupsxx/storage.hpp | 2 + source/wupsxx/text_item.cpp | 2 + 26 files changed, 2012 insertions(+), 953 deletions(-) create mode 100644 source/cfg.cpp create mode 100644 source/cfg.hpp create mode 100644 source/config_screen.cpp create mode 100644 source/config_screen.hpp create mode 100644 source/core.cpp create mode 100644 source/core.hpp create mode 100644 source/http_client.cpp create mode 100644 source/http_client.hpp create mode 100644 source/limited_async.hpp create mode 100644 source/log.cpp create mode 100644 source/log.hpp create mode 100644 source/nintendo_glyphs.hpp create mode 100644 source/ntp.cpp create mode 100644 source/preview_screen.cpp create mode 100644 source/preview_screen.hpp create mode 100644 source/utc.cpp create mode 100644 source/utc.hpp create mode 100644 source/utils.cpp create mode 100644 source/utils.hpp diff --git a/Makefile b/Makefile index b7af3cf..396baee 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ BUILD := build SOURCES := source source/wupsxx DATA := data INCLUDES := source +PLUGIN_NAME := "Wii U Time Sync" # Be verbose by default. V ?= 1 @@ -37,13 +38,23 @@ OPTFLAGS := -O2 -fipa-pta -ffunction-sections CFLAGS := $(WARN_FLAGS) $(OPTFLAGS) $(MACHDEP) -CPPFLAGS := $(INCLUDE) -D__WIIU__ -D__WUT__ -D__WUPS__ - CXXFLAGS := $(CFLAGS) -std=c++23 +# Note: INCLUDE will be defined later, so CPPFLAGS has to be of the recursive flavor. +CPPFLAGS = $(INCLUDE) \ + -D__WIIU__ \ + -D__WUT__ \ + -D__WUPS__ \ + -DPLUGIN_NAME=\"$(PLUGIN_NAME)\" + ASFLAGS := -g $(ARCH) -LDFLAGS = -g $(ARCH) $(RPXSPECS) -Wl,-Map,$(notdir $*.map) $(WUPSSPECS) +LDFLAGS = -g \ + $(ARCH) \ + $(RPXSPECS) \ + $(WUPSSPECS) \ + -Wl,-Map,$(notdir $*.map) \ + $(OPTFLAGS) LIBS := -lnotifications -lwups -lwut @@ -51,7 +62,7 @@ LIBS := -lnotifications -lwups -lwut # list of directories containing libraries, this must be the top level # containing include and lib #------------------------------------------------------------------------------- -LIBDIRS := $(PORTLIBS) $(WUMS_ROOT) $(WUPS_ROOT) $(WUT_ROOT) +LIBDIRS := $(WUMS_ROOT) $(WUPS_ROOT) $(WUT_ROOT) #------------------------------------------------------------------------------- # no real need to edit anything past this point unless you need to add additional @@ -93,12 +104,11 @@ export OFILES := $(OFILES_BIN) $(OFILES_SRC) export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ - $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ - -I$(CURDIR)/$(BUILD) + $(foreach dir,$(LIBDIRS),-I$(dir)/include) export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) -.PHONY: $(BUILD) clean all upload +.PHONY: $(BUILD) clean all #------------------------------------------------------------------------------- all: $(BUILD) diff --git a/source/cfg.cpp b/source/cfg.cpp new file mode 100644 index 0000000..b5cfe83 --- /dev/null +++ b/source/cfg.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +#include "cfg.hpp" + +#include "utc.hpp" +#include "wupsxx/storage.hpp" + + +namespace cfg { + + namespace key { + const char* hours = "hours"; + const char* minutes = "minutes"; + const char* msg_duration = "msg_duration"; + const char* notify = "notify"; + const char* server = "server"; + const char* sync = "sync"; + const char* tolerance = "tolerance"; + } + + + int hours = 0; + int minutes = 0; + int msg_duration = 5; + bool notify = true; + std::string server = "pool.ntp.org"; + bool sync = false; + int tolerance = 250; + + + template + void + load_or_init(const std::string& key, + T& variable) + { + auto val = wups::load(key); + if (!val) + wups::store(key, variable); + else + variable = *val; + } + + + void + load() + { + load_or_init(key::hours, hours); + load_or_init(key::minutes, minutes); + load_or_init(key::msg_duration, msg_duration); + load_or_init(key::notify, notify); + load_or_init(key::server, server); + load_or_init(key::sync, sync); + load_or_init(key::tolerance, tolerance); + } + + + void + update_utc_offset() + { + double offset_seconds = (hours * 60.0 + minutes) * 60.0; + utc::timezone_offset = offset_seconds; + } + +} // namespace cfg diff --git a/source/cfg.hpp b/source/cfg.hpp new file mode 100644 index 0000000..26b50c8 --- /dev/null +++ b/source/cfg.hpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +#ifndef CFG_HPP +#define CFG_HPP + +#include + + +namespace cfg { + + namespace key { + extern const char* hours; + extern const char* minutes; + extern const char* msg_duration; + extern const char* notify; + extern const char* server; + extern const char* sync; + extern const char* tolerance; + } + + extern int hours; + extern int minutes; + extern int msg_duration; + extern bool notify; + extern std::string server; + extern bool sync; + extern int tolerance; + + + void load(); + + + // send the hours and minutes variables to the utc module + void update_utc_offset(); + +} + +#endif diff --git a/source/config_screen.cpp b/source/config_screen.cpp new file mode 100644 index 0000000..10e4634 --- /dev/null +++ b/source/config_screen.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT + +#include // make_unique() + +#include "wupsxx/bool_item.hpp" +#include "wupsxx/int_item.hpp" +#include "wupsxx/text_item.hpp" + +#include "config_screen.hpp" + +#include "cfg.hpp" +#include "http_client.hpp" +#include "nintendo_glyphs.hpp" +#include "utils.hpp" + + +using wups::bool_item; +using wups::int_item; +using wups::text_item; +using std::make_unique; + +using namespace std::literals; + + +struct timezone_item : wups::text_item { + + timezone_item() : + wups::text_item{"", + "Detect Timezone (press " NIN_GLYPH_BTN_A ")", + "Using http://ip-api.com"} + {} + + + void + on_button_pressed(WUPSConfigButtons buttons) + override + { + text_item::on_button_pressed(buttons); + + if (buttons & WUPS_CONFIG_BUTTON_A) + query_timezone(); + } + + + void + query_timezone() + try { + std::string tz = http::get("http://ip-api.com/line/?fields=timezone,offset"); + auto tokens = utils::split(tz, " \r\n"); + if (tokens.size() != 2) + throw std::runtime_error{"Could not parse response from \"ip-api.com\"."}; + + int tz_offset = std::stoi(tokens[1]); + text = tokens[0]; + + cfg::hours = tz_offset / (60 * 60); + cfg::minutes = tz_offset % (60 * 60) / 60; + if (cfg::minutes < 0) { + cfg::minutes += 60; + --cfg::hours; + } + } + catch (std::exception& e) { + text = "Error: "s + e.what(); + } + +}; + + +config_screen::config_screen() : + wups::category{"Configuration"} +{ + add(make_unique(cfg::key::sync, "Syncing Enabled", cfg::sync)); + add(make_unique(cfg::key::notify, "Show Notifications", cfg::notify)); + add(make_unique(cfg::key::msg_duration, "Notification Duration (seconds)", + cfg::msg_duration, 0, 30)); + add(make_unique(cfg::key::hours, "Hours Offset", cfg::hours, -12, 14)); + add(make_unique(cfg::key::minutes, "Minutes Offset", cfg::minutes, 0, 59)); + + add(make_unique()); + + add(make_unique(cfg::key::tolerance, "Tolerance (milliseconds)", + cfg::tolerance, 0, 5000)); + + // show current NTP server address, no way to change it. + add(make_unique(cfg::key::server, "NTP servers", cfg::server)); +} diff --git a/source/config_screen.hpp b/source/config_screen.hpp new file mode 100644 index 0000000..40d2013 --- /dev/null +++ b/source/config_screen.hpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +#ifndef CONFIG_SCREEN_HPP +#define CONFIG_SCREEN_HPP + +#include "wupsxx/category.hpp" + + +struct config_screen : wups::category { + + config_screen(); + +}; + +#endif diff --git a/source/core.cpp b/source/core.cpp new file mode 100644 index 0000000..1ccd6d2 --- /dev/null +++ b/source/core.cpp @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: MIT + +#include +#include +#include // fabs() +#include +#include // accumulate() +#include // views::zip() +#include +#include +#include +#include +#include + +// WUT/WUPS headers +#include +#include + +// unix headers +#include // select() +#include // connect(), send(), recv() + +#include "core.hpp" + +#include "cfg.hpp" +#include "limited_async.hpp" +#include "log.hpp" +#include "ntp.hpp" +#include "utc.hpp" +#include "utils.hpp" + + +using namespace std::literals; + + +extern "C" int32_t CCRSysSetSystemTime(OSTime time); // from nn_ccr.rpl +extern "C" BOOL __OSSetAbsoluteSystemTime(OSTime time); // from coreinit.rpl + + +std::counting_semaphore<> async_limit{5}; // limit to 5 threads + + +namespace { + + // Difference from NTP (1900) to Wii U (2000) epochs. + // There are 24 leap years in this period. + constexpr double seconds_per_day = 24 * 60 * 60; + constexpr double epoch_diff = seconds_per_day * (100 * 365 + 24); + + + // Wii U -> NTP epoch. + ntp::timestamp + to_ntp(utc::timestamp t) + { + return ntp::timestamp{t.value + epoch_diff}; + } + + + // NTP -> Wii U epoch. + utc::timestamp + to_utc(ntp::timestamp t) + { + return utc::timestamp{static_cast(t) - epoch_diff}; + } + + + + std::string + ticks_to_string(OSTime wt) + { + OSCalendarTime cal; + OSTicksToCalendarTime(wt, &cal); + char buffer[256]; + std::snprintf(buffer, sizeof buffer, + "%04d-%02d-%02d %02d:%02d:%02d.%03d", + cal.tm_year, cal.tm_mon + 1, cal.tm_mday, + cal.tm_hour, cal.tm_min, cal.tm_sec, cal.tm_msec); + return buffer; + } + + + std::string + to_string(ntp::timestamp t) + { + auto ut = to_utc(t); + OSTime ticks = ut.value * OSTimerClockSpeed; + return ticks_to_string(ticks); + } + +} + + +namespace core { + + // Note: hardcoded for IPv4, the Wii U doesn't have IPv6. + std::pair + ntp_query(struct sockaddr_in address) + { + using std::to_string; + + utils::socket_guard s{PF_INET, SOCK_DGRAM, IPPROTO_UDP}; + + connect(s.fd, reinterpret_cast(&address), sizeof address); + + ntp::packet packet; + packet.version(4); + packet.mode(ntp::packet::mode_flag::client); + + + unsigned num_send_tries = 0; + try_again_send: + auto t1 = to_ntp(utc::now()); + packet.transmit_time = t1; + + if (send(s.fd, &packet, sizeof packet, 0) == -1) { + int e = errno; + if (e != ENOMEM) + throw std::runtime_error{"send() failed: "s + + utils::errno_to_string(e)}; + if (++num_send_tries < 4) { + std::this_thread::sleep_for(100ms); + goto try_again_send; + } else + throw std::runtime_error{"No resources for send(), too many retries!"}; + } + + + struct timeval timeout = { 4, 0 }; + fd_set read_set; + unsigned num_select_tries = 0; + try_again_select: + FD_ZERO(&read_set); + FD_SET(s.fd, &read_set); + + if (select(s.fd + 1, &read_set, nullptr, nullptr, &timeout) == -1) { + // Wii U's OS can only handle 16 concurrent select() calls, + // so we may need to try again later. + int e = errno; + if (e != ENOMEM) + throw std::runtime_error{"select() failed: "s + + utils::errno_to_string(e)}; + if (++num_select_tries < 4) { + std::this_thread::sleep_for(10ms); + goto try_again_select; + } else + throw std::runtime_error{"No resources for select(), too many retries!"}; + } + + if (!FD_ISSET(s.fd, &read_set)) + throw std::runtime_error{"Timeout reached!"}; + + // Measure the arrival time as soon as possible. + auto t4 = to_ntp(utc::now()); + + if (recv(s.fd, &packet, sizeof packet, 0) < 48) + throw std::runtime_error{"Invalid NTP response!"}; + + auto v = packet.version(); + if (v < 3 || v > 4) + throw std::runtime_error{"Unsupported NTP version: "s + to_string(v)}; + + auto m = packet.mode(); + if (m != ntp::packet::mode_flag::server) + throw std::runtime_error{"Invalid NTP packet mode: "s + to_string(m)}; + + auto l = packet.leap(); + if (l == ntp::packet::leap_flag::unknown) + throw std::runtime_error{"Unknown value for leap flag."}; + + ntp::timestamp t1_received = packet.origin_time; + if (t1 != t1_received) + throw std::runtime_error{"NTP response mismatch: ["s + + ::to_string(t1) + "] vs ["s + + ::to_string(t1_received) + "]"s}; + + // when our request arrived at the server + auto t2 = packet.receive_time; + // when the server sent out a response + auto t3 = packet.transmit_time; + + // Zero is not a valid timestamp. + if (!t2 || !t3) + throw std::runtime_error{"NTP response has invalid timestamps."}; + + /* + * We do all calculations in double precision to never worry about overflows. Since + * double precision has 53 mantissa bits, we're guaranteed to have 53 - 32 = 21 + * fractional bits in Era 0, and 20 fractional bits in Era 1 (starting in 2036). We + * still have sub-microsecond resolution. + */ + double d1 = static_cast(t1); + double d2 = static_cast(t2); + double d3 = static_cast(t3); + double d4 = static_cast(t4); + + // Detect the wraparound that will happen at the end of Era 0. + if (d4 < d1) + d4 += 0x1.0p32; // d4 += 2^32 + if (d3 < d2) + d3 += 0x1.0p32; // d3 += 2^32 + + double roundtrip = (d4 - d1) - (d3 - d2); + double latency = roundtrip / 2; + + // t4 + correction = t3 + latency + double correction = d3 + latency - d4; + + /* + * If the local clock enters Era 1 ahead of NTP, we get a massive positive correction + * because the local clock wrapped back to zero. + */ + if (correction > 0x1.0p31) // if correcting more than 68 years forward + correction -= 0x1.0p32; + + /* + * If NTP enters Era 1 ahead of the local clock, we get a massive negative correction + * because NTP wrapped back to zero. + */ + if (correction < -0x1.0p31) // if correcting more than 68 years backward + correction += 0x1.0p32; + + return { correction, latency }; + } + + + + bool + apply_clock_correction(double correction) + { + OSTime correction_ticks = correction * OSTimerClockSpeed; + + nn::pdm::NotifySetTimeBeginEvent(); + + OSTime now = OSGetTime(); + OSTime corrected = now + correction_ticks; + + if (CCRSysSetSystemTime(corrected)) { + nn::pdm::NotifySetTimeEndEvent(); + return false; + } + + bool res = __OSSetAbsoluteSystemTime(corrected); + + nn::pdm::NotifySetTimeEndEvent(); + + return res; + } + + + + void + sync_clock() + { + using utils::seconds_to_human; + + if (!cfg::sync) + return; + + static std::atomic executing = false; + + utils::exec_guard guard{executing}; + if (!guard.guarded) { + // Another thread is already executing this function. + report_info("Skipping NTP task: already in progress."); + return; + } + + cfg::update_utc_offset(); + + std::vector servers = utils::split(cfg::server, " \t,;"); + + utils::addrinfo_query query = { + .family = AF_INET, + .socktype = SOCK_DGRAM, + .protocol = IPPROTO_UDP + }; + + // First, resolve all the names, in parallel. + // Some IP addresses might be duplicated when we use *.pool.ntp.org. + std::set addresses; + { + using info_vec = std::vector; + std::vector> futures(servers.size()); + + // Launch DNS queries asynchronously. + for (auto [fut, server] : std::views::zip(futures, servers)) + fut = limited_async(utils::get_address_info, server, "123", query); + + // Collect all future results. + for (auto& fut : futures) + try { + for (auto info : fut.get()) + addresses.insert(info.address); + } + catch (std::exception& e) { + report_error(e.what()); + } + } + + // Launch NTP queries asynchronously. + std::vector>> futures(addresses.size()); + for (auto [fut, address] : std::views::zip(futures, addresses)) + fut = limited_async(ntp_query, address); + + // Collect all future results. + std::vector corrections; + for (auto [address, fut] : std::views::zip(addresses, futures)) + try { + auto [correction, latency] = fut.get(); + corrections.push_back(correction); + report_info(utils::to_string(address) + + ": correction = "s + seconds_to_human(correction) + + ", latency = "s + seconds_to_human(latency)); + } + catch (std::exception& e) { + report_error(utils::to_string(address) + ": "s + e.what()); + } + + + if (corrections.empty()) { + report_error("No NTP server could be used!"); + return; + } + + double avg_correction = std::accumulate(corrections.begin(), + corrections.end(), + 0.0) + / corrections.size(); + + if (std::fabs(avg_correction) * 1000 <= cfg::tolerance) { + report_success("Tolerating clock drift (correction is only " + + seconds_to_human(avg_correction) + ")."s); + return; + } + + if (cfg::sync) { + if (!apply_clock_correction(avg_correction)) { + report_error("Failed to set system clock!"); + return; + } + report_success("Clock corrected by " + seconds_to_human(avg_correction)); + } + + } + + + std::string + local_clock_to_string() + { + return ticks_to_string(OSGetTime()); + } + +} // namespace core diff --git a/source/core.hpp b/source/core.hpp new file mode 100644 index 0000000..697b73d --- /dev/null +++ b/source/core.hpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +#ifndef CORE_HPP +#define CORE_HPP + +#include // pair<> +#include + +#include // struct sockaddr_in + + +namespace core { + + std::pair ntp_query(struct sockaddr_in address); + + void sync_clock(); + + std::string local_clock_to_string(); + +} // namespace core + + +#endif diff --git a/source/http_client.cpp b/source/http_client.cpp new file mode 100644 index 0000000..7dc913c --- /dev/null +++ b/source/http_client.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include // connect() + +#include "http_client.hpp" + +#include "utils.hpp" + + +#define LOG(FMT, ...) WHBLogPrintf(FMT __VA_OPT__(,) __VA_ARGS__) + + +namespace http { + + const std::string CRLF = "\r\n"; + + + struct url_fields { + std::string protocol; + std::string host; + std::optional port; + std::optional path; + }; + + + url_fields + parse_url(const std::string& url) + { + url_fields fields; + + std::regex re{R"((https?)://([^:/\s]+)(:(\d+))?(/.*)?)", + std::regex_constants::ECMAScript}; + + std::smatch m; + if (!regex_match(url, m, re)) + throw std::runtime_error{"failed to parse URL: \"" + url + "\""}; + + fields.protocol = m[1]; + fields.host = m[2]; + if (m[4].matched) + fields.port = std::stoi(m[4]); + if (m[5].matched) + fields.path = m[5]; + return fields; + } + + + std::string + build_request(const url_fields& fields) + { + std::string req = "GET "; + if (fields.path) + req += *fields.path; + else + req += "/"; + req += " HTTP/1.1" + CRLF; + req += "Host: " + fields.host + CRLF; + req += "User-Agent: Wii U Time Sync Plugin" + CRLF; + req += "Accept: text/plain" + CRLF; + req += "Connection: close" + CRLF; + req += CRLF; + return req; + } + + + struct response_header { + unsigned long length; // We only care about Content-Length. + std::string type; + }; + + + response_header + parse_header(const std::string input) + { + auto lines = utils::split(input, CRLF); + if (lines.empty()) + throw std::runtime_error{"Empty HTTP response."}; + + { + std::regex re{R"(HTTP/1\.1 (\d+)( (.*))?)", + std::regex_constants::ECMAScript}; + std::smatch m; + if (!regex_match(lines[0], m, re)) + throw std::runtime_error{"Could not parse HTTP response: \"" + + lines[0] + "\""}; + int status = std::stoi(m[1]); + if (status < 200 || status > 299) + throw std::runtime_error{"HTTP status was " + m[1].str()}; + } + + std::map fields; + for (const auto& line : lines | std::views::drop(1)) { + auto key_val = utils::split(line, ": ", 2); + if (key_val.size() != 2) + throw std::runtime_error{"invalid HTTP header field: " + line}; + auto key = key_val[0]; + auto val = key_val[1]; + fields[key] = val; + } + + if (!fields.contains("Content-Length")) + throw std::runtime_error{"HTTP header is missing mandatory Content-Length field."}; + + response_header header; + header.length = std::stoul(fields.at("Content-Length")); + header.type = fields.at("Content-Type"); + + return header; + } + + + std::string + get(const std::string& url) + { + auto fields = parse_url(url); + + if (fields.protocol != "http") + throw std::runtime_error{"Protocol '" + fields.protocol + "' not supported."}; + + if (!fields.port) + fields.port = 80; + + utils::addrinfo_query query = { + .family = AF_INET, + .socktype = SOCK_STREAM, + .protocol = IPPROTO_TCP + }; + auto addresses = utils::get_address_info(fields.host, + std::to_string(*fields.port), + query); + if (addresses.empty()) + throw std::runtime_error{"Host '" + fields.host + "' has no IP addresses."}; + + const auto& addr = addresses.front(); + utils::socket_guard sock{addr.family, addr.socktype, addr.protocol}; + + if (connect(sock.fd, + reinterpret_cast(&addr.address), + sizeof addr.address) == -1) { + int e = errno; + throw std::runtime_error{"connect() failed: " + utils::errno_to_string(e)}; + } + + auto request = build_request(fields); + utils::send_all(sock.fd, request); + + auto header_str = utils::recv_until(sock.fd, CRLF + CRLF); + auto header = parse_header(header_str); + + if (!header.type.starts_with("text/plain")) + throw std::runtime_error{"HTTP response is not plain text: \"" + + header.type + "\""}; + + return utils::recv_all(sock.fd, header.length); + + } + +} diff --git a/source/http_client.hpp b/source/http_client.hpp new file mode 100644 index 0000000..f78ae2b --- /dev/null +++ b/source/http_client.hpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT + +#ifndef HTTP_CLIENT_HPP +#define HTTP_CLIENT_HPP + +#include + +namespace http { + + std::string get(const std::string& url); + +} // namespace http + + +#endif diff --git a/source/limited_async.hpp b/source/limited_async.hpp new file mode 100644 index 0000000..ea00e56 --- /dev/null +++ b/source/limited_async.hpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +#include // invoke() +#include +#include + + +/* + * This is an implementation of a wrapper for std::async() that limits the number of + * concurrent threads. Any call beyond the limit will block, so make sure the function + * argument does not block indefinitely. + * + * This is needed because the Wii U's socket implementation can only handle a small + * number of concurrent threads. + */ + +enum class guard_type { + acquire_and_release, + only_acquire, + only_release +}; + + +template +struct semaphore_guard { + Sem& sem; + guard_type type; + + semaphore_guard(Sem& s, + guard_type t = guard_type::acquire_and_release) : + sem(s), + type{t} + { + if (type == guard_type::acquire_and_release || + type == guard_type::only_acquire) + sem.acquire(); + } + + ~semaphore_guard() + { + if (type == guard_type::acquire_and_release || + type == guard_type::only_release) + sem.release(); + } +}; + + +extern std::counting_semaphore<> async_limit; + + +template +[[nodiscard]] +std::future, std::decay_t...>> +limited_async(Func&& func, + Args&&... args) +{ + + semaphore_guard caller_guard{async_limit}; // acquire the semaphore, may block + + auto result = std::async(std::launch::async, + [](auto&& f, auto&&... a) -> auto + { + semaphore_guard callee_guard{async_limit, + guard_type::only_release}; + return std::invoke(std::forward(f), + std::forward(a)...); + }, + std::forward(func), + std::forward(args)...); + + // If async() didn't fail, let the async thread handle the semaphore release. + caller_guard.type = guard_type::only_acquire; + + return result; +} diff --git a/source/log.cpp b/source/log.cpp new file mode 100644 index 0000000..9ad49d2 --- /dev/null +++ b/source/log.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT + +#include + +#include "log.hpp" + +#include "cfg.hpp" + + +void +report_error(const std::string& arg) +{ + LOG("ERROR: %s", arg.c_str()); + + if (!cfg::notify) + return; + + std::string msg = LOG_PREFIX + arg; + NotificationModule_AddErrorNotificationEx(msg.c_str(), + cfg::msg_duration, + 1, + {255, 255, 255, 255}, + {160, 32, 32, 255}, + nullptr, + nullptr); +} + + +void +report_info(const std::string& arg) +{ + LOG("INFO: %s", arg.c_str()); + + if (!cfg::notify) + return; + + std::string msg = LOG_PREFIX + arg; + NotificationModule_AddInfoNotificationEx(msg.c_str(), + cfg::msg_duration, + {255, 255, 255, 255}, + {32, 32, 160, 255}, + nullptr, + nullptr); +} + + +void +report_success(const std::string& arg) +{ + LOG("SUCCESS: %s", arg.c_str()); + + if (!cfg::notify) + return; + + std::string msg = LOG_PREFIX + arg; + NotificationModule_AddInfoNotificationEx(msg.c_str(), + cfg::msg_duration, + {255, 255, 255, 255}, + {32, 160, 32, 255}, + nullptr, + nullptr); +} diff --git a/source/log.hpp b/source/log.hpp new file mode 100644 index 0000000..6158b83 --- /dev/null +++ b/source/log.hpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +#ifndef LOG_HPP +#define LOG_HPP + +#include + +#include + + +#define LOG_PREFIX "[" PLUGIN_NAME "] " + +#define LOG(FMT, ...) WHBLogPrintf(LOG_PREFIX FMT __VA_OPT__(,) __VA_ARGS__) + + +void report_error(const std::string& arg); +void report_info(const std::string& arg); +void report_success(const std::string& arg); + +#endif diff --git a/source/main.cpp b/source/main.cpp index f9ff708..78f4e24 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,69 +1,21 @@ // SPDX-License-Identifier: MIT // standard headers -#include -#include -#include -#include -#include -#include -#include -#include -#include // invoke() -#include -#include -#include // unique_ptr<> -#include // accumulate() -#include -#include // ranges::zip() -#include -#include -#include -#include +#include // make_unique() #include -#include // pair<> -#include - -// unix headers -#include -#include -#include -#include -#include -#include -#include // WUT/WUPS headers -#include -#include #include #include -#include -#include -#include - -#include "ntp.hpp" +#include -#include "wupsxx/bool_item.hpp" -#include "wupsxx/category.hpp" +// local headers +#include "cfg.hpp" +#include "config_screen.hpp" +#include "preview_screen.hpp" +#include "core.hpp" #include "wupsxx/config.hpp" -#include "wupsxx/int_item.hpp" -#include "wupsxx/storage.hpp" -#include "wupsxx/text_item.hpp" - - -using namespace std::literals; - -#define PLUGIN_NAME "Wii U Time Sync" - -#define CFG_HOURS "hours" -#define CFG_MINUTES "minutes" -#define CFG_MSG_DURATION "msg_duration" -#define CFG_NOTIFY "notify" -#define CFG_SERVER "server" -#define CFG_SYNC "sync" -#define CFG_TOLERANCE "tolerance" // Important plugin information. WUPS_PLUGIN_NAME(PLUGIN_NAME); @@ -76,887 +28,34 @@ WUPS_USE_WUT_DEVOPTAB(); WUPS_USE_STORAGE(PLUGIN_NAME); -namespace cfg { - int hours = 0; - int minutes = 0; - int msg_duration = 5; - bool notify = false; - std::string server = "pool.ntp.org"; - bool sync = false; - int tolerance = 250; - - OSTime offset = 0; // combines hours and minutes offsets -} - - -// The code below implements a wrapper for std::async() that respects a thread limit. - -enum class guard_type { - acquire_and_release, - only_acquire, - only_release -}; - - -template -struct semaphore_guard { - Sem& sem; - guard_type type; - - semaphore_guard(Sem& s, - guard_type t = guard_type::acquire_and_release) : - sem(s), - type{t} - { - if (type == guard_type::acquire_and_release || - type == guard_type::only_acquire) - sem.acquire(); - } - - ~semaphore_guard() - { - if (type == guard_type::acquire_and_release || - type == guard_type::only_release) - sem.release(); - } -}; - - -template -[[nodiscard]] -std::future, std::decay_t...>> -limited_async(Func&& func, - Args&&... args) -{ - static std::counting_semaphore async_limit{6}; - - semaphore_guard caller_guard{async_limit}; - - auto result = std::async(std::launch::async, - [](auto&& f, auto&&... a) -> auto - { - semaphore_guard callee_guard{async_limit, - guard_type::only_release}; - return std::invoke(std::forward(f), - std::forward(a)...); - }, - std::forward(func), - std::forward(args)...); - - // If async() didn't fail, let the async thread handle the semaphore release. - caller_guard.type = guard_type::only_acquire; - - return result; -} - - -#ifdef __WUT__ -// These can usually be found in , but WUT does not provide them. - -constexpr -std::uint64_t -htobe64(std::uint64_t x) -{ - if constexpr (std::endian::native == std::endian::big) - return x; - else - return std::byteswap(x); -} - - -constexpr -std::uint64_t -be64toh(std::uint64_t x) -{ - return htobe64(x); -} - -#endif - - -void -report_error(const std::string& arg) -{ - std::string msg = "[" PLUGIN_NAME "] " + arg; - NotificationModule_AddErrorNotificationEx(msg.c_str(), - cfg::msg_duration, - 1, - {255, 255, 255, 255}, - {160, 32, 32, 255}, - nullptr, - nullptr); -} - - -void -report_info(const std::string& arg) -{ - if (!cfg::notify) - return; - - std::string msg = "[" PLUGIN_NAME "] " + arg; - NotificationModule_AddInfoNotificationEx(msg.c_str(), - cfg::msg_duration, - {255, 255, 255, 255}, - {32, 32, 160, 255}, - nullptr, - nullptr); -} - - -void -report_success(const std::string& arg) -{ - if (!cfg::notify) - return; - - std::string msg = "[" PLUGIN_NAME "] " + arg; - NotificationModule_AddInfoNotificationEx(msg.c_str(), - cfg::msg_duration, - {255, 255, 255, 255}, - {32, 160, 32, 255}, - nullptr, - nullptr); -} - - -// Wrapper for strerror_r() -std::string -errno_to_string(int e) -{ - char buf[100]; - strerror_r(e, buf, sizeof buf); - return buf; -} - - -OSTime -get_utc_time() -{ - return OSGetTime() - cfg::offset; -} - - -double -ntp_to_double(ntp::timestamp t) -{ - return std::ldexp(static_cast(t), -32); -} - - -ntp::timestamp -double_to_ntp(double t) -{ - return std::ldexp(t, 32); -} - - -OSTime -ntp_to_wiiu(ntp::timestamp t) -{ - // Change t from NTP epoch (1900) to Wii U epoch (2000). - // There are 24 leap years in this period. - constexpr std::uint64_t seconds_per_day = 24 * 60 * 60; - constexpr std::uint64_t seconds_offset = seconds_per_day * (100 * 365 + 24); - t -= seconds_offset << 32; - - // Convert from u32.32 to Wii U ticks count. - double dt = ntp_to_double(t); - - // Note: do the conversion in floating point to avoid overflows. - OSTime r = dt * OSTimerClockSpeed; - - return r; -} - - -ntp::timestamp -wiiu_to_ntp(OSTime t) -{ - // Convert from Wii U ticks to seconds. - // Note: do the conversion in floating point to avoid overflows. - double dt = static_cast(t) / OSTimerClockSpeed; - ntp::timestamp r = double_to_ntp(dt); - - // Change r from Wii U epoch (2000) to NTP epoch (1900). - constexpr std::uint64_t seconds_per_day = 24 * 60 * 60; - constexpr std::uint64_t seconds_offset = seconds_per_day * (100 * 365 + 24); - r += seconds_offset << 32; - - return r; -} - - -std::string -to_string(const struct sockaddr_in& addr) -{ - char buf[32]; - return inet_ntop(addr.sin_family, &addr.sin_addr, - buf, sizeof buf); -} - - -std::string -seconds_to_human(double s) -{ - char buf[64]; - - if (std::fabs(s) < 2) // less than 2 seconds - std::snprintf(buf, sizeof buf, "%.1f ms", 1000 * s); - else if (std::fabs(s) < 2 * 60) // less than 2 minutes - std::snprintf(buf, sizeof buf, "%.1f s", s); - else if (std::fabs(s) < 2 * 60 * 60) // less than 2 hours - std::snprintf(buf, sizeof buf, "%.1f min", s / 60); - else if (std::fabs(s) < 2 * 24 * 60 * 60) // less than 2 days - std::snprintf(buf, sizeof buf, "%.1f hrs", s / (60 * 60)); - else - std::snprintf(buf, sizeof buf, "%.1f days", s / (24 * 60 * 60)); - - return buf; -} - - -std::string -format_wiiu_time(OSTime wt) -{ - OSCalendarTime cal; - OSTicksToCalendarTime(wt, &cal); - char buffer[256]; - std::snprintf(buffer, sizeof buffer, - "%04d-%02d-%02d %02d:%02d:%02d.%03d", - cal.tm_year, cal.tm_mon + 1, cal.tm_mday, - cal.tm_hour, cal.tm_min, cal.tm_sec, cal.tm_msec); - return buffer; -} - - -std::string -format_ntp(ntp::timestamp t) -{ - OSTime wt = ntp_to_wiiu(t); - return format_wiiu_time(wt); -} - - -std::vector -split(const std::string& input, - const std::string& separators) -{ - using std::string; - - std::vector result; - - string::size_type start = input.find_first_not_of(separators); - while (start != string::npos) { - auto finish = input.find_first_of(separators, start); - result.push_back(input.substr(start, finish - start)); - start = input.find_first_not_of(separators, finish); - } - - return result; -} - - -extern "C" int32_t CCRSysSetSystemTime(OSTime time); // from nn_ccr -extern "C" BOOL __OSSetAbsoluteSystemTime(OSTime time); // from coreinit - - -bool -apply_clock_correction(double correction) -{ - OSTime correction_ticks = correction * OSTimerClockSpeed; - - OSTime now = OSGetTime(); - OSTime corrected = now + correction_ticks; - - nn::pdm::NotifySetTimeBeginEvent(); - - if (CCRSysSetSystemTime(corrected)) { - nn::pdm::NotifySetTimeEndEvent(); - return false; - } - - bool res = __OSSetAbsoluteSystemTime(corrected); - - nn::pdm::NotifySetTimeEndEvent(); - - return res; -} - - -// RAII class to close down a socket -struct socket_guard { - int fd; - - socket_guard(int ns, int st, int pr) : - fd{socket(ns, st, pr)} - {} - - ~socket_guard() - { - if (fd != -1) - close(); - } - - void - close() - { - ::close(fd); - fd = -1; - } -}; - - -// Note: hardcoded for IPv4, the Wii U doesn't have IPv6. -std::pair -ntp_query(struct sockaddr_in address) -{ - socket_guard s{PF_INET, SOCK_DGRAM, IPPROTO_UDP}; - if (s.fd == -1) - throw std::runtime_error{"Unable to create socket!"}; - - connect(s.fd, reinterpret_cast(&address), sizeof address); - - ntp::packet packet; - packet.version(4); - packet.mode(ntp::packet::mode::client); - - - unsigned num_send_tries = 0; - try_again_send: - - ntp::timestamp t1 = wiiu_to_ntp(get_utc_time()); - packet.transmit_time = htobe64(t1); - - if (send(s.fd, &packet, sizeof packet, 0) == -1) { - int e = errno; - if (e != ENOMEM) - throw std::runtime_error{"Unable to send NTP request: "s + errno_to_string(e)}; - if (++num_send_tries < 4) { - std::this_thread::sleep_for(100ms); - goto try_again_send; - } else - throw std::runtime_error{"No resources for send(), too many retries!"}; - } - - struct timeval timeout = { 4, 0 }; - fd_set read_set; - - - unsigned num_select_tries = 0; - try_again_select: - - FD_ZERO(&read_set); - FD_SET(s.fd, &read_set); - - if (select(s.fd + 1, &read_set, nullptr, nullptr, &timeout) == -1) { - // Wii U's OS can only handle 16 concurrent select() calls, - // so we may need to try again later. - int e = errno; - if (e != ENOMEM) - throw std::runtime_error{"select() failed: "s + errno_to_string(e)}; - if (++num_select_tries < 4) { - std::this_thread::sleep_for(10ms); - goto try_again_select; - } else - throw std::runtime_error{"No resources for select(), too many retries!"}; - } - - - if (!FD_ISSET(s.fd, &read_set)) - throw std::runtime_error{"Timeout reached!"}; - - if (recv(s.fd, &packet, sizeof packet, 0) < 48) - throw std::runtime_error{"Invalid NTP response!"}; - - ntp::timestamp t4 = wiiu_to_ntp(get_utc_time()); - - ntp::timestamp t1_copy = be64toh(packet.origin_time); - if (t1 != t1_copy) - throw std::runtime_error{"NTP response does not match request: ["s - + format_ntp(t1) + "] vs ["s - + format_ntp(t1_copy) + "]"s}; - - // when our request arrived at the server - ntp::timestamp t2 = be64toh(packet.receive_time); - // when the server sent out a response - ntp::timestamp t3 = be64toh(packet.transmit_time); - - double roundtrip = ntp_to_double((t4 - t1) - (t3 - t2)); - double latency = roundtrip / 2; - - // t4 + correction = t3 + latency - double correction = ntp_to_double(t3) + latency - ntp_to_double(t4); - - return { correction, latency }; -} - - -// Wrapper for getaddrinfo(), hardcoded for IPv4 - -struct addrinfo_query { - int flags = 0; - int family = AF_UNSPEC; - int socktype = 0; - int protocol = 0; -}; - - -struct addrinfo_result { - int family; - int socktype; - int protocol; - struct sockaddr_in address; - std::optional canonname; -}; - - -std::vector -get_address_info(const std::optional& name, - const std::optional& port = {}, - std::optional query = {}) -{ - // RAII: unique_ptr is used to invoke freeaddrinfo() on function exit - std::unique_ptr - info; - - { - struct addrinfo hints; - const struct addrinfo *hints_ptr = nullptr; - - if (query) { - hints_ptr = &hints; - std::memset(&hints, 0, sizeof hints); - hints.ai_flags = query->flags; - hints.ai_family = query->family; - hints.ai_socktype = query->socktype; - hints.ai_protocol = query->protocol; - } - - struct addrinfo* raw_info = nullptr; - int err = getaddrinfo(name ? name->c_str() : nullptr, - port ? port->c_str() : nullptr, - hints_ptr, - &raw_info); - if (err) - throw std::runtime_error{gai_strerror(err)}; - - info.reset(raw_info); // put it in the smart pointer - } - - std::vector result; - - // walk through the linked list - for (auto a = info.get(); a; a = a->ai_next) { - - // sanity check: Wii U only supports IPv4 - if (a->ai_addrlen != sizeof(struct sockaddr_in)) - throw std::logic_error{"getaddrinfo() returned invalid result!"}; - - addrinfo_result item; - item.family = a->ai_family; - item.socktype = a->ai_socktype; - item.protocol = a->ai_protocol, - std::memcpy(&item.address, a->ai_addr, sizeof item.address); - if (a->ai_canonname) - item.canonname = a->ai_canonname; - - result.push_back(std::move(item)); - } - - return result; -} - - -// ordering operator, so we can put sockaddr_in inside a std::set. -constexpr -bool -operator <(const struct sockaddr_in& a, - const struct sockaddr_in& b) - noexcept -{ - return a.sin_addr.s_addr < b.sin_addr.s_addr; -} - - -// RAII type to ensure a function is never executed in parallel. - -struct exec_guard { - std::atomic& flag; - bool guarded = false; - - exec_guard(std::atomic& f) : - flag(f) - { - bool expected_flag = false; - if (flag.compare_exchange_strong(expected_flag, true)) - guarded = true; // Exactly one thread can have the "guarded" flag as true. - } - - ~exec_guard() - { - if (guarded) - flag = false; - } -}; - - -void -update_offset() -{ - cfg::offset = OSSecondsToTicks(cfg::minutes * 60); - if (cfg::hours < 0) - cfg::offset -= OSSecondsToTicks(-cfg::hours * 60 * 60); - else - cfg::offset += OSSecondsToTicks(cfg::hours * 60 * 60); -} - - -void -update_time() -{ - if (!cfg::sync) - return; - - static std::atomic executing = false; - - exec_guard guard{executing}; - if (!guard.guarded) { - // Another thread is already executing this function. - report_info("Skipping NTP task: already in progress."); - return; - } - - update_offset(); - - std::vector servers = split(cfg::server, " \t,;"); - - addrinfo_query query = { - .family = AF_INET, - .socktype = SOCK_DGRAM, - .protocol = IPPROTO_UDP - }; - - // First, resolve all the names, in parallel. - // Some IP addresses might be duplicated when we use *.pool.ntp.org. - std::set addresses; - { - std::vector>> infos(servers.size()); - for (auto [info_vec, server] : std::views::zip(infos, servers)) - info_vec = limited_async(get_address_info, server, "123", query); - - for (auto& info_vec : infos) - try { - for (auto info : info_vec.get()) - addresses.insert(info.address); - } - catch (std::exception& e) { - report_error(e.what()); - } - } - - // Launch all NTP queries in parallel. - std::vector>> results(addresses.size()); - for (auto [address, result] : std::views::zip(addresses, results)) - result = limited_async(ntp_query, address); - - // Now collect all results. - std::vector corrections; - for (auto [address, result] : std::views::zip(addresses, results)) - try { - auto [correction, latency] = result.get(); - corrections.push_back(correction); - report_info(to_string(address) - + ": correction = "s + seconds_to_human(correction) - + ", latency = "s + seconds_to_human(latency)); - } - catch (std::exception& e) { - report_error(to_string(address) + ": "s + e.what()); - } - - - if (corrections.empty()) { - report_error("No NTP server could be used!"); - return; - } - - double avg_correction = std::accumulate(corrections.begin(), - corrections.end(), - 0.0) - / corrections.size(); - - if (std::fabs(avg_correction) * 1000 <= cfg::tolerance) { - report_success("Tolerating clock drift (correction is only " - + seconds_to_human(avg_correction) + ")."s); - return; - } - - if (cfg::sync) { - if (!apply_clock_correction(avg_correction)) { - report_error("Failed to set system clock!"); - return; - } - } - - if (cfg::notify) - report_success("Clock corrected by " + seconds_to_human(avg_correction)); -} - - -template -void -load_or_init(const std::string& key, - T& variable) -{ - auto val = wups::load(key); - if (!val) - wups::store(key, variable); - else - variable = *val; -} - - INITIALIZE_PLUGIN() { + WHBLogUdpInit(); + NotificationModule_InitLibrary(); // Set up for notifications. + // Check if the plugin's settings have been saved before. if (WUPS_OpenStorage() == WUPS_STORAGE_ERROR_SUCCESS) { - - load_or_init(CFG_HOURS, cfg::hours); - load_or_init(CFG_MINUTES, cfg::minutes); - load_or_init(CFG_MSG_DURATION, cfg::msg_duration); - load_or_init(CFG_NOTIFY, cfg::notify); - load_or_init(CFG_SERVER, cfg::server); - load_or_init(CFG_SYNC, cfg::sync); - load_or_init(CFG_TOLERANCE, cfg::tolerance); - + cfg::load(); WUPS_CloseStorage(); } - NotificationModule_InitLibrary(); // Set up for notifications. - if (cfg::sync) - update_time(); // Update time when plugin is loaded. + core::sync_clock(); // Update clock when plugin is loaded. } -struct statistics { - double min = 0; - double max = 0; - double avg = 0; -}; - - -statistics -get_statistics(const std::vector& values) -{ - statistics result; - double total = 0; - - if (values.empty()) - return result; - - result.min = result.max = values.front(); - for (auto x : values) { - result.min = std::fmin(result.min, x); - result.max = std::fmax(result.max, x); - total += x; - } - - result.avg = total / values.size(); - - return result; -} - - -struct preview_item : wups::text_item { - - struct server_info { - wups::text_item* name_item = nullptr; - wups::text_item* corr_item = nullptr; - wups::text_item* late_item = nullptr; - }; - - wups::category* category; - - std::map server_infos; - - preview_item(wups::category* cat) : - wups::text_item{"", "Clock (\ue000 to refresh)"}, - category{cat} - { - category->add(this); - - std::vector servers = split(cfg::server, " \t,;"); - for (const auto& server : servers) { - if (!server_infos.contains(server)) { - auto& si = server_infos[server]; - - auto name_item = std::make_unique("", server + ":"); - si.name_item = name_item.get(); - category->add(std::move(name_item)); - - auto corr_item = std::make_unique("", " Correction:"); - si.corr_item = corr_item.get(); - category->add(std::move(corr_item)); - - auto late_item = std::make_unique("", " Latency:"); - si.late_item = late_item.get(); - category->add(std::move(late_item)); - } - } - } - - - void - on_button_pressed(WUPSConfigButtons buttons) - override - { - wups::text_item::on_button_pressed(buttons); - - if (buttons & WUPS_CONFIG_BUTTON_A) - run_preview(); - } - - - void - run_preview() - try { - - using std::make_unique; - - update_offset(); - - for (auto& [key, value] : server_infos) { - value.name_item->text.clear(); - value.corr_item->text.clear(); - value.late_item->text.clear(); - } - - std::vector servers = split(cfg::server, " \t,;"); - - addrinfo_query query = { - .family = AF_INET, - .socktype = SOCK_DGRAM, - .protocol = IPPROTO_UDP - }; - - double total = 0; - unsigned num_values = 0; - - for (const auto& server : servers) { - auto& si = server_infos.at(server); - try { - auto infos = get_address_info(server, "123", query); - - si.name_item->text = std::to_string(infos.size()) - + (infos.size() > 1 ? " addresses."s : " address."s); - - std::vector server_corrections; - std::vector server_latencies; - unsigned errors = 0; - - for (const auto& info : infos) { - try { - auto [correction, latency] = ntp_query(info.address); - server_corrections.push_back(correction); - server_latencies.push_back(latency); - total += correction; - ++num_values; - } - catch (std::exception& e) { - ++errors; - } - } - - if (errors) - si.name_item->text += " "s + std::to_string(errors) - + (errors > 1 ? " errors."s : "error."s); - if (!server_corrections.empty()) { - auto corr_stats = get_statistics(server_corrections); - si.corr_item->text = "min = "s + seconds_to_human(corr_stats.min) - + ", max = "s + seconds_to_human(corr_stats.max) - + ", avg = "s + seconds_to_human(corr_stats.avg); - auto late_stats = get_statistics(server_latencies); - si.late_item->text = "min = "s + seconds_to_human(late_stats.min) - + ", max = "s + seconds_to_human(late_stats.max) - + ", avg = "s + seconds_to_human(late_stats.avg); - } else { - si.corr_item->text = "No data."; - si.late_item->text = "No data."; - } - } - catch (std::exception& e) { - si.name_item->text = e.what(); - } - } - - text = format_wiiu_time(OSGetTime()); - - if (num_values) { - double avg = total / num_values; - text += ", needs "s + seconds_to_human(avg); - } - - } - catch (std::exception& e) { - text = "Error: "s + e.what(); - } - -}; - - WUPS_GET_CONFIG() { if (WUPS_OpenStorage() != WUPS_STORAGE_ERROR_SUCCESS) return 0; - using std::make_unique; - try { + auto root = std::make_unique(PLUGIN_NAME); - auto config = make_unique("Configuration"); - - config->add(make_unique(CFG_SYNC, - "Syncing Enabled", - cfg::sync)); - - config->add(make_unique(CFG_NOTIFY, - "Show Notifications", - cfg::notify)); - - config->add(make_unique(CFG_MSG_DURATION, - "Notification Duration (seconds)", - cfg::msg_duration, 0, 30)); - - config->add(make_unique(CFG_HOURS, - "Hours Offset", - cfg::hours, -12, 14)); - - config->add(make_unique(CFG_MINUTES, - "Minutes Offset", - cfg::minutes, 0, 59)); - - config->add(make_unique(CFG_TOLERANCE, - "Tolerance (milliseconds, \ue083/\ue084 for +/- 50)", - cfg::tolerance, 0, 5000)); - - // show current NTP server address, no way to change it. - config->add(make_unique(CFG_SERVER, - "NTP servers", - cfg::server)); - - auto preview = make_unique("Preview"); - // The preview_item adds itself to the category already. - make_unique(preview.get()).release(); - - auto root = make_unique(PLUGIN_NAME); - root->add(std::move(config)); - root->add(std::move(preview)); + root->add(std::make_unique()); + root->add(std::make_unique()); return root.release()->handle; - } catch (...) { return 0; @@ -966,7 +65,7 @@ WUPS_GET_CONFIG() WUPS_CONFIG_CLOSED() { - std::jthread update_time_thread(update_time); + std::jthread update_time_thread(core::sync_clock); update_time_thread.detach(); // Update time when settings are closed. WUPS_CloseStorage(); // Save all changes. diff --git a/source/nintendo_glyphs.hpp b/source/nintendo_glyphs.hpp new file mode 100644 index 0000000..5652a90 --- /dev/null +++ b/source/nintendo_glyphs.hpp @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT + +#ifndef NINTENDO_GLYPHS_HPP +#define NINTENDO_GLYPHS_HPP + +#define NIN_GLYPH_BTN_A "\uE000" +#define NIN_GLYPH_BTN_B "\uE001" +#define NIN_GLYPH_BTN_X "\uE002" +#define NIN_GLYPH_BTN_Y "\uE003" +#define NIN_GLYPH_BTN_L "\uE004" +#define NIN_GLYPH_BTN_R "\uE005" +#define NIN_GLYPH_BTN_DPAD "\uE006" + +// Used for touch screen calibration. +#define NIN_GLYPH_TARGET "\uE01D" + +#define NIN_GLYPH_CAPTURE_STILL "\uE01E" +#define NIN_GLYPH_CAPTURE_VIDEO "\uE076" + + +#define NIN_GLYPH_SPINNER_0 "\uE020" +#define NIN_GLYPH_SPINNER_1 "\uE021" +#define NIN_GLYPH_SPINNER_2 "\uE022" +#define NIN_GLYPH_SPINNER_3 "\uE023" +#define NIN_GLYPH_SPINNER_4 "\uE024" +#define NIN_GLYPH_SPINNER_5 "\uE025" +#define NIN_GLYPH_SPINNER_6 "\uE026" +#define NIN_GLYPH_SPINNER_7 "\uE027" + +#define NIN_GLYPH_WIIMOTE_BTN_POWER "\uE040" +#define NIN_GLYPH_WIIMOTE_BTN_DPAD "\uE041" +#define NIN_GLYPH_WIIMOTE_BTN_A "\uE042" +#define NIN_GLYPH_WIIMOTE_BTN_B "\uE043" +#define NIN_GLYPH_WIIMOTE_BTN_HOME "\uE044" +#define NIN_GLYPH_WIIMOTE_BTN_PLUS "\uE045" +#define NIN_GLYPH_WIIMOTE_BTN_MINUS "\uE046" +#define NIN_GLYPH_WIIMOTE_BTN_1 "\uE047" +#define NIN_GLYPH_WIIMOTE_BTN_2 "\uE048" + +#define NIN_GLYPH_NUNCHUK_STICK "\uE049" +#define NIN_GLYPH_NUNCHUK_BTN_C "\uE04A" +#define NIN_GLYPH_NUNCHUK_BTN_Z "\uE04B" + +#define NIN_GLYPH_CLASSIC_BTN_DPAD NIN_GLYPH_WIIMOTE_BTN_DPAD +#define NIN_GLYPH_CLASSIC_BTN_HOME NIN_GLYPH_WIIMOTE_BTN_HOME +#define NIN_GLYPH_CLASSIC_BTN_PLUS NIN_GLYPH_WIIMOTE_BTN_PLUS +#define NIN_GLYPH_CLASSIC_BTN_MINUS NIN_GLYPH_WIIMOTE_BTN_MINUS +#define NIN_GLYPH_CLASSIC_BTN_A "\uE04C" +#define NIN_GLYPH_CLASSIC_BTN_B "\uE04D" +#define NIN_GLYPH_CLASSIC_BTN_X "\uE04E" +#define NIN_GLYPH_CLASSIC_BTN_Y "\uE04F" +#define NIN_GLYPH_CLASSIC_L_STICK "\uE050" +#define NIN_GLYPH_CLASSIC_R_STICK "\uE051" +#define NIN_GLYPH_CLASSIC_BTN_L "\uE052" +#define NIN_GLYPH_CLASSIC_BTN_R "\uE053" +#define NIN_GLYPH_CLASSIC_BTN_ZL "\uE054" +#define NIN_GLYPH_CLASSIC_BTN_ZR "\uE055" + +#define NIN_GLYPH_KBD_RETURN "\uE056" +#define NIN_GLYPH_KBD_SPACE "\uE057" + +#define NIN_GLYPH_HAND_POINT "\uE058" +#define NIN_GLYPH_HAND_POINT_1 "\uE059" +#define NIN_GLYPH_HAND_POINT_2 "\uE05A" +#define NIN_GLYPH_HAND_POINT_3 "\uE05B" +#define NIN_GLYPH_HAND_POINT_4 "\uE05C" + +#define NIN_GLYPH_HAND_FIST "\uE05D" +#define NIN_GLYPH_HAND_FIST_1 "\uE05E" +#define NIN_GLYPH_HAND_FIST_2 "\uE05F" +#define NIN_GLYPH_HAND_FIST_3 "\uE060" +#define NIN_GLYPH_HAND_FIST_4 "\uE061" + +#define NIN_GLYPH_HAND_OPEN "\uE062" +#define NIN_GLYPH_HAND_OPEN_1 "\uE063" +#define NIN_GLYPH_HAND_OPEN_2 "\uE064" +#define NIN_GLYPH_HAND_OPEN_3 "\uE065" +#define NIN_GLYPH_HAND_OPEN_4 "\uE066" + +// Wii logo +#define NIN_GLYPH_WII "\uE067" + +// Question mark block icon. +#define NIN_GLYPH_HELP "\uE06B" + +// Close icon. +#define NIN_GLYPH_CLOSE "\uE070" +#define NIN_GLYPH_CLOSE_ALT "\uE071" + +// Navigation images. +#define NIN_GLYPH_BACK "\uE072" +#define NIN_GLYPH_HOME "\uE073" + +// Controller images. +#define NIN_GLYPH_GAMEPAD "\uE087" +#define NIN_GLYPH_WIIMOTE "\uE088" + +#define NIN_GLYPH_3DS_BTN_DPAD NIN_GLYPH_WIIMOTE_BTN_DPAD +#define NIN_GLYPH_3DS_BTN_A NIN_GLYPH_BTN_A +#define NIN_GLYPH_3DS_BTN_B NIN_GLYPH_BTN_B +#define NIN_GLYPH_3DS_BTN_X NIN_GLYPH_BTN_X +#define NIN_GLYPH_3DS_BTN_Y NIN_GLYPH_BTN_Y +#define NIN_GLYPH_3DS_BTN_HOME NIN_GLYPH_HOME +#define NIN_GLYPH_3DS_CIRCLEPAD "\uE077" +#define NIN_GLYPH_3DS_BTN_POWER "\uE078" +#define NIN_GLYPH_3DS_STEPS "\uE074" +#define NIN_GLYPH_3DS_PLAYCOIN "\uE075" + +#define NIN_GLYPH_BTN_DPAD_UP "\uE079" +#define NIN_GLYPH_BTN_DPAD_DOWN "\uE07A" +#define NIN_GLYPH_BTN_DPAD_LEFT "\uE07B" +#define NIN_GLYPH_BTN_DPAD_RIGHT "\uE07C" +#define NIN_GLYPH_BTN_DPAD_UP_DOWN "\uE07D" +#define NIN_GLYPH_BTN_DPAD_DOWN_UP NIN_GLYPH_BTN_DPAD_UP_DOWN +#define NIN_GLYPH_BTN_DPAD_LEFT_RIGHT "\uE07E" +#define NIN_GLYPH_BTN_DPAD_RIGHT_LEFT NIN_GLYPH_BTN_DPAD_LEFT_RIGHT + +#define NIN_GLYPH_GAMEPAD_BTN_DPAD NIN_GLYPH_WIIMOTE_BTN_DPAD +#define NIN_GLYPH_GAMEPAD_STICK "\uE080" +#define NIN_GLYPH_GAMEPAD_L_STICK "\uE081" +#define NIN_GLYPH_GAMEPAD_R_STICK "\uE082" +#define NIN_GLYPH_GAMEPAD_BTN_L "\uE083" +#define NIN_GLYPH_GAMEPAD_BTN_R "\uE084" +#define NIN_GLYPH_GAMEPAD_BTN_ZL "\uE085" +#define NIN_GLYPH_GAMEPAD_BTN_ZR "\uE086" +#define NIN_GLYPH_GAMEPAD_BTN_HOME NIN_GLYPH_WIIMOTE_BTN_HOME +#define NIN_GLYPH_GAMEPAD_BTN_PLUS NIN_GLYPH_WIIMOTE_BTN_PLUS +#define NIN_GLYPH_GAMEPAD_BTN_MINUS NIN_GLYPH_WIIMOTE_BTN_MINUS +#define NIN_GLYPH_GAMEPAD_BTN_TV "\uE089" +#define NIN_GLYPH_GAMEPAD_L_STICK_PRESS "\uE08A" +#define NIN_GLYPH_GAMEPAD_R_STICK_PRESS "\uE08B" + +#define NIN_GLYPH_ARROW_LEFT_RIGHT "\uE08C" +#define NIN_GLYPH_ARROW_RIGHT_LEFT NIN_GLYPH_ARROW_LEFT_RIGHT +#define NIN_GLYPH_ARROW_UP_DOWN "\uE08D" +#define NIN_GLYPH_ARROW_DOWN_UP NIN_GLYPH_ARROW_UP_DOWN + +#define NIN_GLYPH_ARROW_CW "\uE08E" +#define NIN_GLYPH_ARROW_CCW "\uE08F" + +#define NIN_GLYPH_ARROW_RIGHT "\uE090" +#define NIN_GLYPH_ARROW_LEFT "\uE091" +#define NIN_GLYPH_ARROW_UP "\uE092" +#define NIN_GLYPH_ARROW_DOWN "\uE093" + +#define NIN_GLYPH_ARROW_UP_RIGHT "\uE094" +#define NIN_GLYPH_ARROW_RIGHT_UP NIN_GLYPH_ARROW_RIGHT_UP +#define NIN_GLYPH_ARROW_DOWN_RIGHT "\uE095" +#define NIN_GLYPH_ARROW_RIGHT_DOWN NIN_GLYPH_ARROW_DOWN_RIGHT +#define NIN_GLYPH_ARROW_DOWN_LEFT "\uE096" +#define NIN_GLYPH_ARROW_LEFT_DOWN NIN_GLYPH_ARROW_DOWN_LEFT +#define NIN_GLYPH_ARROW_UP_LEFT "\uE097" +#define NIN_GLYPH_ARROW_LEFT_UP NIN_GLYPH_ARROW_UP_LEFT + +#define NIN_GLYPH_X "\uE098" + +#define NIN_GLYPH_NFC "\uE099" + +#endif diff --git a/source/ntp.cpp b/source/ntp.cpp new file mode 100644 index 0000000..b99adec --- /dev/null +++ b/source/ntp.cpp @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT + +#include // endian, byteswap() +#include + +#include "ntp.hpp" + + +#ifdef __WIIU__ +namespace { + + // These can usually be found in , but devkitPPC/WUT does not provide them. + + constexpr + std::uint64_t + htobe64(std::uint64_t x) + { + if constexpr (std::endian::native == std::endian::big) + return x; + else + return std::byteswap(x); + } + + + constexpr + std::uint64_t + be64toh(std::uint64_t x) + { + return htobe64(x); + } + +} +#endif + + +namespace ntp { + + timestamp::timestamp(double d) + noexcept + { + store(std::ldexp(d, 32)); + } + + + timestamp::operator double() + const noexcept + { + return std::ldexp(static_cast(load()), -32); + } + + + std::uint64_t + timestamp::load() + const noexcept + { + return be64toh(stored); + } + + + void + timestamp::store(std::uint64_t v) + noexcept + { + stored = htobe64(v); + } + + + std::strong_ordering + timestamp::operator <=>(timestamp other) + const noexcept + { + return load() <=> other.load(); + } + + + + std::string + to_string(packet::mode_flag m) + { + switch (m) { + case packet::mode_flag::reserved: + return "reserved"; + case packet::mode_flag::active: + return "active"; + case packet::mode_flag::passive: + return "passive"; + case packet::mode_flag::client: + return "client"; + case packet::mode_flag::server: + return "server"; + case packet::mode_flag::broadcast: + return "broadcast"; + case packet::mode_flag::control: + return "control"; + case packet::mode_flag::reserved_private: + return "reserved_private"; + default: + return "error"; + } + } + + + + void + packet::leap(leap_flag x) + noexcept + { + lvm = static_cast(x) | (lvm & 0b0011'1111); + } + + + packet::leap_flag + packet::leap() + const noexcept + { + return static_cast((lvm & 0b1100'0000) >> 6); + } + + + void + packet::version(unsigned v) + noexcept + { + lvm = ((v << 3) & 0b0011'1000) | (lvm & 0b1100'0111); + } + + + unsigned + packet::version() + const noexcept + { + return (lvm & 0b0011'1000) >> 3; + } + + + void + packet::mode(packet::mode_flag m) + noexcept + { + lvm = static_cast(m) | (lvm & 0b1111'1000); + } + + + packet::mode_flag + packet::mode() + const noexcept + { + return static_cast(lvm & 0b000'0111); + } + + +} // namespace ntp diff --git a/source/ntp.hpp b/source/ntp.hpp index 451d44b..ef51e69 100644 --- a/source/ntp.hpp +++ b/source/ntp.hpp @@ -3,14 +3,45 @@ #ifndef NTP_HPP #define NTP_HPP +#include #include +#include namespace ntp { // For details, see https://www.ntp.org/reflib/rfc/rfc5905.txt - // This is u32.32 fixed-point format, seconds since 1900-01-01. - using timestamp = std::uint64_t; + // This is u32.32 fixed-point format, seconds since 1900-01-01 00:00:00 UTC + class timestamp { + + std::uint64_t stored = 0; // in big-endian format + + public: + + constexpr timestamp() noexcept = default; + + timestamp(std::uint64_t v) = delete; + + // Allow explicit conversions from/to double + explicit timestamp(double d) noexcept; + explicit operator double() const noexcept; + + // Checks if timestamp is non-zero. Zero has a special meaning. + constexpr explicit operator bool() const noexcept { return stored; } + + + // These will byteswap if necessary. + std::uint64_t load() const noexcept; + void store(std::uint64_t v) noexcept; + + + constexpr + bool operator ==(const timestamp& other) const noexcept = default; + + std::strong_ordering operator <=>(timestamp other) const noexcept; + + }; + // This is a u16.16 fixed-point format. using short_timestamp = std::uint32_t; @@ -19,14 +50,14 @@ namespace ntp { // Note: all fields are big-endian struct packet { - enum class leap : std::uint8_t { + enum class leap_flag : std::uint8_t { no_warning = 0 << 6, one_more_second = 1 << 6, one_less_second = 2 << 6, unknown = 3 << 6 }; - enum class mode : std::uint8_t { + enum class mode_flag : std::uint8_t { reserved = 0, active = 1, passive = 2, @@ -48,31 +79,29 @@ namespace ntp { short_timestamp root_dispersion = 0; // Total dispersion to the reference clock. char reference_id[4] = {0, 0, 0, 0}; // Reference clock identifier. - timestamp reference_time = 0; // Reference timestamp. - timestamp origin_time = 0; // Origin timestamp, aka T1. - timestamp receive_time = 0; // Receive timestamp, aka T2. - timestamp transmit_time = 0; // Transmit timestamp, aka T3. + timestamp reference_time; // Reference timestamp. + timestamp origin_time; // Origin timestamp. + timestamp receive_time; // Receive timestamp. + timestamp transmit_time; // Transmit timestamp. + + void leap(leap_flag x) noexcept; + leap_flag leap() const noexcept; - void leap(leap x) - { - lvm = static_cast(x) | (lvm & 0b0011'1111); - } - void version(unsigned v) - { - lvm = ((v << 3) & 0b0011'1000) | (lvm & 0b1100'0111); - } + void version(unsigned v) noexcept; + unsigned version() const noexcept; - void mode(mode m) - { - lvm = static_cast(m) | (lvm & 0b1111'1000); - } + + void mode(mode_flag m) noexcept; + mode_flag mode() const noexcept; }; static_assert(sizeof(packet) == 48); + std::string to_string(packet::mode_flag m); + } // namespace ntp diff --git a/source/preview_screen.cpp b/source/preview_screen.cpp new file mode 100644 index 0000000..c0ef52a --- /dev/null +++ b/source/preview_screen.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include "preview_screen.hpp" + +#include "cfg.hpp" +#include "core.hpp" +#include "log.hpp" +#include "nintendo_glyphs.hpp" +#include "utils.hpp" + + +using std::make_unique; +using namespace std::literals; + + +struct clock_item : wups::text_item { + + preview_screen* parent; + + clock_item(preview_screen* p) : + wups::text_item{"", "Clock (" NIN_GLYPH_BTN_A " to refresh)"}, + parent{p} + {} + + + void + on_button_pressed(WUPSConfigButtons buttons) + override + { + wups::text_item::on_button_pressed(buttons); + + if (buttons & WUPS_CONFIG_BUTTON_A) + parent->run(); + } + +}; + + +preview_screen::preview_screen() : + wups::category{"Preview"} +{ + auto c = make_unique(this); + clock = c.get(); + add(std::move(c)); + + auto servers = utils::split(cfg::server, " \t,;"); + for (const auto& server : servers) { + if (!server_infos.contains(server)) { + auto& si = server_infos[server]; + + auto name = make_unique("", server + ":"); + si.name = name.get(); + add(std::move(name)); + + auto correction = make_unique("", "┣ Correction:"); + si.correction = correction.get(); + add(std::move(correction)); + + auto latency = make_unique("", "┗ Latency:"); + si.latency = latency.get(); + add(std::move(latency)); + } + } +} + + +namespace { + struct statistics { + double min = 0; + double max = 0; + double avg = 0; + }; + + + statistics + get_statistics(const std::vector& values) + { + statistics result; + double total = 0; + + if (values.empty()) + return result; + + result.min = result.max = values.front(); + for (auto x : values) { + result.min = std::fmin(result.min, x); + result.max = std::fmax(result.max, x); + total += x; + } + + result.avg = total / values.size(); + + return result; + } +} + + +void +preview_screen::run() +try { + + using std::to_string; + using utils::seconds_to_human; + using utils::to_string; + + cfg::update_utc_offset(); + + for (auto& [key, value] : server_infos) { + value.name->text.clear(); + value.correction->text.clear(); + value.latency->text.clear(); + } + + auto servers = utils::split(cfg::server, " \t,;"); + + utils::addrinfo_query query = { + .family = AF_INET, + .socktype = SOCK_DGRAM, + .protocol = IPPROTO_UDP + }; + + double total = 0; + unsigned num_values = 0; + + for (const auto& server : servers) { + auto& si = server_infos.at(server); + try { + auto infos = utils::get_address_info(server, "123", query); + + si.name->text = to_string(infos.size()) + + (infos.size() > 1 ? " addresses."s : " address."s); + + std::vector server_corrections; + std::vector server_latencies; + unsigned errors = 0; + + for (const auto& info : infos) { + try { + auto [correction, latency] = core::ntp_query(info.address); + server_corrections.push_back(correction); + server_latencies.push_back(latency); + total += correction; + ++num_values; + LOG("%s (%s): correction = %s, latency = %s", + server.c_str(), + to_string(info.address).c_str(), + seconds_to_human(correction).c_str(), + seconds_to_human(latency).c_str()); + } + catch (std::exception& e) { + ++errors; + LOG("Error: %s", e.what()); + } + } + + if (errors) + si.name->text += " "s + to_string(errors) + + (errors > 1 ? " errors."s : "error."s); + if (!server_corrections.empty()) { + auto corr_stats = get_statistics(server_corrections); + si.correction->text = "min = "s + seconds_to_human(corr_stats.min) + + ", max = "s + seconds_to_human(corr_stats.max) + + ", avg = "s + seconds_to_human(corr_stats.avg); + auto late_stats = get_statistics(server_latencies); + si.latency->text = "min = "s + seconds_to_human(late_stats.min) + + ", max = "s + seconds_to_human(late_stats.max) + + ", avg = "s + seconds_to_human(late_stats.avg); + } else { + si.correction->text = "No data."; + si.latency->text = "No data."; + } + } + catch (std::exception& e) { + si.name->text = e.what(); + } + } + + clock->text = core::local_clock_to_string(); + + if (num_values) { + double avg = total / num_values; + clock->text += ", needs "s + seconds_to_human(avg); + } + +} +catch (std::exception& e) { + clock->text = "Error: "s + e.what(); +} diff --git a/source/preview_screen.hpp b/source/preview_screen.hpp new file mode 100644 index 0000000..ab1eda1 --- /dev/null +++ b/source/preview_screen.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +#ifndef PREVIEW_SCREEN_HPP +#define PREVIEW_SCREEN_HPP + +#include +#include + +#include "wupsxx/category.hpp" +#include "wupsxx/text_item.hpp" + + +struct preview_screen : wups::category { + + struct server_info { + wups::text_item* name = nullptr; + wups::text_item* correction = nullptr; + wups::text_item* latency = nullptr; + }; + + + wups::text_item* clock = nullptr; + std::map server_infos; + + + preview_screen(); + + void run(); + +}; + + +#endif diff --git a/source/utc.cpp b/source/utc.cpp new file mode 100644 index 0000000..cebe040 --- /dev/null +++ b/source/utc.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +#include + +#include "utc.hpp" + + +namespace utc { + + + double timezone_offset = 0; + + + static + double + local_time() + { + return static_cast(OSGetTime()) / OSTimerClockSpeed; + } + + + timestamp + now() + noexcept + { + return timestamp{ local_time() - timezone_offset }; + } + + +} // namespace utc diff --git a/source/utc.hpp b/source/utc.hpp new file mode 100644 index 0000000..c1edea5 --- /dev/null +++ b/source/utc.hpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +#ifndef UTC_HPP +#define UTC_HPP + +namespace utc { + + extern double timezone_offset; + + + // Seconds since 2000-01-01 00:00:00 UTC + struct timestamp { + double value; + }; + + + timestamp now() noexcept; + +} // namespace utc + +#endif diff --git a/source/utils.cpp b/source/utils.cpp new file mode 100644 index 0000000..4399889 --- /dev/null +++ b/source/utils.cpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: MIT + +// standard headers +#include // fabs() +#include // snprintf() +#include // memset(), memcpy() +#include // unique_ptr<> +#include // runtime_error, logic_error +#include // move() + +// unix headers +#include // inet_ntop() +#include // getaddrinfo() +#include // socket() +#include // close() + +// local headers +#include "utils.hpp" + + +namespace utils { + + std::string + errno_to_string(int e) + { + char buf[100]; + strerror_r(e, buf, sizeof buf); + return buf; + } + + + std::string + seconds_to_human(double s) + { + char buf[64]; + + if (std::fabs(s) < 2) // less than 2 seconds + std::snprintf(buf, sizeof buf, "%.1f ms", 1000 * s); + else if (std::fabs(s) < 2 * 60) // less than 2 minutes + std::snprintf(buf, sizeof buf, "%.1f s", s); + else if (std::fabs(s) < 2 * 60 * 60) // less than 2 hours + std::snprintf(buf, sizeof buf, "%.1f min", s / 60); + else if (std::fabs(s) < 2 * 24 * 60 * 60) // less than 2 days + std::snprintf(buf, sizeof buf, "%.1f hrs", s / (60 * 60)); + else + std::snprintf(buf, sizeof buf, "%.1f days", s / (24 * 60 * 60)); + + return buf; + } + + + std::vector + split(const std::string& input, + const std::string& separators, + std::size_t max_tokens) + { + using std::string; + + std::vector result; + + string::size_type start = input.find_first_not_of(separators); + while (start != string::npos) { + + // if we can only include one more token + if (max_tokens && result.size() + 1 == max_tokens) { + // the last token will be the remaining of the input + result.push_back(input.substr(start)); + break; + } + + auto finish = input.find_first_of(separators, start); + result.push_back(input.substr(start, finish - start)); + start = input.find_first_not_of(separators, finish); + } + + return result; + } + + + + bool + less_sockaddr_in::operator ()(const struct sockaddr_in& a, + const struct sockaddr_in& b) + const noexcept + { + return a.sin_addr.s_addr < b.sin_addr.s_addr; + } + + + + std::string + to_string(const struct sockaddr_in& addr) + { + char buf[32]; + return inet_ntop(addr.sin_family, &addr.sin_addr, + buf, sizeof buf); + } + + + + socket_guard::socket_guard(int ns, int st, int pr) : + fd{::socket(ns, st, pr)} + { + if (fd == -1) + throw std::runtime_error{"Unable to create socket!"}; + } + + socket_guard::~socket_guard() + { + if (fd != -1) + close(); + } + + void + socket_guard::close() + { + ::close(fd); + fd = -1; + } + + + void + send_all(int fd, + const std::string& msg, + int flags) + { + ssize_t sent = 0; + ssize_t total = msg.size(); + const char* start = msg.data(); + + while (sent < total) { + auto r = send(fd, start, total - sent, flags); + if (r <= 0) { + int e = errno; + throw std::runtime_error{"send() failed: " + + utils::errno_to_string(e)}; + } + sent += r; + start = msg.data() + sent; + } + } + + + std::string + recv_all(int fd, + std::size_t size, + int flags) + { + std::string result; + + char buffer[1024]; + + while (result.size() < size) { + ssize_t r = recv(fd, buffer, sizeof buffer, flags); + if (r == -1) { + int e = errno; + if (result.empty()) + throw std::runtime_error{"recv() failed: " + + utils::errno_to_string(e)}; + else + break; + } + + if (r == 0) + break; + + result.append(buffer, r); + } + + return result; + } + + + // Not very efficient, read one byte at a time. + std::string + recv_until(int fd, + const std::string& end_token, + int flags) + { + std::string result; + + char buffer[1]; + + while (true) { + + ssize_t r = recv(fd, buffer, sizeof buffer, flags); + if (r == -1) { + int e = errno; + if (result.empty()) + throw std::runtime_error{"recv() failed: " + + utils::errno_to_string(e)}; + else + break; + } + + if (r == 0) + break; + + result.append(buffer, r); + + // if we found the end token, remove it from the result and break out + auto end = result.find(end_token); + if (end != std::string::npos) { + result.erase(end); + break; + } + + } + + return result; + } + + + + std::vector + get_address_info(const std::optional& name, + const std::optional& port, + std::optional query) + { + // RAII: unique_ptr is used to invoke freeaddrinfo() on function exit + std::unique_ptr + info; + + { + struct addrinfo hints; + const struct addrinfo *hints_ptr = nullptr; + + if (query) { + hints_ptr = &hints; + std::memset(&hints, 0, sizeof hints); + hints.ai_flags = query->flags; + hints.ai_family = query->family; + hints.ai_socktype = query->socktype; + hints.ai_protocol = query->protocol; + } + + struct addrinfo* raw_info = nullptr; + int err = getaddrinfo(name ? name->c_str() : nullptr, + port ? port->c_str() : nullptr, + hints_ptr, + &raw_info); + if (err) + throw std::runtime_error{gai_strerror(err)}; + + info.reset(raw_info); // put it in the smart pointer + } + + std::vector result; + + // walk through the linked list + for (auto a = info.get(); a; a = a->ai_next) { + + // sanity check: Wii U only supports IPv4 + if (a->ai_addrlen != sizeof(struct sockaddr_in)) + throw std::logic_error{"getaddrinfo() returned invalid result!"}; + + addrinfo_result item; + item.family = a->ai_family; + item.socktype = a->ai_socktype; + item.protocol = a->ai_protocol, + std::memcpy(&item.address, a->ai_addr, sizeof item.address); + if (a->ai_canonname) + item.canonname = a->ai_canonname; + + result.push_back(std::move(item)); + } + + return result; + } + + + + exec_guard::exec_guard(std::atomic& f) : + flag(f), + guarded{false} + { + bool expected_flag = false; + if (flag.compare_exchange_strong(expected_flag, true)) + guarded = true; // Exactly one thread can have the "guarded" flag as true. + } + + exec_guard::~exec_guard() + { + if (guarded) + flag = false; + } + + +} // namespace utils diff --git a/source/utils.hpp b/source/utils.hpp new file mode 100644 index 0000000..0a0cf69 --- /dev/null +++ b/source/utils.hpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT + +#ifndef UTILS_HPP +#define UTILS_HPP + +#include +#include +#include +#include + +#include // struct sockaddr_in +#include // AF_* + +namespace utils { + + + // Wrapper for strerror_r() + std::string errno_to_string(int e); + + + // Generate time duration strings for humans. + std::string seconds_to_human(double s); + + + /** + * Split input string into tokens, according to separators. + * + * If max_tokens is not zero, only up to max_tokens will be generated; the last token + * will be the remaining of the string. + */ + std::vector + split(const std::string& input, + const std::string& separators, + std::size_t max_tokens = 0); + + + + // Ordering for sockaddr_in, so we can put it inside a std::set. + struct less_sockaddr_in { + bool + operator ()(const struct sockaddr_in& a, + const struct sockaddr_in& b) const noexcept; + }; + + + // Generate A.B.C.D string from IP address. + std::string to_string(const struct sockaddr_in& addr); + + + + // RAII class to create and close down a socket on exit. + struct socket_guard { + int fd; + + socket_guard(int ns, int st, int pr); + ~socket_guard(); + + void close(); + }; + + + void send_all(int fd, const std::string& msg, int flags = 0); + + std::string recv_all(int fd, std::size_t size, int flags = 0); + + std::string recv_until(int fd, const std::string& end_token, int flags = 0); + + + // Wrapper for getaddrinfo(), hardcoded for IPv4 + struct addrinfo_query { + int flags = 0; + int family = AF_UNSPEC; + int socktype = 0; + int protocol = 0; + }; + + struct addrinfo_result { + int family; + int socktype; + int protocol; + struct sockaddr_in address; + std::optional canonname; + }; + + std::vector + get_address_info(const std::optional& name, + const std::optional& port = {}, + std::optional query = {}); + + + + + // RAII type to ensure a function is never executed in parallel. + struct exec_guard { + + std::atomic& flag; + bool guarded; // when false, the function is already executing in some thread. + + exec_guard(std::atomic& f); + + ~exec_guard(); + + }; + + +} // namespace utils + +#endif diff --git a/source/wupsxx/bool_item.cpp b/source/wupsxx/bool_item.cpp index 4132927..353fe0e 100644 --- a/source/wupsxx/bool_item.cpp +++ b/source/wupsxx/bool_item.cpp @@ -6,6 +6,8 @@ #include "bool_item.hpp" #include "storage.hpp" +#include "../nintendo_glyphs.hpp" + namespace wups { @@ -33,9 +35,9 @@ namespace wups { const { if (variable) - std::snprintf(buf, size, "< %s ", true_str.c_str()); + std::snprintf(buf, size, "%s %s ", NIN_GLYPH_BTN_DPAD_LEFT, true_str.c_str()); else - std::snprintf(buf, size, " %s >", false_str.c_str()); + std::snprintf(buf, size, " %s %s", false_str.c_str(), NIN_GLYPH_BTN_DPAD_RIGHT); return 0; } diff --git a/source/wupsxx/int_item.cpp b/source/wupsxx/int_item.cpp index a047c76..bb53481 100644 --- a/source/wupsxx/int_item.cpp +++ b/source/wupsxx/int_item.cpp @@ -7,6 +7,8 @@ #include "int_item.hpp" #include "storage.hpp" +#include "../nintendo_glyphs.hpp" + namespace wups { @@ -37,13 +39,23 @@ namespace wups { int_item::get_current_value_selected_display(char* buf, std::size_t size) const { - char left = ' '; - char right = ' '; - if (variable > min_value) - left = '<'; - if (variable < max_value) - right = '>'; - std::snprintf(buf, size, "%c %d %c", left, variable, right); + const char* left = ""; + const char* right = ""; + const char* fast_left = ""; + const char* fast_right = ""; + if (variable > min_value) { + left = NIN_GLYPH_BTN_DPAD_LEFT; + fast_left = NIN_GLYPH_BTN_L; + } if (variable < max_value) { + right = NIN_GLYPH_BTN_DPAD_RIGHT; + fast_right = NIN_GLYPH_BTN_R; + } + std::snprintf(buf, size, "%s%s %d %s%s", + fast_left, + left, + variable, + right, + fast_right); return 0; } diff --git a/source/wupsxx/storage.hpp b/source/wupsxx/storage.hpp index ca302d9..e6e0983 100644 --- a/source/wupsxx/storage.hpp +++ b/source/wupsxx/storage.hpp @@ -6,6 +6,8 @@ #include #include +#include + namespace wups { diff --git a/source/wupsxx/text_item.cpp b/source/wupsxx/text_item.cpp index 2c7484a..bde6cd8 100644 --- a/source/wupsxx/text_item.cpp +++ b/source/wupsxx/text_item.cpp @@ -47,6 +47,8 @@ namespace wups { void text_item::on_button_pressed(WUPSConfigButtons buttons) { + base_item::on_button_pressed(buttons); + if (text.empty()) return;