Compare commits

...

6 Commits

Author SHA1 Message Date
DragonSpirit 844093e3e7 fix leaked client socket after receiving file 2026-04-26 17:06:01 +03:00
DragonSpirit 64b30e9835 redesign, broadcast server crash fix 2026-04-26 12:41:58 +03:00
DragonSpirit 4ffa6ed970 flow improvements 2026-04-25 20:11:49 +03:00
DragonSpirit 22b669fae0 create backup only when receiver found 2026-04-25 10:46:20 +03:00
DragonSpirit a2e874de85 ui improve 2026-04-25 10:34:50 +03:00
DragonSpirit 7515e0334b arch codereview 2026-04-25 09:55:34 +03:00
33 changed files with 1491 additions and 613 deletions
+10
View File
@@ -62,4 +62,14 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
---
## Project: NXST — Nintendo Switch Save Transfer
**Terminology (inverted from typical client/server):**
- **server** (`source/server.cpp`) — the Switch that **receives** files (listens on TCP, accepts incoming connection)
- **client** (`source/client.cpp`) — the Switch that **sends** files (discovers server via multicast, connects, transmits)
UI buttons map as: "Transfer" → runs client (sends), "Receive" → runs server (receives).
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

+11 -9
View File
@@ -32,31 +32,31 @@ include $(DEVKITPRO)/libnx/switch_rules
#---------------------------------------------------------------------------------
TARGET := NXST
BUILD := build
SOURCES := source source/fs lib/Plutonium/source
INCLUDES := include include/fs lib/Plutonium/include
SOURCES := source lib/Plutonium/source
INCLUDES := include include/net lib/Plutonium/include
EXEFS_SRC := exefs_src
APP_TITLE := NXST
APP_AUTHOR := DragonSpirit
APP_VERSION := 04.24.2026
ICON := icon.jpg
APP_VERSION := 04.26.2026
ICON := icon.png
#---------------------------------------------------------------------------------
# options for code generation
#---------------------------------------------------------------------------------
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
CFLAGS += -g -O2 -ffunction-sections \
$(ARCH) $(DEFINES)
CFLAGS += -g -O2 -ffunction-sections $(ARCH) $(DEFINES)
CFLAGS += $(INCLUDE) -D__SWITCH__ -D_GNU_SOURCE=1
CXXFLAGS:= $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++17
CXXFLAGS:= $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++17 -g
ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := -lpu -lz -lminizip -lfreetype -lSDL2_mixer -lopusfile -lopus -lmodplug -lmpg123 -lvorbisidec -logg -lSDL2_ttf -lSDL2_gfx -lSDL2_image -lSDL2 -lEGL -lGLESv2 -lglapi -ldrm_nouveau -lwebp -lpng -ljpeg `sdl2-config --libs` `freetype-config --libs` -lnx
PKGCONF := $(DEVKITPRO)/portlibs/switch/bin/aarch64-none-elf-pkg-config
PKG_LIBS := SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer freetype2 harfbuzz minizip libpng libjpeg libwebp glesv2 egl glapi zlib
LIBS := -lpu $(shell $(PKGCONF) --libs $(PKG_LIBS)) -ldrm_nouveau -lharfbuzz -lfreetype -lz
#---------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level containing
@@ -155,6 +155,8 @@ clean:
@echo clean ...
@rm -fr $(BUILD) $(TARGET).pfs0 $(TARGET).nso $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst
cleanbuild: clean all
#---------------------------------------------------------------------------------
send: $(BUILD)
@nxlink $(TARGET).nro
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

+78
View File
@@ -0,0 +1,78 @@
#pragma once
#include <pu/Plutonium>
#include <string>
namespace theme {
using pu::ui::Color;
namespace color {
constexpr Color BgBase{0x10, 0x14, 0x1C, 0xFF};
constexpr Color BgSurface{0x18, 0x1F, 0x2A, 0xFF};
constexpr Color BgSurface2{0x22, 0x2B, 0x39, 0xFF};
constexpr Color Scrim{0x00, 0x00, 0x00, 0xB8};
constexpr Color Primary{0xE2, 0x4B, 0x55, 0xFF};
constexpr Color PrimaryDim{0x9C, 0x33, 0x3A, 0xFF};
constexpr Color Accent{0x4A, 0xC2, 0xE0, 0xFF};
constexpr Color TextPrimary{0xF2, 0xF4, 0xF8, 0xFF};
constexpr Color TextSecondary{0xB6, 0xBE, 0xCB, 0xFF};
constexpr Color TextMuted{0x70, 0x7A, 0x8C, 0xFF};
constexpr Color Success{0x55, 0xC8, 0x8A, 0xFF};
constexpr Color Error{0xE0, 0x6C, 0x6C, 0xFF};
constexpr Color Warning{0xE6, 0xB4, 0x55, 0xFF};
constexpr Color Divider{0x2A, 0x33, 0x42, 0xFF};
constexpr Color FocusRing{0xE2, 0x4B, 0x55, 0xFF};
}
namespace space {
constexpr int xs = 4;
constexpr int sm = 8;
constexpr int md = 16;
constexpr int lg = 24;
constexpr int xl = 32;
constexpr int xxl = 48;
}
namespace radius {
constexpr int sm = 6;
constexpr int md = 12;
constexpr int lg = 20;
constexpr int pill = 9999;
}
namespace type {
constexpr int Display = 38;
constexpr int Title = 30;
constexpr int Body = 25;
constexpr int Label = 20;
constexpr int Caption = 18;
inline std::string font(int size) {
return "DefaultFont@" + std::to_string(size);
}
}
namespace layout {
constexpr int ScreenW = 1280;
constexpr int ScreenH = 720;
constexpr int HeaderH = 72;
constexpr int HintH = 56;
constexpr int ContentTop = HeaderH;
constexpr int ContentH = ScreenH - HeaderH - HintH;
}
namespace motion {
constexpr int FadeFrames = 20;
constexpr int SlideFrames = 14;
constexpr int SpinnerFrames = 72;
}
namespace font {
constexpr const char* Default = "Inter";
constexpr const char* Medium = "InterMedium";
}
}
+39 -1
View File
@@ -1,19 +1,57 @@
#include <pu/Plutonium>
#include <const.h>
#include <title.hpp>
#include <account.hpp>
#include <unordered_map>
#include <vector>
#include <memory>
#include <ui/HeaderBar.hpp>
#include <ui/HintBar.hpp>
namespace ui {
enum class TitlesFocus { List, Actions };
enum class TitlesAction { Transfer, Receive };
class TitlesLayout : public pu::ui::Layout {
private:
pu::ui::elm::Menu::Ref titlesMenu;
std::unordered_map<AccountUid, std::vector<pu::ui::elm::MenuItem::Ref>> menuCache;
bool m_inputLocked = false;
std::unique_ptr<HeaderBar> header;
std::unique_ptr<HintBar> hints;
pu::ui::elm::Rectangle::Ref panelBg;
pu::ui::elm::TextBlock::Ref panelTitle;
pu::ui::elm::TextBlock::Ref panelHint;
pu::ui::elm::Rectangle::Ref btnTransferBg;
pu::ui::elm::TextBlock::Ref btnTransferText;
pu::ui::elm::Rectangle::Ref btnReceiveBg;
pu::ui::elm::TextBlock::Ref btnReceiveText;
pu::ui::elm::TextBlock::Ref panelFooter;
pu::ui::elm::TextBlock::Ref emptyText;
pu::ui::elm::TextBlock::Ref emptySub;
TitlesFocus focus = TitlesFocus::List;
TitlesAction action = TitlesAction::Transfer;
int lockedListIndex = 0;
void refreshPanel();
void refreshButtons();
void updateHints();
void runTransfer(int index, Title& title);
void runReceive(int index, Title& title);
public:
TitlesLayout();
void InitTitles();
void LockInput() { m_inputLocked = true; }
void UnlockInput() { m_inputLocked = false; }
void onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos);
PU_SMART_CTOR(TitlesLayout)
};
}
}
+94
View File
@@ -0,0 +1,94 @@
#pragma once
#include <pu/Plutonium>
#include <Theme.hpp>
#include <util.hpp>
namespace ui {
class TransferOverlay : public pu::ui::Overlay {
private:
pu::ui::elm::Rectangle::Ref card;
pu::ui::elm::TextBlock::Ref titleText;
pu::ui::elm::TextBlock::Ref statusText;
pu::ui::elm::Rectangle::Ref progressTrack;
pu::ui::elm::ProgressBar::Ref progressBar;
pu::ui::elm::TextBlock::Ref indeterminateText;
pu::ui::elm::TextBlock::Ref hintText;
static constexpr int CardW = 720;
static constexpr int CardH = 360;
static constexpr int CardX = (theme::layout::ScreenW - CardW) / 2;
static constexpr int CardY = (theme::layout::ScreenH - CardH) / 2;
public:
TransferOverlay(const std::string &title)
: Overlay(0, 0, theme::layout::ScreenW, theme::layout::ScreenH, theme::color::Scrim)
{
using namespace theme;
card = pu::ui::elm::Rectangle::New(
CardX, CardY, CardW, CardH, color::BgSurface, radius::lg);
titleText = pu::ui::elm::TextBlock::New(
CardX + space::lg, CardY + space::lg, title);
titleText->SetFont(type::font(type::Title));
titleText->SetColor(color::TextPrimary);
statusText = pu::ui::elm::TextBlock::New(
CardX + space::lg,
CardY + space::lg + 56,
"");
statusText->SetFont(type::font(type::Body));
statusText->SetColor(color::TextSecondary);
int barX = CardX + space::lg;
int barY = CardY + space::lg + 56 + 56;
int barW = CardW - 2 * space::lg;
progressTrack = pu::ui::elm::Rectangle::New(
barX, barY, barW, 8, color::Divider, radius::sm);
progressBar = pu::ui::elm::ProgressBar::New(
barX, barY, barW, 8, 100.0);
progressBar->SetProgressColor(color::Primary);
progressBar->SetBackgroundColor(color::Divider);
indeterminateText = pu::ui::elm::TextBlock::New(
barX, barY - 4, "Preparing transfer...");
indeterminateText->SetFont(type::font(type::Body));
indeterminateText->SetColor(color::TextMuted);
indeterminateText->SetVisible(false);
hintText = pu::ui::elm::TextBlock::New(
CardX + space::lg,
CardY + CardH - space::lg - 18,
"B to cancel");
hintText->SetFont(type::font(type::Caption));
hintText->SetColor(color::TextMuted);
this->Add(card);
this->Add(titleText);
this->Add(statusText);
this->Add(progressTrack);
this->Add(progressBar);
this->Add(indeterminateText);
this->Add(hintText);
}
PU_SMART_CTOR(TransferOverlay)
void SetStatus(const std::string &status) {
statusText->SetText(StringUtils::elide(status, 56));
}
void SetProgress(double val) {
progressBar->SetProgress(val);
}
void SetProgressVisible(bool visible) {
progressTrack->SetVisible(visible);
progressBar->SetVisible(visible);
indeterminateText->SetVisible(!visible);
}
};
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <atomic>
#include <mutex>
#include <string>
struct TransferState {
std::atomic<bool> done{false};
std::atomic<bool> cancelled{false};
std::atomic<bool> connection_failed{false};
std::atomic<uint64_t> bytes_done{0};
std::atomic<uint64_t> bytes_total{0};
std::string status;
std::string fail_reason;
mutable std::mutex status_mutex;
void reset() {
done = false;
cancelled = false;
connection_failed = false;
bytes_done = 0;
bytes_total = 0;
fail_reason.clear();
std::lock_guard<std::mutex> lock(status_mutex);
status.clear();
}
double progress() const {
uint64_t t = bytes_total.load();
return t ? (double)bytes_done.load() / (double)t * 100.0 : 0.0;
}
std::string getStatus() const {
std::lock_guard<std::mutex> lock(status_mutex);
return status;
}
void setStatus(const std::string& s) {
std::lock_guard<std::mutex> lock(status_mutex);
status = s;
}
};
+7
View File
@@ -1,5 +1,8 @@
#include <pu/Plutonium>
#include <const.h>
#include <ui/HeaderBar.hpp>
#include <ui/HintBar.hpp>
#include <memory>
namespace ui {
@@ -7,6 +10,10 @@ namespace ui {
private:
pu::ui::elm::Menu::Ref usersMenu;
pu::ui::elm::Rectangle::Ref loadingBg;
pu::ui::elm::TextBlock::Ref loadingText;
std::unique_ptr<HeaderBar> header;
std::unique_ptr<HintBar> hints;
public:
+3 -1
View File
@@ -54,7 +54,8 @@ inline bool operator==(const AccountUid& x, u64 y)
inline bool operator<(const AccountUid& x, const AccountUid& y)
{
return x.uid[0] < y.uid[0] && x.uid[1] == y.uid[1];
if (x.uid[0] != y.uid[0]) return x.uid[0] < y.uid[0];
return x.uid[1] < y.uid[1];
}
struct User {
@@ -69,6 +70,7 @@ namespace Account {
std::vector<AccountUid> ids(void);
AccountUid selectAccount(void);
std::string username(AccountUid id);
std::string iconPath(AccountUid id);
}
#endif
+12 -3
View File
@@ -1,4 +1,13 @@
namespace fs = std::filesystem;
using path = fs::path;
#include <string>
#include <switch.h>
int transfer_files(path directory);
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();
+6 -2
View File
@@ -1,2 +1,6 @@
#define BACKGROUND_COLOR COLOR("00FFFFFF")
#define COLOR(hex) pu::ui::Color::FromHex(hex)
#pragma once
#include <Theme.hpp>
#define COLOR(hex) pu::ui::Color::FromHex(hex)
#define BACKGROUND_COLOR theme::color::BgBase
+6 -5
View File
@@ -40,17 +40,18 @@ public:
return mLogger;
}
inline static const std::string INFO = "[ INFO]";
inline static const std::string INFO = "[INFO]";
inline static const std::string DEBUG = "[DEBUG]";
inline static const std::string ERROR = "[ERROR]";
inline static const std::string WARN = "[ WARN]";
inline static const std::string WARN = "[WARN]";
template <typename... Args>
void log(const std::string& level, const std::string& format = {}, Args... args)
{
// xcbuffer += StringUtils::format(("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...);
printf("%s\n", format.c_str());
// printf("%s\n",StringUtils::format("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...);
// buffer += StringUtils::format(("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...);
// buffer += StringUtils::format("%s\n", format.c_str());
// buffer += StringUtils::format("%s\n",StringUtils::format("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...);
printf(StringUtils::format("[" + DateTime::logDateTime() + "] " + level + " " + format + "\n").c_str(), args...);
}
void flush(void)
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <unistd.h>
struct Socket {
int fd = -1;
Socket() = default;
explicit Socket(int fd) : fd(fd) {}
~Socket() { if (fd >= 0) close(fd); }
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
Socket(Socket&& o) : fd(o.fd) { o.fd = -1; }
operator int() const { return fd; }
bool valid() const { return fd >= 0; }
void release() { fd = -1; }
};
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
namespace proto {
constexpr uint16_t TCP_PORT = 8080;
constexpr uint16_t MULTICAST_PORT = 8081;
constexpr char MULTICAST_GROUP[] = "239.0.0.1";
constexpr size_t BUF_SIZE = 65536;
constexpr uint32_t MAX_FILENAME = 4096;
constexpr uint32_t EOF_SENTINEL = 0;
// Wire layout per file:
// [filename_len : uint32_t LE] — 0 == end-of-stream
// [filename : filename_len bytes]
// [file_size : uint64_t LE]
// [file_data : file_size bytes]
}
+8 -1
View File
@@ -1 +1,8 @@
int startSendingThread();
#include <string>
int startSendingThread();
bool isServerTransferDone();
bool isServerTransferCancelled();
bool isServerWorkersIdle();
void cancelServerTransfer();
double getServerProgress();
std::string getServerStatusText();
+1
View File
@@ -82,6 +82,7 @@ private:
void getTitle(Title& dst, AccountUid uid, size_t i);
size_t getTitleCount(AccountUid uid);
void loadTitles(void);
bool areTitlesLoaded(void);
void sortTitles(void);
void rotateSortMode(void);
void refreshDirectories(u64 id);
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <pu/Plutonium>
#include <Theme.hpp>
namespace ui {
class Card {
public:
pu::ui::elm::Rectangle::Ref bg;
Card(pu::ui::Layout* parent, int x, int y, int w, int h,
pu::ui::Color color = theme::color::BgSurface,
int rad = theme::radius::lg) {
bg = pu::ui::elm::Rectangle::New(x, y, w, h, color, rad);
parent->Add(bg);
}
};
}
+86
View File
@@ -0,0 +1,86 @@
#pragma once
#include <pu/Plutonium>
#include <Theme.hpp>
#include <ui/UiContext.hpp>
#include <account.hpp>
namespace ui {
class HeaderBar {
private:
pu::ui::elm::Rectangle::Ref bg;
pu::ui::elm::Rectangle::Ref divider;
pu::ui::elm::TextBlock::Ref appName;
pu::ui::elm::TextBlock::Ref subtitle;
pu::ui::elm::Rectangle::Ref chipBg;
pu::ui::elm::Image::Ref avatar;
pu::ui::elm::TextBlock::Ref userName;
public:
HeaderBar(pu::ui::Layout* parent, const std::string& sub = "Save Transfer") {
using namespace theme;
bg = pu::ui::elm::Rectangle::New(
0, 0, layout::ScreenW, layout::HeaderH, color::BgSurface);
divider = pu::ui::elm::Rectangle::New(
0, layout::HeaderH - 1, layout::ScreenW, 1, color::Divider);
appName = pu::ui::elm::TextBlock::New(space::lg, 8, "NXST");
appName->SetFont(type::font(type::Title));
appName->SetColor(color::TextPrimary);
subtitle = pu::ui::elm::TextBlock::New(space::lg, 46, sub);
subtitle->SetFont(type::font(type::Caption));
subtitle->SetColor(color::TextMuted);
const int chipW = 280;
const int chipX = layout::ScreenW - chipW - space::lg;
chipBg = pu::ui::elm::Rectangle::New(
chipX, 16, chipW, 40,
color::BgSurface2, radius::pill);
chipBg->SetVisible(false);
avatar = pu::ui::elm::Image::New(chipX + 4, 20, "");
avatar->SetWidth(32);
avatar->SetHeight(32);
avatar->SetVisible(false);
userName = pu::ui::elm::TextBlock::New(chipX + 44, 24, "");
userName->SetFont(type::font(type::Body));
userName->SetColor(color::TextPrimary);
userName->SetVisible(false);
parent->Add(bg);
parent->Add(divider);
parent->Add(appName);
parent->Add(subtitle);
parent->Add(chipBg);
parent->Add(avatar);
parent->Add(userName);
}
void SetUser(const std::optional<AccountUid>& uid, const std::string& name) {
const bool show = uid.has_value();
chipBg->SetVisible(show);
userName->SetVisible(show);
if (show) {
userName->SetText(name);
std::string path = Account::iconPath(*uid);
if (!path.empty()) {
avatar->SetImage(path);
avatar->SetWidth(32);
avatar->SetHeight(32);
avatar->SetVisible(avatar->IsImageValid());
} else {
avatar->SetVisible(false);
}
} else {
avatar->SetVisible(false);
}
}
void SetSubtitle(const std::string& text) {
subtitle->SetText(text);
}
};
}
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <pu/Plutonium>
#include <Theme.hpp>
#include <vector>
#include <string>
namespace ui {
struct Hint {
std::string glyph;
std::string label;
};
class HintBar {
private:
pu::ui::Layout* parent;
pu::ui::elm::Rectangle::Ref bg;
pu::ui::elm::Rectangle::Ref divider;
std::vector<pu::ui::elm::TextBlock::Ref> labels;
public:
HintBar(pu::ui::Layout* p) : parent(p) {
using namespace theme;
bg = pu::ui::elm::Rectangle::New(
0, layout::ScreenH - layout::HintH,
layout::ScreenW, layout::HintH, color::BgSurface);
divider = pu::ui::elm::Rectangle::New(
0, layout::ScreenH - layout::HintH,
layout::ScreenW, 1, color::Divider);
parent->Add(bg);
parent->Add(divider);
}
void SetHints(const std::vector<Hint>& hints) {
using namespace theme;
for (auto& l : labels) l->SetVisible(false);
labels.clear();
int x = layout::ScreenW - space::lg;
int y = layout::ScreenH - layout::HintH + 18;
for (auto it = hints.rbegin(); it != hints.rend(); ++it) {
std::string text = it->glyph + " " + it->label;
auto tb = pu::ui::elm::TextBlock::New(0, y, text);
tb->SetFont(type::font(type::Label));
tb->SetColor(color::TextSecondary);
int w = tb->GetWidth();
x -= w;
tb->SetX(x);
x -= space::xl;
parent->Add(tb);
labels.push_back(tb);
}
}
};
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <string>
#include <optional>
#include <switch.h>
#include <account.hpp>
namespace ui {
struct UiContext {
std::optional<AccountUid> selectedUser;
std::string selectedUserName;
};
}
+1
View File
@@ -47,6 +47,7 @@ namespace StringUtils {
std::string removeAccents(std::string str);
std::string removeNotAscii(std::string str);
std::u16string UTF8toUTF16(const char* src);
std::string elide(const std::string& s, size_t maxChars);
}
#endif
+26 -12
View File
@@ -1,6 +1,9 @@
#include <MainApplication.hpp>
#include "util.hpp"
#include "main.hpp"
#include <server.hpp>
#include <client.hpp>
#include <unistd.h>
static int nxlink_sock = -1;
@@ -19,6 +22,11 @@ extern "C" void userAppInit() {
}
extern "C" void userAppExit() {
cancelServerTransfer();
cancelClientTransfer();
for (int i = 0; i < 150 && (!isServerWorkersIdle() || !isClientWorkersIdle()); i++) {
usleep(10000);
}
if (nxlink_sock != -1) {
close(nxlink_sock);
}
@@ -41,24 +49,30 @@ int main() {
exit(res);
}
loadTitles();
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();
// 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);
// Create our main application from the renderer
auto main = ui::MainApplication::New(renderer);
main->Prepare();
main->Prepare();
main->Show();
main->Show();
servicesExit();
return 0;
}
+289 -47
View File
@@ -4,67 +4,309 @@
#include <const.h>
#include <client.hpp>
#include <server.hpp>
static std::vector<uint64_t> accSids, devSids, bcatSids, cacheSids;
#include <TransferOverlay.hpp>
namespace ui {
extern MainApplication *mainApp;
void TitlesLayout::InitTitles() {
this->titlesMenu = pu::ui::elm::Menu::New(0, 0, 1280, COLOR("#67000000"), COLOR("#170909FF"), 94, 7);
for (size_t i = 0; i < getTitleCount(g_currentUId); i++) {
Title title;
getTitle(title, g_currentUId, i);
auto titleItem = pu::ui::elm::MenuItem::New(title.name().c_str());
titleItem->SetColor(COLOR("#FFFFFFFF"));
this->titlesMenu->AddItem(titleItem);
}
namespace {
constexpr int ListX = theme::space::lg;
constexpr int ListW = 760;
constexpr int PanelX = ListX + ListW + theme::space::xl;
constexpr int PanelW = theme::layout::ScreenW - PanelX - theme::space::lg;
constexpr int ContentY = theme::layout::ContentTop + theme::space::md;
constexpr int ContentH = theme::layout::ContentH - 2 * theme::space::md;
constexpr int BtnH = 56;
constexpr int BtnW = PanelW - 2 * theme::space::lg;
}
TitlesLayout::TitlesLayout() : Layout::Layout() {
using namespace theme;
this->titlesMenu = pu::ui::elm::Menu::New(
ListX, ContentY, ListW,
color::BgBase, color::BgSurface2,
88, 6);
this->titlesMenu->SetScrollbarColor(color::Primary);
this->titlesMenu->SetItemsFocusColor(color::BgSurface2);
this->titlesMenu->SetOnSelectionChanged([this]() { this->refreshPanel(); });
this->SetBackgroundColor(color::BgBase);
this->Add(this->titlesMenu);
this->SetBackgroundColor(BACKGROUND_COLOR);
this->panelBg = pu::ui::elm::Rectangle::New(
PanelX, ContentY, PanelW, ContentH, color::BgSurface, radius::lg);
this->Add(this->panelBg);
this->panelTitle = pu::ui::elm::TextBlock::New(
PanelX + space::lg, ContentY + space::lg, "");
this->panelTitle->SetFont(type::font(type::Title));
this->panelTitle->SetColor(color::TextPrimary);
this->Add(this->panelTitle);
this->panelHint = pu::ui::elm::TextBlock::New(
PanelX + space::lg, ContentY + space::lg + 48, "Pick an action:");
this->panelHint->SetFont(type::font(type::Body));
this->panelHint->SetColor(color::TextSecondary);
this->Add(this->panelHint);
int btnY = ContentY + 200;
this->btnTransferBg = pu::ui::elm::Rectangle::New(
PanelX + space::lg, btnY, BtnW, BtnH, color::BgSurface2, radius::md);
this->Add(this->btnTransferBg);
this->btnTransferText = pu::ui::elm::TextBlock::New(
PanelX + space::lg + space::md, btnY + 14, "Transfer to another device");
this->btnTransferText->SetFont(type::font(type::Body));
this->btnTransferText->SetColor(color::TextSecondary);
this->Add(this->btnTransferText);
int btnY2 = btnY + BtnH + space::md;
this->btnReceiveBg = pu::ui::elm::Rectangle::New(
PanelX + space::lg, btnY2, BtnW, BtnH, color::BgSurface2, radius::md);
this->Add(this->btnReceiveBg);
this->btnReceiveText = pu::ui::elm::TextBlock::New(
PanelX + space::lg + space::md, btnY2 + 14, "Receive from another device");
this->btnReceiveText->SetFont(type::font(type::Body));
this->btnReceiveText->SetColor(color::TextSecondary);
this->Add(this->btnReceiveText);
this->panelFooter = pu::ui::elm::TextBlock::New(
PanelX + space::lg,
ContentY + ContentH - space::lg - 18,
"Save data only");
this->panelFooter->SetFont(type::font(type::Caption));
this->panelFooter->SetColor(color::TextMuted);
this->Add(this->panelFooter);
this->emptyText = pu::ui::elm::TextBlock::New(
ListX + ListW / 2 - 280, ContentY + ContentH / 2 - 40,
"No save data on this profile");
this->emptyText->SetFont(type::font(type::Display));
this->emptyText->SetColor(color::TextPrimary);
this->emptyText->SetVisible(false);
this->Add(this->emptyText);
this->emptySub = pu::ui::elm::TextBlock::New(
ListX + ListW / 2 - 220, ContentY + ContentH / 2 + 16,
"Play something first, then come back.");
this->emptySub->SetFont(type::font(type::Body));
this->emptySub->SetColor(color::TextMuted);
this->emptySub->SetVisible(false);
this->Add(this->emptySub);
this->header = std::make_unique<HeaderBar>(this, "Save Transfer");
this->hints = std::make_unique<HintBar>(this);
this->updateHints();
}
void TitlesLayout::InitTitles() {
using namespace theme;
Logger::getInstance().log(Logger::INFO, "InitTitles");
auto it = this->menuCache.find(g_currentUId);
std::vector<pu::ui::elm::MenuItem::Ref>* items;
if (it != this->menuCache.end()) {
items = &it->second;
} else {
std::vector<pu::ui::elm::MenuItem::Ref> built;
for (size_t i = 0; i < getTitleCount(g_currentUId); i++) {
Title title;
getTitle(title, g_currentUId, 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));
items = &inserted.first->second;
}
this->titlesMenu->ClearItems();
for (auto& item : *items) {
this->titlesMenu->AddItem(item);
}
this->titlesMenu->SetSelectedIndex(0);
const bool empty = items->empty();
this->titlesMenu->SetVisible(!empty);
this->panelBg->SetVisible(!empty);
this->panelTitle->SetVisible(!empty);
this->panelHint->SetVisible(!empty);
this->btnTransferBg->SetVisible(!empty);
this->btnTransferText->SetVisible(!empty);
this->btnReceiveBg->SetVisible(!empty);
this->btnReceiveText->SetVisible(!empty);
this->panelFooter->SetVisible(!empty);
this->emptyText->SetVisible(empty);
this->emptySub->SetVisible(empty);
this->focus = TitlesFocus::List;
this->action = TitlesAction::Transfer;
this->refreshPanel();
this->refreshButtons();
this->updateHints();
this->header->SetUser(g_currentUId, Account::username(g_currentUId));
}
void TitlesLayout::refreshPanel() {
if (this->titlesMenu->GetItems().empty()) return;
int idx = this->titlesMenu->GetSelectedIndex();
Title title;
getTitle(title, g_currentUId, idx);
this->panelTitle->SetText(StringUtils::elide(title.name(), 24));
}
void TitlesLayout::refreshButtons() {
using namespace theme;
const bool active = (focus == TitlesFocus::Actions);
if (active && action == TitlesAction::Transfer) {
this->btnTransferBg->SetColor(color::Primary);
this->btnTransferText->SetColor(color::TextPrimary);
this->btnReceiveBg->SetColor(color::BgSurface2);
this->btnReceiveText->SetColor(color::TextSecondary);
} else if (active && action == TitlesAction::Receive) {
this->btnTransferBg->SetColor(color::BgSurface2);
this->btnTransferText->SetColor(color::TextSecondary);
this->btnReceiveBg->SetColor(color::Accent);
this->btnReceiveText->SetColor(color::BgBase);
} else {
this->btnTransferBg->SetColor(color::BgSurface2);
this->btnTransferText->SetColor(color::TextSecondary);
this->btnReceiveBg->SetColor(color::BgSurface2);
this->btnReceiveText->SetColor(color::TextSecondary);
}
}
void TitlesLayout::updateHints() {
if (focus == TitlesFocus::List) {
this->hints->SetHints({{"A", "Choose action"}, {"B", "Back"}, {"+", "Quit"}});
} else {
this->hints->SetHints({{"A", "Confirm"}, {"B", "Back"}});
}
}
void TitlesLayout::runTransfer(int index, Title& title) {
auto ovl = TransferOverlay::New("Transferring save data...");
this->titlesMenu->SetVisible(false);
mainApp->StartOverlay(ovl);
this->LockInput();
if (transfer_files(index, g_currentUId) != 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());
mainApp->CallForRender();
if (mainApp->GetButtonsDown() & HidNpadButton_B) {
cancelClientTransfer();
}
svcSleepThread(16666666LL);
}
mainApp->EndOverlay();
this->titlesMenu->SetVisible(true);
this->UnlockInput();
if (isClientConnectionFailed()) {
mainApp->CreateShowDialog("Transfer", getClientFailReason(), {"OK"}, true);
} else if (isClientTransferCancelled()) {
mainApp->CreateShowDialog("Transfer", "Transfer cancelled.", {"OK"}, true);
} else {
mainApp->CreateShowDialog("Transfer", "Save data sent successfully!", {"OK"}, true);
}
}
void TitlesLayout::runReceive(int index, Title& title) {
if (startSendingThread() != 0) {
mainApp->CreateShowDialog("Receive", "Failed to start receiver.\nCheck network connection.", {"OK"}, true);
return;
}
auto ovl = TransferOverlay::New("Receiving save data...");
this->titlesMenu->SetVisible(false);
mainApp->StartOverlay(ovl);
this->LockInput();
while (!isServerTransferDone()) {
ovl->SetStatus(getServerStatusText());
ovl->SetProgress(getServerProgress());
mainApp->CallForRender();
if (mainApp->GetButtonsDown() & HidNpadButton_B) {
cancelServerTransfer();
}
svcSleepThread(16666666LL);
}
mainApp->EndOverlay();
this->titlesMenu->SetVisible(true);
this->UnlockInput();
if (isServerTransferCancelled()) {
mainApp->CreateShowDialog("Receive", "Transfer cancelled.", {"OK"}, true);
return;
}
auto restoreResult = io::restore(index, g_currentUId, 0, title.name());
if (std::get<0>(restoreResult)) {
mainApp->CreateShowDialog("Receive", "Save data received and restored successfully!", {"OK"}, true);
} else {
mainApp->CreateShowDialog("Receive", "Restore failed:\n" + std::get<2>(restoreResult), {"OK"}, true);
}
}
void TitlesLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
if (m_inputLocked) return;
if (Down & HidNpadButton_Plus) {
cancelClientTransfer();
cancelServerTransfer();
mainApp->Close();
return;
}
if (Down & HidNpadButton_A) {
auto index = this->titlesMenu->GetSelectedIndex();
Title title;
getTitle(title, g_currentUId, index);
printf("userid is 0x%lX%lX\n", title.userId().uid[1], title.userId().uid[0]);
// printf("current game index is %i\n", index);
int opt = mainApp->CreateShowDialog(title.name().c_str(), "What do you want?", { "Transfer", "Receive" }, false);
printf("opt is %i\n", opt);
switch (opt) {
case 0: {
// Transfer selected
auto result = io::backup(index, g_currentUId);
if (std::get<0>(result)) {
printf("path is %s\n", std::get<2>(result).c_str());
std::string path = std::get<2>(result);
auto directory = std::filesystem::path(path);
transfer_files(directory);
}
break;
}
case 1: {
// Receive selected
printf("startSendingThread\n");
int result = startSendingThread();
printf("result is %i\n", result);
if (result == 0) {
auto restoreResult = io::restore(index, g_currentUId, 0, title.name());
if (std::get<0>(restoreResult)) {
printf("%s\n", std::get<2>(restoreResult).c_str());
} else {
printf("Failed to restore with error %s\n", std::get<2>(restoreResult).c_str());
}
}
break;
if (focus == TitlesFocus::List) {
if (Down & HidNpadButton_B) {
this->header->SetUser(std::nullopt, "");
mainApp->LoadLayout(mainApp->usersLayout);
return;
}
if (Down & HidNpadButton_A) {
if (this->titlesMenu->GetItems().empty()) return;
this->lockedListIndex = this->titlesMenu->GetSelectedIndex();
this->focus = TitlesFocus::Actions;
this->action = TitlesAction::Transfer;
this->refreshButtons();
this->updateHints();
return;
}
} else {
if (this->titlesMenu->GetSelectedIndex() != this->lockedListIndex) {
this->titlesMenu->SetSelectedIndex(this->lockedListIndex);
}
if (Down & HidNpadButton_B) {
this->focus = TitlesFocus::List;
this->refreshButtons();
this->updateHints();
return;
}
if (Down & (HidNpadButton_Up | HidNpadButton_Down |
HidNpadButton_StickLUp | HidNpadButton_StickLDown)) {
this->action = (action == TitlesAction::Transfer)
? TitlesAction::Receive : TitlesAction::Transfer;
this->refreshButtons();
return;
}
if (Down & HidNpadButton_A) {
int idx = this->titlesMenu->GetSelectedIndex();
Title title;
getTitle(title, g_currentUId, idx);
TitlesAction chosen = action;
this->focus = TitlesFocus::List;
this->refreshButtons();
this->updateHints();
if (chosen == TitlesAction::Transfer) {
this->runTransfer(idx, title);
} else {
this->runReceive(idx, title);
}
}
}
+47 -9
View File
@@ -6,16 +6,42 @@ namespace ui {
extern MainApplication *mainApp;
UsersLayout::UsersLayout() : Layout::Layout() {
this->usersMenu = pu::ui::elm::Menu::New(0, 0, 1280, COLOR("#67000000"), COLOR("#170909FF"), 94, 7);
this->usersMenu->SetScrollbarColor(COLOR("#170909FF"));
using namespace theme;
this->usersMenu = pu::ui::elm::Menu::New(
0, layout::ContentTop + space::md,
layout::ScreenW,
color::BgBase, color::BgSurface2,
88, 6);
this->usersMenu->SetScrollbarColor(color::Primary);
this->usersMenu->SetItemsFocusColor(color::BgSurface2);
for (AccountUid const& uid : Account::ids()) {
auto username = pu::ui::elm::MenuItem::New(Account::username(uid) + ": " + std::to_string(getTitleCount(uid)));
username->SetColor(COLOR("#FFFFFFFF"));
this->usersMenu->AddItem(username);
auto item = pu::ui::elm::MenuItem::New(Account::username(uid));
item->SetColor(color::TextPrimary);
this->usersMenu->AddItem(item);
}
this->SetBackgroundColor(BACKGROUND_COLOR);
this->loadingBg = pu::ui::elm::Rectangle::New(
0, 0, layout::ScreenW, layout::ScreenH, color::Scrim);
this->loadingBg->SetVisible(false);
this->loadingText = pu::ui::elm::TextBlock::New(
layout::ScreenW / 2 - 120,
layout::ScreenH / 2 - 12,
"Loading saves...");
this->loadingText->SetFont(type::font(type::Body));
this->loadingText->SetColor(color::TextSecondary);
this->loadingText->SetVisible(false);
this->SetBackgroundColor(color::BgBase);
this->Add(this->usersMenu);
this->Add(this->loadingBg);
this->Add(this->loadingText);
this->header = std::make_unique<HeaderBar>(this, "Select a user");
this->hints = std::make_unique<HintBar>(this);
this->hints->SetHints({{"A", "Select"}, {"+", "Quit"}});
}
int32_t UsersLayout::GetCurrentIndex() {
@@ -24,13 +50,25 @@ namespace ui {
void UsersLayout::onInput(u64 Down, u64 Up, u64 Held, pu::ui::TouchPoint Pos) {
if (Down & HidNpadButton_Plus) {
mainApp->Close();
return;
svcExitProcess();
}
if (Down & HidNpadButton_A) {
printf("current index is %i\n", this->usersMenu->GetSelectedIndex());
g_currentUId = Account::ids().at(this->usersMenu->GetSelectedIndex());
if (!areTitlesLoaded()) {
this->usersMenu->SetVisible(false);
this->loadingBg->SetVisible(true);
this->loadingText->SetVisible(true);
mainApp->CallForRender();
loadTitles();
this->loadingBg->SetVisible(false);
this->loadingText->SetVisible(false);
this->usersMenu->SetVisible(true);
}
mainApp->titlesLayout->InitTitles();
mainApp->LoadLayout(mainApp->titlesLayout);
}
+52 -5
View File
@@ -26,12 +26,23 @@
#include "account.hpp"
#include <main.hpp>
#include <sys/stat.h>
#include <cstdio>
static std::map<AccountUid, User> mUsers;
Result Account::init(void)
{
return accountInitialize(AccountServiceType_Application);
Result res = accountInitialize(AccountServiceType_Application);
if (R_FAILED(res)) return res;
AccountUid uids[8];
s32 count = 0;
accountListAllUsers(uids, 8, &count);
for (s32 i = 0; i < count; i++) {
Account::username(uids[i]); // populates mUsers as side effect
}
return 0;
}
void Account::exit(void)
@@ -55,11 +66,12 @@ static User getUser(AccountUid id)
AccountProfileBase profilebase;
memset(&profilebase, 0, sizeof(profilebase));
if (R_SUCCEEDED(accountGetProfile(&profile, id)) && R_SUCCEEDED(accountProfileGet(&profile, NULL, &profilebase))) {
user.name = std::string(profilebase.nickname);
if (R_SUCCEEDED(accountGetProfile(&profile, id))) {
if (R_SUCCEEDED(accountProfileGet(&profile, NULL, &profilebase))) {
user.name = std::string(profilebase.nickname);
}
accountProfileClose(&profile);
}
accountProfileClose(&profile);
return user;
}
@@ -75,6 +87,41 @@ std::string Account::username(AccountUid id)
return got->second.name;
}
std::string Account::iconPath(AccountUid id)
{
char path[128];
snprintf(path, sizeof(path), "sdmc:/switch/NXST/cache/%016lX%016lX.jpg",
id.uid[0], id.uid[1]);
struct stat st;
if (stat(path, &st) == 0 && st.st_size > 0) return std::string(path);
mkdir("sdmc:/switch", 0755);
mkdir("sdmc:/switch/NXST", 0755);
mkdir("sdmc:/switch/NXST/cache", 0755);
AccountProfile profile;
if (R_FAILED(accountGetProfile(&profile, id))) return "";
u32 imgSize = 0;
if (R_FAILED(accountProfileGetImageSize(&profile, &imgSize)) || imgSize == 0) {
accountProfileClose(&profile);
return "";
}
std::vector<u8> buf(imgSize);
u32 outSize = 0;
Result r = accountProfileLoadImage(&profile, buf.data(), imgSize, &outSize);
accountProfileClose(&profile);
if (R_FAILED(r) || outSize == 0) return "";
FILE* f = fopen(path, "wb");
if (!f) return "";
fwrite(buf.data(), 1, outSize, f);
fclose(f);
return std::string(path);
}
AccountUid Account::selectAccount(void)
{
LibAppletArgs args;
+205 -203
View File
@@ -1,247 +1,249 @@
#include <string>
#include <arpa/inet.h>
#include <chrono>
#include <cstring>
#include <fcntl.h>
#include <filesystem>
#include <sys/select.h>
#include <fstream>
#include <iostream>
#include <netinet/in.h>
#include <ostream>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
#ifdef __SWITCH__
#include <client.hpp>
#include <io.hpp>
#include <switch.h>
#endif
#define PORT 8080
#define BUFFER_SIZE 65536
#define MULTICAST_PORT 8081
#define MULTICAST_GROUP "239.0.0.1" // Multicast group IP
#include <protocol.hpp>
#include <TransferState.hpp>
namespace fs = std::filesystem;
using path = fs::path;
class Logger {
public:
inline static const std::string INFO = "[INFO]";
inline static const std::string DEBUG = "[DEBUG]";
inline static const std::string ERROR = "[ERROR]";
inline static const std::string WARN = "[WARN]";
};
static TransferState g_client_state;
static std::atomic<int> g_client_udp_sock{-1};
static std::atomic<int> g_client_tcp_sock{-1};
static std::atomic<bool> g_client_thread_active{false};
struct ThreadArgs {
int sock;
fs::path directory;
};
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<const char*>(buf) + sent, len - sent, 0);
if (n <= 0) return false;
sent += n;
}
return true;
}
// Отправка строки с учетом её длины
void send_string(int sock, const std::string &str) {
size_t length = str.size();
send(sock, &length, sizeof(length), 0);
send(sock, str.c_str(), length, 0);
}
void sendFile(int sock, const fs::path &base_path, 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;
}
// std::string relative_path = fs::relative(filepath, base_path).string();
uint32_t filename_len = filepath.string().size();
std::cout << "send filepath length: " << filename_len << std::endl;
if (send(sock, &filename_len, sizeof(filename_len), 0) == -1) {
std::cerr << "Failed to send filename length" << std::endl;
}
std::cout << "send filepath: " << filepath.string() << std::endl;
// Send the filename
if (send(sock, filepath.c_str(), filename_len, 0) ==
-1) { // Send the filename length
std::cerr << "Failed to send filename" << std::endl;
}
// Get the size of the file
std::streamsize file_size = infile.tellg();
infile.seekg(0, std::ios::beg);
char* buffer = new char[BUFFER_SIZE];
std::cout << "send filesize: " << file_size << std::endl;
// Send the file size
if (send(sock, &file_size, sizeof(file_size), 0) == -1) {
std::cerr << "Failed to send file size" << std::endl;
}
while (file_size > 0) {
infile.read(buffer, BUFFER_SIZE);
std::streamsize count = infile.gcount();
if (!send_all(sock, buffer, count)) {
std::cerr << "Failed to send file data" << std::endl;
delete[] buffer;
pthread_exit(nullptr);
size_t sent = 0;
while (sent < len) {
ssize_t n = send(sock, static_cast<const char*>(buf) + sent, len - sent, 0);
if (n <= 0) return false;
sent += n;
}
file_size -= count;
}
std::cout << "File sent successfully: " << filepath << std::endl;
delete[] buffer;
infile.close();
return true;
}
void *send_files_thread(void *args) {
ThreadArgs *thread_args = static_cast<ThreadArgs *>(args);
int sock = thread_args->sock;
fs::path cwd = thread_args->directory;
delete thread_args;
std::cout << "cwd is: " << cwd << std::endl;
char buffer[BUFFER_SIZE];
for (const auto &entry : fs::recursive_directory_iterator(cwd)) {
path path = entry.path();
std::cout << "path is " << path << std::endl;
if (fs::is_regular_file(path)) {
std::cout << "regular file | path is: " << path << std::endl;
sendFile(sock, path.parent_path(), path);
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;
}
}
close(sock);
pthread_exit(nullptr);
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<char> 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;
}
int find_server(char *server_ip) {
std::cout << Logger::INFO << "Init find_server" << std::endl;
int sockfd;
struct sockaddr_in multicast_addr;
struct ThreadArgs { size_t index; AccountUid uid; };
std::cout << Logger::INFO << "Create socket" << std::endl;
static int find_server(char* server_ip);
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
std::cout << Logger::ERROR << "Socket creation error" << std::endl;
return -1;
}
memset(&multicast_addr, 0, sizeof(multicast_addr));
multicast_addr.sin_family = AF_INET;
multicast_addr.sin_port = htons(MULTICAST_PORT);
multicast_addr.sin_addr.s_addr = inet_addr(MULTICAST_GROUP);
std::cout << Logger::INFO << "Send DISCOVER_SERVER" << std::endl;
const char *multicast_message = "DISCOVER_SERVER";
if (sendto(sockfd, multicast_message, strlen(multicast_message), 0,
(struct sockaddr *)&multicast_addr, sizeof(multicast_addr)) < 0) {
std::cout << Logger::ERROR << "sendto failed" << std::endl;
close(sockfd);
return -1;
} else {
std::cout << Logger::INFO << "send multicast message success" << std::endl;
}
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
char buffer[BUFFER_SIZE];
ssize_t n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&cliaddr, &len);
std::cout << Logger::INFO << "recvfrom n: %i" << n << std::endl;
if (n < 0) {
std::cout << Logger::ERROR << "recvfrom failed" << std::endl;
close(sockfd);
return -1;
}
std::cout << Logger::INFO << "buffer: " << buffer << std::endl;
buffer[n] = '\0';
if (strcmp(buffer, "SERVER_HERE") == 0) {
std::cout << Logger::INFO << "Server found" << std::endl;
inet_ntop(AF_INET, &cliaddr.sin_addr, server_ip, INET_ADDRSTRLEN);
} else {
std::cout << Logger::ERROR << "Unable to find server, close socket"
<< std::endl;
;
close(sockfd);
return -1;
}
close(sockfd);
return 0;
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);
}
int transfer_files(fs::path directory) {
std::cout << Logger::INFO << "Init transfer_files" << std::endl;
char server_ip[INET_ADDRSTRLEN];
if (find_server(server_ip) != 0) {
std::cout << Logger::ERROR << "Failed to find server" << std::endl;
return -1;
}
static void* discovery_and_send_thread(void* arg) {
g_client_thread_active.store(true);
ThreadArgs* targs = static_cast<ThreadArgs*>(arg);
size_t index = targs->index;
AccountUid uid = targs->uid;
delete targs;
int sock = 0;
struct sockaddr_in serv_addr;
auto finish = [](void*) {
g_client_state.done.store(true);
g_client_thread_active.store(false);
return (void*)nullptr;
};
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
std::cout << Logger::ERROR << "Socket creation error" << std::endl;
return -1;
}
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);
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
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 (inet_pton(AF_INET, server_ip, &serv_addr.sin_addr) <= 0) {
std::cout << Logger::ERROR << "Invalid address / Address not supported"
<< std::endl;
return -1;
}
if (g_client_state.cancelled.load()) return finish(nullptr);
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
std::cout << Logger::ERROR << "Connection failed" << std::endl;
return -1;
}
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);
pthread_t file_thread;
ThreadArgs *thread_args = new ThreadArgs{sock, directory};
if (pthread_create(&file_thread, nullptr, send_files_thread,
(void *)thread_args) < 0) {
std::cout << Logger::ERROR << "Thread creation failed" << std::endl;
close(sock);
delete thread_args;
return -1;
} else {
std::cout << Logger::INFO << "Wait for file_thread" << std::endl;
pthread_join(file_thread, nullptr);
}
return 0;
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);
}
#ifndef __SWITCH__ // for desktop
int main(int argc, char *argv[]) {
if (argc < 1) {
std::cerr << "Usage: " << argv[0] << " <path> " << std::endl;
return 1;
}
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);
fs::path directory = fs::path(argv[1]);
std::cout << "directory is " << directory << std::endl;
transfer_files(directory);
auto release_udp = [&]() {
int owned = g_client_udp_sock.exchange(-1);
if (owned == udp_fd) close(udp_fd);
};
return 0;
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;
}
#endif
+2 -3
View File
@@ -45,10 +45,9 @@ Directory::Directory(const std::string& root)
struct DirectoryEntry de = {name, directory};
mList.push_back(de);
}
closedir(dir);
mGood = true;
}
closedir(dir);
mGood = true;
}
Result Directory::error(void)
+36 -12
View File
@@ -62,6 +62,10 @@ void io::copyFile(const std::string& srcPath, const std::string& dstPath)
while (offset < sz) {
u32 count = fread((char*)buf, 1, BUFFER_SIZE, src);
if (count == 0) {
Logger::getInstance().log(Logger::ERROR, "fread returned 0 for file {} at offset {}/{} with errno {}. Aborting copy.", srcPath, offset, sz, errno);
break;
}
fwrite((char*)buf, 1, count, dst);
offset += count;
}
@@ -161,6 +165,7 @@ std::tuple<bool, Result, std::string> io::backup(size_t index, AccountUid uid)
if (R_SUCCEEDED(res)) {
int rc = FileSystem::mount(fileSystem);
if (rc == -1) {
fsFsClose(&fileSystem);
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during backup. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
title.userId().uid[1], title.userId().uid[0]);
@@ -176,26 +181,34 @@ std::tuple<bool, Result, std::string> io::backup(size_t index, AccountUid uid)
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId())));
io::createDirectory(title.path());
std::string dstPath = title.path() + "/" + suggestion;
if (io::directoryExists(dstPath)) {
int rc = io::deleteFolderRecursively((dstPath + "/").c_str());
if (rc != 0) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to recursively delete directory " + dstPath + " with result %d.", rc);
return std::make_tuple(false, (Result)rc, "Failed to delete the existing backup\ndirectory recursively.");
}
// Write to a temp dir first; rename on success so the existing backup
// is never destroyed if the copy is interrupted mid-way.
std::string tmpPath = dstPath + ".tmp";
if (io::directoryExists(tmpPath)) {
io::deleteFolderRecursively((tmpPath + "/").c_str());
}
io::createDirectory(dstPath);
res = io::copyDirectory("save:/", dstPath + "/");
io::createDirectory(tmpPath);
res = io::copyDirectory("save:/", tmpPath + "/");
if (R_FAILED(res)) {
FileSystem::unmount();
io::deleteFolderRecursively((dstPath + "/").c_str());
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory " + dstPath + " with result 0x%08lX. Skipping...", res);
io::deleteFolderRecursively((tmpPath + "/").c_str());
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory to " + tmpPath + " with result 0x%08lX.", res);
return std::make_tuple(false, res, "Failed to backup save.");
}
// Swap: delete old backup only after new one is fully written.
if (io::directoryExists(dstPath)) {
io::deleteFolderRecursively((dstPath + "/").c_str());
}
if (rename(tmpPath.c_str(), dstPath.c_str()) != 0) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to rename temp backup to " + dstPath);
return std::make_tuple(false, (Result)-1, "Failed to finalise backup.");
}
refreshDirectories(title.id());
FileSystem::unmount();
@@ -249,6 +262,7 @@ std::tuple<bool, Result, std::string> io::restore(size_t index, AccountUid uid,
if (R_SUCCEEDED(res)) {
int rc = FileSystem::mount(fileSystem);
if (rc == -1) {
fsFsClose(&fileSystem);
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Failed to mount filesystem during restore. Title id: 0x%016lX; User id: 0x%lX%lX.", title.id(),
uid.uid[1], uid.uid[0]);
@@ -266,6 +280,16 @@ std::tuple<bool, Result, std::string> io::restore(size_t index, AccountUid uid,
std::string srcPath = title.path() + "/" + suggestion + "/";
std::string dstPath = "save:/";
// Validate source exists and is non-empty before touching live save data.
{
Directory srcCheck(srcPath);
if (!srcCheck.good() || srcCheck.size() == 0) {
FileSystem::unmount();
Logger::getInstance().log(Logger::ERROR, "Restore source is empty or missing: " + srcPath);
return std::make_tuple(false, (Result)-1, "Restore source is empty or missing.");
}
}
{
Directory saveRoot(dstPath);
for (size_t i = 0, sz = saveRoot.size(); i < sz; i++) {
+283 -269
View File
@@ -1,18 +1,16 @@
#include <arpa/inet.h>
#include <atomic>
#include <cstring>
#include <fcntl.h>
#include <iomanip>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <netinet/in.h>
#include <ostream>
#include <pthread.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>
#ifdef __SWITCH__
#include <server.hpp>
@@ -20,312 +18,328 @@
#include <main.hpp>
#endif
#define PORT 8080
#define BUFFER_SIZE 65536
#define MULTICAST_PORT 8081
#define MULTICAST_GROUP "239.0.0.1" // Multicast group IP
#include <protocol.hpp>
#include <TransferState.hpp>
#include <net/Socket.hpp>
namespace fs = std::filesystem;
static TransferState g_server_state;
static std::atomic<int> g_server_client_sock{-1};
static std::atomic<int> g_server_listen_sock{-1};
static std::atomic<int> g_broadcast_sock{-1};
static std::atomic<bool> g_accept_thread_active{false};
static std::atomic<bool> 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__
std::string replaceUsername(const std::string &path) {
std::string replacedString = StringUtils::removeNotAscii(
StringUtils::removeAccents(Account::username(g_currentUId)));
// Найдём позицию последнего символа '/'
size_t lastSlashPos = path.rfind('/');
// Если нет '/', возвращаем исходный путь
if (lastSlashPos == std::string::npos) {
return path;
}
// Найдём позицию предыдущего символа '/' (начало последней папки)
size_t prevSlashPos = path.rfind('/', lastSlashPos - 1);
// Если предыдущий '/' не найден, значит путь состоит из одной папки и файла
// Заменим последнюю папку и вернём полный путь
if (prevSlashPos == std::string::npos) {
return replacedString + path.substr(lastSlashPos);
}
// Собираем путь, заменяя последнюю папку на "name"
return path.substr(0, prevSlashPos + 1) + replacedString +
path.substr(lastSlashPos);
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
// Читает ровно len байт из сокета, повторяя read при частичном получении.
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<char *>(buf) + received, len - received);
if (n <= 0) return false;
received += n;
}
return true;
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<char*>(buf) + received, len - received);
if (n <= 0) return false;
received += n;
}
return true;
}
// Создаёт все компоненты пути через POSIX mkdir.
// std::filesystem::create_directories не работает с devkitPro-путями sdmc:/.
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);
int rc = mkdir(component.c_str(), 0777);
std::cout << "mkdirs: mkdir [" << component << "] rc=" << rc << " errno=" << errno << std::endl;
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);
}
}
}
int rc = mkdir(path.c_str(), 0777);
std::cout << "mkdirs: mkdir [" << path << "] rc=" << rc << " errno=" << errno << std::endl;
mkdir(path.c_str(), 0777);
}
// Функция для получения файла
void receive_file(int sock, const std::string &relative_path,
size_t file_size) {
std::cout << "relative_path is: " << relative_path << std::endl;
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;
// Печатаем путь побайтово — ловим невидимые символы
std::cout << "receive_file len=" << relative_path.size() << " path=[";
for (unsigned char c : relative_path) {
if (c >= 0x20 && c <= 0x7e) std::cout << c;
else std::cout << "\\x" << std::hex << std::setw(2) << std::setfill('0') << (int)c << std::dec;
}
std::cout << "]" << std::endl;
size_t last_slash = relative_path.rfind('/');
std::string dir = (last_slash != std::string::npos)
? relative_path.substr(0, last_slash)
: "";
std::cout << "receive_file dir=[" << dir << "]" << std::endl;
if (!dir.empty()) mkdirs(dir);
// Проверяем stat на папке перед fopen
struct stat st;
int statrc = stat(dir.c_str(), &st);
std::cout << "stat(dir) rc=" << statrc << " is_dir="
<< (statrc == 0 && S_ISDIR(st.st_mode)) << std::endl;
FILE *outfile = fopen(relative_path.c_str(), "wb");
if (!outfile) {
int saved_errno = errno;
std::cerr << "Failed to open for writing: " << relative_path
<< " dir=[" << dir << "] fopen_errno=" << saved_errno << std::endl;
// Дренируем байты, чтобы отправитель не завис
char* drain_buf = new char[BUFFER_SIZE];
size_t remaining = file_size;
while (remaining > 0) {
ssize_t n = read(sock, drain_buf, remaining < (size_t)BUFFER_SIZE ? remaining : (size_t)BUFFER_SIZE);
if (n <= 0) break;
remaining -= n;
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);
}
delete[] drain_buf;
return;
}
char* buffer = new char[BUFFER_SIZE]();
size_t total_bytes_received = 0;
while (total_bytes_received < file_size) {
size_t to_read = std::min((size_t)BUFFER_SIZE, file_size - total_bytes_received);
ssize_t bytes_received = read(sock, buffer, to_read);
std::cout << "Bytes received: " << bytes_received << std::endl;
if (bytes_received <= 0) {
std::cerr << "Error reading file data from socket." << std::endl;
break;
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<char> 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;
}
fwrite(buffer, 1, bytes_received, outfile);
total_bytes_received += bytes_received;
}
std::cout << "File received successfully: " << relative_path << std::endl;
delete[] buffer;
fclose(outfile);
g_server_state.bytes_total.store(file_size);
g_server_state.bytes_done.store(0);
std::vector<char> 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;
}
void *handle_client(void *socket_desc) {
int client_socket = *(int *)socket_desc;
free(socket_desc);
static void* handle_client(void* socket_desc) {
int client_socket = *(int*)socket_desc;
delete static_cast<int*>(socket_desc);
std::cout << "Обработка нового клиента в потоке " << pthread_self() << "\n";
while (true) {
uint32_t filename_len = 0;
if (!recv_all(client_socket, &filename_len, sizeof(filename_len)))
break;
while (true) {
if (filename_len == proto::EOF_SENTINEL) {
std::cout << "End of transfer." << std::endl;
break;
}
uint32_t filename_len;
ssize_t bytes_read =
read(client_socket, &filename_len, sizeof(filename_len));
if (filename_len > proto::MAX_FILENAME) {
std::cerr << "filename_len=" << filename_len << " exceeds MAX_FILENAME, aborting." << std::endl;
break;
}
// Check for end-of-transmission signal
if (bytes_read <= 0 || filename_len == 0) {
std::cout << "End of transmission detected." << std::endl;
break;
pthread_exit(nullptr);
std::vector<char> 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);
}
// Receive filename
char *filename = new char[filename_len + 1]();
if (!recv_all(client_socket, filename, filename_len)) {
std::cerr << "Short read on filename, aborting." << std::endl;
delete[] filename;
break;
int owned_client = g_server_client_sock.exchange(-1);
if (owned_client == client_socket) {
close(client_socket);
}
filename[filename_len] = '\0';
std::string filename_str(filename);
delete[] filename;
std::cout << "Received filename_str is " << filename_str << std::endl;
#ifdef __SWITCH__
std::cout << "Replaced filename from " << filename_str << std::endl;
filename_str = replaceUsername(filename_str);
std::cout << "to " << filename_str << std::endl;
#endif
size_t file_size;
if (!recv_all(client_socket, &file_size, sizeof(file_size))) {
std::cerr << "Short read on file_size, aborting." << std::endl;
break;
}
std::cout << "file size is: " << file_size << std::endl;
receive_file(client_socket, filename_str, file_size);
}
close(client_socket);
pthread_exit(nullptr);
return nullptr;
}
void *broadcast_listener(void *) {
int sockfd;
struct sockaddr_in servaddr;
char buffer[BUFFER_SIZE + 1];
struct ip_mreq group;
struct AcceptArgs { int server_fd; };
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
pthread_exit(nullptr);
}
static void* accept_and_handle(void* arg) {
g_accept_thread_active.store(true);
int server_fd = static_cast<AcceptArgs*>(arg)->server_fd;
delete static_cast<AcceptArgs*>(arg);
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(MULTICAST_PORT);
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) {
perror("binding datagram socket");
close(sockfd);
pthread_exit(nullptr);
}
group.imr_multiaddr.s_addr = inet_addr(MULTICAST_GROUP);
group.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&group,
sizeof(group)) < 0) {
perror("setsockopt failed");
close(sockfd);
pthread_exit(nullptr);
}
std::cout << "Broadcast listener started" << std::endl;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
while (true) {
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &addr_len);
if (n < 0) {
perror("recvfrom failed");
continue;
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);
}
std::cout << buffer << std::endl;
buffer[n] = '\0';
if (strcmp(buffer, "DISCOVER_SERVER") == 0) {
const char *message = "SERVER_HERE";
sendto(sockfd, message, strlen(message), 0,
(const struct sockaddr *)&client_addr, addr_len);
std::cout << "Server discovery response sent to multicast group"
<< std::endl;
pthread_exit(0);
}
}
close(sockfd);
pthread_exit(nullptr);
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() {
pthread_t broadcast_thread;
if (pthread_create(&broadcast_thread, nullptr, broadcast_listener, nullptr) <
0) {
perror("Thread creation failed");
return 1;
}
g_server_state.reset();
g_server_state.setStatus("Waiting for connection...");
int server_fd, new_socket;
struct sockaddr_in address;
socklen_t addrlen = sizeof(address);
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);
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("Bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
if (listen(server_fd, 3) < 0) {
perror("Listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
std::cout << "Wait for broadcast thread done " << std::endl;
pthread_join(broadcast_thread, NULL);
std::cout << "Broadcast thread done " << std::endl;
std::cout << "Server listening on port " << PORT << std::endl;
while (true) {
sockaddr_in client_address;
socklen_t client_len = sizeof(client_address);
int client_socket =
accept(server_fd, (sockaddr *)&client_address, &client_len);
if (client_socket < 0) {
std::cerr << "Ошибка принятия подключения\n";
continue;
Socket server(socket(AF_INET, SOCK_STREAM, 0));
if (!server.valid()) {
perror("startSendingThread: socket");
cancelServerTransfer();
return 1;
}
// Создаем новый поток для обработки клиента
pthread_t thread_id;
int *pclient = new (std::nothrow) int(client_socket);
if (!pclient) {
std::cerr << "Ошибка выделения памяти\n";
close(client_socket);
continue;
}
if (pthread_create(&thread_id, nullptr, handle_client, pclient) != 0) {
std::cerr << "Ошибка создания потока\n";
delete pclient; // Освобождаем память при ошибке
} else {
pthread_join(thread_id, NULL);
break;
}
}
int yes = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
close(server_fd);
return 0;
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__ // for desktop
#ifndef __SWITCH__
int main() {
return startSendingThread();
if (startSendingThread() != 0) return 1;
while (!isServerTransferDone()) usleep(16000);
return 0;
}
#endif
+9 -19
View File
@@ -28,6 +28,12 @@
#include "main.hpp"
static std::unordered_map<AccountUid, std::vector<Title>> titles;
static bool s_titlesLoaded = false;
bool areTitlesLoaded(void)
{
return s_titlesLoaded;
}
void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string& name, const std::string& author)
{
@@ -65,10 +71,6 @@ void Title::init(u8 saveDataType, u64 id, AccountUid userID, const std::string&
}
}
if (!io::directoryExists(mPath)) {
io::createDirectory(mPath);
}
refreshDirectories();
}
@@ -180,25 +182,13 @@ void Title::refreshDirectories(void)
else {
Logger::getInstance().log(Logger::ERROR, "Couldn't retrieve the extdata directory list for the title " + name());
}
// save backups from configuration
// std::vector<std::string> additionalFolders = Configuration::getInstance().additionalSaveFolders(mId);
// for (std::vector<std::string>::const_iterator it = additionalFolders.begin(); it != additionalFolders.end(); ++it) {
// we have other folders to parse
// Directory list(*it);
// if (list.good()) {
// for (size_t i = 0, sz = list.size(); i < sz; i++) {
// if (list.folder(i)) {
// mSaves.push_back(list.entry(i));
// mFullSavePaths.push_back(*it + "/" + list.entry(i));
// }
// }
// }
// }
}
void loadTitles(void)
{
if (s_titlesLoaded) return;
s_titlesLoaded = true;
titles.clear();
FsSaveDataInfoReader reader;
+18 -12
View File
@@ -32,10 +32,7 @@
void servicesExit(void)
{
Logger::getInstance().flush();
pdmqryExit();
socketExit();
Account::exit();
nsExit();
plExit();
romfsExit();
}
@@ -46,10 +43,8 @@ Result servicesInit(void)
io::createDirectory("sdmc:/switch/NXST");
io::createDirectory("sdmc:/switch/NXST/saves");
Logger::getInstance().log(Logger::INFO, "Starting Checkpoint loading...");
if (appletGetAppletType() != AppletType_Application) {
Logger::getInstance().log(Logger::WARN, "Please do not run Checkpoint in applet mode.");
Logger::getInstance().log(Logger::WARN, "Please do not run NXST in applet mode.");
}
// Result socinit = 0;
@@ -80,16 +75,17 @@ Result servicesInit(void)
}
if (R_FAILED(res = nsInitialize())) {
Logger::getInstance().log(Logger::ERROR, "nsInitialize failed. Result code 0x%08lX.", res);
Logger::getInstance().log(Logger::ERROR, "nsInitialize failed. Result code 0x{:08X}.", res);
return res;
}
// Configuration::getInstance();
if (R_SUCCEEDED(res = pdmqryInitialize())) {}
else {
Logger::getInstance().log(Logger::WARN, "pdmqryInitialize failed with result 0x%08lX.", res);
if (R_SUCCEEDED(res = hidsysInitialize())) {
g_notificationLedAvailable = true;
}
else {
Logger::getInstance().log(Logger::INFO, "Notification led not available. Result code 0x{:08X}.", res);
}
Logger::getInstance().log(Logger::INFO, "NXST loading completed!");
@@ -130,6 +126,16 @@ std::string StringUtils::removeNotAscii(std::string str)
return str;
}
std::string StringUtils::elide(const std::string& s, size_t maxChars)
{
if (s.size() <= maxChars || maxChars < 6) return s;
constexpr const char* dots = "...";
size_t budget = maxChars - 3;
size_t head = (budget + 1) / 2;
size_t tail = budget - head;
return s.substr(0, head) + dots + s.substr(s.size() - tail);
}
HidsysNotificationLedPattern blinkLedPattern(u8 times)
{
HidsysNotificationLedPattern pattern;