Add update check and download on boot.

This commit is contained in:
J-D-K 2025-09-26 01:09:37 -04:00
parent 74678357bb
commit 22b9734c37
15 changed files with 204 additions and 21 deletions

View File

@ -39,7 +39,7 @@ INCLUDES := include ./Libraries/FsLib/Switch/FsLib/include ./Libraries/SDLLib/SD
EXEFS_SRC := exefs_src
APP_TITLE := JKSV
APP_AUTHOR := JK
APP_VERSION := 09.25.2025
APP_VERSION := 09.26.2025
ROMFS := romfs
ICON := icon.jpg

View File

@ -46,7 +46,7 @@ class ConfirmState final : public BaseState
, m_yesText(strings::get_by_name(strings::names::YES_NO_OK, 0))
, m_noText(strings::get_by_name(strings::names::YES_NO_OK, 1))
, m_holdRequired(holdRequired)
, m_transition(280, 720, 280, 262, 4)
, m_transition(280, 720, 280, 229, 4)
, m_function(function)
, m_taskData(taskData)
{

View File

@ -35,6 +35,9 @@ class MainMenuState final : public BaseState
/// @brief Renders menu to screen.
void render() override;
/// @brief Allows the update task to signal it found an update.
void signal_update_found();
/// @brief Signals to
static void initialize_view_states();
@ -45,6 +48,7 @@ class MainMenuState final : public BaseState
struct DataStruct : sys::Task::DataStruct
{
data::UserList userList;
MainMenuState *spawningState{};
};
// clang-format on
@ -70,6 +74,9 @@ class MainMenuState final : public BaseState
/// @brief This is the data struct passed to tasks.
std::shared_ptr<MainMenuState::DataStruct> m_dataStruct{};
/// @brief Allows the check_for_update task to signal that an update was found (to prevent corrupted textures!)
std::atomic_bool m_updateFound{};
/// @brief Records the size of the sm_users vector.
static inline int sm_userCount{};
@ -94,6 +101,9 @@ class MainMenuState final : public BaseState
/// @brief Initializes the data struct.
void initialize_data_struct();
/// @brief Silently checks for an update in the background.
void check_for_update();
/// @brief Pushes the target state to the vector.
void push_target_state();
@ -102,4 +112,6 @@ class MainMenuState final : public BaseState
/// @brief Backups up all save data for all users.
void backup_all_for_all();
void confirm_update();
};

8
include/builddate.hpp Normal file
View File

@ -0,0 +1,8 @@
#pragma once
namespace builddate
{
inline constexpr int MONTH = 9;
inline constexpr int DAY = 26;
inline constexpr int YEAR = 2025;
}

View File

@ -56,6 +56,8 @@ namespace curl
static inline void reset_handle(curl::Handle &curl)
{
curl_easy_reset(curl.get());
curl::set_option(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl::set_option(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl::set_option(curl, CURLOPT_USERAGENT, curl::STRING_USER_AGENT);
curl::set_option(curl, CURLOPT_CONNECTTIMEOUT, 5L);
}

View File

@ -38,6 +38,7 @@ namespace strings::names
inline constexpr std::string_view TITLEOPTION_STATUS = "TitleOptionStatus";
inline constexpr std::string_view TITLEOPTION = "TitleOptions";
inline constexpr std::string_view TRANSLATION = "TranslationInfo";
inline constexpr std::string_view UPDATE_CONFIRMATION = "UpdateConfirmation";
inline constexpr std::string_view USEROPTION_CONFS = "UserOptionConfirmations";
inline constexpr std::string_view USEROPTION_STATUS = "UserOptionStatus";
inline constexpr std::string_view USEROPTION_MENU = "UserOptions";

11
include/tasks/update.hpp Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include "sys/threadpool.hpp"
namespace tasks::update
{
/// @brief Checks for updates. If one is found, prompts to download it.
void check_for_update(sys::threadpool::JobData jobData);
/// @brief Downloads the new JKSV NRO.
void download_update(sys::threadpool::JobData jobData);
}

View File

@ -267,6 +267,9 @@
"0: Translated by: %s",
"1: NULL"
],
"UpdateConfirmation": [
"A new update has been found for JKSV. Would you like to download it now?"
],
"UserOptionConfirmations": [
"0: Are you sure you want to backup the save data for every title found for `%s`? This can take a while.",
"1: Are you sure you want to create save data for all titles found on your system for `%s`? This can take a while.",

View File

@ -4,6 +4,7 @@
#include "appstates/FileModeState.hpp"
#include "appstates/MainMenuState.hpp"
#include "appstates/TaskState.hpp"
#include "builddate.hpp"
#include "config/config.hpp"
#include "curl/curl.hpp"
#include "data/data.hpp"
@ -29,13 +30,6 @@
namespace
{
/// @brief Build month.
constexpr uint8_t BUILD_MON = 9;
/// @brief Build day.
constexpr uint8_t BUILD_DAY = 25;
/// @brief Year.
constexpr uint16_t BUILD_YEAR = 2025;
/// @brief Config for socket.
constexpr SocketInitConfig SOCKET_INIT_CONFIG = {.tcp_tx_buf_size = 0x20000,
.tcp_rx_buf_size = 0x20000,
@ -83,7 +77,7 @@ JKSV::JKSV()
const char *author = strings::get_by_name(strings::names::TRANSLATION, 1);
m_showTranslationInfo = std::char_traits<char>::compare(author, "NULL", 4) != 0; // This is whether or not to show.
m_translationInfo = stringutil::get_formatted_string(translationFormat, author);
m_buildString = stringutil::get_formatted_string("v. %02d.%02d.%04d", BUILD_MON, BUILD_DAY, BUILD_YEAR);
m_buildString = stringutil::get_formatted_string("v. %02d.%02d.%04d", builddate::MONTH, builddate::DAY, builddate::YEAR);
// This needs the config init'd or read to work.
JKSV::create_directories();

View File

@ -16,7 +16,9 @@
#include "sdl.hpp"
#include "strings/strings.hpp"
#include "stringutil.hpp"
#include "sys/sys.hpp"
#include "tasks/mainmenu.hpp"
#include "tasks/update.hpp"
#include "ui/PopMessageManager.hpp"
MainMenuState::MainMenuState()
@ -32,6 +34,7 @@ MainMenuState::MainMenuState()
MainMenuState::initialize_menu();
MainMenuState::initialize_view_states();
MainMenuState::initialize_data_struct();
MainMenuState::check_for_update();
}
void MainMenuState::update()
@ -47,6 +50,11 @@ void MainMenuState::update()
if (aPressed) { MainMenuState::push_target_state(); }
else if (toUserOptions) { MainMenuState::create_user_options(); }
else if (yPressed) { MainMenuState::backup_all_for_all(); }
else if (m_updateFound.load())
{
m_updateFound.store(false);
MainMenuState::confirm_update();
}
m_mainMenu->update(hasFocus);
m_controlGuide->update(hasFocus);
@ -73,6 +81,8 @@ void MainMenuState::render()
}
}
void MainMenuState::signal_update_found() { m_updateFound.store(true); }
void MainMenuState::initialize_view_states()
{
const bool jksmMode = config::get_by_key(config::keys::JKSM_TEXT_MODE);
@ -118,7 +128,13 @@ void MainMenuState::initialize_menu()
m_mainMenu->add_option(m_extrasIcon);
}
void MainMenuState::initialize_data_struct() { m_dataStruct->userList = sm_users; }
void MainMenuState::initialize_data_struct()
{
m_dataStruct->userList = sm_users;
m_dataStruct->spawningState = this;
}
void MainMenuState::check_for_update() { sys::threadpool::push_job(tasks::update::check_for_update, m_dataStruct); }
void MainMenuState::push_target_state()
{
@ -165,3 +181,11 @@ void MainMenuState::backup_all_for_all()
}
else { ConfirmProgress::create_push_fade(query, true, tasks::mainmenu::backup_all_for_all_local, m_dataStruct); }
}
void MainMenuState::confirm_update()
{
const char *confirmUpdate = strings::get_by_name(strings::names::UPDATE_CONFIRMATION, 0);
auto taskData = std::make_shared<sys::Task::DataStruct>();
ConfirmProgress::create_push_fade(confirmUpdate, false, tasks::update::download_update, taskData);
}

View File

@ -8,7 +8,7 @@
MessageState::MessageState(std::string_view message)
: m_message(message)
, m_transition(280, 720, 280, 262, 4)
, m_transition(280, 720, 280, 229, 4)
{
MessageState::initialize_static_members();
}

View File

@ -13,9 +13,9 @@
namespace
{
constexpr int COORD_BAR_X = 312;
constexpr int COORD_BAR_Y = 470;
constexpr int COORD_BAR_Y = 437;
constexpr int COORD_TEXT_Y = 475;
constexpr int COORD_TEXT_Y = 442;
constexpr int COORD_DISPLAY_CENTER = 640;
constexpr double SIZE_BAR_WIDTH = 656.0f;
@ -54,9 +54,9 @@ void ProgressState::render()
sdl::render_rect_fill(sdl::Texture::Null, 0, 0, 1280, 720, colors::DIM_BACKGROUND);
sm_dialog->render(sdl::Texture::Null, hasFocus);
sdl::text::render(sdl::Texture::Null, 312, 288, BaseTask::FONT_SIZE, 656, colors::WHITE, status);
sdl::text::render(sdl::Texture::Null, 312, 255, BaseTask::FONT_SIZE, 656, colors::WHITE, status);
sdl::render_line(sdl::Texture::Null, 280, 454, 999, 454, colors::DIV_COLOR);
sdl::render_line(sdl::Texture::Null, 280, 421, 999, 421, colors::DIV_COLOR);
sdl::render_rect_fill(sdl::Texture::Null, COORD_BAR_X, COORD_BAR_Y, barWidth, 32, colors::BLACK);
sdl::render_rect_fill(sdl::Texture::Null, COORD_BAR_X, COORD_BAR_Y, m_progressBarWidth, 32, colors::BAR_GREEN);
@ -80,7 +80,7 @@ void ProgressState::initialize_static_members()
if (sm_dialog && sm_barEdges) { return; }
sm_dialog = ui::DialogBox::create(280, 262, 720, 256);
sm_dialog = ui::DialogBox::create(280, 229, 720, 256);
sm_barEdges = sdl::TextureManager::load(BAR_EDGE_NAME, "romfs:/Textures/BarEdges.png");
}

View File

@ -181,7 +181,7 @@ void curl::prepare_get(curl::Handle &curl)
// Setup basic request.
curl::set_option(curl, CURLOPT_HTTPGET, 1L);
// curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
}
void curl::prepare_post(curl::Handle &curl)
@ -189,7 +189,7 @@ void curl::prepare_post(curl::Handle &curl)
curl::reset_handle(curl);
curl::set_option(curl, CURLOPT_POST, 1L);
// curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
}
void curl::prepare_upload(curl::Handle &curl)
@ -197,5 +197,5 @@ void curl::prepare_upload(curl::Handle &curl)
curl::reset_handle(curl);
curl::set_option(curl, CURLOPT_UPLOAD, 1L);
// curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
curl::set_option(curl, CURLOPT_ACCEPT_ENCODING, "");
}

View File

@ -5,7 +5,7 @@
int main(int argc, const char *argv[])
{
JKSV jksv{};
while (appletMainLoop() && jksv.is_running())
while (jksv.is_running())
{
jksv.update();
jksv.render();

128
source/tasks/update.cpp Normal file
View File

@ -0,0 +1,128 @@
#include "tasks/update.hpp"
#include "JKSV.hpp"
#include "appstates/ConfirmState.hpp"
#include "appstates/MainMenuState.hpp"
#include "builddate.hpp"
#include "curl/curl.hpp"
#include "error.hpp"
#include "json.hpp"
#include "logging/logger.hpp"
#include "remote/remote.hpp"
#include "strings/strings.hpp"
#include "stringutil.hpp"
#include <string_view>
// Defined at bottom.
static std::string get_git_json(curl::Handle &handle);
static bool get_date_as_ints(std::string_view dateString, int &month, int &day, int &year);
void tasks::update::check_for_update(sys::threadpool::JobData jobData)
{
auto castData = std::static_pointer_cast<MainMenuState::DataStruct>(jobData);
MainMenuState *spawningState = castData->spawningState;
// Just borrow this.
if (!remote::has_internet_connection()) { return; }
curl::Handle curlHandle = curl::new_handle();
const std::string gitJson = get_git_json(curlHandle);
json::Object parser = json::new_object(json_tokener_parse, gitJson.c_str());
if (!parser) { return; }
json_object *tagName = json::get_object(parser, "tag_name");
if (!tagName) { return; }
int month{}, day{}, year{};
const char *dateString = json_object_get_string(tagName);
get_date_as_ints(dateString, month, day, year);
const bool greaterYear = year > builddate::YEAR;
const bool greaterMonth = year == builddate::YEAR && month > builddate::MONTH;
const bool greaterDay = year == builddate::YEAR && month == builddate::MONTH && day > builddate::DAY;
const bool updateAvailable = greaterYear || greaterMonth || greaterDay;
if (updateAvailable) { spawningState->signal_update_found(); }
}
void tasks::update::download_update(sys::threadpool::JobData jobData)
{
auto castData = std::static_pointer_cast<sys::Task::DataStruct>(jobData);
sys::ProgressTask *task = static_cast<sys::ProgressTask *>(castData->task);
if (error::is_null(task)) { return; }
curl::Handle downloadCurl = curl::new_handle();
const std::string gitJson = get_git_json(downloadCurl);
json::Object parser = json::new_object(json_tokener_parse, gitJson.c_str());
if (!parser) { TASK_FINISH_RETURN(task); }
const char *downloadingFormat = strings::get_by_name(strings::names::IO_STATUSES, 4);
std::string status = stringutil::get_formatted_string(downloadingFormat, "JKSV.nro");
task->set_status(status);
json_object *assets = json::get_object(parser, "assets");
if (!assets) { TASK_FINISH_RETURN(task); }
json_object *assetZero = json_object_array_get_idx(assets, 0);
if (!assetZero) { TASK_FINISH_RETURN(task); }
json_object *downloadUrl = json_object_object_get(assetZero, "browser_download_url");
json_object *downloadSize = json_object_object_get(assetZero, "size");
if (!downloadUrl || !downloadSize) { TASK_FINISH_RETURN(task); }
const char *nroUrl = json_object_get_string(downloadUrl);
const uint64_t nroSize = json_object_get_uint64(downloadSize);
task->reset(static_cast<double>(nroSize));
// To do: Figure out how to get the argument from main here.
error::libnx(romfsExit()); // This is needed so I can overwrite the NRO.
fslib::File jksv{"sdmc:/switch/JKSV.nro", FsOpenMode_Create | FsOpenMode_Write, static_cast<int64_t>(nroSize)};
if (error::fslib(jksv.is_open())) { TASK_FINISH_RETURN(task); }
auto download = curl::create_download_struct(jksv, task, nroSize);
curl::set_option(downloadCurl, CURLOPT_URL, nroUrl);
curl::set_option(downloadCurl, CURLOPT_WRITEFUNCTION, curl::download_file_threaded);
curl::set_option(downloadCurl, CURLOPT_WRITEDATA, download.get());
curl::set_option(downloadCurl, CURLOPT_FOLLOWLOCATION, 1L);
sys::threadpool::push_job(curl::download_write_thread_function, download);
if (!curl::perform(downloadCurl)) { TASK_FINISH_RETURN(task); }
download->writeComplete.acquire();
task->complete();
}
static std::string get_git_json(curl::Handle &handle)
{
static constexpr const char *URL_GIT_API = "https://api.github.com/repos/J-D-K/JKSV/releases/latest";
std::string response{};
curl::prepare_get(handle);
curl::set_option(handle, CURLOPT_URL, URL_GIT_API);
curl::set_option(handle, CURLOPT_WRITEFUNCTION, curl::write_response_string);
curl::set_option(handle, CURLOPT_WRITEDATA, &response);
curl::set_option(handle, CURLOPT_FOLLOWLOCATION, 1L);
curl::perform(handle);
return response;
}
static bool get_date_as_ints(std::string_view dateString, int &month, int &day, int &year)
{
size_t monthSlash = dateString.find_first_of('/');
if (monthSlash == dateString.npos) { return false; }
const std::string monthString{dateString.substr(0, monthSlash)};
size_t daySlash = dateString.find_first_of('/', ++monthSlash);
if (daySlash == dateString.npos) { return false; }
const std::string dayString{dateString.substr(monthSlash, daySlash - monthSlash)};
const std::string yearString{dateString.substr(++daySlash, dateString.npos)};
month = std::strtol(monthString.c_str(), nullptr, 10);
day = std::strtol(dayString.c_str(), nullptr, 10);
year = std::strtol(yearString.c_str(), nullptr, 10);
return true;
}