Compare commits
4 Commits
844093e3e7
...
7bd6a90bde
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bd6a90bde | |||
| dc65a4c8a9 | |||
| 895fee6235 | |||
| b5c506cf03 |
@@ -0,0 +1,28 @@
|
||||
---
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 4
|
||||
ColumnLimit: 110
|
||||
PointerAlignment: Left
|
||||
AlignAfterOpenBracket: Align
|
||||
AllowShortFunctionsOnASingleLine: Empty
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLambdasOnASingleLine: Empty
|
||||
BreakBeforeBraces: Attach
|
||||
SortIncludes: true
|
||||
IncludeBlocks: Regroup
|
||||
IncludeCategories:
|
||||
# Project headers: nxst/
|
||||
- Regex: '^(<|")(nxst/)'
|
||||
Priority: 3
|
||||
SortPriority: 3
|
||||
# Third-party: Plutonium, libnx, SDL, switch.h
|
||||
- Regex: '^(<|")(pu/|switch\.h|libnx|SDL|freetype|harfbuzz|zlib)'
|
||||
Priority: 2
|
||||
SortPriority: 2
|
||||
# System / C++ standard library
|
||||
- Regex: '^<'
|
||||
Priority: 1
|
||||
SortPriority: 1
|
||||
SpacesBeforeTrailingComments: 2
|
||||
Cpp11BracedListStyle: true
|
||||
Standard: c++17
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
---
|
||||
Checks: >
|
||||
bugprone-*,
|
||||
readability-*,
|
||||
modernize-*,
|
||||
cppcoreguidelines-pro-type-cstyle-cast,
|
||||
-fuchsia-*,
|
||||
-google-*,
|
||||
-llvm-*,
|
||||
-readability-magic-numbers,
|
||||
-readability-named-parameter,
|
||||
-modernize-use-trailing-return-type,
|
||||
-modernize-use-nodiscard,
|
||||
-modernize-avoid-c-arrays,
|
||||
-bugprone-easily-swappable-parameters
|
||||
|
||||
WarningsAsErrors: ''
|
||||
|
||||
HeaderFilterRegex: 'include/nxst/.*'
|
||||
|
||||
CheckOptions:
|
||||
- key: readability-identifier-naming.ClassCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.StructCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.FunctionCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.MethodCase
|
||||
value: camelBack
|
||||
- key: readability-identifier-naming.VariableCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.ParameterCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.MemberCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.NamespaceCase
|
||||
value: lower_case
|
||||
- key: readability-identifier-naming.ConstexprVariablePrefix
|
||||
value: 'k'
|
||||
- key: readability-identifier-naming.ConstexprVariableCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumCase
|
||||
value: CamelCase
|
||||
- key: readability-identifier-naming.EnumConstantCase
|
||||
value: CamelCase
|
||||
- key: readability-braces-around-statements.ShortStatementLines
|
||||
value: '2'
|
||||
@@ -0,0 +1,22 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{cpp,hpp,h,c}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{json,md}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,14 @@
|
||||
# Normalize line endings
|
||||
* text=auto eol=lf
|
||||
|
||||
# Vendor: do not count toward language stats
|
||||
lib/Plutonium/** linguist-vendored=true
|
||||
deps/** linguist-vendored=true
|
||||
|
||||
# Binary assets
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.nro binary
|
||||
*.nso binary
|
||||
*.pfs0 binary
|
||||
*.elf binary
|
||||
@@ -0,0 +1,72 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
nro:
|
||||
name: Build NRO
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: devkitpro/devkita64:latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Configure
|
||||
run: cmake --preset switch
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build -j$(nproc)
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NXST-${{ github.sha }}
|
||||
path: build/NXST.nro
|
||||
|
||||
release:
|
||||
name: Upload release asset
|
||||
runs-on: ubuntu-latest
|
||||
needs: nro
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: NXST-${{ github.sha }}
|
||||
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: NXST.nro
|
||||
|
||||
format:
|
||||
name: Format check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install clang-format
|
||||
run: sudo apt-get install -y clang-format
|
||||
|
||||
- name: Check formatting
|
||||
run: |
|
||||
find src include \( -name '*.cpp' -o -name '*.hpp' \) \
|
||||
| xargs clang-format --dry-run --Werror
|
||||
|
||||
layering:
|
||||
name: Layering check
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: UI must not include net/sys headers
|
||||
run: |
|
||||
! grep -rE '^#include\s*[<"](arpa/inet|sys/socket|pthread)' src/ui/
|
||||
+17
-2
@@ -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
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to NXST follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
Version numbers are calendar-based (`MM.DD.YYYY`) until the protocol stabilises.
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- Full architectural refactor (8 phases):
|
||||
- Layered directory structure: `src/` + `include/nxst/` mirroring domain / infra / service / ui
|
||||
- CMake build system replacing Makefile
|
||||
- `nxst::TransferService` — all network state and threads moved out of UI layer
|
||||
- `nxst::Result<T>` tagged-union type replacing `std::tuple<bool, Result, std::string>`
|
||||
- RAII handles: `FsFileSystemHandle`, `FileHandle`
|
||||
- Logger rewritten with `nxst::log` namespace and compile-time format checking
|
||||
- snake_case filenames and identifiers throughout
|
||||
|
||||
### Fixed
|
||||
- `mkdir(path, 777)` was decimal (octal `01411`) — corrected to `0777`
|
||||
- Logger format args were silently dropped (double-substitution bug)
|
||||
- `new u8[]` / `malloc` in IO paths replaced with `std::vector`
|
||||
|
||||
---
|
||||
|
||||
## [04.26.2026]
|
||||
|
||||
### Added
|
||||
- Initial public release: save transfer between two Nintendo Switch consoles over local Wi-Fi
|
||||
- Multicast discovery (UDP `239.0.0.1:8081`) + TCP file transfer (`:8080`)
|
||||
- Send and Receive modes with progress overlay and cancel support
|
||||
- Automatic save backup before transfer
|
||||
- Restore replaces live save data and commits to the save device
|
||||
+104
@@ -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()
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": 6,
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "switch",
|
||||
"displayName": "Nintendo Switch",
|
||||
"toolchainFile": "$env{DEVKITPRO}/cmake/Switch.cmake",
|
||||
"binaryDir": "${sourceDir}/build"
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "switch",
|
||||
"configurePreset": "switch"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
For the full license text see <https://www.gnu.org/licenses/gpl-3.0.txt>.
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
NXST includes code derived from Checkpoint by Bernardo Giordano / FlagBrew,
|
||||
licensed under GPLv3. The derived files are:
|
||||
|
||||
src/infra/fs/io.cpp
|
||||
src/domain/account.cpp
|
||||
src/domain/title.cpp
|
||||
include/nxst/infra/fs/filesystem.hpp
|
||||
src/infra/fs/filesystem.cpp
|
||||
src/infra/fs/directory.cpp
|
||||
|
||||
All original NXST code is also released under the GNU General Public
|
||||
License v3 or later, to satisfy the license inheritance requirement.
|
||||
|
||||
Original Checkpoint repository: https://github.com/BernardoGiordano/Checkpoint
|
||||
@@ -1,204 +0,0 @@
|
||||
#---------------------------------------------------------------------------------
|
||||
.SUFFIXES:
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
ifeq ($(strip $(DEVKITPRO)),)
|
||||
$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>/devkitpro")
|
||||
endif
|
||||
|
||||
TOPDIR ?= $(CURDIR)
|
||||
include $(DEVKITPRO)/libnx/switch_rules
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# TARGET is the name of the output
|
||||
# BUILD is the directory where object files & intermediate files will be placed
|
||||
# SOURCES is a list of directories containing source code
|
||||
# DATA is a list of directories containing data files
|
||||
# INCLUDES is a list of directories containing header files
|
||||
# EXEFS_SRC is the optional input directory containing data copied into exefs, if anything this normally should only contain "main.npdm".
|
||||
# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional)
|
||||
#
|
||||
# NO_ICON: if set to anything, do not use icon.
|
||||
# NO_NACP: if set to anything, no .nacp file is generated.
|
||||
# APP_TITLE is the name of the app stored in the .nacp file (Optional)
|
||||
# APP_AUTHOR is the author of the app stored in the .nacp file (Optional)
|
||||
# APP_VERSION is the version of the app stored in the .nacp file (Optional)
|
||||
# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional)
|
||||
# ICON is the filename of the icon (.jpg), relative to the project folder.
|
||||
# If not set, it attempts to use one of the following (in this order):
|
||||
# - <Project name>.jpg
|
||||
# - icon.jpg
|
||||
# - <libnx folder>/default_icon.jpg
|
||||
#---------------------------------------------------------------------------------
|
||||
TARGET := NXST
|
||||
BUILD := build
|
||||
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.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 += $(INCLUDE) -D__SWITCH__ -D_GNU_SOURCE=1
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
# include and lib
|
||||
#---------------------------------------------------------------------------------
|
||||
LIBDIRS := $(PORTLIBS) $(LIBNX) $(CURDIR)/lib/Plutonium
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# no real need to edit anything past this point unless you need to add additional
|
||||
# rules for different file extensions
|
||||
#---------------------------------------------------------------------------------
|
||||
ifneq ($(BUILD),$(notdir $(CURDIR)))
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
export OUTPUT := $(CURDIR)/$(TARGET)
|
||||
export TOPDIR := $(CURDIR)
|
||||
|
||||
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
|
||||
$(foreach dir,$(DATA),$(CURDIR)/$(dir))
|
||||
|
||||
export DEPSDIR := $(CURDIR)/$(BUILD)
|
||||
|
||||
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
|
||||
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
|
||||
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
|
||||
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# use CXX for linking C++ projects, CC for standard C
|
||||
#---------------------------------------------------------------------------------
|
||||
ifeq ($(strip $(CPPFILES)),)
|
||||
#---------------------------------------------------------------------------------
|
||||
export LD := $(CC)
|
||||
#---------------------------------------------------------------------------------
|
||||
else
|
||||
#---------------------------------------------------------------------------------
|
||||
export LD := $(CXX)
|
||||
#---------------------------------------------------------------------------------
|
||||
endif
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
export OFILES_BIN := $(addsuffix .o,$(BINFILES))
|
||||
export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
|
||||
export OFILES := $(OFILES_BIN) $(OFILES_SRC)
|
||||
export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES)))
|
||||
|
||||
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
|
||||
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
|
||||
-I$(CURDIR)/$(BUILD)
|
||||
|
||||
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
|
||||
|
||||
export BUILD_EXEFS_SRC := $(TOPDIR)/$(EXEFS_SRC)
|
||||
|
||||
ifeq ($(strip $(ICON)),)
|
||||
icons := $(wildcard *.jpg)
|
||||
ifneq (,$(findstring $(TARGET).jpg,$(icons)))
|
||||
export APP_ICON := $(TOPDIR)/$(TARGET).jpg
|
||||
else
|
||||
ifneq (,$(findstring icon.jpg,$(icons)))
|
||||
export APP_ICON := $(TOPDIR)/icon.jpg
|
||||
endif
|
||||
endif
|
||||
else
|
||||
export APP_ICON := $(TOPDIR)/$(ICON)
|
||||
endif
|
||||
|
||||
ifeq ($(strip $(NO_ICON)),)
|
||||
export NROFLAGS += --icon=$(APP_ICON)
|
||||
endif
|
||||
|
||||
ifeq ($(strip $(NO_NACP)),)
|
||||
export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
|
||||
endif
|
||||
|
||||
ifneq ($(APP_TITLEID),)
|
||||
export NACPFLAGS += --titleid=$(APP_TITLEID)
|
||||
endif
|
||||
|
||||
ifneq ($(ROMFS),)
|
||||
export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS)
|
||||
endif
|
||||
|
||||
.PHONY: $(BUILD) clean all
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
all: $(BUILD)
|
||||
|
||||
$(BUILD):
|
||||
@[ -d $@ ] || mkdir -p $@
|
||||
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
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
|
||||
|
||||
debug: $(BUILD)
|
||||
@nxlink -s $(TARGET).nro
|
||||
|
||||
|
||||
else
|
||||
.PHONY: all
|
||||
|
||||
DEPENDS := $(OFILES:.o=.d)
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# main targets
|
||||
#---------------------------------------------------------------------------------
|
||||
all : $(OUTPUT).pfs0 $(OUTPUT).nro
|
||||
|
||||
$(OUTPUT).pfs0 : $(OUTPUT).nso
|
||||
|
||||
$(OUTPUT).nso : $(OUTPUT).elf
|
||||
|
||||
ifeq ($(strip $(NO_NACP)),)
|
||||
$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp
|
||||
else
|
||||
$(OUTPUT).nro : $(OUTPUT).elf
|
||||
endif
|
||||
|
||||
$(OUTPUT).elf : $(OFILES)
|
||||
|
||||
$(OFILES_SRC) : $(HFILES_BIN)
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# you need a rule like this for each extension you use as binary data
|
||||
#---------------------------------------------------------------------------------
|
||||
%.bin.o %_bin.h : %.bin
|
||||
#---------------------------------------------------------------------------------
|
||||
@echo $(notdir $<)
|
||||
@$(bin2o)
|
||||
|
||||
-include $(DEPENDS)
|
||||
|
||||
#---------------------------------------------------------------------------------------
|
||||
endif
|
||||
#---------------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,434 @@
|
||||
# NXST Architectural Refactor Plan
|
||||
|
||||
## Current Phase Tracker
|
||||
|
||||
| Phase | Title | Status | Effort | Notes |
|
||||
|-------|-------|--------|--------|-------|
|
||||
| 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 | ✅ Done | M (~1d) | `src/` + `include/nxst/` layered tree |
|
||||
| 4 | Make → CMake migration | ✅ Done | M (~1d) | devkitpro `Switch.cmake` toolchain |
|
||||
| 5 | TransferService extraction | ✅ Done | L (~2d) | kill globals, sever UI ↔ net coupling |
|
||||
| 6 | `Result<T>` + RAII | ✅ Done | M (~1d) | tagged union, OS handle wrappers, fix raw memory |
|
||||
| 7 | Documentation + license | ✅ Done | S (~half-day) | README, ARCHITECTURE, PROTOCOL, CHANGELOG, GPLv3 LICENSE |
|
||||
| 8 | CI | ✅ Done | S (~2h) | GitHub Actions, `.nro` artifact, format check, layering check |
|
||||
|
||||
**Active phase:** — All phases complete.
|
||||
**Last updated:** 2026-04-27.
|
||||
|
||||
Mark a phase `🟡 In progress` when starting and `✅ Done` when verified on hardware. Keep this table the source of truth.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
NXST is a Nintendo Switch homebrew save-transfer app (~3.2K LOC across 13 .cpp + 24 .hpp files, plus the vendored Plutonium UI submodule). The current codebase works but reads as a prototype:
|
||||
|
||||
- Flat `source/` + mostly flat `include/` with mixed `PascalCase` and `lowercase` filenames.
|
||||
- Mixed identifier style (`recv_all` next to `isServerTransferDone`).
|
||||
- 11 headers use `#pragma once`, 9 use legacy `#ifndef X_HPP` guards.
|
||||
- **Severe layering violation**: UI layouts (`TitlesLayout::runTransfer`, `runReceive`) call into raw socket code and `io::restore` directly.
|
||||
- Globals: 6 in `server.cpp`, 4 in `client.cpp`, 2 in `io.cpp`, plus `g_currentUId` everywhere.
|
||||
- **Logger is silently broken** (`include/logger.hpp:54`): `printf(StringUtils::format(...).c_str(), args...)` already substitutes the format, then passes leftover variadic args to a string with no remaining `%` specifiers — every error log loses its arguments.
|
||||
- **Permission bug** in `source/io.cpp:119`: `mkdir(path.c_str(), 777)` — decimal 777 is octal 1411 (no read for owner). Has been wrong since day one.
|
||||
- One monster function: `io::restore` is 112 lines mixing mount, create, clear, copy, commit.
|
||||
- Russian comment in `io.cpp:231` (mixed languages).
|
||||
- Dead commented-out code in `util.cpp:50-58` and `logger.hpp:51-54`.
|
||||
- No `.clang-format`, no `.clang-tidy`, no `.editorconfig`, no `.gitattributes`, no CI, no `README.md`, no `ARCHITECTURE.md`, no `LICENSE` file (despite `io.cpp` carrying GPLv3 from Checkpoint).
|
||||
|
||||
Goal: transform NXST into a layered, conventionally-named, tooled-up project that builds on Switch via CMake and is readable by a stranger in under ten minutes. Solo developer; bar is "professional indie", not enterprise.
|
||||
|
||||
User decisions (already made):
|
||||
- **Full 8-phase plan**.
|
||||
- **Migrate Make → CMake** with devkitpro toolchain file.
|
||||
- **camelCase for functions, PascalCase for classes**, snake_case for files and locals, `kPascalCase` for constants.
|
||||
- **No tests** (Switch homebrew rarely has them; pure-logic surface is small).
|
||||
|
||||
---
|
||||
|
||||
## Target Directory Tree
|
||||
|
||||
```
|
||||
NXST/
|
||||
├── .clang-format # LLVM-derived, 4-space, 110 col
|
||||
├── .clang-tidy # bugprone-*, readability-*, modernize-*
|
||||
├── .editorconfig # LF, UTF-8, trim WS, final newline
|
||||
├── .gitattributes # eol=lf; lib/Plutonium linguist-vendored
|
||||
├── .github/workflows/build.yml # CI: devkitpro container, cmake, .nro upload
|
||||
├── docs/
|
||||
│ ├── ARCHITECTURE.md
|
||||
│ ├── PROTOCOL.md
|
||||
│ └── screenshots/
|
||||
├── cmake/
|
||||
│ └── modules/ # FindPlutonium.cmake (if needed)
|
||||
├── include/nxst/
|
||||
│ ├── app/ # MainApplication, AppContext
|
||||
│ ├── domain/ # Title, Account, TransferState, Result
|
||||
│ ├── infra/
|
||||
│ │ ├── fs/ # filesystem, directory, RAII handles
|
||||
│ │ ├── net/ # socket, sendAll/recvAll
|
||||
│ │ └── sys/ # logger, threading, switch services
|
||||
│ ├── service/ # TransferService
|
||||
│ └── ui/ # layouts, widgets, theme
|
||||
├── src/ # mirrors include/nxst/
|
||||
│ ├── app/
|
||||
│ ├── domain/
|
||||
│ ├── infra/{fs,net,sys}/
|
||||
│ ├── service/
|
||||
│ ├── ui/
|
||||
│ └── main.cpp
|
||||
├── tools/format.sh # one-line clang-format helper
|
||||
├── lib/Plutonium/ # submodule, untouched
|
||||
├── deps/asprintf/ # untouched
|
||||
├── CMakeLists.txt # top-level
|
||||
├── README.md
|
||||
├── ARCHITECTURE.md # → docs/ARCHITECTURE.md
|
||||
├── CHANGELOG.md
|
||||
├── LICENSE # GPLv3 (forced by Checkpoint inheritance)
|
||||
└── icon.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naming Convention Spec
|
||||
|
||||
| Axis | Rule | Examples |
|
||||
|---|---|---|
|
||||
| Files | `snake_case.{cpp,hpp}` | `transfer_service.cpp`, `socket.hpp` |
|
||||
| Classes / structs | `PascalCase` | `TransferService`, `AccountProfile`, `Socket` |
|
||||
| Functions / methods | `camelCase` | `sendAll()`, `recvAll()`, `replaceUsername()` |
|
||||
| Locals / params | `snake_case` | `int sock_fd`, `std::string file_name` |
|
||||
| Members | `snake_case`, no prefix | `bytes_done`, `client_sock` |
|
||||
| Constants / constexpr | `kPascalCase` | `kTcpPort`, `kBufSize`, `kMulticastGroup` |
|
||||
| Enums | `enum class Foo { Bar, Baz }` | `enum class TransferKind { Send, Receive }` |
|
||||
| Macros (avoid) | `NXST_UPPER_SNAKE` | `NXST_UNREACHABLE` |
|
||||
| Namespaces | lowercase, short | `nxst`, `nxst::net`, `nxst::ui` |
|
||||
|
||||
---
|
||||
|
||||
## Layering (dependency direction)
|
||||
|
||||
```
|
||||
┌────────────────────────┐
|
||||
│ Presentation (ui/) │
|
||||
│ layouts, widgets │
|
||||
└───────────┬────────────┘
|
||||
│
|
||||
┌───────────▼────────────┐
|
||||
│ Service (svc/) │
|
||||
│ TransferService │
|
||||
└───────────┬────────────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌────────▼──────┐ ┌────────▼──────┐
|
||||
│ Domain │ │ Infra/net │ │ Infra/fs │ Infra/sys
|
||||
│ Title, │ │ Socket, │ │ Directory, │ Logger,
|
||||
│ Account, │ │ sendAll, │ │ FsHandle, │ Thread,
|
||||
│ Result<T> │ │ recvAll │ │ FileHandle │ Account svc
|
||||
└─────────────┘ └───────────────┘ └───────────────┘
|
||||
│
|
||||
libnx, SDL, Plutonium
|
||||
```
|
||||
|
||||
**Hard rules.** `domain/` depends on nothing in the project. `infra/` depends only on `domain/`. `service/` depends on `domain/` + `infra/`. `ui/` depends only on `service/` + `domain/` — **must not include `<arpa/inet.h>`, `<sys/socket.h>`, `pthread.h`, or call `recv`/`send` directly**. `app/` wires everything.
|
||||
|
||||
CI enforcement: a 20-line shell step greps `src/ui/**` for forbidden headers.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Tooling and ground rules (S, ~2h)
|
||||
|
||||
**Goal.** Lay down conventions before touching code.
|
||||
|
||||
**Files created.**
|
||||
- `.clang-format`: BasedOnStyle LLVM, IndentWidth 4, ColumnLimit 110, PointerAlignment Left, IncludeBlocks Regroup with three categories (system, third-party, project).
|
||||
- `.clang-tidy`: enable `bugprone-*`, `readability-*` (minus `magic-numbers`), `modernize-*` (minus `use-trailing-return-type`, `use-nodiscard`), `cppcoreguidelines-pro-type-cstyle-cast`. Disable `fuchsia-*`, `google-*`, `llvm-header-guard`.
|
||||
- `.editorconfig`: LF, UTF-8, 4-space, trim trailing whitespace, final newline. Tab indent for `Makefile` (only while it still exists).
|
||||
- `.gitattributes`: `* text=auto eol=lf`, `lib/Plutonium/** linguist-vendored=true`, `deps/** linguist-vendored=true`. Stops GitHub mis-classifying the repo language.
|
||||
- `tools/format.sh`: `clang-format -i` over `src/` + `include/`.
|
||||
|
||||
**Risk.** None — no compiled changes.
|
||||
|
||||
**Buildable?** Unaffected.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Bug fixes and dead code (S, ~3h)
|
||||
|
||||
**Goal.** Fix concrete bugs before any structural change can mask them.
|
||||
|
||||
**Tasks.**
|
||||
1. Replace broken `Logger` (`include/logger.hpp`) with free-function API in `include/nxst/infra/sys/log.hpp`:
|
||||
```cpp
|
||||
namespace nxst::log {
|
||||
enum class Level { Debug, Info, Warn, Error };
|
||||
void write(Level, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void info (const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void warn (const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void error(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
}
|
||||
```
|
||||
Implementation: `vsnprintf` into stack buffer, prefix with `[YYYY-MM-DD HH:MM:SS][LEVEL]`, append to `/switch/NXST/log.log` and stderr. TU-local `std::mutex`. No singleton class. Drop the GPLv3 Checkpoint header (the new file is original).
|
||||
- `format(printf, ...)` attribute gives compile-time format-string checking — that is the actual reason this rewrite matters.
|
||||
- Leave thin shim `LOG_INFO(...)`/`LOG_ERROR(...)` macros so existing call sites keep compiling during phase migration.
|
||||
2. Fix `source/io.cpp:119` — `mkdir(path.c_str(), 777)` → `mkdir(path.c_str(), 0777)`.
|
||||
3. Translate Russian comment at `source/io.cpp:231` to English.
|
||||
4. Delete dead commented-out code in `source/util.cpp:50-58` and old `include/logger.hpp:51-54` block.
|
||||
|
||||
**Risk.** Logger API change ripples to <30 call sites. Macros bridge the gap.
|
||||
|
||||
**Buildable?** Yes after each file.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — File renames + `#pragma once` (S, ~2h)
|
||||
|
||||
**Goal.** Land the filename convention before the directory move so git history sees rename + move as separate commits.
|
||||
|
||||
**Tasks.**
|
||||
- Rename to `snake_case` via `git mv` (use two-step on case-insensitive macOS): `Main.cpp`→`main.cpp`, `MainApplication.{cpp,hpp}`→`main_application.{cpp,hpp}`, `TitlesLayout.{cpp,hpp}`→`titles_layout.{cpp,hpp}`, `UsersLayout.{cpp,hpp}`→`users_layout.{cpp,hpp}`, plus matching headers under `include/`.
|
||||
- Convert all `#ifndef X_HPP / #define X_HPP / #endif` guards to `#pragma once`. Affected: `logger.hpp`, `account.hpp`, and the 7 other headers using legacy guards.
|
||||
- Update affected `#include` lines.
|
||||
- `Makefile` `SOURCES` globs `*.cpp`, so no Makefile change needed for renames.
|
||||
|
||||
**Risk.** macOS case-insensitive FS hides case-only renames — use `git mv Foo.cpp tmp.cpp && git mv tmp.cpp foo.cpp`.
|
||||
|
||||
**Buildable?** Yes after each pair.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Directory restructure (M, ~1 day)
|
||||
|
||||
**Goal.** Move flat `source/` and `include/` into the layered tree. **No code changes**, just relocation + include-path updates.
|
||||
|
||||
**Tasks.**
|
||||
1. Create the `src/{app,domain,infra/{net,fs,sys},service,ui}` and `include/nxst/{...}` skeleton.
|
||||
2. Move files:
|
||||
- `protocol.hpp`, `transfer_state.hpp`, `account.hpp` (the `AccountUid` struct + ordering only), `title.hpp/cpp`, `common.hpp/cpp`, `util.hpp/cpp` → `domain/`.
|
||||
- `client.{hpp,cpp}` → `infra/net/transfer_sender.{hpp,cpp}`, `server.{hpp,cpp}` → `infra/net/transfer_receiver.{hpp,cpp}`. Renaming is intentional: `CLAUDE.md` already documents that "server = receiver, client = sender" is inverted from typical usage; renaming kills the confusion permanently.
|
||||
- `net/socket.hpp` → `infra/net/socket.hpp`.
|
||||
- `io.hpp/cpp`, `directory.hpp/cpp`, `filesystem.hpp/cpp` → `infra/fs/`.
|
||||
- new `log.hpp/cpp`, switch-service init pieces from `account.cpp` → `infra/sys/`.
|
||||
- `main_application.{hpp,cpp}`, `main.{hpp,cpp}` → `app/`.
|
||||
- `titles_layout.*`, `users_layout.*`, `transfer_overlay.hpp`, `theme.hpp`, `ui/*.hpp`, `header_bar.hpp` → `ui/`.
|
||||
3. Update `Makefile` (still in use — CMake migration is Phase 4):
|
||||
```make
|
||||
SOURCES := src 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
|
||||
```
|
||||
Remove the now-defunct `include/net` from `INCLUDES`.
|
||||
4. Update **every** `#include` to `#include <nxst/...>` form: `#include <nxst/domain/protocol.hpp>`, `#include <nxst/infra/net/socket.hpp>`. The single root `include/` keeps the prefix mandatory and grepable.
|
||||
|
||||
**Risk.** Big disruptive phase. Do on a feature branch, build after every directory's worth of moves. `service/` directory stays empty for now — scaffolding only.
|
||||
|
||||
**Buildable?** Yes if you move-and-rebuild incrementally.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Migrate Make → CMake (M, ~1 day)
|
||||
|
||||
**Goal.** Replace `Makefile` with `CMakeLists.txt` using devkitpro's bundled `Switch.cmake` toolchain file. Project compiles via `cmake -B build -DCMAKE_TOOLCHAIN_FILE=$DEVKITPRO/cmake/Switch.cmake && cmake --build build`.
|
||||
|
||||
**Tasks.**
|
||||
1. Create top-level `CMakeLists.txt`:
|
||||
- `cmake_minimum_required(VERSION 3.20)`, `project(NXST CXX)`.
|
||||
- C++17, `-fno-rtti -fno-exceptions` (matches current `CXXFLAGS`).
|
||||
- ARM flags `-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE` mirror current `ARCH`.
|
||||
- `find_package(PkgConfig REQUIRED)` + `pkg_check_modules(SWITCH_LIBS REQUIRED SDL2_ttf SDL2_gfx SDL2_image SDL2_mixer freetype2 harfbuzz minizip libpng libjpeg libwebp glesv2 egl glapi zlib)` against `aarch64-none-elf-pkg-config` (already wired up per memory ID 101).
|
||||
- Manual link: `pu` (Plutonium), `drm_nouveau`, plus the trailing `-lharfbuzz -lfreetype -lz` static-link-order workaround (memory IDs 97, 98, 100 — required after recent libnx update).
|
||||
- Add Plutonium as `add_subdirectory(lib/Plutonium)` if its CMake exists, else as an INTERFACE library wrapping its prebuilt `.a`. Inspection during this phase decides.
|
||||
- Use devkitpro's `nx_create_nro` / `nx_generate_nacp` helpers from the toolchain file to emit `NXST.nro` with icon + NACP metadata (`APP_TITLE=NXST`, `APP_AUTHOR=DragonSpirit`, `APP_VERSION=04.26.2026`).
|
||||
2. Create `cmake/modules/` if any custom Find module proves necessary (likely not).
|
||||
3. Add convenience targets: `cmake --build build --target send` to invoke `nxlink NXST.nro`.
|
||||
4. Delete old `Makefile` only after CMake build produces a working `.nro` matching the Make output.
|
||||
5. Update `.gitignore`: replace `build/` Make artifacts with CMake's `build/` directory rules and `CMakeCache.txt`, `CMakeFiles/`, `compile_commands.json`.
|
||||
|
||||
**Risk.** Highest in the plan. Mitigation:
|
||||
- Keep `Makefile` alongside CMake during the phase. Verify `.nro` from both is identical (or at minimum boots and runs on hardware).
|
||||
- If the devkitpro `Switch.cmake` toolchain proves brittle for Plutonium's transitive headers, fall back to keeping Make and document why CMake was deferred.
|
||||
- The pkg-config tweaks captured in memory IDs 96–101 are non-obvious — re-apply them in the CMake config explicitly, not via `pkg_check_modules` defaults.
|
||||
|
||||
**Buildable?** Yes — both build systems coexist until CMake is verified.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — TransferService extraction, kill globals (L, ~2 days)
|
||||
|
||||
**Goal.** Sever the UI → net coupling. Single biggest "this looks like real software" change.
|
||||
|
||||
**Tasks.**
|
||||
1. Create `src/service/transfer_service.{hpp,cpp}`. Class owns:
|
||||
- one `TransferState` per direction (sender, receiver),
|
||||
- all listen / accept / broadcast threads (currently 6 globals in `server.cpp`, 4 in `client.cpp`),
|
||||
- public API: `start(TransferKind, AccountUid, std::vector<TitleId>)`, `cancel()`, `progress()`, `statusText()`, `isDone()`, `failureReason()`, `setOnComplete(std::function<void(TransferResult)>)`.
|
||||
2. Move file globals (`g_server_state`, `g_client_state`, all socket/pthread atomics) into `TransferService` private members. pthread C trampolines: `static void* threadEntry(void* self)` immediately calls `*static_cast<TransferService*>(self)`.
|
||||
3. Refactor `titles_layout.cpp` `runTransfer`/`runReceive` to call `app_ctx.transfer.start(...)` / `cancel()` instead of free `transfer_files()` / `start_listening()`. Inject via `AppContext&` (already partially present per `include/ui/UiContext.hpp`).
|
||||
4. Move `g_currentUId` out of global scope into `AppContext::current_user`. Pass `AppContext&` down to layouts at construction. There is exactly one `AppContext` and its lifetime equals the app — safe and cheap.
|
||||
5. UI's `runReceive` currently calls `io::restore` directly after server work completes. Move into the service's completion callback so UI never touches FS directly.
|
||||
|
||||
**Strategy.** Keep old `transfer_files()` / `start_listening()` free functions as thin wrappers around `TransferService` for the duration of the phase. Convert UI call sites one at a time. Delete wrappers only after both sites are on the service.
|
||||
|
||||
**Buildable?** Yes via the wrappers.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — `Result<T>` and RAII (M, ~1 day)
|
||||
|
||||
**Goal.** Replace bool/out-param/silent-failure patterns with `Result<T>`; wrap raw OS handles in RAII.
|
||||
|
||||
**Tasks.**
|
||||
1. `include/nxst/domain/result.hpp` — minimal `Result<T, E=std::string>` with tagged union (no `std::variant` because `-fno-exceptions` rules out monostate-on-throw):
|
||||
```cpp
|
||||
template <class T, class E = std::string>
|
||||
class Result {
|
||||
bool ok_;
|
||||
union { T val_; E err_; };
|
||||
public:
|
||||
static Result success(T v);
|
||||
static Result failure(E e);
|
||||
bool isOk() const noexcept;
|
||||
const T& value() const; // UB if !isOk
|
||||
const E& error() const; // UB if isOk
|
||||
template <class F> auto map(F&&) const;
|
||||
};
|
||||
```
|
||||
Resist importing `tl::expected`. 60 lines of in-house code beats a vendored dep at this scale.
|
||||
2. RAII wrappers in `include/nxst/infra/`:
|
||||
- `FdHandle` — owns `int fd`, closes on dtor, move-only.
|
||||
- `FsFileSystemHandle` — owns `FsFileSystem`, calls `fsFsClose`. Plugs the "save"-mount leak from memory ID 59.
|
||||
- `AccountProfileHandle` — owns `AccountProfile`, calls `accountProfileClose`. Plugs the bug from memory ID 64.
|
||||
- `FileHandle` — owns `FILE*`, `fclose` on dtor.
|
||||
3. Replace `io.cpp:57` `new u8[BUFFER_SIZE] / delete[]` and `io.cpp:234` `malloc/free` with `std::vector<u8>` (already the pattern in `client.cpp`/`server.cpp`).
|
||||
4. Split `io::restore` (the 112-line monster, `io.cpp:221-332`) into `restoreSaveForTitle`, `extractAndCommit`, `clearStaleSaveFiles`. Each returns `Result<void>`.
|
||||
5. Convert `recvAll` / `sendAll` / socket setup paths in `infra/net` to return `Result<void>` so error paths compose under `-fno-exceptions`.
|
||||
|
||||
**Strategy.** Adopt incrementally — start at IO boundaries, leave `bool` returns untouched in modules not yet refactored.
|
||||
|
||||
**Buildable?** Yes per file.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Documentation + license (S, ~half day)
|
||||
|
||||
**Goal.** A stranger can build, run, and understand this in under 15 minutes.
|
||||
|
||||
**Files created.**
|
||||
- `README.md`: what NXST does, screenshot, install (drop `.nro` into `/switch/`), build (devkitpro + `cmake --build`), credit Plutonium and Checkpoint, link to `docs/`.
|
||||
- `docs/ARCHITECTURE.md`: layering diagram from this plan, threading model (one accept thread, one broadcast thread, one transfer thread per direction; cancellation via `shutdown(2)` + atomic flag), pointers to key files.
|
||||
- `docs/PROTOCOL.md`: promote the 9-line wire-format comment from `protocol.hpp` into a real spec — byte-level diagrams, multicast discovery on `239.0.0.1:8081`, TCP transfer on `:8080`, EOF sentinel = `filename_len == 0`, `kBufSize=65536`, `kMaxFilename=4096`.
|
||||
- `CHANGELOG.md`: Keep-a-Changelog format. Version `0.x` until protocol stabilizes.
|
||||
- `LICENSE`: GPLv3 — forced by `io.cpp` carrying Checkpoint's GPLv3 header. Document credit + license inheritance in README.
|
||||
|
||||
**Risk.** None.
|
||||
|
||||
**Buildable?** Unaffected.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — CI (S, ~2h)
|
||||
|
||||
**Goal.** Every push verifies `.nro` builds. One green badge tells visitors the repo is alive.
|
||||
|
||||
**Tasks.**
|
||||
- `.github/workflows/build.yml`:
|
||||
```yaml
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
nro:
|
||||
runs-on: ubuntu-latest
|
||||
container: devkitpro/devkita64:latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { submodules: recursive }
|
||||
- run: |
|
||||
cmake -B build \
|
||||
-DCMAKE_TOOLCHAIN_FILE=$DEVKITPRO/cmake/Switch.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: NXST-${{ github.sha }}
|
||||
path: build/NXST.nro
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
sudo apt-get install -y clang-format
|
||||
find src include -name '*.cpp' -o -name '*.hpp' \
|
||||
| xargs clang-format --dry-run --Werror
|
||||
layering:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
! grep -rE '^#include\s*[<"](arpa/|sys/socket|pthread\.h)' src/ui/
|
||||
```
|
||||
- Optional release job on tag `v*`: same CMake build, attach `.nro` to GitHub Release.
|
||||
|
||||
**Risk.** First run typically fails on submodule init or container path — iterate on a branch.
|
||||
|
||||
**Buildable?** Pass or fix.
|
||||
|
||||
---
|
||||
|
||||
## Critical files to modify
|
||||
|
||||
- `Makefile` — Phase 3 SOURCES/INCLUDES update; deleted in Phase 4 after CMake parity.
|
||||
- `include/logger.hpp` — Phase 1 replace with `nxst::log` free-function API.
|
||||
- `source/io.cpp` — Phase 1 mkdir + comment fix; Phase 6 split `restore()` + RAII; Phase 5 stop being called from UI.
|
||||
- `source/server.cpp` — Phase 3 rename to `transfer_receiver.cpp`; Phase 5 globals migrate into `TransferService`.
|
||||
- `source/client.cpp` — Phase 3 rename to `transfer_sender.cpp`; Phase 5 globals migrate into `TransferService`.
|
||||
- `source/TitlesLayout.cpp` — Phase 2 rename; Phase 5 sever direct calls into network and `io::restore`.
|
||||
- `source/Main.cpp` — Phase 2 rename; Phase 5 construct `AppContext` with `TransferService`.
|
||||
- `source/util.cpp` — Phase 1 delete dead block.
|
||||
|
||||
New files (not yet present):
|
||||
- `.clang-format`, `.clang-tidy`, `.editorconfig`, `.gitattributes` — Phase 0.
|
||||
- `CMakeLists.txt` — Phase 4.
|
||||
- `include/nxst/domain/result.hpp` — Phase 6.
|
||||
- `include/nxst/service/transfer_service.hpp` + `src/service/transfer_service.cpp` — Phase 5.
|
||||
- `README.md`, `docs/ARCHITECTURE.md`, `docs/PROTOCOL.md`, `CHANGELOG.md`, `LICENSE` — Phase 7.
|
||||
- `.github/workflows/build.yml` — Phase 8.
|
||||
|
||||
---
|
||||
|
||||
## What is deliberately NOT in this plan
|
||||
|
||||
1. **C++20.** Devkitpro's `gnu++17` is fine; nothing in the codebase wants concepts/ranges/coroutines, and coroutines under `-fno-exceptions` are a maze. Stay on `gnu++17`.
|
||||
2. **Abstracting Plutonium.** It is *the* UI framework; you will not port it. UI calls Plutonium directly.
|
||||
3. **`tl::expected` / `std::expected`.** 60-line in-house `Result<T>` is enough.
|
||||
4. **DI containers, factories, abstract logger interfaces.** Solo developer, one service, one logger sink. Add complexity only when a second sink actually appears.
|
||||
5. **Integration tests against the network.** No bug class is meaningfully prevented; cost is high; value comes from running on real hardware.
|
||||
6. **Migrating from pthread to `std::thread`.** Libnx's pthread is the supported path. A single `Thread` RAII wrapper inside `infra/sys/` is the most you should add.
|
||||
7. **PImpl broadly.** At 2.3 KLOC compile time is sub-second; PImpl just adds indirection.
|
||||
8. **Re-vendoring or modifying Plutonium.** Submodule, untouched. `linguist-vendored` keeps it out of GitHub language stats.
|
||||
9. **UPPER_SNAKE constants.** Visually clash with macros; `kPascalCase` is the modern norm.
|
||||
|
||||
---
|
||||
|
||||
## Recommended phasing for a solo developer
|
||||
|
||||
- **Weekend 1:** Phase 0 + 1 + 2. Tooled, bugs fixed, files renamed, builds. Already feels different.
|
||||
- **Weekend 2:** Phase 3. Big move, one sitting on a branch.
|
||||
- **Weekend 3:** Phase 4 (CMake). Build system swap. Highest individual phase risk — schedule when fresh.
|
||||
- **Weekend 4 (longest):** Phase 5 (TransferService). UI no longer touches sockets. The win.
|
||||
- **Weekend 5:** Phase 6. `Result<T>`, RAII, `restore()` split.
|
||||
- **Evening:** Phase 7. README + ARCHITECTURE + LICENSE.
|
||||
- **Evening:** Phase 8. CI.
|
||||
|
||||
Stopping after Weekend 4 still leaves the project visibly more serious than it started. Each weekend produces a defensible "ship it" state.
|
||||
|
||||
---
|
||||
|
||||
## Verification (end-to-end test plan)
|
||||
|
||||
After each phase:
|
||||
1. `cmake --build build` (Phase 4+) or `make` produces `NXST.nro` with no warnings.
|
||||
2. Copy `NXST.nro` to Switch SD card `/switch/NXST/`, launch via hbmenu, verify the title list and user list render (regression tests for memory IDs 73, 78).
|
||||
3. Run a save transfer between two Switches: select a title on the sender, "Receive" on the receiver, verify the file lands and `io::restore` replaces it on the receiver. Verify cancel works mid-transfer (regression for memory IDs 60, 103, 104).
|
||||
4. Verify the log file at `/switch/NXST/log.log` contains formatted entries with timestamps and levels (regression for the broken-Logger bug).
|
||||
5. Phase 8 only: confirm a green CI run on a PR; download the `NXST.nro` artifact; install it on Switch; run the same flow.
|
||||
@@ -0,0 +1,88 @@
|
||||
# NXST
|
||||
|
||||
Transfer save data between two Nintendo Switch consoles over local Wi-Fi.
|
||||
|
||||
Pick a game, pick a user. One Switch sends; the other receives. The sender backs up its own save first, so you can never lose data in transit.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
1. Download `NXST.nro` from [Releases](../../releases).
|
||||
2. Copy it to `/switch/NXST/NXST.nro` on your SD card.
|
||||
3. Launch via hbmenu (hold R while starting any game, or Album).
|
||||
|
||||
Both Switches must be on the same Wi-Fi network. No router configuration needed — discovery uses UDP multicast.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
**Sender** (the Switch whose save you want to copy):
|
||||
|
||||
1. Open NXST → select a title → press **A** → **Transfer**.
|
||||
2. Wait for "Waiting for receiver…" to change to "Transferring…".
|
||||
|
||||
**Receiver** (the Switch that will receive the save):
|
||||
|
||||
1. Open NXST → select the same title → press **A** → **Receive**.
|
||||
2. Wait. The save is restored automatically when the transfer finishes.
|
||||
|
||||
Press **B** on either side to cancel mid-transfer.
|
||||
|
||||
Logs are written to `/switch/NXST/log.log`.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
**Prerequisites:** [devkitPro](https://devkitpro.org/wiki/Getting_Started) with `switch-dev` and `switch-portlibs` packages, plus `cmake ≥ 3.20`.
|
||||
|
||||
```bash
|
||||
# Clone with submodules (Plutonium UI)
|
||||
git clone --recurse-submodules https://github.com/your-username/NXST.git
|
||||
cd NXST
|
||||
|
||||
# Configure (once)
|
||||
cmake --preset switch
|
||||
|
||||
# Build
|
||||
cmake --build build
|
||||
|
||||
# Send to Switch via nxlink (Switch must be on same network, nxlink running)
|
||||
cmake --build build --target send
|
||||
```
|
||||
|
||||
Output: `build/NXST.nro`
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the layer diagram, threading model, and key types.
|
||||
|
||||
See [`docs/PROTOCOL.md`](docs/PROTOCOL.md) for the wire protocol (UDP multicast discovery + TCP file stream).
|
||||
|
||||
```
|
||||
ui/ — TitlesLayout, UsersLayout, TransferOverlay, HeaderBar
|
||||
service/ — TransferService (all network threads and state)
|
||||
infra/net/ — Socket RAII, sendAll/recvAll
|
||||
infra/fs/ — io::backup, io::restore, directory iterator, RAII handles
|
||||
infra/sys/ — nxst::log (printf-checked, timestamped)
|
||||
domain/ — Title, Account, Result<T>, TransferState, protocol constants
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
- **[Plutonium](https://github.com/XorTroll/Plutonium)** — Switch UI framework by XorTroll
|
||||
- **[Checkpoint](https://github.com/BernardoGiordano/Checkpoint)** — save management library by Bernardo Giordano / FlagBrew; several files in `src/infra/fs/` and `src/domain/` are derived from Checkpoint
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
GPLv3 — see [`LICENSE`](LICENSE).
|
||||
|
||||
NXST includes code derived from Checkpoint (GPLv3). All original NXST code is released under the same license to satisfy the GPL inheritance requirement.
|
||||
@@ -0,0 +1,163 @@
|
||||
# NXST Architecture
|
||||
|
||||
## Layer Diagram
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ ui/ │
|
||||
│ TitlesLayout, UsersLayout │
|
||||
│ TransferOverlay, HeaderBar │
|
||||
└──────────────┬───────────────┘
|
||||
│ calls
|
||||
┌──────────────▼───────────────┐
|
||||
│ service/ │
|
||||
│ TransferService │
|
||||
└──────┬───────────────┬───────┘
|
||||
│ │
|
||||
┌────────────▼──┐ ┌───────▼────────┐
|
||||
│ infra/net/ │ │ infra/fs/ │
|
||||
│ TransferService│ │ io, directory │
|
||||
│ (sender thread │ │ filesystem │
|
||||
│ recv threads) │ │ handles │
|
||||
└────────────┬──┘ └───────┬────────┘
|
||||
│ │
|
||||
┌────────────▼───────────────▼────────┐
|
||||
│ domain/ │
|
||||
│ Title, Account, TransferState │
|
||||
│ Result<T>, protocol constants │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────▼────────────────┐
|
||||
│ libnx, Plutonium, SDL2, devkitpro│
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Hard layering rules:**
|
||||
- `domain/` depends on nothing in the project.
|
||||
- `infra/` depends only on `domain/`.
|
||||
- `service/` depends on `domain/` + `infra/`.
|
||||
- `ui/` depends only on `service/` + `domain/` — must not include `<arpa/inet.h>`, `<sys/socket.h>`, `<pthread.h>`, or call `recv`/`send` directly.
|
||||
- `app/` (MainApplication, main.cpp) wires everything together.
|
||||
|
||||
---
|
||||
|
||||
## Key Types
|
||||
|
||||
### `nxst::TransferService` (`include/nxst/service/transfer_service.hpp`)
|
||||
|
||||
Owns all network state for both transfer directions. One instance lives in `MainApplication`.
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `startSend(index, uid)` | Backup save, discover receiver, stream files |
|
||||
| `startReceive(index, uid, title_name)` | Listen for sender, receive files, restore save |
|
||||
| `cancelSend()` / `cancelReceive()` | Interrupt in-flight transfer (socket shutdown) |
|
||||
| `isSendDone()` / `isReceiveDone()` | Polled by UI render loop |
|
||||
| `restoreSucceeded()` / `restoreError()` | Restore outcome after receive |
|
||||
|
||||
**Threading model:**
|
||||
|
||||
```
|
||||
UI thread (Plutonium event loop)
|
||||
└─ startSend()
|
||||
└─ senderEntry [pthread, detached]
|
||||
├─ findServer() — UDP multicast, 100 ms poll slices
|
||||
├─ io::backup() — creates local save backup
|
||||
└─ TCP send loop
|
||||
|
||||
└─ startReceive()
|
||||
├─ broadcastEntry [pthread, detached] — UDP multicast listener
|
||||
└─ acceptEntry [pthread, detached]
|
||||
├─ TCP receive loop
|
||||
└─ io::restore() — mounts save, clears, copies, commits
|
||||
```
|
||||
|
||||
Cancellation: `cancelSend()` / `cancelReceive()` call `shutdown(SHUT_RDWR)` on all open sockets. Blocking `read`/`recvfrom`/`accept` return errors immediately. Atomic flags are checked at loop boundaries.
|
||||
|
||||
### `nxst::Result<T, E>` (`include/nxst/domain/result.hpp`)
|
||||
|
||||
85-line tagged-union result type. No exceptions, no `std::variant`. Used for `io::backup` and `io::restore` return values.
|
||||
|
||||
```cpp
|
||||
auto result = io::backup(title_index, uid);
|
||||
if (!result.isOk()) {
|
||||
failSend("Backup failed: " + result.error());
|
||||
return;
|
||||
}
|
||||
fs::path dir = result.value();
|
||||
```
|
||||
|
||||
`Result<void, E>` specialisation available for operations that succeed without a value.
|
||||
|
||||
### `nxst::FsFileSystemHandle` / `nxst::FileHandle` (`include/nxst/infra/fs/handles.hpp`)
|
||||
|
||||
RAII wrappers ensuring `fsFsClose` / `fclose` are called on all exit paths.
|
||||
|
||||
---
|
||||
|
||||
## Threading Invariants
|
||||
|
||||
- All `std::atomic` members in `TransferService` use sequential-consistency (default). No explicit `memory_order` needed at this concurrency level.
|
||||
- `TransferState::status` is a `std::string` protected by `std::mutex status_mutex`. All other `TransferState` fields are atomics.
|
||||
- `pthread_create` + `pthread_detach` is used throughout (libnx's supported path). Threads are never joined — they signal completion via `state.done = true` and set their active flag to false.
|
||||
- Cancel is safe to call from any thread.
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
```
|
||||
include/nxst/
|
||||
├── app/
|
||||
│ ├── main.hpp — global state (g_currentTime, g_sortMode, …)
|
||||
│ └── main_application.hpp — MainApplication : pu::ui::Application
|
||||
├── domain/
|
||||
│ ├── account.hpp — AccountUid, Account::ids(), Account::username()
|
||||
│ ├── common.hpp — StringUtils (UTF-8/16, elide, accents)
|
||||
│ ├── protocol.hpp — wire constants (ports, sentinel, buffer size)
|
||||
│ ├── result.hpp — Result<T, E> tagged union
|
||||
│ ├── title.hpp — Title, getTitle(), getTitleCount()
|
||||
│ ├── transfer_state.hpp — TransferState (atomics + mutex-guarded status)
|
||||
│ └── util.hpp — StringUtils helpers, blinkLed
|
||||
├── infra/
|
||||
│ ├── fs/
|
||||
│ │ ├── directory.hpp — Directory iterator (libnx FsDir)
|
||||
│ │ ├── filesystem.hpp — FileSystem::mount/unmount wrappers
|
||||
│ │ ├── handles.hpp — FsFileSystemHandle, FileHandle (RAII)
|
||||
│ │ └── io.hpp — io::backup, io::restore, io::copyFile, …
|
||||
│ ├── net/
|
||||
│ │ └── socket.hpp — Socket RAII wrapper (int fd)
|
||||
│ └── sys/
|
||||
│ └── logger.hpp — nxst::log::info/warn/error (printf-checked)
|
||||
├── service/
|
||||
│ └── transfer_service.hpp — TransferService
|
||||
└── ui/
|
||||
├── card.hpp — Card UI component
|
||||
├── const.h — layout/color/font constants
|
||||
├── header_bar.hpp — HeaderBar (title + user avatar row)
|
||||
├── hint_bar.hpp — HintBar (button legend)
|
||||
├── theme.hpp — color, radius, spacing, type tokens
|
||||
├── titles_layout.hpp — TitlesLayout
|
||||
├── transfer_overlay.hpp — TransferOverlay (progress modal)
|
||||
├── ui_context.hpp — UiContext (selected user state)
|
||||
└── users_layout.hpp — UsersLayout
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
# Configure (once — toolchain preset reads $DEVKITPRO automatically)
|
||||
cmake --preset switch
|
||||
|
||||
# Build
|
||||
cmake --build build
|
||||
|
||||
# Send to Switch via nxlink
|
||||
cmake --build build --target send
|
||||
```
|
||||
|
||||
Output: `build/NXST.nro`
|
||||
|
||||
See `README.md` for full build prerequisites.
|
||||
@@ -0,0 +1,95 @@
|
||||
# NXST Wire Protocol
|
||||
|
||||
## Overview
|
||||
|
||||
NXST uses two sockets per transfer session:
|
||||
|
||||
| Socket | Purpose |
|
||||
|--------|---------|
|
||||
| UDP multicast | Receiver advertisement (discovery) |
|
||||
| TCP | File data stream |
|
||||
|
||||
Both sides must be on the same local network segment. The sender (Transfer mode) initiates; the receiver (Receive mode) listens.
|
||||
|
||||
---
|
||||
|
||||
## Discovery — UDP Multicast
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Group | `239.0.0.1` |
|
||||
| Port | `8081` |
|
||||
| Direction | sender → receiver |
|
||||
|
||||
**Flow:**
|
||||
|
||||
1. Receiver joins multicast group and binds `0.0.0.0:8081`.
|
||||
2. Sender sends `"DISCOVER_SERVER"` (15 bytes, no null terminator) to `239.0.0.1:8081`.
|
||||
3. Receiver replies `"SERVER_HERE"` (11 bytes) to the sender's source address.
|
||||
4. Sender extracts the receiver's IP from the reply source and closes the UDP socket.
|
||||
|
||||
Sender polls in 100 ms slices for up to 3 seconds. Cancel is checked each slice.
|
||||
|
||||
---
|
||||
|
||||
## File Transfer — TCP
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Port | `8080` |
|
||||
| Direction | sender connects → receiver listens |
|
||||
| Buffer size | 65 536 bytes (`proto::BUF_SIZE`) |
|
||||
|
||||
**Connection:**
|
||||
|
||||
1. Receiver listens on `0.0.0.0:8080` (started concurrently with multicast listener).
|
||||
2. Sender connects after receiving `"SERVER_HERE"`.
|
||||
|
||||
**Wire layout — one file:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ filename_len │ uint32_t LE │ 4 bytes
|
||||
├─────────────────────────────────┤
|
||||
│ filename │ filename_len │ bytes, no null terminator
|
||||
│ │ bytes │
|
||||
├─────────────────────────────────┤
|
||||
│ file_size │ uint64_t LE │ 8 bytes
|
||||
├─────────────────────────────────┤
|
||||
│ file_data │ file_size │ bytes
|
||||
│ │ bytes │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
Files are sent sequentially. The stream ends with a sentinel frame:
|
||||
|
||||
```
|
||||
filename_len == 0 (proto::EOF_SENTINEL)
|
||||
```
|
||||
|
||||
No `filename` or `file_size` field follows the sentinel.
|
||||
|
||||
**Constraints:**
|
||||
|
||||
- `filename_len > proto::MAX_FILENAME` (4 096) is treated as a protocol error; the receiver aborts.
|
||||
- Filenames are full paths as produced by `io::backup` (e.g. `/switch/NXST/<title>/<user>/...`).
|
||||
- On the receiver, the username path component is rewritten to match the local user's nickname before writing to disk.
|
||||
|
||||
---
|
||||
|
||||
## Post-Transfer
|
||||
|
||||
After the TCP stream closes (sentinel received), the receiver calls `io::restore`:
|
||||
|
||||
1. Mounts the title's save filesystem.
|
||||
2. Clears existing save data.
|
||||
3. Copies received files into `save:/`.
|
||||
4. Commits via `fsdevCommitDevice("save")`.
|
||||
|
||||
The sender creates a local backup via `io::backup` before connecting, so the sender's own save is never at risk.
|
||||
|
||||
---
|
||||
|
||||
## Cancellation
|
||||
|
||||
Either side can cancel at any time by closing the relevant socket (`shutdown` + `close`). The other side's blocking read/write will return an error and the transfer loop exits cleanly. The receiver's accept thread sets `receiver_state.done = true` regardless of how the connection ends, so the UI poll loop always terminates.
|
||||
@@ -1,13 +0,0 @@
|
||||
#include <string>
|
||||
#include <switch.h>
|
||||
|
||||
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();
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#ifndef LOGGER_HPP
|
||||
#define LOGGER_HPP
|
||||
|
||||
#include "common.hpp"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
static Logger& getInstance(void)
|
||||
{
|
||||
static Logger mLogger;
|
||||
return mLogger;
|
||||
}
|
||||
|
||||
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]";
|
||||
|
||||
template <typename... Args>
|
||||
void log(const std::string& level, const std::string& format = {}, Args... 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)
|
||||
{
|
||||
mFile = fopen(mPath.c_str(), "a");
|
||||
if (mFile != NULL) {
|
||||
fprintf(mFile, buffer.c_str());
|
||||
fprintf(stderr, buffer.c_str());
|
||||
fclose(mFile);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Logger(void) { buffer = ""; }
|
||||
~Logger(void) {}
|
||||
|
||||
Logger(Logger const&) = delete;
|
||||
void operator=(Logger const&) = delete;
|
||||
|
||||
#if defined(__SWITCH__)
|
||||
const std::string mPath = "/switch/NXST/log.log";
|
||||
#else
|
||||
const std::string mPath = "log.log";
|
||||
#endif
|
||||
|
||||
FILE* mFile;
|
||||
|
||||
std::string buffer;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -1,17 +1,15 @@
|
||||
#ifndef MAIN_HPP
|
||||
#define MAIN_HPP
|
||||
#include <const.h>
|
||||
#include "account.hpp"
|
||||
#include "title.hpp"
|
||||
#include "util.hpp"
|
||||
#pragma once
|
||||
#include <nxst/ui/const.h>
|
||||
#include <nxst/domain/account.hpp>
|
||||
#include <nxst/domain/title.hpp>
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <memory>
|
||||
#include <switch.h>
|
||||
#include "logger.hpp"
|
||||
#include <nxst/infra/sys/logger.hpp>
|
||||
|
||||
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;
|
||||
@@ -23,4 +21,3 @@ inline std::string g_currentFile = "";
|
||||
inline bool g_isTransferringFile = false;
|
||||
inline const std::string g_emptySave = "New...";
|
||||
|
||||
#endif
|
||||
@@ -1,8 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <pu/Plutonium>
|
||||
#include <UsersLayout.hpp>
|
||||
#include <TitlesLayout.hpp>
|
||||
#include <nxst/service/transfer_service.hpp>
|
||||
#include <nxst/ui/users_layout.hpp>
|
||||
#include <nxst/ui/titles_layout.hpp>
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -14,8 +15,8 @@ namespace ui {
|
||||
|
||||
void OnLoad() override;
|
||||
|
||||
// Layout instance
|
||||
UsersLayout::Ref usersLayout;
|
||||
TitlesLayout::Ref titlesLayout;
|
||||
UsersLayout::Ref users_layout;
|
||||
TitlesLayout::Ref titles_layout;
|
||||
nxst::TransferService transfer;
|
||||
};
|
||||
}
|
||||
@@ -24,9 +24,7 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#ifndef ACCOUNT_HPP
|
||||
#define ACCOUNT_HPP
|
||||
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string.h>
|
||||
#include <string>
|
||||
@@ -73,4 +71,3 @@ namespace Account {
|
||||
std::string iconPath(AccountUid id);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -24,9 +24,7 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#ifndef COMMON_HPP
|
||||
#define COMMON_HPP
|
||||
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <arpa/inet.h>
|
||||
#include <codecvt>
|
||||
@@ -62,4 +60,3 @@ namespace StringUtils {
|
||||
|
||||
char* getConsoleIP(void);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace nxst {
|
||||
|
||||
template <class T, class E = std::string>
|
||||
class Result {
|
||||
bool ok;
|
||||
alignas(T) alignas(E) unsigned char storage[sizeof(T) > sizeof(E) ? sizeof(T) : sizeof(E)];
|
||||
|
||||
Result() = default;
|
||||
|
||||
public:
|
||||
static Result success(T val) {
|
||||
Result res;
|
||||
res.ok = true;
|
||||
new (res.storage) T(std::move(val));
|
||||
return res;
|
||||
}
|
||||
|
||||
static Result failure(E err) {
|
||||
Result res;
|
||||
res.ok = false;
|
||||
new (res.storage) E(std::move(err));
|
||||
return res;
|
||||
}
|
||||
|
||||
~Result() {
|
||||
if (ok) reinterpret_cast<T*>(storage)->~T();
|
||||
else reinterpret_cast<E*>(storage)->~E();
|
||||
}
|
||||
|
||||
Result(const Result& other) : ok(other.ok) {
|
||||
if (ok) new (storage) T(*reinterpret_cast<const T*>(other.storage));
|
||||
else new (storage) E(*reinterpret_cast<const E*>(other.storage));
|
||||
}
|
||||
|
||||
Result(Result&& other) : ok(other.ok) {
|
||||
if (ok) new (storage) T(std::move(*reinterpret_cast<T*>(other.storage)));
|
||||
else new (storage) E(std::move(*reinterpret_cast<E*>(other.storage)));
|
||||
}
|
||||
|
||||
Result& operator=(const Result&) = delete;
|
||||
|
||||
bool isOk() const noexcept { return ok; }
|
||||
const T& value() const { return *reinterpret_cast<const T*>(storage); }
|
||||
const E& error() const { return *reinterpret_cast<const E*>(storage); }
|
||||
};
|
||||
|
||||
// Specialisation for Result<void>
|
||||
template <class E>
|
||||
class Result<void, E> {
|
||||
bool ok;
|
||||
alignas(E) unsigned char storage[sizeof(E)];
|
||||
|
||||
Result() = default;
|
||||
|
||||
public:
|
||||
static Result success() { Result res; res.ok = true; return res; }
|
||||
|
||||
static Result failure(E err) {
|
||||
Result res;
|
||||
res.ok = false;
|
||||
new (res.storage) E(std::move(err));
|
||||
return res;
|
||||
}
|
||||
|
||||
~Result() { if (!ok) reinterpret_cast<E*>(storage)->~E(); }
|
||||
|
||||
Result(const Result& other) : ok(other.ok) {
|
||||
if (!ok) new (storage) E(*reinterpret_cast<const E*>(other.storage));
|
||||
}
|
||||
|
||||
Result(Result&& other) : ok(other.ok) {
|
||||
if (!ok) new (storage) E(std::move(*reinterpret_cast<E*>(other.storage)));
|
||||
}
|
||||
|
||||
Result& operator=(const Result&) = delete;
|
||||
|
||||
bool isOk() const noexcept { return ok; }
|
||||
const E& error() const { return *reinterpret_cast<const E*>(storage); }
|
||||
};
|
||||
|
||||
} // namespace nxst
|
||||
@@ -24,12 +24,10 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#ifndef TITLE_HPP
|
||||
#define TITLE_HPP
|
||||
|
||||
#include "account.hpp"
|
||||
#include "filesystem.hpp"
|
||||
#include "io.hpp"
|
||||
#pragma once
|
||||
#include <nxst/domain/account.hpp>
|
||||
#include <nxst/infra/fs/filesystem.hpp>
|
||||
#include <nxst/infra/fs/io.hpp>
|
||||
#include <algorithm>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
@@ -88,4 +86,3 @@ void rotateSortMode(void);
|
||||
void refreshDirectories(u64 id);
|
||||
std::unordered_map<std::string, std::string> getCompleteTitleList(void);
|
||||
|
||||
#endif
|
||||
@@ -24,12 +24,10 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#ifndef UTIL_HPP
|
||||
#define UTIL_HPP
|
||||
|
||||
#include "account.hpp"
|
||||
#include "common.hpp"
|
||||
#include "io.hpp"
|
||||
#pragma once
|
||||
#include <nxst/domain/account.hpp>
|
||||
#include <nxst/domain/common.hpp>
|
||||
#include <nxst/infra/fs/io.hpp>
|
||||
#include <switch.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
@@ -50,4 +48,4 @@ namespace StringUtils {
|
||||
std::string elide(const std::string& s, size_t maxChars);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -24,9 +24,7 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#ifndef DIRECTORY_HPP
|
||||
#define DIRECTORY_HPP
|
||||
|
||||
#pragma once
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <string>
|
||||
@@ -55,4 +53,3 @@ private:
|
||||
bool mGood;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -24,10 +24,8 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#ifndef FILESYSTEM_HPP
|
||||
#define FILESYSTEM_HPP
|
||||
|
||||
#include "account.hpp"
|
||||
#pragma once
|
||||
#include <nxst/domain/account.hpp>
|
||||
#include <switch.h>
|
||||
|
||||
namespace FileSystem {
|
||||
@@ -36,4 +34,3 @@ namespace FileSystem {
|
||||
void unmount(void);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
#include <cstdio>
|
||||
#include <switch.h>
|
||||
|
||||
namespace nxst {
|
||||
|
||||
// RAII wrapper for FsFileSystem — auto-closes on destruction.
|
||||
struct FsFileSystemHandle {
|
||||
FsFileSystem fs{};
|
||||
bool valid{false};
|
||||
|
||||
FsFileSystemHandle() = default;
|
||||
~FsFileSystemHandle() { if (valid) fsFsClose(&fs); } // NOLINT(modernize-use-equals-default)
|
||||
|
||||
FsFileSystemHandle(const FsFileSystemHandle&) = delete;
|
||||
FsFileSystemHandle& operator=(const FsFileSystemHandle&) = delete;
|
||||
|
||||
FsFileSystem* get() { return &fs; }
|
||||
|
||||
void release() { valid = false; } // transfer ownership to devfs
|
||||
};
|
||||
|
||||
// RAII wrapper for FILE* — auto-fclose on destruction.
|
||||
struct FileHandle {
|
||||
FILE* ptr{nullptr};
|
||||
|
||||
explicit FileHandle(FILE* file) : ptr(file) {}
|
||||
~FileHandle() { if (ptr != nullptr) fclose(ptr); } // NOLINT(modernize-use-equals-default)
|
||||
|
||||
FileHandle(const FileHandle&) = delete;
|
||||
FileHandle& operator=(const FileHandle&) = delete;
|
||||
|
||||
explicit operator bool() const { return ptr != nullptr; }
|
||||
FILE* get() const { return ptr; }
|
||||
};
|
||||
|
||||
} // namespace nxst
|
||||
@@ -24,25 +24,22 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#ifndef IO_HPP
|
||||
#define IO_HPP
|
||||
|
||||
#include "account.hpp"
|
||||
#include "directory.hpp"
|
||||
#include "title.hpp"
|
||||
#include "util.hpp"
|
||||
#pragma once
|
||||
#include <nxst/domain/account.hpp>
|
||||
#include <nxst/domain/result.hpp>
|
||||
#include <nxst/infra/fs/directory.hpp>
|
||||
#include <nxst/domain/title.hpp>
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <dirent.h>
|
||||
#include <switch.h>
|
||||
#include <sys/stat.h>
|
||||
#include <tuple>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
|
||||
#define BUFFER_SIZE 0x80000
|
||||
|
||||
namespace io {
|
||||
std::tuple<bool, Result, std::string> backup(size_t index, AccountUid uid);
|
||||
std::tuple<bool, Result, std::string> restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell);
|
||||
nxst::Result<std::string> backup(size_t index, AccountUid uid);
|
||||
nxst::Result<std::string> restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell);
|
||||
|
||||
Result copyDirectory(const std::string& srcPath, const std::string& dstPath);
|
||||
void copyFile(const std::string& srcPath, const std::string& dstPath);
|
||||
@@ -52,4 +49,3 @@ namespace io {
|
||||
bool fileExists(const std::string& path);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
#pragma once
|
||||
@@ -0,0 +1 @@
|
||||
#pragma once
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// New API — use these going forward.
|
||||
namespace nxst::log {
|
||||
|
||||
enum class Level { Debug, Info, Warn, Error };
|
||||
|
||||
void write(Level level, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
|
||||
void debug(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void info (const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void warn (const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void error(const char* fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
|
||||
// No-op: writes are immediate. Kept for source compatibility during migration.
|
||||
inline void flush() {}
|
||||
|
||||
} // namespace nxst::log
|
||||
|
||||
// Backward-compat shim — existing Logger::getInstance().log(...) call sites compile
|
||||
// unchanged. Format args are dropped (same behavior as broken original). Migrate
|
||||
// call sites to nxst::log::* in Phase 3.
|
||||
struct Logger {
|
||||
static Logger& getInstance()
|
||||
{
|
||||
static Logger instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// clang-tidy naming suppressed: these must match existing call sites during migration.
|
||||
static constexpr const char* INFO = "[INFO]"; // NOLINT(readability-identifier-naming)
|
||||
static constexpr const char* DEBUG = "[DEBUG]"; // NOLINT(readability-identifier-naming)
|
||||
static constexpr const char* ERROR = "[ERROR]"; // NOLINT(readability-identifier-naming)
|
||||
static constexpr const char* WARN = "[WARN]"; // NOLINT(readability-identifier-naming)
|
||||
|
||||
static void flush() { nxst::log::flush(); }
|
||||
|
||||
// Args intentionally dropped — format string still logged for visibility.
|
||||
template <typename... Args>
|
||||
void log(const std::string& level, const std::string& fmt, Args&&... /*args*/)
|
||||
{
|
||||
if (level == ERROR) nxst::log::error("%s", fmt.c_str());
|
||||
else if (level == WARN) nxst::log::warn("%s", fmt.c_str());
|
||||
else if (level == DEBUG) nxst::log::debug("%s", fmt.c_str());
|
||||
else nxst::log::info("%s", fmt.c_str());
|
||||
}
|
||||
|
||||
Logger() = default;
|
||||
~Logger() = default;
|
||||
|
||||
Logger(const Logger&) = delete; // NOLINT(modernize-use-equals-delete)
|
||||
Logger& operator=(const Logger&) = delete; // NOLINT(modernize-use-equals-delete)
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
#include <atomic>
|
||||
#include <pthread.h>
|
||||
#include <string>
|
||||
#include <switch.h>
|
||||
#include <nxst/domain/transfer_state.hpp>
|
||||
|
||||
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<int> sender_udp_sock{-1};
|
||||
std::atomic<int> sender_tcp_sock{-1};
|
||||
std::atomic<bool> sender_active{false};
|
||||
|
||||
// Receiver
|
||||
TransferState receiver_state;
|
||||
std::atomic<int> receiver_client_sock{-1};
|
||||
std::atomic<int> receiver_listen_sock{-1};
|
||||
std::atomic<int> receiver_bcast_sock{-1};
|
||||
std::atomic<bool> receiver_accept_active{false};
|
||||
std::atomic<bool> 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
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
#include <pu/Plutonium>
|
||||
#include <Theme.hpp>
|
||||
#include <nxst/ui/theme.hpp>
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <Theme.hpp>
|
||||
#include <nxst/ui/theme.hpp>
|
||||
|
||||
#define COLOR(hex) pu::ui::Color::FromHex(hex)
|
||||
#define BACKGROUND_COLOR theme::color::BgBase
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
#include <pu/Plutonium>
|
||||
#include <Theme.hpp>
|
||||
#include <ui/UiContext.hpp>
|
||||
#include <account.hpp>
|
||||
#include <nxst/ui/theme.hpp>
|
||||
#include <nxst/ui/ui_context.hpp>
|
||||
#include <nxst/domain/account.hpp>
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
#include <pu/Plutonium>
|
||||
#include <Theme.hpp>
|
||||
#include <nxst/ui/theme.hpp>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#pragma once
|
||||
#include <pu/Plutonium>
|
||||
#include <const.h>
|
||||
#include <title.hpp>
|
||||
#include <account.hpp>
|
||||
#include <nxst/ui/const.h>
|
||||
#include <nxst/domain/title.hpp>
|
||||
#include <nxst/domain/account.hpp>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <ui/HeaderBar.hpp>
|
||||
#include <ui/HintBar.hpp>
|
||||
#include <nxst/ui/header_bar.hpp>
|
||||
#include <nxst/ui/hint_bar.hpp>
|
||||
|
||||
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; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include <pu/Plutonium>
|
||||
#include <Theme.hpp>
|
||||
#include <util.hpp>
|
||||
#include <nxst/ui/theme.hpp>
|
||||
#include <nxst/domain/util.hpp>
|
||||
|
||||
namespace ui {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <switch.h>
|
||||
#include <account.hpp>
|
||||
#include <nxst/domain/account.hpp>
|
||||
|
||||
namespace ui {
|
||||
struct UiContext {
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <pu/Plutonium>
|
||||
#include <const.h>
|
||||
#include <ui/HeaderBar.hpp>
|
||||
#include <ui/HintBar.hpp>
|
||||
#include <nxst/ui/const.h>
|
||||
#include <nxst/ui/header_bar.hpp>
|
||||
#include <nxst/ui/hint_bar.hpp>
|
||||
#include <memory>
|
||||
|
||||
namespace ui {
|
||||
@@ -1,8 +0,0 @@
|
||||
#include <string>
|
||||
int startSendingThread();
|
||||
bool isServerTransferDone();
|
||||
bool isServerTransferCancelled();
|
||||
bool isServerWorkersIdle();
|
||||
void cancelServerTransfer();
|
||||
double getServerProgress();
|
||||
std::string getServerStatusText();
|
||||
@@ -1,21 +0,0 @@
|
||||
#include <string>
|
||||
#include <switch.h>
|
||||
#include <switch/services/hid.h>
|
||||
#include <vector>
|
||||
#include <MainApplication.hpp>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <sys/select.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <netinet/in.h>
|
||||
#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
|
||||
|
||||
#include <protocol.hpp>
|
||||
#include <TransferState.hpp>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using path = fs::path;
|
||||
|
||||
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};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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<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;
|
||||
}
|
||||
|
||||
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<ThreadArgs*>(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;
|
||||
}
|
||||
-333
@@ -1,333 +0,0 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include "io.hpp"
|
||||
#include "main.hpp"
|
||||
#include <logger.hpp>
|
||||
|
||||
bool io::fileExists(const std::string& path)
|
||||
{
|
||||
struct stat buffer;
|
||||
return (stat(path.c_str(), &buffer) == 0);
|
||||
}
|
||||
|
||||
void io::copyFile(const std::string& srcPath, const std::string& dstPath)
|
||||
{
|
||||
g_isTransferringFile = true;
|
||||
|
||||
FILE* src = fopen(srcPath.c_str(), "rb");
|
||||
if (src == NULL) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to open source file " + srcPath + " during copy with errno %d. Skipping...", errno);
|
||||
return;
|
||||
}
|
||||
FILE* dst = fopen(dstPath.c_str(), "wb");
|
||||
if (dst == NULL) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to open destination file " + dstPath + " during copy with errno " + std::to_string(errno) + ". Skipping...");
|
||||
fclose(src);
|
||||
return;
|
||||
}
|
||||
|
||||
fseek(src, 0, SEEK_END);
|
||||
u64 sz = ftell(src);
|
||||
rewind(src);
|
||||
|
||||
u8* buf = new u8[BUFFER_SIZE];
|
||||
u64 offset = 0;
|
||||
|
||||
size_t slashpos = srcPath.rfind("/");
|
||||
g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
delete[] buf;
|
||||
fclose(src);
|
||||
fclose(dst);
|
||||
|
||||
if (dstPath.rfind("save:/", 0) == 0) {
|
||||
fsdevCommitDevice("save");
|
||||
}
|
||||
|
||||
g_isTransferringFile = false;
|
||||
}
|
||||
|
||||
Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
|
||||
{
|
||||
Result res = 0;
|
||||
bool quit = false;
|
||||
Directory items(srcPath);
|
||||
|
||||
if (!items.good()) {
|
||||
return items.error();
|
||||
}
|
||||
|
||||
for (size_t i = 0, sz = items.size(); i < sz && !quit; i++) {
|
||||
std::string newsrc = srcPath + items.entry(i);
|
||||
std::string newdst = dstPath + items.entry(i);
|
||||
|
||||
if (items.folder(i)) {
|
||||
res = io::createDirectory(newdst);
|
||||
if (R_SUCCEEDED(res)) {
|
||||
newsrc += "/";
|
||||
newdst += "/";
|
||||
res = io::copyDirectory(newsrc, newdst);
|
||||
}
|
||||
else {
|
||||
quit = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
io::copyFile(newsrc, newdst);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Result io::createDirectory(const std::string& path)
|
||||
{
|
||||
mkdir(path.c_str(), 777);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool io::directoryExists(const std::string& path)
|
||||
{
|
||||
struct stat sb;
|
||||
return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
|
||||
}
|
||||
|
||||
Result io::deleteFolderRecursively(const std::string& path)
|
||||
{
|
||||
Directory dir(path);
|
||||
if (!dir.good()) {
|
||||
return dir.error();
|
||||
}
|
||||
|
||||
for (size_t i = 0, sz = dir.size(); i < sz; i++) {
|
||||
if (dir.folder(i)) {
|
||||
std::string newpath = path + "/" + dir.entry(i) + "/";
|
||||
deleteFolderRecursively(newpath);
|
||||
newpath = path + dir.entry(i);
|
||||
rmdir(newpath.c_str());
|
||||
}
|
||||
else {
|
||||
std::string newpath = path + dir.entry(i);
|
||||
std::remove(newpath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir(path.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::tuple<bool, Result, std::string> io::backup(size_t index, AccountUid uid)
|
||||
{
|
||||
Result res = 0;
|
||||
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
|
||||
Title title;
|
||||
getTitle(title, uid, index);
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
|
||||
FsFileSystem fileSystem;
|
||||
res = FileSystem::mount(&fileSystem, title.id(), title.userId());
|
||||
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]);
|
||||
return std::make_tuple(false, -2, "Failed to mount save.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger::getInstance().log(Logger::ERROR,
|
||||
"Failed to mount filesystem during backup with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
return std::make_tuple(false, res, "Failed to mount save.");
|
||||
}
|
||||
|
||||
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId())));
|
||||
|
||||
io::createDirectory(title.path());
|
||||
std::string dstPath = title.path() + "/" + suggestion;
|
||||
|
||||
// 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(tmpPath);
|
||||
res = io::copyDirectory("save:/", tmpPath + "/");
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
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();
|
||||
|
||||
ret = std::make_tuple(true, 0, dstPath);
|
||||
Logger::getInstance().log(Logger::INFO, "Backup succeeded.");
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::tuple<bool, Result, std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell)
|
||||
{
|
||||
Result res = 0;
|
||||
std::tuple<bool, Result, std::string> ret = std::make_tuple(false, -1, "");
|
||||
Title title;
|
||||
getTitle(title, uid, index);
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.", title.name().c_str(), title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
|
||||
// Если сейв ещё не существует (игра не запускалась) — создаём его через NACP.
|
||||
// fsCreateSaveDataFileSystem возвращает ошибку если сейв уже есть — это нормально.
|
||||
{
|
||||
NsApplicationControlData* nsacd = (NsApplicationControlData*)malloc(sizeof(NsApplicationControlData));
|
||||
if (nsacd != NULL) {
|
||||
memset(nsacd, 0, sizeof(NsApplicationControlData));
|
||||
size_t outsize = 0;
|
||||
if (R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage, title.id(), nsacd, sizeof(NsApplicationControlData), &outsize))) {
|
||||
static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail};
|
||||
|
||||
FsSaveDataAttribute attr = {};
|
||||
attr.application_id = title.id();
|
||||
attr.uid = uid;
|
||||
attr.save_data_type = FsSaveDataType_Account;
|
||||
attr.save_data_rank = FsSaveDataRank_Primary;
|
||||
|
||||
FsSaveDataCreationInfo createInfo = {};
|
||||
createInfo.save_data_size = (s64)nsacd->nacp.user_account_save_data_size;
|
||||
createInfo.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size;
|
||||
createInfo.available_size = 0x4000;
|
||||
createInfo.owner_id = nsacd->nacp.save_data_owner_id;
|
||||
createInfo.save_data_space_id = FsSaveDataSpaceId_User;
|
||||
|
||||
fsCreateSaveDataFileSystem(&attr, &createInfo, &meta);
|
||||
}
|
||||
free(nsacd);
|
||||
}
|
||||
}
|
||||
|
||||
FsFileSystem fileSystem;
|
||||
res = FileSystem::mount(&fileSystem, title.id(), 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]);
|
||||
return std::make_tuple(false, -2, "Failed to mount save.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger::getInstance().log(Logger::ERROR,
|
||||
"Failed to mount filesystem during restore with result 0x%08lX. Title id: 0x%016lX; User id: 0x%lX%lX.", res, title.id(),
|
||||
uid.uid[1], uid.uid[0]);
|
||||
return std::make_tuple(false, res, "Failed to mount save.");
|
||||
}
|
||||
|
||||
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(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++) {
|
||||
if (saveRoot.folder(i)) {
|
||||
io::deleteFolderRecursively((dstPath + saveRoot.entry(i) + "/").c_str());
|
||||
rmdir((dstPath + saveRoot.entry(i)).c_str());
|
||||
} else {
|
||||
std::remove((dstPath + saveRoot.entry(i)).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res = fsdevCommitDevice("save");
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to commit save after clearing with result 0x%08lX.", res);
|
||||
return std::make_tuple(false, res, "Failed to commit save after delete.");
|
||||
}
|
||||
|
||||
res = io::copyDirectory(srcPath, dstPath);
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to copy directory " + srcPath + " to " + dstPath + " with result 0x%08lX. Skipping...", res);
|
||||
return std::make_tuple(false, res, "Failed to restore save.");
|
||||
}
|
||||
|
||||
res = fsdevCommitDevice("save");
|
||||
if (R_FAILED(res)) {
|
||||
Logger::getInstance().log(Logger::ERROR, "Failed to commit save with result 0x%08lX.", res);
|
||||
return std::make_tuple(false, res, "Failed to commit to save device.");
|
||||
}
|
||||
else {
|
||||
blinkLed(4);
|
||||
ret = std::make_tuple(true, 0, nameFromCell + "\nhas been restored successfully.");
|
||||
}
|
||||
|
||||
FileSystem::unmount();
|
||||
|
||||
Logger::getInstance().log(Logger::INFO, "Restore succeeded.");
|
||||
return ret;
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
#include <arpa/inet.h>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <netinet/in.h>
|
||||
#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>
|
||||
#include <switch.h>
|
||||
#include <main.hpp>
|
||||
#endif
|
||||
|
||||
#include <protocol.hpp>
|
||||
#include <TransferState.hpp>
|
||||
#include <net/Socket.hpp>
|
||||
|
||||
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__
|
||||
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<char*>(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<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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static void* handle_client(void* socket_desc) {
|
||||
int client_socket = *(int*)socket_desc;
|
||||
delete static_cast<int*>(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<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);
|
||||
}
|
||||
|
||||
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<AcceptArgs*>(arg)->server_fd;
|
||||
delete static_cast<AcceptArgs*>(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
|
||||
@@ -1,10 +1,10 @@
|
||||
#include <MainApplication.hpp>
|
||||
#include "util.hpp"
|
||||
#include "main.hpp"
|
||||
#include <server.hpp>
|
||||
#include <client.hpp>
|
||||
#include <nxst/app/main_application.hpp>
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <nxst/app/main.hpp>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace ui { extern MainApplication* mainApp; }
|
||||
|
||||
static int nxlink_sock = -1;
|
||||
|
||||
extern "C" void userAppInit() {
|
||||
@@ -22,11 +22,15 @@ extern "C" void userAppInit() {
|
||||
}
|
||||
|
||||
extern "C" void userAppExit() {
|
||||
cancelServerTransfer();
|
||||
cancelClientTransfer();
|
||||
for (int i = 0; i < 150 && (!isServerWorkersIdle() || !isClientWorkersIdle()); i++) {
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#include <string>
|
||||
#include <switch.h>
|
||||
#include <switch/services/hid.h>
|
||||
#include <vector>
|
||||
#include <nxst/app/main_application.hpp>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,7 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include "account.hpp"
|
||||
#include <main.hpp>
|
||||
#include <nxst/domain/account.hpp>
|
||||
#include <sys/stat.h>
|
||||
#include <cstdio>
|
||||
|
||||
@@ -138,5 +137,5 @@ AccountUid Account::selectAccount(void)
|
||||
return uid;
|
||||
}
|
||||
|
||||
return g_currentUId;
|
||||
return AccountUid{};
|
||||
}
|
||||
@@ -24,7 +24,7 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include "common.hpp"
|
||||
#include <nxst/domain/common.hpp>
|
||||
|
||||
std::string DateTime::timeStr(void)
|
||||
{
|
||||
@@ -24,8 +24,8 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include "title.hpp"
|
||||
#include "main.hpp"
|
||||
#include <nxst/domain/title.hpp>
|
||||
#include <nxst/app/main.hpp>
|
||||
|
||||
static std::unordered_map<AccountUid, std::vector<Title>> titles;
|
||||
static bool s_titlesLoaded = false;
|
||||
@@ -24,10 +24,10 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include "util.hpp"
|
||||
#include <logger.hpp>
|
||||
#include <MainApplication.hpp>
|
||||
#include "main.hpp"
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <nxst/infra/sys/logger.hpp>
|
||||
#include <nxst/app/main_application.hpp>
|
||||
#include <nxst/app/main.hpp>
|
||||
|
||||
void servicesExit(void)
|
||||
{
|
||||
@@ -47,16 +47,6 @@ Result servicesInit(void)
|
||||
Logger::getInstance().log(Logger::WARN, "Please do not run NXST in applet mode.");
|
||||
}
|
||||
|
||||
// Result socinit = 0;
|
||||
// if ((socinit = socketInitializeDefault()) == 0) {
|
||||
// nxlinkStdio();
|
||||
// }
|
||||
// else {
|
||||
// Logger::getInstance().log(Logger::INFO, "Unable to initialize socket. Result code 0x%08lX.", socinit);
|
||||
// }
|
||||
|
||||
// g_shouldExitNetworkLoop = R_FAILED(socinit);
|
||||
|
||||
Result res = 0;
|
||||
|
||||
romfsInit();
|
||||
@@ -24,7 +24,7 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include "directory.hpp"
|
||||
#include <nxst/infra/fs/directory.hpp>
|
||||
|
||||
Directory::Directory(const std::string& root)
|
||||
{
|
||||
@@ -24,7 +24,7 @@
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include "filesystem.hpp"
|
||||
#include <nxst/infra/fs/filesystem.hpp>
|
||||
|
||||
Result FileSystem::mount(FsFileSystem* fileSystem, u64 titleID, AccountUid userID)
|
||||
{
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* This file is part of Checkpoint
|
||||
* Copyright (C) 2017-2021 Bernardo Giordano, FlagBrew
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional Terms 7.b and 7.c of GPLv3 apply to this file:
|
||||
* * Requiring preservation of specified reasonable legal notices or
|
||||
* author attributions in that material or in the Appropriate Legal
|
||||
* Notices displayed by works containing it.
|
||||
* * Prohibiting misrepresentation of the origin of that material,
|
||||
* or requiring that modified versions of such material be marked in
|
||||
* reasonable ways as different from the original version.
|
||||
*/
|
||||
|
||||
#include <nxst/infra/fs/io.hpp>
|
||||
#include <nxst/infra/fs/handles.hpp>
|
||||
#include <nxst/app/main.hpp>
|
||||
#include <nxst/infra/sys/logger.hpp>
|
||||
#include <vector>
|
||||
|
||||
bool io::fileExists(const std::string& path)
|
||||
{
|
||||
struct stat buffer;
|
||||
return (stat(path.c_str(), &buffer) == 0);
|
||||
}
|
||||
|
||||
void io::copyFile(const std::string& srcPath, const std::string& dstPath)
|
||||
{
|
||||
g_isTransferringFile = true;
|
||||
|
||||
nxst::FileHandle src(fopen(srcPath.c_str(), "rb"));
|
||||
if (!src) {
|
||||
nxst::log::error("Failed to open source file %s during copy with errno %d. Skipping...",
|
||||
srcPath.c_str(), errno);
|
||||
g_isTransferringFile = false;
|
||||
return;
|
||||
}
|
||||
nxst::FileHandle dst(fopen(dstPath.c_str(), "wb"));
|
||||
if (!dst) {
|
||||
nxst::log::error("Failed to open destination file %s during copy with errno %d. Skipping...",
|
||||
dstPath.c_str(), errno);
|
||||
g_isTransferringFile = false;
|
||||
return;
|
||||
}
|
||||
|
||||
fseek(src.get(), 0, SEEK_END);
|
||||
u64 sz = (u64)ftell(src.get());
|
||||
rewind(src.get());
|
||||
|
||||
std::vector<u8> buf(BUFFER_SIZE);
|
||||
u64 offset = 0;
|
||||
|
||||
size_t slashpos = srcPath.rfind('/');
|
||||
g_currentFile = srcPath.substr(slashpos + 1, srcPath.length() - slashpos - 1);
|
||||
|
||||
while (offset < sz) {
|
||||
u32 count = (u32)fread(buf.data(), 1, BUFFER_SIZE, src.get());
|
||||
if (count == 0) {
|
||||
nxst::log::error("fread returned 0 for %s at offset %llu/%llu (errno %d). Aborting.",
|
||||
srcPath.c_str(), (unsigned long long)offset,
|
||||
(unsigned long long)sz, errno);
|
||||
break;
|
||||
}
|
||||
fwrite(buf.data(), 1, count, dst.get());
|
||||
offset += count;
|
||||
}
|
||||
|
||||
if (dstPath.rfind("save:/", 0) == 0) {
|
||||
fsdevCommitDevice("save");
|
||||
}
|
||||
|
||||
g_isTransferringFile = false;
|
||||
}
|
||||
|
||||
Result io::copyDirectory(const std::string& srcPath, const std::string& dstPath)
|
||||
{
|
||||
Result res = 0;
|
||||
bool quit = false;
|
||||
Directory items(srcPath);
|
||||
|
||||
if (!items.good()) {
|
||||
return items.error();
|
||||
}
|
||||
|
||||
for (size_t i = 0, sz = items.size(); i < sz && !quit; i++) {
|
||||
std::string newsrc = srcPath + items.entry(i);
|
||||
std::string newdst = dstPath + items.entry(i);
|
||||
|
||||
if (items.folder(i)) {
|
||||
res = io::createDirectory(newdst);
|
||||
if (R_SUCCEEDED(res)) {
|
||||
newsrc += "/";
|
||||
newdst += "/";
|
||||
res = io::copyDirectory(newsrc, newdst);
|
||||
}
|
||||
else {
|
||||
quit = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
io::copyFile(newsrc, newdst);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Result io::createDirectory(const std::string& path)
|
||||
{
|
||||
mkdir(path.c_str(), 0777);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool io::directoryExists(const std::string& path)
|
||||
{
|
||||
struct stat sb;
|
||||
return (stat(path.c_str(), &sb) == 0 && S_ISDIR(sb.st_mode));
|
||||
}
|
||||
|
||||
Result io::deleteFolderRecursively(const std::string& path)
|
||||
{
|
||||
Directory dir(path);
|
||||
if (!dir.good()) {
|
||||
return dir.error();
|
||||
}
|
||||
|
||||
for (size_t i = 0, sz = dir.size(); i < sz; i++) {
|
||||
if (dir.folder(i)) {
|
||||
std::string newpath = path + "/" + dir.entry(i) + "/";
|
||||
deleteFolderRecursively(newpath);
|
||||
newpath = path + dir.entry(i);
|
||||
rmdir(newpath.c_str());
|
||||
}
|
||||
else {
|
||||
std::string newpath = path + dir.entry(i);
|
||||
std::remove(newpath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
rmdir(path.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
nxst::Result<std::string> io::backup(size_t index, AccountUid uid)
|
||||
{
|
||||
Title title;
|
||||
getTitle(title, uid, index);
|
||||
|
||||
nxst::log::info("Started backup of %s. Title id: 0x%016lX; User id: 0x%lX%lX.",
|
||||
title.name().c_str(), title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
|
||||
nxst::FsFileSystemHandle fsHandle;
|
||||
Result res = FileSystem::mount(fsHandle.get(), title.id(), title.userId());
|
||||
if (R_FAILED(res)) {
|
||||
nxst::log::error("Failed to mount filesystem during backup with result 0x%08lX. "
|
||||
"Title id: 0x%016lX; User id: 0x%lX%lX.",
|
||||
res, title.id(), title.userId().uid[1], title.userId().uid[0]);
|
||||
return nxst::Result<std::string>::failure("Failed to mount save.");
|
||||
}
|
||||
fsHandle.valid = true;
|
||||
|
||||
if (FileSystem::mount(*fsHandle.get()) == -1) {
|
||||
nxst::log::error("Failed to mount devfs during backup. Title id: 0x%016lX; User id: 0x%lX%lX.",
|
||||
title.id(), title.userId().uid[1], title.userId().uid[0]);
|
||||
FileSystem::unmount();
|
||||
return nxst::Result<std::string>::failure("Failed to mount save.");
|
||||
}
|
||||
fsHandle.release(); // devfs now owns the kernel handle
|
||||
|
||||
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(title.userId())));
|
||||
|
||||
io::createDirectory(title.path());
|
||||
std::string dst_path = title.path() + "/" + suggestion;
|
||||
std::string tmp_path = dst_path + ".tmp";
|
||||
|
||||
if (io::directoryExists(tmp_path)) {
|
||||
io::deleteFolderRecursively(tmp_path + "/");
|
||||
}
|
||||
io::createDirectory(tmp_path);
|
||||
res = io::copyDirectory("save:/", tmp_path + "/");
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
io::deleteFolderRecursively(tmp_path + "/");
|
||||
nxst::log::error("Failed to copy directory to %s with result 0x%08lX.", tmp_path.c_str(), res);
|
||||
return nxst::Result<std::string>::failure("Failed to backup save.");
|
||||
}
|
||||
|
||||
if (io::directoryExists(dst_path)) {
|
||||
io::deleteFolderRecursively(dst_path + "/");
|
||||
}
|
||||
if (rename(tmp_path.c_str(), dst_path.c_str()) != 0) {
|
||||
FileSystem::unmount();
|
||||
nxst::log::error("Failed to rename temp backup to %s.", dst_path.c_str());
|
||||
return nxst::Result<std::string>::failure("Failed to finalise backup.");
|
||||
}
|
||||
|
||||
refreshDirectories(title.id());
|
||||
FileSystem::unmount();
|
||||
|
||||
nxst::log::info("Backup succeeded.");
|
||||
return nxst::Result<std::string>::success(dst_path);
|
||||
}
|
||||
|
||||
// Creates the save data filesystem for a title if it doesn't exist yet.
|
||||
static void createSaveIfNeeded(u64 title_id, AccountUid uid)
|
||||
{
|
||||
std::vector<u8> nsacd_buf(sizeof(NsApplicationControlData), 0);
|
||||
auto* nsacd = reinterpret_cast<NsApplicationControlData*>(nsacd_buf.data());
|
||||
|
||||
size_t outsize = 0;
|
||||
if (!R_SUCCEEDED(nsGetApplicationControlData(NsApplicationControlSource_Storage,
|
||||
title_id, nsacd,
|
||||
sizeof(NsApplicationControlData), &outsize))) {
|
||||
return;
|
||||
}
|
||||
|
||||
static const FsSaveDataMetaInfo meta = {.size = 0x40060, .type = FsSaveDataMetaType_Thumbnail};
|
||||
|
||||
FsSaveDataAttribute attr = {};
|
||||
attr.application_id = title_id;
|
||||
attr.uid = uid;
|
||||
attr.save_data_type = FsSaveDataType_Account;
|
||||
attr.save_data_rank = FsSaveDataRank_Primary;
|
||||
|
||||
FsSaveDataCreationInfo create_info = {};
|
||||
create_info.save_data_size = (s64)nsacd->nacp.user_account_save_data_size;
|
||||
create_info.journal_size = (s64)nsacd->nacp.user_account_save_data_journal_size;
|
||||
create_info.available_size = 0x4000;
|
||||
create_info.owner_id = nsacd->nacp.save_data_owner_id;
|
||||
create_info.save_data_space_id = FsSaveDataSpaceId_User;
|
||||
|
||||
fsCreateSaveDataFileSystem(&attr, &create_info, &meta);
|
||||
}
|
||||
|
||||
nxst::Result<std::string> io::restore(size_t index, AccountUid uid, size_t cellIndex, const std::string& nameFromCell)
|
||||
{
|
||||
(void)cellIndex;
|
||||
|
||||
Title title;
|
||||
getTitle(title, uid, index);
|
||||
|
||||
nxst::log::info("Started restore of %s. Title id: 0x%016lX; User id: 0x%lX%lX.",
|
||||
title.name().c_str(), title.id(),
|
||||
title.userId().uid[1], title.userId().uid[0]);
|
||||
|
||||
createSaveIfNeeded(title.id(), uid);
|
||||
|
||||
nxst::FsFileSystemHandle fsHandle;
|
||||
Result res = FileSystem::mount(fsHandle.get(), title.id(), uid);
|
||||
if (R_FAILED(res)) {
|
||||
nxst::log::error("Failed to mount filesystem during restore with result 0x%08lX. "
|
||||
"Title id: 0x%016lX; User id: 0x%lX%lX.",
|
||||
res, title.id(), uid.uid[1], uid.uid[0]);
|
||||
return nxst::Result<std::string>::failure("Failed to mount save.");
|
||||
}
|
||||
fsHandle.valid = true;
|
||||
|
||||
if (FileSystem::mount(*fsHandle.get()) == -1) {
|
||||
nxst::log::error("Failed to mount devfs during restore. Title id: 0x%016lX; User id: 0x%lX%lX.",
|
||||
title.id(), uid.uid[1], uid.uid[0]);
|
||||
FileSystem::unmount();
|
||||
return nxst::Result<std::string>::failure("Failed to mount save.");
|
||||
}
|
||||
fsHandle.release(); // devfs now owns the kernel handle
|
||||
|
||||
std::string suggestion = StringUtils::removeNotAscii(StringUtils::removeAccents(Account::username(uid)));
|
||||
std::string src_path = title.path() + "/" + suggestion + "/";
|
||||
const std::string dst_path = "save:/";
|
||||
|
||||
{
|
||||
Directory src_check(src_path);
|
||||
if (!src_check.good() || src_check.size() == 0) {
|
||||
FileSystem::unmount();
|
||||
nxst::log::error("Restore source is empty or missing: %s", src_path.c_str());
|
||||
return nxst::Result<std::string>::failure("Restore source is empty or missing.");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Directory save_root(dst_path);
|
||||
for (size_t i = 0, sz = save_root.size(); i < sz; i++) {
|
||||
if (save_root.folder(i)) {
|
||||
io::deleteFolderRecursively(dst_path + save_root.entry(i) + "/");
|
||||
rmdir((dst_path + save_root.entry(i)).c_str());
|
||||
} else {
|
||||
std::remove((dst_path + save_root.entry(i)).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res = fsdevCommitDevice("save");
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
nxst::log::error("Failed to commit save after clearing with result 0x%08lX.", res);
|
||||
return nxst::Result<std::string>::failure("Failed to commit save after delete.");
|
||||
}
|
||||
|
||||
res = io::copyDirectory(src_path, dst_path);
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
nxst::log::error("Failed to copy %s to save:/ with result 0x%08lX.", src_path.c_str(), res);
|
||||
return nxst::Result<std::string>::failure("Failed to restore save.");
|
||||
}
|
||||
|
||||
res = fsdevCommitDevice("save");
|
||||
if (R_FAILED(res)) {
|
||||
FileSystem::unmount();
|
||||
nxst::log::error("Failed to commit save with result 0x%08lX.", res);
|
||||
return nxst::Result<std::string>::failure("Failed to commit to save device.");
|
||||
}
|
||||
|
||||
blinkLed(4);
|
||||
FileSystem::unmount();
|
||||
|
||||
nxst::log::info("Restore succeeded.");
|
||||
return nxst::Result<std::string>::success(nameFromCell + "\nhas been restored successfully.");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// Logic moved to src/service/transfer_service.cpp
|
||||
@@ -0,0 +1 @@
|
||||
// Logic moved to src/service/transfer_service.cpp
|
||||
@@ -0,0 +1,64 @@
|
||||
#include <nxst/infra/sys/logger.hpp>
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
|
||||
namespace {
|
||||
|
||||
std::mutex g_log_mutex;
|
||||
|
||||
#if defined(__SWITCH__)
|
||||
constexpr const char* kLogPath = "/switch/NXST/log.log";
|
||||
#else
|
||||
constexpr const char* kLogPath = "nxst.log";
|
||||
#endif
|
||||
|
||||
void writeEntry(const char* tag, const char* fmt, va_list args)
|
||||
{
|
||||
char msg[2048];
|
||||
vsnprintf(msg, sizeof(msg), fmt, args);
|
||||
|
||||
time_t now = time(nullptr);
|
||||
struct tm tm_buf;
|
||||
localtime_r(&now, &tm_buf);
|
||||
char time_str[16];
|
||||
strftime(time_str, sizeof(time_str), "%H:%M:%S", &tm_buf);
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_log_mutex);
|
||||
|
||||
fprintf(stderr, "[%s]%s %s\n", time_str, tag, msg);
|
||||
|
||||
FILE* log_file = fopen(kLogPath, "a");
|
||||
if (log_file != nullptr) {
|
||||
fprintf(log_file, "[%s]%s %s\n", time_str, tag, msg);
|
||||
fclose(log_file);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace nxst::log {
|
||||
|
||||
void write(Level level, const char* fmt, ...)
|
||||
{
|
||||
const char* tag = "[INFO] ";
|
||||
switch (level) {
|
||||
case Level::Debug: tag = "[DEBUG]"; break;
|
||||
case Level::Info: tag = "[INFO] "; break;
|
||||
case Level::Warn: tag = "[WARN] "; break;
|
||||
case Level::Error: tag = "[ERROR]"; break;
|
||||
}
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
writeEntry(tag, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void debug(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[DEBUG]", fmt, args); va_end(args); }
|
||||
void info (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[INFO] ", fmt, args); va_end(args); }
|
||||
void warn (const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[WARN] ", fmt, args); va_end(args); }
|
||||
void error(const char* fmt, ...) { va_list args; va_start(args, fmt); writeEntry("[ERROR]", fmt, args); va_end(args); }
|
||||
|
||||
} // namespace nxst::log
|
||||
@@ -0,0 +1,483 @@
|
||||
#include <nxst/service/transfer_service.hpp>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
#ifdef __SWITCH__
|
||||
#include <switch.h>
|
||||
#include <nxst/infra/fs/io.hpp>
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <nxst/domain/account.hpp>
|
||||
#endif
|
||||
|
||||
#include <nxst/domain/protocol.hpp>
|
||||
#include <nxst/infra/net/socket.hpp>
|
||||
|
||||
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<const char*>(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<char*>(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<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 (!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<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;
|
||||
}
|
||||
|
||||
state.bytes_total.store(file_size);
|
||||
state.bytes_done.store(0);
|
||||
|
||||
std::vector<char> 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<SenderArgs*>(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 (!backup_result.isOk()) {
|
||||
failSend("Failed to create backup:\n" + backup_result.error());
|
||||
return finish();
|
||||
}
|
||||
fs::path directory = backup_result.value();
|
||||
#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<TransferService*>(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<AcceptArgs*>(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<char> 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 = result.isOk();
|
||||
restore_error = result.isOk() ? "" : result.error();
|
||||
#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
|
||||
@@ -1,10 +1,7 @@
|
||||
#include <MainApplication.hpp>
|
||||
#include <stdio.h>
|
||||
#include <main.hpp>
|
||||
#include <const.h>
|
||||
#include <client.hpp>
|
||||
#include <server.hpp>
|
||||
#include <TransferOverlay.hpp>
|
||||
#include <nxst/app/main_application.hpp>
|
||||
#include <nxst/domain/util.hpp>
|
||||
#include <nxst/ui/transfer_overlay.hpp>
|
||||
#include <nxst/ui/const.h>
|
||||
|
||||
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<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++) {
|
||||
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();
|
||||
@@ -1,6 +1,4 @@
|
||||
#include <cstdio>
|
||||
#include <MainApplication.hpp>
|
||||
#include "main.hpp"
|
||||
#include <nxst/app/main_application.hpp>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
find src include -name '*.cpp' -o -name '*.hpp' | xargs clang-format -i
|
||||
echo "Formatted."
|
||||
Reference in New Issue
Block a user