From 7882abc2556f8a0262af5e8f667316ede8e19a0b Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Fri, 6 Oct 2023 22:38:58 -0300 Subject: [PATCH 01/17] created upstream branch --HG-- branch : upstream From 1159cbde3e7e4a76e03b64c04f73a41a1274468d Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Fri, 6 Oct 2023 22:55:33 -0300 Subject: [PATCH 02/17] backported changes to upstream --HG-- branch : upstream --- Makefile | 4 +- source/main.cpp | 890 +++++++++++++++++++++++++++++++++++++----------- source/ntp.hpp | 79 +++++ 3 files changed, 764 insertions(+), 209 deletions(-) create mode 100644 source/ntp.hpp diff --git a/Makefile b/Makefile index 9ac82ff..231a999 100644 --- a/Makefile +++ b/Makefile @@ -28,13 +28,13 @@ INCLUDES := source #------------------------------------------------------------------------------- # options for code generation #------------------------------------------------------------------------------- -CFLAGS := -Wall -Wextra -Wundef -Wshadow -Wpointer-arith -Wcast-align \ +CFLAGS := -Wall -Wextra -Wundef -Wpointer-arith -Wcast-align \ -O2 -fipa-pta -pipe -ffunction-sections \ $(MACHDEP) CFLAGS += $(INCLUDE) -D__WIIU__ -D__WUT__ -D__WUPS__ -CXXFLAGS := $(CFLAGS) +CXXFLAGS := $(CFLAGS) -std=c++23 ASFLAGS := -g $(ARCH) LDFLAGS = -g $(ARCH) $(RPXSPECS) -Wl,-Map,$(notdir $*.map) $(WUPSSPECS) diff --git a/source/main.cpp b/source/main.cpp index 98e80ca..4a2314d 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,318 +1,794 @@ +// SPDX-License-Identifier: MIT + +// standard headers +#include +#include +#include +#include +#include +#include +#include +#include // invoke() +#include +#include // unique_ptr<> +#include // accumulate() +#include +#include // ranges::zip() +#include +#include +#include +#include +#include +#include // pair<> +#include + +// unix headers #include -#include #include #include -#include +#include #include +#include #include -#include -#include -#include -#include +// WUT/WUPS headers #include #include #include -#include #include #include -#include #include +#include -#include -#include -#include -#include +#include "ntp.hpp" -#define SYNCING_ENABLED_CONFIG_ID "enabledSync" -#define DST_ENABLED_CONFIG_ID "enabledDST" -#define NOTIFY_ENABLED_CONFIG_ID "enabledNotify" -#define OFFSET_HOURS_CONFIG_ID "offsetHours" -#define OFFSET_MINUTES_CONFIG_ID "offsetMinutes" -// Seconds between 1900 (NTP epoch) and 2000 (Wii U epoch) -#define NTP_TIMESTAMP_DELTA 3155673600llu + +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("Wii U Time Sync"); +WUPS_PLUGIN_NAME(PLUGIN_NAME); WUPS_PLUGIN_DESCRIPTION("A plugin that synchronizes a Wii U's clock to the Internet."); WUPS_PLUGIN_VERSION("v1.1.0"); -WUPS_PLUGIN_AUTHOR("Nightkingale"); +WUPS_PLUGIN_AUTHOR("Nightkingale, Daniel K. O."); WUPS_PLUGIN_LICENSE("MIT"); WUPS_USE_WUT_DEVOPTAB(); -WUPS_USE_STORAGE("Wii U Time Sync"); +WUPS_USE_STORAGE(PLUGIN_NAME); + + +namespace cfg { + int hours = 0; + int minutes = 0; + int msg_duration = 5; + bool notify = true; + char server[512] = "pool.ntp.org"; + bool sync = false; + int tolerance = 200; + + OSTime offset = 0; // combines hours and minutes offsets +} + + +std::atomic in_progress = false; + + +// RAII type that handles the in_progress flag. + +struct progress_error : std::runtime_error { + progress_error() : + std::runtime_error{"progress_error"} + {} +}; + +struct progress_guard { + progress_guard() + { + bool expected_progress = false; + if (!in_progress.compare_exchange_strong(expected_progress, true)) + throw progress_error{}; + } + + ~progress_guard() + { + in_progress = false; + } +}; + + +// The code below implements a wrapper for std::async() that respects a thread limit. + +std::counting_semaphore async_limit{6}; + + +template +struct semaphore_releaser { + Sem& s; + + semaphore_releaser(Sem& s) : + s(s) + {} + + ~semaphore_releaser() + { + s.release(); + } +}; + + +template +[[nodiscard]] +std::future, std::decay_t...>> +limited_async(Func&& func, + Args&&... args) +{ + async_limit.acquire(); + + try { + return std::async(std::launch::async, + [](auto&& f, auto&&... a) -> auto + { + semaphore_releaser guard{async_limit}; + return std::invoke(std::forward(f), + std::forward(a)...); + }, + std::forward(func), + std::forward(args)...); + } + catch (...) { + async_limit.release(); + throw; + } +} + + +#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, "%.3f 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; +} -bool enabledSync = false; -bool enabledDST = false; -bool enabledNotify = true; -int offsetHours = 0; -int offsetMinutes = 0; -// From https://github.com/lettier/ntpclient/blob/master/source/c/main.c -typedef struct +std::string +format_ntp(ntp::timestamp t) { - uint8_t li_vn_mode; // Eight bits. li, vn, and mode. - // li. Two bits. Leap indicator. - // vn. Three bits. Version number of the protocol. - // mode. Three bits. Client will pick mode 3 for client. + OSTime wt = ntp_to_wiiu(t); + return format_wiiu_time(wt); +} - uint8_t stratum; // Eight bits. Stratum level of the local clock. - uint8_t poll; // Eight bits. Maximum interval between successive messages. - uint8_t precision; // Eight bits. Precision of the local clock. - uint32_t rootDelay; // 32 bits. Total round trip delay time. - uint32_t rootDispersion; // 32 bits. Max error aloud from primary clock source. - uint32_t refId; // 32 bits. Reference clock identifier. +std::vector +split(const std::string& input, + const std::string& separators) +{ + using std::string; - uint32_t refTm_s; // 32 bits. Reference time-stamp seconds. - uint32_t refTm_f; // 32 bits. Reference time-stamp fraction of a second. + std::vector result; - uint32_t origTm_s; // 32 bits. Originate time-stamp seconds. - uint32_t origTm_f; // 32 bits. Originate time-stamp fraction of a second. + string::size_type start = 0; + 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); + } - uint32_t rxTm_s; // 32 bits. Received time-stamp seconds. - uint32_t rxTm_f; // 32 bits. Received time-stamp fraction of a second. + return result; +} - uint32_t txTm_s; // 32 bits and the most important field the client cares about. Transmit time-stamp seconds. - uint32_t txTm_f; // 32 bits. Transmit time-stamp fraction of a second. -} ntp_packet; // Total: 384 bits or 48 bytes. +extern "C" int32_t CCRSysSetSystemTime(OSTime time); // from nn_ccr +extern "C" BOOL __OSSetAbsoluteSystemTime(OSTime time); // from coreinit -extern "C" int32_t CCRSysSetSystemTime(OSTime time); -extern "C" BOOL __OSSetAbsoluteSystemTime(OSTime time); -bool SetSystemTime(OSTime time) +bool +apply_clock_correction(double correction) { + OSTime correction_ticks = correction * OSTimerClockSpeed; + + OSTime now = OSGetTime(); + OSTime corrected = now + correction_ticks; + nn::pdm::NotifySetTimeBeginEvent(); - if (CCRSysSetSystemTime(time) != 0) { + if (CCRSysSetSystemTime(corrected)) { nn::pdm::NotifySetTimeEndEvent(); return false; } - BOOL res = __OSSetAbsoluteSystemTime(time); + bool res = __OSSetAbsoluteSystemTime(corrected); nn::pdm::NotifySetTimeEndEvent(); - return res != FALSE; + return res; } -OSTime NTPGetTime(const char* hostname) -{ - ntp_packet packet; - memset(&packet, 0, sizeof(packet)); - // Set the first byte's bits to 00,011,011 for li = 0, vn = 3, and mode = 3. The rest will be left set to zero. - packet.li_vn_mode = 0x1b; +// RAII class to close down a socket +struct socket_guard { + int fd; - // Create a socket - int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (sockfd < 0) { - return 0; + socket_guard(int ns, int st, int pr) : + fd{socket(ns, st, pr)} + {} + + ~socket_guard() + { + if (fd != -1) + close(); } - // Get host address by name - struct hostent* server = gethostbyname(hostname); - if (!server) { - return 0; + void + close() + { + ::close(fd); + fd = -1; } +}; - // Prepare socket address - struct sockaddr_in serv_addr; - memset(&serv_addr, 0, sizeof(serv_addr)); - serv_addr.sin_family = AF_INET; - // Copy the server's IP address to the server address structure. - memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length); +// 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"}; - // Convert the port number integer to network big-endian style and save it to the server address structure. - serv_addr.sin_port = htons(123); // UDP port + connect(s.fd, reinterpret_cast(&address), sizeof address); - // Call up the server using its IP address and port number. - if (connect(sockfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) < 0) { - close(sockfd); - return 0; - } + ntp::packet packet; + packet.version(4); + packet.mode(ntp::packet::mode::client); - // Send it the NTP packet it wants. If n == -1, it failed. - if (write(sockfd, &packet, sizeof(packet)) < 0) { - close(sockfd); - return 0; + + 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"}; } - // Wait and receive the packet back from the server. If n == -1, it failed. - if (read(sockfd, &packet, sizeof(packet)) < 0) { - close(sockfd); - return 0; + 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"}; } - // Close the socket - close(sockfd); - - // These two fields contain the time-stamp seconds as the packet left the NTP server. - // The number of seconds correspond to the seconds passed since 1900. - // ntohl() converts the bit/byte order from the network's to host's "endianness". - packet.txTm_s = ntohl(packet.txTm_s); // Time-stamp seconds. - packet.txTm_f = ntohl(packet.txTm_f); // Time-stamp fraction of a second. - - OSTime tick = 0; - // Convert seconds to ticks and adjust timestamp - tick += OSSecondsToTicks(packet.txTm_s - NTP_TIMESTAMP_DELTA); - // Convert fraction to ticks - tick += OSNanosecondsToTicks((packet.txTm_f * 1000000000llu) >> 32); - return tick; + + 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 }; } -void updateTime() { - uint64_t roundtripStart = 0; - uint64_t roundtripEnd = 0; - // Get the time from the server. - roundtripStart = OSGetTime(); - OSTime time = NTPGetTime("pool.ntp.org"); // Connect to the time server. - roundtripEnd = OSGetTime(); +// 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; +}; + - if (time == 0) { - return; // Probably didn't connect correctly. +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 } - // Calculate the roundtrip time. - uint64_t roundtrip = roundtripEnd - roundtripStart; + std::vector result; + + // walk through the linked list + for (auto a = info.get(); a; a = a->ai_next) { - // Calculate the time it took to get the time from the server. - uint64_t timeTook = roundtrip / 2; + // sanity check: Wii U only supports IPv4 + if (a->ai_addrlen != sizeof(struct sockaddr_in)) + throw std::logic_error{"getaddrinfo() returned invalid result"}; - // Subtract the time it took to get the time from the server. - time -= timeTook; + 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; - if (offsetHours < 0) { - time -= OSSecondsToTicks(abs(offsetHours) * 60 * 60); - } else { - time += OSSecondsToTicks(offsetHours * 60 * 60); + result.push_back(std::move(item)); } - if (enabledDST) { - time += OSSecondsToTicks(60 * 60); // DST adds an hour. + 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; +} + + +void +update_time() +try +{ + progress_guard guard; + + 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); + + 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()); + } } - time += OSSecondsToTicks(offsetMinutes * 60); + // 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()); + } - OSTime currentTime = OSGetTime(); - int timeDifference = abs(time - currentTime); - if (static_cast(timeDifference) <= OSMillisecondsToTicks(250)) { - return; // Time difference is within 250 milliseconds, no need to update. + if (corrections.empty()) { + report_error("no NTP server could be used"); + return; } - SetSystemTime(time); // This finally sets the console time. + double avg_correction = std::accumulate(corrections.begin(), + corrections.end(), + 0.0) + / corrections.size(); - if (enabledNotify) { - NotificationModule_AddInfoNotification("The time has been changed based on your Internet connection."); + 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)); +} +catch (progress_error&) { + report_info("skipping NTP task: already in progress"); } -INITIALIZE_PLUGIN() { + +INITIALIZE_PLUGIN() +{ WUPSStorageError storageRes = WUPS_OpenStorage(); // Check if the plugin's settings have been saved before. if (storageRes == WUPS_STORAGE_ERROR_SUCCESS) { - if ((storageRes = WUPS_GetBool(nullptr, SYNCING_ENABLED_CONFIG_ID, &enabledSync)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreBool(nullptr, SYNCING_ENABLED_CONFIG_ID, enabledSync); - } + if (WUPS_GetBool(nullptr, CFG_SYNC, &cfg::sync) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreBool(nullptr, CFG_SYNC, cfg::sync); - if ((storageRes = WUPS_GetBool(nullptr, DST_ENABLED_CONFIG_ID, &enabledDST)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreBool(nullptr, DST_ENABLED_CONFIG_ID, enabledDST); - } + if (WUPS_GetBool(nullptr, CFG_NOTIFY, &cfg::notify) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreBool(nullptr, CFG_NOTIFY, cfg::notify); - if ((storageRes = WUPS_GetBool(nullptr, NOTIFY_ENABLED_CONFIG_ID, &enabledNotify)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreBool(nullptr, NOTIFY_ENABLED_CONFIG_ID, enabledNotify); - } + if (WUPS_GetInt(nullptr, CFG_MSG_DURATION, &cfg::msg_duration) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreInt(nullptr, CFG_MSG_DURATION, cfg::msg_duration); - if ((storageRes = WUPS_GetInt(nullptr, OFFSET_HOURS_CONFIG_ID, &offsetHours)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreInt(nullptr, OFFSET_HOURS_CONFIG_ID, offsetHours); - } + if (WUPS_GetInt(nullptr, CFG_HOURS, &cfg::hours) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreInt(nullptr, CFG_HOURS, cfg::hours); - if ((storageRes = WUPS_GetInt(nullptr, OFFSET_MINUTES_CONFIG_ID, &offsetMinutes)) == WUPS_STORAGE_ERROR_NOT_FOUND) { - WUPS_StoreInt(nullptr, OFFSET_MINUTES_CONFIG_ID, offsetMinutes); - } + if (WUPS_GetInt(nullptr, CFG_MINUTES, &cfg::minutes) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreInt(nullptr, CFG_MINUTES, cfg::minutes); - NotificationModule_InitLibrary(); // Set up for notifications. - WUPS_CloseStorage(); // Close the storage. - } + if (WUPS_GetInt(nullptr, CFG_TOLERANCE, &cfg::tolerance) == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreInt(nullptr, CFG_TOLERANCE, cfg::tolerance); + + if (WUPS_GetString(nullptr, CFG_SERVER, cfg::server, sizeof cfg::server) + == WUPS_STORAGE_ERROR_NOT_FOUND) + WUPS_StoreString(nullptr, CFG_SERVER, cfg::server); - if (enabledSync) { - updateTime(); // Update time when plugin is loaded. + WUPS_CloseStorage(); } -} -void syncingEnabled(ConfigItemBoolean *item, bool value) -{ - (void)item; - // If false, bro is literally a time traveler! - WUPS_StoreBool(nullptr, SYNCING_ENABLED_CONFIG_ID, value); - enabledSync = value; -} + NotificationModule_InitLibrary(); // Set up for notifications. -void savingsEnabled(ConfigItemBoolean *item, bool value) -{ - (void)item; - WUPS_StoreBool(nullptr, DST_ENABLED_CONFIG_ID, value); - enabledDST = value; + if (cfg::sync) + update_time(); // Update time when plugin is loaded. } -void notifyEnabled(ConfigItemBoolean *item, bool value) -{ - (void)item; - WUPS_StoreBool(nullptr, NOTIFY_ENABLED_CONFIG_ID, value); - enabledNotify = value; -} -void onHourOffsetChanged(ConfigItemIntegerRange *item, int32_t offset) +WUPS_GET_CONFIG() { - (void)item; - WUPS_StoreInt(nullptr, OFFSET_HOURS_CONFIG_ID, offset); - offsetHours = offset; -} - -void onMinuteOffsetChanged(ConfigItemIntegerRange *item, int32_t offset) -{ - (void)item; - WUPS_StoreInt(nullptr, OFFSET_MINUTES_CONFIG_ID, offset); - offsetMinutes = offset; -} - -WUPS_GET_CONFIG() { - if (WUPS_OpenStorage() != WUPS_STORAGE_ERROR_SUCCESS) { + if (WUPS_OpenStorage() != WUPS_STORAGE_ERROR_SUCCESS) return 0; - } WUPSConfigHandle settings; - WUPSConfig_CreateHandled(&settings, "Wii U Time Sync"); + WUPSConfig_CreateHandled(&settings, PLUGIN_NAME); WUPSConfigCategoryHandle config; WUPSConfig_AddCategoryByNameHandled(settings, "Configuration", &config); WUPSConfigCategoryHandle preview; WUPSConfig_AddCategoryByNameHandled(settings, "Preview Time", &preview); - WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, "enabledSync", "Syncing Enabled", enabledSync, &syncingEnabled); - WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, "enabledDST", "Daylight Savings", enabledDST, &savingsEnabled); - WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, "enabledNotify", "Receive Notifications", enabledNotify, ¬ifyEnabled); - WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, "offsetHours", "Time Offset (hours)", offsetHours, -12, 14, &onHourOffsetChanged); - WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, "offsetMinutes", "Time Offset (minutes)", offsetMinutes, 0, 59, &onMinuteOffsetChanged); - - OSCalendarTime ct; - OSTicksToCalendarTime(OSGetTime(), &ct); - char timeString[256]; - snprintf(timeString, 255, "Current Time: %04d-%02d-%02d %02d:%02d:%02d:%04d:%04d\n", ct.tm_year, ct.tm_mon + 1, ct.tm_mday, ct.tm_hour, ct.tm_min, ct.tm_sec, ct.tm_msec, ct.tm_usec); - WUPSConfigItemStub_AddToCategoryHandled(settings, preview, "time", timeString); + WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, CFG_SYNC, + "Syncing Enabled", + cfg::sync, + [](ConfigItemBoolean*, bool value) + { + WUPS_StoreBool(nullptr, CFG_NOTIFY, value); + cfg::notify = value; + }); + WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, CFG_NOTIFY, + "Show Notifications", + cfg::notify, + [](ConfigItemBoolean*, bool value) + { + WUPS_StoreBool(nullptr, CFG_NOTIFY, value); + cfg::notify = value; + }); + WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_MSG_DURATION, + "Messages Duration (seconds)", + cfg::msg_duration, 0, 30, + [](ConfigItemIntegerRange*, int32_t value) + { + WUPS_StoreInt(nullptr, CFG_MSG_DURATION, + value); + cfg::msg_duration = value; + }); + WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_HOURS, + "Hours Offset", + cfg::hours, -12, 14, + [](ConfigItemIntegerRange*, int32_t value) + { + WUPS_StoreInt(nullptr, CFG_HOURS, value); + cfg::hours = value; + }); + WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_MINUTES, + "Minutes Offset", + cfg::minutes, 0, 59, + [](ConfigItemIntegerRange*, int32_t value) + { + WUPS_StoreInt(nullptr, CFG_MINUTES, + value); + cfg::minutes = value; + }); + WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_TOLERANCE, + "Tolerance (milliseconds)", + cfg::tolerance, 0, 5000, + [](ConfigItemIntegerRange*, int32_t value) + { + WUPS_StoreInt(nullptr, CFG_TOLERANCE, + value); + cfg::tolerance = value; + }); + + // show current NTP server address, no way to change it. + std::string server = "NTP servers: "s + cfg::server; + WUPSConfigItemStub_AddToCategoryHandled(settings, config, CFG_SERVER, server.c_str()); + + WUPSConfigItemStub_AddToCategoryHandled(settings, preview, "time", + format_wiiu_time(OSGetTime()).c_str()); return settings; } -WUPS_CONFIG_CLOSED() { - if (enabledSync) { - std::thread updateTimeThread(updateTime); - updateTimeThread.detach(); // Update time when settings are closed. - } - + +WUPS_CONFIG_CLOSED() +{ + std::jthread update_time_thread(update_time); + update_time_thread.detach(); // Update time when settings are closed. + WUPS_CloseStorage(); // Save all changes. } diff --git a/source/ntp.hpp b/source/ntp.hpp new file mode 100644 index 0000000..451d44b --- /dev/null +++ b/source/ntp.hpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +#ifndef NTP_HPP +#define NTP_HPP + +#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 a u16.16 fixed-point format. + using short_timestamp = std::uint32_t; + + + // Note: all fields are big-endian + struct packet { + + enum class leap : 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 { + reserved = 0, + active = 1, + passive = 2, + client = 3, + server = 4, + broadcast = 5, + control = 6, + reserved_private = 7 + }; + + + // Note: all fields are zero-initialized by default constructor. + std::uint8_t lvm = 0; // leap, version and mode + std::uint8_t stratum = 0; // Stratum level of the local clock. + std::int8_t poll_exp = 0; // Maximum interval between successive messages. + std::int8_t precision_exp = 0; // Precision of the local clock. + + short_timestamp root_delay = 0; // Total round trip delay time to the reference clock. + 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. + + + void leap(leap x) + { + lvm = static_cast(x) | (lvm & 0b0011'1111); + } + + void version(unsigned v) + { + lvm = ((v << 3) & 0b0011'1000) | (lvm & 0b1100'0111); + } + + void mode(mode m) + { + lvm = static_cast(m) | (lvm & 0b1111'1000); + } + + }; + + static_assert(sizeof(packet) == 48); + +} // namespace ntp + + +#endif From 2d305099a8ecce4992566497018baec77e0401da Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Sat, 7 Oct 2023 13:25:02 -0300 Subject: [PATCH 03/17] fixed a typo, now the sync option should work properly --HG-- branch : upstream --- source/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/main.cpp b/source/main.cpp index 4a2314d..bff29a2 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -727,8 +727,8 @@ WUPS_GET_CONFIG() cfg::sync, [](ConfigItemBoolean*, bool value) { - WUPS_StoreBool(nullptr, CFG_NOTIFY, value); - cfg::notify = value; + WUPS_StoreBool(nullptr, CFG_SYNC, value); + cfg::sync = value; }); WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, CFG_NOTIFY, "Show Notifications", From de4590296a5a22cc54d2159286f5c19c4029f9c9 Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Tue, 10 Oct 2023 11:35:18 -0300 Subject: [PATCH 04/17] backported changes to upstream --HG-- branch : upstream --- .gitignore | 8 +- Makefile | 28 ++- source/main.cpp | 433 +++++++++++++++++++++++------------- source/wupsxx/base_item.cpp | 151 +++++++++++++ source/wupsxx/base_item.hpp | 48 ++++ source/wupsxx/bool_item.cpp | 81 +++++++ source/wupsxx/bool_item.hpp | 39 ++++ source/wupsxx/category.cpp | 38 ++++ source/wupsxx/category.hpp | 33 +++ source/wupsxx/config.cpp | 38 ++++ source/wupsxx/config.hpp | 32 +++ source/wupsxx/int_item.cpp | 94 ++++++++ source/wupsxx/int_item.hpp | 40 ++++ source/wupsxx/storage.cpp | 80 +++++++ source/wupsxx/storage.hpp | 21 ++ source/wupsxx/text_item.cpp | 33 +++ source/wupsxx/text_item.hpp | 25 +++ 17 files changed, 1053 insertions(+), 169 deletions(-) create mode 100644 source/wupsxx/base_item.cpp create mode 100644 source/wupsxx/base_item.hpp create mode 100644 source/wupsxx/bool_item.cpp create mode 100644 source/wupsxx/bool_item.hpp create mode 100644 source/wupsxx/category.cpp create mode 100644 source/wupsxx/category.hpp create mode 100644 source/wupsxx/config.cpp create mode 100644 source/wupsxx/config.hpp create mode 100644 source/wupsxx/int_item.cpp create mode 100644 source/wupsxx/int_item.hpp create mode 100644 source/wupsxx/storage.cpp create mode 100644 source/wupsxx/storage.hpp create mode 100644 source/wupsxx/text_item.cpp create mode 100644 source/wupsxx/text_item.hpp diff --git a/.gitignore b/.gitignore index e2c40e1..0c804b1 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,10 @@ dkms.conf *.wuhb *.lst *.rpx -*.wps \ No newline at end of file +*.wps +*.json +*.zip +compile_flags.txt + +# Temporary files +*~ diff --git a/Makefile b/Makefile index 231a999..b7af3cf 100644 --- a/Makefile +++ b/Makefile @@ -21,22 +21,28 @@ WUT_ROOT := $(DEVKITPRO)/wut #------------------------------------------------------------------------------- TARGET := Wii_U_Time_Sync BUILD := build -SOURCES := source +SOURCES := source source/wupsxx DATA := data INCLUDES := source +# Be verbose by default. +V ?= 1 + #------------------------------------------------------------------------------- # options for code generation #------------------------------------------------------------------------------- -CFLAGS := -Wall -Wextra -Wundef -Wpointer-arith -Wcast-align \ - -O2 -fipa-pta -pipe -ffunction-sections \ - $(MACHDEP) +WARN_FLAGS := -Wall -Wextra -Wundef -Wpointer-arith -Wcast-align + +OPTFLAGS := -O2 -fipa-pta -ffunction-sections -CFLAGS += $(INCLUDE) -D__WIIU__ -D__WUT__ -D__WUPS__ +CFLAGS := $(WARN_FLAGS) $(OPTFLAGS) $(MACHDEP) + +CPPFLAGS := $(INCLUDE) -D__WIIU__ -D__WUT__ -D__WUPS__ CXXFLAGS := $(CFLAGS) -std=c++23 ASFLAGS := -g $(ARCH) + LDFLAGS = -g $(ARCH) $(RPXSPECS) -Wl,-Map,$(notdir $*.map) $(WUPSSPECS) LIBS := -lnotifications -lwups -lwut @@ -92,19 +98,19 @@ export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) -.PHONY: $(BUILD) clean all +.PHONY: $(BUILD) clean all upload #------------------------------------------------------------------------------- all: $(BUILD) $(BUILD): @[ -d $@ ] || mkdir -p $@ - @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + $(MAKE) -C $(BUILD) -f $(CURDIR)/Makefile V=$(V) #------------------------------------------------------------------------------- clean: - @echo clean ... - @rm -fr $(BUILD) $(TARGET).wps $(TARGET).elf + $(info clean ...) + $(RM) -r $(BUILD) $(TARGET).wps $(TARGET).elf #------------------------------------------------------------------------------- else @@ -127,8 +133,8 @@ $(OFILES_SRC) : $(HFILES_BIN) #------------------------------------------------------------------------------- %.bin.o %_bin.h : %.bin #------------------------------------------------------------------------------- - @echo $(notdir $<) - @$(bin2o) + $(info $(notdir $<)) + $(bin2o) -include $(DEPENDS) diff --git a/source/main.cpp b/source/main.cpp index d05f285..89ee8e5 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -3,6 +3,7 @@ // standard headers #include #include +#include #include #include #include @@ -10,6 +11,7 @@ #include #include // invoke() #include +#include #include // unique_ptr<> #include // accumulate() #include @@ -42,6 +44,13 @@ #include "ntp.hpp" +#include "wupsxx/bool_item.hpp" +#include "wupsxx/category.hpp" +#include "wupsxx/config.hpp" +#include "wupsxx/int_item.hpp" +#include "wupsxx/storage.hpp" +#include "wupsxx/text_item.hpp" + using namespace std::literals; @@ -68,60 +77,47 @@ WUPS_USE_STORAGE(PLUGIN_NAME); namespace cfg { - int hours = 0; - int minutes = 0; - int msg_duration = 5; - bool notify = false; - char server[512] = "pool.ntp.org"; - bool sync = false; - int tolerance = 250; + 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 } -std::atomic in_progress = false; - - -// RAII type that handles the in_progress flag. +// The code below implements a wrapper for std::async() that respects a thread limit. -struct progress_error : std::runtime_error { - progress_error() : - std::runtime_error{"progress_error"} - {} +enum class guard_type { + acquire_and_release, + only_acquire, + only_release }; -struct progress_guard { - progress_guard() - { - bool expected_progress = false; - if (!in_progress.compare_exchange_strong(expected_progress, true)) - throw progress_error{}; - } - ~progress_guard() +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} { - in_progress = false; + if (type == guard_type::acquire_and_release || + type == guard_type::only_acquire) + sem.acquire(); } -}; - - -// The code below implements a wrapper for std::async() that respects a thread limit. - -std::counting_semaphore async_limit{6}; - -template -struct semaphore_releaser { - Sem& s; - - semaphore_releaser(Sem& s) : - s(s) - {} - - ~semaphore_releaser() + ~semaphore_guard() { - s.release(); + if (type == guard_type::acquire_and_release || + type == guard_type::only_release) + sem.release(); } }; @@ -133,23 +129,25 @@ std::future, std::decay_t limited_async(Func&& func, Args&&... args) { - async_limit.acquire(); + static std::counting_semaphore async_limit{6}; - try { - return std::async(std::launch::async, - [](auto&& f, auto&&... a) -> auto - { - semaphore_releaser guard{async_limit}; - return std::invoke(std::forward(f), - std::forward(a)...); - }, - std::forward(func), - std::forward(args)...); - } - catch (...) { - async_limit.release(); - throw; - } + 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; } @@ -583,20 +581,55 @@ operator <(const struct sockaddr_in& a, } +// 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_time() -try +update_offset() { - if (!cfg::sync) - return; - - progress_guard guard; - 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,;"); @@ -637,7 +670,7 @@ try corrections.push_back(correction); report_info(to_string(address) + ": correction = "s + seconds_to_human(correction) - + ", latency = "s + seconds_to_human(latency) + "."s); + + ", latency = "s + seconds_to_human(latency)); } catch (std::exception& e) { report_error(to_string(address) + ": "s + e.what()); @@ -670,8 +703,18 @@ try if (cfg::notify) report_success("Clock corrected by " + seconds_to_human(avg_correction)); } -catch (progress_error&) { - report_info("Skipping NTP task: already in progress."); + + +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; } @@ -680,27 +723,14 @@ INITIALIZE_PLUGIN() WUPSStorageError storageRes = WUPS_OpenStorage(); // Check if the plugin's settings have been saved before. if (storageRes == WUPS_STORAGE_ERROR_SUCCESS) { - if (WUPS_GetBool(nullptr, CFG_SYNC, &cfg::sync) == WUPS_STORAGE_ERROR_NOT_FOUND) - WUPS_StoreBool(nullptr, CFG_SYNC, cfg::sync); - - if (WUPS_GetBool(nullptr, CFG_NOTIFY, &cfg::notify) == WUPS_STORAGE_ERROR_NOT_FOUND) - WUPS_StoreBool(nullptr, CFG_NOTIFY, cfg::notify); - if (WUPS_GetInt(nullptr, CFG_MSG_DURATION, &cfg::msg_duration) == WUPS_STORAGE_ERROR_NOT_FOUND) - WUPS_StoreInt(nullptr, CFG_MSG_DURATION, cfg::msg_duration); - - if (WUPS_GetInt(nullptr, CFG_HOURS, &cfg::hours) == WUPS_STORAGE_ERROR_NOT_FOUND) - WUPS_StoreInt(nullptr, CFG_HOURS, cfg::hours); - - if (WUPS_GetInt(nullptr, CFG_MINUTES, &cfg::minutes) == WUPS_STORAGE_ERROR_NOT_FOUND) - WUPS_StoreInt(nullptr, CFG_MINUTES, cfg::minutes); - - if (WUPS_GetInt(nullptr, CFG_TOLERANCE, &cfg::tolerance) == WUPS_STORAGE_ERROR_NOT_FOUND) - WUPS_StoreInt(nullptr, CFG_TOLERANCE, cfg::tolerance); - - if (WUPS_GetString(nullptr, CFG_SERVER, cfg::server, sizeof cfg::server) - == WUPS_STORAGE_ERROR_NOT_FOUND) - WUPS_StoreString(nullptr, CFG_SERVER, cfg::server); + 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); WUPS_CloseStorage(); } @@ -712,81 +742,170 @@ INITIALIZE_PLUGIN() } +struct preview_item : wups::text_item { + + wups::category* category; + + std::map server_items; + std::map address_items; + + + preview_item(wups::category* cat) : + wups::text_item{"", "Clock", "Press A"}, + category{cat} + {} + + + void + on_button_pressed(WUPSConfigButtons buttons) + override + { + wups::text_item::on_button_pressed(buttons); + + if (buttons & WUPS_CONFIG_BUTTON_A) { + try { + using std::make_unique; + + update_offset(); + + for (auto& [key, value] : server_items) + value->text.clear(); + + for (auto& [key, value] : address_items) + value->text.clear(); + + std::vector servers = split(cfg::server, " \t,;"); + + // first, ensure each server has a text_item + for (const auto& server : servers) + if (!server_items[server]) { + auto item = make_unique("", server); + server_items[server] = item.get(); + category->add(std::move(item)); + } + + addrinfo_query query = { + .family = AF_INET, + .socktype = SOCK_DGRAM, + .protocol = IPPROTO_UDP + }; + + std::set addresses; + for (const auto& server : servers) { + auto item = server_items.at(server); + try { + auto infos = get_address_info(server, "123", query); + + item->text = std::to_string(infos.size()) + " IP " + + (infos.size() > 1 ? "addresses" : "address"); + + for (auto info : infos) + addresses.insert(info.address); + } + catch (std::exception& e) { + item->text = e.what(); + } + } + + std::vector corrections; + for (auto addr : addresses) { + // ensure each address has a text_item + if (!address_items[addr]) { + auto item = make_unique("", to_string(addr)); + address_items[addr] = item.get(); + category->add(std::move(item)); + } + auto item = address_items.at(addr); + + try { + auto [correction, latency] = ntp_query(addr); + item->text += "correction = " + seconds_to_human(correction) + + ", latency = " + seconds_to_human(latency); + + corrections.push_back(correction); + } + catch (std::exception& e) { + item->text = e.what(); + } + } + + text = format_wiiu_time(OSGetTime()); + + if (corrections.empty()) + text += ": no NTP server could be used."; + else { + double avg_correction = std::accumulate(corrections.begin(), + corrections.end(), + 0.0) + / corrections.size(); + text += " is off by "s + seconds_to_human(avg_correction); + } + + } + catch (std::exception& e) { + text = "Error: "s + e.what(); + } + + } + + } + +}; + + WUPS_GET_CONFIG() { if (WUPS_OpenStorage() != WUPS_STORAGE_ERROR_SUCCESS) return 0; - WUPSConfigHandle settings; - WUPSConfig_CreateHandled(&settings, PLUGIN_NAME); - - WUPSConfigCategoryHandle config; - WUPSConfig_AddCategoryByNameHandled(settings, "Configuration", &config); - WUPSConfigCategoryHandle preview; - WUPSConfig_AddCategoryByNameHandled(settings, "Preview Time", &preview); - - WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, CFG_SYNC, - "Syncing Enabled", - cfg::sync, - [](ConfigItemBoolean*, bool value) - { - WUPS_StoreBool(nullptr, CFG_SYNC, value); - cfg::sync = value; - }); - WUPSConfigItemBoolean_AddToCategoryHandled(settings, config, CFG_NOTIFY, - "Show Notifications", - cfg::notify, - [](ConfigItemBoolean*, bool value) - { - WUPS_StoreBool(nullptr, CFG_NOTIFY, value); - cfg::notify = value; - }); - WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_HOURS, - "Time Offset (hours)", - cfg::hours, -12, 14, - [](ConfigItemIntegerRange*, int32_t value) - { - WUPS_StoreInt(nullptr, CFG_HOURS, value); - cfg::hours = value; - }); - WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_MINUTES, - "Time Offset (minutes)", - cfg::minutes, 0, 59, - [](ConfigItemIntegerRange*, int32_t value) - { - WUPS_StoreInt(nullptr, CFG_MINUTES, - value); - cfg::minutes = value; - }); - WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_MSG_DURATION, - "Message Duration (seconds)", - cfg::msg_duration, 0, 30, - [](ConfigItemIntegerRange*, int32_t value) - { - WUPS_StoreInt(nullptr, CFG_MSG_DURATION, - value); - cfg::msg_duration = value; - }); - WUPSConfigItemIntegerRange_AddToCategoryHandled(settings, config, CFG_TOLERANCE, - "Tolerance (milliseconds)", - cfg::tolerance, 0, 5000, - [](ConfigItemIntegerRange*, int32_t value) - { - WUPS_StoreInt(nullptr, CFG_TOLERANCE, - value); - cfg::tolerance = value; - }); - - // show current NTP server address, no way to change it. - std::string server = "NTP Servers: "s + cfg::server; - WUPSConfigItemStub_AddToCategoryHandled(settings, config, CFG_SERVER, server.c_str()); - - // Prepare the time to be shown for the user. - std::string time = "Current Time: "s + format_wiiu_time(OSGetTime()); - WUPSConfigItemStub_AddToCategoryHandled(settings, preview, "time", - time.c_str()); - - return settings; + using std::make_unique; + + try { + + auto config = make_unique("Configuration"); + auto preview = make_unique("Preview"); + + 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, L/R 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)); + + preview->add(make_unique(preview.get())); + + auto root = make_unique(PLUGIN_NAME); + root->add(std::move(config)); + root->add(std::move(preview)); + + return root.release()->handle; + + } + catch (...) { + return 0; + } } diff --git a/source/wupsxx/base_item.cpp b/source/wupsxx/base_item.cpp new file mode 100644 index 0000000..fdaad5a --- /dev/null +++ b/source/wupsxx/base_item.cpp @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT + +#include +#include + +#include "base_item.hpp" + + +namespace wups { + + base_item::base_item(const std::string& key, + const std::string& name) : + key{key}, + name{name} + { + WUPSConfigCallbacks_t cb; + + cb.getCurrentValueDisplay = [](void* ctx, char* buf, int size) -> int + { + if (!ctx) + return -1; + auto item = reinterpret_cast(ctx); + return item->get_current_value_display(buf, size); + }; + + cb.getCurrentValueSelectedDisplay = [](void* ctx, char* buf, int size) -> int + { + if (!ctx) + return -1; + auto item = reinterpret_cast(ctx); + return item->get_current_value_selected_display(buf, size); + }; + + cb.onSelected = [](void* ctx, bool is_selected) + { + if (!ctx) + return; + auto item = reinterpret_cast(ctx); + item->on_selected(is_selected); + }; + + cb.restoreDefault = [](void* ctx) + { + if (!ctx) + return; + auto item = reinterpret_cast(ctx); + item->restore(); + }; + + cb.isMovementAllowed = [](void* ctx) -> bool + { + if (!ctx) + return true; + auto item = reinterpret_cast(ctx); + return item->is_movement_allowed(); + }; + + cb.callCallback = [](void* ctx) -> bool + { + if (!ctx) + return false; + auto item = reinterpret_cast(ctx); + return item->callback(); + }; + + cb.onButtonPressed = [](void* ctx, WUPSConfigButtons button) + { + if (!ctx) + return; + auto item = reinterpret_cast(ctx); + item->on_button_pressed(button); + }; + + // Called when WUPS is destroying the object. + cb.onDelete = [](void* ctx) + { + if (!ctx) + return; + auto item = reinterpret_cast(ctx); + item->handle = 0; + delete item; + }; + + + if (WUPSConfigItem_Create(&handle, key.c_str(), name.c_str(), cb, this) < 0) + throw std::runtime_error{"could not create config item"}; + + } + + + base_item::~base_item() + { + if (handle) + WUPSConfigItem_Destroy(handle); + } + + + int + base_item::get_current_value_display(char* buf, + std::size_t size) + const + { + std::snprintf(buf, size, "NOT IMPLEMENTED"); + return 0; + } + + + int + base_item::get_current_value_selected_display(char* buf, + std::size_t size) + const + { + return get_current_value_display(buf, size); + } + + + void + base_item::on_selected(bool) + const + {} + + + void + base_item::restore() + {} + + + bool + base_item::is_movement_allowed() + const + { + return true; + } + + + bool + base_item::callback() + { + return false; + } + + + void + base_item::on_button_pressed(WUPSConfigButtons buttons) + { + if (buttons & WUPS_CONFIG_BUTTON_X) + restore(); + } + + +} // namespace wups diff --git a/source/wupsxx/base_item.hpp b/source/wupsxx/base_item.hpp new file mode 100644 index 0000000..98b4a0f --- /dev/null +++ b/source/wupsxx/base_item.hpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_BASE_ITEM_HPP +#define WUPSXX_BASE_ITEM_HPP + +#include + +#include + + +namespace wups { + + struct base_item { + + WUPSConfigItemHandle handle = 0; + std::string key; + std::string name; + + + base_item(const std::string& key, + const std::string& name); + + // disallow moving, since the callbacks store the `this` pointer. + base_item(base_item&&) = delete; + + + virtual ~base_item(); + + + virtual int get_current_value_display(char* buf, std::size_t size) const; + + virtual int get_current_value_selected_display(char* buf, std::size_t size) const; + + virtual void on_selected(bool is_selected) const; + + virtual void restore(); + + virtual bool is_movement_allowed() const; + + virtual bool callback(); + + virtual void on_button_pressed(WUPSConfigButtons buttons); + + }; + +} // namespace wups + +#endif diff --git a/source/wupsxx/bool_item.cpp b/source/wupsxx/bool_item.cpp new file mode 100644 index 0000000..4132927 --- /dev/null +++ b/source/wupsxx/bool_item.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +#include +#include + +#include "bool_item.hpp" +#include "storage.hpp" + + +namespace wups { + + bool_item::bool_item(const std::string& key, + const std::string& name, + bool& variable) : + base_item{key, name}, + variable(variable), + default_value{variable} + {} + + + int + bool_item::get_current_value_display(char* buf, std::size_t size) + const + { + std::snprintf(buf, size, "%s", + variable ? true_str.c_str() : false_str.c_str()); + return 0; + } + + + int + bool_item::get_current_value_selected_display(char* buf, std::size_t size) + const + { + if (variable) + std::snprintf(buf, size, "< %s ", true_str.c_str()); + else + std::snprintf(buf, size, " %s >", false_str.c_str()); + return 0; + } + + + void + bool_item::restore() + { + variable = default_value; + } + + + bool + bool_item::callback() + { + if (key.empty()) + return false; + + try { + store(key, variable); + return true; + } + catch (...) { + return false; + } + } + + + void + bool_item::on_button_pressed(WUPSConfigButtons buttons) + { + base_item::on_button_pressed(buttons); + + if (buttons & WUPS_CONFIG_BUTTON_A) + variable = !variable; + + if (buttons & WUPS_CONFIG_BUTTON_LEFT) + variable = false; + + if (buttons & WUPS_CONFIG_BUTTON_RIGHT) + variable = true; + } + +} // namespace wups diff --git a/source/wupsxx/bool_item.hpp b/source/wupsxx/bool_item.hpp new file mode 100644 index 0000000..09c2585 --- /dev/null +++ b/source/wupsxx/bool_item.hpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_BOOL_ITEM_HPP +#define WUPSXX_BOOL_ITEM_HPP + +#include + +#include "base_item.hpp" + +namespace wups { + + struct bool_item : base_item { + + bool& variable; + bool default_value; + std::string true_str = "true"; + std::string false_str = "false"; + + + bool_item(const std::string& key, + const std::string& name, + bool& variable); + + + virtual int get_current_value_display(char* buf, std::size_t size) const override; + + virtual int get_current_value_selected_display(char* buf, std::size_t size) const override; + + virtual void restore() override; + + virtual bool callback() override; + + virtual void on_button_pressed(WUPSConfigButtons button) override; + + }; + +} // namespace wups + +#endif diff --git a/source/wupsxx/category.cpp b/source/wupsxx/category.cpp new file mode 100644 index 0000000..98849e1 --- /dev/null +++ b/source/wupsxx/category.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +#include + +#include "category.hpp" + + +namespace wups { + + category::category(const std::string& name) + { + if (WUPSConfigCategory_Create(&handle, name.c_str()) < 0) + throw std::runtime_error{"could not create category"}; + } + + + category::~category() + { + if (handle) + WUPSConfigCategory_Destroy(handle); + } + + + void + category::add(std::unique_ptr&& item) + { + if (!item) + throw std::logic_error{"cannot add null item to category"}; + if (!item->handle) + throw std::logic_error{"cannot add null item handle to category"}; + + if (WUPSConfigCategory_AddItem(handle, item->handle) < 0) + throw std::runtime_error{"cannot add item to category"}; + + item.release(); // WUPS now owns this item + } + +} // namespace wups diff --git a/source/wupsxx/category.hpp b/source/wupsxx/category.hpp new file mode 100644 index 0000000..e146832 --- /dev/null +++ b/source/wupsxx/category.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_CATEGORY_HPP +#define WUPSXX_CATEGORY_HPP + +#include +#include + +#include + +#include "base_item.hpp" + + +namespace wups { + + struct category { + + WUPSConfigCategoryHandle handle = 0; + + category(const std::string& name); + + category(category&&) = delete; + + ~category(); + + void add(std::unique_ptr&& item); + + }; + +} // namespace wups + + +#endif diff --git a/source/wupsxx/config.cpp b/source/wupsxx/config.cpp new file mode 100644 index 0000000..ff7d4f3 --- /dev/null +++ b/source/wupsxx/config.cpp @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT + +#include + +#include "config.hpp" + + +namespace wups { + + config::config(const std::string& name) + { + if (WUPSConfig_Create(&handle, name.c_str()) < 0) + throw std::runtime_error{"could not create config"}; + } + + + config::~config() + { + if (handle) + WUPSConfig_Destroy(handle); + } + + + void + config::add(std::unique_ptr&& cat) + { + if (!cat) + throw std::logic_error{"cannot add null category to config"}; + if (!cat->handle) + throw std::logic_error{"cannot add null category handle to config"}; + + if (WUPSConfig_AddCategory(handle, cat->handle) < 0) + throw std::runtime_error{"cannot add category to config"}; + + cat.release(); // WUPS now owns this category + } + +} // namespace wups diff --git a/source/wupsxx/config.hpp b/source/wupsxx/config.hpp new file mode 100644 index 0000000..d05bfdf --- /dev/null +++ b/source/wupsxx/config.hpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_CONFIG_HPP +#define WUPSXX_CONFIG_HPP + +#include +#include + +#include + +#include "category.hpp" + + +namespace wups { + + struct config { + + WUPSConfigHandle handle = 0; + + config(const std::string& name); + + config(config&&) = delete; + + ~config(); + + void add(std::unique_ptr&& cat); + + }; + +} // namespace wups + +#endif diff --git a/source/wupsxx/int_item.cpp b/source/wupsxx/int_item.cpp new file mode 100644 index 0000000..a047c76 --- /dev/null +++ b/source/wupsxx/int_item.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +#include // clamp() +#include +#include + +#include "int_item.hpp" +#include "storage.hpp" + + +namespace wups { + + + int_item::int_item(const std::string& key, + const std::string& name, + int& variable, + int min_value, + int max_value) : + base_item{key, name}, + variable(variable), + default_value{variable}, + min_value{min_value}, + max_value{max_value} + {} + + + int + int_item::get_current_value_display(char* buf, std::size_t size) + const + { + std::snprintf(buf, size, "%d", variable); + return 0; + } + + + int + 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); + return 0; + } + + + void + int_item::restore() + { + variable = default_value; + } + + + bool + int_item::callback() + { + if (key.empty()) + return false; + + try { + store(key, variable); + return true; + } + catch (...) { + return false; + } + } + + + void + int_item::on_button_pressed(WUPSConfigButtons buttons) + { + base_item::on_button_pressed(buttons); + + if (buttons & WUPS_CONFIG_BUTTON_LEFT) + --variable; + + if (buttons & WUPS_CONFIG_BUTTON_RIGHT) + ++variable; + + if (buttons & WUPS_CONFIG_BUTTON_L) + variable -= 50; + + if (buttons & WUPS_CONFIG_BUTTON_R) + variable += 50; + + variable = std::clamp(variable, min_value, max_value); + } + +} // namespace wups diff --git a/source/wupsxx/int_item.hpp b/source/wupsxx/int_item.hpp new file mode 100644 index 0000000..398bf1c --- /dev/null +++ b/source/wupsxx/int_item.hpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_INT_ITEM_HPP +#define WUPSXX_INT_ITEM_HPP + +#include "base_item.hpp" + + +namespace wups { + + struct int_item : base_item { + + int& variable; + int default_value = 0; + int min_value; + int max_value; + + + int_item(const std::string& key, + const std::string& name, + int& variable, + int min_value, + int max_value); + + + virtual int get_current_value_display(char* buf, std::size_t size) const override; + + virtual int get_current_value_selected_display(char* buf, std::size_t size) const override; + + virtual void restore() override; + + virtual bool callback() override; + + virtual void on_button_pressed(WUPSConfigButtons buttons) override; + + }; + +} // namespace wups + +#endif diff --git a/source/wupsxx/storage.cpp b/source/wupsxx/storage.cpp new file mode 100644 index 0000000..59f12d7 --- /dev/null +++ b/source/wupsxx/storage.cpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT + +#include + +#include "storage.hpp" + + +namespace wups { + + template<> + std::expected + load(const std::string& key) + { + bool value; + auto err = WUPS_GetBool(nullptr, key.c_str(), &value); + if (err != WUPS_STORAGE_ERROR_SUCCESS) + return std::unexpected{err}; + return value; + } + + + template<> + std::expected + load(const std::string& key) + { + int value; + auto err = WUPS_GetInt(nullptr, key.c_str(), &value); + if (err != WUPS_STORAGE_ERROR_SUCCESS) + return std::unexpected{err}; + return value; + } + + + template<> + std::expected + load(const std::string& key) + { + // Note: we don't have WUPS_GetSize() so we can't know how big this has to be. + std::string value(1024, '\0'); + auto err = WUPS_GetString(nullptr, key.c_str(), value.data(), value.size()); + if (err != WUPS_STORAGE_ERROR_SUCCESS) + return std::unexpected{err}; + auto end = value.find('\0'); + return value.substr(0, end); + } + + + template<> + void + store(const std::string& key, + const bool& value) + { + auto err = WUPS_StoreBool(nullptr, key.c_str(), value); + if (err != WUPS_STORAGE_ERROR_SUCCESS) + throw err; + } + + + template<> + void + store(const std::string& key, + const int& value) + { + auto err = WUPS_StoreInt(nullptr, key.c_str(), value); + if (err != WUPS_STORAGE_ERROR_SUCCESS) + throw err; + } + + + template<> + void + store(const std::string& key, + const std::string& value) + { + auto err = WUPS_StoreString(nullptr, key.c_str(), value.c_str()); + if (err != WUPS_STORAGE_ERROR_SUCCESS) + throw err; + } + +} // namespace wups diff --git a/source/wupsxx/storage.hpp b/source/wupsxx/storage.hpp new file mode 100644 index 0000000..ca302d9 --- /dev/null +++ b/source/wupsxx/storage.hpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_STORAGE_HPP +#define WUPSXX_STORAGE_HPP + +#include +#include + + +namespace wups { + + template + std::expected + load(const std::string& key); + + template + void store(const std::string& key, const T& value); + +} // namespace wups + +#endif diff --git a/source/wupsxx/text_item.cpp b/source/wupsxx/text_item.cpp new file mode 100644 index 0000000..4e28a7c --- /dev/null +++ b/source/wupsxx/text_item.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +#include +#include + +#include "text_item.hpp" + + +using namespace std::literals; + +namespace wups { + + text_item::text_item(const std::string& key, + const std::string& name, + const std::string& text) : + base_item{key, name}, + text{text} + {} + + + int + text_item::get_current_value_display(char* buf, + std::size_t size) + const + { + std::snprintf(buf, size, text.c_str()); + if (size > 3 && text.size() + 1 > size) + // replace last 3 chars of buf with "..." + buf[size - 2] = buf[size - 3] = buf[size - 4] = '.'; + return 0; + } + +} // namespace wups diff --git a/source/wupsxx/text_item.hpp b/source/wupsxx/text_item.hpp new file mode 100644 index 0000000..531f43a --- /dev/null +++ b/source/wupsxx/text_item.hpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_TEXT_ITEM_HPP +#define WUPSXX_TEXT_ITEM_HPP + +#include "base_item.hpp" + +namespace wups { + + struct text_item : base_item { + + std::string text; + + text_item(const std::string& key = "", + const std::string& name = "", + const std::string& text = ""); + + virtual int get_current_value_display(char* buf, std::size_t size) const override; + + }; + +} // namespace wups + + +#endif From deaba0c454a42f4d926e65348d7304f8748a83c0 Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Thu, 12 Oct 2023 12:10:35 -0300 Subject: [PATCH 05/17] Backported to upstream. --HG-- branch : upstream --- source/main.cpp | 205 +++++++++++++++++++++++------------- source/wupsxx/base_item.cpp | 3 +- source/wupsxx/base_item.hpp | 2 +- source/wupsxx/category.cpp | 14 +++ source/wupsxx/category.hpp | 2 + source/wupsxx/text_item.cpp | 45 +++++++- source/wupsxx/text_item.hpp | 5 + 7 files changed, 194 insertions(+), 82 deletions(-) diff --git a/source/main.cpp b/source/main.cpp index 89ee8e5..f9ff708 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -303,7 +303,7 @@ seconds_to_human(double s) char buf[64]; if (std::fabs(s) < 2) // less than 2 seconds - std::snprintf(buf, sizeof buf, "%.3f ms", 1000 * s); + 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 @@ -347,7 +347,7 @@ split(const std::string& input, std::vector result; - string::size_type start = 0; + 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)); @@ -708,7 +708,7 @@ update_time() template void load_or_init(const std::string& key, - T& variable) + T& variable) { auto val = wups::load(key); if (!val) @@ -720,9 +720,8 @@ load_or_init(const std::string& key, INITIALIZE_PLUGIN() { - WUPSStorageError storageRes = WUPS_OpenStorage(); // Check if the plugin's settings have been saved before. - if (storageRes == WUPS_STORAGE_ERROR_SUCCESS) { + if (WUPS_OpenStorage() == WUPS_STORAGE_ERROR_SUCCESS) { load_or_init(CFG_HOURS, cfg::hours); load_or_init(CFG_MINUTES, cfg::minutes); @@ -742,18 +741,72 @@ INITIALIZE_PLUGIN() } +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 { - wups::category* category; + struct server_info { + wups::text_item* name_item = nullptr; + wups::text_item* corr_item = nullptr; + wups::text_item* late_item = nullptr; + }; - std::map server_items; - std::map address_items; + wups::category* category; + std::map server_infos; preview_item(wups::category* cat) : - wups::text_item{"", "Clock", "Press A"}, + 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 @@ -762,93 +815,94 @@ struct preview_item : wups::text_item { { wups::text_item::on_button_pressed(buttons); - if (buttons & WUPS_CONFIG_BUTTON_A) { - try { - using std::make_unique; + if (buttons & WUPS_CONFIG_BUTTON_A) + run_preview(); + } - update_offset(); - for (auto& [key, value] : server_items) - value->text.clear(); + void + run_preview() + try { - for (auto& [key, value] : address_items) - value->text.clear(); + using std::make_unique; - std::vector servers = split(cfg::server, " \t,;"); + update_offset(); - // first, ensure each server has a text_item - for (const auto& server : servers) - if (!server_items[server]) { - auto item = make_unique("", server); - server_items[server] = item.get(); - category->add(std::move(item)); - } + for (auto& [key, value] : server_infos) { + value.name_item->text.clear(); + value.corr_item->text.clear(); + value.late_item->text.clear(); + } - addrinfo_query query = { - .family = AF_INET, - .socktype = SOCK_DGRAM, - .protocol = IPPROTO_UDP - }; + std::vector servers = split(cfg::server, " \t,;"); - std::set addresses; - for (const auto& server : servers) { - auto item = server_items.at(server); - try { - auto infos = get_address_info(server, "123", query); + addrinfo_query query = { + .family = AF_INET, + .socktype = SOCK_DGRAM, + .protocol = IPPROTO_UDP + }; - item->text = std::to_string(infos.size()) + " IP " - + (infos.size() > 1 ? "addresses" : "address"); + double total = 0; + unsigned num_values = 0; - for (auto info : infos) - addresses.insert(info.address); - } - catch (std::exception& e) { - item->text = e.what(); - } - } + for (const auto& server : servers) { + auto& si = server_infos.at(server); + try { + auto infos = get_address_info(server, "123", query); - std::vector corrections; - for (auto addr : addresses) { - // ensure each address has a text_item - if (!address_items[addr]) { - auto item = make_unique("", to_string(addr)); - address_items[addr] = item.get(); - category->add(std::move(item)); - } - auto item = address_items.at(addr); + si.name_item->text = std::to_string(infos.size()) + + (infos.size() > 1 ? " addresses."s : " address."s); - try { - auto [correction, latency] = ntp_query(addr); - item->text += "correction = " + seconds_to_human(correction) - + ", latency = " + seconds_to_human(latency); + std::vector server_corrections; + std::vector server_latencies; + unsigned errors = 0; - corrections.push_back(correction); + 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) { - item->text = e.what(); + ++errors; } } - text = format_wiiu_time(OSGetTime()); - - if (corrections.empty()) - text += ": no NTP server could be used."; - else { - double avg_correction = std::accumulate(corrections.begin(), - corrections.end(), - 0.0) - / corrections.size(); - text += " is off by "s + seconds_to_human(avg_correction); + 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) { - text = "Error: "s + e.what(); + 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(); + } }; @@ -863,7 +917,6 @@ WUPS_GET_CONFIG() try { auto config = make_unique("Configuration"); - auto preview = make_unique("Preview"); config->add(make_unique(CFG_SYNC, "Syncing Enabled", @@ -886,7 +939,7 @@ WUPS_GET_CONFIG() cfg::minutes, 0, 59)); config->add(make_unique(CFG_TOLERANCE, - "Tolerance (milliseconds, L/R for +/- 50)", + "Tolerance (milliseconds, \ue083/\ue084 for +/- 50)", cfg::tolerance, 0, 5000)); // show current NTP server address, no way to change it. @@ -894,7 +947,9 @@ WUPS_GET_CONFIG() "NTP servers", cfg::server)); - preview->add(make_unique(preview.get())); + 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)); diff --git a/source/wupsxx/base_item.cpp b/source/wupsxx/base_item.cpp index fdaad5a..332e4e7 100644 --- a/source/wupsxx/base_item.cpp +++ b/source/wupsxx/base_item.cpp @@ -35,7 +35,7 @@ namespace wups { { if (!ctx) return; - auto item = reinterpret_cast(ctx); + auto item = reinterpret_cast(ctx); item->on_selected(is_selected); }; @@ -116,7 +116,6 @@ namespace wups { void base_item::on_selected(bool) - const {} diff --git a/source/wupsxx/base_item.hpp b/source/wupsxx/base_item.hpp index 98b4a0f..3a0091e 100644 --- a/source/wupsxx/base_item.hpp +++ b/source/wupsxx/base_item.hpp @@ -31,7 +31,7 @@ namespace wups { virtual int get_current_value_selected_display(char* buf, std::size_t size) const; - virtual void on_selected(bool is_selected) const; + virtual void on_selected(bool is_selected); virtual void restore(); diff --git a/source/wupsxx/category.cpp b/source/wupsxx/category.cpp index 98849e1..ddc2e2c 100644 --- a/source/wupsxx/category.cpp +++ b/source/wupsxx/category.cpp @@ -35,4 +35,18 @@ namespace wups { item.release(); // WUPS now owns this item } + + void + category::add(base_item* item) + { + if (!item) + throw std::logic_error{"cannot add null item to category"}; + if (!item->handle) + throw std::logic_error{"cannot add null item handle to category"}; + + if (WUPSConfigCategory_AddItem(handle, item->handle) < 0) + throw std::runtime_error{"cannot add item to category"}; + } + + } // namespace wups diff --git a/source/wupsxx/category.hpp b/source/wupsxx/category.hpp index e146832..0a7cbd9 100644 --- a/source/wupsxx/category.hpp +++ b/source/wupsxx/category.hpp @@ -25,6 +25,8 @@ namespace wups { void add(std::unique_ptr&& item); + void add(base_item* item); + }; } // namespace wups diff --git a/source/wupsxx/text_item.cpp b/source/wupsxx/text_item.cpp index 4e28a7c..2c7484a 100644 --- a/source/wupsxx/text_item.cpp +++ b/source/wupsxx/text_item.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT +#include #include #include @@ -23,11 +24,47 @@ namespace wups { std::size_t size) const { - std::snprintf(buf, size, text.c_str()); - if (size > 3 && text.size() + 1 > size) - // replace last 3 chars of buf with "..." - buf[size - 2] = buf[size - 3] = buf[size - 4] = '.'; + auto width = std::min(size - 1, max_width); + + std::snprintf(buf, size, + "%*.*s", + width, + width, + text.c_str() + start); + return 0; } + + void + text_item::on_selected(bool is_selected) + { + if (!is_selected) + start = 0; + } + + + void + text_item::on_button_pressed(WUPSConfigButtons buttons) + { + if (text.empty()) + return; + + int tsize = static_cast(text.size()); + + if (tsize <= max_width) + return; + + if (buttons & WUPS_CONFIG_BUTTON_LEFT) + start -= 5; + + if (buttons & WUPS_CONFIG_BUTTON_RIGHT) + start += 5; + + if (start >= tsize - max_width) + start = tsize - max_width; + if (start < 0) + start = 0; + } + } // namespace wups diff --git a/source/wupsxx/text_item.hpp b/source/wupsxx/text_item.hpp index 531f43a..6955344 100644 --- a/source/wupsxx/text_item.hpp +++ b/source/wupsxx/text_item.hpp @@ -10,6 +10,8 @@ namespace wups { struct text_item : base_item { std::string text; + int max_width = 50; + int start = 0; text_item(const std::string& key = "", const std::string& name = "", @@ -17,6 +19,9 @@ namespace wups { virtual int get_current_value_display(char* buf, std::size_t size) const override; + virtual void on_selected(bool is_selected) override; + + virtual void on_button_pressed(WUPSConfigButtons buttons) override; }; } // namespace wups From e5a3c191b700f1f6e6270c038695fd3a96c4ee7c Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Fri, 20 Oct 2023 07:23:10 -0300 Subject: [PATCH 06/17] Backported to upstream. --HG-- branch : upstream --- 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; From 3aa9608b99edd168dd9e5ad4c9d06a2bb4ee044c Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Sat, 11 Nov 2023 09:03:22 -0300 Subject: [PATCH 07/17] Backported changes. --HG-- branch : upstream --- Dockerfile | 4 ++-- source/core.cpp | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 35533f3..803ab37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM ghcr.io/wiiu-env/devkitppc:20230621 +FROM devkitpro/devkitppc COPY --from=ghcr.io/wiiu-env/wiiupluginsystem:20230719 /artifacts $DEVKITPRO COPY --from=ghcr.io/wiiu-env/libnotifications:20230621 /artifacts $DEVKITPRO -WORKDIR project \ No newline at end of file +WORKDIR project diff --git a/source/core.cpp b/source/core.cpp index 1ccd6d2..548f4e4 100644 --- a/source/core.cpp +++ b/source/core.cpp @@ -14,7 +14,8 @@ // WUT/WUPS headers #include -#include +#include // CCRSysSetSystemTime() +#include // __OSSetAbsoluteSystemTime() // unix headers #include // select() @@ -33,10 +34,6 @@ 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 From 8f1c3796fd0379c3193d42978283577251172d3a Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Thu, 30 May 2024 10:39:16 -0300 Subject: [PATCH 08/17] Backported changes to upstream. --HG-- branch : upstream --- Dockerfile | 4 +- Makefile | 65 +- include/async_queue.hpp | 117 +++ include/cfg.hpp | 54 +- include/clock_item.hpp | 42 + include/config_screen.hpp | 6 +- include/core.hpp | 9 +- include/http_client.hpp | 2 +- include/limited_async.hpp | 76 -- include/log.hpp | 20 - include/logging.hpp | 18 + include/net/address.hpp | 55 ++ include/net/addrinfo.hpp | 43 + include/net/error.hpp | 22 + include/net/socket.hpp | 340 +++++++ ...{nintendo_glyphs.hpp => nintendo_glyphs.h} | 4 +- include/notify.hpp | 41 + include/ntp.hpp | 5 +- include/preview_screen.hpp | 24 +- include/thread_pool.hpp | 72 ++ include/timezone_offset_item.hpp | 43 + include/timezone_query_item.hpp | 25 + include/utc.hpp | 4 +- include/utils.hpp | 79 +- include/verbosity_item.hpp | 33 + include/wupsxx/base_item.hpp | 48 - include/wupsxx/bool_item.hpp | 50 +- include/wupsxx/category.hpp | 24 +- include/wupsxx/config.hpp | 32 - include/wupsxx/config_error.hpp | 22 + include/wupsxx/int_item.hpp | 50 +- include/wupsxx/item.hpp | 61 ++ include/wupsxx/storage.hpp | 38 +- include/wupsxx/storage_error.hpp | 22 + include/wupsxx/text_item.hpp | 41 +- include/wupsxx/var_watch.hpp | 275 ++++++ source/cfg.cpp | 158 ++- source/clock_item.cpp | 173 ++++ source/config_screen.cpp | 121 ++- source/core.cpp | 195 ++-- source/http_client.cpp | 69 +- source/log.cpp | 62 -- source/logging.cpp | 87 ++ source/main.cpp | 74 +- source/net/address.cpp | 64 ++ source/net/addrinfo.cpp | 105 ++ source/net/error.cpp | 19 + source/net/socket.cpp | 931 ++++++++++++++++++ source/notify.cpp | 122 +++ source/ntp.cpp | 34 +- source/preview_screen.cpp | 186 +--- source/thread_pool.cpp | 45 + source/timezone_offset_item.cpp | 122 +++ source/timezone_query_item.cpp | 50 + source/utc.cpp | 10 +- source/utils.cpp | 259 +---- source/verbosity_item.cpp | 76 ++ source/wupsxx/base_item.cpp | 150 --- source/wupsxx/bool_item.cpp | 104 +- source/wupsxx/category.cpp | 64 +- source/wupsxx/config.cpp | 38 - source/wupsxx/config_error.cpp | 18 + source/wupsxx/int_item.cpp | 131 ++- source/wupsxx/item.cpp | 214 ++++ source/wupsxx/storage.cpp | 74 +- source/wupsxx/storage_error.cpp | 14 + source/wupsxx/text_item.cpp | 165 +++- 67 files changed, 4304 insertions(+), 1496 deletions(-) create mode 100644 include/async_queue.hpp create mode 100644 include/clock_item.hpp delete mode 100644 include/limited_async.hpp delete mode 100644 include/log.hpp create mode 100644 include/logging.hpp create mode 100644 include/net/address.hpp create mode 100644 include/net/addrinfo.hpp create mode 100644 include/net/error.hpp create mode 100644 include/net/socket.hpp rename include/{nintendo_glyphs.hpp => nintendo_glyphs.h} (99%) create mode 100644 include/notify.hpp create mode 100644 include/thread_pool.hpp create mode 100644 include/timezone_offset_item.hpp create mode 100644 include/timezone_query_item.hpp create mode 100644 include/verbosity_item.hpp delete mode 100644 include/wupsxx/base_item.hpp delete mode 100644 include/wupsxx/config.hpp create mode 100644 include/wupsxx/config_error.hpp create mode 100644 include/wupsxx/item.hpp create mode 100644 include/wupsxx/storage_error.hpp create mode 100644 include/wupsxx/var_watch.hpp create mode 100644 source/clock_item.cpp delete mode 100644 source/log.cpp create mode 100644 source/logging.cpp create mode 100644 source/net/address.cpp create mode 100644 source/net/addrinfo.cpp create mode 100644 source/net/error.cpp create mode 100644 source/net/socket.cpp create mode 100644 source/notify.cpp create mode 100644 source/thread_pool.cpp create mode 100644 source/timezone_offset_item.cpp create mode 100644 source/timezone_query_item.cpp create mode 100644 source/verbosity_item.cpp delete mode 100644 source/wupsxx/base_item.cpp delete mode 100644 source/wupsxx/config.cpp create mode 100644 source/wupsxx/config_error.cpp create mode 100644 source/wupsxx/item.cpp create mode 100644 source/wupsxx/storage_error.cpp diff --git a/Dockerfile b/Dockerfile index 9fa95f7..99b666b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM devkitpro/devkitppc -COPY --from=ghcr.io/wiiu-env/wiiupluginsystem:20230719 /artifacts $DEVKITPRO -COPY --from=ghcr.io/wiiu-env/libnotifications:20230621 /artifacts $DEVKITPRO +COPY --from=ghcr.io/wiiu-env/wiiupluginsystem:20240505 /artifacts $DEVKITPRO +COPY --from=ghcr.io/wiiu-env/libnotifications:20240426 /artifacts $DEVKITPRO RUN git config --global --add safe.directory /project diff --git a/Makefile b/Makefile index 605bb85..7c7dac5 100644 --- a/Makefile +++ b/Makefile @@ -20,11 +20,11 @@ WUT_ROOT := $(DEVKITPRO)/wut # PLUGIN_AUTHOR sets the author of the plugin. # PLUGIN_LICENSE sets the license of the plugin. #------------------------------------------------------------------------------- -PLUGIN_NAME := "Wii U Time Sync" -PLUGIN_DESCRIPTION := "A plugin that synchronizes a Wii U\'s clock to the Internet." -PLUGIN_VERSION := "v2.1.1" -PLUGIN_AUTHOR := "Nightkingale, Daniel K. O." -PLUGIN_LICENSE := "MIT" +PLUGIN_NAME := Wii U Time Sync +PLUGIN_DESCRIPTION := A plugin that synchronizes the system clock to the Internet. +PLUGIN_VERSION := v2.1.1 +PLUGIN_AUTHOR := Nightkingale, Daniel K. O. +PLUGIN_LICENSE := MIT #------------------------------------------------------------------------------- # TARGET is the name of the output. @@ -33,11 +33,11 @@ PLUGIN_LICENSE := "MIT" # DATA is a list of directories containing data files. # INCLUDES is a list of directories containing header files. #------------------------------------------------------------------------------- -TARGET := Wii_U_Time_Sync -BUILD := build -SOURCES := source source/wupsxx -DATA := data -INCLUDES := include include/wupsxx +TARGET := Wii_U_Time_Sync +BUILD := build +SOURCES := source source/net source/wupsxx +DATA := data +INCLUDES := include #------------------------------------------------------------------------------- # DEBUG sets the debug flag for the plugin. @@ -48,7 +48,7 @@ INCLUDES := include include/wupsxx DEBUG := 0 # This appends the git hash to the version string. -ifeq ($(DEBUG), 1) +ifeq ($(DEBUG),1) GIT_HASH := $(shell git rev-parse --short HEAD) PLUGIN_VERSION := $(PLUGIN_VERSION)-$(GIT_HASH) endif @@ -56,32 +56,33 @@ endif #------------------------------------------------------------------------------- # options for code generation #------------------------------------------------------------------------------- -WARN_FLAGS := -Wall -Wextra -Wundef -Wpointer-arith -Wcast-align +WARN_FLAGS := -Wall -Wextra -Wundef -Wpointer-arith -Wcast-align -OPTFLAGS := -O2 -fipa-pta -ffunction-sections +OPTFLAGS := -O2 -fipa-pta -ffunction-sections -CFLAGS := $(WARN_FLAGS) $(OPTFLAGS) $(MACHDEP) +CFLAGS := $(WARN_FLAGS) $(OPTFLAGS) $(MACHDEP) -CXXFLAGS := $(CFLAGS) -std=c++23 +CXXFLAGS := $(CFLAGS) -std=c++23 + +DEFINES := '-DPLUGIN_NAME="$(PLUGIN_NAME)"' \ + '-DPLUGIN_DESCRIPTION="$(PLUGIN_DESCRIPTION)"' \ + '-DPLUGIN_VERSION="$(PLUGIN_VERSION)"' \ + '-DPLUGIN_AUTHOR="$(PLUGIN_AUTHOR)"' \ + '-DPLUGIN_LICENSE="$(PLUGIN_LICENSE)"' # 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)\" \ - -DPLUGIN_DESCRIPTION=\"$(PLUGIN_DESCRIPTION)\" \ - -DPLUGIN_VERSION=\"$(PLUGIN_VERSION)\" \ - -DPLUGIN_AUTHOR=\"$(PLUGIN_AUTHOR)\" \ - -DPLUGIN_LICENSE=\"$(PLUGIN_LICENSE)\" - -ASFLAGS := -g $(ARCH) - -LDFLAGS = -g \ - $(ARCH) \ - $(RPXSPECS) \ - $(WUPSSPECS) \ - -Wl,-Map,$(notdir $*.map) \ - $(OPTFLAGS) - -LIBS := -lnotifications -lwups -lwut +CPPFLAGS = $(INCLUDE) -D__WIIU__ -D__WUT__ -D__WUPS__ $(DEFINES) + +ASFLAGS := -g $(ARCH) + +LDFLAGS = -g \ + $(ARCH) \ + $(RPXSPECS) \ + $(WUPSSPECS) \ + -Wl,-Map,$(notdir $*.map) \ + $(CXXFLAGS) + +LIBS := -lnotifications -lwups -lwut #------------------------------------------------------------------------------- # list of directories containing libraries, this must be the top level diff --git a/include/async_queue.hpp b/include/async_queue.hpp new file mode 100644 index 0000000..de3e329 --- /dev/null +++ b/include/async_queue.hpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: MIT + +#ifndef ASYNC_QUEUE_HPP +#define ASYNC_QUEUE_HPP + +#include +#include +#include +#include +#include // forward(), move() + + +template> +class async_queue { + + std::mutex mutex; + std::condition_variable empty_cond; + Q queue; + bool should_stop = false; + +public: + + struct stop_request {}; + + // Makes the pool usable again after a stop(). + void + reset() + { + std::lock_guard guard{mutex}; + should_stop = false; + } + + + // This will make all future pop() calls throw a stop_request{}. + // It also wakes up all threads waiting on empty_cond. + void + stop() + { + std::lock_guard guard{mutex}; + should_stop = true; + empty_cond.notify_all(); // make sure all threads can see the updated flag + } + + + bool + is_stopping() const + { + std::lock_guard guard{mutex}; + return should_stop; + } + + + bool + empty() + const + { + std::lock_guard guard{mutex}; + return queue.empty(); + } + + + template + void + push(U&& x) + { + std::lock_guard guard{mutex}; + queue.push(std::forward(x)); + empty_cond.notify_one(); + } + + + T + pop() + { + std::unique_lock guard{mutex}; + // Stop waiting if a stop was requested, or the queue as data. + empty_cond.wait(guard, [this] { return should_stop || !queue.empty(); }); + if (should_stop) + throw stop_request{}; + T result = std::move(queue.front()); + queue.pop(); + return result; + } + + + template + bool + try_push(U&& x) + { + std::unique_lock guard{mutex, std::try_to_lock}; + if (!guard) + return false; + queue.push(std::forward(x)); + return true; + } + + + std::optional + try_pop() + { + std::unique_lock guard{mutex, std::try_to_lock}; + if (!guard) + return {}; + if (queue.empty()) + return {}; + if (should_stop) + throw stop_request{}; + T result = std::move(queue.front()); + queue.pop(); + return result; + } + +}; + + +#endif diff --git a/include/cfg.hpp b/include/cfg.hpp index 26b50c8..34acc84 100644 --- a/include/cfg.hpp +++ b/include/cfg.hpp @@ -3,36 +3,64 @@ #ifndef CFG_HPP #define CFG_HPP +#include #include namespace cfg { namespace key { - extern const char* hours; - extern const char* minutes; + extern const char* auto_tz; extern const char* msg_duration; extern const char* notify; extern const char* server; extern const char* sync; + extern const char* threads; extern const char* tolerance; + extern const char* utc_offset; } - extern int hours; - extern int minutes; - extern int msg_duration; - extern bool notify; - extern std::string server; - extern bool sync; - extern int tolerance; + namespace label { + extern const char* auto_tz; + extern const char* msg_duration; + extern const char* notify; + extern const char* server; + extern const char* sync; + extern const char* threads; + extern const char* tolerance; + extern const char* utc_offset; + } + + namespace defaults { + extern const bool auto_tz; + extern const int msg_duration; + extern const int notify; + extern const std::string server; + extern const bool sync; + extern const int threads; + extern const int tolerance; + } - void load(); + extern bool auto_tz; + extern int msg_duration; + extern int notify; + extern std::string server; + extern bool sync; + extern int threads; + extern int tolerance; + extern std::chrono::minutes utc_offset; + + + void load(); + void reload(); + void save(); + void migrate_old_config(); - // send the hours and minutes variables to the utc module - void update_utc_offset(); + std::chrono::minutes get_utc_offset(); + void set_utc_offset(std::chrono::minutes tz_offset); -} +} // namespace cfg #endif diff --git a/include/clock_item.hpp b/include/clock_item.hpp new file mode 100644 index 0000000..ea9d292 --- /dev/null +++ b/include/clock_item.hpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +#ifndef CLOCK_ITEM_HPP +#define CLOCK_ITEM_HPP + +#include +#include // unique_ptr<> +#include + +#include "wupsxx/text_item.hpp" + + +struct clock_item : wups::config::text_item { + + struct server_info { + text_item* name = nullptr; + text_item* correction = nullptr; + text_item* latency = nullptr; + }; + + + std::string now_str; + std::string stats_str; + std::map server_infos; + + clock_item(); + + static + std::unique_ptr create(); + + + void on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) override; + + + void refresh_now_str(); + + void run(); + +}; + +#endif diff --git a/include/config_screen.hpp b/include/config_screen.hpp index 40d2013..ab2a057 100644 --- a/include/config_screen.hpp +++ b/include/config_screen.hpp @@ -6,10 +6,6 @@ #include "wupsxx/category.hpp" -struct config_screen : wups::category { - - config_screen(); - -}; +wups::config::category make_config_screen(); #endif diff --git a/include/core.hpp b/include/core.hpp index 697b73d..7a3cb89 100644 --- a/include/core.hpp +++ b/include/core.hpp @@ -3,21 +3,20 @@ #ifndef CORE_HPP #define CORE_HPP -#include // pair<> #include +#include // pair<> -#include // struct sockaddr_in +#include "net/address.hpp" namespace core { - std::pair ntp_query(struct sockaddr_in address); + std::pair ntp_query(net::address address); - void sync_clock(); + void run(); std::string local_clock_to_string(); } // namespace core - #endif diff --git a/include/http_client.hpp b/include/http_client.hpp index f78ae2b..a48122e 100644 --- a/include/http_client.hpp +++ b/include/http_client.hpp @@ -5,11 +5,11 @@ #include + namespace http { std::string get(const std::string& url); } // namespace http - #endif diff --git a/include/limited_async.hpp b/include/limited_async.hpp deleted file mode 100644 index ea00e56..0000000 --- a/include/limited_async.hpp +++ /dev/null @@ -1,76 +0,0 @@ -// 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/include/log.hpp b/include/log.hpp deleted file mode 100644 index 6158b83..0000000 --- a/include/log.hpp +++ /dev/null @@ -1,20 +0,0 @@ -// 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/include/logging.hpp b/include/logging.hpp new file mode 100644 index 0000000..6d05691 --- /dev/null +++ b/include/logging.hpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +#ifndef LOGGING_HPP +#define LOGGING_HPP + + +namespace logging { + + void initialize(); + + void finalize(); + + __attribute__(( __format__ (__printf__, 1, 2) )) + void printf(const char* fmt, ...); + +} // namespace logging + +#endif diff --git a/include/net/address.hpp b/include/net/address.hpp new file mode 100644 index 0000000..199d03a --- /dev/null +++ b/include/net/address.hpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +#ifndef NET_ADDRESS_HPP +#define NET_ADDRESS_HPP + +#include +#include +#include + +#include // sockaddr_in, in_addr_t, in_port_t + + +// Note: only IPv4 address families, the Wii U does not support IPv6 + +namespace net { + + using ipv4_t = in_addr_t; + using port_t = in_port_t; + + + // Note: this is small enough, you can pass it by value everywhere. + + struct address { + + ipv4_t ip = 0; + port_t port = 0; + + + constexpr + address() noexcept = default; + + address(const sockaddr_in& src) noexcept; + + address(ipv4_t ip, port_t port) noexcept; + + address(const sockaddr* ptr, socklen_t size); + + + sockaddr_in + data() const noexcept; + + + // let the compiler generate comparisons + constexpr bool operator ==(const address&) const noexcept = default; + constexpr std::strong_ordering operator <=>(const address&) const noexcept = default; + + }; + + + std::string to_string(address addr); + + +} // namespace net + +#endif diff --git a/include/net/addrinfo.hpp b/include/net/addrinfo.hpp new file mode 100644 index 0000000..0c5d074 --- /dev/null +++ b/include/net/addrinfo.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +#ifndef NET_ADDRINFO_HPP +#define NET_ADDRINFO_HPP + +#include +#include +#include + +#include + +#include "address.hpp" +#include "socket.hpp" + +// Note: Wii U only supports IPv4, so this is hardcoded for IPv4 + +namespace net::addrinfo { + + struct hints { + std::optional type; + bool canon_name = false; + bool numeric_host = false; + bool passive = false; + }; + + + struct result { + socket::type type; + address addr; + std::optional canon_name; + }; + + + std::vector + lookup(const std::optional& name, + const std::optional& service = {}, + std::optional options = {}); + + + +} // namespace net::addrinfo + +#endif diff --git a/include/net/error.hpp b/include/net/error.hpp new file mode 100644 index 0000000..73af641 --- /dev/null +++ b/include/net/error.hpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +#ifndef NET_ERROR_HPP +#define NET_ERROR_HPP + +#include +#include + + +namespace net { + + struct error : std::system_error { + + error(int code, const std::string& msg); + + error(int code); + + }; + +} // namespace net + +#endif diff --git a/include/net/socket.hpp b/include/net/socket.hpp new file mode 100644 index 0000000..c85f5d7 --- /dev/null +++ b/include/net/socket.hpp @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: MIT + +#ifndef NET_SOCKET_HPP +#define NET_SOCKET_HPP + +#include +#include +#include +#include // pair<> + +#include // IP_* +#include // TCP_* +#include +#include // SO_*, MSG_* + +#include "net/address.hpp" +#include "net/error.hpp" + + +// Note: very simplified socket class, only what the Wii U supports. + +namespace net { + + class socket { + + int fd = -1; + + public: + + enum class ip_option : int { + tos = IP_TOS, + ttl = IP_TTL, + }; + + + enum class msg_flags : int { + dontroute = MSG_DONTROUTE, + dontwait = MSG_DONTWAIT, + none = 0, + oob = MSG_OOB, + peek = MSG_PEEK, + }; + + + enum class poll_flags : int { + err = POLLERR, + hup = POLLHUP, + in = POLLIN, + none = 0, + nval = POLLNVAL, + out = POLLOUT, + pri = POLLPRI, + }; + + + enum class socket_option : int { + bio = SO_BIO, + broadcast = SO_BROADCAST, + dontroute = SO_DONTROUTE, + error = SO_ERROR, + hopcnt = SO_HOPCNT, + keepalive = SO_KEEPALIVE, + keepcnt = 0x101B, + keepidle = 0x1019, + keepintvl = 0x101A, + linger = SO_LINGER, + maxmsg = SO_MAXMSG, + myaddr = SO_MYADDR, + nbio = SO_NBIO, + nonblock = SO_NONBLOCK, + noslowstart = SO_NOSLOWSTART, + oobinline = SO_OOBINLINE, + rcvbuf = SO_RCVBUF, + rcvlowat = SO_RCVLOWAT, + reuseaddr = SO_REUSEADDR, + rusrbuf = SO_RUSRBUF, + rxdata = SO_RXDATA, + sndbuf = SO_SNDBUF, + sndlowat = SO_SNDLOWAT, + tcpsack = SO_TCPSACK, + txdata = SO_TXDATA, + type = SO_TYPE, + winscale = SO_WINSCALE, + }; + + + enum class tcp_option : int { + ackdelaytime = TCP_ACKDELAYTIME, + ackfrequency = 0x2005, + maxseg = TCP_MAXSEG, + noackdelay = TCP_NOACKDELAY, + nodelay = TCP_NODELAY, + }; + + + enum class type { + tcp, + udp + }; + + + constexpr + socket() noexcept = default; + + explicit + socket(int fd); + + explicit + socket(type t); + + // move constructor + socket(socket&& other) noexcept; + + // move assignment + socket& operator =(socket&& other) noexcept; + + ~socket(); + + + // convenience named constructors + + static + socket make_tcp(); + + static + socket make_udp(); + + + // check if socket is valid + explicit + operator bool() const noexcept; + + bool is_socket() const noexcept; + + + // The regular BSD sockets API, throw net::error + + std::pair accept(); + + void bind(address addr); + + void close(); + + void connect(ipv4_t ip, port_t port); + void connect(address addr); + + + std::expected + getsockopt(ip_option opt) const noexcept; + + template + std::expected + getsockopt(socket_option opt) const noexcept; + + std::expected + getsockopt(tcp_option opt) const noexcept; + + // convenience getters + + // getters for ip_option + std::expected get_tos() const noexcept; + std::expected get_ttl() const noexcept; + + // getters for socket_option + std::expected get_broadcast() const noexcept; + std::expected get_dontroute() const noexcept; + std::expected get_error() const noexcept; + std::expected get_hopcnt() const noexcept; + std::expected get_keepalive() const noexcept; + std::expected get_keepcnt() const noexcept; + std::expected get_keepidle() const noexcept; + std::expected get_keepintvl() const noexcept; + std::expected<::linger, error> get_linger() const noexcept; + std::expected get_maxmsg() const noexcept; + std::expected get_myaddr() const noexcept; + std::expected get_nonblock() const noexcept; + std::expected get_oobinline() const noexcept; + std::expected get_rcvbuf() const noexcept; + std::expected get_rcvlowat() const noexcept; + std::expected get_reuseaddr() const noexcept; + std::expected get_rusrbuf() const noexcept; + std::expected get_rxdata() const noexcept; + std::expected get_sndbuf() const noexcept; + std::expected get_sndlowat() const noexcept; + std::expected get_tcpsack() const noexcept; + std::expected get_txdata() const noexcept; + std::expected get_type() const noexcept; + std::expected get_winscale() const noexcept; + + // getters for tcp_option + std::expected get_ackdelaytime() const noexcept; + std::expected get_ackfrequency() const noexcept; + std::expected get_maxseg() const noexcept; + std::expected get_noackdelay() const noexcept; + std::expected get_nodelay() const noexcept; + + + address getpeername() const; + address get_remote_address() const; + + address getsockname() const; + address get_local_address() const; + + void listen(int backlog); + + + poll_flags poll(poll_flags flags, std::chrono::milliseconds timeout = {}) const; + // convenience wrappers for poll() + bool is_readable(std::chrono::milliseconds timeout = {}) const; + bool is_writable(std::chrono::milliseconds timeout = {}) const; + + + std::size_t + recv(void* buf, std::size_t len, + msg_flags flags = msg_flags::none); + + std::size_t + recv_all(void* buf, std::size_t total, + msg_flags flags = msg_flags::none); + + std::pair + recvfrom(void* buf, std::size_t len, + msg_flags flags = msg_flags::none); + + + // Disassociate the handle from this socket. + int release() noexcept; + + + std::size_t + send(const void* buf, std::size_t len, + msg_flags flags = msg_flags::none); + + std::size_t + send_all(const void* buf, std::size_t total, + msg_flags flags = msg_flags::none); + + std::size_t + sendto(const void* buf, std::size_t len, + address dst, + msg_flags flags = msg_flags::none); + + + void setsockopt(ip_option opt, std::uint8_t arg); + + void setsockopt(socket_option opt); + void setsockopt(socket_option opt, unsigned arg); + void setsockopt(socket_option opt, const struct ::linger& arg); + + void setsockopt(tcp_option opt, unsigned arg); + + + // convenience setters + + // setters for ip_option + void set_tos(std::uint8_t t); + void set_ttl(std::uint8_t t); + + // setters for socket_option + void set_bio(); + void set_broadcast(bool enable); + void set_dontroute(bool enable); + void set_keepalive(bool enable); + void set_keepcnt(unsigned count); + void set_keepidle(unsigned period); + void set_keepintvl(unsigned interval); + void set_linger(bool enable, int period = 0); + void set_maxmsg(unsigned size); + void set_nbio(); + void set_nonblock(bool enable); + void set_noslowstart(bool enable); + void set_oobinline(bool enable); + void set_rcvbuf(unsigned size); + void set_reuseaddr(bool enable); + void set_rusrbuf(bool enable); + void set_sndbuf(unsigned size); + void set_tcpsack(bool enable); + void set_winscale(bool enable); + + // setters for tcp_option + void set_ackdelaytime(unsigned ms); + void set_ackfrequency(unsigned pending); + void set_maxseg(unsigned size); + void set_noackdelay(); + void set_nodelay(bool enable); + + + std::expected + try_poll(poll_flags flags, std::chrono::milliseconds timeout = {}) + const noexcept; + + std::expected + try_is_readable(std::chrono::milliseconds timeout = {}) + const noexcept; + + std::expected + try_is_writable(std::chrono::milliseconds timeout = {}) + const noexcept; + + + std::expected + try_recv(void* buf, std::size_t len, + msg_flags flags = msg_flags::none) + noexcept; + + std::expected, + error> + try_recvfrom(void* buf, std::size_t len, + msg_flags flags = msg_flags::none) + noexcept; + + + std::expected + try_send(const void* buf, std::size_t len, + msg_flags flags = msg_flags::none) + noexcept; + + std::expected + try_sendto(const void* buf, std::size_t len, + address dst, + msg_flags flags = msg_flags::none) + noexcept; + }; + + + socket::msg_flags operator &(socket::msg_flags a, socket::msg_flags b) noexcept; + socket::msg_flags operator ^(socket::msg_flags a, socket::msg_flags b) noexcept; + socket::msg_flags operator |(socket::msg_flags a, socket::msg_flags b) noexcept; + socket::msg_flags operator ~(socket::msg_flags a) noexcept; + + + socket::poll_flags operator &(socket::poll_flags a, socket::poll_flags b) noexcept; + socket::poll_flags operator ^(socket::poll_flags a, socket::poll_flags b) noexcept; + socket::poll_flags operator |(socket::poll_flags a, socket::poll_flags b) noexcept; + socket::poll_flags operator ~(socket::poll_flags a) noexcept; + +} // namespace net + + +#endif diff --git a/include/nintendo_glyphs.hpp b/include/nintendo_glyphs.h similarity index 99% rename from include/nintendo_glyphs.hpp rename to include/nintendo_glyphs.h index 5652a90..0fd0fdc 100644 --- a/include/nintendo_glyphs.hpp +++ b/include/nintendo_glyphs.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -#ifndef NINTENDO_GLYPHS_HPP -#define NINTENDO_GLYPHS_HPP +#ifndef NINTENDO_GLYPHS_H +#define NINTENDO_GLYPHS_H #define NIN_GLYPH_BTN_A "\uE000" #define NIN_GLYPH_BTN_B "\uE001" diff --git a/include/notify.hpp b/include/notify.hpp new file mode 100644 index 0000000..07a2441 --- /dev/null +++ b/include/notify.hpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +#ifndef NOTIFY_HPP +#define NOTIFY_HPP + +#include + + +namespace notify { + + + enum class level : int { + quiet = 0, + normal = 1, + verbose = 2 + }; + + void initialize(); + + void finalize(); + + + void error(level lvl, const std::string& arg); + + void info(level lvl, const std::string& arg); + + void success(level lvl, const std::string& arg); + + + // RAII type to ensure it's intialized and finalized + class guard { + bool must_finalize; + public: + guard(bool init = true); + ~guard(); + void release(); + }; + +} + +#endif diff --git a/include/ntp.hpp b/include/ntp.hpp index ef51e69..1cbd01f 100644 --- a/include/ntp.hpp +++ b/include/ntp.hpp @@ -41,6 +41,9 @@ namespace ntp { std::strong_ordering operator <=>(timestamp other) const noexcept; }; + // TODO: implement difference calculations for timestamps, as recommended by the RFC. + // Differences sould be done in fixed-point, should check for overflows, and return + // floating-point. // This is a u16.16 fixed-point format. @@ -100,9 +103,9 @@ namespace ntp { static_assert(sizeof(packet) == 48); + std::string to_string(packet::mode_flag m); } // namespace ntp - #endif diff --git a/include/preview_screen.hpp b/include/preview_screen.hpp index ab1eda1..87f7097 100644 --- a/include/preview_screen.hpp +++ b/include/preview_screen.hpp @@ -3,31 +3,9 @@ #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(); -}; +wups::config::category make_preview_screen(); #endif diff --git a/include/thread_pool.hpp b/include/thread_pool.hpp new file mode 100644 index 0000000..21ce8ac --- /dev/null +++ b/include/thread_pool.hpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT + +#ifndef THREAD_POOL_HPP +#define THREAD_POOL_HPP + +#include +#include +#include // bind(), move_only_function<> +#include +#include +#include +#include // decay_t<>, invoke_result_t<> +#include // forward(), move() +#include + +#include "async_queue.hpp" + + +class thread_pool { + + unsigned max_workers; + + std::vector workers; + + // Note: we can't use std::function because we're putting std::packaged_task in there, + // and std::packaged_task is only movable, but std::function always tries to copy. + using task_type = std::move_only_function; + async_queue tasks; + + std::atomic_int num_idle_workers = 0; + + void worker_thread(std::stop_token token); + + void add_worker(); + +public: + + thread_pool(unsigned max_workers); + + ~thread_pool(); + + + // This method behaves like std::async(). + template + std::future, + std::decay_t...>> + submit(Func&& func, Args&&... args) + { + using Ret = std::invoke_result_t, std::decay_t...>; + + auto bfunc = std::bind(std::forward(func), + std::forward(args)...); + + std::packaged_task task{std::move(bfunc)}; + auto future = task.get_future(); + + if (max_workers == 0) + task(); // If no worker will handle this, execute it immediately. + else { + // If all threads are busy, try to add another to the pool. + if (num_idle_workers == 0) + add_worker(); + + tasks.push(std::move(task)); + } + + return future; + } + +}; + +#endif diff --git a/include/timezone_offset_item.hpp b/include/timezone_offset_item.hpp new file mode 100644 index 0000000..a06d205 --- /dev/null +++ b/include/timezone_offset_item.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT + +#ifndef TIMEZONE_OFFSET_ITEM_HPP +#define TIMEZONE_OFFSET_ITEM_HPP + +#include +#include + +#include "wupsxx/item.hpp" +#include "wupsxx/var_watch.hpp" + + +struct timezone_offset_item : wups::config::item { + + wups::config::var_watch variable; + + timezone_offset_item(const std::string& key, + const std::string& label, + std::chrono::minutes& variable); + + static + std::unique_ptr + create(const std::string& key, + const std::string& label, + std::chrono::minutes& variable); + + virtual int get_display(char* buf, std::size_t size) const override; + + virtual int get_selected_display(char* buf, std::size_t size) const override; + + virtual void restore() override; + + virtual void on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) override; + +private: + + void on_changed(); + +}; + + +#endif diff --git a/include/timezone_query_item.hpp b/include/timezone_query_item.hpp new file mode 100644 index 0000000..10df9d7 --- /dev/null +++ b/include/timezone_query_item.hpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +#ifndef TIMEZONE_QUERY_ITEM_HPP +#define TIMEZONE_QUERY_ITEM_HPP + +#include + +#include "wupsxx/text_item.hpp" + + +struct timezone_query_item : wups::config::text_item { + + timezone_query_item(); + + static + std::unique_ptr create(); + + void on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) override; + + void run(); + +}; + +#endif diff --git a/include/utc.hpp b/include/utc.hpp index c1edea5..6c7f041 100644 --- a/include/utc.hpp +++ b/include/utc.hpp @@ -3,10 +3,8 @@ #ifndef UTC_HPP #define UTC_HPP -namespace utc { - - extern double timezone_offset; +namespace utc { // Seconds since 2000-01-01 00:00:00 UTC struct timestamp { diff --git a/include/utils.hpp b/include/utils.hpp index 0a0cf69..662b91b 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -4,22 +4,17 @@ #define UTILS_HPP #include -#include +#include +#include // size_t #include +#include // pair<> #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); + std::string seconds_to_human(double s, bool show_positive = false); /** @@ -34,66 +29,10 @@ namespace utils { 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; + std::atomic_bool& flag; bool guarded; // when false, the function is already executing in some thread. exec_guard(std::atomic& f); @@ -103,6 +42,14 @@ namespace utils { }; + std::pair + fetch_timezone(); + + + std::string tz_offset_to_string(std::chrono::minutes offset); + + } // namespace utils #endif diff --git a/include/verbosity_item.hpp b/include/verbosity_item.hpp new file mode 100644 index 0000000..8032967 --- /dev/null +++ b/include/verbosity_item.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT + +#ifndef VERBOSITY_ITEM_HPP +#define VERBOSITY_ITEM_HPP + +#include + +#include "wupsxx/int_item.hpp" + + +struct verbosity_item : wups::config::int_item { + + verbosity_item(const std::string& key, + const std::string& label, + int& variable, + int default_value); + + static + std::unique_ptr + create(const std::string& notify_key, + const std::string& label, + int& variable, + int default_value); + + + virtual int get_display(char* buf, std::size_t size) const override; + + virtual int get_selected_display(char* buf, std::size_t size) const override; + +}; + + +#endif diff --git a/include/wupsxx/base_item.hpp b/include/wupsxx/base_item.hpp deleted file mode 100644 index 3a0091e..0000000 --- a/include/wupsxx/base_item.hpp +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_BASE_ITEM_HPP -#define WUPSXX_BASE_ITEM_HPP - -#include - -#include - - -namespace wups { - - struct base_item { - - WUPSConfigItemHandle handle = 0; - std::string key; - std::string name; - - - base_item(const std::string& key, - const std::string& name); - - // disallow moving, since the callbacks store the `this` pointer. - base_item(base_item&&) = delete; - - - virtual ~base_item(); - - - virtual int get_current_value_display(char* buf, std::size_t size) const; - - virtual int get_current_value_selected_display(char* buf, std::size_t size) const; - - virtual void on_selected(bool is_selected); - - virtual void restore(); - - virtual bool is_movement_allowed() const; - - virtual bool callback(); - - virtual void on_button_pressed(WUPSConfigButtons buttons); - - }; - -} // namespace wups - -#endif diff --git a/include/wupsxx/bool_item.hpp b/include/wupsxx/bool_item.hpp index 09c2585..37297e1 100644 --- a/include/wupsxx/bool_item.hpp +++ b/include/wupsxx/bool_item.hpp @@ -3,37 +3,55 @@ #ifndef WUPSXX_BOOL_ITEM_HPP #define WUPSXX_BOOL_ITEM_HPP -#include +#include -#include "base_item.hpp" +#include "item.hpp" -namespace wups { +#include "var_watch.hpp" - struct bool_item : base_item { - bool& variable; - bool default_value; - std::string true_str = "true"; - std::string false_str = "false"; +namespace wups::config { + class bool_item : public item { - bool_item(const std::string& key, - const std::string& name, - bool& variable); + var_watch variable; + const bool default_value; + std::string true_str; + std::string false_str; + public: - virtual int get_current_value_display(char* buf, std::size_t size) const override; + bool_item(const std::optional& key, + const std::string& label, + bool& variable, + bool default_value, + const std::string& true_str = "true", + const std::string& false_str = "false"); - virtual int get_current_value_selected_display(char* buf, std::size_t size) const override; + static + std::unique_ptr + create(const std::optional& key, + const std::string& label, + bool& variable, + bool default_value, + const std::string& true_str = "true", + const std::string& false_str = "false"); + + virtual int get_display(char* buf, std::size_t size) const override; + + virtual int get_selected_display(char* buf, std::size_t size) const override; virtual void restore() override; - virtual bool callback() override; + virtual void on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) override; + + private: - virtual void on_button_pressed(WUPSConfigButtons button) override; + void on_changed(); }; -} // namespace wups +} // namespace wups::config #endif diff --git a/include/wupsxx/category.hpp b/include/wupsxx/category.hpp index 0a7cbd9..b345f38 100644 --- a/include/wupsxx/category.hpp +++ b/include/wupsxx/category.hpp @@ -8,28 +8,34 @@ #include -#include "base_item.hpp" +#include "item.hpp" -namespace wups { +namespace wups::config { - struct category { + class category final { - WUPSConfigCategoryHandle handle = 0; + WUPSConfigCategoryHandle handle; + bool own_handle; // if true, will destroy the handle in the destructor - category(const std::string& name); + public: - category(category&&) = delete; + // This constructor does not take ownership of the handle. + category(WUPSConfigCategoryHandle handle); + + category(const std::string& label); + category(category&& other) noexcept; ~category(); - void add(std::unique_ptr&& item); + void release(); + + void add(std::unique_ptr&& item); - void add(base_item* item); + void add(category&& child); }; } // namespace wups - #endif diff --git a/include/wupsxx/config.hpp b/include/wupsxx/config.hpp deleted file mode 100644 index d05bfdf..0000000 --- a/include/wupsxx/config.hpp +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_CONFIG_HPP -#define WUPSXX_CONFIG_HPP - -#include -#include - -#include - -#include "category.hpp" - - -namespace wups { - - struct config { - - WUPSConfigHandle handle = 0; - - config(const std::string& name); - - config(config&&) = delete; - - ~config(); - - void add(std::unique_ptr&& cat); - - }; - -} // namespace wups - -#endif diff --git a/include/wupsxx/config_error.hpp b/include/wupsxx/config_error.hpp new file mode 100644 index 0000000..1c810be --- /dev/null +++ b/include/wupsxx/config_error.hpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_CONFIG_ERROR_HPP +#define WUPSXX_CONFIG_ERROR_HPP + +#include +#include + +#include + + +namespace wups::config { + + struct config_error : std::runtime_error { + + config_error(WUPSConfigAPIStatus status, const std::string& msg); + + }; + +} // namespace wups::config + +#endif diff --git a/include/wupsxx/int_item.hpp b/include/wupsxx/int_item.hpp index 398bf1c..ec77e7a 100644 --- a/include/wupsxx/int_item.hpp +++ b/include/wupsxx/int_item.hpp @@ -3,38 +3,58 @@ #ifndef WUPSXX_INT_ITEM_HPP #define WUPSXX_INT_ITEM_HPP -#include "base_item.hpp" +#include +#include "item.hpp" +#include "var_watch.hpp" -namespace wups { - struct int_item : base_item { +namespace wups::config { - int& variable; - int default_value = 0; + class int_item : public item { + + protected: + + var_watch variable; + const int default_value; int min_value; int max_value; + int fast_increment; + int slow_increment; + public: - int_item(const std::string& key, - const std::string& name, - int& variable, - int min_value, - int max_value); + int_item(const std::optional& key, + const std::string& label, + int& variable, int default_value, + int min_value, int max_value, + int fast_increment = 10, + int slow_increment = 1); + static + std::unique_ptr + create(const std::optional& key, + const std::string& label, + int& variable, int default_value, + int min_value, int max_value, + int fast_increment = 10, + int slow_increment = 1); - virtual int get_current_value_display(char* buf, std::size_t size) const override; + virtual int get_display(char* buf, std::size_t size) const override; - virtual int get_current_value_selected_display(char* buf, std::size_t size) const override; + virtual int get_selected_display(char* buf, std::size_t size) const override; virtual void restore() override; - virtual bool callback() override; + virtual void on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) override; + + private: - virtual void on_button_pressed(WUPSConfigButtons buttons) override; + void on_changed(); }; -} // namespace wups +} // namespace wups::config #endif diff --git a/include/wupsxx/item.hpp b/include/wupsxx/item.hpp new file mode 100644 index 0000000..46f0a18 --- /dev/null +++ b/include/wupsxx/item.hpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_ITEM_HPP +#define WUPSXX_ITEM_HPP + +#include // size_t +#include +#include + +#include + + +namespace wups::config { + + class item { + + WUPSConfigItemHandle handle; + + protected: + + std::optional key; + + public: + + item(const std::optional& key, + const std::string& label); + + // Disallow moving, since the callbacks store the `this` pointer. + item(item&&) = delete; + + virtual ~item(); + + // Gives up ownership of the handle. + void release(); + + + virtual int get_display(char* buf, std::size_t size) const; + + virtual int get_selected_display(char* buf, std::size_t size) const; + + virtual void on_selected(bool is_selected); + + virtual void restore(); + + virtual bool is_movement_allowed() const; + + virtual void on_close(); + + virtual void on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat); + + virtual void on_input(WUPSConfigComplexPadData input); + + + friend class category; + + }; + +} // namespace wups::config + +#endif diff --git a/include/wupsxx/storage.hpp b/include/wupsxx/storage.hpp index e6e0983..a59fea1 100644 --- a/include/wupsxx/storage.hpp +++ b/include/wupsxx/storage.hpp @@ -3,21 +3,47 @@ #ifndef WUPSXX_STORAGE_HPP #define WUPSXX_STORAGE_HPP -#include #include +#include #include +#include "storage_error.hpp" -namespace wups { + +namespace wups::storage { template - std::expected - load(const std::string& key); + std::expected + load(const std::string& key) + { + T value; + auto status = WUPSStorageAPI::GetEx(nullptr, + key, + value, + WUPSStorageAPI::GetOptions::RESIZE_EXISTING_BUFFER); + if (status != WUPS_STORAGE_ERROR_SUCCESS) + return std::unexpected{storage_error{"error loading key \"" + key + "\"", + status}}; + return value; + } + template - void store(const std::string& key, const T& value); + void + store(const std::string& key, const T& value) + { + auto status = WUPSStorageAPI::StoreEx(nullptr, key, value); + if (status != WUPS_STORAGE_ERROR_SUCCESS) + throw storage_error{"error storing key \"" + key + "\"", + status}; + } + + + void save(); + + void reload(); -} // namespace wups +} // namespace wups::storage #endif diff --git a/include/wupsxx/storage_error.hpp b/include/wupsxx/storage_error.hpp new file mode 100644 index 0000000..ac79865 --- /dev/null +++ b/include/wupsxx/storage_error.hpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +#ifndef WUPSXX_STORAGE_ERROR_HPP +#define WUPSXX_STORAGE_ERROR_HPP + +#include +#include + +#include + + +namespace wups::storage { + + struct storage_error : std::runtime_error { + + storage_error(const std::string& msg, WUPSStorageError status); + + }; + +} // namespace wups::storage + +#endif diff --git a/include/wupsxx/text_item.hpp b/include/wupsxx/text_item.hpp index 6955344..7dbf703 100644 --- a/include/wupsxx/text_item.hpp +++ b/include/wupsxx/text_item.hpp @@ -3,28 +3,45 @@ #ifndef WUPSXX_TEXT_ITEM_HPP #define WUPSXX_TEXT_ITEM_HPP -#include "base_item.hpp" +#include -namespace wups { +#include "item.hpp" - struct text_item : base_item { + +namespace wups::config { + + // Note: this class doesn't do much on its own, so it's all public. + + struct text_item : item { std::string text; - int max_width = 50; - int start = 0; + std::size_t max_width; + std::size_t first = 0; // first visible character + + text_item(const std::optional& key, + const std::string& label, + const std::string& text = "", + std::size_t max_width = 50); - text_item(const std::string& key = "", - const std::string& name = "", - const std::string& text = ""); + static + std::unique_ptr + create(const std::optional& key, + const std::string& label, + const std::string& text = "", + std::size_t max_width = 50); - virtual int get_current_value_display(char* buf, std::size_t size) const override; + + virtual int get_display(char* buf, std::size_t size) const override; + + virtual int get_selected_display(char* buf, std::size_t size) const override; virtual void on_selected(bool is_selected) override; - virtual void on_button_pressed(WUPSConfigButtons buttons) override; - }; + virtual void on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) override; -} // namespace wups + }; +} // namespace wups::config #endif diff --git a/include/wupsxx/var_watch.hpp b/include/wupsxx/var_watch.hpp new file mode 100644 index 0000000..f482b32 --- /dev/null +++ b/include/wupsxx/var_watch.hpp @@ -0,0 +1,275 @@ +#ifndef WUPSXX_VAR_WATCH_HPP +#define WUPSXX_VAR_WATCH_HPP + +#include // forward() + + +// A helper class that tracks changes to a variable. + +namespace wups::config { + + template + class var_watch { + + T& ref; + bool modified = false; + + public: + + var_watch(T& var) : + ref(var) + {} + + + bool + changed() + const noexcept + { + return modified; + } + + + void + reset() + noexcept + { + modified = false; + } + + + template + var_watch& + operator =(U&& val) + noexcept + { + T old = ref; + ref = std::forward(val); + if (old != ref) + modified = true; + return *this; + } + + + // operator T() + // const noexcept + // { + // return ref; + // } + + + T + value() + const noexcept + { + return ref; + } + + + // pointer-like read-only accessors + + const T& + operator *() + const noexcept + { + return ref; + } + + + const T* + operator ->() + const noexcept + { + return &ref; + } + + + // modifier below this point + + + // increment/decrement + var_watch& + operator ++() + noexcept + { + T old = ref; + ++ref; + if (old != ref) + modified = true; + return *this; + } + + + var_watch& + operator --() + noexcept + { + T old = ref; + --ref; + if (old != ref) + modified = true; + return *this; + } + + + T + operator ++(int) + noexcept + { + T old = ref; + T result = ref++; + if (old != ref) + modified = true; + return result; + } + + + T + operator --(int) + noexcept + { + T old = ref; + T result = ref--; + if (old != ref) + modified = true; + return result; + } + + + // assignment modifiers + + template + var_watch& + operator +=(U&& arg) + noexcept + { + T old = ref; + ref += std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + template + var_watch& + operator -=(U&& arg) + noexcept + { + T old = ref; + ref -= std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + template + var_watch& + operator *=(U&& arg) + noexcept + { + T old = ref; + ref *= std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + template + var_watch& + operator /=(U&& arg) + noexcept + { + T old = ref; + ref /= std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + template + var_watch& + operator %=(U&& arg) + noexcept + { + T old = ref; + ref %= std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + template + var_watch& + operator <<=(U&& arg) + noexcept + { + T old = ref; + ref <<= std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + template + var_watch& + operator >>=(U&& arg) + noexcept + { + T old = ref; + ref >>= std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + template + var_watch& + operator ^=(U&& arg) + noexcept + { + T old = ref; + ref ^= std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + template + var_watch& + operator &=(U&& arg) + noexcept + { + T old = ref; + ref &= std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + template + var_watch& + operator |=(U&& arg) + noexcept + { + T old = ref; + ref |= std::forward(arg); + if (old != ref) + modified = true; + return *this; + } + + + }; + +} + + +#endif diff --git a/source/cfg.cpp b/source/cfg.cpp index cf69ee8..0fc709d 100644 --- a/source/cfg.cpp +++ b/source/cfg.cpp @@ -2,30 +2,61 @@ #include "cfg.hpp" -#include "utc.hpp" +#include "logging.hpp" +#include "utils.hpp" #include "wupsxx/storage.hpp" +using std::chrono::minutes; + +using namespace std::literals; + + namespace cfg { namespace key { - const char* hours = "hours"; - const char* minutes = "minutes"; + const char* auto_tz = "auto_tz"; const char* msg_duration = "msg_duration"; const char* notify = "notify"; const char* server = "server"; const char* sync = "sync"; + const char* threads = "threads"; const char* tolerance = "tolerance"; + const char* utc_offset = "utc_offset"; + } + + + namespace label { + const char* auto_tz = "Auto-update Timezone Offset"; + const char* msg_duration = "Notification Duration (seconds)"; + const char* notify = "Show Notifications"; + const char* server = "NTP Servers"; + const char* sync = "Syncing Enabled"; + const char* threads = "Background threads"; + const char* tolerance = "Tolerance (milliseconds)"; + const char* utc_offset = "UTC Offset"; + } + + + namespace defaults { + const bool auto_tz = false; + const int msg_duration = 5; + const int notify = 0; + const std::string server = "pool.ntp.org"; + const bool sync = false; + const int threads = 4; + const int tolerance = 500; } - 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; + bool auto_tz = defaults::auto_tz; + int msg_duration = defaults::msg_duration; + int notify = defaults::notify; + std::string server = defaults::server; + bool sync = defaults::sync; + int threads = defaults::threads; + int tolerance = defaults::tolerance; + minutes utc_offset = 0min; template @@ -33,32 +64,115 @@ namespace cfg { load_or_init(const std::string& key, T& variable) { - auto val = wups::load(key); + auto val = wups::storage::load(key); if (!val) - wups::store(key, variable); + wups::storage::store(key, variable); else variable = *val; } + void + load_or_init(const std::string& key, + minutes& variable) + { + auto val = wups::storage::load(key); + if (!val) + wups::storage::store(key, variable.count()); + else + variable = minutes{*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); + try { + load_or_init(key::auto_tz, auto_tz); + 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::threads, threads); + load_or_init(key::tolerance, tolerance); + load_or_init(key::utc_offset, utc_offset); + // logging::printf("Loaded settings."); + } + catch (std::exception& e) { + logging::printf("Error loading config: %s", e.what()); + } + } + + + void + reload() + { + try { + wups::storage::reload(); + load(); + } + catch (std::exception& e) { + logging::printf("Error reloading config: %s", e.what()); + } + } + + + void + save() + { + try { + wups::storage::save(); + // logging::printf("Saved settings"); + } + catch (std::exception& e) { + logging::printf("Error saving config: %s", e.what()); + } + } + + + void + migrate_old_config() + { + // check for leftovers from old versions + auto hrs = wups::storage::load("hours"); + auto min = wups::storage::load("minutes"); + if (hrs || min) { + int h = hrs.value_or(0); + int m = min.value_or(0); + set_utc_offset(minutes{h * 60 + m}); + WUPSStorageAPI::DeleteItem("hours"); + WUPSStorageAPI::DeleteItem("minutes"); + save(); + logging::printf("Migrated old config: %d hrs + %d min -> %s.", + h, m, + utils::tz_offset_to_string(get_utc_offset()).c_str()); + } + } + + + std::chrono::minutes + get_utc_offset() + { + return utc_offset; } void - update_utc_offset() + set_utc_offset(std::chrono::minutes offset) { - double offset_seconds = (hours * 60.0 + minutes) * 60.0; - utc::timezone_offset = offset_seconds; + /* + * Normally, `utc_offset` is saved on the config storage by the + * `timezone_offset_item`. This function is supposed to be called by other parts + * of the code, so it needs to manually store and save the new `utc_offset`. + */ + utc_offset = offset; + try { + wups::storage::store(key::utc_offset, utc_offset.count()); + wups::storage::save(); + } + catch (std::exception& e) { + logging::printf("Error storing utc_offset: %s", e.what()); + } } } // namespace cfg diff --git a/source/clock_item.cpp b/source/clock_item.cpp new file mode 100644 index 0000000..71bf203 --- /dev/null +++ b/source/clock_item.cpp @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT + +#include // fmax(), fmin() +#include +#include + +#include "clock_item.hpp" + +#include "cfg.hpp" +#include "core.hpp" +#include "logging.hpp" +#include "net/addrinfo.hpp" +#include "nintendo_glyphs.h" +#include "utils.hpp" + + +using wups::config::text_item; + +using namespace std::literals; + + +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; + } + +} + + +clock_item::clock_item() : + text_item{{}, "Clock (" NIN_GLYPH_BTN_A " to refresh)", "", 42} +{} + + +std::unique_ptr +clock_item::create() +{ + return std::make_unique(); +} + + +void +clock_item::on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) +{ + text_item::on_input(input, repeat); + + if (input.buttons_d & WUPS_CONFIG_BUTTON_A) { + try { + run(); + } + catch (std::exception& e) { + text = "Error: "s + e.what(); + } + } + + refresh_now_str(); +} + + +void +clock_item::refresh_now_str() +{ + now_str = core::local_clock_to_string(); + text = now_str + stats_str; +} + + +void +clock_item::run() +{ + using std::to_string; + using utils::seconds_to_human; + + 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,;"); + + net::addrinfo::hints opts{ .type = net::socket::type::udp }; + + double total = 0; + unsigned num_values = 0; + + for (const auto& server : servers) { + auto& si = server_infos.at(server); + try { + auto infos = net::addrinfo::lookup(server, "123", opts); + + 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.addr); + server_corrections.push_back(correction); + server_latencies.push_back(latency); + total += correction; + ++num_values; + logging::printf("%s (%s): correction = %s, latency = %s", + server.c_str(), + to_string(info.addr).c_str(), + seconds_to_human(correction, true).c_str(), + seconds_to_human(latency).c_str()); + } + catch (std::exception& e) { + ++errors; + logging::printf("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, true) + + ", max = "s + seconds_to_human(corr_stats.max, true) + + ", avg = "s + seconds_to_human(corr_stats.avg, true); + 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(); + } + } + + if (num_values) { + double avg = total / num_values; + stats_str = ", needs "s + seconds_to_human(avg, true); + } else + stats_str = ""; + + refresh_now_str(); +} diff --git a/source/config_screen.cpp b/source/config_screen.cpp index fec5d33..9b4e384 100644 --- a/source/config_screen.cpp +++ b/source/config_screen.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT +#include #include // make_unique() #include "wupsxx/bool_item.hpp" @@ -9,78 +10,68 @@ #include "config_screen.hpp" #include "cfg.hpp" -#include "http_client.hpp" -#include "nintendo_glyphs.hpp" -#include "utils.hpp" +#include "timezone_offset_item.hpp" +#include "timezone_query_item.hpp" +#include "verbosity_item.hpp" -using wups::bool_item; -using wups::int_item; -using wups::text_item; -using std::make_unique; +using wups::config::bool_item; +using wups::config::int_item; +using wups::config::text_item; 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"} +wups::config::category +make_config_screen() { - 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::hours, "Hours Offset", cfg::hours, -12, 14)); - add(make_unique(cfg::key::minutes, "Minutes Offset", cfg::minutes, 0, 59)); - add(make_unique(cfg::key::msg_duration, "Notification Duration (seconds)", - cfg::msg_duration, 0, 30)); - add(make_unique(cfg::key::tolerance, "Tolerance (milliseconds)", - cfg::tolerance, 0, 5000)); - - add(make_unique()); + wups::config::category cat{"Configuration"}; + + cat.add(bool_item::create(cfg::key::sync, + cfg::label::sync, + cfg::sync, + cfg::defaults::sync, + "yes", "no")); + + cat.add(verbosity_item::create(cfg::key::notify, + cfg::label::notify, + cfg::notify, + cfg::defaults::notify)); + + cat.add(int_item::create(cfg::key::msg_duration, + cfg::label::msg_duration, + cfg::msg_duration, + cfg::defaults::msg_duration, + 0, 30, 5)); + + cat.add(timezone_offset_item::create(cfg::key::utc_offset, + cfg::label::utc_offset, + cfg::utc_offset)); + + cat.add(timezone_query_item::create()); + + cat.add(bool_item::create(cfg::key::auto_tz, + cfg::label::auto_tz, + cfg::auto_tz, + cfg::defaults::auto_tz, + "yes", "no")); + + cat.add(int_item::create(cfg::key::tolerance, + cfg::label::tolerance, + cfg::tolerance, + cfg::defaults::tolerance, + 0, 5000, 100)); // show current NTP server address, no way to change it. - add(make_unique(cfg::key::server, "NTP Servers", cfg::server)); + cat.add(text_item::create(cfg::key::server, + cfg::label::server, + cfg::server)); + + cat.add(int_item::create(cfg::key::threads, + cfg::label::threads, + cfg::threads, + cfg::defaults::threads, + 0, 8, 2)); + + return cat; } diff --git a/source/core.cpp b/source/core.cpp index 548f4e4..f34a1f1 100644 --- a/source/core.cpp +++ b/source/core.cpp @@ -3,30 +3,28 @@ #include #include #include // fabs() -#include +#include // snprintf() #include // accumulate() #include // views::zip() #include -#include +#include // runtime_error #include #include #include -// WUT/WUPS headers #include #include // CCRSysSetSystemTime() #include // __OSSetAbsoluteSystemTime() -// unix headers -#include // select() -#include // connect(), send(), recv() - #include "core.hpp" #include "cfg.hpp" -#include "limited_async.hpp" -#include "log.hpp" +#include "logging.hpp" +#include "net/addrinfo.hpp" +#include "net/socket.hpp" +#include "notify.hpp" #include "ntp.hpp" +#include "thread_pool.hpp" #include "utc.hpp" #include "utils.hpp" @@ -34,9 +32,6 @@ using namespace std::literals; -std::counting_semaphore<> async_limit{5}; // limit to 5 threads - - namespace { // Difference from NTP (1900) to Wii U (2000) epochs. @@ -61,7 +56,6 @@ namespace { } - std::string ticks_to_string(OSTime wt) { @@ -91,30 +85,30 @@ namespace core { // Note: hardcoded for IPv4, the Wii U doesn't have IPv6. std::pair - ntp_query(struct sockaddr_in address) + ntp_query(net::address address) { using std::to_string; - utils::socket_guard s{PF_INET, SOCK_DGRAM, IPPROTO_UDP}; - - connect(s.fd, reinterpret_cast(&address), sizeof address); + net::socket sock{net::socket::type::udp}; + sock.connect(address); ntp::packet packet; packet.version(4); packet.mode(ntp::packet::mode_flag::client); - unsigned num_send_tries = 0; + unsigned send_attempts = 0; + const unsigned max_send_attempts = 4; 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) { + auto send_status = sock.try_send(&packet, sizeof packet); + if (!send_status) { + auto& e = send_status.error(); + if (e.code() != std::errc::not_enough_memory) + throw e; + if (++send_attempts < max_send_attempts) { std::this_thread::sleep_for(100ms); goto try_again_send; } else @@ -122,36 +116,34 @@ namespace core { } - 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, + unsigned poll_attempts = 0; + const unsigned max_poll_attempts = 4; + try_again_poll: + auto readable_status = sock.try_is_readable(4s); + if (!readable_status) { + // Wii U OS can only handle 16 concurrent select()/poll() 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) { + auto& e = readable_status.error(); + if (e.code() != std::errc::not_enough_memory) + throw e; + if (++poll_attempts < max_poll_attempts) { std::this_thread::sleep_for(10ms); - goto try_again_select; + goto try_again_poll; } else - throw std::runtime_error{"No resources for select(), too many retries!"}; + throw std::runtime_error{"No resources for poll(), too many retries!"}; } - if (!FD_ISSET(s.fd, &read_set)) + if (!*readable_status) 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) + if (sock.recv(&packet, sizeof packet) < 48) throw std::runtime_error{"Invalid NTP response!"}; + sock.close(); // close it early + auto v = packet.version(); if (v < 3 || v > 4) throw std::runtime_error{"Unsupported NTP version: "s + to_string(v)}; @@ -222,83 +214,113 @@ namespace core { bool - apply_clock_correction(double correction) + apply_clock_correction(double seconds) { - OSTime correction_ticks = correction * OSTimerClockSpeed; + // OSTime before = OSGetSystemTime(); - nn::pdm::NotifySetTimeBeginEvent(); + OSTime ticks = seconds * OSTimerClockSpeed; - OSTime now = OSGetTime(); - OSTime corrected = now + correction_ticks; + nn::pdm::NotifySetTimeBeginEvent(); - if (CCRSysSetSystemTime(corrected)) { - nn::pdm::NotifySetTimeEndEvent(); - return false; - } + // OSTime ccr_start = OSGetSystemTime(); + bool success1 = !CCRSysSetSystemTime(OSGetTime() + ticks); + // OSTime ccr_finish = OSGetSystemTime(); - bool res = __OSSetAbsoluteSystemTime(corrected); + // OSTime abs_start = OSGetSystemTime(); + bool success2 = __OSSetAbsoluteSystemTime(OSGetTime() + ticks); + // OSTime abs_finish = OSGetSystemTime(); nn::pdm::NotifySetTimeEndEvent(); - return res; - } + // logging::printf("CCRSysSetSystemTime() took %f ms", + // 1000.0 * (ccr_finish - ccr_start) / OSTimerClockSpeed); + // logging::printf("__OSSetAbsoluteSystemTime() took %f ms", + // 1000.0 * (abs_finish - abs_start) / OSTimerClockSpeed); + // OSTime after = OSGetSystemTime(); + // logging::printf("Total time: %f ms", + // 1000.0 * (after - before) / OSTimerClockSpeed); + + return success1 && success2; + } void - sync_clock() + run() { using utils::seconds_to_human; if (!cfg::sync) return; - static std::atomic executing = false; + // ensure notification is initialized if needed + notify::guard notify_guard{cfg::notify > 0}; - utils::exec_guard guard{executing}; - if (!guard.guarded) { + static std::atomic executing = false; + utils::exec_guard exec_guard{executing}; + if (!exec_guard.guarded) { // Another thread is already executing this function. - report_info("Skipping NTP task: already in progress."); + notify::info(notify::level::verbose, + "Skipping NTP task: operation already in progress."); return; } - cfg::update_utc_offset(); - std::vector servers = utils::split(cfg::server, " \t,;"); + if (cfg::auto_tz) { + try { + auto [name, offset] = utils::fetch_timezone(); + if (offset != cfg::get_utc_offset()) { + cfg::set_utc_offset(offset); + notify::info(notify::level::verbose, + "Auto-updated timezone to " + name + + "(" + utils::tz_offset_to_string(offset) + ")"); + } + } + catch (std::exception& e) { + notify::error(notify::level::verbose, + "Failed to auto-update timezone: "s + e.what()); + } + } + + thread_pool pool(cfg::threads); - utils::addrinfo_query query = { - .family = AF_INET, - .socktype = SOCK_DGRAM, - .protocol = IPPROTO_UDP - }; + + std::vector servers = utils::split(cfg::server, " \t,;"); // First, resolve all the names, in parallel. - // Some IP addresses might be duplicated when we use *.pool.ntp.org. - std::set addresses; + // Some IP addresses might be duplicated when we use "pool.ntp.org". + std::set addresses; { - using info_vec = std::vector; + // nested scope so the futures vector is destroyed + using info_vec = std::vector; std::vector> futures(servers.size()); + net::addrinfo::hints opts{ .type = net::socket::type::udp }; // Launch DNS queries asynchronously. for (auto [fut, server] : std::views::zip(futures, servers)) - fut = limited_async(utils::get_address_info, server, "123", query); + fut = pool.submit(net::addrinfo::lookup, server, "123"s, opts); // Collect all future results. for (auto& fut : futures) try { for (auto info : fut.get()) - addresses.insert(info.address); + addresses.insert(info.addr); } catch (std::exception& e) { - report_error(e.what()); + notify::error(notify::level::verbose, e.what()); } } + if (addresses.empty()) { + // Probably a mistake in config, or network failure. + notify::error(notify::level::normal, "No NTP address could be used."); + return; + } + // Launch NTP queries asynchronously. std::vector>> futures(addresses.size()); for (auto [fut, address] : std::views::zip(futures, addresses)) - fut = limited_async(ntp_query, address); + fut = pool.submit(ntp_query, address); // Collect all future results. std::vector corrections; @@ -306,17 +328,20 @@ namespace core { 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)); + notify::info(notify::level::verbose, + to_string(address) + + ": correction = "s + seconds_to_human(correction, true) + + ", latency = "s + seconds_to_human(latency)); } catch (std::exception& e) { - report_error(utils::to_string(address) + ": "s + e.what()); + notify::error(notify::level::verbose, + to_string(address) + ": "s + e.what()); } if (corrections.empty()) { - report_error("No NTP server could be used!"); + notify::error(notify::level::normal, + "No NTP server could be used!"); return; } @@ -326,17 +351,21 @@ namespace core { / corrections.size(); if (std::fabs(avg_correction) * 1000 <= cfg::tolerance) { - report_success("Tolerating clock drift (correction is only " - + seconds_to_human(avg_correction) + ")."s); + notify::success(notify::level::verbose, + "Tolerating clock drift (correction is only " + + seconds_to_human(avg_correction, true) + ")."s); return; } if (cfg::sync) { if (!apply_clock_correction(avg_correction)) { - report_error("Failed to set system clock!"); + // This error woudl be so bad, the user should always know about it. + notify::error(notify::level::quiet, + "Failed to set system clock!"); return; } - report_success("Clock corrected by " + seconds_to_human(avg_correction)); + notify::success(notify::level::normal, + "Clock corrected by " + seconds_to_human(avg_correction, true)); } } diff --git a/source/http_client.cpp b/source/http_client.cpp index 7dc913c..43b43e0 100644 --- a/source/http_client.cpp +++ b/source/http_client.cpp @@ -2,20 +2,17 @@ #include #include -#include +#include // views::drop() #include -#include - -#include // connect() +#include // runtime_error #include "http_client.hpp" +#include "net/addrinfo.hpp" +#include "net/socket.hpp" #include "utils.hpp" -#define LOG(FMT, ...) WHBLogPrintf(FMT __VA_OPT__(,) __VA_ARGS__) - - namespace http { const std::string CRLF = "\r\n"; @@ -61,7 +58,7 @@ namespace http { req += "/"; req += " HTTP/1.1" + CRLF; req += "Host: " + fields.host + CRLF; - req += "User-Agent: Wii U Time Sync Plugin" + CRLF; + req += "User-Agent: " PLUGIN_NAME "/" PLUGIN_VERSION " (Wii U; Aroma)" + CRLF; req += "Accept: text/plain" + CRLF; req += "Connection: close" + CRLF; req += CRLF; @@ -115,6 +112,30 @@ namespace http { } + // Not very efficient, read one byte at a time. + std::string + recv_until(net::socket& sock, + const std::string& end_token) + { + std::string result; + + char buffer[1]; + while (true) { + if (sock.recv(buffer, 1) == 0) + break; + result.append(buffer, 1); + + // if we found the end token, remove it from the result and break out + if (result.ends_with(end_token)) { + result.resize(result.size() - end_token.size()); + break; + } + } + + return result; + } + + std::string get(const std::string& url) { @@ -126,39 +147,31 @@ namespace http { 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); + net::addrinfo::hints opts { .type = net::socket::type::tcp }; + auto addresses = net::addrinfo::lookup(fields.host, + std::to_string(*fields.port), + opts); 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}; + const auto& host = addresses.front(); + net::socket sock{host.type}; - 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)}; - } + sock.connect(host.addr); auto request = build_request(fields); - utils::send_all(sock.fd, request); + sock.send_all(request.data(), request.size()); - auto header_str = utils::recv_until(sock.fd, CRLF + CRLF); + auto header_str = recv_until(sock, 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); - + std::string result(header.length, '\0'); + sock.recv_all(result.data(), result.size()); + return result; } } diff --git a/source/log.cpp b/source/log.cpp deleted file mode 100644 index 9ad49d2..0000000 --- a/source/log.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// 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/logging.cpp b/source/logging.cpp new file mode 100644 index 0000000..c0b06bf --- /dev/null +++ b/source/logging.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT + +#include +#include +#include // vsnprintf() +#include + +#include +#include +#include +#include + +#include "logging.hpp" + + +using namespace std::literals; + + +namespace logging { + + std::atomic_uint refs = 0; + + + bool init_cafe = false; + bool init_module = false; + bool init_udp = false; + + + void + initialize() + { + // don't initialize again if refs was already positive + if (refs++) + return; + + init_cafe = WHBLogCafeInit(); + init_module = WHBLogModuleInit(); + init_udp = WHBLogUdpInit(); + } + + + void + finalize() + { + // don't finalize if refs is still positive + if (--refs) + return; + + if (init_cafe) + WHBLogCafeDeinit(); + init_cafe = false; + + if (init_module) + WHBLogModuleDeinit(); + init_module = false; + + if (init_udp) + WHBLogUdpDeinit(); + init_udp = false; + } + + + void + printf(const char* fmt, ...) + { + std::string buf(256, '\0'); + std::string xfmt = std::string("[" PLUGIN_NAME "] ") + fmt; + + std::va_list args; + + va_start(args, fmt); + int sz = std::vsnprintf(buf.data(), buf.size(), xfmt.c_str(), args); + va_end(args); + + if (sz > 0 && static_cast(sz) >= buf.size()) { + buf.resize(sz + 1); + + va_start(args, fmt); + std::vsnprintf(buf.data(), buf.size(), xfmt.c_str(), args); + va_end(args); + } + + if (sz > 0) + WHBLogPrint(buf.c_str()); + } + +} // namespace logging diff --git a/source/main.cpp b/source/main.cpp index 5b387e7..7e2786f 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,20 +1,17 @@ // SPDX-License-Identifier: MIT -// standard headers -#include // make_unique() +#include #include -// WUT/WUPS headers -#include #include -#include -// local headers #include "cfg.hpp" #include "config_screen.hpp" -#include "preview_screen.hpp" #include "core.hpp" -#include "wupsxx/config.hpp" +#include "logging.hpp" +#include "notify.hpp" +#include "preview_screen.hpp" +#include "wupsxx/category.hpp" // Important plugin information. @@ -28,45 +25,64 @@ WUPS_USE_WUT_DEVOPTAB(); WUPS_USE_STORAGE(PLUGIN_NAME); +static WUPSConfigAPICallbackStatus open_config(WUPSConfigCategoryHandle root_handle); +static void close_config(); + + 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) { - cfg::load(); - WUPS_CloseStorage(); + logging::initialize(); + + auto status = WUPSConfigAPI_Init({ .name = PLUGIN_NAME }, + open_config, + close_config); + if (status != WUPSCONFIG_API_RESULT_SUCCESS) { + logging::printf("Init error: %s", WUPSConfigAPI_GetStatusStr(status)); + return; } + cfg::load(); + cfg::migrate_old_config(); + if (cfg::sync) - core::sync_clock(); // Update clock when plugin is loaded. + core::run(); // Update clock when plugin is loaded. } -WUPS_GET_CONFIG() +DEINITIALIZE_PLUGIN() { - if (WUPS_OpenStorage() != WUPS_STORAGE_ERROR_SUCCESS) - return 0; + logging::finalize(); +} + +static +WUPSConfigAPICallbackStatus +open_config(WUPSConfigCategoryHandle root_handle) +{ try { - auto root = std::make_unique(PLUGIN_NAME); + cfg::reload(); + + wups::config::category root{root_handle}; - root->add(std::make_unique()); - root->add(std::make_unique()); + root.add(make_config_screen()); + root.add(make_preview_screen()); - return root.release()->handle; + return WUPSCONFIG_API_CALLBACK_RESULT_SUCCESS; } - catch (...) { - return 0; + catch (std::exception& e) { + logging::printf("Error opening config: %s", e.what()); + return WUPSCONFIG_API_CALLBACK_RESULT_ERROR; } } -WUPS_CONFIG_CLOSED() +static +void +close_config() { - std::jthread update_time_thread(core::sync_clock); - update_time_thread.detach(); // Update time when settings are closed. + cfg::save(); - WUPS_CloseStorage(); // Save all changes. + // Update time when settings are closed. + std::jthread update_time_thread{core::run}; + update_time_thread.detach(); } diff --git a/source/net/address.cpp b/source/net/address.cpp new file mode 100644 index 0000000..6fbe06d --- /dev/null +++ b/source/net/address.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT + +#include // memset() +#include + +#include // htons(), inet_ntop() +#include // AF_INET + +#include "net/address.hpp" + + +namespace net { + + address::address(const sockaddr_in& src) + noexcept + { + ip = ntohl(src.sin_addr.s_addr); + port = ntohs(src.sin_port); + } + + + address::address(ipv4_t ip, port_t port) + noexcept : + ip{ip}, + port{port} + {} + + + address::address(const sockaddr* ptr, socklen_t size) + { + if (size != sizeof(sockaddr_in)) + throw std::logic_error{"address size mismatch"}; + sockaddr_in src; + std::memcpy(&src, ptr, size); + ip = ntohl(src.sin_addr.s_addr); + port = ntohs(src.sin_port); + } + + + sockaddr_in + address::data() + const noexcept + { + sockaddr_in result; + + result.sin_family = AF_INET; + result.sin_port = htons(port); + result.sin_addr.s_addr = htonl(ip); + std::memset(result.sin_zero, 0, sizeof result.sin_zero); + + return result; + } + + + std::string + to_string(address addr) + { + auto raw_addr = addr.data(); + char buf[INET_ADDRSTRLEN]; + return inet_ntop(AF_INET, &raw_addr.sin_addr, buf, sizeof buf); + } + + +} // namespace net diff --git a/source/net/addrinfo.cpp b/source/net/addrinfo.cpp new file mode 100644 index 0000000..11ad532 --- /dev/null +++ b/source/net/addrinfo.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT + +#include // memcpy(), memset() +#include +#include + +#include "net/addrinfo.hpp" + + +namespace net::addrinfo { + + struct addrinfo_deleter { + void operator ()(struct ::addrinfo* p) + const noexcept + { ::freeaddrinfo(p); } + }; + + using ai_ptr = std::unique_ptr; + + + int + to_flags(const hints& opt) + { + int flags = 0; + if (opt.canon_name) + flags |= AI_CANONNAME; + if (opt.numeric_host) + flags |= AI_NUMERICHOST; + if (opt.passive) + flags |= AI_PASSIVE; + return flags; + } + + + socket::type + to_type(int socktype, int protocol) + { + if (socktype == SOCK_STREAM && protocol == IPPROTO_TCP) + return socket::type::tcp; + if (socktype == SOCK_DGRAM && protocol == IPPROTO_UDP) + return socket::type::udp; + return {}; + } + + + std::vector + lookup(const std::optional& name, + const std::optional& service, + std::optional opts) + { + ai_ptr info; + + struct ::addrinfo raw_hints; + struct ::addrinfo* raw_hints_ptr = nullptr; + + if (opts) { + raw_hints_ptr = &raw_hints; + std::memset(&raw_hints, 0, sizeof raw_hints); + raw_hints.ai_family = AF_INET; + raw_hints.ai_flags = to_flags(*opts); + if (opts->type) { + switch (*opts->type) { + case socket::type::tcp: + raw_hints.ai_socktype = SOCK_STREAM; + raw_hints.ai_protocol = IPPROTO_TCP; + break; + case socket::type::udp: + raw_hints.ai_socktype = SOCK_DGRAM; + raw_hints.ai_protocol = IPPROTO_UDP; + break; + } + } + } + + struct ::addrinfo* raw_result_ptr = nullptr; + int status = ::getaddrinfo(name ? name->c_str() : nullptr, + service ? service->c_str() : nullptr, + raw_hints_ptr, + &raw_result_ptr); + if (status) + throw std::runtime_error{::gai_strerror(status)}; + + info.reset(raw_result_ptr); + + std::vector res; + + // 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(sockaddr_in)) + throw std::logic_error{"getaddrinfo() returned invalid result!"}; + + result item; + item.type = to_type(a->ai_socktype, a->ai_protocol); + item.addr = address(a->ai_addr, a->ai_addrlen); + if (a->ai_canonname) + item.canon_name = std::string(a->ai_canonname); + + res.push_back(std::move(item)); + } + + return res; + } + +} // namespace net::addrinfo diff --git a/source/net/error.cpp b/source/net/error.cpp new file mode 100644 index 0000000..6ea2cb9 --- /dev/null +++ b/source/net/error.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +#include "net/error.hpp" + +#include "utils.hpp" + + +namespace net { + + error::error(int code, const std::string& msg) : + std::system_error{std::make_error_code(std::errc{code}), msg} + {} + + + error::error(int code) : + std::system_error{std::make_error_code(std::errc{code})} + {} + +} // namespace std diff --git a/source/net/socket.cpp b/source/net/socket.cpp new file mode 100644 index 0000000..502a1dc --- /dev/null +++ b/source/net/socket.cpp @@ -0,0 +1,931 @@ +// SPDX-License-Identifier: MIT + +#include +#include // byte +#include +#include + +#include // ntohl() +#include // socket() +#include // close() + +#include "net/socket.hpp" + +#include "logging.hpp" + + +// Note: WUT doesn't have SOL_IP, but IPPROTO_IP seems to work. +#ifndef SOL_IP +#define SOL_IP IPPROTO_IP +#endif + + +namespace net { + + // bitwise operations for socket::msg_flags + + socket::msg_flags + operator &(socket::msg_flags a, socket::msg_flags b) + noexcept + { return socket::msg_flags{static_cast(a) & static_cast(b)}; } + + socket::msg_flags + operator ^(socket::msg_flags a, socket::msg_flags b) + noexcept + { return socket::msg_flags{static_cast(a) ^ static_cast(b)}; } + + socket::msg_flags + operator |(socket::msg_flags a, socket::msg_flags b) + noexcept + { return socket::msg_flags{static_cast(a) | static_cast(b)}; } + + socket::msg_flags + operator ~(socket::msg_flags a) + noexcept + { return socket::msg_flags{~static_cast(a)}; } + + + // bitwise operations for socket::poll_flags + + socket::poll_flags + operator &(socket::poll_flags a, socket::poll_flags b) + noexcept + { return socket::poll_flags{static_cast(a) & static_cast(b)}; } + + socket::poll_flags + operator ^(socket::poll_flags a, socket::poll_flags b) + noexcept + { return socket::poll_flags{static_cast(a) ^ static_cast(b)}; } + + socket::poll_flags + operator |(socket::poll_flags a, socket::poll_flags b) + noexcept + { return socket::poll_flags{static_cast(a) | static_cast(b)}; } + + socket::poll_flags + operator ~(socket::poll_flags a) + noexcept + { return socket::poll_flags{~static_cast(a)}; } + + + + socket::socket(int fd) : + fd{fd} + {} + + + socket::socket(type t) + { + switch (t) { + case type::tcp: + fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + break; + case type::udp: + fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + break; + } + + if (fd == -1) + throw error{errno}; + } + + + socket::socket(socket&& other) + noexcept : + fd{other.fd} + { + other.fd = -1; + } + + + socket& + socket::operator =(socket&& other) + noexcept + { + if (this != &other) { + try { + close(); + fd = other.fd; + other.fd = -1; + } + catch (std::exception& e) { + logging::printf("socket::operator=() failed: %s", e.what()); + } + } + return *this; + } + + + socket::~socket() + { + try { + close(); + } + catch (std::exception& e) { + logging::printf("socket::~socket() failed: %s", e.what()); + } + } + + + socket + socket::make_tcp() + { + return socket{type::tcp}; + } + + + socket + socket::make_udp() + { + return socket{type::udp}; + } + + + socket::operator bool() + const noexcept + { + return is_socket(); + } + + + bool + socket::is_socket() + const noexcept + { + return fd != -1; + } + + + std::pair + socket::accept() + { + sockaddr_in raw_addr = {}; + socklen_t len = sizeof raw_addr; + int new_fd = ::accept(fd, + reinterpret_cast(&raw_addr), + &len); + if (new_fd == -1) + throw error{errno}; + + socket new_socket{new_fd}; + + if (len != sizeof raw_addr) + throw std::logic_error{"unknown address size in accept(): " + std::to_string(len)}; + + return { std::move(new_socket), address{raw_addr} }; + } + + + void + socket::bind(address a) + { + const auto raw_addr = a.data(); + int status = ::bind(fd, + reinterpret_cast(&raw_addr), + sizeof raw_addr); + if (status == -1) + throw error{errno}; + } + + + void + socket::close() + { + if (is_socket()) { + if (::close(fd) == -1) + throw error{errno}; + } + fd = -1; + } + + + void + socket::connect(ipv4_t ip, + port_t port) + { + connect({ip, port}); + } + + + void + socket::connect(address a) + { + const auto raw_addr = a.data(); + int status = ::connect(fd, + reinterpret_cast(&raw_addr), + sizeof raw_addr); + if (status == -1) + throw error{errno}; + } + + + std::expected + socket::getsockopt(ip_option opt) + const noexcept + { + unsigned val = 0; + socklen_t len = sizeof val; + int status = ::getsockopt(fd, SOL_IP, static_cast(opt), + &val, &len); + if (status == -1) + return std::unexpected{error{errno}}; + + return val; + } + + + template + std::expected + socket::getsockopt(socket_option opt) + const noexcept + { + T val = {}; + socklen_t len = sizeof val; + int status = ::getsockopt(fd, SOL_SOCKET, static_cast(opt), + &val, &len); + if (status == -1) + return std::unexpected{error{errno}}; + + return val; + } + + + std::expected + socket::getsockopt(tcp_option opt) + const noexcept + { + unsigned val = 0; + socklen_t len = sizeof val; + int status = ::getsockopt(fd, SOL_TCP, static_cast(opt), + &val, &len); + if (status == -1) + return std::unexpected{error{errno}}; + + return val; + } + + + namespace { + + // convenience function to convert between std::expected<> types. + template + std::expected + convert(const std::expected& e) + { + if (!e) + return std::unexpected{e.error()}; + return static_cast(*e); + } + + } + + // getters for ip_option + + std::expected + socket::get_tos() + const noexcept + { return getsockopt(ip_option::tos); } + + std::expected + socket::get_ttl() + const noexcept + { return getsockopt(ip_option::ttl); } + + + // getters for socket_option + + std::expected + socket::get_broadcast() + const noexcept + { return convert(getsockopt(socket_option::broadcast)); } + + std::expected + socket::get_dontroute() + const noexcept + { return convert(getsockopt(socket_option::dontroute)); } + + std::expected + socket::get_error() + const noexcept + { return convert(getsockopt(socket_option::error)); } + + std::expected + socket::get_hopcnt() + const noexcept + { return getsockopt(socket_option::hopcnt); } + + std::expected + socket::get_keepalive() + const noexcept + { return convert(getsockopt(socket_option::keepalive)); } + + std::expected + socket::get_keepcnt() + const noexcept + { return getsockopt(socket_option::keepcnt); } + + std::expected + socket::get_keepidle() + const noexcept + { return getsockopt(socket_option::keepidle); } + + std::expected + socket::get_keepintvl() + const noexcept + { return getsockopt(socket_option::keepintvl); } + + std::expected<::linger, error> + socket::get_linger() + const noexcept + { return getsockopt<::linger>(socket_option::linger); } + + std::expected + socket::get_maxmsg() + const noexcept + { return getsockopt(socket_option::maxmsg); } + + + std::expected + socket::get_myaddr() + const noexcept + { + auto r = getsockopt(socket_option::myaddr); + if (!r) + return std::unexpected{r.error()}; + ipv4_t ip = ntohl(*r); + return address{ip, 0}; + } + + + std::expected + socket::get_nonblock() + const noexcept + { return convert(getsockopt(socket_option::nonblock)); } + + std::expected + socket::get_oobinline() + const noexcept + { return convert(getsockopt(socket_option::oobinline)); } + + std::expected + socket::get_rcvbuf() + const noexcept + { return getsockopt(socket_option::rcvbuf); } + + std::expected + socket::get_rcvlowat() + const noexcept + { return getsockopt(socket_option::rcvlowat); } + + std::expected + socket::get_reuseaddr() + const noexcept + { return convert(getsockopt(socket_option::reuseaddr)); } + + std::expected + socket::get_rusrbuf() + const noexcept + { return convert(getsockopt(socket_option::rusrbuf)); } + + std::expected + socket::get_rxdata() + const noexcept + { return getsockopt(socket_option::rxdata); } + + std::expected + socket::get_sndbuf() + const noexcept + { return getsockopt(socket_option::sndbuf); } + + std::expected + socket::get_sndlowat() + const noexcept + { return getsockopt(socket_option::sndlowat); } + + std::expected + socket::get_tcpsack() + const noexcept + { return convert(getsockopt(socket_option::tcpsack)); } + + std::expected + socket::get_txdata() + const noexcept + { return getsockopt(socket_option::txdata); } + + + std::expected + socket::get_type() + const noexcept + { + auto r = getsockopt(socket_option::type); + if (!r) + return std::unexpected{r.error()}; + if (*r == SOCK_STREAM) + return type::tcp; + if (*r == SOCK_DGRAM) + return type::udp; + return std::unexpected{error{0}}; + } + + + std::expected + socket::get_winscale() + const noexcept + { return convert(getsockopt(socket_option::winscale)); } + + + // getters for tcp_option + + std::expected + socket::get_ackdelaytime() + const noexcept + { return convert(getsockopt(tcp_option::ackdelaytime)); } + + std::expected + socket::get_ackfrequency() + const noexcept + { return getsockopt(tcp_option::ackfrequency); } + + std::expected + socket::get_maxseg() + const noexcept + { return getsockopt(tcp_option::maxseg); } + + std::expected + socket::get_noackdelay() + const noexcept + { return convert(getsockopt(tcp_option::noackdelay)); } + + std::expected + socket::get_nodelay() + const noexcept + { return convert(getsockopt(tcp_option::nodelay)); } + + + address + socket::getpeername() const + { + sockaddr_in raw_addr = {}; + socklen_t len = sizeof raw_addr; + int status = ::getpeername(fd, + reinterpret_cast(&raw_addr), + &len); + if (status == -1) + throw error{errno}; + + return address{raw_addr}; + } + + + address + socket::get_remote_address() + const + { + return getpeername(); + } + + + address + socket::getsockname() + const + { + sockaddr_in raw_addr = {}; + socklen_t len = sizeof raw_addr; + int status = ::getsockname(fd, + reinterpret_cast(&raw_addr), + &len); + if (status == -1) + throw error{errno}; + + return address{raw_addr}; + } + + + address + socket::get_local_address() + const + { + return getsockname(); + } + + + void + socket::listen(int backlog) + { + int status = ::listen(fd, backlog); + if (status == -1) + throw error{errno}; + } + + + socket::poll_flags + socket::poll(poll_flags flags, + std::chrono::milliseconds timeout) + const + { + auto status = try_poll(flags, timeout); + if (!status) + throw status.error(); + return *status; + } + + + bool + socket::is_readable(std::chrono::milliseconds timeout) + const + { + auto status = try_is_readable(timeout); + if (!status) + throw status.error(); + return *status; + } + + + bool + socket::is_writable(std::chrono::milliseconds timeout) + const + { + auto status = try_is_writable(timeout); + if (!status) + throw status.error(); + return *status; + } + + + std::size_t + socket::recv(void* buf, std::size_t len, + msg_flags flags) + { + auto status = try_recv(buf, len, flags); + if (!status) + throw status.error(); + return *status; + } + + + std::size_t + socket::recv_all(void* vbuf, std::size_t total, + msg_flags flags) + { + auto buf = static_cast(vbuf); + std::size_t received = 0; + while (received < total) { + auto status = try_recv(buf + received, total - received, flags); + if (!status) { + auto& e = status.error(); + if (e.code() == std::errc::operation_would_block) { + // harmless error, just try again + std::this_thread::yield(); + continue; + } + throw e; + } + if (!*status) // connection was closed gracefully + break; + received += *status; + } + return received; + } + + + std::pair + socket::recvfrom(void* buf, std::size_t len, + msg_flags flags) + { + sockaddr_in src; + socklen_t src_size = sizeof src; + auto status = ::recvfrom(fd, + buf, len, + static_cast(flags), + reinterpret_cast(&src), + &src_size); + if (status == -1) + throw error{errno}; + return {status, src}; + } + + + int + socket::release() + noexcept + { + int result = fd; + fd = -1; + return result; + } + + + std::size_t + socket::send(const void* buf, std::size_t len, + msg_flags flags) + { + auto status = ::send(fd, buf, len, static_cast(flags)); + if (status == -1) + throw error{errno}; + return status; + } + + + std::size_t + socket::send_all(const void* vbuf, + std::size_t total, + msg_flags flags) + { + auto buf = static_cast(vbuf); + std::size_t sent = 0; + while (sent < total) { + auto status = try_send(buf + sent, total - sent, flags); + if (!status) { + auto& e = status.error(); + if (e.code() == std::errc::operation_would_block) { + // harmless error, just try again + std::this_thread::yield(); + continue; + } + throw e; + } + if (!*status) // connection was closed gracefully + break; + sent += *status; + } + return sent; + } + + + std::size_t + socket::sendto(const void* buf, std::size_t len, + address dst, + msg_flags flags) + { + auto raw_dst = dst.data(); + auto status = ::sendto(fd, + buf, len, + static_cast(flags), + reinterpret_cast(&raw_dst), + sizeof raw_dst); + if (status == -1) + throw error{errno}; + return status; + } + + + void + socket::setsockopt(ip_option opt, + std::uint8_t arg) + { + unsigned uarg = arg; + int status = ::setsockopt(fd, SOL_IP, static_cast(opt), &uarg, sizeof uarg); + if (status == -1) + throw error{errno}; + } + + + void + socket::setsockopt(socket_option opt) + { + int status = ::setsockopt(fd, SOL_SOCKET, static_cast(opt), nullptr, 0); + if (status == -1) + throw error{errno}; + } + + + void + socket::setsockopt(socket_option opt, + unsigned arg) + { + int status = ::setsockopt(fd, SOL_SOCKET, static_cast(opt), &arg, sizeof arg); + if (status == -1) + throw error{errno}; + } + + + void + socket::setsockopt(socket_option opt, + const struct ::linger& arg) + { + int status = ::setsockopt(fd, SOL_SOCKET, static_cast(opt), &arg, sizeof arg); + if (status == -1) + throw error{errno}; + } + + + void + socket::setsockopt(tcp_option opt, + unsigned arg) + { + int status = ::setsockopt(fd, SOL_TCP, static_cast(opt), &arg, sizeof arg); + if (status == -1) + throw error{errno}; + } + + + // IP + + void + socket::set_tos(std::uint8_t t) + { setsockopt(ip_option::tos, t); } + + void + socket::set_ttl(std::uint8_t t) + { setsockopt(ip_option::ttl, t); } + + + // socket + + void + socket::set_bio() + { setsockopt(socket_option::bio); } + + void + socket::set_broadcast(bool enable) + { setsockopt(socket_option::broadcast, enable); } + + void + socket::set_dontroute(bool enable) + { setsockopt(socket_option::dontroute, enable); } + + void + socket::set_keepalive(bool enable) + { setsockopt(socket_option::keepalive, enable); } + + void + socket::set_keepcnt(unsigned count) + { setsockopt(socket_option::keepcnt, count); } + + void + socket::set_keepidle(unsigned period) + { setsockopt(socket_option::keepidle, period); } + + void + socket::set_keepintvl(unsigned interval) + { setsockopt(socket_option::keepintvl, interval); } + + void + socket::set_linger(bool enable, int period) + { setsockopt(socket_option::linger, ::linger{enable, period}); } + + void + socket::set_maxmsg(unsigned size) + { setsockopt(socket_option::maxmsg, size); } + + void + socket::set_nbio() + { setsockopt(socket_option::nbio); } + + void + socket::set_nonblock(bool enable) + { setsockopt(socket_option::nonblock, enable); } + + void + socket::set_noslowstart(bool enable) + { setsockopt(socket_option::noslowstart, enable); } + + void + socket::set_oobinline(bool enable) + { setsockopt(socket_option::oobinline, enable); } + + void + socket::set_rcvbuf(unsigned size) + { setsockopt(socket_option::rcvbuf, size); } + + void + socket::set_reuseaddr(bool enable) + { setsockopt(socket_option::reuseaddr, enable); } + + void + socket::set_sndbuf(unsigned size) + { setsockopt(socket_option::sndbuf, size); } + + void + socket::set_rusrbuf(bool enable) + { setsockopt(socket_option::rusrbuf, enable); } + + void + socket::set_tcpsack(bool enable) + { setsockopt(socket_option::tcpsack, enable); } + + void + socket::set_winscale(bool enable) + { setsockopt(socket_option::winscale, enable); } + + + // TCP + + void + socket::set_ackdelaytime(unsigned ms) + { setsockopt(tcp_option::ackdelaytime, ms); } + + void + socket::set_ackfrequency(unsigned pending) + { setsockopt(tcp_option::ackfrequency, pending); } + + void + socket::set_maxseg(unsigned size) + { setsockopt(tcp_option::maxseg, size); } + + void + socket::set_noackdelay() + { setsockopt(tcp_option::noackdelay, 0); } + + void + socket::set_nodelay(bool enable) + { setsockopt(tcp_option::nodelay, enable); } + + + std::expected + socket::try_poll(poll_flags flags, + std::chrono::milliseconds timeout) + const noexcept + { + pollfd pf{ fd, static_cast(flags), 0 }; + int status = ::poll(&pf, 1, timeout.count()); + if (status == -1) + return std::unexpected{error{errno}}; + return poll_flags{pf.revents}; + } + + + std::expected + socket::try_is_readable(std::chrono::milliseconds timeout) + const noexcept + { + auto status = try_poll(poll_flags::in, timeout); + if (!status) + return std::unexpected{status.error()}; + return (*status & poll_flags::in) != poll_flags::none; + } + + + std::expected + socket::try_is_writable(std::chrono::milliseconds timeout) + const noexcept + { + auto status = try_poll(poll_flags::out, timeout); + if (!status) + return std::unexpected{status.error()}; + return (*status & poll_flags::out) != poll_flags::none; + } + + + std::expected + socket::try_recv(void* buf, std::size_t len, + msg_flags flags) + noexcept + { + auto status = ::recv(fd, buf, len, static_cast(flags)); + if (status == -1) + return std::unexpected{error{errno}}; + return status; + } + + + std::expected, + error> + socket::try_recvfrom(void* buf, std::size_t len, + msg_flags flags) + noexcept + { + sockaddr_in raw_src; + socklen_t raw_src_size = sizeof raw_src; + auto status = ::recvfrom(fd, + buf, len, + static_cast(flags), + reinterpret_cast(&raw_src), + &raw_src_size); + if (status == -1) + return std::unexpected{error{errno}}; + return std::pair(status, address{raw_src}); + } + + + std::expected + socket::try_send(const void* buf, std::size_t len, + msg_flags flags) + noexcept + { + auto status = ::send(fd, buf, len, static_cast(flags)); + if (status == -1) + return std::unexpected{error{errno}}; + return status; + } + + + std::expected + socket::try_sendto(const void* buf, std::size_t len, + address dst, + msg_flags flags) + noexcept + { + auto raw_dst = dst.data(); + auto status = ::sendto(fd, + buf, len, + static_cast(flags), + reinterpret_cast(&raw_dst), + sizeof raw_dst); + if (status == -1) + return std::unexpected{error{errno}}; + return status; + } + +} // namespace net diff --git a/source/notify.cpp b/source/notify.cpp new file mode 100644 index 0000000..31b102d --- /dev/null +++ b/source/notify.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT + +#include + +#include + +#include "notify.hpp" + +#include "cfg.hpp" +#include "logging.hpp" + + +namespace notify { + + std::atomic_uint refs = 0; + + + void + initialize() + { + // don't initialize again if refs was already positive + if (refs++) + return; + + NotificationModule_InitLibrary(); + } + + + void + finalize() + { + if (!refs) + return; + + // don't finalize if refs is still positive + if (--refs) + return; + + NotificationModule_DeInitLibrary(); + } + + + void + error(level lvl, const std::string& arg) + { + logging::printf("ERROR: %s", arg.c_str()); + + if (static_cast(lvl) > cfg::notify) + return; + + 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, + true); + } + + + void + info(level lvl, const std::string& arg) + { + logging::printf("INFO: %s", arg.c_str()); + + if (static_cast(lvl) > 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, + true); + } + + + void + success(level lvl, const std::string& arg) + { + logging::printf("SUCCESS: %s", arg.c_str()); + + if (static_cast(lvl) > 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, + true); + } + + + guard::guard(bool init) : + must_finalize{init} + { + initialize(); + } + + + guard::~guard() + { + if (must_finalize) + finalize(); + } + + + void + guard::release() + { + must_finalize = false; + } + + +} diff --git a/source/ntp.cpp b/source/ntp.cpp index b99adec..8997d00 100644 --- a/source/ntp.cpp +++ b/source/ntp.cpp @@ -1,36 +1,11 @@ // 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); - } +#include // ldexp() +#include // be64toh(), htobe64() - constexpr - std::uint64_t - be64toh(std::uint64_t x) - { - return htobe64(x); - } - -} -#endif +#include "ntp.hpp" namespace ntp { @@ -73,7 +48,6 @@ namespace ntp { } - std::string to_string(packet::mode_flag m) { @@ -100,7 +74,6 @@ namespace ntp { } - void packet::leap(leap_flag x) noexcept @@ -148,5 +121,4 @@ namespace ntp { return static_cast(lvm & 0b000'0111); } - } // namespace ntp diff --git a/source/preview_screen.cpp b/source/preview_screen.cpp index 418483f..767beb8 100644 --- a/source/preview_screen.cpp +++ b/source/preview_screen.cpp @@ -1,192 +1,50 @@ // SPDX-License-Identifier: MIT -#include -#include -#include +#include // move() #include "preview_screen.hpp" #include "cfg.hpp" -#include "core.hpp" -#include "log.hpp" -#include "nintendo_glyphs.hpp" +#include "clock_item.hpp" #include "utils.hpp" +#include "wupsxx/text_item.hpp" -using std::make_unique; -using namespace std::literals; +using wups::config::text_item; -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(); - } - -}; +/* + * Note: the clock item needs to know about the server items added later. + * It's a bit ugly, because we can't manage it from the category object. + */ +wups::config::category +make_preview_screen() +{ + wups::config::category cat{"Preview"}; + auto clock = clock_item::create(); + auto& server_infos = clock->server_infos; -preview_screen::preview_screen() : - wups::category{"Preview"} -{ - auto c = make_unique(this); - clock = c.get(); - add(std::move(c)); + cat.add(std::move(clock)); 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 + ":"); + auto name = text_item::create({}, server + ":"); si.name = name.get(); - add(std::move(name)); + cat.add(std::move(name)); - auto correction = make_unique("", "┣ Correction:"); + auto correction = text_item::create({}, "┣ Correction:", "", 50); si.correction = correction.get(); - add(std::move(correction)); + cat.add(std::move(correction)); - auto latency = make_unique("", "┗ Latency:"); + auto latency = text_item::create({}, "┗ 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(); + cat.add(std::move(latency)); } } - 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(); + return cat; } diff --git a/source/thread_pool.cpp b/source/thread_pool.cpp new file mode 100644 index 0000000..0e89122 --- /dev/null +++ b/source/thread_pool.cpp @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +#include "thread_pool.hpp" + + +void +thread_pool::worker_thread(std::stop_token token) +{ + try { + while (!token.stop_requested()) { + auto task = tasks.pop(); + --num_idle_workers; + task(); + ++num_idle_workers; + } + } + catch (async_queue::stop_request& r) {} +} + + +void +thread_pool::add_worker() +{ + // Obey the limit. + if (workers.size() >= max_workers) + return; + ++num_idle_workers; + workers.emplace_back([this](std::stop_token token) { worker_thread(token); }); +} + + +thread_pool::thread_pool(unsigned max_workers) : + max_workers{max_workers} +{} + + +thread_pool::~thread_pool() +{ + // This will wake up all threads stuck waiting for more tasks, they will all throw + // tasks_queue::stop_request{}. + tasks.stop(); + + // The jthread destructor will also notify the stop token, so we don't need to do + // anything else. +} diff --git a/source/timezone_offset_item.cpp b/source/timezone_offset_item.cpp new file mode 100644 index 0000000..d276494 --- /dev/null +++ b/source/timezone_offset_item.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT + +#include // clamp() +#include // abs() +#include // snprintf() +#include // BSD strlcpy() + +#include "timezone_offset_item.hpp" + +#include "logging.hpp" +#include "nintendo_glyphs.h" +#include "utils.hpp" +#include "wupsxx/storage.hpp" + + +using namespace std::literals; + + +timezone_offset_item::timezone_offset_item(const std::string& key, + const std::string& label, + std::chrono::minutes& variable) : + item{key, label}, + variable(variable) +{} + + +std::unique_ptr +timezone_offset_item::create(const std::string& key, + const std::string& label, + std::chrono::minutes& variable) +{ + return std::make_unique(key, label, variable); +} + + +int +timezone_offset_item::get_display(char* buf, std::size_t size) + const +{ + auto str = utils::tz_offset_to_string(*variable); + ::strlcpy(buf, str.c_str(), size); + return 0; +} + + +int +timezone_offset_item::get_selected_display(char* buf, std::size_t size) + const +{ + const char* slow_left = ""; + const char* slow_right = ""; + const char* fast_left = ""; + const char* fast_right = ""; + if (*variable > -12h) { + slow_left = NIN_GLYPH_BTN_DPAD_LEFT; + fast_left = NIN_GLYPH_BTN_L; + } if (*variable < 14h) { + slow_right = NIN_GLYPH_BTN_DPAD_RIGHT; + fast_right = NIN_GLYPH_BTN_R; + } + + auto str = utils::tz_offset_to_string(*variable); + std::snprintf(buf, size, "%s%s" "%s" "%s%s", + fast_left, slow_left, + str.c_str(), + slow_right, fast_right); + return 0; +} + + +void +timezone_offset_item::restore() +{ + variable = 0min; + on_changed(); +} + + +void +timezone_offset_item::on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) +{ + item::on_input(input, repeat); + + if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT || + repeat & WUPS_CONFIG_BUTTON_LEFT) + variable -= 1min; + + if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT || + repeat & WUPS_CONFIG_BUTTON_RIGHT) + variable += 1min; + + if (input.buttons_d & WUPS_CONFIG_BUTTON_L || + repeat & WUPS_CONFIG_BUTTON_L) + variable -= 1h; + + if (input.buttons_d & WUPS_CONFIG_BUTTON_R || + repeat & WUPS_CONFIG_BUTTON_R) + variable += 1h; + + variable = std::clamp(*variable, -12h, 14h); + + on_changed(); +} + + +void +timezone_offset_item::on_changed() +{ + if (!key) + return; + if (!variable.changed()) + return; + + try { + wups::storage::store(*key, variable->count()); + variable.reset(); + } + catch (std::exception& e) { + logging::printf("Error storing timezone offset: %s", e.what()); + } +} diff --git a/source/timezone_query_item.cpp b/source/timezone_query_item.cpp new file mode 100644 index 0000000..9434aaa --- /dev/null +++ b/source/timezone_query_item.cpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +#include "timezone_query_item.hpp" + +#include "cfg.hpp" +#include "nintendo_glyphs.h" +#include "utils.hpp" + + +using namespace std::literals; + + +timezone_query_item::timezone_query_item() : + wups::config::text_item{{}, + "Detect Timezone (press " NIN_GLYPH_BTN_A ")", + "Using http://ip-api.com", + 30} +{} + + +std::unique_ptr +timezone_query_item::create() +{ + return std::make_unique(); +} + + +void +timezone_query_item::on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) +{ + text_item::on_input(input, repeat); + + if (input.buttons_d & WUPS_CONFIG_BUTTON_A) + run(); +} + + +void +timezone_query_item::run() +{ + try { + auto [name, offset] = utils::fetch_timezone(); + text = name; + cfg::set_utc_offset(offset); + } + catch (std::exception& e) { + text = "Error: "s + e.what(); + } +} diff --git a/source/utc.cpp b/source/utc.cpp index cebe040..0d164d2 100644 --- a/source/utc.cpp +++ b/source/utc.cpp @@ -4,12 +4,10 @@ #include "utc.hpp" +#include "cfg.hpp" -namespace utc { - - - double timezone_offset = 0; +namespace utc { static double @@ -23,8 +21,8 @@ namespace utc { now() noexcept { - return timestamp{ local_time() - timezone_offset }; + auto offset_seconds = duration_cast(cfg::get_utc_offset()); + return timestamp{ local_time() - offset_seconds.count() }; } - } // namespace utc diff --git a/source/utils.cpp b/source/utils.cpp index 4399889..70e6aea 100644 --- a/source/utils.cpp +++ b/source/utils.cpp @@ -1,51 +1,38 @@ // SPDX-License-Identifier: MIT -// standard headers -#include // fabs() +#include // abs() #include // snprintf() -#include // memset(), memcpy() -#include // unique_ptr<> -#include // runtime_error, logic_error -#include // move() +#include // runtime_error -// unix headers -#include // inet_ntop() -#include // getaddrinfo() -#include // socket() -#include // close() - -// local headers #include "utils.hpp" +#include "http_client.hpp" -namespace utils { - - std::string - errno_to_string(int e) - { - char buf[100]; - strerror_r(e, buf, sizeof buf); - return buf; - } +namespace utils { std::string - seconds_to_human(double s) + seconds_to_human(double s, bool show_positive) { char buf[64]; - if (std::fabs(s) < 2) // less than 2 seconds + if (std::abs(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 + else if (std::abs(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 + else if (std::abs(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 + else if (std::abs(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 result = buf; + + if (show_positive && s > 0) + result = "+" + result; + + return result; } @@ -77,214 +64,44 @@ namespace utils { } - - 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() + exec_guard::exec_guard(std::atomic& f) : + flag(f), + guarded{false} { - ::close(fd); - fd = -1; + bool expected_flag = false; + if (flag.compare_exchange_strong(expected_flag, true)) + guarded = true; // Exactly one thread can have the "guarded" flag as true. } - void - send_all(int fd, - const std::string& msg, - int flags) + exec_guard::~exec_guard() { - 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; - } + if (guarded) + flag = false; } - std::string - recv_all(int fd, - std::size_t size, - int flags) + std::pair + fetch_timezone() { - 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; - } + 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\"."}; - if (r == 0) - break; - - result.append(buffer, r); - } - - return result; + int tz_offset_min = std::stoi(tokens[1]) / 60; + return {tokens[0], std::chrono::minutes{tz_offset_min}}; } - // Not very efficient, read one byte at a time. std::string - recv_until(int fd, - const std::string& end_token, - int flags) + tz_offset_to_string(std::chrono::minutes offset) { - 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; + char buf[32]; + int hours = offset.count() / 60; + int minutes = std::abs(offset.count() % 60); + std::snprintf(buf, sizeof buf, "%+03d:%02d", hours, minutes); + return buf; } - } // namespace utils diff --git a/source/verbosity_item.cpp b/source/verbosity_item.cpp new file mode 100644 index 0000000..c56c3c3 --- /dev/null +++ b/source/verbosity_item.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +#include // clamp() + +#include "verbosity_item.hpp" + +#include "nintendo_glyphs.h" + + +namespace { + const char* value_str[] = { + "no", + "yes", + "verbose" + }; + + const char* + value_to_str(int v) + { + v = std::clamp(v, 0, 2); + return value_str[v]; + } +} + + +verbosity_item::verbosity_item(const std::string& key, + const std::string& label, + int& variable, + int default_value) : + int_item{key, label, + variable, default_value, + 0, 2, 2} +{} + + +std::unique_ptr +verbosity_item::create(const std::string& key, + const std::string& label, + int& variable, + int default_value) +{ + return std::make_unique(key, label, variable, default_value); +} + + +int +verbosity_item::get_display(char* buf, std::size_t size) + const +{ + std::snprintf(buf, size, "%s", value_to_str(*variable)); + return 0; +} + + +int +verbosity_item::get_selected_display(char* buf, std::size_t size) + const +{ + const char* left = ""; + const char* right = ""; + + switch (*variable) { + case 0: + right = " " NIN_GLYPH_BTN_DPAD_RIGHT; + break; + case 1: + left = NIN_GLYPH_BTN_DPAD_LEFT " "; + right = " " NIN_GLYPH_BTN_DPAD_RIGHT; + break; + case 2: + left = NIN_GLYPH_BTN_DPAD_LEFT " "; + break; + } + std::snprintf(buf, size, "%s%s%s", left, value_to_str(*variable), right); + return 0; +} diff --git a/source/wupsxx/base_item.cpp b/source/wupsxx/base_item.cpp deleted file mode 100644 index 83e3d88..0000000 --- a/source/wupsxx/base_item.cpp +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include -#include - -#include "wupsxx/base_item.hpp" - - -namespace wups { - - base_item::base_item(const std::string& key, - const std::string& name) : - key{key}, - name{name} - { - WUPSConfigCallbacks_t cb; - - cb.getCurrentValueDisplay = [](void* ctx, char* buf, int size) -> int - { - if (!ctx) - return -1; - auto item = reinterpret_cast(ctx); - return item->get_current_value_display(buf, size); - }; - - cb.getCurrentValueSelectedDisplay = [](void* ctx, char* buf, int size) -> int - { - if (!ctx) - return -1; - auto item = reinterpret_cast(ctx); - return item->get_current_value_selected_display(buf, size); - }; - - cb.onSelected = [](void* ctx, bool is_selected) - { - if (!ctx) - return; - auto item = reinterpret_cast(ctx); - item->on_selected(is_selected); - }; - - cb.restoreDefault = [](void* ctx) - { - if (!ctx) - return; - auto item = reinterpret_cast(ctx); - item->restore(); - }; - - cb.isMovementAllowed = [](void* ctx) -> bool - { - if (!ctx) - return true; - auto item = reinterpret_cast(ctx); - return item->is_movement_allowed(); - }; - - cb.callCallback = [](void* ctx) -> bool - { - if (!ctx) - return false; - auto item = reinterpret_cast(ctx); - return item->callback(); - }; - - cb.onButtonPressed = [](void* ctx, WUPSConfigButtons button) - { - if (!ctx) - return; - auto item = reinterpret_cast(ctx); - item->on_button_pressed(button); - }; - - // Called when WUPS is destroying the object. - cb.onDelete = [](void* ctx) - { - if (!ctx) - return; - auto item = reinterpret_cast(ctx); - item->handle = 0; - delete item; - }; - - - if (WUPSConfigItem_Create(&handle, key.c_str(), name.c_str(), cb, this) < 0) - throw std::runtime_error{"could not create config item"}; - - } - - - base_item::~base_item() - { - if (handle) - WUPSConfigItem_Destroy(handle); - } - - - int - base_item::get_current_value_display(char* buf, - std::size_t size) - const - { - std::snprintf(buf, size, "NOT IMPLEMENTED"); - return 0; - } - - - int - base_item::get_current_value_selected_display(char* buf, - std::size_t size) - const - { - return get_current_value_display(buf, size); - } - - - void - base_item::on_selected(bool) - {} - - - void - base_item::restore() - {} - - - bool - base_item::is_movement_allowed() - const - { - return true; - } - - - bool - base_item::callback() - { - return false; - } - - - void - base_item::on_button_pressed(WUPSConfigButtons buttons) - { - if (buttons & WUPS_CONFIG_BUTTON_X) - restore(); - } - - -} // namespace wups diff --git a/source/wupsxx/bool_item.cpp b/source/wupsxx/bool_item.cpp index 9c67d4e..ecf14d0 100644 --- a/source/wupsxx/bool_item.cpp +++ b/source/wupsxx/bool_item.cpp @@ -1,43 +1,66 @@ // SPDX-License-Identifier: MIT #include -#include +#include -#include "bool_item.hpp" -#include "storage.hpp" +#include "wupsxx/bool_item.hpp" -#include "nintendo_glyphs.hpp" +#include "logging.hpp" +#include "nintendo_glyphs.h" +#include "wupsxx/storage.hpp" -namespace wups { +namespace wups::config { - bool_item::bool_item(const std::string& key, - const std::string& name, - bool& variable) : - base_item{key, name}, + bool_item::bool_item(const std::optional& key, + const std::string& label, + bool& variable, + bool default_value, + const std::string& true_str, + const std::string& false_str) : + item{key, label}, variable(variable), - default_value{variable} + default_value{default_value}, + true_str{true_str}, + false_str{false_str} {} + std::unique_ptr + bool_item::create(const std::optional& key, + const std::string& label, + bool& variable, + bool default_value, + const std::string& true_str, + const std::string& false_str) + { + return std::make_unique(key, label, + variable, default_value, + true_str, false_str); + } + + int - bool_item::get_current_value_display(char* buf, std::size_t size) + bool_item::get_display(char* buf, std::size_t size) const { std::snprintf(buf, size, "%s", - variable ? true_str.c_str() : false_str.c_str()); + *variable ? true_str.c_str() : false_str.c_str()); return 0; } int - bool_item::get_current_value_selected_display(char* buf, std::size_t size) + bool_item::get_selected_display(char* buf, std::size_t size) const { - if (variable) - std::snprintf(buf, size, "%s %s ", NIN_GLYPH_BTN_DPAD_LEFT, true_str.c_str()); - else - std::snprintf(buf, size, " %s %s", false_str.c_str(), NIN_GLYPH_BTN_DPAD_RIGHT); + const char* str = *variable ? true_str.c_str() : false_str.c_str(); + + std::snprintf(buf, size, + "%s %s %s", + NIN_GLYPH_BTN_DPAD_LEFT, + str, + NIN_GLYPH_BTN_DPAD_RIGHT); return 0; } @@ -46,38 +69,41 @@ namespace wups { bool_item::restore() { variable = default_value; + on_changed(); } - bool - bool_item::callback() + void + bool_item::on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) { - if (key.empty()) - return false; + item::on_input(input, repeat); - try { - store(key, variable); - return true; - } - catch (...) { - return false; - } + // Allow toggling with A, left or right. + auto mask = WUPS_CONFIG_BUTTON_A | WUPS_CONFIG_BUTTON_LEFT | WUPS_CONFIG_BUTTON_RIGHT; + + if (input.buttons_d & mask) + variable = !*variable; + + on_changed(); } void - bool_item::on_button_pressed(WUPSConfigButtons buttons) + bool_item::on_changed() { - base_item::on_button_pressed(buttons); - - if (buttons & WUPS_CONFIG_BUTTON_A) - variable = !variable; + if (!key) + return; + if (!variable.changed()) + return; - if (buttons & WUPS_CONFIG_BUTTON_LEFT) - variable = false; - - if (buttons & WUPS_CONFIG_BUTTON_RIGHT) - variable = true; + try { + storage::store(*key, *variable); + variable.reset(); + } + catch (std::exception& e) { + logging::printf("Error storing bool: %s", e.what()); + } } -} // namespace wups +} // namespace wups::config diff --git a/source/wupsxx/category.cpp b/source/wupsxx/category.cpp index 39d4776..285b0ba 100644 --- a/source/wupsxx/category.cpp +++ b/source/wupsxx/category.cpp @@ -1,52 +1,72 @@ // SPDX-License-Identifier: MIT -#include - #include "wupsxx/category.hpp" +#include "wupsxx/config_error.hpp" + +namespace wups::config { + + category::category(WUPSConfigCategoryHandle handle) : + handle{handle}, + own_handle{false} + {} + + category::category(const std::string& label) : + own_handle{true} + { + WUPSConfigAPICreateCategoryOptionsV1 options{ .name = label.c_str() }; + auto status = WUPSConfigAPI_Category_Create(options, &handle); + if (status != WUPSCONFIG_API_RESULT_SUCCESS) + throw config_error{status, "could not create category \"" + label + "\""}; + } -namespace wups { - category::category(const std::string& name) + category::category(category&& other) + noexcept { - if (WUPSConfigCategory_Create(&handle, name.c_str()) < 0) - throw std::runtime_error{"could not create category"}; + handle = other.handle; + other.handle = {}; } category::~category() { - if (handle) - WUPSConfigCategory_Destroy(handle); + if (own_handle && handle.handle) + WUPSConfigAPI_Category_Destroy(handle); } void - category::add(std::unique_ptr&& item) + category::release() + { + handle = {}; + } + + + void + category::add(std::unique_ptr&& item) { if (!item) throw std::logic_error{"cannot add null item to category"}; - if (!item->handle) + if (!item->handle.handle) throw std::logic_error{"cannot add null item handle to category"}; - if (WUPSConfigCategory_AddItem(handle, item->handle) < 0) - throw std::runtime_error{"cannot add item to category"}; + auto status = WUPSConfigAPI_Category_AddItem(handle, item->handle); + if (status != WUPSCONFIG_API_RESULT_SUCCESS) + throw config_error{status, "cannot add item to category: "}; - item.release(); // WUPS now owns this item + item.release(); // WUPS will call .onDelete() later } void - category::add(base_item* item) + category::add(category&& child) { - if (!item) - throw std::logic_error{"cannot add null item to category"}; - if (!item->handle) - throw std::logic_error{"cannot add null item handle to category"}; + auto status = WUPSConfigAPI_Category_AddCategory(handle, child.handle); + if (status != WUPSCONFIG_API_RESULT_SUCCESS) + throw config_error{status, "cannot add child category to category: "}; - if (WUPSConfigCategory_AddItem(handle, item->handle) < 0) - throw std::runtime_error{"cannot add item to category"}; + child.release(); } - -} // namespace wups +} // namespace wups::config diff --git a/source/wupsxx/config.cpp b/source/wupsxx/config.cpp deleted file mode 100644 index e8ea77d..0000000 --- a/source/wupsxx/config.cpp +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include - -#include "wupsxx/config.hpp" - - -namespace wups { - - config::config(const std::string& name) - { - if (WUPSConfig_Create(&handle, name.c_str()) < 0) - throw std::runtime_error{"could not create config"}; - } - - - config::~config() - { - if (handle) - WUPSConfig_Destroy(handle); - } - - - void - config::add(std::unique_ptr&& cat) - { - if (!cat) - throw std::logic_error{"cannot add null category to config"}; - if (!cat->handle) - throw std::logic_error{"cannot add null category handle to config"}; - - if (WUPSConfig_AddCategory(handle, cat->handle) < 0) - throw std::runtime_error{"cannot add category to config"}; - - cat.release(); // WUPS now owns this category - } - -} // namespace wups diff --git a/source/wupsxx/config_error.cpp b/source/wupsxx/config_error.cpp new file mode 100644 index 0000000..1e3444d --- /dev/null +++ b/source/wupsxx/config_error.cpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +#include + +#include "wupsxx/config_error.hpp" + + +using namespace std::literals; + + +namespace wups::config { + + config_error::config_error(WUPSConfigAPIStatus status, + const std::string& msg) : + std::runtime_error{msg + ": "s + WUPSConfigAPI_GetStatusStr(status)} + {} + +} // namespace wups::config diff --git a/source/wupsxx/int_item.cpp b/source/wupsxx/int_item.cpp index d2c5e17..0da5f25 100644 --- a/source/wupsxx/int_item.cpp +++ b/source/wupsxx/int_item.cpp @@ -2,59 +2,76 @@ #include // clamp() #include -#include +#include #include "wupsxx/int_item.hpp" -#include "wupsxx/storage.hpp" - -#include "nintendo_glyphs.hpp" +#include "logging.hpp" +#include "nintendo_glyphs.h" +#include "wupsxx/storage.hpp" -namespace wups { +namespace wups::config { - int_item::int_item(const std::string& key, - const std::string& name, - int& variable, - int min_value, - int max_value) : - base_item{key, name}, + int_item::int_item(const std::optional& key, + const std::string& label, + int& variable, int default_value, + int min_value, int max_value, + int fast_increment, int slow_increment) : + item{key, label}, variable(variable), - default_value{variable}, + default_value{default_value}, min_value{min_value}, - max_value{max_value} + max_value{max_value}, + fast_increment{fast_increment}, + slow_increment{slow_increment} {} + std::unique_ptr + int_item::create(const std::optional& key, + const std::string& label, + int& variable, int default_value, + int min_value, int max_value, + int fast_increment, int slow_increment) + { + return std::make_unique(key, label, + variable, default_value, + min_value, max_value, + fast_increment, slow_increment); + } + + int - int_item::get_current_value_display(char* buf, std::size_t size) + int_item::get_display(char* buf, std::size_t size) const { - std::snprintf(buf, size, "%d", variable); + std::snprintf(buf, size, "%d", *variable); return 0; } int - int_item::get_current_value_selected_display(char* buf, std::size_t size) + int_item::get_selected_display(char* buf, std::size_t size) const { - const char* left = ""; - const char* right = ""; + const char* slow_left = ""; + const char* slow_right = ""; const char* fast_left = ""; const char* fast_right = ""; - if (variable > min_value) { - left = NIN_GLYPH_BTN_DPAD_LEFT; + if (*variable > min_value) { + slow_left = NIN_GLYPH_BTN_DPAD_LEFT " "; fast_left = NIN_GLYPH_BTN_L; - } if (variable < max_value) { - right = NIN_GLYPH_BTN_DPAD_RIGHT; + } if (*variable < max_value) { + slow_right = " " NIN_GLYPH_BTN_DPAD_RIGHT; fast_right = NIN_GLYPH_BTN_R; } - std::snprintf(buf, size, "%s%s %d %s%s", + std::snprintf(buf, size, + "%s%s%d%s%s", fast_left, - left, - variable, - right, + slow_left, + *variable, + slow_right, fast_right); return 0; } @@ -64,43 +81,53 @@ namespace wups { int_item::restore() { variable = default_value; + on_changed(); } - bool - int_item::callback() + void + int_item::on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) { - if (key.empty()) - return false; + item::on_input(input, repeat); - try { - store(key, variable); - return true; - } - catch (...) { - return false; - } - } + if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT || + repeat & WUPS_CONFIG_BUTTON_LEFT) + variable -= slow_increment; + if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT || + repeat & WUPS_CONFIG_BUTTON_RIGHT) + variable += slow_increment; - void - int_item::on_button_pressed(WUPSConfigButtons buttons) - { - base_item::on_button_pressed(buttons); + if (input.buttons_d & WUPS_CONFIG_BUTTON_L || + repeat & WUPS_CONFIG_BUTTON_L) + variable -= fast_increment; - if (buttons & WUPS_CONFIG_BUTTON_LEFT) - --variable; + if (input.buttons_d & WUPS_CONFIG_BUTTON_R || + repeat & WUPS_CONFIG_BUTTON_R) + variable += fast_increment; - if (buttons & WUPS_CONFIG_BUTTON_RIGHT) - ++variable; + variable = std::clamp(*variable, min_value, max_value); + + on_changed(); + } - if (buttons & WUPS_CONFIG_BUTTON_L) - variable -= 50; - if (buttons & WUPS_CONFIG_BUTTON_R) - variable += 50; + void + int_item::on_changed() + { + if (!key) + return; + if (!variable.changed()) + return; - variable = std::clamp(variable, min_value, max_value); + try { + storage::store(*key, *variable); + variable.reset(); + } + catch (std::exception& e) { + logging::printf("Error storing int: %s", e.what()); + } } -} // namespace wups +} // namespace wups::config diff --git a/source/wupsxx/item.cpp b/source/wupsxx/item.cpp new file mode 100644 index 0000000..d0439f7 --- /dev/null +++ b/source/wupsxx/item.cpp @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: MIT + +#include +#include +#include // snprintf() + +#include "wupsxx/item.hpp" + +#include "wupsxx/config_error.hpp" + + +using namespace std::literals; + + +namespace wups::config { + + namespace dispatchers { + + int32_t + get_display(void* ctx, char* buf, int32_t size) + { + auto i = static_cast(ctx); + return i->get_display(buf, size); + } + + + int32_t + get_selected_display(void* ctx, char* buf, int32_t size) + { + auto i = static_cast(ctx); + return i->get_selected_display(buf, size); + } + + + bool + is_movement_allowed(void* ctx) + { + auto i = static_cast(ctx); + return i->is_movement_allowed(); + } + + + void + on_close(void* ctx) + { + auto i = static_cast(ctx); + i->on_close(); + } + + + void + on_delete(void* ctx) + { + auto i = static_cast(ctx); + i->release(); // don't destroy the handle, it's already happening + delete i; + } + + + void + on_input(void* ctx, WUPSConfigSimplePadData input) + { + // Here we implement a "repeat" function. + using clock = std::chrono::steady_clock; + using time_point = clock::time_point; + + constexpr auto repeat_delay = 500ms; + static std::array pressed_time{}; + auto now = clock::now(); + + unsigned repeat = 0; + for (unsigned b = 0; b < 16; ++b) { + unsigned mask = 1u << b; + if (input.buttons_d & mask) + pressed_time[b] = now; + + if (input.buttons_h & mask) + // if button was held long enough, flag it as being on a repeat state + if (now - pressed_time[b] >= repeat_delay) + repeat |= mask; + + if (input.buttons_r & mask) + pressed_time[b] = {}; + } + + auto i = static_cast(ctx); + i->on_input(input, static_cast(repeat)); + } + + + void + on_input_ex(void* ctx, WUPSConfigComplexPadData input) + { + // TODO: implement "repeat" functionality for extended input too + auto i = static_cast(ctx); + i->on_input(input); + } + + + void + on_selected(void* ctx, bool is_selected) + { + auto i = static_cast(ctx); + i->on_selected(is_selected); + } + + + void + restore_default(void* ctx) + { + auto i = static_cast(ctx); + i->restore(); + } + + } // namespace dispatchers + + + item::item(const std::optional& key, + const std::string& label) : + key{key} + { + WUPSConfigAPIItemOptionsV2 options { + .displayName = label.c_str(), + .context = this, + .callbacks = { + // Note: do not sort, must be initialized in the order of declaration. + .getCurrentValueDisplay = dispatchers::get_display, + .getCurrentValueSelectedDisplay = dispatchers::get_selected_display, + .onSelected = dispatchers::on_selected, + .restoreDefault = dispatchers::restore_default, + .isMovementAllowed = dispatchers::is_movement_allowed, + .onCloseCallback = dispatchers::on_close, + .onInput = dispatchers::on_input, + .onInputEx = dispatchers::on_input_ex, + .onDelete = dispatchers::on_delete, + } + }; + + auto status = WUPSConfigAPI_Item_Create(options, &handle); + if (status != WUPSCONFIG_API_RESULT_SUCCESS) + throw config_error{status, "could not create config item \"" + label + "\""}; + } + + + item::~item() + { + if (handle.handle) + WUPSConfigAPI_Item_Destroy(handle); + } + + + void + item::release() + { + handle = {}; + } + + + int + item::get_display(char* buf, + std::size_t size) + const + { + std::snprintf(buf, size, "NOT IMPLEMENTED"); + return 0; + } + + + int + item::get_selected_display(char* buf, + std::size_t size) + const + { + return get_display(buf, size); + } + + + void + item::on_selected(bool) + {} + + + void + item::restore() + {} + + + bool + item::is_movement_allowed() + const + { + return true; + } + + + void + item::on_close() + {} + + + void + item::on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT /*repeat*/) + { + if (input.buttons_d & WUPS_CONFIG_BUTTON_X) + restore(); + } + + + void + item::on_input(WUPSConfigComplexPadData /*input*/) + {} + +} // namespace wups::config diff --git a/source/wupsxx/storage.cpp b/source/wupsxx/storage.cpp index 2e9c38c..59b0fa8 100644 --- a/source/wupsxx/storage.cpp +++ b/source/wupsxx/storage.cpp @@ -1,80 +1,28 @@ // SPDX-License-Identifier: MIT -#include +#include #include "wupsxx/storage.hpp" -namespace wups { +namespace wups::storage { - template<> - std::expected - load(const std::string& key) - { - bool value; - auto err = WUPS_GetBool(nullptr, key.c_str(), &value); - if (err != WUPS_STORAGE_ERROR_SUCCESS) - return std::unexpected{err}; - return value; - } - - - template<> - std::expected - load(const std::string& key) - { - int value; - auto err = WUPS_GetInt(nullptr, key.c_str(), &value); - if (err != WUPS_STORAGE_ERROR_SUCCESS) - return std::unexpected{err}; - return value; - } - - - template<> - std::expected - load(const std::string& key) - { - // Note: we don't have WUPS_GetSize() so we can't know how big this has to be. - std::string value(1024, '\0'); - auto err = WUPS_GetString(nullptr, key.c_str(), value.data(), value.size()); - if (err != WUPS_STORAGE_ERROR_SUCCESS) - return std::unexpected{err}; - auto end = value.find('\0'); - return value.substr(0, end); - } - - - template<> void - store(const std::string& key, - const bool& value) + save() { - auto err = WUPS_StoreBool(nullptr, key.c_str(), value); - if (err != WUPS_STORAGE_ERROR_SUCCESS) - throw err; + auto status = WUPSStorageAPI::SaveStorage(); + if (status != WUPS_STORAGE_ERROR_SUCCESS) + throw storage_error{"error saving storage", status}; } - template<> void - store(const std::string& key, - const int& value) + reload() { - auto err = WUPS_StoreInt(nullptr, key.c_str(), value); - if (err != WUPS_STORAGE_ERROR_SUCCESS) - throw err; + auto status = WUPSStorageAPI::ForceReloadStorage(); + if (status != WUPS_STORAGE_ERROR_SUCCESS) + throw storage_error{"error reloading storage", status}; } - template<> - void - store(const std::string& key, - const std::string& value) - { - auto err = WUPS_StoreString(nullptr, key.c_str(), value.c_str()); - if (err != WUPS_STORAGE_ERROR_SUCCESS) - throw err; - } - -} // namespace wups +} // namespace wups::storage diff --git a/source/wupsxx/storage_error.cpp b/source/wupsxx/storage_error.cpp new file mode 100644 index 0000000..9b80410 --- /dev/null +++ b/source/wupsxx/storage_error.cpp @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +#include "wupsxx/storage_error.hpp" + + +namespace wups::storage { + + storage_error::storage_error(const std::string& msg, + WUPSStorageError status) : + std::runtime_error{msg + ": " + + std::string(WUPSStorageAPI::GetStatusStr(status))} + {} + +} diff --git a/source/wupsxx/text_item.cpp b/source/wupsxx/text_item.cpp index c414b52..7973643 100644 --- a/source/wupsxx/text_item.cpp +++ b/source/wupsxx/text_item.cpp @@ -1,72 +1,169 @@ // SPDX-License-Identifier: MIT -#include -#include -#include +#include // clamp(), min() +#include // snprintf() #include "wupsxx/text_item.hpp" +#include "nintendo_glyphs.h" -using namespace std::literals; -namespace wups { +namespace { - text_item::text_item(const std::string& key, - const std::string& name, - const std::string& text) : - base_item{key, name}, - text{text} + const std::string left_glyph = NIN_GLYPH_BTN_DPAD_LEFT; + const std::string right_glyph = NIN_GLYPH_BTN_DPAD_RIGHT; + +} + + +namespace wups::config { + + text_item::text_item(const std::optional& key, + const std::string& label, + const std::string& text, + std::size_t max_width) : + item{key, label}, + text{text}, + max_width{std::min(max_width, 79)} {} + std::unique_ptr + text_item::create(const std::optional& key, + const std::string& label, + const std::string& text, + std::size_t max_width) + { + return std::make_unique(key, label, text, max_width); + } + + + int + text_item::get_display(char* buf, + std::size_t size) + const + { + // Note: `buf` is a C string, it needs a null terminator at the end, + // so the effective `width` is one less than `size`. + std::size_t width = std::min(size - 1, max_width); + + if (width >= text.size()) { + // Easy case: text fits, just show it all. + std::snprintf(buf, size, "%s", text.c_str()); + return 0; + } + + const char* ellipsis = "…"; + + std::string prefix; + if (first > 0) + prefix = ellipsis; + if (width < prefix.size()) // sanity check + return -1; + width -= prefix.size(); + + std::size_t last = first + width; + std::string suffix; + if (last < text.size()) + suffix = ellipsis; + if (width < suffix.size()) // sanity check + return -1; + width -= suffix.size(); + + std::string slice = text.substr(first, width); + + std::snprintf(buf, size, + "%s%s%s", + prefix.c_str(), + slice.c_str(), + suffix.c_str()); + + return 0; + } + + int - text_item::get_current_value_display(char* buf, - std::size_t size) + text_item::get_selected_display(char* buf, + std::size_t size) const { - auto width = std::min(size - 1, max_width); + // Note: `buf` is a C string, it needs a null terminator at the end, + // so the effective `width` is one less than `size`. + std::size_t width = std::min(size - 1, max_width); + + if (width >= text.size()) { + // Easy case: text fits, just show it all. + std::snprintf(buf, size, "%s", text.c_str()); + return 0; + } + + std::string prefix; + if (first > 0) + prefix = left_glyph; + if (width < prefix.size()) // sanity check + return -1; + width -= prefix.size(); + + std::size_t last = first + width; + std::string suffix; + if (last < text.size()) + suffix = right_glyph; + if (width < suffix.size()) // sanity check + return -1; + width -= suffix.size(); + + std::string slice = text.substr(first, width); std::snprintf(buf, size, - "%*.*s", - width, - width, - text.c_str() + start); + "%s%s%s", + prefix.c_str(), + slice.c_str(), + suffix.c_str()); return 0; } void - text_item::on_selected(bool is_selected) + text_item::on_selected(bool /*is_selected*/) { - if (!is_selected) - start = 0; + // if (!is_selected) + // first = 0; } void - text_item::on_button_pressed(WUPSConfigButtons buttons) + text_item::on_input(WUPSConfigSimplePadData input, + WUPS_CONFIG_SIMPLE_INPUT repeat) { - base_item::on_button_pressed(buttons); + item::on_input(input, repeat); if (text.empty()) return; - int tsize = static_cast(text.size()); - - if (tsize <= max_width) + // If text is fully visible, no scrolling happens. + if (text.size() <= max_width) return; - if (buttons & WUPS_CONFIG_BUTTON_LEFT) - start -= 5; + // Handle text scrolling + + const std::size_t max_first = text.size() - max_width + left_glyph.size(); + + if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT || + repeat & WUPS_CONFIG_BUTTON_LEFT) + if (first > 0) + --first; + + if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT || + repeat & WUPS_CONFIG_BUTTON_RIGHT) + if (first < max_first) + ++first; - if (buttons & WUPS_CONFIG_BUTTON_RIGHT) - start += 5; + if (input.buttons_d & WUPS_CONFIG_BUTTON_L) + first = 0; - if (start >= tsize - max_width) - start = tsize - max_width; - if (start < 0) - start = 0; + if (input.buttons_d & WUPS_CONFIG_BUTTON_R) + first = max_first; } -} // namespace wups +} // namespace wups::config From 8b92a0c11dea1b846c8becfffaa32a7453ac8fa3 Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Wed, 5 Jun 2024 22:58:33 -0300 Subject: [PATCH 09/17] Merged libcurlwrapper and timezone service changes. --HG-- branch : upstream --- Dockerfile | 3 +- Makefile | 4 +- include/cfg.hpp | 4 + include/timezone_query_item.hpp | 35 ++++++- include/utils.hpp | 10 +- source/cfg.cpp | 6 ++ source/config_screen.cpp | 5 +- source/core.cpp | 6 +- source/http_client.cpp | 169 ++------------------------------ source/timezone_query_item.cpp | 95 ++++++++++++++++-- source/utils.cpp | 142 +++++++++++++++++++++++++-- 11 files changed, 290 insertions(+), 189 deletions(-) diff --git a/Dockerfile b/Dockerfile index 99b666b..90410f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,8 @@ FROM devkitpro/devkitppc -COPY --from=ghcr.io/wiiu-env/wiiupluginsystem:20240505 /artifacts $DEVKITPRO +COPY --from=ghcr.io/wiiu-env/libcurlwrapper:20240505 /artifacts $DEVKITPRO COPY --from=ghcr.io/wiiu-env/libnotifications:20240426 /artifacts $DEVKITPRO +COPY --from=ghcr.io/wiiu-env/wiiupluginsystem:20240505 /artifacts $DEVKITPRO RUN git config --global --add safe.directory /project diff --git a/Makefile b/Makefile index ac0dc85..e19e581 100644 --- a/Makefile +++ b/Makefile @@ -82,13 +82,13 @@ LDFLAGS = -g \ -Wl,-Map,$(notdir $*.map) \ $(CXXFLAGS) -LIBS := -lnotifications -lwups -lwut +LIBS := -lcurlwrapper -lnotifications -lwups -lwut #------------------------------------------------------------------------------- # list of directories containing libraries, this must be the top level # containing include and lib #------------------------------------------------------------------------------- -LIBDIRS := $(WUMS_ROOT) $(WUPS_ROOT) $(WUT_ROOT) +LIBDIRS := $(WUMS_ROOT) $(WUPS_ROOT) $(WUT_ROOT) $(PORTLIBS) #------------------------------------------------------------------------------- # no real need to edit anything past this point unless you need to add additional diff --git a/include/cfg.hpp b/include/cfg.hpp index 99f331b..1e69b57 100644 --- a/include/cfg.hpp +++ b/include/cfg.hpp @@ -17,6 +17,7 @@ namespace cfg { extern const char* sync; extern const char* threads; extern const char* tolerance; + extern const char* tz_service; extern const char* utc_offset; } @@ -29,6 +30,7 @@ namespace cfg { extern const char* sync; extern const char* threads; extern const char* tolerance; + extern const char* tz_service; extern const char* utc_offset; } @@ -40,6 +42,7 @@ namespace cfg { extern const bool sync; extern const int threads; extern const std::chrono::milliseconds tolerance; + extern const int tz_service; } @@ -50,6 +53,7 @@ namespace cfg { extern bool sync; extern int threads; extern std::chrono::milliseconds tolerance; + extern int tz_service; extern std::chrono::minutes utc_offset; diff --git a/include/timezone_query_item.hpp b/include/timezone_query_item.hpp index 10df9d7..1eb761d 100644 --- a/include/timezone_query_item.hpp +++ b/include/timezone_query_item.hpp @@ -5,19 +5,46 @@ #include -#include "wupsxx/text_item.hpp" +#include "wupsxx/item.hpp" +#include "wupsxx/var_watch.hpp" -struct timezone_query_item : wups::config::text_item { +class timezone_query_item : public wups::config::item { - timezone_query_item(); + // We store the geolocation option as an integer, no point in parsing any complex + // string since we need specific implementations for each service. + + wups::config::var_watch variable; + const int default_value; + std::string text; + +public: + + timezone_query_item(const std::string& key, + const std::string& label, + int& variable, + int default_value); static - std::unique_ptr create(); + std::unique_ptr create(const std::string& key, + const std::string& label, + int& variable, + int default_value); + + + int get_display(char* buf, std::size_t size) const override; + + int get_selected_display(char* buf, std::size_t size) const override; + + void restore() override; void on_input(WUPSConfigSimplePadData input, WUPS_CONFIG_SIMPLE_INPUT repeat) override; +private: + + void on_changed(); + void run(); }; diff --git a/include/utils.hpp b/include/utils.hpp index 6393d3e..df1cba5 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -38,9 +38,17 @@ namespace utils { }; + int + get_num_tz_services(); + + + const char* + get_tz_service_name(int idx); + + std::pair - fetch_timezone(); + fetch_timezone(int idx); } // namespace utils diff --git a/source/cfg.cpp b/source/cfg.cpp index 6c9682b..faeb601 100644 --- a/source/cfg.cpp +++ b/source/cfg.cpp @@ -3,6 +3,7 @@ #include "cfg.hpp" #include "logging.hpp" +#include "nintendo_glyphs.h" #include "time_utils.hpp" #include "utils.hpp" #include "wupsxx/storage.hpp" @@ -26,6 +27,7 @@ namespace cfg { const char* sync = "sync"; const char* threads = "threads"; const char* tolerance = "tolerance"; + const char* tz_service = "tz_service"; const char* utc_offset = "utc_offset"; } @@ -38,6 +40,7 @@ namespace cfg { const char* sync = "Syncing Enabled"; const char* threads = "Background Threads"; const char* tolerance = "Tolerance"; + const char* tz_service = "Detect Time Zone (press " NIN_GLYPH_BTN_A ")"; const char* utc_offset = "Time Offset (UTC)"; } @@ -50,6 +53,7 @@ namespace cfg { const bool sync = false; const int threads = 4; const milliseconds tolerance = 500ms; + const int tz_service = 0; } @@ -60,6 +64,7 @@ namespace cfg { bool sync = defaults::sync; int threads = defaults::threads; milliseconds tolerance = defaults::tolerance; + int tz_service = defaults::tz_service; minutes utc_offset = 0min; @@ -87,6 +92,7 @@ namespace cfg { load_or_init(key::sync, sync); load_or_init(key::threads, threads); load_or_init(key::tolerance, tolerance); + load_or_init(key::tz_service, tz_service); load_or_init(key::utc_offset, utc_offset); // logging::printf("Loaded settings."); } diff --git a/source/config_screen.cpp b/source/config_screen.cpp index 0e044f5..857a986 100644 --- a/source/config_screen.cpp +++ b/source/config_screen.cpp @@ -46,7 +46,10 @@ make_config_screen() cfg::label::utc_offset, cfg::utc_offset)); - cat.add(timezone_query_item::create()); + cat.add(timezone_query_item::create(cfg::key::tz_service, + cfg::label::tz_service, + cfg::tz_service, + cfg::defaults::tz_service)); cat.add(bool_item::create(cfg::key::auto_tz, cfg::label::auto_tz, diff --git a/source/core.cpp b/source/core.cpp index f3befc7..3e7716c 100644 --- a/source/core.cpp +++ b/source/core.cpp @@ -272,17 +272,17 @@ namespace core { if (cfg::auto_tz) { try { - auto [name, offset] = utils::fetch_timezone(); + auto [name, offset] = utils::fetch_timezone(cfg::tz_service); if (offset != cfg::utc_offset) { cfg::set_and_store_utc_offset(offset); notify::info(notify::level::verbose, - "Updated timezone to " + name + + "Updated time zone to " + name + "(" + time_utils::tz_offset_to_string(offset) + ")"); } } catch (std::exception& e) { notify::error(notify::level::verbose, - "Failed to update timezone: "s + e.what()); + "Failed to update time zone: "s + e.what()); } } diff --git a/source/http_client.cpp b/source/http_client.cpp index 43b43e0..a48773c 100644 --- a/source/http_client.cpp +++ b/source/http_client.cpp @@ -1,177 +1,26 @@ // SPDX-License-Identifier: MIT -#include -#include -#include // views::drop() -#include -#include // runtime_error - #include "http_client.hpp" -#include "net/addrinfo.hpp" -#include "net/socket.hpp" -#include "utils.hpp" +#include "curl.hpp" 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: " PLUGIN_NAME "/" PLUGIN_VERSION " (Wii U; Aroma)" + 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; - } - - - // Not very efficient, read one byte at a time. - std::string - recv_until(net::socket& sock, - const std::string& end_token) - { - std::string result; - - char buffer[1]; - while (true) { - if (sock.recv(buffer, 1) == 0) - break; - result.append(buffer, 1); - - // if we found the end token, remove it from the result and break out - if (result.ends_with(end_token)) { - result.resize(result.size() - end_token.size()); - break; - } - } - - return result; - } - - 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; - - net::addrinfo::hints opts { .type = net::socket::type::tcp }; - auto addresses = net::addrinfo::lookup(fields.host, - std::to_string(*fields.port), - opts); - if (addresses.empty()) - throw std::runtime_error{"Host '" + fields.host + "' has no IP addresses."}; - - const auto& host = addresses.front(); - net::socket sock{host.type}; - - sock.connect(host.addr); + curl::global guard; - auto request = build_request(fields); - sock.send_all(request.data(), request.size()); + curl::handle handle; - auto header_str = recv_until(sock, CRLF + CRLF); - auto header = parse_header(header_str); + handle.set_useragent(PLUGIN_NAME "/" PLUGIN_VERSION " (Wii U; Aroma)"); + handle.set_followlocation(true); + handle.set_url(url); - if (!header.type.starts_with("text/plain")) - throw std::runtime_error{"HTTP response is not plain text: \"" - + header.type + "\""}; + handle.perform(); - std::string result(header.length, '\0'); - sock.recv_all(result.data(), result.size()); - return result; + return handle.result; } -} +} // namespace http diff --git a/source/timezone_query_item.cpp b/source/timezone_query_item.cpp index 6f3ccdb..dde55c3 100644 --- a/source/timezone_query_item.cpp +++ b/source/timezone_query_item.cpp @@ -1,27 +1,70 @@ // SPDX-License-Identifier: MIT +#include // snprintf() +#include +#include // BSD strlcpy() + #include "timezone_query_item.hpp" #include "cfg.hpp" +#include "logging.hpp" #include "nintendo_glyphs.h" #include "utils.hpp" +#include "wupsxx/storage.hpp" using namespace std::literals; -timezone_query_item::timezone_query_item() : - wups::config::text_item{{}, - "Detect Time Zone (press " NIN_GLYPH_BTN_A ")", - "using http://ip-api.com", - 30} +timezone_query_item::timezone_query_item(const std::string& key, + const std::string& label, + int& variable, + const int default_value) : + wups::config::item{key, label}, + variable(variable), + default_value{default_value}, + text{"Query "s + utils::get_tz_service_name(variable)} {} std::unique_ptr -timezone_query_item::create() +timezone_query_item::create(const std::string& key, + const std::string& label, + int& variable, + const int default_value) +{ + return std::make_unique(key, label, variable, default_value); +} + + +int +timezone_query_item::get_display(char* buf, std::size_t size) + const +{ + ::strlcpy(buf, text.c_str(), size); + return 0; +} + + +int +timezone_query_item::get_selected_display(char* buf, std::size_t size) + const +{ + std::snprintf(buf, size, + "%s %s %s", + NIN_GLYPH_BTN_DPAD_LEFT, + text.c_str(), + NIN_GLYPH_BTN_DPAD_RIGHT); + return 0; +} + + +void +timezone_query_item::restore() { - return std::make_unique(); + variable = default_value; + + on_changed(); } @@ -29,18 +72,52 @@ void timezone_query_item::on_input(WUPSConfigSimplePadData input, WUPS_CONFIG_SIMPLE_INPUT repeat) { - text_item::on_input(input, repeat); + wups::config::item::on_input(input, repeat); + + const int n = utils::get_num_tz_services(); + + if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT) + --variable; + + if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT) + ++variable; + + // let it wrap around + if (*variable < 0) + variable += n; + if (*variable >= n) + variable -= n; + + on_changed(); if (input.buttons_d & WUPS_CONFIG_BUTTON_A) run(); } +void +timezone_query_item::on_changed() +{ + if (!variable.changed()) + return; + + text = "Query "s + utils::get_tz_service_name(*variable); + + try { + wups::storage::store(*key, *variable); + variable.reset(); + } + catch (std::exception& e) { + logging::printf("Error storing %s: %s", key->c_str(), e.what()); + } +} + + void timezone_query_item::run() { try { - auto [name, offset] = utils::fetch_timezone(); + auto [name, offset] = utils::fetch_timezone(*variable); text = name; cfg::set_and_store_utc_offset(offset); } diff --git a/source/utils.cpp b/source/utils.cpp index fcae7cb..f1956d3 100644 --- a/source/utils.cpp +++ b/source/utils.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT -#include // runtime_error +#include // distance() +#include // logic_error, runtime_error #include "utils.hpp" @@ -40,6 +41,42 @@ namespace utils { } + namespace { + + /* + * Split CSV line: + * - Separator is always `,`. + * - Separators inside quotes (`"` or `'`) are ignored. + * - Don't discard empty tokens. + */ + std::vector + csv_split(const std::string& input) + { + using std::string; + + std::vector result; + + string::size_type start = 0; + for (string::size_type i = 0; i < input.size(); ++i) { + char c = input[i]; + if (c == '"' || c == '\'') { + // jump to the closing quote + i = input.find(c, i + 1); + if (i == string::npos) + break; // if there's no closing quote, it's bad input + } else if (c == ',') { + result.push_back(input.substr(start, i - start)); + start = i + 1; + } + } + // whatever remains from `start` to the end is the last token + result.push_back(input.substr(start)); + return result; + } + + } // namespace + + exec_guard::exec_guard(std::atomic& f) : flag(f), guarded{false} @@ -57,16 +94,105 @@ namespace utils { } + namespace { + constexpr int num_tz_services = 3; + } + + + int + get_num_tz_services() + { + return num_tz_services; + } + + + const char* + get_tz_service_name(int idx) + { + switch (idx) { + case 0: + return "http://ip-api.com"; + case 1: + return "https://ipwho.is"; + case 2: + return "https://ipapi.co"; + default: + throw std::logic_error{"invalid tz service"}; + } + } + + std::pair - fetch_timezone() + fetch_timezone(int idx) { - 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\"."}; + if (idx < 0 || idx >= num_tz_services) + throw std::logic_error{"invalid service"}; + + const char* service = get_tz_service_name(idx); + + static const char* urls[num_tz_services] = { + "http://ip-api.com/csv/?fields=timezone,offset", + "https://ipwho.is/?fields=timezone.id,timezone.offset&output=csv", + "https://ipapi.co/csv" + }; + + std::string response = http::get(urls[idx]); + + switch (idx) { + case 0: // http://ip-api.com + case 1: // https://ipwho.is + { + auto tokens = csv_split(response); + if (size(tokens) != 2) + throw std::runtime_error{"Could not parse response from "s + service}; + std::string name = tokens[0]; + auto offset = std::chrono::seconds{std::stoi(tokens[1])}; + return {name, duration_cast(offset)}; + } + + case 2: // https://ipapi.co + { + // This returns a CSV header and CSV fields in two rows, gotta find + // indexes for "timezone" and "utc_offset" fields. The "utc_offset" is + // returned as +HHMM, not seconds. + auto lines = split(response, "\r\n"); + if (size(lines) != 2) + throw std::runtime_error{"Could not parse response from "s + service}; + + auto keys = csv_split(lines[0]); + auto values = csv_split(lines[1]); + if (size(keys) != size(values)) + throw std::runtime_error{"Incoherent response from "s + service}; + + auto tz_it = std::ranges::find(keys, "timezone"); + auto offset_it = std::ranges::find(keys, "utc_offset"); + if (tz_it == keys.end() || offset_it == keys.end()) + throw std::runtime_error{"Could not find timezone or utc_offset fields" + " in response."}; + + auto tz_idx = std::distance(keys.begin(), tz_it);; + auto offset_idx = std::distance(keys.begin(), offset_it); + + std::string name = values[tz_idx]; + std::string hhmm = values[offset_idx]; + if (empty(hhmm)) + throw std::runtime_error{"Invalid UTC offset string."}; + + char sign = hhmm[0]; + std::string hh = hhmm.substr(1, 2); + std::string mm = hhmm.substr(3, 2); + int h = std::stoi(hh); + int m = std::stoi(mm); + int total = h * 60 + m; + if (sign == '-') + total = -total; + return {name, std::chrono::minutes{total}}; + } + + default: + throw std::logic_error{"invalid tz service"}; + } - int tz_offset_min = std::stoi(tokens[1]) / 60; - return {tokens[0], std::chrono::minutes{tz_offset_min}}; } } // namespace utils From 4967adc7669fcb2b2054fbbaeb2998621614d6d8 Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Thu, 6 Jun 2024 11:05:38 -0300 Subject: [PATCH 10/17] Forgot to add curl-related sources. --HG-- branch : upstream --- include/curl.hpp | 80 ++++++++++++++++++++++++++ source/curl.cpp | 145 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 include/curl.hpp create mode 100644 source/curl.cpp diff --git a/include/curl.hpp b/include/curl.hpp new file mode 100644 index 0000000..9233200 --- /dev/null +++ b/include/curl.hpp @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT + +#ifndef CURL_HPP +#define CURL_HPP + +#include +#include // runtime_error +#include + +#include + + +namespace curl { + + struct error : std::runtime_error { + + CURLcode code; + + error(CURLcode code); + + }; + + + struct global { + + global(); + + ~global(); + + }; + + + class handle { + + CURL* h; + std::unique_ptr error_buffer; + + void init(); + + // static int sockopt_callback(handle* h, curl_socket_t fd, curlsocktype purpose); + + static + std::size_t + write_callback(char* buffer, std::size_t size, std::size_t nmemb, void* ctx); + + protected: + + virtual std::size_t on_recv(const char* buffer, std::size_t size); + + + public: + + std::string result; + + + handle(); + + handle(const handle& other); + + ~handle(); + + + void setopt(CURLoption option, bool arg); + void setopt(CURLoption option, const std::string& arg); + + + // convenience setters + + void set_followlocation(bool enable); + void set_url(const std::string& url); + void set_useragent(const std::string& agent); + + void perform(); + + }; + +} // namespace curl + + +#endif diff --git a/source/curl.cpp b/source/curl.cpp new file mode 100644 index 0000000..ef65e66 --- /dev/null +++ b/source/curl.cpp @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: MIT + +#include "curl.hpp" + +#include "logging.hpp" + + +namespace curl { + + error::error(CURLcode code) : + std::runtime_error{curl_easy_strerror(code)}, + code{code} + {} + + + void + check(CURLcode code) + { + if (code == CURLE_OK) + return; + throw error{code}; + } + + + global::global() + { + check(curl_global_init(CURL_GLOBAL_DEFAULT )); + } + + + global::~global() + { + curl_global_cleanup(); + } + + + + handle::handle() : + h{curl_easy_init()} + { + init(); + } + + + handle::handle(const handle& other) : + h{curl_easy_duphandle(other.h)} + { + init(); + } + + + handle::~handle() + { + curl_easy_cleanup(h); + } + + + void + handle::init() + { + if (!h) + throw std::logic_error{"curl easy handle is null"}; + + error_buffer = std::make_unique(CURL_ERROR_SIZE); + + check(curl_easy_setopt(h, CURLOPT_ERRORBUFFER, error_buffer.get())); + + check(curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, &handle::write_callback)); + check(curl_easy_setopt(h, CURLOPT_WRITEDATA, this)); + } + + + std::size_t + handle::write_callback(char* buffer, + std::size_t /*size*/, + std::size_t nmemb, + void* ctx) + { + handle* h = static_cast(ctx); + try { + if (!h) + throw std::logic_error{"null handle"}; + return h->on_recv(buffer, nmemb); + } + catch (std::exception& e) { + logging::printf("curl::handle::write_callback(): %s", e.what()); + return CURL_WRITEFUNC_ERROR; + } + } + + + std::size_t + handle::on_recv(const char* buffer, std::size_t size) + { + result.append(buffer, size); + return size; + } + + + void + handle::setopt(CURLoption option, bool arg) + { + long enable = arg; + check(curl_easy_setopt(h, option, enable)); + } + + + void + handle::setopt(CURLoption option, const std::string& arg) + { + check(curl_easy_setopt(h, option, arg.c_str())); + } + + + // convenience setters + + void + handle::set_followlocation(bool enable) + { + setopt(CURLOPT_FOLLOWLOCATION, enable); + } + + + void + handle::set_url(const std::string& url) + { + setopt(CURLOPT_URL, url); + } + + + void + handle::set_useragent(const std::string& agent) + { + setopt(CURLOPT_USERAGENT, agent); + } + + + void + handle::perform() + { + check(curl_easy_perform(h)); + } + + +} // namespace curl From 258eed960216c703affd2434883e1f6bade7998c Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Wed, 11 Sep 2024 23:46:52 -0300 Subject: [PATCH 11/17] Backported Time Sync changes to upstream. --HG-- branch : upstream --- .gitmodules | 3 + Makefile | 16 +- external/libwupsxx | 1 + include/async_queue.hpp | 8 +- include/cfg.hpp | 56 ++--- include/clock_item.hpp | 37 ++- include/config_screen.hpp | 11 - include/core.hpp | 16 +- include/curl.hpp | 8 +- include/http_client.hpp | 8 +- include/logging.hpp | 18 -- include/net/address.hpp | 8 +- include/net/addrinfo.hpp | 8 +- include/net/error.hpp | 8 +- include/net/socket.hpp | 12 +- include/nintendo_glyphs.h | 159 ------------- include/notify.hpp | 8 +- include/ntp.hpp | 8 +- include/preview_screen.hpp | 10 +- include/synchronize_item.hpp | 50 ++++ include/thread_pool.hpp | 8 +- include/time_utils.hpp | 31 +-- include/time_zone_offset_item.hpp | 55 +++++ include/time_zone_query_item.hpp | 55 +++++ include/timezone_offset_item.hpp | 43 ---- include/timezone_query_item.hpp | 52 ----- include/utc.hpp | 8 +- include/utils.hpp | 8 +- include/verbosity_item.hpp | 18 +- include/wupsxx/bool_item.hpp | 57 ----- include/wupsxx/category.hpp | 41 ---- include/wupsxx/config_error.hpp | 22 -- include/wupsxx/duration_items.hpp | 19 -- include/wupsxx/int_item.hpp | 17 -- include/wupsxx/item.hpp | 61 ----- include/wupsxx/numeric_item.hpp | 63 ----- include/wupsxx/storage.hpp | 69 ------ include/wupsxx/storage_error.hpp | 22 -- include/wupsxx/text_item.hpp | 47 ---- include/wupsxx/var_watch.hpp | 275 ---------------------- source/cfg.cpp | 346 +++++++++++++++++++++------- source/clock_item.cpp | 72 +++--- source/config_screen.cpp | 90 -------- source/core.cpp | 170 +++++++++----- source/curl.cpp | 15 +- source/http_client.cpp | 8 +- source/logging.cpp | 87 ------- source/main.cpp | 93 +++----- source/net/address.cpp | 8 +- source/net/addrinfo.cpp | 8 +- source/net/error.cpp | 10 +- source/net/socket.cpp | 15 +- source/notify.cpp | 20 +- source/ntp.cpp | 8 +- source/preview_screen.cpp | 23 +- source/synchronize_item.cpp | 81 +++++++ source/thread_pool.cpp | 8 +- source/time_utils.cpp | 31 +-- source/time_zone_offset_item.cpp | 111 +++++++++ source/time_zone_query_item.cpp | 130 +++++++++++ source/timezone_offset_item.cpp | 122 ---------- source/timezone_query_item.cpp | 127 ---------- source/utc.cpp | 8 +- source/utils.cpp | 8 +- source/verbosity_item.cpp | 58 +++-- source/wupsxx/bool_item.cpp | 109 --------- source/wupsxx/category.cpp | 72 ------ source/wupsxx/config_error.cpp | 18 -- source/wupsxx/duration_items.cpp | 11 - source/wupsxx/int_item.cpp | 6 - source/wupsxx/item.cpp | 214 ----------------- source/wupsxx/numeric_item_impl.hpp | 155 ------------- source/wupsxx/storage.cpp | 28 --- source/wupsxx/storage_error.cpp | 14 -- source/wupsxx/text_item.cpp | 171 -------------- 75 files changed, 1256 insertions(+), 2623 deletions(-) create mode 100644 .gitmodules create mode 160000 external/libwupsxx delete mode 100644 include/config_screen.hpp delete mode 100644 include/logging.hpp delete mode 100644 include/nintendo_glyphs.h create mode 100644 include/synchronize_item.hpp create mode 100644 include/time_zone_offset_item.hpp create mode 100644 include/time_zone_query_item.hpp delete mode 100644 include/timezone_offset_item.hpp delete mode 100644 include/timezone_query_item.hpp delete mode 100644 include/wupsxx/bool_item.hpp delete mode 100644 include/wupsxx/category.hpp delete mode 100644 include/wupsxx/config_error.hpp delete mode 100644 include/wupsxx/duration_items.hpp delete mode 100644 include/wupsxx/int_item.hpp delete mode 100644 include/wupsxx/item.hpp delete mode 100644 include/wupsxx/numeric_item.hpp delete mode 100644 include/wupsxx/storage.hpp delete mode 100644 include/wupsxx/storage_error.hpp delete mode 100644 include/wupsxx/text_item.hpp delete mode 100644 include/wupsxx/var_watch.hpp delete mode 100644 source/config_screen.cpp delete mode 100644 source/logging.cpp create mode 100644 source/synchronize_item.cpp create mode 100644 source/time_zone_offset_item.cpp create mode 100644 source/time_zone_query_item.cpp delete mode 100644 source/timezone_offset_item.cpp delete mode 100644 source/timezone_query_item.cpp delete mode 100644 source/wupsxx/bool_item.cpp delete mode 100644 source/wupsxx/category.cpp delete mode 100644 source/wupsxx/config_error.cpp delete mode 100644 source/wupsxx/duration_items.cpp delete mode 100644 source/wupsxx/int_item.cpp delete mode 100644 source/wupsxx/item.cpp delete mode 100644 source/wupsxx/numeric_item_impl.hpp delete mode 100644 source/wupsxx/storage.cpp delete mode 100644 source/wupsxx/storage_error.cpp delete mode 100644 source/wupsxx/text_item.cpp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7b898ed --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/libwupsxx"] + path = external/libwupsxx + url = https://github.com/dkosmari/libwupsxx.git diff --git a/Makefile b/Makefile index 740ec19..6aea169 100644 --- a/Makefile +++ b/Makefile @@ -20,11 +20,8 @@ WUT_ROOT := $(DEVKITPRO)/wut # PLUGIN_AUTHOR sets the author of the plugin. # PLUGIN_LICENSE sets the license of the plugin. #------------------------------------------------------------------------------- -PLUGIN_NAME := Wii U Time Sync -PLUGIN_DESCRIPTION := A plugin that synchronizes the system clock to the Internet. -PLUGIN_VERSION := v3.1.0 -PLUGIN_AUTHOR := Nightkingale, Daniel K. O. -PLUGIN_LICENSE := MIT +PLUGIN_NAME := Wii U Time Sync +PLUGIN_VERSION := v3.1.0+ #------------------------------------------------------------------------------- # TARGET is the name of the output. @@ -35,9 +32,9 @@ PLUGIN_LICENSE := MIT #------------------------------------------------------------------------------- TARGET := Wii_U_Time_Sync BUILD := build -SOURCES := source source/net source/wupsxx +SOURCES := source source/net external/libwupsxx/src DATA := data -INCLUDES := include +INCLUDES := include external/libwupsxx/include #------------------------------------------------------------------------------- # DEBUG sets the debug flag for the plugin. @@ -65,10 +62,7 @@ CFLAGS := $(WARN_FLAGS) $(OPTFLAGS) $(MACHDEP) CXXFLAGS := $(CFLAGS) -std=c++23 DEFINES := '-DPLUGIN_NAME="$(PLUGIN_NAME)"' \ - '-DPLUGIN_DESCRIPTION="$(PLUGIN_DESCRIPTION)"' \ - '-DPLUGIN_VERSION="$(PLUGIN_VERSION)"' \ - '-DPLUGIN_AUTHOR="$(PLUGIN_AUTHOR)"' \ - '-DPLUGIN_LICENSE="$(PLUGIN_LICENSE)"' + '-DPLUGIN_VERSION="$(PLUGIN_VERSION)"' # Note: INCLUDE will be defined later, so CPPFLAGS has to be of the recursive flavor. CPPFLAGS = $(INCLUDE) -D__WIIU__ -D__WUT__ -D__WUPS__ $(DEFINES) diff --git a/external/libwupsxx b/external/libwupsxx new file mode 160000 index 0000000..806e94c --- /dev/null +++ b/external/libwupsxx @@ -0,0 +1 @@ +Subproject commit 806e94c6e938b3875ccad5c3451d51e2a6f866ed diff --git a/include/async_queue.hpp b/include/async_queue.hpp index de3e329..a08536f 100644 --- a/include/async_queue.hpp +++ b/include/async_queue.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef ASYNC_QUEUE_HPP #define ASYNC_QUEUE_HPP diff --git a/include/cfg.hpp b/include/cfg.hpp index b006a47..0d683d3 100644 --- a/include/cfg.hpp +++ b/include/cfg.hpp @@ -1,4 +1,11 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * Copyright (C) 2024 Nightkingale + * + * SPDX-License-Identifier: MIT + */ #ifndef CFG_HPP #define CFG_HPP @@ -9,62 +16,25 @@ namespace cfg { - namespace key { - extern const char* auto_tz; - extern const char* msg_duration; - extern const char* notify; - extern const char* server; - extern const char* sync; - extern const char* threads; - extern const char* timeout; - extern const char* tolerance; - extern const char* tz_service; - extern const char* utc_offset; - } - - - namespace label { - extern const char* auto_tz; - extern const char* msg_duration; - extern const char* notify; - extern const char* server; - extern const char* sync; - extern const char* threads; - extern const char* timeout; - extern const char* tolerance; - extern const char* tz_service; - extern const char* utc_offset; - } - - namespace defaults { - extern const bool auto_tz; - extern const std::chrono::seconds msg_duration; - extern const int notify; - extern const std::string server; - extern const bool sync; - extern const int threads; - extern const std::chrono::seconds timeout; - extern const std::chrono::milliseconds tolerance; - extern const int tz_service; - } - - extern bool auto_tz; extern std::chrono::seconds msg_duration; extern int notify; extern std::string server; - extern bool sync; + extern bool sync_on_boot; + extern bool sync_on_changes; extern int threads; extern std::chrono::seconds timeout; extern std::chrono::milliseconds tolerance; extern int tz_service; extern std::chrono::minutes utc_offset; + void save_important_vars(); + + void init(); void load(); void reload(); void save(); - void migrate_old_config(); void set_and_store_utc_offset(std::chrono::minutes tz_offset); diff --git a/include/clock_item.hpp b/include/clock_item.hpp index ea9d292..32087ac 100644 --- a/include/clock_item.hpp +++ b/include/clock_item.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef CLOCK_ITEM_HPP #define CLOCK_ITEM_HPP @@ -7,35 +13,42 @@ #include // unique_ptr<> #include -#include "wupsxx/text_item.hpp" +#include +#include -struct clock_item : wups::config::text_item { +struct clock_item : wups::config::button_item { struct server_info { - text_item* name = nullptr; - text_item* correction = nullptr; - text_item* latency = nullptr; + wups::config::text_item* name = nullptr; + wups::config::text_item* correction = nullptr; + wups::config::text_item* latency = nullptr; }; std::string now_str; - std::string stats_str; + std::string diff_str; std::map server_infos; + clock_item(); static - std::unique_ptr create(); + std::unique_ptr + create(); + + virtual + void + on_started() override; - void on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) override; + void + update_status_msg(); - void refresh_now_str(); - void run(); + void + run(); }; diff --git a/include/config_screen.hpp b/include/config_screen.hpp deleted file mode 100644 index ab2a057..0000000 --- a/include/config_screen.hpp +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef CONFIG_SCREEN_HPP -#define CONFIG_SCREEN_HPP - -#include "wupsxx/category.hpp" - - -wups::config::category make_config_screen(); - -#endif diff --git a/include/core.hpp b/include/core.hpp index 83768ae..a4f73fc 100644 --- a/include/core.hpp +++ b/include/core.hpp @@ -1,8 +1,15 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef CORE_HPP #define CORE_HPP +#include #include #include // pair<> @@ -14,9 +21,12 @@ namespace core { using time_utils::dbl_seconds; - std::pair ntp_query(net::address address); + std::pair ntp_query(std::stop_token token, + net::address address); - void run(); + void + run(std::stop_token token, + bool silent); std::string local_clock_to_string(); diff --git a/include/curl.hpp b/include/curl.hpp index 9233200..1e75302 100644 --- a/include/curl.hpp +++ b/include/curl.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef CURL_HPP #define CURL_HPP diff --git a/include/http_client.hpp b/include/http_client.hpp index a48122e..7718545 100644 --- a/include/http_client.hpp +++ b/include/http_client.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef HTTP_CLIENT_HPP #define HTTP_CLIENT_HPP diff --git a/include/logging.hpp b/include/logging.hpp deleted file mode 100644 index 6d05691..0000000 --- a/include/logging.hpp +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef LOGGING_HPP -#define LOGGING_HPP - - -namespace logging { - - void initialize(); - - void finalize(); - - __attribute__(( __format__ (__printf__, 1, 2) )) - void printf(const char* fmt, ...); - -} // namespace logging - -#endif diff --git a/include/net/address.hpp b/include/net/address.hpp index 199d03a..c7c9516 100644 --- a/include/net/address.hpp +++ b/include/net/address.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef NET_ADDRESS_HPP #define NET_ADDRESS_HPP diff --git a/include/net/addrinfo.hpp b/include/net/addrinfo.hpp index 0c5d074..07457ff 100644 --- a/include/net/addrinfo.hpp +++ b/include/net/addrinfo.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef NET_ADDRINFO_HPP #define NET_ADDRINFO_HPP diff --git a/include/net/error.hpp b/include/net/error.hpp index 73af641..488ebfe 100644 --- a/include/net/error.hpp +++ b/include/net/error.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef NET_ERROR_HPP #define NET_ERROR_HPP diff --git a/include/net/socket.hpp b/include/net/socket.hpp index c85f5d7..512eb49 100644 --- a/include/net/socket.hpp +++ b/include/net/socket.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef NET_SOCKET_HPP #define NET_SOCKET_HPP @@ -13,8 +19,8 @@ #include #include // SO_*, MSG_* -#include "net/address.hpp" -#include "net/error.hpp" +#include "address.hpp" +#include "error.hpp" // Note: very simplified socket class, only what the Wii U supports. diff --git a/include/nintendo_glyphs.h b/include/nintendo_glyphs.h deleted file mode 100644 index 0fd0fdc..0000000 --- a/include/nintendo_glyphs.h +++ /dev/null @@ -1,159 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef NINTENDO_GLYPHS_H -#define NINTENDO_GLYPHS_H - -#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/include/notify.hpp b/include/notify.hpp index 07a2441..63ac131 100644 --- a/include/notify.hpp +++ b/include/notify.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef NOTIFY_HPP #define NOTIFY_HPP diff --git a/include/ntp.hpp b/include/ntp.hpp index a857ec2..07ac454 100644 --- a/include/ntp.hpp +++ b/include/ntp.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef NTP_HPP #define NTP_HPP diff --git a/include/preview_screen.hpp b/include/preview_screen.hpp index 87f7097..d5dd52c 100644 --- a/include/preview_screen.hpp +++ b/include/preview_screen.hpp @@ -1,9 +1,15 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef PREVIEW_SCREEN_HPP #define PREVIEW_SCREEN_HPP -#include "wupsxx/category.hpp" +#include wups::config::category make_preview_screen(); diff --git a/include/synchronize_item.hpp b/include/synchronize_item.hpp new file mode 100644 index 0000000..c286d93 --- /dev/null +++ b/include/synchronize_item.hpp @@ -0,0 +1,50 @@ +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef SYNCHRONIZE_ITEM_HPP +#define SYNCHRONIZE_ITEM_HPP + +#include +#include +#include + +#include + + +struct synchronize_item : wups::config::button_item { + + std::future sync_result; + std::stop_source sync_stopper; + + + synchronize_item(); + + + static + std::unique_ptr + create(); + + + virtual + void + on_started() override; + + + virtual + void + on_finished() override; + + + virtual + void + on_cancel() override; + +}; + + +#endif diff --git a/include/thread_pool.hpp b/include/thread_pool.hpp index 21ce8ac..4f8ff6d 100644 --- a/include/thread_pool.hpp +++ b/include/thread_pool.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef THREAD_POOL_HPP #define THREAD_POOL_HPP diff --git a/include/time_utils.hpp b/include/time_utils.hpp index 7769411..4aaade2 100644 --- a/include/time_utils.hpp +++ b/include/time_utils.hpp @@ -1,11 +1,16 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef TIME_UTILS_HPP #define TIME_UTILS_HPP #include #include -#include namespace time_utils { @@ -14,28 +19,6 @@ namespace time_utils { using dbl_seconds = std::chrono::duration; - // Type trait to identify when a type is std::chrono::duration<> - - template - struct is_duration : std::false_type {}; - - template - struct is_duration> : std::true_type {}; - - // convenience variable template - template - constexpr bool is_duration_v = is_duration::value; - - - template - concept duration = is_duration_v; - - - template - std::string to_string(T t); - - - // Generate time duration strings for humans. std::string seconds_to_human(dbl_seconds s, bool show_positive = false); diff --git a/include/time_zone_offset_item.hpp b/include/time_zone_offset_item.hpp new file mode 100644 index 0000000..5d2cce8 --- /dev/null +++ b/include/time_zone_offset_item.hpp @@ -0,0 +1,55 @@ +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef TIME_ZONE_OFFSET_ITEM_HPP +#define TIME_ZONE_OFFSET_ITEM_HPP + +#include +#include + +#include + + +struct time_zone_offset_item : wups::config::var_item { + + enum field_id : unsigned { + hours, + minutes, + }; + + + field_id editing; + + + time_zone_offset_item(const std::string& label, + std::chrono::minutes& variable, + std::chrono::minutes default_value); + + static + std::unique_ptr + create(const std::string& label, + std::chrono::minutes& variable, + std::chrono::minutes default_value); + + virtual void get_display(char* buf, std::size_t size) const override; + + virtual void get_focused_display(char* buf, std::size_t size) const override; + + virtual + wups::config::focus_status + on_input(const wups::config::simple_pad_data& input) override; + + +private: + + void on_changed(); + +}; + + +#endif diff --git a/include/time_zone_query_item.hpp b/include/time_zone_query_item.hpp new file mode 100644 index 0000000..3e53809 --- /dev/null +++ b/include/time_zone_query_item.hpp @@ -0,0 +1,55 @@ +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ + +#ifndef TIME_ZONE_QUERY_ITEM_HPP +#define TIME_ZONE_QUERY_ITEM_HPP + +#include + +#include + + +class time_zone_query_item : public wups::config::var_item { + + // We store the geolocation option as an integer, no point in parsing any complex + // string since we need specific implementations for each service. + + std::string text; + +public: + + time_zone_query_item(const std::string& label, + int& variable, + int default_value); + + static + std::unique_ptr + create(const std::string& label, + int& variable, + int default_value); + + + virtual + void + get_display(char* buf, std::size_t size) const override; + + virtual + void + get_focused_display(char* buf, std::size_t size) const override; + + virtual + wups::config::focus_status + on_input(const wups::config::simple_pad_data& input) override; + +private: + + void run(); + +}; + +#endif diff --git a/include/timezone_offset_item.hpp b/include/timezone_offset_item.hpp deleted file mode 100644 index a06d205..0000000 --- a/include/timezone_offset_item.hpp +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef TIMEZONE_OFFSET_ITEM_HPP -#define TIMEZONE_OFFSET_ITEM_HPP - -#include -#include - -#include "wupsxx/item.hpp" -#include "wupsxx/var_watch.hpp" - - -struct timezone_offset_item : wups::config::item { - - wups::config::var_watch variable; - - timezone_offset_item(const std::string& key, - const std::string& label, - std::chrono::minutes& variable); - - static - std::unique_ptr - create(const std::string& key, - const std::string& label, - std::chrono::minutes& variable); - - virtual int get_display(char* buf, std::size_t size) const override; - - virtual int get_selected_display(char* buf, std::size_t size) const override; - - virtual void restore() override; - - virtual void on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) override; - -private: - - void on_changed(); - -}; - - -#endif diff --git a/include/timezone_query_item.hpp b/include/timezone_query_item.hpp deleted file mode 100644 index 1eb761d..0000000 --- a/include/timezone_query_item.hpp +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef TIMEZONE_QUERY_ITEM_HPP -#define TIMEZONE_QUERY_ITEM_HPP - -#include - -#include "wupsxx/item.hpp" -#include "wupsxx/var_watch.hpp" - - -class timezone_query_item : public wups::config::item { - - // We store the geolocation option as an integer, no point in parsing any complex - // string since we need specific implementations for each service. - - wups::config::var_watch variable; - const int default_value; - std::string text; - -public: - - timezone_query_item(const std::string& key, - const std::string& label, - int& variable, - int default_value); - - static - std::unique_ptr create(const std::string& key, - const std::string& label, - int& variable, - int default_value); - - - int get_display(char* buf, std::size_t size) const override; - - int get_selected_display(char* buf, std::size_t size) const override; - - void restore() override; - - void on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) override; - -private: - - void on_changed(); - - void run(); - -}; - -#endif diff --git a/include/utc.hpp b/include/utc.hpp index 948f6cf..b319a87 100644 --- a/include/utc.hpp +++ b/include/utc.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef UTC_HPP #define UTC_HPP diff --git a/include/utils.hpp b/include/utils.hpp index df1cba5..1118326 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef UTILS_HPP #define UTILS_HPP diff --git a/include/verbosity_item.hpp b/include/verbosity_item.hpp index 8032967..6a5c9f3 100644 --- a/include/verbosity_item.hpp +++ b/include/verbosity_item.hpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #ifndef VERBOSITY_ITEM_HPP #define VERBOSITY_ITEM_HPP @@ -10,22 +16,20 @@ struct verbosity_item : wups::config::int_item { - verbosity_item(const std::string& key, - const std::string& label, + verbosity_item(const std::string& label, int& variable, int default_value); static std::unique_ptr - create(const std::string& notify_key, - const std::string& label, + create(const std::string& label, int& variable, int default_value); - virtual int get_display(char* buf, std::size_t size) const override; + virtual void get_display(char* buf, std::size_t size) const override; - virtual int get_selected_display(char* buf, std::size_t size) const override; + virtual void get_focused_display(char* buf, std::size_t size) const override; }; diff --git a/include/wupsxx/bool_item.hpp b/include/wupsxx/bool_item.hpp deleted file mode 100644 index 37297e1..0000000 --- a/include/wupsxx/bool_item.hpp +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_BOOL_ITEM_HPP -#define WUPSXX_BOOL_ITEM_HPP - -#include - -#include "item.hpp" - -#include "var_watch.hpp" - - -namespace wups::config { - - class bool_item : public item { - - var_watch variable; - const bool default_value; - std::string true_str; - std::string false_str; - - public: - - bool_item(const std::optional& key, - const std::string& label, - bool& variable, - bool default_value, - const std::string& true_str = "true", - const std::string& false_str = "false"); - - static - std::unique_ptr - create(const std::optional& key, - const std::string& label, - bool& variable, - bool default_value, - const std::string& true_str = "true", - const std::string& false_str = "false"); - - virtual int get_display(char* buf, std::size_t size) const override; - - virtual int get_selected_display(char* buf, std::size_t size) const override; - - virtual void restore() override; - - virtual void on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) override; - - private: - - void on_changed(); - - }; - -} // namespace wups::config - -#endif diff --git a/include/wupsxx/category.hpp b/include/wupsxx/category.hpp deleted file mode 100644 index b345f38..0000000 --- a/include/wupsxx/category.hpp +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_CATEGORY_HPP -#define WUPSXX_CATEGORY_HPP - -#include -#include - -#include - -#include "item.hpp" - - -namespace wups::config { - - class category final { - - WUPSConfigCategoryHandle handle; - bool own_handle; // if true, will destroy the handle in the destructor - - public: - - // This constructor does not take ownership of the handle. - category(WUPSConfigCategoryHandle handle); - - category(const std::string& label); - category(category&& other) noexcept; - - ~category(); - - void release(); - - void add(std::unique_ptr&& item); - - void add(category&& child); - - }; - -} // namespace wups - -#endif diff --git a/include/wupsxx/config_error.hpp b/include/wupsxx/config_error.hpp deleted file mode 100644 index 1c810be..0000000 --- a/include/wupsxx/config_error.hpp +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_CONFIG_ERROR_HPP -#define WUPSXX_CONFIG_ERROR_HPP - -#include -#include - -#include - - -namespace wups::config { - - struct config_error : std::runtime_error { - - config_error(WUPSConfigAPIStatus status, const std::string& msg); - - }; - -} // namespace wups::config - -#endif diff --git a/include/wupsxx/duration_items.hpp b/include/wupsxx/duration_items.hpp deleted file mode 100644 index 131599e..0000000 --- a/include/wupsxx/duration_items.hpp +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_DURATION_ITEMS_HPP -#define WUPSXX_DURATION_ITEMS_HPP - -#include - -#include "numeric_item.hpp" - - -namespace wups::config { - - using milliseconds_item = numeric_item; - - using seconds_item = numeric_item; - -} // namespace wups::config - -#endif diff --git a/include/wupsxx/int_item.hpp b/include/wupsxx/int_item.hpp deleted file mode 100644 index 148f1a1..0000000 --- a/include/wupsxx/int_item.hpp +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_INT_ITEM_HPP -#define WUPSXX_INT_ITEM_HPP - -#include - -#include "numeric_item.hpp" - - -namespace wups::config { - - using int_item = numeric_item; - -} // namespace wups::config - -#endif diff --git a/include/wupsxx/item.hpp b/include/wupsxx/item.hpp deleted file mode 100644 index 46f0a18..0000000 --- a/include/wupsxx/item.hpp +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_ITEM_HPP -#define WUPSXX_ITEM_HPP - -#include // size_t -#include -#include - -#include - - -namespace wups::config { - - class item { - - WUPSConfigItemHandle handle; - - protected: - - std::optional key; - - public: - - item(const std::optional& key, - const std::string& label); - - // Disallow moving, since the callbacks store the `this` pointer. - item(item&&) = delete; - - virtual ~item(); - - // Gives up ownership of the handle. - void release(); - - - virtual int get_display(char* buf, std::size_t size) const; - - virtual int get_selected_display(char* buf, std::size_t size) const; - - virtual void on_selected(bool is_selected); - - virtual void restore(); - - virtual bool is_movement_allowed() const; - - virtual void on_close(); - - virtual void on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat); - - virtual void on_input(WUPSConfigComplexPadData input); - - - friend class category; - - }; - -} // namespace wups::config - -#endif diff --git a/include/wupsxx/numeric_item.hpp b/include/wupsxx/numeric_item.hpp deleted file mode 100644 index 65b88cc..0000000 --- a/include/wupsxx/numeric_item.hpp +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_NUMERIC_ITEM_HPP -#define WUPSXX_NUMERIC_ITEM_HPP - -#include - -#include "item.hpp" -#include "var_watch.hpp" - - -namespace wups::config { - - - template - class numeric_item : public item { - - protected: - - var_watch variable; - const T default_value; - T min_value; - T max_value; - T fast_increment; - T slow_increment; - - public: - - numeric_item(const std::optional& key, - const std::string& label, - T& variable, T default_value, - T min_value, T max_value, - T fast_increment = T{10}, - T slow_increment = T{1}); - - static - std::unique_ptr - create(const std::optional& key, - const std::string& label, - T& variable, T default_value, - T min_value, T max_value, - T fast_increment = T{10}, - T slow_increment = T{1}); - - - virtual int get_display(char* buf, std::size_t size) const override; - - virtual int get_selected_display(char* buf, std::size_t size) const override; - - virtual void restore() override; - - virtual void on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) override; - - private: - - void on_changed(); - - }; - -} // namespace wups::config - -#endif diff --git a/include/wupsxx/storage.hpp b/include/wupsxx/storage.hpp deleted file mode 100644 index 2a40087..0000000 --- a/include/wupsxx/storage.hpp +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_STORAGE_HPP -#define WUPSXX_STORAGE_HPP - -#include -#include - -#include - -#include "storage_error.hpp" -#include "../time_utils.hpp" - - -namespace wups::storage { - - template - std::expected - load(const std::string& key) - { - T value; - auto status = WUPSStorageAPI::GetEx(nullptr, - key, - value, - WUPSStorageAPI::GetOptions::RESIZE_EXISTING_BUFFER); - if (status != WUPS_STORAGE_ERROR_SUCCESS) - return std::unexpected{storage_error{"error loading key \"" + key + "\"", - status}}; - return value; - } - - - template - std::expected - load(const std::string& key) - { - auto value = load(key); - if (!value) - return std::unexpected{value.error()}; - return T{*value}; - } - - - template - void - store(const std::string& key, const T& value) - { - auto status = WUPSStorageAPI::StoreEx(nullptr, key, value); - if (status != WUPS_STORAGE_ERROR_SUCCESS) - throw storage_error{"error storing key \"" + key + "\"", - status}; - } - - - template - void - store(const std::string& key, const T& value) - { - return store(key, value.count()); - } - - - void save(); - - void reload(); - -} // namespace wups::storage - -#endif diff --git a/include/wupsxx/storage_error.hpp b/include/wupsxx/storage_error.hpp deleted file mode 100644 index ac79865..0000000 --- a/include/wupsxx/storage_error.hpp +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_STORAGE_ERROR_HPP -#define WUPSXX_STORAGE_ERROR_HPP - -#include -#include - -#include - - -namespace wups::storage { - - struct storage_error : std::runtime_error { - - storage_error(const std::string& msg, WUPSStorageError status); - - }; - -} // namespace wups::storage - -#endif diff --git a/include/wupsxx/text_item.hpp b/include/wupsxx/text_item.hpp deleted file mode 100644 index 7dbf703..0000000 --- a/include/wupsxx/text_item.hpp +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_TEXT_ITEM_HPP -#define WUPSXX_TEXT_ITEM_HPP - -#include - -#include "item.hpp" - - -namespace wups::config { - - // Note: this class doesn't do much on its own, so it's all public. - - struct text_item : item { - - std::string text; - std::size_t max_width; - std::size_t first = 0; // first visible character - - text_item(const std::optional& key, - const std::string& label, - const std::string& text = "", - std::size_t max_width = 50); - - static - std::unique_ptr - create(const std::optional& key, - const std::string& label, - const std::string& text = "", - std::size_t max_width = 50); - - - virtual int get_display(char* buf, std::size_t size) const override; - - virtual int get_selected_display(char* buf, std::size_t size) const override; - - virtual void on_selected(bool is_selected) override; - - virtual void on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) override; - - }; - -} // namespace wups::config - -#endif diff --git a/include/wupsxx/var_watch.hpp b/include/wupsxx/var_watch.hpp deleted file mode 100644 index f482b32..0000000 --- a/include/wupsxx/var_watch.hpp +++ /dev/null @@ -1,275 +0,0 @@ -#ifndef WUPSXX_VAR_WATCH_HPP -#define WUPSXX_VAR_WATCH_HPP - -#include // forward() - - -// A helper class that tracks changes to a variable. - -namespace wups::config { - - template - class var_watch { - - T& ref; - bool modified = false; - - public: - - var_watch(T& var) : - ref(var) - {} - - - bool - changed() - const noexcept - { - return modified; - } - - - void - reset() - noexcept - { - modified = false; - } - - - template - var_watch& - operator =(U&& val) - noexcept - { - T old = ref; - ref = std::forward(val); - if (old != ref) - modified = true; - return *this; - } - - - // operator T() - // const noexcept - // { - // return ref; - // } - - - T - value() - const noexcept - { - return ref; - } - - - // pointer-like read-only accessors - - const T& - operator *() - const noexcept - { - return ref; - } - - - const T* - operator ->() - const noexcept - { - return &ref; - } - - - // modifier below this point - - - // increment/decrement - var_watch& - operator ++() - noexcept - { - T old = ref; - ++ref; - if (old != ref) - modified = true; - return *this; - } - - - var_watch& - operator --() - noexcept - { - T old = ref; - --ref; - if (old != ref) - modified = true; - return *this; - } - - - T - operator ++(int) - noexcept - { - T old = ref; - T result = ref++; - if (old != ref) - modified = true; - return result; - } - - - T - operator --(int) - noexcept - { - T old = ref; - T result = ref--; - if (old != ref) - modified = true; - return result; - } - - - // assignment modifiers - - template - var_watch& - operator +=(U&& arg) - noexcept - { - T old = ref; - ref += std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - template - var_watch& - operator -=(U&& arg) - noexcept - { - T old = ref; - ref -= std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - template - var_watch& - operator *=(U&& arg) - noexcept - { - T old = ref; - ref *= std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - template - var_watch& - operator /=(U&& arg) - noexcept - { - T old = ref; - ref /= std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - template - var_watch& - operator %=(U&& arg) - noexcept - { - T old = ref; - ref %= std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - template - var_watch& - operator <<=(U&& arg) - noexcept - { - T old = ref; - ref <<= std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - template - var_watch& - operator >>=(U&& arg) - noexcept - { - T old = ref; - ref >>= std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - template - var_watch& - operator ^=(U&& arg) - noexcept - { - T old = ref; - ref ^= std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - template - var_watch& - operator &=(U&& arg) - noexcept - { - T old = ref; - ref &= std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - template - var_watch& - operator |=(U&& arg) - noexcept - { - T old = ref; - ref |= std::forward(arg); - if (old != ref) - modified = true; - return *this; - } - - - }; - -} - - -#endif diff --git a/source/cfg.cpp b/source/cfg.cpp index 1e5fea7..74cdad0 100644 --- a/source/cfg.cpp +++ b/source/cfg.cpp @@ -1,12 +1,31 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * Copyright (C) 2024 Nightkingale + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include #include "cfg.hpp" -#include "logging.hpp" -#include "nintendo_glyphs.h" +#include "core.hpp" +#include "notify.hpp" +#include "preview_screen.hpp" +#include "synchronize_item.hpp" #include "time_utils.hpp" +#include "time_zone_offset_item.hpp" +#include "time_zone_query_item.hpp" #include "utils.hpp" -#include "wupsxx/storage.hpp" +#include "verbosity_item.hpp" using std::chrono::hours; @@ -15,73 +34,219 @@ using std::chrono::minutes; using std::chrono::seconds; using namespace std::literals; +using namespace wups::config; +namespace logger = wups::logger; namespace cfg { - namespace key { - const char* auto_tz = "auto_tz"; - const char* msg_duration = "msg_duration"; - const char* notify = "notify"; - const char* server = "server"; - const char* sync = "sync"; - const char* threads = "threads"; - const char* timeout = "timeout"; - const char* tolerance = "tolerance"; - const char* tz_service = "tz_service"; - const char* utc_offset = "utc_offset"; + namespace keys { + const char* auto_tz = "auto_tz"; + const char* msg_duration = "msg_duration"; + const char* notify = "notify"; + const char* server = "server"; + const char* sync_on_boot = "sync_on_boot"; + const char* sync_on_changes = "sync_on_changes"; + const char* threads = "threads"; + const char* timeout = "timeout"; + const char* tolerance = "tolerance"; + const char* tz_service = "tz_service"; + const char* utc_offset = "utc_offset"; } - namespace label { - const char* auto_tz = "Auto Update Time Zone"; - const char* msg_duration = "Notification Duration"; - const char* notify = "Show Notifications"; - const char* server = "NTP Servers"; - const char* sync = "Syncing Enabled"; - const char* threads = "Background Threads"; - const char* timeout = "Timeout"; - const char* tolerance = "Tolerance"; - const char* tz_service = "Detect Time Zone (press " NIN_GLYPH_BTN_A ")"; - const char* utc_offset = "Time Offset (UTC)"; + namespace labels { + const char* auto_tz = " └ Auto Update Time Zone"; + const char* msg_duration = " └ Notification Duration"; + const char* notify = "Show Notifications"; + const char* server = "NTP Servers"; + const char* sync_on_boot = "Synchronize On Boot"; + const char* sync_on_changes = "Synchronize After Changing Configuration"; + const char* threads = "Background Threads"; + const char* timeout = "Timeout"; + const char* tolerance = "Tolerance"; + const char* tz_service = " └ Detect Time Zone"; + const char* utc_offset = "Time Offset (UTC)"; } namespace defaults { - const bool auto_tz = false; - const seconds msg_duration = 5s; - const int notify = 0; - const std::string server = "pool.ntp.org"; - const bool sync = false; - const int threads = 4; - const seconds timeout = 5s; - const milliseconds tolerance = 500ms; - const int tz_service = 0; + const bool auto_tz = false; + const seconds msg_duration = 5s; + const int notify = 0; + const std::string server = "pool.ntp.org"; + const bool sync_on_boot = false; + const bool sync_on_changes = true; + const int threads = 4; + const seconds timeout = 5s; + const milliseconds tolerance = 500ms; + const int tz_service = 0; + const minutes utc_offset = 0min; + } + + + bool auto_tz = defaults::auto_tz; + seconds msg_duration = defaults::msg_duration; + int notify = defaults::notify; + std::string server = defaults::server; + bool sync_on_boot = defaults::sync_on_boot; + bool sync_on_changes = defaults::sync_on_changes; + int threads = defaults::threads; + seconds timeout = defaults::timeout; + milliseconds tolerance = defaults::tolerance; + int tz_service = defaults::tz_service; + minutes utc_offset = 0min; + + + // variables that, if changed, may affect the sync + namespace previous { + bool auto_tz; + milliseconds tolerance; + int tz_service; + minutes utc_offset; + } + + + void + save_important_vars() + { + previous::auto_tz = auto_tz; + previous::tolerance = tolerance; + previous::tz_service = tz_service; + previous::utc_offset = utc_offset; + } + + + bool + important_vars_changed() + { + return previous::auto_tz != auto_tz + || previous::tolerance != tolerance + || previous::tz_service != tz_service + || previous::utc_offset != utc_offset; + } + + + category + make_config_screen() + { + category cat{"Configuration"}; + + cat.add(bool_item::create(cfg::labels::sync_on_boot, + cfg::sync_on_boot, + cfg::defaults::sync_on_boot, + "on", "off")); + + cat.add(bool_item::create(cfg::labels::sync_on_changes, + cfg::sync_on_changes, + cfg::defaults::sync_on_changes, + "on", "off")); + + cat.add(verbosity_item::create(cfg::labels::notify, + cfg::notify, + cfg::defaults::notify)); + + cat.add(seconds_item::create(cfg::labels::msg_duration, + cfg::msg_duration, + cfg::defaults::msg_duration, + 1s, 30s, 5s)); + + cat.add(time_zone_offset_item::create(cfg::labels::utc_offset, + cfg::utc_offset, + cfg::defaults::utc_offset)); + + cat.add(time_zone_query_item::create(cfg::labels::tz_service, + cfg::tz_service, + cfg::defaults::tz_service)); + + cat.add(bool_item::create(cfg::labels::auto_tz, + cfg::auto_tz, + cfg::defaults::auto_tz, + "on", "off")); + + cat.add(seconds_item::create(cfg::labels::timeout, + cfg::timeout, + cfg::defaults::timeout, + 1s, 10s, 5s)); + + cat.add(milliseconds_item::create(cfg::labels::tolerance, + cfg::tolerance, + cfg::defaults::tolerance, + 0ms, 5000ms, 100ms)); + + cat.add(int_item::create(cfg::labels::threads, + cfg::threads, + cfg::defaults::threads, + 0, 8, 2)); + + // show current NTP server address, no way to change it. + cat.add(text_item::create(cfg::labels::server, + cfg::server)); + + return cat; } - bool auto_tz = defaults::auto_tz; - seconds msg_duration = defaults::msg_duration; - int notify = defaults::notify; - std::string server = defaults::server; - bool sync = defaults::sync; - int threads = defaults::threads; - seconds timeout = defaults::timeout; - milliseconds tolerance = defaults::tolerance; - int tz_service = defaults::tz_service; - minutes utc_offset = 0min; + void + menu_open(category& root) + { + logger::initialize(PLUGIN_NAME); + + // logger::printf("reloading configs\n"); + cfg::reload(); + + // logger::printf("building config items\n"); + root.add(make_config_screen()); + root.add(make_preview_screen()); + root.add(synchronize_item::create()); + + save_important_vars(); + } - template void - load_or_init(const std::string& key, - T& variable) + menu_close() { - auto val = wups::storage::load(key); - if (!val) - wups::storage::store(key, variable); - else - variable = *val; + if (cfg::sync_on_changes && important_vars_changed()) { + std::jthread t{ + [](std::stop_token token) + { + logger::guard lguard{PLUGIN_NAME}; + notify::guard nguard; + try { + core::run(token, false); + } + catch (std::exception& e) { + notify::error(notify::level::normal, e.what()); + } + } + }; + t.detach(); + } + + // logger::printf("saving config\n"); + cfg::save(); + + logger::finalize(); + } + + + void migrate_old_config(); + + + void + init() + { + try { + wups::config::init(PLUGIN_NAME, + menu_open, + menu_close); + cfg::load(); + cfg::migrate_old_config(); + } + catch (std::exception& e) { + logger::printf("Init error: %s\n", e.what()); + } } @@ -89,20 +254,23 @@ namespace cfg { load() { try { - load_or_init(key::auto_tz, auto_tz); - 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::threads, threads); - load_or_init(key::timeout, timeout); - load_or_init(key::tolerance, tolerance); - load_or_init(key::tz_service, tz_service); - load_or_init(key::utc_offset, utc_offset); - // logging::printf("Loaded settings."); +#define LOAD(x) wups::storage::load_or_init(keys::x, x, defaults::x) + LOAD(auto_tz); + LOAD(msg_duration); + LOAD(notify); + LOAD(server); + LOAD(sync_on_boot); + LOAD(sync_on_changes); + LOAD(threads); + LOAD(timeout); + LOAD(tolerance); + LOAD(tz_service); + LOAD(utc_offset); +#undef LOAD + // logger::printf("Loaded settings.\n"); } catch (std::exception& e) { - logging::printf("Error loading config: %s", e.what()); + logger::printf("Error loading config: %s\n", e.what()); } } @@ -115,7 +283,7 @@ namespace cfg { load(); } catch (std::exception& e) { - logging::printf("Error reloading config: %s", e.what()); + logger::printf("Error reloading config: %s\n", e.what()); } } @@ -124,11 +292,24 @@ namespace cfg { save() { try { +#define STORE(x) wups::storage::store(keys::x, x) + STORE(auto_tz); + STORE(msg_duration); + STORE(notify); + STORE(server); + STORE(sync_on_boot); + STORE(sync_on_changes); + STORE(threads); + STORE(timeout); + STORE(tolerance); + STORE(tz_service); + STORE(utc_offset); +#undef STORE wups::storage::save(); - // logging::printf("Saved settings"); + // logger::printf("Saved settings\n"); } catch (std::exception& e) { - logging::printf("Error saving config: %s", e.what()); + logger::printf("Error saving config: %s\n", e.what()); } } @@ -136,6 +317,8 @@ namespace cfg { void migrate_old_config() { + using std::to_string; + // check for leftovers from old versions auto hrs = wups::storage::load("hours"); auto min = wups::storage::load("minutes"); @@ -147,10 +330,19 @@ namespace cfg { WUPSStorageAPI::DeleteItem("hours"); WUPSStorageAPI::DeleteItem("minutes"); save(); - logging::printf("Migrated old config: %s + %s -> %s.", - time_utils::to_string(h).c_str(), - time_utils::to_string(m).c_str(), - time_utils::tz_offset_to_string(utc_offset).c_str()); + logger::printf("Migrated old config: hours=%s, minutes=%s -> utc_offset=%s.\n", + to_string(h.count()).c_str(), + to_string(m.count()).c_str(), + time_utils::tz_offset_to_string(utc_offset).c_str()); + } + auto sync = wups::storage::load("sync"); + if (sync) { + WUPSStorageAPI::DeleteItem("sync"); + sync_on_boot = *sync; + save(); + logger::printf("Migrated old config: sync=%s -> sync_on_boot=%s\n", + (*sync ? "true" : "false"), + (sync_on_boot ? "true" : "false")); } } @@ -158,18 +350,18 @@ namespace cfg { void set_and_store_utc_offset(minutes offset) { + logger::guard guard(PLUGIN_NAME); /* - * Normally, `utc_offset` is saved on the config storage by the - * `timezone_offset_item`. This function is supposed to be called by other parts - * of the code, so it needs to manually store and save the new `utc_offset`. + * Normally, `utc_offset` is saved when closing the config menu. + * If auto_tz is enabled, it will be updated and saved outside the config menu. */ utc_offset = offset; try { - wups::storage::store(key::utc_offset, utc_offset); + wups::storage::store(keys::utc_offset, utc_offset); wups::storage::save(); } catch (std::exception& e) { - logging::printf("Error storing utc_offset: %s", e.what()); + logger::printf("Error storing utc_offset: %s\n", e.what()); } } diff --git a/source/clock_item.cpp b/source/clock_item.cpp index 8282cd5..e756e8f 100644 --- a/source/clock_item.cpp +++ b/source/clock_item.cpp @@ -1,23 +1,34 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * Copyright (C) 2024 Nightkingale + * + * SPDX-License-Identifier: MIT + */ #include // max(), min() #include #include +#include +#include + #include "clock_item.hpp" #include "cfg.hpp" #include "core.hpp" -#include "logging.hpp" #include "net/addrinfo.hpp" -#include "nintendo_glyphs.h" #include "time_utils.hpp" #include "utils.hpp" + using namespace std::literals; +using namespace wups::config; using time_utils::dbl_seconds; -using wups::config::text_item; + +namespace logger = wups::logger; namespace { @@ -52,11 +63,11 @@ namespace { return result; } -} +} // namespace clock_item::clock_item() : - text_item{{}, "Clock (press " NIN_GLYPH_BTN_A ")", "", 48} + button_item{"Clock"} {} @@ -68,32 +79,31 @@ clock_item::create() void -clock_item::on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) +clock_item::on_started() { - text_item::on_input(input, repeat); - - if (input.buttons_d & WUPS_CONFIG_BUTTON_A) { - try { - run(); - } - catch (std::exception& e) { - text = "Error: "s + e.what(); - } + try { + status_msg = ""; + run(); + update_status_msg(); } - - refresh_now_str(); + catch (std::exception& e) { + status_msg = e.what(); + } + current_state = state::finished; } void -clock_item::refresh_now_str() +clock_item::update_status_msg() { now_str = core::local_clock_to_string(); - text = now_str + stats_str; + status_msg = now_str + diff_str; } +/* + * Note: this code is very similar to core::run(), but runs in a single thread. + */ void clock_item::run() { @@ -127,20 +137,20 @@ clock_item::run() for (const auto& info : infos) { try { - auto [correction, latency] = core::ntp_query(info.addr); + auto [correction, latency] = core::ntp_query({}, info.addr); server_corrections.push_back(correction); server_latencies.push_back(latency); total += correction; ++num_values; - logging::printf("%s (%s): correction = %s, latency = %s", - server.c_str(), - to_string(info.addr).c_str(), - seconds_to_human(correction, true).c_str(), - seconds_to_human(latency).c_str()); + logger::printf("%s (%s): correction = %s, latency = %s\n", + server.c_str(), + to_string(info.addr).c_str(), + seconds_to_human(correction, true).c_str(), + seconds_to_human(latency).c_str()); } catch (std::exception& e) { ++errors; - logging::printf("Error: %s", e.what()); + logger::printf("Error: %s\n", e.what()); } } @@ -168,9 +178,7 @@ clock_item::run() if (num_values) { dbl_seconds avg = total / num_values; - stats_str = ", needs "s + seconds_to_human(avg, true); + diff_str = ", needs "s + seconds_to_human(avg, true); } else - stats_str = ""; - - refresh_now_str(); + diff_str = ""; } diff --git a/source/config_screen.cpp b/source/config_screen.cpp deleted file mode 100644 index 66fc78c..0000000 --- a/source/config_screen.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include -#include // make_unique() - -#include "config_screen.hpp" - -#include "cfg.hpp" -#include "timezone_offset_item.hpp" -#include "timezone_query_item.hpp" -#include "verbosity_item.hpp" -#include "wupsxx/bool_item.hpp" -#include "wupsxx/duration_items.hpp" -#include "wupsxx/int_item.hpp" -#include "wupsxx/text_item.hpp" -#include "wupsxx/numeric_item.hpp" - - -using wups::config::bool_item; -using wups::config::int_item; -using wups::config::milliseconds_item; -using wups::config::numeric_item; -using wups::config::seconds_item; -using wups::config::text_item; - -using namespace std::literals; - - -wups::config::category -make_config_screen() -{ - wups::config::category cat{"Configuration"}; - - cat.add(bool_item::create(cfg::key::sync, - cfg::label::sync, - cfg::sync, - cfg::defaults::sync, - "on", "off")); - - cat.add(verbosity_item::create(cfg::key::notify, - cfg::label::notify, - cfg::notify, - cfg::defaults::notify)); - - cat.add(timezone_offset_item::create(cfg::key::utc_offset, - cfg::label::utc_offset, - cfg::utc_offset)); - - cat.add(timezone_query_item::create(cfg::key::tz_service, - cfg::label::tz_service, - cfg::tz_service, - cfg::defaults::tz_service)); - - cat.add(bool_item::create(cfg::key::auto_tz, - cfg::label::auto_tz, - cfg::auto_tz, - cfg::defaults::auto_tz, - "on", "off")); - - cat.add(seconds_item::create(cfg::key::msg_duration, - cfg::label::msg_duration, - cfg::msg_duration, - cfg::defaults::msg_duration, - 1s, 30s, 5s)); - - cat.add(seconds_item::create(cfg::key::timeout, - cfg::label::timeout, - cfg::timeout, - cfg::defaults::timeout, - 1s, 10s, 5s)); - - cat.add(milliseconds_item::create(cfg::key::tolerance, - cfg::label::tolerance, - cfg::tolerance, - cfg::defaults::tolerance, - 0ms, 5000ms, 100ms)); - - cat.add(int_item::create(cfg::key::threads, - cfg::label::threads, - cfg::threads, - cfg::defaults::threads, - 0, 8, 2)); - - // show current NTP server address, no way to change it. - cat.add(text_item::create(cfg::key::server, - cfg::label::server, - cfg::server)); - - return cat; -} diff --git a/source/core.cpp b/source/core.cpp index 31e2665..57f8ada 100644 --- a/source/core.cpp +++ b/source/core.cpp @@ -1,4 +1,11 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * Copyright (C) 2024 Nightkingale + * + * SPDX-License-Identifier: MIT + */ #include #include @@ -15,10 +22,11 @@ #include // CCRSysSetSystemTime() #include // __OSSetAbsoluteSystemTime() +#include + #include "core.hpp" #include "cfg.hpp" -#include "logging.hpp" #include "net/addrinfo.hpp" #include "net/socket.hpp" #include "notify.hpp" @@ -30,6 +38,9 @@ using namespace std::literals; +using std::runtime_error; + +namespace logger = wups::logger; using time_utils::dbl_seconds; @@ -85,9 +96,24 @@ namespace { namespace core { + + struct canceled_error : runtime_error { + canceled_error() : runtime_error{"Operation canceled."} {} + }; + + + void + check_stop(std::stop_token token) + { + if (token.stop_requested()) + throw canceled_error{}; + } + + // Note: hardcoded for IPv4, the Wii U doesn't have IPv6. std::pair - ntp_query(net::address address) + ntp_query(std::stop_token token, + net::address address) { using std::to_string; @@ -101,7 +127,10 @@ namespace core { unsigned send_attempts = 0; const unsigned max_send_attempts = 4; + try_again_send: + // cancellation point: before sending + check_stop(token); auto t1 = to_ntp(utc::now()); packet.transmit_time = t1; @@ -111,16 +140,21 @@ namespace core { if (e.code() != std::errc::not_enough_memory) throw e; if (++send_attempts < max_send_attempts) { + // cancellation point: before sleeping + check_stop(token); std::this_thread::sleep_for(100ms); goto try_again_send; } else - throw std::runtime_error{"No resources for send(), too many retries!"}; + throw runtime_error{"No resources for send(), too many retries!"}; } unsigned poll_attempts = 0; const unsigned max_poll_attempts = 4; + try_again_poll: + // cancellation point: before polling + check_stop(token); auto readable_status = sock.try_is_readable(cfg::timeout); if (!readable_status) { // Wii U OS can only handle 16 concurrent select()/poll() calls, @@ -129,40 +163,42 @@ namespace core { if (e.code() != std::errc::not_enough_memory) throw e; if (++poll_attempts < max_poll_attempts) { + // cancellation point: before sleeping + check_stop(token); std::this_thread::sleep_for(10ms); goto try_again_poll; } else - throw std::runtime_error{"No resources for poll(), too many retries!"}; + throw runtime_error{"No resources for poll(), too many retries!"}; } if (!*readable_status) - throw std::runtime_error{"Timeout reached!"}; + throw runtime_error{"Timeout reached!"}; // Measure the arrival time as soon as possible. auto t4 = to_ntp(utc::now()); if (sock.recv(&packet, sizeof packet) < 48) - throw std::runtime_error{"Invalid NTP response!"}; + throw runtime_error{"Invalid NTP response!"}; sock.close(); // close it early auto v = packet.version(); if (v < 3 || v > 4) - throw std::runtime_error{"Unsupported NTP version: "s + to_string(v)}; + throw 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)}; + throw 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."}; + throw 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}; + throw 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; @@ -171,7 +207,7 @@ namespace core { // Zero is not a valid timestamp. if (!t2 || !t3) - throw std::runtime_error{"NTP response has invalid timestamps."}; + throw runtime_error{"NTP response has invalid timestamps."}; /* * We do all calculations in double precision to never worry about overflows. Since @@ -236,27 +272,25 @@ namespace core { nn::pdm::NotifySetTimeEndEvent(); - // logging::printf("CCRSysSetSystemTime() took %f ms", - // 1000.0 * (ccr_finish - ccr_start) / OSTimerClockSpeed); - // logging::printf("__OSSetAbsoluteSystemTime() took %f ms", - // 1000.0 * (abs_finish - abs_start) / OSTimerClockSpeed); + // logger::printf("CCRSysSetSystemTime() took %f ms\n", + // 1000.0 * (ccr_finish - ccr_start) / OSTimerClockSpeed); + // logger::printf("__OSSetAbsoluteSystemTime() took %f ms\n", + // 1000.0 * (abs_finish - abs_start) / OSTimerClockSpeed); // OSTime after = OSGetSystemTime(); - // logging::printf("Total time: %f ms", - // 1000.0 * (after - before) / OSTimerClockSpeed); + // logger::printf("Total time: %f ms\n", + // 1000.0 * (after - before) / OSTimerClockSpeed); return success1 && success2; } void - run() + run(std::stop_token token, + bool silent) { using time_utils::seconds_to_human; - if (!cfg::sync) - return; - // ensure notification is initialized if needed notify::guard notify_guard{cfg::notify > 0}; @@ -264,30 +298,32 @@ namespace core { utils::exec_guard exec_guard{executing}; if (!exec_guard.guarded) { // Another thread is already executing this function. - notify::info(notify::level::verbose, - "Skipping NTP task: operation already in progress."); - return; + throw runtime_error{"Skipping NTP task: operation already in progress."}; } - if (cfg::auto_tz) { try { auto [name, offset] = utils::fetch_timezone(cfg::tz_service); if (offset != cfg::utc_offset) { cfg::set_and_store_utc_offset(offset); - notify::info(notify::level::verbose, - "Updated time zone to " + name + - "(" + time_utils::tz_offset_to_string(offset) + ")"); + if (!silent) + notify::info(notify::level::verbose, + "Updated time zone to " + name + + "(" + time_utils::tz_offset_to_string(offset) + ")"); } } catch (std::exception& e) { - notify::error(notify::level::verbose, - "Failed to update time zone: "s + e.what()); + if (!silent) + notify::error(notify::level::verbose, + "Failed to update time zone: "s + e.what()); + // Note: not a fatal error, we just keep using the previous time zone. } } - thread_pool pool(cfg::threads); + // cancellation point: after the time zone update + check_stop(token); + thread_pool pool{static_cast(cfg::threads)}; std::vector servers = utils::split(cfg::server, " \t,;"); @@ -304,6 +340,9 @@ namespace core { for (auto [fut, server] : std::views::zip(futures, servers)) fut = pool.submit(net::addrinfo::lookup, server, "123"s, opts); + // cancellation point: after submitting the DNS queries + check_stop(token); + // Collect all future results. for (auto& fut : futures) try { @@ -311,44 +350,55 @@ namespace core { addresses.insert(info.addr); } catch (std::exception& e) { - notify::error(notify::level::verbose, e.what()); + if (!silent) + notify::error(notify::level::verbose, e.what()); } } if (addresses.empty()) { // Probably a mistake in config, or network failure. - notify::error(notify::level::normal, "No NTP address could be used."); - return; + throw runtime_error{"No NTP address could be used."}; } + // cancellation point: before the NTP queries are submitted + check_stop(token); + // Launch NTP queries asynchronously. std::vector>> futures(addresses.size()); for (auto [fut, address] : std::views::zip(futures, addresses)) - fut = pool.submit(ntp_query, address); + fut = pool.submit(ntp_query, token, address); + + // cancellation point: after NTP queries are submited + check_stop(token); // Collect all future results. std::vector corrections; for (auto [address, fut] : std::views::zip(addresses, futures)) try { + // cancellation point: before blocking waiting for a NTP result + check_stop(token); auto [correction, latency] = fut.get(); corrections.push_back(correction); - notify::info(notify::level::verbose, - to_string(address) - + ": correction = "s + seconds_to_human(correction, true) - + ", latency = "s + seconds_to_human(latency)); + if (!silent) + notify::info(notify::level::verbose, + to_string(address) + + ": correction = "s + seconds_to_human(correction, true) + + ", latency = "s + seconds_to_human(latency)); + } + catch (canceled_error&) { + throw; } catch (std::exception& e) { - notify::error(notify::level::verbose, - to_string(address) + ": "s + e.what()); + if (!silent) + notify::error(notify::level::verbose, + to_string(address) + ": "s + e.what()); } - if (corrections.empty()) { - notify::error(notify::level::normal, - "No NTP server could be used!"); - return; - } + if (corrections.empty()) + throw runtime_error{"No NTP server could be used!"}; + dbl_seconds total = std::accumulate(corrections.begin(), corrections.end(), @@ -356,22 +406,22 @@ namespace core { dbl_seconds avg = total / static_cast(corrections.size()); if (abs(avg) <= cfg::tolerance) { - notify::success(notify::level::verbose, - "Tolerating clock drift (correction is only " - + seconds_to_human(avg, true) + ")."s); + if (!silent) + notify::success(notify::level::verbose, + "Tolerating clock drift (correction is only " + + seconds_to_human(avg, true) + ")."s); return; } - if (cfg::sync) { - if (!apply_clock_correction(avg)) { - // This error woudl be so bad, the user should always know about it. - notify::error(notify::level::quiet, - "Failed to set system clock!"); - return; - } + // cancellation point: before modifying the clock + check_stop(token); + + if (!apply_clock_correction(avg)) + throw runtime_error{"Failed to set system clock!"}; + + if (!silent) notify::success(notify::level::normal, "Clock corrected by " + seconds_to_human(avg, true)); - } } diff --git a/source/curl.cpp b/source/curl.cpp index ef65e66..0b7b869 100644 --- a/source/curl.cpp +++ b/source/curl.cpp @@ -1,8 +1,17 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ + +#include #include "curl.hpp" -#include "logging.hpp" + +namespace logger = wups::logger; namespace curl { @@ -83,7 +92,7 @@ namespace curl { return h->on_recv(buffer, nmemb); } catch (std::exception& e) { - logging::printf("curl::handle::write_callback(): %s", e.what()); + logger::printf("curl::handle::write_callback(): %s\n", e.what()); return CURL_WRITEFUNC_ERROR; } } diff --git a/source/http_client.cpp b/source/http_client.cpp index a48773c..59e0e93 100644 --- a/source/http_client.cpp +++ b/source/http_client.cpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include "http_client.hpp" diff --git a/source/logging.cpp b/source/logging.cpp deleted file mode 100644 index c0b06bf..0000000 --- a/source/logging.cpp +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include -#include -#include // vsnprintf() -#include - -#include -#include -#include -#include - -#include "logging.hpp" - - -using namespace std::literals; - - -namespace logging { - - std::atomic_uint refs = 0; - - - bool init_cafe = false; - bool init_module = false; - bool init_udp = false; - - - void - initialize() - { - // don't initialize again if refs was already positive - if (refs++) - return; - - init_cafe = WHBLogCafeInit(); - init_module = WHBLogModuleInit(); - init_udp = WHBLogUdpInit(); - } - - - void - finalize() - { - // don't finalize if refs is still positive - if (--refs) - return; - - if (init_cafe) - WHBLogCafeDeinit(); - init_cafe = false; - - if (init_module) - WHBLogModuleDeinit(); - init_module = false; - - if (init_udp) - WHBLogUdpDeinit(); - init_udp = false; - } - - - void - printf(const char* fmt, ...) - { - std::string buf(256, '\0'); - std::string xfmt = std::string("[" PLUGIN_NAME "] ") + fmt; - - std::va_list args; - - va_start(args, fmt); - int sz = std::vsnprintf(buf.data(), buf.size(), xfmt.c_str(), args); - va_end(args); - - if (sz > 0 && static_cast(sz) >= buf.size()) { - buf.resize(sz + 1); - - va_start(args, fmt); - std::vsnprintf(buf.data(), buf.size(), xfmt.c_str(), args); - va_end(args); - } - - if (sz > 0) - WHBLogPrint(buf.c_str()); - } - -} // namespace logging diff --git a/source/main.cpp b/source/main.cpp index 7e2786f..36c0d57 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,88 +1,61 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * Copyright (C) 2024 Nightkingale + * + * SPDX-License-Identifier: MIT + */ #include #include #include +#include + #include "cfg.hpp" -#include "config_screen.hpp" #include "core.hpp" -#include "logging.hpp" #include "notify.hpp" -#include "preview_screen.hpp" -#include "wupsxx/category.hpp" // Important plugin information. WUPS_PLUGIN_NAME(PLUGIN_NAME); -WUPS_PLUGIN_DESCRIPTION(PLUGIN_DESCRIPTION); WUPS_PLUGIN_VERSION(PLUGIN_VERSION); -WUPS_PLUGIN_AUTHOR(PLUGIN_AUTHOR); -WUPS_PLUGIN_LICENSE(PLUGIN_LICENSE); +WUPS_PLUGIN_DESCRIPTION("A plugin that synchronizes the system clock to the Internet."); +WUPS_PLUGIN_AUTHOR("Nightkingale, Daniel K. O."); +WUPS_PLUGIN_LICENSE("MIT"); WUPS_USE_WUT_DEVOPTAB(); WUPS_USE_STORAGE(PLUGIN_NAME); -static WUPSConfigAPICallbackStatus open_config(WUPSConfigCategoryHandle root_handle); -static void close_config(); - - INITIALIZE_PLUGIN() { - logging::initialize(); - - auto status = WUPSConfigAPI_Init({ .name = PLUGIN_NAME }, - open_config, - close_config); - if (status != WUPSCONFIG_API_RESULT_SUCCESS) { - logging::printf("Init error: %s", WUPSConfigAPI_GetStatusStr(status)); - return; + wups::logger::guard guard{PLUGIN_NAME}; + + cfg::init(); + + if (cfg::sync_on_boot) { + std::jthread t{ + [](std::stop_token token) + { + wups::logger::guard lguard{PLUGIN_NAME}; + notify::guard nguard; + try { + core::run(token, false); + } + catch (std::exception& e) { + notify::error(notify::level::normal, e.what()); + } + } + }; + t.detach(); } - - cfg::load(); - cfg::migrate_old_config(); - - if (cfg::sync) - core::run(); // Update clock when plugin is loaded. } DEINITIALIZE_PLUGIN() { - logging::finalize(); -} - - -static -WUPSConfigAPICallbackStatus -open_config(WUPSConfigCategoryHandle root_handle) -{ - try { - cfg::reload(); - - wups::config::category root{root_handle}; - - root.add(make_config_screen()); - root.add(make_preview_screen()); - - return WUPSCONFIG_API_CALLBACK_RESULT_SUCCESS; - } - catch (std::exception& e) { - logging::printf("Error opening config: %s", e.what()); - return WUPSCONFIG_API_CALLBACK_RESULT_ERROR; - } -} - - -static -void -close_config() -{ - cfg::save(); - - // Update time when settings are closed. - std::jthread update_time_thread{core::run}; - update_time_thread.detach(); + // TODO: should clean up any worker thread } diff --git a/source/net/address.cpp b/source/net/address.cpp index 6fbe06d..168d5e1 100644 --- a/source/net/address.cpp +++ b/source/net/address.cpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include // memset() #include diff --git a/source/net/addrinfo.cpp b/source/net/addrinfo.cpp index 11ad532..42ec2f8 100644 --- a/source/net/addrinfo.cpp +++ b/source/net/addrinfo.cpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include // memcpy(), memset() #include diff --git a/source/net/error.cpp b/source/net/error.cpp index 6ea2cb9..4bcbd65 100644 --- a/source/net/error.cpp +++ b/source/net/error.cpp @@ -1,9 +1,13 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include "net/error.hpp" -#include "utils.hpp" - namespace net { diff --git a/source/net/socket.cpp b/source/net/socket.cpp index 502a1dc..17f37b6 100644 --- a/source/net/socket.cpp +++ b/source/net/socket.cpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include #include // byte @@ -8,11 +14,10 @@ #include // ntohl() #include // socket() #include // close() +#include #include "net/socket.hpp" -#include "logging.hpp" - // Note: WUT doesn't have SOL_IP, but IPPROTO_IP seems to work. #ifndef SOL_IP @@ -109,7 +114,7 @@ namespace net { other.fd = -1; } catch (std::exception& e) { - logging::printf("socket::operator=() failed: %s", e.what()); + WHBLogPrintf("socket::operator=() failed: %s", e.what()); } } return *this; @@ -122,7 +127,7 @@ namespace net { close(); } catch (std::exception& e) { - logging::printf("socket::~socket() failed: %s", e.what()); + WHBLogPrintf("socket::~socket() failed: %s", e.what()); } } diff --git a/source/notify.cpp b/source/notify.cpp index 626f69a..21bbe0e 100644 --- a/source/notify.cpp +++ b/source/notify.cpp @@ -1,13 +1,23 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include #include +#include + #include "notify.hpp" #include "cfg.hpp" -#include "logging.hpp" + + +namespace logger = wups::logger; namespace notify { @@ -43,7 +53,7 @@ namespace notify { void error(level lvl, const std::string& arg) { - logging::printf("ERROR: %s", arg.c_str()); + logger::printf("ERROR: %s\n", arg.c_str()); if (static_cast(lvl) > cfg::notify) return; @@ -63,7 +73,7 @@ namespace notify { void info(level lvl, const std::string& arg) { - logging::printf("INFO: %s", arg.c_str()); + logger::printf("INFO: %s\n", arg.c_str()); if (static_cast(lvl) > cfg::notify) return; @@ -82,7 +92,7 @@ namespace notify { void success(level lvl, const std::string& arg) { - logging::printf("SUCCESS: %s", arg.c_str()); + logger::printf("SUCCESS: %s\n", arg.c_str()); if (static_cast(lvl) > cfg::notify) return; diff --git a/source/ntp.cpp b/source/ntp.cpp index 3991c81..83371a5 100644 --- a/source/ntp.cpp +++ b/source/ntp.cpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include // endian, byteswap() #include // ldexp() diff --git a/source/preview_screen.cpp b/source/preview_screen.cpp index a0ce990..673ee7f 100644 --- a/source/preview_screen.cpp +++ b/source/preview_screen.cpp @@ -1,15 +1,24 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * Copyright (C) 2024 Nightkingale + * + * SPDX-License-Identifier: MIT + */ #include // move() +#include + #include "preview_screen.hpp" #include "cfg.hpp" #include "clock_item.hpp" #include "utils.hpp" -#include "wupsxx/text_item.hpp" +using wups::config::category; using wups::config::text_item; @@ -17,10 +26,10 @@ using wups::config::text_item; * Note: the clock item needs to know about the server items added later. * It's a bit ugly, because we can't manage it from the category object. */ -wups::config::category +category make_preview_screen() { - wups::config::category cat{"Preview Time"}; + category cat{"Preview Time"}; auto clock = clock_item::create(); auto& server_infos = clock->server_infos; @@ -32,15 +41,15 @@ make_preview_screen() if (!server_infos.contains(server)) { auto& si = server_infos[server]; - auto name = text_item::create({}, server + ":"); + auto name = text_item::create(server + ":"); si.name = name.get(); cat.add(std::move(name)); - auto correction = text_item::create({}, "┣ Correction:", "", 48); + auto correction = text_item::create("├ Correction:", "", 48); si.correction = correction.get(); cat.add(std::move(correction)); - auto latency = text_item::create({}, "┗ Latency:"); + auto latency = text_item::create("└ Latency:"); si.latency = latency.get(); cat.add(std::move(latency)); } diff --git a/source/synchronize_item.cpp b/source/synchronize_item.cpp new file mode 100644 index 0000000..03ed516 --- /dev/null +++ b/source/synchronize_item.cpp @@ -0,0 +1,81 @@ +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ + +#include + +#include +#include + +#include "synchronize_item.hpp" + +#include "cfg.hpp" +#include "core.hpp" + + +using namespace std::literals; + +using namespace wups::config; +namespace logger = wups::logger; + + +synchronize_item::synchronize_item() : + button_item{"Synchronize now!"} +{} + + +std::unique_ptr +synchronize_item::create() +{ + return std::make_unique(); +} + + +void +synchronize_item::on_started() +{ + status_msg = "Synchronizing..."; + + sync_stopper = {}; + + auto task = [this](std::stop_token token) + { + try { + logger::guard lguard; + core::run(token, true); + current_state = state::finished; + } + catch (std::exception& e) { + current_state = state::finished; + throw; + } + }; + + sync_result = std::async(task, sync_stopper.get_token()); +} + + +void +synchronize_item::on_finished() +{ + try { + sync_result.get(); + status_msg = "Success!"; + cfg::save_important_vars(); + } + catch (std::exception& e) { + logger::printf("ERROR: %s\n", e.what()); + status_msg = e.what(); + } +} + + +void +synchronize_item::on_cancel() +{ + sync_stopper.request_stop(); +} diff --git a/source/thread_pool.cpp b/source/thread_pool.cpp index 0e89122..f1838c4 100644 --- a/source/thread_pool.cpp +++ b/source/thread_pool.cpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include "thread_pool.hpp" diff --git a/source/time_utils.cpp b/source/time_utils.cpp index 5f4f169..a583342 100644 --- a/source/time_utils.cpp +++ b/source/time_utils.cpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include // abs() #include // snprintf() @@ -16,29 +22,6 @@ using namespace std::literals; namespace time_utils { - // to_string() functions that show the time unit - - template<> - std::string - to_string(milliseconds t) - { return std::to_string(t.count()) + " ms"; } - - template<> - std::string - to_string(seconds t) - { return std::to_string(t.count()) + " s"; } - - template<> - std::string - to_string(minutes t) - { return std::to_string(t.count()) + " min"; } - - template<> - std::string - to_string(hours t) - { return std::to_string(t.count()) + " h"; } - - std::string seconds_to_human(dbl_seconds s, bool show_positive) { diff --git a/source/time_zone_offset_item.cpp b/source/time_zone_offset_item.cpp new file mode 100644 index 0000000..86cad0e --- /dev/null +++ b/source/time_zone_offset_item.cpp @@ -0,0 +1,111 @@ +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * Copyright (C) 2024 Nightkingale + * + * SPDX-License-Identifier: MIT + */ + +#include // clamp() +#include // abs() +#include // snprintf() +#include // BSD strlcpy() + +#include + +#include "time_zone_offset_item.hpp" + +#include "time_utils.hpp" +#include "utils.hpp" + + +using namespace std::literals; +using namespace wups::config; + + +time_zone_offset_item::time_zone_offset_item(const std::string& label, + std::chrono::minutes& variable, + std::chrono::minutes default_value) : + var_item{label, variable, default_value}, + editing{field_id::hours} +{} + + +std::unique_ptr +time_zone_offset_item::create(const std::string& label, + std::chrono::minutes& variable, + std::chrono::minutes default_value) +{ + return std::make_unique(label, variable, default_value); +} + + +void +time_zone_offset_item::get_display(char* buf, std::size_t size) + const +{ + auto str = time_utils::tz_offset_to_string(variable); + ::strlcpy(buf, str.c_str(), size); +} + + +void +time_zone_offset_item::get_focused_display(char* buf, std::size_t size) + const +{ + const char* left_right = editing == field_id::hours + ? CAFE_GLYPH_BTN_RIGHT + : CAFE_GLYPH_BTN_LEFT; + + const char* up_down; + if (variable >= 14h) + up_down = CAFE_GLYPH_BTN_DOWN; + else if (variable <= -12h) + up_down = CAFE_GLYPH_BTN_UP; + else + up_down = CAFE_GLYPH_BTN_UP_DOWN; + + auto str = time_utils::tz_offset_to_string(variable); + + // insert [] around the correct field (before or after ':') + auto colon_pos = str.find(":"); + if (colon_pos == std::string::npos) + throw std::logic_error{"failed to convert time zone offset to string"}; + switch (editing) { + case field_id::hours: + str = "[" + str.substr(0, colon_pos) + "]" + str.substr(colon_pos); + break; + case field_id::minutes: + str = str.substr(0, colon_pos + 1) + "[" + str.substr(colon_pos + 1) + "]"; + break; + } + + std::snprintf(buf, size, + "%s %s %s", + left_right, str.c_str(), up_down); +} + + +focus_status +time_zone_offset_item::on_input(const simple_pad_data& input) +{ + + if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT) + if (editing == field_id::minutes) + editing = field_id::hours; + + if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT) + if (editing == field_id::hours) + editing = field_id::minutes; + + if (input.pressed_or_repeated(WUPS_CONFIG_BUTTON_UP)) + variable += editing == field_id::hours ? 1h : 1min; + + if (input.pressed_or_repeated(WUPS_CONFIG_BUTTON_DOWN)) + variable -= editing == field_id::hours ? 1h : 1min; + + variable = std::clamp(variable, -12h, 14h); + + return var_item::on_input(input); +} diff --git a/source/time_zone_query_item.cpp b/source/time_zone_query_item.cpp new file mode 100644 index 0000000..e7ccab8 --- /dev/null +++ b/source/time_zone_query_item.cpp @@ -0,0 +1,130 @@ +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ + +#include // snprintf() +#include +#include // BSD strlcpy() + +#include +#include +#include + +#include "time_zone_query_item.hpp" + +#include "cfg.hpp" +#include "utils.hpp" + + +using namespace std::literals; +using namespace wups::config; +namespace logger = wups::logger; + + +namespace { + std::string + make_query_text(int idx) + { + return "Query "s + utils::get_tz_service_name(idx); + } +} // namespace + + +time_zone_query_item::time_zone_query_item(const std::string& label, + int& variable, + const int default_value) : + var_item{label, variable, default_value}, + text{make_query_text(variable)} +{} + + +std::unique_ptr +time_zone_query_item::create(const std::string& label, + int& variable, + const int default_value) +{ + return std::make_unique(label, variable, default_value); +} + + +void +time_zone_query_item::get_display(char* buf, std::size_t size) + const +{ + ::strlcpy(buf, text.c_str(), size); +} + + +void +time_zone_query_item::get_focused_display(char* buf, std::size_t size) + const +{ + std::snprintf(buf, size, + "%s %s %s", + CAFE_GLYPH_BTN_LEFT, + make_query_text(variable).c_str(), + CAFE_GLYPH_BTN_RIGHT); +} + + +focus_status +time_zone_query_item::on_input(const simple_pad_data& input) +{ + + const int n = utils::get_num_tz_services(); + + auto prev_variable = variable; + + if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT) + --variable; + + if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT) + ++variable; + + // let it wrap around + if (variable < 0) + variable += n; + if (variable >= n) + variable -= n; + + if (prev_variable != variable) + text = make_query_text(variable); + + if (input.buttons_d & WUPS_CONFIG_BUTTON_X) { + restore_default(); + text = make_query_text(variable); + return focus_status::lose; + } + + if (input.buttons_d & WUPS_CONFIG_BUTTON_B) { + cancel_change(); + text = make_query_text(variable); + return focus_status::lose; + } + + if (input.buttons_d & WUPS_CONFIG_BUTTON_A) { + confirm_change(); + run(); + return focus_status::lose; + } + + return focus_status::keep; +} + + +void +time_zone_query_item::run() +{ + try { + auto [name, offset] = utils::fetch_timezone(variable); + text = name; + cfg::set_and_store_utc_offset(offset); + } + catch (std::exception& e) { + text = "Error: "s + e.what(); + } +} diff --git a/source/timezone_offset_item.cpp b/source/timezone_offset_item.cpp deleted file mode 100644 index c937efe..0000000 --- a/source/timezone_offset_item.cpp +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include // clamp() -#include // abs() -#include // snprintf() -#include // BSD strlcpy() - -#include "timezone_offset_item.hpp" - -#include "logging.hpp" -#include "nintendo_glyphs.h" -#include "utils.hpp" -#include "wupsxx/storage.hpp" - - -using namespace std::literals; - - -timezone_offset_item::timezone_offset_item(const std::string& key, - const std::string& label, - std::chrono::minutes& variable) : - item{key, label}, - variable(variable) -{} - - -std::unique_ptr -timezone_offset_item::create(const std::string& key, - const std::string& label, - std::chrono::minutes& variable) -{ - return std::make_unique(key, label, variable); -} - - -int -timezone_offset_item::get_display(char* buf, std::size_t size) - const -{ - auto str = time_utils::tz_offset_to_string(*variable); - ::strlcpy(buf, str.c_str(), size); - return 0; -} - - -int -timezone_offset_item::get_selected_display(char* buf, std::size_t size) - const -{ - const char* slow_left = ""; - const char* slow_right = ""; - const char* fast_left = ""; - const char* fast_right = ""; - if (*variable > -12h) { - slow_left = NIN_GLYPH_BTN_DPAD_LEFT " "; - fast_left = NIN_GLYPH_BTN_L; - } if (*variable < 14h) { - slow_right = " " NIN_GLYPH_BTN_DPAD_RIGHT; - fast_right = NIN_GLYPH_BTN_R; - } - - auto str = time_utils::tz_offset_to_string(*variable); - std::snprintf(buf, size, "%s%s" "%s" "%s%s", - fast_left, slow_left, - str.c_str(), - slow_right, fast_right); - return 0; -} - - -void -timezone_offset_item::restore() -{ - variable = 0min; - on_changed(); -} - - -void -timezone_offset_item::on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) -{ - item::on_input(input, repeat); - - if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT || - repeat & WUPS_CONFIG_BUTTON_LEFT) - variable -= 1min; - - if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT || - repeat & WUPS_CONFIG_BUTTON_RIGHT) - variable += 1min; - - if (input.buttons_d & WUPS_CONFIG_BUTTON_L || - repeat & WUPS_CONFIG_BUTTON_L) - variable -= 1h; - - if (input.buttons_d & WUPS_CONFIG_BUTTON_R || - repeat & WUPS_CONFIG_BUTTON_R) - variable += 1h; - - variable = std::clamp(*variable, -12h, 14h); - - on_changed(); -} - - -void -timezone_offset_item::on_changed() -{ - if (!key) - return; - if (!variable.changed()) - return; - - try { - wups::storage::store(*key, *variable); - variable.reset(); - } - catch (std::exception& e) { - logging::printf("Error storing time zone offset: %s", e.what()); - } -} diff --git a/source/timezone_query_item.cpp b/source/timezone_query_item.cpp deleted file mode 100644 index dde55c3..0000000 --- a/source/timezone_query_item.cpp +++ /dev/null @@ -1,127 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include // snprintf() -#include -#include // BSD strlcpy() - -#include "timezone_query_item.hpp" - -#include "cfg.hpp" -#include "logging.hpp" -#include "nintendo_glyphs.h" -#include "utils.hpp" -#include "wupsxx/storage.hpp" - - -using namespace std::literals; - - -timezone_query_item::timezone_query_item(const std::string& key, - const std::string& label, - int& variable, - const int default_value) : - wups::config::item{key, label}, - variable(variable), - default_value{default_value}, - text{"Query "s + utils::get_tz_service_name(variable)} -{} - - -std::unique_ptr -timezone_query_item::create(const std::string& key, - const std::string& label, - int& variable, - const int default_value) -{ - return std::make_unique(key, label, variable, default_value); -} - - -int -timezone_query_item::get_display(char* buf, std::size_t size) - const -{ - ::strlcpy(buf, text.c_str(), size); - return 0; -} - - -int -timezone_query_item::get_selected_display(char* buf, std::size_t size) - const -{ - std::snprintf(buf, size, - "%s %s %s", - NIN_GLYPH_BTN_DPAD_LEFT, - text.c_str(), - NIN_GLYPH_BTN_DPAD_RIGHT); - return 0; -} - - -void -timezone_query_item::restore() -{ - variable = default_value; - - on_changed(); -} - - -void -timezone_query_item::on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) -{ - wups::config::item::on_input(input, repeat); - - const int n = utils::get_num_tz_services(); - - if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT) - --variable; - - if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT) - ++variable; - - // let it wrap around - if (*variable < 0) - variable += n; - if (*variable >= n) - variable -= n; - - on_changed(); - - if (input.buttons_d & WUPS_CONFIG_BUTTON_A) - run(); -} - - -void -timezone_query_item::on_changed() -{ - if (!variable.changed()) - return; - - text = "Query "s + utils::get_tz_service_name(*variable); - - try { - wups::storage::store(*key, *variable); - variable.reset(); - } - catch (std::exception& e) { - logging::printf("Error storing %s: %s", key->c_str(), e.what()); - } -} - - -void -timezone_query_item::run() -{ - try { - auto [name, offset] = utils::fetch_timezone(*variable); - text = name; - cfg::set_and_store_utc_offset(offset); - } - catch (std::exception& e) { - text = "Error: "s + e.what(); - } -} diff --git a/source/utc.cpp b/source/utc.cpp index a180d2c..fd08420 100644 --- a/source/utc.cpp +++ b/source/utc.cpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include diff --git a/source/utils.cpp b/source/utils.cpp index f1956d3..3c8256d 100644 --- a/source/utils.cpp +++ b/source/utils.cpp @@ -1,4 +1,10 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * + * SPDX-License-Identifier: MIT + */ #include // distance() #include // logic_error, runtime_error diff --git a/source/verbosity_item.cpp b/source/verbosity_item.cpp index 024d6a7..7e197ca 100644 --- a/source/verbosity_item.cpp +++ b/source/verbosity_item.cpp @@ -1,15 +1,26 @@ -// SPDX-License-Identifier: MIT +/* + * Wii U Time Sync - A NTP client plugin for the Wii U. + * + * Copyright (C) 2024 Daniel K. O. + * Copyright (C) 2024 Nightkingale + * + * SPDX-License-Identifier: MIT + */ #include // clamp() +#include #include // BSD strlcpy() +#include + #include "verbosity_item.hpp" -#include "nintendo_glyphs.h" + +using namespace wups::config; namespace { - const char* value_str[] = { + const std::array value_str = { "quiet", "normal", "verbose" @@ -18,60 +29,59 @@ namespace { const char* value_to_str(int v) { - v = std::clamp(v, 0, 2); + v = std::clamp(v, 0, value_str.size()); return value_str[v]; } } -verbosity_item::verbosity_item(const std::string& key, - const std::string& label, +verbosity_item::verbosity_item(const std::string& label, int& variable, int default_value) : - wups::config::int_item{key, label, - variable, default_value, - 0, 2, 2} + int_item{label, + variable, + default_value, + 0, 2, 2} {} std::unique_ptr -verbosity_item::create(const std::string& key, - const std::string& label, +verbosity_item::create(const std::string& label, int& variable, int default_value) { - return std::make_unique(key, label, variable, default_value); + return std::make_unique(label, variable, default_value); } -int +void verbosity_item::get_display(char* buf, std::size_t size) const { - ::strlcpy(buf, value_to_str(*variable), size); - return 0; + ::strlcpy(buf, value_to_str(variable), size); } -int -verbosity_item::get_selected_display(char* buf, std::size_t size) +void +verbosity_item::get_focused_display(char* buf, std::size_t size) const { const char* left = ""; const char* right = ""; - switch (*variable) { + switch (variable) { case 0: - right = " " NIN_GLYPH_BTN_DPAD_RIGHT; + right = " " CAFE_GLYPH_BTN_RIGHT; break; case 1: - left = NIN_GLYPH_BTN_DPAD_LEFT " "; - right = " " NIN_GLYPH_BTN_DPAD_RIGHT; + left = CAFE_GLYPH_BTN_LEFT " "; + right = " " CAFE_GLYPH_BTN_RIGHT; break; case 2: - left = NIN_GLYPH_BTN_DPAD_LEFT " "; + left = CAFE_GLYPH_BTN_LEFT " "; break; } - std::snprintf(buf, size, "%s%s%s", left, value_to_str(*variable), right); - return 0; + std::snprintf(buf, size, + "%s%s%s", + left, value_to_str(variable), right); } diff --git a/source/wupsxx/bool_item.cpp b/source/wupsxx/bool_item.cpp deleted file mode 100644 index ecf14d0..0000000 --- a/source/wupsxx/bool_item.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include -#include - -#include "wupsxx/bool_item.hpp" - -#include "logging.hpp" -#include "nintendo_glyphs.h" -#include "wupsxx/storage.hpp" - - -namespace wups::config { - - bool_item::bool_item(const std::optional& key, - const std::string& label, - bool& variable, - bool default_value, - const std::string& true_str, - const std::string& false_str) : - item{key, label}, - variable(variable), - default_value{default_value}, - true_str{true_str}, - false_str{false_str} - {} - - - std::unique_ptr - bool_item::create(const std::optional& key, - const std::string& label, - bool& variable, - bool default_value, - const std::string& true_str, - const std::string& false_str) - { - return std::make_unique(key, label, - variable, default_value, - true_str, false_str); - } - - - int - bool_item::get_display(char* buf, std::size_t size) - const - { - std::snprintf(buf, size, "%s", - *variable ? true_str.c_str() : false_str.c_str()); - return 0; - } - - - int - bool_item::get_selected_display(char* buf, std::size_t size) - const - { - const char* str = *variable ? true_str.c_str() : false_str.c_str(); - - std::snprintf(buf, size, - "%s %s %s", - NIN_GLYPH_BTN_DPAD_LEFT, - str, - NIN_GLYPH_BTN_DPAD_RIGHT); - return 0; - } - - - void - bool_item::restore() - { - variable = default_value; - on_changed(); - } - - - void - bool_item::on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) - { - item::on_input(input, repeat); - - // Allow toggling with A, left or right. - auto mask = WUPS_CONFIG_BUTTON_A | WUPS_CONFIG_BUTTON_LEFT | WUPS_CONFIG_BUTTON_RIGHT; - - if (input.buttons_d & mask) - variable = !*variable; - - on_changed(); - } - - - void - bool_item::on_changed() - { - if (!key) - return; - if (!variable.changed()) - return; - - try { - storage::store(*key, *variable); - variable.reset(); - } - catch (std::exception& e) { - logging::printf("Error storing bool: %s", e.what()); - } - } - -} // namespace wups::config diff --git a/source/wupsxx/category.cpp b/source/wupsxx/category.cpp deleted file mode 100644 index 285b0ba..0000000 --- a/source/wupsxx/category.cpp +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include "wupsxx/category.hpp" -#include "wupsxx/config_error.hpp" - -namespace wups::config { - - category::category(WUPSConfigCategoryHandle handle) : - handle{handle}, - own_handle{false} - {} - - - category::category(const std::string& label) : - own_handle{true} - { - WUPSConfigAPICreateCategoryOptionsV1 options{ .name = label.c_str() }; - auto status = WUPSConfigAPI_Category_Create(options, &handle); - if (status != WUPSCONFIG_API_RESULT_SUCCESS) - throw config_error{status, "could not create category \"" + label + "\""}; - } - - - category::category(category&& other) - noexcept - { - handle = other.handle; - other.handle = {}; - } - - - category::~category() - { - if (own_handle && handle.handle) - WUPSConfigAPI_Category_Destroy(handle); - } - - - void - category::release() - { - handle = {}; - } - - - void - category::add(std::unique_ptr&& item) - { - if (!item) - throw std::logic_error{"cannot add null item to category"}; - if (!item->handle.handle) - throw std::logic_error{"cannot add null item handle to category"}; - - auto status = WUPSConfigAPI_Category_AddItem(handle, item->handle); - if (status != WUPSCONFIG_API_RESULT_SUCCESS) - throw config_error{status, "cannot add item to category: "}; - - item.release(); // WUPS will call .onDelete() later - } - - - void - category::add(category&& child) - { - auto status = WUPSConfigAPI_Category_AddCategory(handle, child.handle); - if (status != WUPSCONFIG_API_RESULT_SUCCESS) - throw config_error{status, "cannot add child category to category: "}; - - child.release(); - } - -} // namespace wups::config diff --git a/source/wupsxx/config_error.cpp b/source/wupsxx/config_error.cpp deleted file mode 100644 index 1e3444d..0000000 --- a/source/wupsxx/config_error.cpp +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include - -#include "wupsxx/config_error.hpp" - - -using namespace std::literals; - - -namespace wups::config { - - config_error::config_error(WUPSConfigAPIStatus status, - const std::string& msg) : - std::runtime_error{msg + ": "s + WUPSConfigAPI_GetStatusStr(status)} - {} - -} // namespace wups::config diff --git a/source/wupsxx/duration_items.cpp b/source/wupsxx/duration_items.cpp deleted file mode 100644 index 21b2e8a..0000000 --- a/source/wupsxx/duration_items.cpp +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include "numeric_item_impl.hpp" - -namespace wups::config { - - template class numeric_item; - - template class numeric_item; - -} // namespace wups::config diff --git a/source/wupsxx/int_item.cpp b/source/wupsxx/int_item.cpp deleted file mode 100644 index dc4eb24..0000000 --- a/source/wupsxx/int_item.cpp +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include "numeric_item_impl.hpp" - - -template class wups::config::numeric_item; diff --git a/source/wupsxx/item.cpp b/source/wupsxx/item.cpp deleted file mode 100644 index d0439f7..0000000 --- a/source/wupsxx/item.cpp +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include -#include -#include // snprintf() - -#include "wupsxx/item.hpp" - -#include "wupsxx/config_error.hpp" - - -using namespace std::literals; - - -namespace wups::config { - - namespace dispatchers { - - int32_t - get_display(void* ctx, char* buf, int32_t size) - { - auto i = static_cast(ctx); - return i->get_display(buf, size); - } - - - int32_t - get_selected_display(void* ctx, char* buf, int32_t size) - { - auto i = static_cast(ctx); - return i->get_selected_display(buf, size); - } - - - bool - is_movement_allowed(void* ctx) - { - auto i = static_cast(ctx); - return i->is_movement_allowed(); - } - - - void - on_close(void* ctx) - { - auto i = static_cast(ctx); - i->on_close(); - } - - - void - on_delete(void* ctx) - { - auto i = static_cast(ctx); - i->release(); // don't destroy the handle, it's already happening - delete i; - } - - - void - on_input(void* ctx, WUPSConfigSimplePadData input) - { - // Here we implement a "repeat" function. - using clock = std::chrono::steady_clock; - using time_point = clock::time_point; - - constexpr auto repeat_delay = 500ms; - static std::array pressed_time{}; - auto now = clock::now(); - - unsigned repeat = 0; - for (unsigned b = 0; b < 16; ++b) { - unsigned mask = 1u << b; - if (input.buttons_d & mask) - pressed_time[b] = now; - - if (input.buttons_h & mask) - // if button was held long enough, flag it as being on a repeat state - if (now - pressed_time[b] >= repeat_delay) - repeat |= mask; - - if (input.buttons_r & mask) - pressed_time[b] = {}; - } - - auto i = static_cast(ctx); - i->on_input(input, static_cast(repeat)); - } - - - void - on_input_ex(void* ctx, WUPSConfigComplexPadData input) - { - // TODO: implement "repeat" functionality for extended input too - auto i = static_cast(ctx); - i->on_input(input); - } - - - void - on_selected(void* ctx, bool is_selected) - { - auto i = static_cast(ctx); - i->on_selected(is_selected); - } - - - void - restore_default(void* ctx) - { - auto i = static_cast(ctx); - i->restore(); - } - - } // namespace dispatchers - - - item::item(const std::optional& key, - const std::string& label) : - key{key} - { - WUPSConfigAPIItemOptionsV2 options { - .displayName = label.c_str(), - .context = this, - .callbacks = { - // Note: do not sort, must be initialized in the order of declaration. - .getCurrentValueDisplay = dispatchers::get_display, - .getCurrentValueSelectedDisplay = dispatchers::get_selected_display, - .onSelected = dispatchers::on_selected, - .restoreDefault = dispatchers::restore_default, - .isMovementAllowed = dispatchers::is_movement_allowed, - .onCloseCallback = dispatchers::on_close, - .onInput = dispatchers::on_input, - .onInputEx = dispatchers::on_input_ex, - .onDelete = dispatchers::on_delete, - } - }; - - auto status = WUPSConfigAPI_Item_Create(options, &handle); - if (status != WUPSCONFIG_API_RESULT_SUCCESS) - throw config_error{status, "could not create config item \"" + label + "\""}; - } - - - item::~item() - { - if (handle.handle) - WUPSConfigAPI_Item_Destroy(handle); - } - - - void - item::release() - { - handle = {}; - } - - - int - item::get_display(char* buf, - std::size_t size) - const - { - std::snprintf(buf, size, "NOT IMPLEMENTED"); - return 0; - } - - - int - item::get_selected_display(char* buf, - std::size_t size) - const - { - return get_display(buf, size); - } - - - void - item::on_selected(bool) - {} - - - void - item::restore() - {} - - - bool - item::is_movement_allowed() - const - { - return true; - } - - - void - item::on_close() - {} - - - void - item::on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT /*repeat*/) - { - if (input.buttons_d & WUPS_CONFIG_BUTTON_X) - restore(); - } - - - void - item::on_input(WUPSConfigComplexPadData /*input*/) - {} - -} // namespace wups::config diff --git a/source/wupsxx/numeric_item_impl.hpp b/source/wupsxx/numeric_item_impl.hpp deleted file mode 100644 index f65e8ac..0000000 --- a/source/wupsxx/numeric_item_impl.hpp +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-License-Identifier: MIT - -#ifndef WUPSXX_NUMERIC_ITEM_IMPL_HPP -#define WUPSXX_NUMERIC_ITEM_IMPL_HPP - -#include // clamp() -#include -#include // snprintf() -#include -#include // BSD strlcpy() - -#include "wupsxx/numeric_item.hpp" - -#include "logging.hpp" -#include "nintendo_glyphs.h" -#include "wupsxx/storage.hpp" -#include "time_utils.hpp" - - -using std::to_string; -using time_utils::to_string; - - -namespace wups::config { - - template - numeric_item::numeric_item(const std::optional& key, - const std::string& label, - T& variable, T default_value, - T min_value, T max_value, - T fast_increment, T slow_increment) : - item{key, label}, - variable(variable), - default_value{default_value}, - min_value{min_value}, - max_value{max_value}, - fast_increment{fast_increment}, - slow_increment{slow_increment} - {} - - - template - std::unique_ptr> - numeric_item::create(const std::optional& key, - const std::string& label, - T& variable, T default_value, - T min_value, T max_value, - T fast_increment, T slow_increment) - { - return std::make_unique>(key, label, - variable, default_value, - min_value, max_value, - fast_increment, slow_increment); - } - - - template - int - numeric_item::get_display(char* buf, std::size_t size) - const - { - std::string str = to_string(*variable); - ::strlcpy(buf, str.c_str(), size); - return 0; - } - - - template - int - numeric_item::get_selected_display(char* buf, std::size_t size) - const - { - const char* slow_left = ""; - const char* slow_right = ""; - const char* fast_left = ""; - const char* fast_right = ""; - if (*variable > min_value) { - slow_left = NIN_GLYPH_BTN_DPAD_LEFT " "; - fast_left = NIN_GLYPH_BTN_L; - } if (*variable < max_value) { - slow_right = " " NIN_GLYPH_BTN_DPAD_RIGHT; - fast_right = NIN_GLYPH_BTN_R; - } - std::string str = to_string(*variable); - std::snprintf(buf, size, - "%s%s" "%s" "%s%s", - fast_left, - slow_left, - str.c_str(), - slow_right, - fast_right); - return 0; - } - - - template - void - numeric_item::restore() - { - variable = default_value; - on_changed(); - } - - - template - void - numeric_item::on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) - { - item::on_input(input, repeat); - - if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT || - repeat & WUPS_CONFIG_BUTTON_LEFT) - variable -= slow_increment; - - if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT || - repeat & WUPS_CONFIG_BUTTON_RIGHT) - variable += slow_increment; - - if (input.buttons_d & WUPS_CONFIG_BUTTON_L || - repeat & WUPS_CONFIG_BUTTON_L) - variable -= fast_increment; - - if (input.buttons_d & WUPS_CONFIG_BUTTON_R || - repeat & WUPS_CONFIG_BUTTON_R) - variable += fast_increment; - - variable = std::clamp(*variable, min_value, max_value); - - on_changed(); - } - - - template - void - numeric_item::on_changed() - { - if (!key) - return; - if (!variable.changed()) - return; - - try { - storage::store(*key, *variable); - variable.reset(); - } - catch (std::exception& e) { - logging::printf("Error storing %s: %s", key->c_str(), e.what()); - } - } - -} // namespace wups::config - - -#endif diff --git a/source/wupsxx/storage.cpp b/source/wupsxx/storage.cpp deleted file mode 100644 index 59b0fa8..0000000 --- a/source/wupsxx/storage.cpp +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include - -#include "wupsxx/storage.hpp" - - -namespace wups::storage { - - void - save() - { - auto status = WUPSStorageAPI::SaveStorage(); - if (status != WUPS_STORAGE_ERROR_SUCCESS) - throw storage_error{"error saving storage", status}; - } - - - void - reload() - { - auto status = WUPSStorageAPI::ForceReloadStorage(); - if (status != WUPS_STORAGE_ERROR_SUCCESS) - throw storage_error{"error reloading storage", status}; - } - - -} // namespace wups::storage diff --git a/source/wupsxx/storage_error.cpp b/source/wupsxx/storage_error.cpp deleted file mode 100644 index 9b80410..0000000 --- a/source/wupsxx/storage_error.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include "wupsxx/storage_error.hpp" - - -namespace wups::storage { - - storage_error::storage_error(const std::string& msg, - WUPSStorageError status) : - std::runtime_error{msg + ": " + - std::string(WUPSStorageAPI::GetStatusStr(status))} - {} - -} diff --git a/source/wupsxx/text_item.cpp b/source/wupsxx/text_item.cpp deleted file mode 100644 index 4161899..0000000 --- a/source/wupsxx/text_item.cpp +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: MIT - -#include // clamp(), min() -#include // snprintf() - -#include "wupsxx/text_item.hpp" - -#include "nintendo_glyphs.h" - - -namespace { - - const std::string left_glyph = NIN_GLYPH_BTN_DPAD_LEFT " "; - const std::string right_glyph = " " NIN_GLYPH_BTN_DPAD_RIGHT; - -} - - -namespace wups::config { - - text_item::text_item(const std::optional& key, - const std::string& label, - const std::string& text, - std::size_t max_width) : - item{key, label}, - text{text}, - max_width{std::min(max_width, 79)} - {} - - - std::unique_ptr - text_item::create(const std::optional& key, - const std::string& label, - const std::string& text, - std::size_t max_width) - { - return std::make_unique(key, label, text, max_width); - } - - - int - text_item::get_display(char* buf, - std::size_t size) - const - { - // Note: `buf` is a C string, it needs a null terminator at the end, - // so the effective `width` is one less than `size`. - std::size_t width = std::min(size - 1, max_width); - - if (width >= text.size()) { - // Easy case: text fits, just show it all. - std::snprintf(buf, size, "%s", text.c_str()); - return 0; - } - - const char* ellipsis = "…"; - - std::string prefix; - if (first > 0) - prefix = ellipsis; - if (width < prefix.size()) // sanity check - return -1; - width -= prefix.size(); - - std::size_t last = first + width; - std::string suffix; - if (last < text.size()) - suffix = ellipsis; - if (width < suffix.size()) // sanity check - return -1; - width -= suffix.size(); - - std::string slice = text.substr(first, width); - - std::snprintf(buf, size, - "%s%s%s", - prefix.c_str(), - slice.c_str(), - suffix.c_str()); - - return 0; - } - - - int - text_item::get_selected_display(char* buf, - std::size_t size) - const - { - // Note: `buf` is a C string, it needs a null terminator at the end, - // so the effective `width` is one less than `size`. - std::size_t width = std::min(size - 1, max_width); - - if (width >= text.size()) { - // Easy case: text fits, just show it all. - std::snprintf(buf, size, "%s", text.c_str()); - return 0; - } - - std::string prefix; - if (first > 0) - prefix = left_glyph; - if (width < prefix.size()) // sanity check - return -1; - width -= prefix.size(); - - std::size_t last = first + width; - std::string suffix; - if (last < text.size()) - suffix = right_glyph; - if (width < suffix.size()) // sanity check - return -1; - width -= suffix.size(); - - std::string slice = text.substr(first, width); - - std::snprintf(buf, size, - "%s%s%s", - prefix.c_str(), - slice.c_str(), - suffix.c_str()); - - return 0; - } - - - void - text_item::on_selected(bool /*is_selected*/) - { - // if (!is_selected) - // first = 0; - } - - - void - text_item::on_input(WUPSConfigSimplePadData input, - WUPS_CONFIG_SIMPLE_INPUT repeat) - { - item::on_input(input, repeat); - - if (text.empty()) - return; - - // If text is fully visible, no scrolling happens. - if (text.size() <= max_width) - return; - - // Handle text scrolling - - const std::size_t max_first = text.size() - max_width + left_glyph.size(); - - if (input.buttons_d & WUPS_CONFIG_BUTTON_LEFT || - repeat & WUPS_CONFIG_BUTTON_LEFT) - if (first > 0) { - --first; - } - - if (input.buttons_d & WUPS_CONFIG_BUTTON_RIGHT || - repeat & WUPS_CONFIG_BUTTON_RIGHT) - if (first < max_first) { - ++first; - } - - if (input.buttons_d & WUPS_CONFIG_BUTTON_L) - first = 0; - - if (input.buttons_d & WUPS_CONFIG_BUTTON_R) - first = max_first; - } - -} // namespace wups::config From 9d8c99c1460424977ca99d6d9cfd0a5f63a01cb7 Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Wed, 11 Sep 2024 23:49:36 -0300 Subject: [PATCH 12/17] Update workflow to use git submodules. --HG-- branch : upstream --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dcb2fb4..55e179a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,8 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 with: - fetch-depth: 0 + fetch-depth: 1 + submodules: true - name: Build Binary run: | @@ -21,4 +22,4 @@ jobs: with: name: Wii_U_Time_Sync.wps path: "*.wps" - if-no-files-found: error \ No newline at end of file + if-no-files-found: error From 008243f66ab8f0de8e7dc1f4cb3f9333da97392f Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Sun, 22 Sep 2024 01:44:46 -0300 Subject: [PATCH 13/17] - Ensure background thread is finished when plugin unloads. - Updated libwupsxx. --HG-- branch : upstream --- external/libwupsxx | 2 +- include/core.hpp | 9 ++++++ source/cfg.cpp | 21 ++----------- source/clock_item.cpp | 2 +- source/core.cpp | 60 +++++++++++++++++++++++++++++++++++++ source/main.cpp | 23 ++------------ source/synchronize_item.cpp | 4 +-- 7 files changed, 78 insertions(+), 43 deletions(-) diff --git a/external/libwupsxx b/external/libwupsxx index 806e94c..577533e 160000 --- a/external/libwupsxx +++ b/external/libwupsxx @@ -1 +1 @@ -Subproject commit 806e94c6e938b3875ccad5c3451d51e2a6f866ed +Subproject commit 577533e1179ecdd25e1448a34b4a50a143628c81 diff --git a/include/core.hpp b/include/core.hpp index a4f73fc..4d85ec6 100644 --- a/include/core.hpp +++ b/include/core.hpp @@ -28,8 +28,17 @@ namespace core { run(std::stop_token token, bool silent); + std::string local_clock_to_string(); + + namespace background { + + void run(); + void stop(); + + } // namespace background + } // namespace core #endif diff --git a/source/cfg.cpp b/source/cfg.cpp index 74cdad0..5976b47 100644 --- a/source/cfg.cpp +++ b/source/cfg.cpp @@ -207,24 +207,9 @@ namespace cfg { void menu_close() { - if (cfg::sync_on_changes && important_vars_changed()) { - std::jthread t{ - [](std::stop_token token) - { - logger::guard lguard{PLUGIN_NAME}; - notify::guard nguard; - try { - core::run(token, false); - } - catch (std::exception& e) { - notify::error(notify::level::normal, e.what()); - } - } - }; - t.detach(); - } + if (cfg::sync_on_changes && important_vars_changed()) + core::background::run(); - // logger::printf("saving config\n"); cfg::save(); logger::finalize(); @@ -267,7 +252,6 @@ namespace cfg { LOAD(tz_service); LOAD(utc_offset); #undef LOAD - // logger::printf("Loaded settings.\n"); } catch (std::exception& e) { logger::printf("Error loading config: %s\n", e.what()); @@ -306,7 +290,6 @@ namespace cfg { STORE(utc_offset); #undef STORE wups::storage::save(); - // logger::printf("Saved settings\n"); } catch (std::exception& e) { logger::printf("Error saving config: %s\n", e.what()); diff --git a/source/clock_item.cpp b/source/clock_item.cpp index e756e8f..1035d2b 100644 --- a/source/clock_item.cpp +++ b/source/clock_item.cpp @@ -89,7 +89,7 @@ clock_item::on_started() catch (std::exception& e) { status_msg = e.what(); } - current_state = state::finished; + current_state = state::stopped; } diff --git a/source/core.cpp b/source/core.cpp index 57f8ada..2d83260 100644 --- a/source/core.cpp +++ b/source/core.cpp @@ -432,4 +432,64 @@ namespace core { return ticks_to_string(OSGetTime()); } + + namespace background { + + std::stop_source stopper{std::nostopstate}; + std::atomic_bool running; + + void + run() + { + if (running) + return; + + running = true; + + std::jthread t{ + [](std::stop_token token) + { + wups::logger::guard lguard{PLUGIN_NAME}; + notify::guard nguard; + try { + core::run(token, false); + } + catch (std::exception& e) { + notify::error(notify::level::normal, e.what()); + } + running = false; + } + }; + + stopper = t.get_stop_source(); + + t.detach(); + } + + + void + stop() + { + if (running) { + stopper.request_stop(); + + // Wait up to ~10 seconds for the thread to flag it stopped running. + unsigned max_tries = 100; + do { + std::this_thread::sleep_for(100ms); + } while (running && --max_tries); + + if (max_tries == 0) + logger::printf("WARNING: Background thread did not stop!\n"); + + stopper = std::stop_source{std::nostopstate}; + } + } + + + } // namespace background + + + + } // namespace core diff --git a/source/main.cpp b/source/main.cpp index 36c0d57..3a4ea39 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -7,9 +7,6 @@ * SPDX-License-Identifier: MIT */ -#include -#include - #include #include @@ -36,26 +33,12 @@ INITIALIZE_PLUGIN() cfg::init(); - if (cfg::sync_on_boot) { - std::jthread t{ - [](std::stop_token token) - { - wups::logger::guard lguard{PLUGIN_NAME}; - notify::guard nguard; - try { - core::run(token, false); - } - catch (std::exception& e) { - notify::error(notify::level::normal, e.what()); - } - } - }; - t.detach(); - } + if (cfg::sync_on_boot) + core::background::run(); } DEINITIALIZE_PLUGIN() { - // TODO: should clean up any worker thread + core::background::stop(); } diff --git a/source/synchronize_item.cpp b/source/synchronize_item.cpp index 03ed516..92a8293 100644 --- a/source/synchronize_item.cpp +++ b/source/synchronize_item.cpp @@ -47,10 +47,10 @@ synchronize_item::on_started() try { logger::guard lguard; core::run(token, true); - current_state = state::finished; + current_state = state::stopped; } catch (std::exception& e) { - current_state = state::finished; + current_state = state::stopped; throw; } }; From d5be6e9aab614fd9d5973c45553223ee95c1ac52 Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Sun, 22 Sep 2024 02:27:43 -0300 Subject: [PATCH 14/17] Changed Makefile to build objects in subdirectories, to avoid conflicts when two different files in different directories produce the same object file name. --HG-- branch : upstream --- Makefile | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 6aea169..1be7e90 100644 --- a/Makefile +++ b/Makefile @@ -91,18 +91,15 @@ LIBDIRS := $(WUMS_ROOT) $(WUPS_ROOT) $(WUT_ROOT) $(PORTLIBS) ifneq ($(BUILD),$(notdir $(CURDIR))) #------------------------------------------------------------------------------- -export OUTPUT := $(CURDIR)/$(TARGET) export TOPDIR := $(CURDIR) +export OUTPUT := $(TOPDIR)/$(TARGET) +export VPATH := $(TOPDIR) +export DEPSDIR := $(TOPDIR)/$(BUILD) -export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ - $(foreach dir,$(DATA),$(CURDIR)/$(dir)) - -export DEPSDIR := $(CURDIR)/$(BUILD) - -CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) -CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) -SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) -BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) +CFILES := $(foreach dir,$(SOURCES),$(wildcard $(dir)/*.c)) +CPPFILES := $(foreach dir,$(SOURCES),$(wildcard $(dir)/*.cpp)) +SFILES := $(foreach dir,$(SOURCES),$(wildcard $(dir)/*.s)) +BINFILES := $(foreach dir,$(DATA),$(wildcard $(dir)/*.*)) #------------------------------------------------------------------------------- # use CXX for linking C++ projects, CC for standard C @@ -120,10 +117,10 @@ endif export OFILES_BIN := $(addsuffix .o,$(BINFILES)) export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) -export OFILES := $(OFILES_BIN) $(OFILES_SRC) +export OFILES := $(OFILES_BIN) $(OFILES_SRC) export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) -export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(TOPDIR)/$(dir)) \ $(foreach dir,$(LIBDIRS),-I$(dir)/include) export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) @@ -135,7 +132,8 @@ all: $(BUILD) $(BUILD): @[ -d $@ ] || mkdir -p $@ - $(MAKE) -C $(BUILD) -f $(CURDIR)/Makefile V=$(DEBUG) + mkdir -p $(addprefix build/,$(sort $(dir $(OFILES)))) + $(MAKE) -C $(BUILD) -f $(TOPDIR)/Makefile V=$(DEBUG) #------------------------------------------------------------------------------- clean: From 3519a52650c3c9b66bab6db40b0995541f3995bb Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Thu, 26 Sep 2024 11:45:01 -0300 Subject: [PATCH 15/17] Ensure the network is initialized before doing network operations. --HG-- branch : upstream --- include/utils.hpp | 20 ++++++++++++++++++ source/utils.cpp | 52 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/include/utils.hpp b/include/utils.hpp index 1118326..689234c 100644 --- a/include/utils.hpp +++ b/include/utils.hpp @@ -56,6 +56,26 @@ namespace utils { std::chrono::minutes> fetch_timezone(int idx); + + // RAII class to ensure network is working. + // It blocks until the network is available, of throws std::runtime_error. + class network_guard { + + struct init_guard { + init_guard(); + ~init_guard(); + }; + + struct connect_guard { + connect_guard(); + ~connect_guard(); + }; + + init_guard init; + connect_guard conn; + + }; + } // namespace utils #endif diff --git a/source/utils.cpp b/source/utils.cpp index 3c8256d..f2515c9 100644 --- a/source/utils.cpp +++ b/source/utils.cpp @@ -9,6 +9,8 @@ #include // distance() #include // logic_error, runtime_error +#include + #include "utils.hpp" #include "http_client.hpp" @@ -16,6 +18,9 @@ using namespace std::literals; +using std::logic_error; +using std::runtime_error; + namespace utils { @@ -123,7 +128,7 @@ namespace utils { case 2: return "https://ipapi.co"; default: - throw std::logic_error{"invalid tz service"}; + throw logic_error{"Invalid tz service."}; } } @@ -131,9 +136,6 @@ namespace utils { std::pair fetch_timezone(int idx) { - if (idx < 0 || idx >= num_tz_services) - throw std::logic_error{"invalid service"}; - const char* service = get_tz_service_name(idx); static const char* urls[num_tz_services] = { @@ -142,6 +144,8 @@ namespace utils { "https://ipapi.co/csv" }; + network_guard net_guard; + std::string response = http::get(urls[idx]); switch (idx) { @@ -150,7 +154,7 @@ namespace utils { { auto tokens = csv_split(response); if (size(tokens) != 2) - throw std::runtime_error{"Could not parse response from "s + service}; + throw runtime_error{"Could not parse response from "s + service}; std::string name = tokens[0]; auto offset = std::chrono::seconds{std::stoi(tokens[1])}; return {name, duration_cast(offset)}; @@ -163,18 +167,18 @@ namespace utils { // returned as +HHMM, not seconds. auto lines = split(response, "\r\n"); if (size(lines) != 2) - throw std::runtime_error{"Could not parse response from "s + service}; + throw runtime_error{"Could not parse response from "s + service}; auto keys = csv_split(lines[0]); auto values = csv_split(lines[1]); if (size(keys) != size(values)) - throw std::runtime_error{"Incoherent response from "s + service}; + throw runtime_error{"Incoherent response from "s + service}; auto tz_it = std::ranges::find(keys, "timezone"); auto offset_it = std::ranges::find(keys, "utc_offset"); if (tz_it == keys.end() || offset_it == keys.end()) - throw std::runtime_error{"Could not find timezone or utc_offset fields" - " in response."}; + throw runtime_error{"Could not find timezone or utc_offset fields" + " in response."}; auto tz_idx = std::distance(keys.begin(), tz_it);; auto offset_idx = std::distance(keys.begin(), offset_it); @@ -182,7 +186,7 @@ namespace utils { std::string name = values[tz_idx]; std::string hhmm = values[offset_idx]; if (empty(hhmm)) - throw std::runtime_error{"Invalid UTC offset string."}; + throw runtime_error{"Invalid UTC offset string."}; char sign = hhmm[0]; std::string hh = hhmm.substr(1, 2); @@ -196,9 +200,35 @@ namespace utils { } default: - throw std::logic_error{"invalid tz service"}; + throw logic_error{"Invalid tz service."}; } } + + network_guard::init_guard::init_guard() + { + if (!nn::ac::Initialize()) + throw runtime_error{"Network error (nn::ac::Initialize() failed)"}; + } + + + network_guard::init_guard::~init_guard() + { + nn::ac::Finalize(); + } + + + network_guard::connect_guard::connect_guard() + { + if (!nn::ac::Connect()) + throw runtime_error{"Network error (nn::ac::Connect() failed)"}; + } + + + network_guard::connect_guard::~connect_guard() + { + nn::ac::Close(); + } + } // namespace utils From 65ed68414b496955bbc45b14a6f969313ceb32e7 Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Thu, 26 Sep 2024 12:40:37 -0300 Subject: [PATCH 16/17] Forgot to merge all changes. --HG-- branch : upstream --- source/clock_item.cpp | 2 ++ source/core.cpp | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/source/clock_item.cpp b/source/clock_item.cpp index 1035d2b..cac9808 100644 --- a/source/clock_item.cpp +++ b/source/clock_item.cpp @@ -110,6 +110,8 @@ clock_item::run() using std::to_string; using time_utils::seconds_to_human; + utils::network_guard net_guard; + for (auto& [key, value] : server_infos) { value.name->text.clear(); value.correction->text.clear(); diff --git a/source/core.cpp b/source/core.cpp index 2d83260..7aac978 100644 --- a/source/core.cpp +++ b/source/core.cpp @@ -91,7 +91,7 @@ namespace { return ticks_to_string(ticks); } -} +} // namespace namespace core { @@ -291,6 +291,8 @@ namespace core { { using time_utils::seconds_to_human; + utils::network_guard net_guard; + // ensure notification is initialized if needed notify::guard notify_guard{cfg::notify > 0}; @@ -489,7 +491,4 @@ namespace core { } // namespace background - - - } // namespace core From 989ff1d952ae458e97cf5bdf3558dde95bb91a56 Mon Sep 17 00:00:00 2001 From: "Daniel K. O. (dkosmari)" Date: Fri, 27 Sep 2024 22:05:07 -0300 Subject: [PATCH 17/17] Ensure synchronization task is launched asynchronously. --HG-- branch : upstream --- include/synchronize_item.hpp | 4 ++-- source/cfg.cpp | 2 -- source/synchronize_item.cpp | 10 ++++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/synchronize_item.hpp b/include/synchronize_item.hpp index c286d93..d83e32e 100644 --- a/include/synchronize_item.hpp +++ b/include/synchronize_item.hpp @@ -18,8 +18,8 @@ struct synchronize_item : wups::config::button_item { - std::future sync_result; - std::stop_source sync_stopper; + std::future task_result; + std::stop_source task_stopper; synchronize_item(); diff --git a/source/cfg.cpp b/source/cfg.cpp index 5976b47..d3215a4 100644 --- a/source/cfg.cpp +++ b/source/cfg.cpp @@ -192,10 +192,8 @@ namespace cfg { { logger::initialize(PLUGIN_NAME); - // logger::printf("reloading configs\n"); cfg::reload(); - // logger::printf("building config items\n"); root.add(make_config_screen()); root.add(make_preview_screen()); root.add(synchronize_item::create()); diff --git a/source/synchronize_item.cpp b/source/synchronize_item.cpp index 92a8293..fffa8a6 100644 --- a/source/synchronize_item.cpp +++ b/source/synchronize_item.cpp @@ -40,7 +40,7 @@ synchronize_item::on_started() { status_msg = "Synchronizing..."; - sync_stopper = {}; + task_stopper = {}; auto task = [this](std::stop_token token) { @@ -55,7 +55,9 @@ synchronize_item::on_started() } }; - sync_result = std::async(task, sync_stopper.get_token()); + task_result = std::async(std::launch::async, + std::move(task), + task_stopper.get_token()); } @@ -63,7 +65,7 @@ void synchronize_item::on_finished() { try { - sync_result.get(); + task_result.get(); status_msg = "Success!"; cfg::save_important_vars(); } @@ -77,5 +79,5 @@ synchronize_item::on_finished() void synchronize_item::on_cancel() { - sync_stopper.request_stop(); + task_stopper.request_stop(); }