diff --git a/sphaira/CMakeLists.txt b/sphaira/CMakeLists.txt index ad0c127..8d5d001 100644 --- a/sphaira/CMakeLists.txt +++ b/sphaira/CMakeLists.txt @@ -45,6 +45,7 @@ add_executable(sphaira source/ui/menus/ghdl.cpp source/ui/menus/usb_menu.cpp source/ui/menus/ftp_menu.cpp + source/ui/menus/mtp_menu.cpp source/ui/menus/gc_menu.cpp source/ui/menus/game_menu.cpp source/ui/menus/grid_menu_base.cpp @@ -79,6 +80,7 @@ add_executable(sphaira source/hasher.cpp source/i18n.cpp source/ftpsrv_helper.cpp + source/haze_helper.cpp source/threaded_file_transfer.cpp source/minizip_helper.cpp @@ -160,7 +162,7 @@ FetchContent_Declare(ftpsrv FetchContent_Declare(libhaze GIT_REPOSITORY https://github.com/ITotalJustice/libhaze.git - GIT_TAG 8e16df2 + GIT_TAG d318432 ) FetchContent_Declare(libpulsar diff --git a/sphaira/include/evman.hpp b/sphaira/include/evman.hpp index 092bd62..7004a30 100644 --- a/sphaira/include/evman.hpp +++ b/sphaira/include/evman.hpp @@ -7,7 +7,6 @@ #include #include #include -#include #include "download.hpp" namespace sphaira::evman { @@ -24,7 +23,6 @@ struct ExitEventData { using EventData = std::variant< LaunchNroEventData, ExitEventData, - HazeCallbackData, NxlinkCallbackData, curl::DownloadEventData >; diff --git a/sphaira/include/fs.hpp b/sphaira/include/fs.hpp index e007053..727ddde 100644 --- a/sphaira/include/fs.hpp +++ b/sphaira/include/fs.hpp @@ -195,6 +195,7 @@ struct Dir { ~Dir(); Result GetEntryCount(s64* out); + Result Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf); Result ReadAll(std::vector& buf); void Close(); @@ -398,36 +399,6 @@ struct FsNative : Fs { return fsFsGetTotalSpace(&m_fs, path, out); } - // Result OpenDirectory(const FsPath& path, u32 mode, FsDir *out) { - // return fsFsOpenDirectory(&m_fs, path, mode, out); - // } - - // void DirClose(FsDir *d) { - // fsDirClose(d); - // } - - // Result DirGetEntryCount(FsDir *d, s64* out) { - // return fsDirGetEntryCount(d, out); - // } - - // Result DirGetEntryCount(const FsPath& path, u32 mode, s64* out) { - // FsDir d; - // R_TRY(OpenDirectory(path, mode, &d)); - // ON_SCOPE_EXIT(DirClose(&d)); - // return DirGetEntryCount(&d, out); - // } - - // Result DirRead(FsDir *d, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) { - // return fsDirRead(d, total_entries, max_entries, buf); - // } - - // Result DirRead(const FsPath& path, u32 mode, s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) { - // FsDir d; - // R_TRY(OpenDirectory(path, mode, &d)); - // ON_SCOPE_EXIT(DirClose(&d)); - // return DirRead(&d, total_entries, max_entries, buf); - // } - virtual bool IsFsActive() { return serviceIsActive(&m_fs.s); } diff --git a/sphaira/include/haze_helper.hpp b/sphaira/include/haze_helper.hpp new file mode 100644 index 0000000..a2af04d --- /dev/null +++ b/sphaira/include/haze_helper.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace sphaira::haze { + +bool Init(); +void Exit(); + +using OnInstallStart = std::function; +using OnInstallWrite = std::function; +using OnInstallClose = std::function; + +void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close); +void DisableInstallMode(); + +} // namespace sphaira::haze diff --git a/sphaira/include/ui/menus/mtp_menu.hpp b/sphaira/include/ui/menus/mtp_menu.hpp new file mode 100644 index 0000000..31184e5 --- /dev/null +++ b/sphaira/include/ui/menus/mtp_menu.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include "ui/menus/menu_base.hpp" +#include "yati/source/stream.hpp" + +namespace sphaira::ui::menu::mtp { + +enum class State { + // not connected. + None, + // just connected, starts the transfer. + Connected, + // set whilst transfer is in progress. + Progress, + // set when the transfer is finished. + Done, + // failed to connect. + Failed, +}; + +struct StreamFtp final : yati::source::Stream { + StreamFtp(const fs::FsPath& path, std::stop_token token); + + Result ReadChunk(void* buf, s64 size, u64* bytes_read) override; + bool Push(const void* buf, s64 size); + void Disable(); + +// private: + fs::FsPath m_path{}; + std::stop_token m_token{}; + std::vector m_buffer{}; + Mutex m_mutex{}; + bool m_active{}; + // bool m_push_exit{}; +}; + +struct Menu final : MenuBase { + Menu(u32 flags); + ~Menu(); + + auto GetShortTitle() const -> const char* override { return "MTP"; }; + void Update(Controller* controller, TouchInfo* touch) override; + void Draw(NVGcontext* vg, Theme* theme) override; + void OnFocusGained() override; + +// this should be private +// private: + std::shared_ptr m_source{}; + Thread m_thread{}; + Mutex m_mutex{}; + // the below are shared across threads, lock with the above mutex! + State m_state{State::None}; + + bool m_was_mtp_enabled{}; +}; + +} // namespace sphaira::ui::menu::mtp diff --git a/sphaira/include/usb/usbds.hpp b/sphaira/include/usb/usbds.hpp index 5d56fe8..4a742fc 100644 --- a/sphaira/include/usb/usbds.hpp +++ b/sphaira/include/usb/usbds.hpp @@ -2,6 +2,11 @@ #include "base.hpp" +// TODO: remove these when libnx pr is merged. +enum { UsbDeviceSpeed_None = 0x0 }; +enum { UsbDeviceSpeed_Low = 0x1 }; +Result usbDsGetSpeed(UsbDeviceSpeed *out); + namespace sphaira::usb { // Device Host diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 55fbc89..5fa65d4 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -19,13 +19,13 @@ #include "defines.hpp" #include "i18n.hpp" #include "ftpsrv_helper.hpp" +#include "haze_helper.hpp" #include "web.hpp" #include "swkbd.hpp" #include #include #include -#include #include #include #include @@ -400,11 +400,6 @@ void LoadThemeInternal(ThemeMeta meta, ThemeData& theme_data, int inherit_level } } -void haze_callback(const HazeCallbackData *data) { - App::NotifyFlashLed(); - evman::push(*data, false); -} - void nxlink_callback(const NxlinkCallbackData *data) { App::NotifyFlashLed(); evman::push(*data, false); @@ -460,9 +455,6 @@ void App::Loop() { } else if constexpr(std::is_same_v) { log_write("[ExitEventData] got event\n"); m_quit = true; - } else if constexpr(std::is_same_v) { - // log_write("[ExitEventData] got event\n"); - // m_quit = true; } else if constexpr(std::is_same_v) { switch (arg.type) { case NxlinkCallbackType_Connected: @@ -857,9 +849,9 @@ void App::SetMtpEnable(bool enable) { if (App::GetMtpEnable() != enable) { g_app->m_mtp_enabled.Set(enable); if (enable) { - hazeInitialize(haze_callback, 0x2C, 2); + haze::Init(); } else { - hazeExit(); + haze::Exit(); } } } @@ -1379,7 +1371,7 @@ App::App(const char* argv0) { } if (App::GetMtpEnable()) { - hazeInitialize(haze_callback, PRIO_PREEMPTIVE, 2); + haze::Init(); } if (App::GetFtpEnable()) { @@ -1948,7 +1940,7 @@ App::~App() { if (App::GetMtpEnable()) { log_write("closing mtp\n"); - hazeExit(); + haze::Exit(); } if (App::GetFtpEnable()) { diff --git a/sphaira/source/fs.cpp b/sphaira/source/fs.cpp index be0c501..b27e4ad 100644 --- a/sphaira/source/fs.cpp +++ b/sphaira/source/fs.cpp @@ -702,6 +702,47 @@ Result Dir::GetEntryCount(s64* out) { R_SUCCEED(); } +Result Dir::Read(s64 *total_entries, size_t max_entries, FsDirectoryEntry *buf) { + R_UNLESS(m_fs, Result_FsNotActive); + *total_entries = 0; + + if (m_fs->IsNative()) { + R_TRY(fsDirRead(&m_native, total_entries, max_entries, buf)); + } else { + while (auto d = readdir(m_stdio)) { + if (!std::strcmp(d->d_name, ".") || !std::strcmp(d->d_name, "..")) { + continue; + } + + FsDirectoryEntry entry{}; + + if (d->d_type == DT_DIR) { + if (!(m_mode & FsDirOpenMode_ReadDirs)) { + continue; + } + entry.type = FsDirEntryType_Dir; + } else if (d->d_type == DT_REG) { + if (!(m_mode & FsDirOpenMode_ReadFiles)) { + continue; + } + entry.type = FsDirEntryType_File; + } else { + log_write("[FS] WARNING: unknown type when reading dir: %u\n", d->d_type); + continue; + } + + std::strcpy(entry.name, d->d_name); + std::memcpy(&buf[*total_entries], &entry, sizeof(*buf)); + *total_entries = *total_entries + 1; + if (*total_entries >= max_entries) { + break; + } + } + } + + R_SUCCEED(); +} + Result Dir::ReadAll(std::vector& buf) { buf.clear(); R_UNLESS(m_fs, Result_FsNotActive); diff --git a/sphaira/source/haze_helper.cpp b/sphaira/source/haze_helper.cpp new file mode 100644 index 0000000..34a011e --- /dev/null +++ b/sphaira/source/haze_helper.cpp @@ -0,0 +1,520 @@ +#include "haze_helper.hpp" + +#include "app.hpp" +#include "fs.hpp" +#include "log.hpp" +#include "evman.hpp" +#include "i18n.hpp" + +#include +#include +#include + +namespace sphaira::haze { +namespace { + +struct InstallSharedData { + std::mutex mutex; + std::deque queued_files; + + void* user; + OnInstallStart on_start; + OnInstallWrite on_write; + OnInstallClose on_close; + + bool in_progress; + bool enabled; +}; + +constexpr int THREAD_PRIO = PRIO_PREEMPTIVE; +constexpr int THREAD_CORE = 2; +volatile bool g_should_exit = false; +bool g_is_running{false}; +std::mutex g_mutex{}; +InstallSharedData g_shared_data{}; + +const char* SUPPORTED_EXT[] = { + ".nsp", ".xci", ".nsz", ".xcz", +}; + +// ive given up with good names. +void on_thing() { + log_write("[MTP] doing on_thing\n"); + std::scoped_lock lock{g_shared_data.mutex}; + log_write("[MTP] locked on_thing\n"); + + if (!g_shared_data.in_progress) { + if (!g_shared_data.queued_files.empty()) { + log_write("[MTP] pushing new file data\n"); + if (!g_shared_data.on_start || !g_shared_data.on_start(g_shared_data.user, g_shared_data.queued_files[0].c_str())) { + g_shared_data.queued_files.clear(); + } else { + log_write("[MTP] success on new file push\n"); + g_shared_data.in_progress = true; + } + } + } +} + +struct FsProxyBase : ::haze::FileSystemProxyImpl { + FsProxyBase(const char* name, const char* display_name) : m_name{name}, m_display_name{display_name} { + + } + + auto FixPath(const char* path) const { + fs::FsPath buf; + const auto len = std::strlen(GetName()); + + if (len && !strncasecmp(path + 1, GetName(), len)) { + std::snprintf(buf, sizeof(buf), "/%s", path + 1 + len); + } else { + std::strcpy(buf, path); + } + + log_write("[FixPath] %s -> %s\n", path, buf.s); + return buf; + } + + const char* GetName() const override { + return m_name.c_str(); + } + const char* GetDisplayName() const override { + return m_display_name.c_str(); + } + +protected: + const std::string m_name; + const std::string m_display_name; +}; + +struct FsProxy final : FsProxyBase { + FsProxy(std::shared_ptr fs, const char* name, const char* display_name) : FsProxyBase{name, display_name} { + m_fs = fs; + } + + ~FsProxy() { + if (m_fs->IsNative()) { + auto fs = (fs::FsNative*)m_fs.get(); + fsFsCommit(&fs->m_fs); + } + } + + // TODO: impl this for stdio + Result GetTotalSpace(const char *path, s64 *out) override { + if (m_fs->IsNative()) { + auto fs = (fs::FsNative*)m_fs.get(); + return fsFsGetTotalSpace(&fs->m_fs, FixPath(path), out); + } + *out = 1024ULL * 1024ULL * 1024ULL * 256ULL; + R_SUCCEED(); + } + Result GetFreeSpace(const char *path, s64 *out) override { + if (m_fs->IsNative()) { + auto fs = (fs::FsNative*)m_fs.get(); + return fsFsGetFreeSpace(&fs->m_fs, FixPath(path), out); + } + *out = 1024ULL * 1024ULL * 1024ULL * 256ULL; + R_SUCCEED(); + } + Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override { + const auto rc = m_fs->GetEntryType(FixPath(path), out_entry_type); + log_write("[HAZE] GetEntryType(%s) 0x%X\n", path, rc); + return rc; + } + Result CreateFile(const char* path, s64 size, u32 option) override { + log_write("[HAZE] CreateFile(%s)\n", path); + return m_fs->CreateFile(FixPath(path), size, option); + } + Result DeleteFile(const char* path) override { + log_write("[HAZE] DeleteFile(%s)\n", path); + return m_fs->DeleteFile(FixPath(path)); + } + Result RenameFile(const char *old_path, const char *new_path) override { + log_write("[HAZE] RenameFile(%s -> %s)\n", old_path, new_path); + return m_fs->RenameFile(FixPath(old_path), FixPath(new_path)); + } + Result OpenFile(const char *path, u32 mode, FsFile *out_file) override { + log_write("[HAZE] OpenFile(%s)\n", path); + auto fptr = new fs::File(); + const auto rc = m_fs->OpenFile(FixPath(path), mode, fptr); + + if (R_SUCCEEDED(rc)) { + std::memcpy(&out_file->s, &fptr, sizeof(fptr)); + } else { + delete fptr; + } + + return rc; + } + Result GetFileSize(FsFile *file, s64 *out_size) override { + log_write("[HAZE] GetFileSize()\n"); + fs::File* f; + std::memcpy(&f, &file->s, sizeof(f)); + return f->GetSize(out_size); + } + Result SetFileSize(FsFile *file, s64 size) override { + log_write("[HAZE] SetFileSize()\n"); + fs::File* f; + std::memcpy(&f, &file->s, sizeof(f)); + return f->SetSize(size); + } + Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override { + log_write("[HAZE] ReadFile()\n"); + fs::File* f; + std::memcpy(&f, &file->s, sizeof(f)); + return f->Read(off, buf, read_size, option, out_bytes_read); + } + Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override { + log_write("[HAZE] WriteFile()\n"); + fs::File* f; + std::memcpy(&f, &file->s, sizeof(f)); + return f->Write(off, buf, write_size, option); + } + void CloseFile(FsFile *file) override { + log_write("[HAZE] CloseFile()\n"); + fs::File* f; + std::memcpy(&f, &file->s, sizeof(f)); + if (f) { + delete f; + } + std::memset(file, 0, sizeof(*file)); + } + + Result CreateDirectory(const char* path) override { + log_write("[HAZE] DeleteFile(%s)\n", path); + return m_fs->CreateDirectory(FixPath(path)); + } + Result DeleteDirectoryRecursively(const char* path) override { + log_write("[HAZE] DeleteDirectoryRecursively(%s)\n", path); + return m_fs->DeleteDirectoryRecursively(FixPath(path)); + } + Result RenameDirectory(const char *old_path, const char *new_path) override { + log_write("[HAZE] RenameDirectory(%s -> %s)\n", old_path, new_path); + return m_fs->RenameDirectory(FixPath(old_path), FixPath(new_path)); + } + Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override { + auto fptr = new fs::Dir(); + const auto rc = m_fs->OpenDirectory(FixPath(path), mode, fptr); + + if (R_SUCCEEDED(rc)) { + std::memcpy(&out_dir->s, &fptr, sizeof(fptr)); + } else { + delete fptr; + } + + log_write("[HAZE] OpenDirectory(%s) 0x%X\n", path, rc); + return rc; + } + Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override { + fs::Dir* f; + std::memcpy(&f, &d->s, sizeof(f)); + const auto rc = f->Read(out_total_entries, max_entries, buf); + log_write("[HAZE] ReadDirectory(%zd) 0x%X\n", *out_total_entries, rc); + return rc; + } + Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override { + fs::Dir* f; + std::memcpy(&f, &d->s, sizeof(f)); + const auto rc = f->GetEntryCount(out_count); + log_write("[HAZE] GetDirectoryEntryCount(%zd) 0x%X\n", *out_count, rc); + return rc; + } + void CloseDirectory(FsDir *d) override { + log_write("[HAZE] CloseDirectory()\n"); + fs::Dir* f; + std::memcpy(&f, &d->s, sizeof(f)); + if (f) { + delete f; + } + std::memset(d, 0, sizeof(*d)); + } + +private: + std::shared_ptr m_fs{}; +}; + +struct FsDevNullProxy final : FsProxyBase { + using FsProxyBase::FsProxyBase; + + Result GetTotalSpace(const char *path, s64 *out) override { + *out = 1024ULL * 1024ULL * 1024ULL * 256ULL; + R_SUCCEED(); + } + Result GetFreeSpace(const char *path, s64 *out) override { + *out = 1024ULL * 1024ULL * 1024ULL * 256ULL; + R_SUCCEED(); + } + Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override { + if (FixPath(path) == "/") { + *out_entry_type = FsDirEntryType_Dir; + R_SUCCEED(); + } else { + *out_entry_type = FsDirEntryType_File; + R_SUCCEED(); + } + } + Result CreateFile(const char* path, s64 size, u32 option) override { + R_SUCCEED(); + } + Result DeleteFile(const char* path) override { + R_SUCCEED(); + } + Result RenameFile(const char *old_path, const char *new_path) override { + R_SUCCEED(); + } + Result OpenFile(const char *path, u32 mode, FsFile *out_file) override { + R_SUCCEED(); + } + Result GetFileSize(FsFile *file, s64 *out_size) override { + *out_size = 0; + R_SUCCEED(); + } + Result SetFileSize(FsFile *file, s64 size) override { + R_SUCCEED(); + } + Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override { + *out_bytes_read = 0; + R_SUCCEED(); + } + Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override { + R_SUCCEED(); + } + void CloseFile(FsFile *file) override { + std::memset(file, 0, sizeof(*file)); + } + + Result CreateDirectory(const char* path) override { + R_SUCCEED(); + } + Result DeleteDirectoryRecursively(const char* path) override { + R_SUCCEED(); + } + Result RenameDirectory(const char *old_path, const char *new_path) override { + R_SUCCEED(); + } + Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override { + R_SUCCEED(); + } + Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override { + *out_total_entries = 0; + R_SUCCEED(); + } + Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override { + *out_count = 0; + R_SUCCEED(); + } + void CloseDirectory(FsDir *d) override { + std::memset(d, 0, sizeof(*d)); + } +}; + +struct FsInstallProxy final : FsProxyBase { + using FsProxyBase::FsProxyBase; + + Result FailedIfNotEnabled() { + std::scoped_lock lock{g_shared_data.mutex}; + if (!g_shared_data.enabled) { + App::Notify("Please launch MTP install menu before trying to install"_i18n); + R_THROW(0x1); + } + R_SUCCEED(); + } + + // TODO: impl this. + Result GetTotalSpace(const char *path, s64 *out) override { + if (App::GetApp()->m_install_sd.Get()) { + return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetTotalSpace("/", out); + } else { + return fs::FsNativeContentStorage(FsContentStorageId_User).GetTotalSpace("/", out); + } + } + Result GetFreeSpace(const char *path, s64 *out) override { + if (App::GetApp()->m_install_sd.Get()) { + return fs::FsNativeContentStorage(FsContentStorageId_SdCard).GetFreeSpace("/", out); + } else { + return fs::FsNativeContentStorage(FsContentStorageId_User).GetFreeSpace("/", out); + } + } + Result GetEntryType(const char *path, FsDirEntryType *out_entry_type) override { + if (FixPath(path) == "/") { + *out_entry_type = FsDirEntryType_Dir; + R_SUCCEED(); + } else { + *out_entry_type = FsDirEntryType_File; + R_SUCCEED(); + } + } + Result CreateFile(const char* path, s64 size, u32 option) override { + return FailedIfNotEnabled(); + } + Result DeleteFile(const char* path) override { + R_SUCCEED(); + } + Result RenameFile(const char *old_path, const char *new_path) override { + R_SUCCEED(); + } + Result OpenFile(const char *path, u32 mode, FsFile *out_file) override { + if (mode & FsOpenMode_Read) { + R_SUCCEED(); + } else { + std::scoped_lock lock{g_shared_data.mutex}; + if (!g_shared_data.enabled) { + R_THROW(0x1); + } + + const char* ext = std::strrchr(path, '.'); + if (!ext) { + R_THROW(0x1); + } + + bool found = false; + for (size_t i = 0; i < std::size(SUPPORTED_EXT); i++) { + if (!strcasecmp(ext, SUPPORTED_EXT[i])) { + found = true; + break; + } + } + + if (!found) { + R_THROW(0x1); + } + + // check if we already have this file queued. + auto it = std::ranges::find(g_shared_data.queued_files, path); + if (it != g_shared_data.queued_files.cend()) { + R_THROW(0x1); + } + + g_shared_data.queued_files.push_back(path); + } + + on_thing(); + log_write("[MTP] got file: %s\n", path); + R_SUCCEED(); + } + Result GetFileSize(FsFile *file, s64 *out_size) override { + *out_size = 0; + R_SUCCEED(); + } + Result SetFileSize(FsFile *file, s64 size) override { + R_SUCCEED(); + } + Result ReadFile(FsFile *file, s64 off, void *buf, u64 read_size, u32 option, u64 *out_bytes_read) override { + *out_bytes_read = 0; + R_SUCCEED(); + } + Result WriteFile(FsFile *file, s64 off, const void *buf, u64 write_size, u32 option) override { + std::scoped_lock lock{g_shared_data.mutex}; + if (!g_shared_data.enabled) { + R_THROW(0x1); + } + + if (!g_shared_data.on_write || !g_shared_data.on_write(g_shared_data.user, buf, write_size)) { + R_THROW(0x1); + } + + R_SUCCEED(); + } + void CloseFile(FsFile *file) override { + { + log_write("[MTP] closing file\n"); + std::scoped_lock lock{g_shared_data.mutex}; + log_write("[MTP] closing valid file\n"); + + log_write("[MTP] closing current file\n"); + if (g_shared_data.on_close) { + g_shared_data.on_close(g_shared_data.user); + } + + g_shared_data.in_progress = false; + g_shared_data.queued_files.clear(); + } + + on_thing(); + std::memset(file, 0, sizeof(*file)); + } + + Result CreateDirectory(const char* path) override { + R_SUCCEED(); + } + Result DeleteDirectoryRecursively(const char* path) override { + R_SUCCEED(); + } + Result RenameDirectory(const char *old_path, const char *new_path) override { + R_SUCCEED(); + } + Result OpenDirectory(const char *path, u32 mode, FsDir *out_dir) override { + R_SUCCEED(); + } + Result ReadDirectory(FsDir *d, s64 *out_total_entries, size_t max_entries, FsDirectoryEntry *buf) override { + *out_total_entries = 0; + R_SUCCEED(); + } + Result GetDirectoryEntryCount(FsDir *d, s64 *out_count) override { + *out_count = 0; + R_SUCCEED(); + } + void CloseDirectory(FsDir *d) override { + std::memset(d, 0, sizeof(*d)); + } +}; + +::haze::FsEntries g_fs_entries{}; + +void haze_callback(const ::haze::CallbackData *data) { + App::NotifyFlashLed(); +} + +} // namespace + +bool Init() { + std::scoped_lock lock{g_mutex}; + if (g_is_running) { + log_write("[MTP] already enabled, cannot open\n"); + return false; + } + + g_fs_entries.emplace_back(std::make_shared(std::make_shared(), "", "microSD card")); + g_fs_entries.emplace_back(std::make_shared(std::make_shared(FsImageDirectoryId_Nand), "image_nand", "Image nand")); + g_fs_entries.emplace_back(std::make_shared(std::make_shared(FsImageDirectoryId_Sd), "image_sd", "Image sd")); + g_fs_entries.emplace_back(std::make_shared("DevNull", "DevNull (Speed Test)")); + g_fs_entries.emplace_back(std::make_shared("install", "Install (NSP, XCI, NSZ, XCZ)")); + + g_should_exit = false; + if (!::haze::Initialize(haze_callback, PRIO_PREEMPTIVE, 2, g_fs_entries)) { + return false; + } + + log_write("[MTP] started\n"); + return g_is_running = true; +} + +void Exit() { + std::scoped_lock lock{g_mutex}; + if (!g_is_running) { + return; + } + + ::haze::Exit(); + g_is_running = false; + g_should_exit = true; + g_fs_entries.clear(); + + log_write("[MTP] exitied\n"); +} + +void InitInstallMode(void* user, OnInstallStart on_start, OnInstallWrite on_write, OnInstallClose on_close) { + std::scoped_lock lock{g_shared_data.mutex}; + g_shared_data.user = user; + g_shared_data.on_start = on_start; + g_shared_data.on_write = on_write; + g_shared_data.on_close = on_close; + g_shared_data.enabled = true; +} + +void DisableInstallMode() { + std::scoped_lock lock{g_shared_data.mutex}; + g_shared_data.enabled = false; +} + +} // namespace sphaira::haze diff --git a/sphaira/source/ui/menus/main_menu.cpp b/sphaira/source/ui/menus/main_menu.cpp index 9223542..b443c6c 100644 --- a/sphaira/source/ui/menus/main_menu.cpp +++ b/sphaira/source/ui/menus/main_menu.cpp @@ -13,6 +13,7 @@ #include "ui/menus/ghdl.hpp" #include "ui/menus/usb_menu.hpp" #include "ui/menus/ftp_menu.hpp" +#include "ui/menus/mtp_menu.hpp" #include "ui/menus/gc_menu.hpp" #include "ui/menus/game_menu.hpp" #include "ui/menus/save_menu.hpp" @@ -54,6 +55,7 @@ const MiscMenuEntry MISC_MENU_ENTRIES[] = { { .name = "Themezer", .title = "Themezer", .func = MiscMenuFuncGenerator, .flag = MiscMenuFlag_Shortcut }, { .name = "GitHub", .title = "GitHub", .func = MiscMenuFuncGenerator, .flag = MiscMenuFlag_Shortcut }, { .name = "FTP", .title = "FTP Install", .func = MiscMenuFuncGenerator, .flag = MiscMenuFlag_Install }, + { .name = "MTP", .title = "MTP Install", .func = MiscMenuFuncGenerator, .flag = MiscMenuFlag_Install }, { .name = "USB", .title = "USB Install", .func = MiscMenuFuncGenerator, .flag = MiscMenuFlag_Install }, { .name = "GameCard", .title = "GameCard", .func = MiscMenuFuncGenerator, .flag = MiscMenuFlag_Shortcut }, { .name = "IRS", .title = "IRS (Infrared Joycon Camera)", .func = MiscMenuFuncGenerator, .flag = MiscMenuFlag_Shortcut }, diff --git a/sphaira/source/ui/menus/mtp_menu.cpp b/sphaira/source/ui/menus/mtp_menu.cpp new file mode 100644 index 0000000..b92759e --- /dev/null +++ b/sphaira/source/ui/menus/mtp_menu.cpp @@ -0,0 +1,290 @@ +#include "ui/menus/mtp_menu.hpp" +#include "yati/yati.hpp" +#include "usb/usbds.hpp" +#include "app.hpp" +#include "defines.hpp" +#include "log.hpp" +#include "ui/nvg_util.hpp" +#include "i18n.hpp" +#include "haze_helper.hpp" +#include +#include + +namespace sphaira::ui::menu::mtp { +namespace { + +constexpr u64 MAX_BUFFER_SIZE = 1024*1024*32; +constexpr u64 SLEEPNS = 1000; +volatile bool IN_PUSH_THREAD{}; + +auto GetUsbStateStr(UsbState state) -> const char* { + switch (state) { + case UsbState_Detached: return "Detached"; + case UsbState_Attached: return "Attached"; + case UsbState_Powered: return "Powered"; + case UsbState_Default: return "Default"; + case UsbState_Address: return "Address"; + case UsbState_Configured: return "Configured"; + case UsbState_Suspended: return "Suspended"; + } + + return "Unknown"; +} + +auto GetUsbSpeedStr(UsbDeviceSpeed speed) -> const char* { + // todo: remove this cast when libnx pr is merged. + switch ((u32)speed) { + case UsbDeviceSpeed_None: return "None"; + case UsbDeviceSpeed_Low: return "USB 1.0 Low Speed"; + case UsbDeviceSpeed_Full: return "USB 1.1 Full Speed"; + case UsbDeviceSpeed_High: return "USB 2.0 High Speed"; + case UsbDeviceSpeed_Super: return "USB 3.0 Super Speed"; + } + + return "Unknown"; +} + +bool OnInstallStart(void* user, const char* path) { + auto menu = (Menu*)user; + log_write("[INSTALL] inside OnInstallStart()\n"); + + for (;;) { + mutexLock(&menu->m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex)); + + if (menu->m_state != State::Progress) { + break; + } + + if (menu->GetToken().stop_requested()) { + return false; + } + + svcSleepThread(1e+6); + } + + log_write("[INSTALL] OnInstallStart() got state: %u\n", (u8)menu->m_state); + + if (menu->m_source) { + log_write("[INSTALL] OnInstallStart() we have source\n"); + for (;;) { + mutexLock(&menu->m_source->m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&menu->m_source->m_mutex)); + + if (!IN_PUSH_THREAD) { + break; + } + + if (menu->GetToken().stop_requested()) { + return false; + } + + svcSleepThread(1e+6); + } + + log_write("[INSTALL] OnInstallStart() stopped polling source\n"); + } + + log_write("[INSTALL] OnInstallStart() doing make_shared\n"); + menu->m_source = std::make_shared(path, menu->GetToken()); + + mutexLock(&menu->m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&menu->m_mutex)); + menu->m_state = State::Connected; + log_write("[INSTALL] OnInstallStart() done make shared\n"); + + return true; +} + +bool OnInstallWrite(void* user, const void* buf, size_t size) { + auto menu = (Menu*)user; + + return menu->m_source->Push(buf, size); +} + +void OnInstallClose(void* user) { + auto menu = (Menu*)user; + menu->m_source->Disable(); +} + +} // namespace + +StreamFtp::StreamFtp(const fs::FsPath& path, std::stop_token token) { + m_path = path; + m_token = token; + m_buffer.reserve(MAX_BUFFER_SIZE); + m_active = true; +} + +Result StreamFtp::ReadChunk(void* buf, s64 size, u64* bytes_read) { + while (!m_token.stop_requested()) { + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + if (m_buffer.empty()) { + if (!m_active) { + break; + } + + svcSleepThread(SLEEPNS); + } else { + size = std::min(size, m_buffer.size()); + std::memcpy(buf, m_buffer.data(), size); + m_buffer.erase(m_buffer.begin(), m_buffer.begin() + size); + *bytes_read = size; + R_SUCCEED(); + } + } + + return 0x1; +} + +bool StreamFtp::Push(const void* buf, s64 size) { + IN_PUSH_THREAD = true; + ON_SCOPE_EXIT(IN_PUSH_THREAD = false); + + while (!m_token.stop_requested()) { + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + if (!m_active) { + break; + } + + if (m_buffer.size() + size >= MAX_BUFFER_SIZE) { + svcSleepThread(SLEEPNS); + } else { + const auto offset = m_buffer.size(); + m_buffer.resize(offset + size); + std::memcpy(m_buffer.data() + offset, buf, size); + return true; + } + } + + return false; +} + +void StreamFtp::Disable() { + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + m_active = false; +} + +Menu::Menu(u32 flags) : MenuBase{"MTP Install"_i18n, flags} { + SetAction(Button::B, Action{"Back"_i18n, [this](){ + SetPop(); + }}); + + SetAction(Button::X, Action{"Options"_i18n, [this](){ + App::DisplayInstallOptions(false); + }}); + + App::SetAutoSleepDisabled(true); + + mutexInit(&m_mutex); + m_was_mtp_enabled = App::GetMtpEnable(); + if (!m_was_mtp_enabled) { + log_write("[MTP] wasn't enabled, forcefully enabling\n"); + App::SetMtpEnable(true); + } + + haze::InitInstallMode(this, OnInstallStart, OnInstallWrite, OnInstallClose); +} + +Menu::~Menu() { + // signal for thread to exit and wait. + haze::DisableInstallMode(); + m_stop_source.request_stop(); + + if (m_source) { + m_source->Disable(); + } + + if (!m_was_mtp_enabled) { + log_write("[MTP] disabling on exit\n"); + App::SetMtpEnable(false); + } + + App::SetAutoSleepDisabled(false); + log_write("closing data!!!!\n"); +} + +void Menu::Update(Controller* controller, TouchInfo* touch) { + MenuBase::Update(controller, touch); + + static TimeStamp poll_ts; + if (poll_ts.GetSeconds() >= 1) { + poll_ts.Update(); + + UsbState state{UsbState_Detached}; + usbDsGetState(&state); + + UsbDeviceSpeed speed{(UsbDeviceSpeed)UsbDeviceSpeed_None}; + usbDsGetSpeed(&speed); + + char buf[128]; + std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s", i18n::get(GetUsbStateStr(state)).c_str(), i18n::get(GetUsbSpeedStr(speed)).c_str()); + SetSubHeading(buf); + } + + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + if (m_state == State::Connected) { + log_write("set to progress\n"); + m_state = State::Progress; + log_write("got connection\n"); + App::Push(std::make_shared(0, "Installing "_i18n, "", [this](auto pbox) -> Result { + log_write("inside progress box\n"); + const auto rc = yati::InstallFromSource(pbox, m_source, m_source->m_path); + if (R_FAILED(rc)) { + m_source->Disable(); + R_THROW(rc); + } + + R_SUCCEED(); + }, [this](Result rc){ + App::PushErrorBox(rc, "MTP install failed!"_i18n); + + + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + if (R_SUCCEEDED(rc)) { + App::Notify("MTP install success!"_i18n); + m_state = State::Done; + } else { + m_state = State::Failed; + haze::DisableInstallMode(); + } + })); + } +} + +void Menu::Draw(NVGcontext* vg, Theme* theme) { + MenuBase::Draw(vg, theme); + + mutexLock(&m_mutex); + ON_SCOPE_EXIT(mutexUnlock(&m_mutex)); + + switch (m_state) { + case State::None: + case State::Done: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Drag'n'Drop (NSP, XCI, NSZ, XCZ) to the install folder on PC"_i18n.c_str()); + break; + + case State::Connected: + case State::Progress: + break; + + case State::Failed: + gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to install via MTP, press B to exit..."_i18n.c_str()); + break; + } +} + +void Menu::OnFocusGained() { + MenuBase::OnFocusGained(); +} + +} // namespace sphaira::ui::menu::mtp diff --git a/sphaira/source/usb/usbds.cpp b/sphaira/source/usb/usbds.cpp index 454fc4d..ed0cad7 100644 --- a/sphaira/source/usb/usbds.cpp +++ b/sphaira/source/usb/usbds.cpp @@ -4,22 +4,6 @@ #include #include -namespace sphaira::usb { -namespace { - -// TODO: pr missing speed fields to libnx. -enum { UsbDeviceSpeed_None = 0x0 }; -enum { UsbDeviceSpeed_Low = 0x1 }; - -constexpr u16 DEVICE_SPEED[] = { - [UsbDeviceSpeed_None] = 0x0, - [UsbDeviceSpeed_Low] = 0x0, - [UsbDeviceSpeed_Full] = 0x40, - [UsbDeviceSpeed_High] = 0x200, - [UsbDeviceSpeed_Super] = 0x400, -}; - -// TODO: pr this to libnx. Result usbDsGetSpeed(UsbDeviceSpeed *out) { if (hosversionBefore(8,0,0)) { return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer); @@ -29,6 +13,17 @@ Result usbDsGetSpeed(UsbDeviceSpeed *out) { return serviceDispatchOut(usbDsGetServiceSession(), hosversionAtLeast(11,0,0) ? 11 : 12, *out); } +namespace sphaira::usb { +namespace { + +constexpr u16 DEVICE_SPEED[] = { + [UsbDeviceSpeed_None] = 0x0, + [UsbDeviceSpeed_Low] = 0x0, + [UsbDeviceSpeed_Full] = 0x40, + [UsbDeviceSpeed_High] = 0x200, + [UsbDeviceSpeed_Super] = 0x400, +}; + } // namespace UsbDs::~UsbDs() {