From 05dec93795460bf1f1dd14409efe8f6329effd7d Mon Sep 17 00:00:00 2001 From: Pablo Curiel Date: Sun, 25 Jul 2021 01:37:13 -0400 Subject: [PATCH] Implemented AsyncTask class. Other changes include: * Updated borealis. * Updated Makefile. * Began implementation of a (very) simple, CURL-based HTTP handler. * OptionsTab: added a small disclaimer about dump options. * OptionsTab: added notifications for the update application item (running as NSO, app already updated). * config: improved boundary handling while validating integer entries. * utils: (de)initialize CURL (this will be moved to http.c eventually). --- Makefile | 3 - http.c | 23 +++ include/async_task.hpp | 307 ++++++++++++++++++++++++++++++ include/core/config.h | 2 + include/core/http.h | 39 ++++ include/core/nxdt_includes.h | 3 + include/options_tab.hpp | 7 + libs/borealis | 2 +- romfs/i18n/en-US/options_tab.json | 7 + romfs/i18n/en-US/tasks.json | 12 +- source/core/config.c | 8 +- source/core/nxdt_utils.c | 18 +- source/options_tab.cpp | 64 ++++++- todo.txt | 1 + 14 files changed, 474 insertions(+), 22 deletions(-) create mode 100644 http.c create mode 100644 include/async_task.hpp create mode 100644 include/core/http.h diff --git a/Makefile b/Makefile index c938779..8c88e2a 100644 --- a/Makefile +++ b/Makefile @@ -82,9 +82,6 @@ CFLAGS += -DVERSION_MAJOR=${VERSION_MAJOR} -DVERSION_MINOR=${VERSION_MINOR} -DV CFLAGS += -DAPP_TITLE=\"${APP_TITLE}\" -DAPP_AUTHOR=\"${APP_AUTHOR}\" -DAPP_VERSION=\"${APP_VERSION}\" CFLAGS += -DGIT_BRANCH=\"${GIT_BRANCH}\" -DGIT_COMMIT=\"${GIT_COMMIT}\" -DGIT_REV=\"${GIT_REV}\" CFLAGS += -DBOREALIS_RESOURCES="\"${BOREALIS_RESOURCES}\"" -CFLAGS += `aarch64-none-elf-pkg-config zlib --cflags` -CFLAGS += `aarch64-none-elf-pkg-config libxml-2.0 --cflags` -CFLAGS += `aarch64-none-elf-pkg-config json-c --cflags` CXXFLAGS := $(CFLAGS) -std=c++20 -Wno-volatile -Wno-unused-parameter diff --git a/http.c b/http.c new file mode 100644 index 0000000..01b72cf --- /dev/null +++ b/http.c @@ -0,0 +1,23 @@ +/* + * http.c + * + * Copyright (c) 2020-2021, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nxdt_utils.h" +#include "http.h" diff --git a/include/async_task.hpp b/include/async_task.hpp new file mode 100644 index 0000000..55b7878 --- /dev/null +++ b/include/async_task.hpp @@ -0,0 +1,307 @@ +/* + * async_task.hpp + * + * Copyright (c) 2020-2021, DarkMatterCore . + * + * Based on attcs' C++ implementation at: + * https://github.com/attcs/AsyncTask/blob/master/asynctask.h. + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#ifndef __ASYNC_TASK_HPP__ +#define __ASYNC_TASK_HPP__ + +#include +#include +#include + +namespace nxdt::utils +{ + /* Used by AsyncTask to throw exceptions whenever required. */ + class AsyncTaskException : std::exception + { + public: + enum class eEx : int + { + TaskIsAlreadyRunning, ///< Task is already running. + TaskIsAlreadyFinished, ///< Task is already finished. + TaskIsPending, ///< Task hasn't been executed. + TaskIsCancelled, ///< Task has been cancelled. + TaskWaitTimeout ///< Timed out while waiting for the task to finish. + }; + + eEx e; + + AsyncTaskException() = default; + AsyncTaskException(eEx e) : e(e) { } + }; + + /* Used by AsyncTask to indicate the current status of the asynchronous task. */ + enum class AsyncTaskStatus : int + { + PENDING, ///< The task hasn't been executed yet. + RUNNING, ///< The task is currently running. + FINISHED ///< The task is finished. + }; + + /* Asynchronous task handler class. */ + template + class AsyncTask + { + private: + AsyncTaskStatus m_status = AsyncTaskStatus::PENDING; + Result m_result{}; + std::future m_future{}; + std::atomic m_progress{}; + std::atomic_bool m_cancelled = false; + std::atomic_bool m_rethrowException = false; + std::exception_ptr m_exceptionPtr{}; + + /* Runs on the calling thread after doInBackground() finishes execution. */ + void finish(Result&& result) + { + /* Copy result. */ + this->m_result = result; + + /* Call appropiate post-execution function. */ + if (this->isCancelled()) + { + this->onCancelled(this->m_result); + } else { + this->onPostExecute(this->m_result); + } + + /* Update status. */ + this->m_status = AsyncTaskStatus::FINISHED; + + /* Rethrow asynchronous task exception (if available). */ + if (this->m_rethrowException.load() && this->m_exceptionPtr) std::rethrow_exception(this->m_exceptionPtr); + } + + protected: + /* Set class as non-copyable. */ + NON_COPYABLE(AsyncTask); + + virtual ~AsyncTask(void) noexcept + { + /* Return right away if the task isn't running. */ + if (this->getStatus() != AsyncTaskStatus::RUNNING) return; + + /* Cancel task. This won't do anything if it has already been cancelled. */ + this->cancel(); + + /* Return right away if the result was already retrieved. */ + if (!this->m_future.valid()) return; + + /* Wait until a result is provided by the task thread. */ + /* Avoid rethrowing any exceptions here - program execution could end if another exception has already been rethrown. */ + m_future.wait(); + } + + /* Asynchronous task function. */ + /* This function should periodically call isCancelled() to determine if it should end prematurely. */ + virtual Result doInBackground(const Params&... params) = 0; + + /* Posts asynchronous task result. Runs on the asynchronous task thread. */ + virtual Result postResult(Result&& result) + { + return result; + } + + /* Cleanup function called if the task is cancelled. Runs on the calling thread. */ + virtual void onCancelled(const Result& result) { } + + /* Post-execution function called right after the task finishes. Runs on the calling thread. */ + virtual void onPostExecute(const Result& result) { } + + /* Pre-execution function called right before the task starts. Runs on the calling thread. */ + virtual void onPreExecute(void) { } + + /* Progress update function. Runs on the calling thread. */ + virtual void onProgressUpdate(const Progress& progress) { } + + /* Stores the current progress inside the class. Runs on the asynchronous task thread. */ + virtual void publishProgress(const Progress& progress) + { + /* Don't proceed if the task isn't running. */ + if (this->getStatus() != AsyncTaskStatus::RUNNING || this->isCancelled()) return; + + /* Update progress. */ + this->m_progress.store(progress); + } + + public: + AsyncTask(void) = default; + + /* Cancels the task. Runs on the calling thread. */ + void cancel(void) noexcept + { + /* Return right away if the task has already completed, or if it has already been cancelled. */ + if (this->getStatus() == AsyncTaskStatus::FINISHED || this->isCancelled()) return; + + /* Update cancel flag. */ + this->m_cancelled.store(true); + } + + /* Starts the asynchronous task. Runs on the calling thread. */ + AsyncTask& execute(const Params&... params) + { + /* Return right away if the task was cancelled before starting. */ + if (this->isCancelled()) return *this; + + /* Verify task status. */ + switch(this->getStatus()) + { + case AsyncTaskStatus::RUNNING: + throw AsyncTaskException(AsyncTaskException::eEx::TaskIsAlreadyRunning); + case AsyncTaskStatus::FINISHED: + throw AsyncTaskException(AsyncTaskException::eEx::TaskIsAlreadyFinished); + default: + break; + } + + /* Update task status. */ + this->m_status = AsyncTaskStatus::RUNNING; + + /* Run onPreExecute() callback. */ + this->onPreExecute(); + + /* Start asynchronous task on a new thread. */ + this->m_future = std::async(std::launch::async, [this](const Params&... params) -> Result { + /* Catch any exceptions thrown by the asynchronous task. */ + try { + return this->postResult(this->doInBackground(params...)); + } catch(...) { + this->cancel(); + this->m_rethrowException.store(true); + this->m_exceptionPtr = std::current_exception(); + } + + return {}; + }, params...); + + return *this; + } + + /* Waits for the asynchronous task to complete, then returns its result. Runs on the calling thread. */ + /* If an exception is thrown by the asynchronous task, it will be rethrown by this function. */ + Result get(void) + { + auto status = this->getStatus(); + + /* Throw an exception if the asynchronous task hasn't been executed. */ + if (status == AsyncTaskStatus::PENDING) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsPending); + + /* If the task is still running, wait until it finishes. */ + /* get() calls wait() on its own if the result hasn't been retrieved. */ + /* finish() takes care of rethrowing any exceptions thrown by the asynchronous task. */ + if (status == AsyncTaskStatus::RUNNING) this->finish(this->m_future.get()); + + /* Throw an exception if the asynchronous task was cancelled. */ + if (this->isCancelled()) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsCancelled); + + /* Return result. */ + return this->m_result; + } + + /* Waits for at most the given time for the asynchronous task to complete, then returns its result. Runs on the calling thread. */ + /* If an exception is thrown by the asynchronous task, it will be rethrown by this function. */ + template + Result get(const std::chrono::duration& timeout) + { + auto status = this->getStatus(); + + /* Throw an exception if the asynchronous task hasn't been executed. */ + if (status == AsyncTaskStatus::PENDING) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsPending); + + /* Check if the task is still running. */ + if (status == AsyncTaskStatus::RUNNING) + { + /* Wait for at most the given time for the asynchronous task to complete. */ + auto thread_status = this->m_future.wait_for(timeout); + switch(thread_status) + { + case std::future_status::timeout: + /* Throw an exception if we timed out while waiting for the task to finish. */ + throw AsyncTaskException(AsyncTaskException::eEx::TaskWaitTimeout); + case std::future_status::ready: + /* Retrieve the task result. */ + /* finish() takes care of rethrowing any exceptions thrown by the asynchronous task. */ + this->finish(this->m_future.get()); + + /* Throw an exception if the asynchronous task was cancelled. */ + if (this->isCancelled()) throw AsyncTaskException(AsyncTaskException::eEx::TaskIsCancelled); + + break; + default: + break; + } + } + + /* Return result. */ + return this->m_result; + } + + /* Returns the current task status. Runs on both threads. */ + AsyncTaskStatus getStatus(void) noexcept + { + return this->m_status; + } + + /* Returns true if the task was cancelled before it completed normally. Runs on both threads. */ + /* Can be used by the asynchronous task to return prematurely. */ + bool isCancelled(void) noexcept + { + return this->m_cancelled.load(); + } + + /* Used by the calling thread to refresh the task progress, preferrably inside a loop. Returns true if the task finished. */ + /* If an exception is thrown by the asynchronous task, it will be rethrown by this function. */ + bool loopCallback(void) + { + auto status = this->getStatus(); + + /* Return immediately if the task already finished. */ + if (status == AsyncTaskStatus::FINISHED) return true; + + /* Return immediately if the task hasn't started, or if its result was already retrieved. */ + if (status == AsyncTaskStatus::PENDING || !this->m_future.valid()) return false; + + /* Get task thread status without waiting. */ + auto thread_status = this->m_future.wait_for(std::chrono::seconds(0)); + switch(thread_status) + { + case std::future_status::timeout: + /* Update progress. */ + this->onProgressUpdate(this->m_progress.load()); + return false; + case std::future_status::ready: + /* Finish task. */ + this->finish(this->m_future.get()); + return true; + default: + break; + } + + return false; + } + }; +} + +#endif /* __ASYNC_TASK_HPP__ */ diff --git a/include/core/config.h b/include/core/config.h index b97e6db..747e342 100644 --- a/include/core/config.h +++ b/include/core/config.h @@ -24,6 +24,8 @@ #ifndef __CONFIG_H__ #define __CONFIG_H__ +#include + #ifdef __cplusplus extern "C" { #endif diff --git a/include/core/http.h b/include/core/http.h new file mode 100644 index 0000000..0ee2e8e --- /dev/null +++ b/include/core/http.h @@ -0,0 +1,39 @@ +/* + * http.h + * + * Copyright (c) 2020-2021, DarkMatterCore . + * + * This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool). + * + * nxdumptool is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * nxdumptool is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#ifndef __HTTP_H__ +#define __HTTP_H__ + +#include +/* +#ifdef __cplusplus +extern "C" { +#endif + + + +#ifdef __cplusplus +} +#endif +*/ +#endif /* __HTTP_H__ */ diff --git a/include/core/nxdt_includes.h b/include/core/nxdt_includes.h index 40c7bcd..b773873 100644 --- a/include/core/nxdt_includes.h +++ b/include/core/nxdt_includes.h @@ -59,6 +59,9 @@ /* Configuration handler. */ #include "config.h" +/* HTTP requests handler. */ +#include "http.h" + /* USB Mass Storage support. */ #include "ums.h" diff --git a/include/options_tab.hpp b/include/options_tab.hpp index 6b9e95c..ab67bc7 100644 --- a/include/options_tab.hpp +++ b/include/options_tab.hpp @@ -30,8 +30,15 @@ namespace nxdt::views { class OptionsTab: public brls::List { + private: + bool display_notification = true; + brls::menu_timer_t notification_timer = 0.0f; + brls::menu_timer_ctx_entry_t notification_timer_ctx = {0}; + + void DisplayNotification(std::string str); public: OptionsTab(void); + ~OptionsTab(void); }; } diff --git a/libs/borealis b/libs/borealis index bfc32db..097693e 160000 --- a/libs/borealis +++ b/libs/borealis @@ -1 +1 @@ -Subproject commit bfc32dba9394409691a940896d2a10dfc65c10df +Subproject commit 097693eb5264941f8697902ea88a89e2efd25c11 diff --git a/romfs/i18n/en-US/options_tab.json b/romfs/i18n/en-US/options_tab.json index 483ae38..2880c01 100644 --- a/romfs/i18n/en-US/options_tab.json +++ b/romfs/i18n/en-US/options_tab.json @@ -1,4 +1,6 @@ { + "dump_options_info": "Dump options are displayed in their respective menus for convenience.", + "overclock": { "label": "Overclock", "description": "Overclocks both CPU and MEM to 1785 MHz and 1600 MHz, respectively, in order to speed up dump operations. This is considered a relatively safe action.\n\nIf the application is running under title override mode, and sys-clk is active, and a clock profile has been created for the overriden title, this setting has no effect at all.", @@ -16,5 +18,10 @@ "update_app": { "label": "Update application", "description": "Checks if an update is available in nxdumptool's GitHub repository. Requires Internet connectivity." + }, + + "notifications": { + "is_nso": "The application is running as a NSO. Unable to update.", + "already_updated": "The application has already been updated. Please reload." } } diff --git a/romfs/i18n/en-US/tasks.json b/romfs/i18n/en-US/tasks.json index e72cd01..35a1d28 100644 --- a/romfs/i18n/en-US/tasks.json +++ b/romfs/i18n/en-US/tasks.json @@ -1,10 +1,10 @@ { "notifications": { - "gamecard_status_updated": "Gamecard status updated", - "gamecard_ejected": "Gamecard ejected", - "user_titles": "User titles updated", - "ums_device": "USB Mass Storage devices updated", - "usb_host_connected": "USB host connected", - "usb_host_disconnected": "USB host disconnected" + "gamecard_status_updated": "Gamecard status updated.", + "gamecard_ejected": "Gamecard ejected.", + "user_titles": "User titles updated.", + "ums_device": "USB Mass Storage devices updated.", + "usb_host_connected": "USB host connected.", + "usb_host_disconnected": "USB host disconnected." } } diff --git a/source/core/config.c b/source/core/config.c index 30baec1..0f4fee7 100644 --- a/source/core/config.c +++ b/source/core/config.c @@ -23,8 +23,6 @@ #include "config.h" #include "title.h" -#include - #define JSON_VALIDATE_FIELD(type, name, ...) \ if (!strcmp(key, #name)) { \ if (name##_found || !configValidateJson##type(val, ##__VA_ARGS__)) goto end; \ @@ -241,8 +239,8 @@ static bool configValidateJsonRootObject(const struct json_object *obj) json_object_object_foreach(obj, key, val) { JSON_VALIDATE_FIELD(Boolean, overclock); - JSON_VALIDATE_FIELD(Integer, naming_convention, TitleNamingConvention_Full, TitleNamingConvention_IdAndVersionOnly); - JSON_VALIDATE_FIELD(Integer, dump_destination, ConfigDumpDestination_SdCard, ConfigDumpDestination_UsbHost); + JSON_VALIDATE_FIELD(Integer, naming_convention, TitleNamingConvention_Full, TitleNamingConvention_Count - 1); + JSON_VALIDATE_FIELD(Integer, dump_destination, ConfigDumpDestination_SdCard, ConfigDumpDestination_Count - 1); JSON_VALIDATE_OBJECT(GameCard, gamecard); JSON_VALIDATE_OBJECT(Nsp, nsp); JSON_VALIDATE_OBJECT(Ticket, ticket); @@ -268,7 +266,7 @@ static bool configValidateJsonGameCardObject(const struct json_object *obj) JSON_VALIDATE_FIELD(Boolean, keep_certificate); JSON_VALIDATE_FIELD(Boolean, trim_dump); JSON_VALIDATE_FIELD(Boolean, calculate_checksum); - JSON_VALIDATE_FIELD(Integer, checksum_lookup_method, ConfigChecksumLookupMethod_None, ConfigChecksumLookupMethod_NoIntro); + JSON_VALIDATE_FIELD(Integer, checksum_lookup_method, ConfigChecksumLookupMethod_None, ConfigChecksumLookupMethod_Count - 1); goto end; } diff --git a/source/core/nxdt_utils.c b/source/core/nxdt_utils.c index c260151..a7327a5 100644 --- a/source/core/nxdt_utils.c +++ b/source/core/nxdt_utils.c @@ -210,9 +210,19 @@ bool utilsInitializeResources(const int program_argc, const char **program_argv) /* TODO: only use this function while dealing with a dump process - make sure to handle power button presses as well. */ appletSetMediaPlaybackState(true); - /* Redirect stdout and stderr over network to nxlink. */ + /* Initialize socket driver. */ rc = socketInitializeDefault(); - if (R_SUCCEEDED(rc)) g_nxLinkSocketFd = nxlinkConnectToHost(true, true); + if (R_FAILED(rc)) + { + LOG_MSG("socketInitializeDefault failed! (0x%08X).", rc); + break; + } + + /* Initialize CURL. */ + curl_global_init(CURL_GLOBAL_ALL); + + /* Redirect stdout and stderr over network to nxlink. */ + g_nxLinkSocketFd = nxlinkConnectToHost(true, true); /* Update flags. */ ret = g_resourcesInit = true; @@ -227,6 +237,9 @@ void utilsCloseResources(void) { SCOPED_LOCK(&g_resourcesMutex) { + /* Cleanup CURL. */ + curl_global_cleanup(); + /* Close nxlink socket. */ if (g_nxLinkSocketFd >= 0) { @@ -234,6 +247,7 @@ void utilsCloseResources(void) g_nxLinkSocketFd = -1; } + /* Deinitialize socket driver. */ socketExit(); /* Enable screen dimming and auto sleep. */ diff --git a/source/options_tab.cpp b/source/options_tab.cpp index d5a0a65..d771c95 100644 --- a/source/options_tab.cpp +++ b/source/options_tab.cpp @@ -33,15 +33,28 @@ namespace nxdt::views this->setSpacing(this->getSpacing() / 2); this->setMarginBottom(20); + /* Information about actual dump options. */ + brls::Label *dump_options_info = new brls::Label(brls::LabelStyle::DESCRIPTION, "options_tab/dump_options_info"_i18n, true); + dump_options_info->setHorizontalAlign(NVG_ALIGN_CENTER); + this->addView(dump_options_info); + /* Overclock. */ brls::ToggleListItem *overclock = new brls::ToggleListItem("options_tab/overclock/label"_i18n, configGetBoolean("overclock"), \ "options_tab/overclock/description"_i18n, "options_tab/overclock/value_enabled"_i18n, \ "options_tab/overclock/value_disabled"_i18n); overclock->getClickEvent()->subscribe([](brls::View* view) { brls::ToggleListItem *item = static_cast(view); + + /* Get current value. */ bool value = item->getToggleState(); + + /* Change hardware clocks based on the current value. */ utilsOverclockSystem(value); + + /* Update configuration. */ configSetBoolean("overclock", value); + + brls::Logger::debug("Overclock setting changed by user."); }); this->addView(overclock); @@ -52,16 +65,57 @@ namespace nxdt::views }, static_cast(configGetInteger("naming_convention")), "options_tab/naming_convention/description"_i18n); naming_convention->getValueSelectedEvent()->subscribe([](int selected){ + /* Make sure the current value isn't out of bounds. */ if (selected < 0 || selected > static_cast(TitleNamingConvention_Count)) return; + + /* Update configuration. */ configSetInteger("naming_convention", selected); + + brls::Logger::debug("Naming convention setting changed by user."); }); this->addView(naming_convention); /* Update application. */ - if (!envIsNso()) - { - brls::ListItem *update_app = new brls::ListItem("options_tab/update_app/label"_i18n, "options_tab/update_app/description"_i18n); - this->addView(update_app); - } + brls::ListItem *update_app = new brls::ListItem("options_tab/update_app/label"_i18n, "options_tab/update_app/description"_i18n); + update_app->getClickEvent()->subscribe([this](brls::View* view) { + if (envIsNso()) + { + /* Display a notification if we're running as a NSO. */ + this->DisplayNotification("options_tab/notifications/is_nso"_i18n); + return; + } else + if (false) + { + /* Display a notification if the application has already been updated. */ + this->DisplayNotification("options_tab/notifications/already_updated"_i18n); + return; + } + + /*brls::StagedAppletFrame *staged_frame = new brls::StagedAppletFrame(); + staged_frame->setTitle("options_tab/update_app/label"_i18n); + + brls::Application::pushView(staged_frame);*/ + }); + this->addView(update_app); + } + + OptionsTab::~OptionsTab(void) + { + brls::menu_timer_kill(&(this->notification_timer)); + } + + void OptionsTab::DisplayNotification(std::string str) + { + if (str == "" || !this->display_notification) return; + + brls::Application::notify(str); + this->display_notification = false; + + this->notification_timer_ctx.duration = brls::Application::getStyle()->AnimationDuration.notificationTimeout; + this->notification_timer_ctx.cb = [this](void *userdata) { this->display_notification = true; }; + this->notification_timer_ctx.tick = [](void*){}; + this->notification_timer_ctx.userdata = nullptr; + + brls::menu_timer_start(&(this->notification_timer), &(this->notification_timer_ctx)); } } diff --git a/todo.txt b/todo.txt index f231c9a..2d3f41d 100644 --- a/todo.txt +++ b/todo.txt @@ -23,6 +23,7 @@ todo: usb: improve abi (make it rest-like?) usb: improve cancel mechanism + others: move curl (de)initialization to http.c others: use hardcoded directories, move data to hardcoded directory if the launch path isn't the right one others: dump verification via nswdb / no-intro others: update application feature