diff --git a/.gitignore b/.gitignore index 4d54a32..3c9059b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.serena # Prerequisites *.d @@ -32,5 +33,19 @@ *.exe *.out *.app -server -client + +# Switch build artifacts +*.nro +*.nso +*.pfs0 +*.nacp +*.elf +*.lst +*.map + +# CMake +build/ +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..888c756 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,104 @@ +cmake_minimum_required(VERSION 3.20) +project(NXST + LANGUAGES CXX + VERSION 0.1.0 +) + +# ── C++ standard and flags ──────────────────────────────────────────────────── +# Arch/linker/libnx flags are already injected by the Switch.cmake toolchain. +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS ON) # gnu++17 + +add_compile_options( + -fno-rtti + -fno-exceptions + -O2 + -g + -D_GNU_SOURCE=1 +) + +# Export compilation database (enables clangd / clang-tidy on the host) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# ── Sources ─────────────────────────────────────────────────────────────────── +file(GLOB_RECURSE NXST_SOURCES + src/app/*.cpp + src/domain/*.cpp + src/infra/net/*.cpp + src/infra/fs/*.cpp + src/infra/sys/*.cpp + src/service/*.cpp + src/ui/*.cpp +) + +file(GLOB_RECURSE PLUTONIUM_SOURCES + lib/Plutonium/source/*.cpp +) + +add_executable(NXST ${NXST_SOURCES} ${PLUTONIUM_SOURCES}) + +# ── Include paths ───────────────────────────────────────────────────────────── +target_include_directories(NXST PRIVATE + include + lib/Plutonium/include +) + +# ── pkg-config (uses aarch64-none-elf-pkg-config set by Switch.cmake) ───────── +find_package(PkgConfig REQUIRED) + +set(NXST_PKG_MODULES + SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer + freetype2 harfbuzz minizip libpng libjpeg libwebp + glesv2 egl glapi zlib +) +pkg_check_modules(PORTLIBS REQUIRED IMPORTED_TARGET ${NXST_PKG_MODULES}) + +target_include_directories(NXST PRIVATE ${PORTLIBS_INCLUDE_DIRS}) + +# ── Link libraries ──────────────────────────────────────────────────────────── +# Order matters for static linking: put most dependent libs first. +# libpu.a first (contains C wrappers not in Plutonium source). +# drm_nouveau, harfbuzz, freetype, z appended explicitly after pkg-config output +# to fix the freetype→harfbuzz static link order (see build notes from libnx update). +target_link_libraries(NXST PRIVATE + ${CMAKE_SOURCE_DIR}/lib/Plutonium/lib/libpu.a + PkgConfig::PORTLIBS + drm_nouveau + harfbuzz + freetype + z +) + +# ── NACP + NRO ──────────────────────────────────────────────────────────────── +set(NXST_NACP ${CMAKE_CURRENT_BINARY_DIR}/NXST.nacp) + +nx_generate_nacp( + OUTPUT ${NXST_NACP} + NAME "NXST" + AUTHOR "DragonSpirit" + VERSION "04.26.2026" +) + +nx_create_nro(NXST + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/NXST.nro + ICON ${CMAKE_SOURCE_DIR}/icon.png + NACP ${NXST_NACP} +) + +# ── Convenience targets ──────────────────────────────────────────────────────── +find_program(NXLINK_EXE nxlink HINTS ${DEVKITPRO}/tools/bin) + +if(NXLINK_EXE) + add_custom_target(send + COMMAND ${NXLINK_EXE} ${CMAKE_CURRENT_BINARY_DIR}/NXST.nro + DEPENDS NXST_nro + COMMENT "Sending NXST.nro via nxlink" + ) + + add_custom_target(debug + COMMAND ${NXLINK_EXE} -s ${CMAKE_CURRENT_BINARY_DIR}/NXST.nro + DEPENDS NXST_nro + COMMENT "Sending NXST.nro with stdio bridge" + ) +endif() diff --git a/Makefile b/Makefile index 356daa7..2f23ac4 100644 --- a/Makefile +++ b/Makefile @@ -32,8 +32,8 @@ include $(DEVKITPRO)/libnx/switch_rules #--------------------------------------------------------------------------------- TARGET := NXST BUILD := build -SOURCES := source lib/Plutonium/source -INCLUDES := include include/net lib/Plutonium/include +SOURCES := src/app src/domain src/infra/net src/infra/fs src/infra/sys src/service src/ui lib/Plutonium/source +INCLUDES := include lib/Plutonium/include EXEFS_SRC := exefs_src APP_TITLE := NXST APP_AUTHOR := DragonSpirit diff --git a/PLAN.md b/PLAN.md index 2c61461..80f5349 100644 --- a/PLAN.md +++ b/PLAN.md @@ -7,14 +7,14 @@ | 0 | Tooling & ground rules | ✅ Done | S (~2h) | `.clang-format`, `.clang-tidy`, `.editorconfig`, `.gitattributes` | | 1 | Bug fixes & dead code | ✅ Done | S (~3h) | logger rewrite, `mkdir 0777`, RU comment, dead code | | 2 | File renames + `#pragma once` | ✅ Done | S (~2h) | snake_case filenames, unify guards | -| 3 | Directory restructure | ☐ Not started | M (~1d) | `src/` + `include/nxst/` layered tree | -| 4 | Make → CMake migration | ☐ Not started | M (~1d) | devkitpro `Switch.cmake` toolchain | +| 3 | Directory restructure | ✅ Done | M (~1d) | `src/` + `include/nxst/` layered tree | +| 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain | | 5 | TransferService extraction | ☐ Not started | L (~2d) | kill globals, sever UI ↔ net coupling | | 6 | `Result` + RAII | ☐ Not started | M (~1d) | tagged union, OS handle wrappers, split `restore()` | | 7 | Documentation + license | ☐ Not started | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE | | 8 | CI | ☐ Not started | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check | -**Active phase:** Phase 3 — Directory restructure. +**Active phase:** Phase 5 — TransferService extraction. **Last updated:** 2026-04-26. Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth. diff --git a/include/client.hpp b/include/client.hpp deleted file mode 100644 index 582b8a6..0000000 --- a/include/client.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include - -int transfer_files(size_t index, AccountUid uid); -bool isClientTransferDone(); -bool isClientTransferCancelled(); -bool isClientConnectionFailed(); -bool isClientProgressKnown(); -bool isClientWorkersIdle(); -void cancelClientTransfer(); -double getClientProgress(); -std::string getClientStatusText(); -std::string getClientFailReason(); diff --git a/include/main.hpp b/include/nxst/app/main.hpp similarity index 79% rename from include/main.hpp rename to include/nxst/app/main.hpp index cdff2fc..44d004e 100644 --- a/include/main.hpp +++ b/include/nxst/app/main.hpp @@ -1,16 +1,15 @@ #pragma once -#include -#include "account.hpp" -#include "title.hpp" -#include "util.hpp" +#include +#include +#include +#include #include #include -#include "logger.hpp" +#include typedef enum { SORT_ALPHA, SORT_LAST_PLAYED, SORT_PLAY_TIME, SORT_MODES_COUNT } sort_t; inline float g_currentTime = 0; -inline AccountUid g_currentUId; inline bool g_backupScrollEnabled = 0; inline bool g_notificationLedAvailable = false; inline bool g_shouldExitNetworkLoop = false; diff --git a/include/main_application.hpp b/include/nxst/app/main_application.hpp similarity index 50% rename from include/main_application.hpp rename to include/nxst/app/main_application.hpp index 342e2a2..77a0a7c 100644 --- a/include/main_application.hpp +++ b/include/nxst/app/main_application.hpp @@ -1,21 +1,22 @@ -#pragma once - -#include -#include -#include - -namespace ui { - - class MainApplication : public pu::ui::Application { - - public: - using Application::Application; - PU_SMART_CTOR(MainApplication) - - void OnLoad() override; - - // Layout instance - UsersLayout::Ref usersLayout; - TitlesLayout::Ref titlesLayout; - }; +#pragma once + +#include +#include +#include +#include + +namespace ui { + + class MainApplication : public pu::ui::Application { + + public: + using Application::Application; + PU_SMART_CTOR(MainApplication) + + void OnLoad() override; + + UsersLayout::Ref users_layout; + TitlesLayout::Ref titles_layout; + nxst::TransferService transfer; + }; } \ No newline at end of file diff --git a/include/account.hpp b/include/nxst/domain/account.hpp similarity index 100% rename from include/account.hpp rename to include/nxst/domain/account.hpp diff --git a/include/common.hpp b/include/nxst/domain/common.hpp similarity index 100% rename from include/common.hpp rename to include/nxst/domain/common.hpp diff --git a/include/protocol.hpp b/include/nxst/domain/protocol.hpp similarity index 100% rename from include/protocol.hpp rename to include/nxst/domain/protocol.hpp diff --git a/include/title.hpp b/include/nxst/domain/title.hpp similarity index 96% rename from include/title.hpp rename to include/nxst/domain/title.hpp index acbb900..b257872 100644 --- a/include/title.hpp +++ b/include/nxst/domain/title.hpp @@ -25,9 +25,9 @@ */ #pragma once -#include "account.hpp" -#include "filesystem.hpp" -#include "io.hpp" +#include +#include +#include #include #include #include diff --git a/include/transfer_state.hpp b/include/nxst/domain/transfer_state.hpp similarity index 100% rename from include/transfer_state.hpp rename to include/nxst/domain/transfer_state.hpp diff --git a/include/util.hpp b/include/nxst/domain/util.hpp similarity index 94% rename from include/util.hpp rename to include/nxst/domain/util.hpp index b2b865d..89db34c 100644 --- a/include/util.hpp +++ b/include/nxst/domain/util.hpp @@ -25,9 +25,9 @@ */ #pragma once -#include "account.hpp" -#include "common.hpp" -#include "io.hpp" +#include +#include +#include #include #include diff --git a/include/directory.hpp b/include/nxst/infra/fs/directory.hpp similarity index 100% rename from include/directory.hpp rename to include/nxst/infra/fs/directory.hpp diff --git a/include/filesystem.hpp b/include/nxst/infra/fs/filesystem.hpp similarity index 97% rename from include/filesystem.hpp rename to include/nxst/infra/fs/filesystem.hpp index 6b93c4c..0b17f8d 100644 --- a/include/filesystem.hpp +++ b/include/nxst/infra/fs/filesystem.hpp @@ -25,7 +25,7 @@ */ #pragma once -#include "account.hpp" +#include #include namespace FileSystem { diff --git a/include/io.hpp b/include/nxst/infra/fs/io.hpp similarity index 93% rename from include/io.hpp rename to include/nxst/infra/fs/io.hpp index 37f95c6..4d542b7 100644 --- a/include/io.hpp +++ b/include/nxst/infra/fs/io.hpp @@ -25,10 +25,10 @@ */ #pragma once -#include "account.hpp" -#include "directory.hpp" -#include "title.hpp" -#include "util.hpp" +#include +#include +#include +#include #include #include #include diff --git a/include/net/socket.hpp b/include/nxst/infra/net/socket.hpp similarity index 100% rename from include/net/socket.hpp rename to include/nxst/infra/net/socket.hpp diff --git a/include/nxst/infra/net/transfer_receiver.hpp b/include/nxst/infra/net/transfer_receiver.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/include/nxst/infra/net/transfer_receiver.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/include/nxst/infra/net/transfer_sender.hpp b/include/nxst/infra/net/transfer_sender.hpp new file mode 100644 index 0000000..6f70f09 --- /dev/null +++ b/include/nxst/infra/net/transfer_sender.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/include/logger.hpp b/include/nxst/infra/sys/logger.hpp similarity index 100% rename from include/logger.hpp rename to include/nxst/infra/sys/logger.hpp diff --git a/include/nxst/service/transfer_service.hpp b/include/nxst/service/transfer_service.hpp new file mode 100644 index 0000000..677b206 --- /dev/null +++ b/include/nxst/service/transfer_service.hpp @@ -0,0 +1,76 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace nxst { + +class TransferService { +public: + int startSend(size_t title_index, AccountUid uid); + void cancelSend(); + + bool isSendDone() const { return sender_state.done.load(); } + bool isSendCancelled() const { return sender_state.cancelled.load(); } + bool isSendConnectionFailed() const { return sender_state.connection_failed.load(); } + bool isSendProgressKnown() const { return sender_state.bytes_total.load() > 0; } + bool isSendWorkersIdle() const { return !sender_active.load(); } + double sendProgress() const { return sender_state.progress(); } + std::string sendStatusText() const { return sender_state.getStatus(); } + std::string sendFailReason() const { return sender_state.fail_reason; } + + int startReceive(size_t title_index, AccountUid uid, std::string title_name); + void cancelReceive(); + + bool isReceiveDone() const { return receiver_state.done.load(); } + bool isReceiveCancelled() const { return receiver_state.cancelled.load(); } + bool isReceiveWorkersIdle() const { + return !receiver_accept_active.load() && !receiver_broadcast_active.load(); + } + double receiveProgress() const { return receiver_state.progress(); } + std::string receiveStatusText() const { return receiver_state.getStatus(); } + bool restoreSucceeded() const { return restore_ok; } + std::string restoreError() const { return restore_error; } + +private: + // Sender + TransferState sender_state; + std::atomic sender_udp_sock{-1}; + std::atomic sender_tcp_sock{-1}; + std::atomic sender_active{false}; + + // Receiver + TransferState receiver_state; + std::atomic receiver_client_sock{-1}; + std::atomic receiver_listen_sock{-1}; + std::atomic receiver_bcast_sock{-1}; + std::atomic receiver_accept_active{false}; + std::atomic receiver_broadcast_active{false}; + pthread_t receiver_bcast_thread{}; + + // Stored at startReceive, read after network transfer completes + size_t restore_title_index{0}; + AccountUid restore_uid{}; + std::string restore_title_name; + bool restore_ok{false}; + std::string restore_error; + + // Sender thread + struct SenderArgs { TransferService* svc; size_t title_index; AccountUid uid; }; + static void* senderEntry(void* arg); + void runSender(size_t title_index, AccountUid uid); + void failSend(const std::string& reason); + int findServer(char* out_ip); + + // Receiver threads + struct AcceptArgs { TransferService* svc; int server_fd; }; + static void* broadcastEntry(void* arg); + static void* acceptEntry(void* arg); + void runBroadcast(); + void runAccept(int server_fd); + std::string replaceUsername(const std::string& file_path) const; +}; + +} // namespace nxst diff --git a/include/ui/card.hpp b/include/nxst/ui/card.hpp similarity index 93% rename from include/ui/card.hpp rename to include/nxst/ui/card.hpp index 5861a32..ddc9517 100644 --- a/include/ui/card.hpp +++ b/include/nxst/ui/card.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include namespace ui { diff --git a/include/const.h b/include/nxst/ui/const.h similarity index 78% rename from include/const.h rename to include/nxst/ui/const.h index 935f045..6d9908f 100644 --- a/include/const.h +++ b/include/nxst/ui/const.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #define COLOR(hex) pu::ui::Color::FromHex(hex) #define BACKGROUND_COLOR theme::color::BgBase diff --git a/include/ui/header_bar.hpp b/include/nxst/ui/header_bar.hpp similarity index 96% rename from include/ui/header_bar.hpp rename to include/nxst/ui/header_bar.hpp index 54f5c2f..fbb19ae 100644 --- a/include/ui/header_bar.hpp +++ b/include/nxst/ui/header_bar.hpp @@ -1,8 +1,8 @@ #pragma once #include -#include -#include -#include +#include +#include +#include namespace ui { diff --git a/include/ui/hint_bar.hpp b/include/nxst/ui/hint_bar.hpp similarity index 98% rename from include/ui/hint_bar.hpp rename to include/nxst/ui/hint_bar.hpp index f67769c..0cb5ab5 100644 --- a/include/ui/hint_bar.hpp +++ b/include/nxst/ui/hint_bar.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include diff --git a/include/theme.hpp b/include/nxst/ui/theme.hpp similarity index 100% rename from include/theme.hpp rename to include/nxst/ui/theme.hpp diff --git a/include/titles_layout.hpp b/include/nxst/ui/titles_layout.hpp similarity index 86% rename from include/titles_layout.hpp rename to include/nxst/ui/titles_layout.hpp index 1903589..15a220e 100644 --- a/include/titles_layout.hpp +++ b/include/nxst/ui/titles_layout.hpp @@ -1,12 +1,13 @@ +#pragma once #include -#include -#include -#include +#include +#include +#include #include #include #include -#include -#include +#include +#include namespace ui { @@ -33,6 +34,7 @@ namespace ui { pu::ui::elm::TextBlock::Ref emptyText; pu::ui::elm::TextBlock::Ref emptySub; + AccountUid current_uid{}; TitlesFocus focus = TitlesFocus::List; TitlesAction action = TitlesAction::Transfer; int lockedListIndex = 0; @@ -46,7 +48,7 @@ namespace ui { public: TitlesLayout(); - void InitTitles(); + void InitTitles(AccountUid uid); void LockInput() { m_inputLocked = true; } void UnlockInput() { m_inputLocked = false; } diff --git a/include/transfer_overlay.hpp b/include/nxst/ui/transfer_overlay.hpp similarity index 98% rename from include/transfer_overlay.hpp rename to include/nxst/ui/transfer_overlay.hpp index ddfe16b..a7b02b5 100644 --- a/include/transfer_overlay.hpp +++ b/include/nxst/ui/transfer_overlay.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include -#include +#include +#include namespace ui { diff --git a/include/ui/ui_context.hpp b/include/nxst/ui/ui_context.hpp similarity index 85% rename from include/ui/ui_context.hpp rename to include/nxst/ui/ui_context.hpp index 3d444e8..8f1b6d7 100644 --- a/include/ui/ui_context.hpp +++ b/include/nxst/ui/ui_context.hpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include namespace ui { struct UiContext { diff --git a/include/users_layout.hpp b/include/nxst/ui/users_layout.hpp similarity index 85% rename from include/users_layout.hpp rename to include/nxst/ui/users_layout.hpp index 2529ca8..ca46b94 100644 --- a/include/users_layout.hpp +++ b/include/nxst/ui/users_layout.hpp @@ -1,7 +1,7 @@ #include -#include -#include -#include +#include +#include +#include #include namespace ui { diff --git a/include/server.hpp b/include/server.hpp deleted file mode 100644 index 5e917e0..0000000 --- a/include/server.hpp +++ /dev/null @@ -1,8 +0,0 @@ -#include -int startSendingThread(); -bool isServerTransferDone(); -bool isServerTransferCancelled(); -bool isServerWorkersIdle(); -void cancelServerTransfer(); -double getServerProgress(); -std::string getServerStatusText(); \ No newline at end of file diff --git a/source/client.cpp b/source/client.cpp deleted file mode 100644 index f84577c..0000000 --- a/source/client.cpp +++ /dev/null @@ -1,249 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __SWITCH__ -#include -#include -#include -#endif - -#include -#include - -namespace fs = std::filesystem; -using path = fs::path; - -static TransferState g_client_state; -static std::atomic g_client_udp_sock{-1}; -static std::atomic g_client_tcp_sock{-1}; -static std::atomic g_client_thread_active{false}; - -bool isClientTransferDone() { return g_client_state.done.load(); } -bool isClientTransferCancelled() { return g_client_state.cancelled.load(); } -bool isClientConnectionFailed() { return g_client_state.connection_failed.load(); } -bool isClientProgressKnown() { return g_client_state.bytes_total.load() > 0; } -bool isClientWorkersIdle() { return !g_client_thread_active.load(); } -double getClientProgress() { return g_client_state.progress(); } -std::string getClientStatusText() { return g_client_state.getStatus(); } -std::string getClientFailReason() { return g_client_state.fail_reason; } - -void cancelClientTransfer() { - g_client_state.cancelled.store(true); - int udp = g_client_udp_sock.exchange(-1); - if (udp >= 0) { - shutdown(udp, SHUT_RDWR); - close(udp); - } - int tcp = g_client_tcp_sock.exchange(-1); - if (tcp >= 0) { - shutdown(tcp, SHUT_RDWR); - close(tcp); - } -} - -static bool send_all(int sock, const void* buf, size_t len) { - size_t sent = 0; - while (sent < len) { - ssize_t n = send(sock, static_cast(buf) + sent, len - sent, 0); - if (n <= 0) return false; - sent += n; - } - return true; -} - -static bool sendFile(int sock, const fs::path& filepath) { - std::ifstream infile(filepath, std::ios::binary | std::ios::ate); - if (!infile.is_open()) { - std::cerr << "File not found: " << filepath << std::endl; - return false; - } - - uint32_t filename_len = (uint32_t)filepath.string().size(); - uint64_t file_size = (uint64_t)infile.tellg(); - infile.seekg(0, std::ios::beg); - - std::cout << "Sending: " << filepath << " (" << file_size << " bytes)" << std::endl; - - if (!send_all(sock, &filename_len, sizeof(filename_len))) return false; - if (!send_all(sock, filepath.c_str(), filename_len)) return false; - if (!send_all(sock, &file_size, sizeof(file_size))) return false; - - std::vector buffer(proto::BUF_SIZE); - uint64_t remaining = file_size; - while (remaining > 0) { - size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE); - infile.read(buffer.data(), (std::streamsize)to_read); - std::streamsize count = infile.gcount(); - if (count <= 0) break; - if (!send_all(sock, buffer.data(), (size_t)count)) { - std::cerr << "Failed to send data for: " << filepath << std::endl; - return false; - } - g_client_state.bytes_done.fetch_add((uint64_t)count); - remaining -= (uint64_t)count; - } - - return true; -} - -struct ThreadArgs { size_t index; AccountUid uid; }; - -static int find_server(char* server_ip); - -static void fail_connect(const std::string& reason) { - g_client_state.fail_reason = reason; - g_client_state.connection_failed.store(true); - g_client_state.done.store(true); -} - -static void* discovery_and_send_thread(void* arg) { - g_client_thread_active.store(true); - ThreadArgs* targs = static_cast(arg); - size_t index = targs->index; - AccountUid uid = targs->uid; - delete targs; - - auto finish = [](void*) { - g_client_state.done.store(true); - g_client_thread_active.store(false); - return (void*)nullptr; - }; - - char server_ip[INET_ADDRSTRLEN]; - if (find_server(server_ip) != 0) { - if (!g_client_state.cancelled.load()) - fail_connect("No receiver found.\nMake sure the other Switch is in Receive mode."); - return finish(nullptr); - } - if (g_client_state.cancelled.load()) return finish(nullptr); - - g_client_state.setStatus("Creating backup..."); - auto backupResult = io::backup(index, uid); - if (!std::get<0>(backupResult)) { - fail_connect("Failed to create backup:\n" + std::get<2>(backupResult)); - return finish(nullptr); - } - fs::path directory = std::get<2>(backupResult); - - if (g_client_state.cancelled.load()) return finish(nullptr); - - g_client_state.setStatus("Connecting..."); - int tcp_fd = socket(AF_INET, SOCK_STREAM, 0); - if (tcp_fd < 0) { fail_connect("Failed to open socket."); return finish(nullptr); } - g_client_tcp_sock.store(tcp_fd); - - auto release_tcp = [&]() { - int owned = g_client_tcp_sock.exchange(-1); - if (owned == tcp_fd) close(tcp_fd); - }; - - sockaddr_in serv{}; - serv.sin_family = AF_INET; - serv.sin_port = htons(proto::TCP_PORT); - if (inet_pton(AF_INET, server_ip, &serv.sin_addr) <= 0 || - connect(tcp_fd, (sockaddr*)&serv, sizeof(serv)) < 0) { - if (!g_client_state.cancelled.load()) - fail_connect("Failed to connect to receiver."); - release_tcp(); - return finish(nullptr); - } - - uint64_t total = 0; - for (const auto& entry : fs::recursive_directory_iterator(directory)) - if (fs::is_regular_file(entry.path())) - total += fs::file_size(entry.path()); - g_client_state.bytes_total.store(total); - - for (const auto& entry : fs::recursive_directory_iterator(directory)) { - if (g_client_state.cancelled.load()) break; - const path& p = entry.path(); - if (fs::is_regular_file(p)) { - g_client_state.setStatus(p.filename().string()); - if (!sendFile(tcp_fd, p)) break; - } - } - - uint32_t sentinel = proto::EOF_SENTINEL; - send_all(tcp_fd, &sentinel, sizeof(sentinel)); - - release_tcp(); - g_client_state.setStatus(""); - return finish(nullptr); -} - -static int find_server(char* server_ip) { - int udp_fd = socket(AF_INET, SOCK_DGRAM, 0); - if (udp_fd < 0) return -1; - g_client_udp_sock.store(udp_fd); - - auto release_udp = [&]() { - int owned = g_client_udp_sock.exchange(-1); - if (owned == udp_fd) close(udp_fd); - }; - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_port = htons(proto::MULTICAST_PORT); - addr.sin_addr.s_addr = inet_addr(proto::MULTICAST_GROUP); - - const char* msg = "DISCOVER_SERVER"; - if (sendto(udp_fd, msg, strlen(msg), 0, (sockaddr*)&addr, sizeof(addr)) < 0) { - release_udp(); - return -1; - } - - // Poll in 100ms slices so we can react to cancel within 100ms - auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); - while (std::chrono::steady_clock::now() < deadline) { - if (g_client_state.cancelled.load()) { - release_udp(); - return -1; - } - struct timeval tv{0, 100000}; - fd_set fds; - FD_ZERO(&fds); - FD_SET(udp_fd, &fds); - if (select(udp_fd + 1, &fds, nullptr, nullptr, &tv) > 0) { - sockaddr_in from{}; - socklen_t fromlen = sizeof(from); - char buf[256]; - ssize_t n = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen); - if (n > 0) { - buf[n] = '\0'; - if (strcmp(buf, "SERVER_HERE") == 0) { - inet_ntop(AF_INET, &from.sin_addr, server_ip, INET_ADDRSTRLEN); - release_udp(); - return 0; - } - } - } - } - - release_udp(); - return -1; -} - -int transfer_files(size_t index, AccountUid uid) { - g_client_state.reset(); - g_client_state.setStatus("Searching for receiver..."); - - ThreadArgs* arg = new ThreadArgs{index, uid}; - pthread_t thread; - if (pthread_create(&thread, nullptr, discovery_and_send_thread, arg) != 0) { - delete arg; - return -1; - } - pthread_detach(thread); - return 0; -} diff --git a/source/main_application.cpp b/source/main_application.cpp deleted file mode 100644 index d9e93bc..0000000 --- a/source/main_application.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include -#include -#include -#include -#include - -namespace ui { - MainApplication *mainApp; - - void MainApplication::OnLoad() { - mainApp = this; - this->usersLayout = UsersLayout::New(); - this->titlesLayout = TitlesLayout::New(); - this->usersLayout->SetOnInput( - std::bind(&UsersLayout::onInput, this->usersLayout, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); - this->titlesLayout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titlesLayout,std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4)); - this->LoadLayout(this->usersLayout); - } -} \ No newline at end of file diff --git a/source/server.cpp b/source/server.cpp deleted file mode 100644 index 9bc3a7b..0000000 --- a/source/server.cpp +++ /dev/null @@ -1,345 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __SWITCH__ -#include -#include -#include -#endif - -#include -#include -#include - -static TransferState g_server_state; -static std::atomic g_server_client_sock{-1}; -static std::atomic g_server_listen_sock{-1}; -static std::atomic g_broadcast_sock{-1}; -static std::atomic g_accept_thread_active{false}; -static std::atomic g_broadcast_thread_active{false}; -static pthread_t g_broadcast_thread{}; - -bool isServerTransferDone() { return g_server_state.done.load(); } -bool isServerTransferCancelled() { return g_server_state.cancelled.load(); } -bool isServerWorkersIdle() { return !g_accept_thread_active.load() && !g_broadcast_thread_active.load(); } -double getServerProgress() { return g_server_state.progress(); } -std::string getServerStatusText() { return g_server_state.getStatus(); } - -void cancelServerTransfer() { - g_server_state.cancelled.store(true); - int sock = g_server_client_sock.exchange(-1); - if (sock >= 0) { - shutdown(sock, SHUT_RDWR); - close(sock); - } - int lsock = g_server_listen_sock.exchange(-1); - if (lsock >= 0) { - shutdown(lsock, SHUT_RDWR); - close(lsock); - } - int bsock = g_broadcast_sock.exchange(-1); - if (bsock >= 0) { - shutdown(bsock, SHUT_RDWR); - close(bsock); - } - if (g_broadcast_thread_active.load()) { - pthread_cancel(g_broadcast_thread); - } -} - -#ifdef __SWITCH__ -static std::string replaceUsername(const std::string& path) { - std::string username = StringUtils::removeNotAscii( - StringUtils::removeAccents(Account::username(g_currentUId))); - size_t lastSlash = path.rfind('/'); - if (lastSlash == std::string::npos) return path; - size_t prevSlash = path.rfind('/', lastSlash - 1); - if (prevSlash == std::string::npos) - return username + path.substr(lastSlash); - return path.substr(0, prevSlash + 1) + username + path.substr(lastSlash); -} -#endif - -static bool recv_all(int sock, void* buf, size_t len) { - size_t received = 0; - while (received < len) { - ssize_t n = read(sock, static_cast(buf) + received, len - received); - if (n <= 0) return false; - received += n; - } - return true; -} - -static void mkdirs(const std::string& path) { - for (size_t i = 1; i < path.size(); i++) { - if (path[i] == '/') { - std::string component = path.substr(0, i); - mkdir(component.c_str(), 0777); - } - } - mkdir(path.c_str(), 0777); -} - -static void receive_file(int sock, const std::string& relative_path, uint64_t file_size) { - std::cout << "Receiving: " << relative_path << " (" << file_size << " bytes)" << std::endl; - - size_t last_slash = relative_path.rfind('/'); - if (last_slash != std::string::npos) { - std::string dir = relative_path.substr(0, last_slash); - if (!dir.empty()) mkdirs(dir); - } - - FILE* outfile = fopen(relative_path.c_str(), "wb"); - if (!outfile) { - std::cerr << "Failed to open for writing: " << relative_path - << " errno=" << errno << std::endl; - // Drain so sender doesn't hang - std::vector drain(proto::BUF_SIZE); - uint64_t remaining = file_size; - while (remaining > 0) { - size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE); - ssize_t n = read(sock, drain.data(), to_read); - if (n <= 0) break; - remaining -= (uint64_t)n; - } - return; - } - - g_server_state.bytes_total.store(file_size); - g_server_state.bytes_done.store(0); - - std::vector buffer(proto::BUF_SIZE); - uint64_t total_received = 0; - while (total_received < file_size) { - size_t to_read = (size_t)std::min(file_size - total_received, (uint64_t)proto::BUF_SIZE); - ssize_t n = read(sock, buffer.data(), to_read); - if (n <= 0) { - std::cerr << "Read error receiving: " << relative_path << std::endl; - break; - } - fwrite(buffer.data(), 1, (size_t)n, outfile); - total_received += (uint64_t)n; - g_server_state.bytes_done.store(total_received); - } - - fclose(outfile); - std::cout << "Received: " << relative_path << std::endl; -} - -static void* handle_client(void* socket_desc) { - int client_socket = *(int*)socket_desc; - delete static_cast(socket_desc); - - while (true) { - uint32_t filename_len = 0; - if (!recv_all(client_socket, &filename_len, sizeof(filename_len))) - break; - - if (filename_len == proto::EOF_SENTINEL) { - std::cout << "End of transfer." << std::endl; - break; - } - - if (filename_len > proto::MAX_FILENAME) { - std::cerr << "filename_len=" << filename_len << " exceeds MAX_FILENAME, aborting." << std::endl; - break; - } - - std::vector filename(filename_len + 1, '\0'); - if (!recv_all(client_socket, filename.data(), filename_len)) { - std::cerr << "Short read on filename, aborting." << std::endl; - break; - } - std::string filename_str(filename.data(), filename_len); - -#ifdef __SWITCH__ - filename_str = replaceUsername(filename_str); -#endif - - { - size_t sl = filename_str.rfind('/'); - g_server_state.setStatus( - sl != std::string::npos ? filename_str.substr(sl + 1) : filename_str); - } - - uint64_t file_size = 0; - if (!recv_all(client_socket, &file_size, sizeof(file_size))) { - std::cerr << "Short read on file_size, aborting." << std::endl; - break; - } - - receive_file(client_socket, filename_str, file_size); - } - - int owned_client = g_server_client_sock.exchange(-1); - if (owned_client == client_socket) { - close(client_socket); - } - return nullptr; -} - -struct AcceptArgs { int server_fd; }; - -static void* accept_and_handle(void* arg) { - g_accept_thread_active.store(true); - int server_fd = static_cast(arg)->server_fd; - delete static_cast(arg); - - g_server_listen_sock.store(server_fd); - sockaddr_in client_addr{}; - socklen_t client_len = sizeof(client_addr); - int client_socket = accept(server_fd, (sockaddr*)&client_addr, &client_len); - int owned_listen = g_server_listen_sock.exchange(-1); - if (owned_listen == server_fd) { - close(server_fd); - } - - if (client_socket >= 0) { - g_server_client_sock.store(client_socket); - int* pclient = new (std::nothrow) int(client_socket); - if (pclient) { - handle_client(pclient); - } else { - close(client_socket); - } - } - - g_server_state.done.store(true); - g_accept_thread_active.store(false); - return nullptr; -} - -static void* broadcast_listener(void* arg) { - g_broadcast_thread_active.store(true); - pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr); - pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr); - int udp = socket(AF_INET, SOCK_DGRAM, 0); - if (udp < 0) { - perror("broadcast_listener: socket"); - g_broadcast_thread_active.store(false); - return nullptr; - } - - g_broadcast_sock.store(udp); - - struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit - setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_ANY); - addr.sin_port = htons(proto::MULTICAST_PORT); - - if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) { - perror("broadcast_listener: bind"); - int owned = g_broadcast_sock.exchange(-1); - if (owned == udp) close(udp); - g_broadcast_thread_active.store(false); - return nullptr; - } - - ip_mreq group{}; - group.imr_multiaddr.s_addr = inet_addr(proto::MULTICAST_GROUP); - group.imr_interface.s_addr = htonl(INADDR_ANY); - if (setsockopt(udp, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) { - perror("broadcast_listener: setsockopt"); - int owned = g_broadcast_sock.exchange(-1); - if (owned == udp) close(udp); - g_broadcast_thread_active.store(false); - return nullptr; - } - - std::cout << "Broadcast listener started" << std::endl; - char buf[256]; - sockaddr_in from{}; - socklen_t fromlen = sizeof(from); - - while (true) { - ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen); - if (n < 0) { - if (g_server_state.cancelled.load()) break; - continue; - } - buf[n] = '\0'; - if (strcmp(buf, "DISCOVER_SERVER") == 0) { - const char* reply = "SERVER_HERE"; - sendto(udp, reply, strlen(reply), 0, (sockaddr*)&from, fromlen); - std::cout << "Discovery replied." << std::endl; - break; - } - } - - int owned = g_broadcast_sock.exchange(-1); - if (owned == udp) close(udp); - g_broadcast_thread_active.store(false); - return nullptr; -} - -int startSendingThread() { - g_server_state.reset(); - g_server_state.setStatus("Waiting for connection..."); - - pthread_t broadcast_thread; - if (pthread_create(&broadcast_thread, nullptr, broadcast_listener, nullptr) != 0) { - perror("startSendingThread: broadcast thread"); - return 1; - } - g_broadcast_thread = broadcast_thread; - pthread_detach(broadcast_thread); - - Socket server(socket(AF_INET, SOCK_STREAM, 0)); - if (!server.valid()) { - perror("startSendingThread: socket"); - cancelServerTransfer(); - return 1; - } - - int yes = 1; - setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); - - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = INADDR_ANY; - addr.sin_port = htons(proto::TCP_PORT); - - if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0) { - perror("startSendingThread: bind"); - cancelServerTransfer(); - return 1; - } - if (listen(server, 3) < 0) { - perror("startSendingThread: listen"); - cancelServerTransfer(); - return 1; - } - - AcceptArgs* acc_args = new AcceptArgs{server.fd}; - pthread_t accept_thread; - if (pthread_create(&accept_thread, nullptr, accept_and_handle, acc_args) != 0) { - delete acc_args; - cancelServerTransfer(); - return 1; - } - pthread_detach(accept_thread); - server.release(); // accepted by accept_and_handle - return 0; -} - -#ifndef __SWITCH__ -int main() { - if (startSendingThread() != 0) return 1; - while (!isServerTransferDone()) usleep(16000); - return 0; -} -#endif diff --git a/source/main.cpp b/src/app/main.cpp similarity index 78% rename from source/main.cpp rename to src/app/main.cpp index 9eb33dd..89ac179 100644 --- a/source/main.cpp +++ b/src/app/main.cpp @@ -1,78 +1,82 @@ -#include -#include "util.hpp" -#include "main.hpp" -#include -#include -#include - -static int nxlink_sock = -1; - -extern "C" void userAppInit() { - appletInitialize(); - hidInitialize(); - nsInitialize(); - setsysInitialize(); - setInitialize(); - accountInitialize(AccountServiceType_Administrator); - pmshellInitialize(); - socketInitializeDefault(); - pdmqryInitialize(); - nxlink_sock = nxlinkStdio(); - printf("userAppInit\n"); -} - -extern "C" void userAppExit() { - cancelServerTransfer(); - cancelClientTransfer(); - for (int i = 0; i < 150 && (!isServerWorkersIdle() || !isClientWorkersIdle()); i++) { - usleep(10000); - } - if (nxlink_sock != -1) { - close(nxlink_sock); - } - appletExit(); - hidExit(); - nsExit(); - setsysExit(); - setExit(); - accountExit(); - pmshellExit(); - socketExit(); - pdmqryExit(); -} - -// Main entrypoint -int main() { - Result res = servicesInit(); - if (R_FAILED(res)) { - servicesExit(); - exit(res); - } - - printf("main"); - - // First create our renderer, where one can customize SDL or other stuff's - // initialization. - auto renderer_opts = pu::ui::render::RendererInitOptions( - SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags); - renderer_opts.UseImage(pu::ui::render::IMGAllFlags); - renderer_opts.UseAudio(pu::ui::render::MixerAllFlags); - renderer_opts.UseTTF(); - renderer_opts.SetExtraDefaultFontSize(theme::type::Caption); - renderer_opts.SetExtraDefaultFontSize(theme::type::Label); - renderer_opts.SetExtraDefaultFontSize(theme::type::Body); - renderer_opts.SetExtraDefaultFontSize(theme::type::Title); - renderer_opts.SetExtraDefaultFontSize(theme::type::Display); - - auto renderer = pu::ui::render::Renderer::New(renderer_opts); - - // Create our main application from the renderer - auto main = ui::MainApplication::New(renderer); - - main->Prepare(); - - main->Show(); - - servicesExit(); - return 0; -} +#include +#include +#include +#include + +namespace ui { extern MainApplication* mainApp; } + +static int nxlink_sock = -1; + +extern "C" void userAppInit() { + appletInitialize(); + hidInitialize(); + nsInitialize(); + setsysInitialize(); + setInitialize(); + accountInitialize(AccountServiceType_Administrator); + pmshellInitialize(); + socketInitializeDefault(); + pdmqryInitialize(); + nxlink_sock = nxlinkStdio(); + printf("userAppInit\n"); +} + +extern "C" void userAppExit() { + if (ui::mainApp) { + ui::mainApp->transfer.cancelReceive(); + ui::mainApp->transfer.cancelSend(); + for (int i = 0; i < 150 && + (!ui::mainApp->transfer.isReceiveWorkersIdle() || + !ui::mainApp->transfer.isSendWorkersIdle()); i++) { + usleep(10000); + } + } + if (nxlink_sock != -1) { + close(nxlink_sock); + } + appletExit(); + hidExit(); + nsExit(); + setsysExit(); + setExit(); + accountExit(); + pmshellExit(); + socketExit(); + pdmqryExit(); +} + +// Main entrypoint +int main() { + Result res = servicesInit(); + if (R_FAILED(res)) { + servicesExit(); + exit(res); + } + + printf("main"); + + // First create our renderer, where one can customize SDL or other stuff's + // initialization. + auto renderer_opts = pu::ui::render::RendererInitOptions( + SDL_INIT_EVERYTHING, pu::ui::render::RendererHardwareFlags); + renderer_opts.UseImage(pu::ui::render::IMGAllFlags); + renderer_opts.UseAudio(pu::ui::render::MixerAllFlags); + renderer_opts.UseTTF(); + renderer_opts.SetExtraDefaultFontSize(theme::type::Caption); + renderer_opts.SetExtraDefaultFontSize(theme::type::Label); + renderer_opts.SetExtraDefaultFontSize(theme::type::Body); + renderer_opts.SetExtraDefaultFontSize(theme::type::Title); + renderer_opts.SetExtraDefaultFontSize(theme::type::Display); + + auto renderer = pu::ui::render::Renderer::New(renderer_opts); + + // Create our main application from the renderer + auto main = ui::MainApplication::New(renderer); + + main->Prepare(); + + main->Show(); + + servicesExit(); + return 0; +} diff --git a/src/app/main_application.cpp b/src/app/main_application.cpp new file mode 100644 index 0000000..a80be72 --- /dev/null +++ b/src/app/main_application.cpp @@ -0,0 +1,21 @@ +#include +#include +#include +#include +#include + +namespace ui { + MainApplication *mainApp; + + void MainApplication::OnLoad() { + mainApp = this; + this->users_layout = UsersLayout::New(); + this->titles_layout = TitlesLayout::New(); + this->users_layout->SetOnInput( + std::bind(&UsersLayout::onInput, this->users_layout, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->titles_layout->SetOnInput(std::bind(&TitlesLayout::onInput, this->titles_layout, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4)); + this->LoadLayout(this->users_layout); + } +} \ No newline at end of file diff --git a/source/account.cpp b/src/domain/account.cpp similarity index 98% rename from source/account.cpp rename to src/domain/account.cpp index 12d3967..e92e37f 100644 --- a/source/account.cpp +++ b/src/domain/account.cpp @@ -24,8 +24,7 @@ * reasonable ways as different from the original version. */ -#include "account.hpp" -#include +#include #include #include @@ -138,5 +137,5 @@ AccountUid Account::selectAccount(void) return uid; } - return g_currentUId; + return AccountUid{}; } \ No newline at end of file diff --git a/source/common.cpp b/src/domain/common.cpp similarity index 99% rename from source/common.cpp rename to src/domain/common.cpp index dbd2cea..e5ddf47 100644 --- a/source/common.cpp +++ b/src/domain/common.cpp @@ -24,7 +24,7 @@ * reasonable ways as different from the original version. */ -#include "common.hpp" +#include std::string DateTime::timeStr(void) { diff --git a/source/title.cpp b/src/domain/title.cpp similarity index 99% rename from source/title.cpp rename to src/domain/title.cpp index 190cb1c..b48139f 100644 --- a/source/title.cpp +++ b/src/domain/title.cpp @@ -24,8 +24,8 @@ * reasonable ways as different from the original version. */ -#include "title.hpp" -#include "main.hpp" +#include +#include static std::unordered_map> titles; static bool s_titlesLoaded = false; diff --git a/source/util.cpp b/src/domain/util.cpp similarity index 97% rename from source/util.cpp rename to src/domain/util.cpp index 79b1f8b..c4e6b8d 100644 --- a/source/util.cpp +++ b/src/domain/util.cpp @@ -24,10 +24,10 @@ * reasonable ways as different from the original version. */ -#include "util.hpp" -#include -#include -#include "main.hpp" +#include +#include +#include +#include void servicesExit(void) { diff --git a/source/directory.cpp b/src/infra/fs/directory.cpp similarity index 98% rename from source/directory.cpp rename to src/infra/fs/directory.cpp index b6acbe3..d97610b 100644 --- a/source/directory.cpp +++ b/src/infra/fs/directory.cpp @@ -24,7 +24,7 @@ * reasonable ways as different from the original version. */ -#include "directory.hpp" +#include Directory::Directory(const std::string& root) { diff --git a/source/filesystem.cpp b/src/infra/fs/filesystem.cpp similarity index 97% rename from source/filesystem.cpp rename to src/infra/fs/filesystem.cpp index 7df86a1..28c991e 100644 --- a/source/filesystem.cpp +++ b/src/infra/fs/filesystem.cpp @@ -24,7 +24,7 @@ * reasonable ways as different from the original version. */ -#include "filesystem.hpp" +#include Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID) { diff --git a/source/io.cpp b/src/infra/fs/io.cpp similarity index 99% rename from source/io.cpp rename to src/infra/fs/io.cpp index d303384..d792c61 100644 --- a/source/io.cpp +++ b/src/infra/fs/io.cpp @@ -24,9 +24,9 @@ * reasonable ways as different from the original version. */ -#include "io.hpp" -#include "main.hpp" -#include +#include +#include +#include bool io::fileExists(const std::string& path) { diff --git a/src/infra/net/transfer_receiver.cpp b/src/infra/net/transfer_receiver.cpp new file mode 100644 index 0000000..bfd75ba --- /dev/null +++ b/src/infra/net/transfer_receiver.cpp @@ -0,0 +1 @@ +// Logic moved to src/service/transfer_service.cpp diff --git a/src/infra/net/transfer_sender.cpp b/src/infra/net/transfer_sender.cpp new file mode 100644 index 0000000..bfd75ba --- /dev/null +++ b/src/infra/net/transfer_sender.cpp @@ -0,0 +1 @@ +// Logic moved to src/service/transfer_service.cpp diff --git a/source/logger.cpp b/src/infra/sys/logger.cpp similarity index 97% rename from source/logger.cpp rename to src/infra/sys/logger.cpp index 10c5bca..3837579 100644 --- a/source/logger.cpp +++ b/src/infra/sys/logger.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/service/transfer_service.cpp b/src/service/transfer_service.cpp new file mode 100644 index 0000000..2c59c8c --- /dev/null +++ b/src/service/transfer_service.cpp @@ -0,0 +1,483 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __SWITCH__ +#include +#include +#include +#include +#endif + +#include +#include + +namespace fs = std::filesystem; + +namespace nxst { + +// ─── File-transfer helpers ──────────────────────────────────────────────────── + +static bool sendAll(int sock, const void* buf, size_t len) { + size_t sent = 0; + while (sent < len) { + ssize_t n = send(sock, static_cast(buf) + sent, len - sent, 0); + if (n <= 0) return false; + sent += n; + } + return true; +} + +static bool recvAll(int sock, void* buf, size_t len) { + size_t got = 0; + while (got < len) { + ssize_t n = read(sock, static_cast(buf) + got, len - got); + if (n <= 0) return false; + got += n; + } + return true; +} + +static bool sendFile(int sock, const fs::path& filepath, TransferState& state) { + std::ifstream infile(filepath, std::ios::binary | std::ios::ate); + if (!infile.is_open()) return false; + + uint32_t filename_len = (uint32_t)filepath.string().size(); + uint64_t file_size = (uint64_t)infile.tellg(); + infile.seekg(0, std::ios::beg); + + if (!sendAll(sock, &filename_len, sizeof(filename_len))) return false; + if (!sendAll(sock, filepath.c_str(), filename_len)) return false; + if (!sendAll(sock, &file_size, sizeof(file_size))) return false; + + std::vector buffer(proto::BUF_SIZE); + uint64_t remaining = file_size; + while (remaining > 0) { + size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE); + infile.read(buffer.data(), (std::streamsize)to_read); + std::streamsize count = infile.gcount(); + if (count <= 0) break; + if (!sendAll(sock, buffer.data(), (size_t)count)) return false; + state.bytes_done.fetch_add((uint64_t)count); + remaining -= (uint64_t)count; + } + return true; +} + +static void mkdirs(const std::string& path) { + for (size_t i = 1; i < path.size(); i++) { + if (path[i] == '/') { + std::string component = path.substr(0, i); + mkdir(component.c_str(), 0777); + } + } + mkdir(path.c_str(), 0777); +} + +static void receiveFile(int sock, const std::string& rel_path, uint64_t file_size, + TransferState& state) { + size_t last_slash = rel_path.rfind('/'); + if (last_slash != std::string::npos) { + std::string dir = rel_path.substr(0, last_slash); + if (!dir.empty()) mkdirs(dir); + } + + FILE* outfile = fopen(rel_path.c_str(), "wb"); + if (!outfile) { + std::vector drain(proto::BUF_SIZE); + uint64_t remaining = file_size; + while (remaining > 0) { + size_t to_read = (size_t)std::min(remaining, (uint64_t)proto::BUF_SIZE); + ssize_t n = read(sock, drain.data(), to_read); + if (n <= 0) break; + remaining -= (uint64_t)n; + } + return; + } + + state.bytes_total.store(file_size); + state.bytes_done.store(0); + + std::vector buffer(proto::BUF_SIZE); + uint64_t total = 0; + while (total < file_size) { + size_t to_read = (size_t)std::min(file_size - total, (uint64_t)proto::BUF_SIZE); + ssize_t n = read(sock, buffer.data(), to_read); + if (n <= 0) break; + fwrite(buffer.data(), 1, (size_t)n, outfile); + total += (uint64_t)n; + state.bytes_done.store(total); + } + fclose(outfile); +} + +// ─── Sender ────────────────────────────────────────────────────────────────── + +void TransferService::failSend(const std::string& reason) { + sender_state.fail_reason = reason; + sender_state.connection_failed.store(true); + sender_state.done.store(true); +} + +int TransferService::findServer(char* out_ip) { + int udp_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (udp_fd < 0) return -1; + sender_udp_sock.store(udp_fd); + + auto releaseUdp = [&]() { + int owned = sender_udp_sock.exchange(-1); + if (owned == udp_fd) close(udp_fd); + }; + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(proto::MULTICAST_PORT); + addr.sin_addr.s_addr = inet_addr(proto::MULTICAST_GROUP); + + if (sendto(udp_fd, "DISCOVER_SERVER", 15, 0, (sockaddr*)&addr, sizeof(addr)) < 0) { + releaseUdp(); + return -1; + } + + // Poll in 100ms slices so cancel races within 100ms + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (std::chrono::steady_clock::now() < deadline) { + if (sender_state.cancelled.load()) { releaseUdp(); return -1; } + struct timeval tv{0, 100000}; + fd_set fds; + FD_ZERO(&fds); + FD_SET(udp_fd, &fds); + if (select(udp_fd + 1, &fds, nullptr, nullptr, &tv) > 0) { + sockaddr_in from{}; + socklen_t fromlen = sizeof(from); + char buf[256]; + ssize_t n = recvfrom(udp_fd, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen); + if (n > 0) { + buf[n] = '\0'; + if (strcmp(buf, "SERVER_HERE") == 0) { + inet_ntop(AF_INET, &from.sin_addr, out_ip, INET_ADDRSTRLEN); + releaseUdp(); + return 0; + } + } + } + } + releaseUdp(); + return -1; +} + +void* TransferService::senderEntry(void* arg) { + auto* a = static_cast(arg); + TransferService* svc = a->svc; + size_t idx = a->title_index; + AccountUid uid = a->uid; + delete a; + svc->runSender(idx, uid); + return nullptr; +} + +void TransferService::runSender(size_t title_index, AccountUid uid) { + sender_active.store(true); + + auto finish = [this]() { + sender_state.done.store(true); + sender_active.store(false); + }; + + char server_ip[INET_ADDRSTRLEN]; + if (findServer(server_ip) != 0) { + if (!sender_state.cancelled.load()) + failSend("No receiver found.\nMake sure the other Switch is in Receive mode."); + return finish(); + } + if (sender_state.cancelled.load()) return finish(); + + sender_state.setStatus("Creating backup..."); +#ifdef __SWITCH__ + auto backup_result = io::backup(title_index, uid); + if (!std::get<0>(backup_result)) { + failSend("Failed to create backup:\n" + std::get<2>(backup_result)); + return finish(); + } + fs::path directory = std::get<2>(backup_result); +#else + fs::path directory = "."; + (void)title_index; (void)uid; +#endif + + if (sender_state.cancelled.load()) return finish(); + + sender_state.setStatus("Connecting..."); + int tcp_fd = socket(AF_INET, SOCK_STREAM, 0); + if (tcp_fd < 0) { failSend("Failed to open socket."); return finish(); } + sender_tcp_sock.store(tcp_fd); + + auto releaseTcp = [&]() { + int owned = sender_tcp_sock.exchange(-1); + if (owned == tcp_fd) close(tcp_fd); + }; + + sockaddr_in serv{}; + serv.sin_family = AF_INET; + serv.sin_port = htons(proto::TCP_PORT); + if (inet_pton(AF_INET, server_ip, &serv.sin_addr) <= 0 || + connect(tcp_fd, (sockaddr*)&serv, sizeof(serv)) < 0) { + if (!sender_state.cancelled.load()) + failSend("Failed to connect to receiver."); + releaseTcp(); + return finish(); + } + + uint64_t total = 0; + for (const auto& entry : fs::recursive_directory_iterator(directory)) + if (fs::is_regular_file(entry.path())) + total += fs::file_size(entry.path()); + sender_state.bytes_total.store(total); + + for (const auto& entry : fs::recursive_directory_iterator(directory)) { + if (sender_state.cancelled.load()) break; + const fs::path& p = entry.path(); + if (fs::is_regular_file(p)) { + sender_state.setStatus(p.filename().string()); + if (!sendFile(tcp_fd, p, sender_state)) break; + } + } + + uint32_t sentinel = proto::EOF_SENTINEL; + sendAll(tcp_fd, &sentinel, sizeof(sentinel)); + + releaseTcp(); + sender_state.setStatus(""); + return finish(); +} + +int TransferService::startSend(size_t title_index, AccountUid uid) { + sender_state.reset(); + sender_state.setStatus("Searching for receiver..."); + + auto* arg = new SenderArgs{this, title_index, uid}; + pthread_t thread; + if (pthread_create(&thread, nullptr, senderEntry, arg) != 0) { + delete arg; + return -1; + } + pthread_detach(thread); + return 0; +} + +void TransferService::cancelSend() { + sender_state.cancelled.store(true); + int udp = sender_udp_sock.exchange(-1); + if (udp >= 0) { shutdown(udp, SHUT_RDWR); close(udp); } + int tcp = sender_tcp_sock.exchange(-1); + if (tcp >= 0) { shutdown(tcp, SHUT_RDWR); close(tcp); } +} + +// ─── Receiver ──────────────────────────────────────────────────────────────── + +std::string TransferService::replaceUsername(const std::string& file_path) const { +#ifdef __SWITCH__ + std::string username = StringUtils::removeNotAscii( + StringUtils::removeAccents(Account::username(restore_uid))); + size_t last_slash = file_path.rfind('/'); + if (last_slash == std::string::npos) return file_path; + size_t prev_slash = file_path.rfind('/', last_slash - 1); + if (prev_slash == std::string::npos) + return username + file_path.substr(last_slash); + return file_path.substr(0, prev_slash + 1) + username + file_path.substr(last_slash); +#else + return file_path; +#endif +} + +void* TransferService::broadcastEntry(void* arg) { + static_cast(arg)->runBroadcast(); + return nullptr; +} + +void TransferService::runBroadcast() { + receiver_broadcast_active.store(true); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, nullptr); + pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr); + + int udp = socket(AF_INET, SOCK_DGRAM, 0); + if (udp < 0) { receiver_broadcast_active.store(false); return; } + receiver_bcast_sock.store(udp); + + auto releaseUdp = [&]() { + int owned = receiver_bcast_sock.exchange(-1); + if (owned == udp) close(udp); + }; + + struct timeval tv{0, 20000}; // 20ms poll so cancel/exit wins race with socketExit + setsockopt(udp, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(proto::MULTICAST_PORT); + + if (bind(udp, (sockaddr*)&addr, sizeof(addr)) < 0) { + releaseUdp(); + receiver_broadcast_active.store(false); + return; + } + + ip_mreq group{}; + group.imr_multiaddr.s_addr = inet_addr(proto::MULTICAST_GROUP); + group.imr_interface.s_addr = htonl(INADDR_ANY); + if (setsockopt(udp, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) { + releaseUdp(); + receiver_broadcast_active.store(false); + return; + } + + char buf[256]; + sockaddr_in from{}; + socklen_t fromlen = sizeof(from); + while (true) { + ssize_t n = recvfrom(udp, buf, sizeof(buf) - 1, 0, (sockaddr*)&from, &fromlen); + if (n < 0) { + if (receiver_state.cancelled.load()) break; + continue; + } + buf[n] = '\0'; + if (strcmp(buf, "DISCOVER_SERVER") == 0) { + sendto(udp, "SERVER_HERE", 11, 0, (sockaddr*)&from, fromlen); + break; + } + } + + releaseUdp(); + receiver_broadcast_active.store(false); +} + +void* TransferService::acceptEntry(void* arg) { + auto* a = static_cast(arg); + TransferService* svc = a->svc; + int server_fd = a->server_fd; + delete a; + svc->runAccept(server_fd); + return nullptr; +} + +void TransferService::runAccept(int server_fd) { + receiver_accept_active.store(true); + receiver_listen_sock.store(server_fd); + + sockaddr_in client_addr{}; + socklen_t client_len = sizeof(client_addr); + int client_sock = accept(server_fd, (sockaddr*)&client_addr, &client_len); + + int owned_listen = receiver_listen_sock.exchange(-1); + if (owned_listen == server_fd) close(server_fd); + + if (client_sock >= 0) { + receiver_client_sock.store(client_sock); + + while (true) { + uint32_t filename_len = 0; + if (!recvAll(client_sock, &filename_len, sizeof(filename_len))) break; + if (filename_len == proto::EOF_SENTINEL) break; + if (filename_len > proto::MAX_FILENAME) break; + + std::vector filename_buf(filename_len + 1, '\0'); + if (!recvAll(client_sock, filename_buf.data(), filename_len)) break; + std::string filename_str(filename_buf.data(), filename_len); + filename_str = replaceUsername(filename_str); + + { + size_t sl = filename_str.rfind('/'); + receiver_state.setStatus( + sl != std::string::npos ? filename_str.substr(sl + 1) : filename_str); + } + + uint64_t file_size = 0; + if (!recvAll(client_sock, &file_size, sizeof(file_size))) break; + receiveFile(client_sock, filename_str, file_size, receiver_state); + } + + int owned = receiver_client_sock.exchange(-1); + if (owned == client_sock) close(client_sock); + + if (!receiver_state.cancelled.load()) { +#ifdef __SWITCH__ + receiver_state.setStatus("Restoring..."); + auto result = io::restore(restore_title_index, restore_uid, 0, restore_title_name); + restore_ok = std::get<0>(result); + restore_error = restore_ok ? "" : std::get<2>(result); +#else + restore_ok = true; +#endif + } + } + + receiver_state.done.store(true); + receiver_accept_active.store(false); +} + +int TransferService::startReceive(size_t title_index, AccountUid uid, std::string title_name) { + receiver_state.reset(); + receiver_state.setStatus("Waiting for connection..."); + restore_title_index = title_index; + restore_uid = uid; + restore_title_name = std::move(title_name); + restore_ok = false; + restore_error.clear(); + + pthread_t bcast_thread; + if (pthread_create(&bcast_thread, nullptr, broadcastEntry, this) != 0) return 1; + receiver_bcast_thread = bcast_thread; + pthread_detach(bcast_thread); + + Socket server(socket(AF_INET, SOCK_STREAM, 0)); + if (!server.valid()) { cancelReceive(); return 1; } + + int yes = 1; + setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(proto::TCP_PORT); + + if (bind(server, (sockaddr*)&addr, sizeof(addr)) < 0 || + listen(server, 3) < 0) { + cancelReceive(); + return 1; + } + + auto* acc_args = new AcceptArgs{this, server.fd}; + pthread_t accept_thread; + if (pthread_create(&accept_thread, nullptr, acceptEntry, acc_args) != 0) { + delete acc_args; + cancelReceive(); + return 1; + } + pthread_detach(accept_thread); + server.release(); + return 0; +} + +void TransferService::cancelReceive() { + receiver_state.cancelled.store(true); + int sock = receiver_client_sock.exchange(-1); + if (sock >= 0) { shutdown(sock, SHUT_RDWR); close(sock); } + int lsock = receiver_listen_sock.exchange(-1); + if (lsock >= 0) { shutdown(lsock, SHUT_RDWR); close(lsock); } + int bsock = receiver_bcast_sock.exchange(-1); + if (bsock >= 0) { shutdown(bsock, SHUT_RDWR); close(bsock); } + if (receiver_broadcast_active.load()) pthread_cancel(receiver_bcast_thread); +} + +} // namespace nxst diff --git a/source/titles_layout.cpp b/src/ui/titles_layout.cpp similarity index 85% rename from source/titles_layout.cpp rename to src/ui/titles_layout.cpp index 68e5d55..8b98a78 100644 --- a/source/titles_layout.cpp +++ b/src/ui/titles_layout.cpp @@ -1,10 +1,7 @@ -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include namespace ui { extern MainApplication *mainApp; @@ -98,24 +95,24 @@ namespace ui { this->updateHints(); } - void TitlesLayout::InitTitles() { + void TitlesLayout::InitTitles(AccountUid uid) { using namespace theme; - Logger::getInstance().log(Logger::INFO, "InitTitles"); + this->current_uid = uid; - auto it = this->menuCache.find(g_currentUId); + auto it = this->menuCache.find(uid); std::vector* items; if (it != this->menuCache.end()) { items = &it->second; } else { std::vector built; - for (size_t i = 0; i < getTitleCount(g_currentUId); i++) { + for (size_t i = 0; i < getTitleCount(uid); i++) { Title title; - getTitle(title, g_currentUId, i); + getTitle(title, uid, i); auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str()); titleItem->SetColor(color::TextPrimary); built.push_back(titleItem); } - auto inserted = this->menuCache.emplace(g_currentUId, std::move(built)); + auto inserted = this->menuCache.emplace(uid, std::move(built)); items = &inserted.first->second; } @@ -144,14 +141,14 @@ namespace ui { this->refreshButtons(); this->updateHints(); - this->header->SetUser(g_currentUId, Account::username(g_currentUId)); + this->header->SetUser(uid, Account::username(uid)); } void TitlesLayout::refreshPanel() { if (this->titlesMenu->GetItems().empty()) return; int idx = this->titlesMenu->GetSelectedIndex(); Title title; - getTitle(title, g_currentUId, idx); + getTitle(title, this->current_uid, idx); this->panelTitle->SetText(StringUtils::elide(title.name(), 24)); } @@ -185,24 +182,25 @@ namespace ui { } void TitlesLayout::runTransfer(int index, Title& title) { + (void)title; auto ovl = TransferOverlay::New("Transferring save data..."); this->titlesMenu->SetVisible(false); mainApp->StartOverlay(ovl); this->LockInput(); - if (transfer_files(index, g_currentUId) != 0) { + if (mainApp->transfer.startSend((size_t)index, this->current_uid) != 0) { mainApp->EndOverlay(); this->titlesMenu->SetVisible(true); this->UnlockInput(); mainApp->CreateShowDialog("Transfer", "Failed to start transfer.", {"OK"}, true); return; } - while (!isClientTransferDone()) { - ovl->SetStatus(getClientStatusText()); - ovl->SetProgressVisible(isClientProgressKnown()); - ovl->SetProgress(getClientProgress()); + while (!mainApp->transfer.isSendDone()) { + ovl->SetStatus(mainApp->transfer.sendStatusText()); + ovl->SetProgressVisible(mainApp->transfer.isSendProgressKnown()); + ovl->SetProgress(mainApp->transfer.sendProgress()); mainApp->CallForRender(); if (mainApp->GetButtonsDown() & HidNpadButton_B) { - cancelClientTransfer(); + mainApp->transfer.cancelSend(); } svcSleepThread(16666666LL); } @@ -210,9 +208,9 @@ namespace ui { this->titlesMenu->SetVisible(true); this->UnlockInput(); - if (isClientConnectionFailed()) { - mainApp->CreateShowDialog("Transfer", getClientFailReason(), {"OK"}, true); - } else if (isClientTransferCancelled()) { + if (mainApp->transfer.isSendConnectionFailed()) { + mainApp->CreateShowDialog("Transfer", mainApp->transfer.sendFailReason(), {"OK"}, true); + } else if (mainApp->transfer.isSendCancelled()) { mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true); } else { mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true); @@ -220,7 +218,7 @@ namespace ui { } void TitlesLayout::runReceive(int index, Title& title) { - if (startSendingThread() != 0) { + if (mainApp->transfer.startReceive((size_t)index, this->current_uid, title.name()) != 0) { mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true); return; } @@ -228,12 +226,12 @@ namespace ui { this->titlesMenu->SetVisible(false); mainApp->StartOverlay(ovl); this->LockInput(); - while (!isServerTransferDone()) { - ovl->SetStatus(getServerStatusText()); - ovl->SetProgress(getServerProgress()); + while (!mainApp->transfer.isReceiveDone()) { + ovl->SetStatus(mainApp->transfer.receiveStatusText()); + ovl->SetProgress(mainApp->transfer.receiveProgress()); mainApp->CallForRender(); if (mainApp->GetButtonsDown() & HidNpadButton_B) { - cancelServerTransfer(); + mainApp->transfer.cancelReceive(); } svcSleepThread(16666666LL); } @@ -241,24 +239,22 @@ namespace ui { this->titlesMenu->SetVisible(true); this->UnlockInput(); - if (isServerTransferCancelled()) { + if (mainApp->transfer.isReceiveCancelled()) { mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true); - return; - } - auto restoreResult = io::restore(index, g_currentUId, 0, title.name()); - if (std::get<0>(restoreResult)) { + } else if (mainApp->transfer.restoreSucceeded()) { mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true); } else { - mainApp->CreateShowDialog("Receive", "Restore failed:\n" + std::get<2>(restoreResult), {"OK"}, true); + mainApp->CreateShowDialog("Receive", "Restore failed:\n" + mainApp->transfer.restoreError(), {"OK"}, true); } } void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { + (void)Up; (void)Held; (void)Pos; if (m_inputLocked) return; if (Down & HidNpadButton_Plus) { - cancelClientTransfer(); - cancelServerTransfer(); + mainApp->transfer.cancelSend(); + mainApp->transfer.cancelReceive(); mainApp->Close(); return; } @@ -266,7 +262,7 @@ namespace ui { if (focus == TitlesFocus::List) { if (Down & HidNpadButton_B) { this->header->SetUser(std::nullopt, ""); - mainApp->LoadLayout(mainApp->usersLayout); + mainApp->LoadLayout(mainApp->users_layout); return; } if (Down & HidNpadButton_A) { @@ -298,7 +294,7 @@ namespace ui { if (Down & HidNpadButton_A) { int idx = this->titlesMenu->GetSelectedIndex(); Title title; - getTitle(title, g_currentUId, idx); + getTitle(title, this->current_uid, idx); TitlesAction chosen = action; this->focus = TitlesFocus::List; this->refreshButtons(); diff --git a/source/users_layout.cpp b/src/ui/users_layout.cpp similarity index 88% rename from source/users_layout.cpp rename to src/ui/users_layout.cpp index 96f0d26..c290d2d 100644 --- a/source/users_layout.cpp +++ b/src/ui/users_layout.cpp @@ -1,6 +1,4 @@ -#include -#include -#include "main.hpp" +#include namespace ui { extern MainApplication *mainApp; @@ -50,11 +48,12 @@ namespace ui { void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) { if (Down & HidNpadButton_Plus) { - svcExitProcess(); + mainApp->Close(); + return; } if (Down & HidNpadButton_A) { - g_currentUId = Account::ids().at(this->usersMenu->GetSelectedIndex()); + AccountUid uid = Account::ids().at(this->usersMenu->GetSelectedIndex()); if (!areTitlesLoaded()) { this->usersMenu->SetVisible(false); @@ -69,8 +68,8 @@ namespace ui { this->usersMenu->SetVisible(true); } - mainApp->titlesLayout->InitTitles(); - mainApp->LoadLayout(mainApp->titlesLayout); + mainApp->titles_layout->InitTitles(uid); + mainApp->LoadLayout(mainApp->titles_layout); } } }