webui: replace nautctl web server with a loadable plugin
Drop the web UI that was compiled into nautctl and serve the torrent-ui front end (../torrent-ui/public) from a native plugin (plugins/webui) loaded via `nautd --plugin`. The plugin talks to the engine only through the host call_rpc ABI and adapts the daemon's RPC surface to the qBittorrent-style contract the UI expects (snapshot/SSE, torrent detail tabs, add/delete, cookie auth). Also folds in the daemon refactor that owns per-torrent worker threads and the swarm engine (naut_swarm) used by the plugin's data source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
50a357968a
commit
8dde48c05a
27 changed files with 2706 additions and 403 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -8,3 +8,10 @@ compile_commands.json
|
||||||
|
|
||||||
# Test scratch
|
# Test scratch
|
||||||
/tmp/
|
/tmp/
|
||||||
|
|
||||||
|
# Local downloads and packaging scratch
|
||||||
|
/downloads/
|
||||||
|
/package/
|
||||||
|
/package.zip
|
||||||
|
# Stray torrents dropped at the repo root (fixtures under tests/ stay tracked)
|
||||||
|
/*.torrent
|
||||||
|
|
|
||||||
Binary file not shown.
196
CMakeLists.txt
196
CMakeLists.txt
|
|
@ -5,6 +5,11 @@ set(CMAKE_C_STANDARD 11)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
option(NAUT_STANDALONE
|
||||||
|
"Statically embed Jansson and Lua in the daemon and client" OFF)
|
||||||
|
option(NAUT_NATIVE
|
||||||
|
"Optimize Release builds for the build machine's CPU" OFF)
|
||||||
|
|
||||||
if(NOT CMAKE_BUILD_TYPE)
|
if(NOT CMAKE_BUILD_TYPE)
|
||||||
set(CMAKE_BUILD_TYPE Release)
|
set(CMAKE_BUILD_TYPE Release)
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -12,8 +17,11 @@ endif()
|
||||||
add_compile_definitions(_GNU_SOURCE)
|
add_compile_definitions(_GNU_SOURCE)
|
||||||
add_compile_options(-Wall -Wextra -Wshadow -Wvla -Wpointer-arith
|
add_compile_options(-Wall -Wextra -Wshadow -Wvla -Wpointer-arith
|
||||||
-fno-omit-frame-pointer)
|
-fno-omit-frame-pointer)
|
||||||
set(CMAKE_C_FLAGS_RELEASE "-O3 -march=native -DNDEBUG")
|
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")
|
||||||
set(CMAKE_C_FLAGS_DEBUG "-O0 -g3")
|
set(CMAKE_C_FLAGS_DEBUG "-O0 -g3")
|
||||||
|
if(NAUT_NATIVE)
|
||||||
|
add_compile_options($<$<CONFIG:Release>:-march=native>)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Sanitizer convenience build: -DNAUT_SAN=address|thread|undefined
|
# Sanitizer convenience build: -DNAUT_SAN=address|thread|undefined
|
||||||
if(NAUT_SAN)
|
if(NAUT_SAN)
|
||||||
|
|
@ -30,9 +38,80 @@ if(NOT URING_LIB OR NOT URING_INC)
|
||||||
message(FATAL_ERROR "liburing not found (install liburing-dev)")
|
message(FATAL_ERROR "liburing not found (install liburing-dev)")
|
||||||
endif()
|
endif()
|
||||||
find_package(OpenSSL REQUIRED COMPONENTS Crypto)
|
find_package(OpenSSL REQUIRED COMPONENTS Crypto)
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
pkg_check_modules(JANSSON REQUIRED IMPORTED_TARGET jansson)
|
if(NAUT_STANDALONE)
|
||||||
pkg_check_modules(LUA REQUIRED IMPORTED_TARGET lua)
|
include(FetchContent)
|
||||||
|
|
||||||
|
# Jansson 2.14.1 predates CMake 4's removal of pre-3.5 policy defaults.
|
||||||
|
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
|
||||||
|
set(JANSSON_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JANSSON_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JANSSON_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
|
set(JANSSON_INSTALL OFF CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_Declare(jansson
|
||||||
|
URL https://github.com/akheron/jansson/archive/refs/tags/v2.14.1.tar.gz
|
||||||
|
URL_HASH
|
||||||
|
SHA256=979210eaffdffbcf54cfc34d047fccde13f21b529a381df26db871d886f729a4
|
||||||
|
DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
|
||||||
|
FetchContent_MakeAvailable(jansson)
|
||||||
|
target_include_directories(jansson INTERFACE
|
||||||
|
${jansson_SOURCE_DIR}/src
|
||||||
|
${jansson_BINARY_DIR}/include)
|
||||||
|
|
||||||
|
FetchContent_Declare(lua
|
||||||
|
URL https://www.lua.org/ftp/lua-5.4.8.tar.gz
|
||||||
|
URL_HASH
|
||||||
|
SHA256=4f18ddae154e793e46eeab727c59ef1c0c0c2b744e7b94219710d76f530629ae
|
||||||
|
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||||
|
SOURCE_SUBDIR cmake-unused)
|
||||||
|
FetchContent_MakeAvailable(lua)
|
||||||
|
set(LUA_SRC_DIR ${lua_SOURCE_DIR}/src)
|
||||||
|
add_library(naut_lua STATIC
|
||||||
|
${LUA_SRC_DIR}/lapi.c
|
||||||
|
${LUA_SRC_DIR}/lauxlib.c
|
||||||
|
${LUA_SRC_DIR}/lbaselib.c
|
||||||
|
${LUA_SRC_DIR}/lcode.c
|
||||||
|
${LUA_SRC_DIR}/lcorolib.c
|
||||||
|
${LUA_SRC_DIR}/lctype.c
|
||||||
|
${LUA_SRC_DIR}/ldblib.c
|
||||||
|
${LUA_SRC_DIR}/ldebug.c
|
||||||
|
${LUA_SRC_DIR}/ldo.c
|
||||||
|
${LUA_SRC_DIR}/ldump.c
|
||||||
|
${LUA_SRC_DIR}/lfunc.c
|
||||||
|
${LUA_SRC_DIR}/lgc.c
|
||||||
|
${LUA_SRC_DIR}/linit.c
|
||||||
|
${LUA_SRC_DIR}/liolib.c
|
||||||
|
${LUA_SRC_DIR}/llex.c
|
||||||
|
${LUA_SRC_DIR}/lmathlib.c
|
||||||
|
${LUA_SRC_DIR}/lmem.c
|
||||||
|
${LUA_SRC_DIR}/loadlib.c
|
||||||
|
${LUA_SRC_DIR}/lobject.c
|
||||||
|
${LUA_SRC_DIR}/lopcodes.c
|
||||||
|
${LUA_SRC_DIR}/loslib.c
|
||||||
|
${LUA_SRC_DIR}/lparser.c
|
||||||
|
${LUA_SRC_DIR}/lstate.c
|
||||||
|
${LUA_SRC_DIR}/lstring.c
|
||||||
|
${LUA_SRC_DIR}/lstrlib.c
|
||||||
|
${LUA_SRC_DIR}/ltable.c
|
||||||
|
${LUA_SRC_DIR}/ltablib.c
|
||||||
|
${LUA_SRC_DIR}/ltm.c
|
||||||
|
${LUA_SRC_DIR}/lundump.c
|
||||||
|
${LUA_SRC_DIR}/lutf8lib.c
|
||||||
|
${LUA_SRC_DIR}/lvm.c
|
||||||
|
${LUA_SRC_DIR}/lzio.c)
|
||||||
|
target_compile_definitions(naut_lua PRIVATE LUA_USE_LINUX)
|
||||||
|
target_include_directories(naut_lua PUBLIC ${LUA_SRC_DIR})
|
||||||
|
target_link_libraries(naut_lua PUBLIC m ${CMAKE_DL_LIBS})
|
||||||
|
|
||||||
|
set(NAUT_JANSSON_TARGET jansson)
|
||||||
|
set(NAUT_LUA_TARGET naut_lua)
|
||||||
|
else()
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(JANSSON REQUIRED IMPORTED_TARGET jansson)
|
||||||
|
pkg_check_modules(LUA REQUIRED IMPORTED_TARGET lua)
|
||||||
|
set(NAUT_JANSSON_TARGET PkgConfig::JANSSON)
|
||||||
|
set(NAUT_LUA_TARGET PkgConfig::LUA)
|
||||||
|
endif()
|
||||||
|
|
||||||
# --- core: zero-dependency foundation ---------------------------------------
|
# --- core: zero-dependency foundation ---------------------------------------
|
||||||
add_library(naut_core STATIC
|
add_library(naut_core STATIC
|
||||||
|
|
@ -73,10 +152,13 @@ target_link_libraries(naut_dht PUBLIC naut_bencode naut_tracker)
|
||||||
|
|
||||||
# --- peer: wire protocol codec (sans-IO) ------------------------------------
|
# --- peer: wire protocol codec (sans-IO) ------------------------------------
|
||||||
add_library(naut_peer STATIC
|
add_library(naut_peer STATIC
|
||||||
src/peer/wire.c src/peer/extension.c src/peer/metadata.c src/peer/mse.c
|
src/peer/wire.c src/peer/extension.c src/peer/metadata.c src/peer/pipeline.c)
|
||||||
src/peer/pipeline.c)
|
|
||||||
target_link_libraries(naut_peer PUBLIC
|
target_link_libraries(naut_peer PUBLIC
|
||||||
naut_core naut_crypto naut_bencode naut_tracker OpenSSL::Crypto m)
|
naut_core naut_crypto naut_bencode naut_tracker m)
|
||||||
|
|
||||||
|
# MSE is optional at the application boundary and is the only OpenSSL user.
|
||||||
|
add_library(naut_mse STATIC src/peer/mse.c)
|
||||||
|
target_link_libraries(naut_mse PUBLIC naut_peer OpenSSL::Crypto)
|
||||||
|
|
||||||
# --- storage: file backend --------------------------------------------------
|
# --- storage: file backend --------------------------------------------------
|
||||||
add_library(naut_storage STATIC src/storage/storage.c)
|
add_library(naut_storage STATIC src/storage/storage.c)
|
||||||
|
|
@ -95,6 +177,10 @@ add_library(naut_platform STATIC
|
||||||
target_include_directories(naut_platform PUBLIC ${URING_INC})
|
target_include_directories(naut_platform PUBLIC ${URING_INC})
|
||||||
target_link_libraries(naut_platform PUBLIC naut_core ${URING_LIB})
|
target_link_libraries(naut_platform PUBLIC naut_core ${URING_LIB})
|
||||||
|
|
||||||
|
# CPU topology helpers do not require the io_uring platform backend.
|
||||||
|
add_library(naut_system STATIC src/platform/system.c)
|
||||||
|
target_link_libraries(naut_system PUBLIC naut_core)
|
||||||
|
|
||||||
# --- session + extensibility control plane ---------------------------------
|
# --- session + extensibility control plane ---------------------------------
|
||||||
add_library(naut_session STATIC src/session/event.c)
|
add_library(naut_session STATIC src/session/event.c)
|
||||||
target_link_libraries(naut_session PUBLIC naut_core)
|
target_link_libraries(naut_session PUBLIC naut_core)
|
||||||
|
|
@ -105,36 +191,49 @@ target_link_libraries(naut_torrents PUBLIC naut_storage)
|
||||||
|
|
||||||
add_library(naut_rpc STATIC src/rpc/rpc.c)
|
add_library(naut_rpc STATIC src/rpc/rpc.c)
|
||||||
target_link_libraries(naut_rpc PUBLIC
|
target_link_libraries(naut_rpc PUBLIC
|
||||||
naut_session naut_core PkgConfig::JANSSON)
|
naut_session naut_core ${NAUT_JANSSON_TARGET})
|
||||||
|
|
||||||
add_library(naut_plugin STATIC src/plugin/plugin.c)
|
add_library(naut_plugin STATIC src/plugin/plugin.c)
|
||||||
target_link_libraries(naut_plugin PUBLIC naut_rpc naut_session dl)
|
target_link_libraries(naut_plugin PUBLIC naut_rpc naut_session dl)
|
||||||
|
|
||||||
add_library(naut_script STATIC src/script/script.c)
|
add_library(naut_script STATIC src/script/script.c)
|
||||||
target_link_libraries(naut_script PUBLIC
|
target_link_libraries(naut_script PUBLIC
|
||||||
naut_session naut_core PkgConfig::LUA)
|
naut_session naut_core ${NAUT_LUA_TARGET})
|
||||||
|
|
||||||
add_library(naut_example MODULE plugins/example/example.c)
|
add_library(naut_example MODULE plugins/example/example.c)
|
||||||
target_include_directories(naut_example PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
target_include_directories(naut_example PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||||
set_target_properties(naut_example PROPERTIES PREFIX "")
|
set_target_properties(naut_example PROPERTIES PREFIX "")
|
||||||
|
|
||||||
|
add_library(naut_webui MODULE plugins/webui/webui.c)
|
||||||
|
target_include_directories(naut_webui PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||||
|
target_link_libraries(naut_webui PRIVATE ${NAUT_JANSSON_TARGET} pthread)
|
||||||
|
set_target_properties(naut_webui PROPERTIES PREFIX "")
|
||||||
|
|
||||||
# --- echo: Phase 1 gate (io_uring echo server on the buffer pool) -----------
|
# --- echo: Phase 1 gate (io_uring echo server on the buffer pool) -----------
|
||||||
add_executable(naut_echo apps/echo/main.c)
|
# add_executable(naut_echo apps/echo/main.c)
|
||||||
target_link_libraries(naut_echo PRIVATE naut_platform naut_core)
|
# target_link_libraries(naut_echo PRIVATE naut_platform naut_core)
|
||||||
|
|
||||||
# --- leech: Phase 3 gate (single-peer download, byte-correct + verified) ----
|
# --- leech: Phase 3 gate (single-peer download, byte-correct + verified) ----
|
||||||
add_executable(naut_leech apps/leech/main.c)
|
# add_executable(naut_leech apps/leech/main.c)
|
||||||
target_link_libraries(naut_leech PRIVATE naut_piece naut_peer naut_metainfo)
|
# target_link_libraries(naut_leech PRIVATE
|
||||||
|
# naut_piece naut_peer naut_mse naut_metainfo)
|
||||||
|
|
||||||
# --- swarm: Phase 4 gate (multi-peer download, rarest-first + endgame) -------
|
# --- swarm: Phase 4 gate (multi-peer download, rarest-first + endgame) -------
|
||||||
add_executable(naut_swarm apps/swarm/main.c)
|
add_library(naut_swarm_engine STATIC apps/swarm/main.c)
|
||||||
target_link_libraries(naut_swarm PRIVATE
|
target_compile_definitions(naut_swarm_engine PRIVATE NAUT_SWARM_LIBRARY)
|
||||||
naut_piece naut_peer naut_metainfo naut_tracker naut_dht naut_platform)
|
target_link_libraries(naut_swarm_engine PUBLIC
|
||||||
|
naut_piece naut_peer naut_metainfo naut_tracker naut_dht naut_system
|
||||||
|
naut_session)
|
||||||
|
|
||||||
|
# add_executable(naut_swarm apps/swarm/main.c)
|
||||||
|
# target_link_libraries(naut_swarm PRIVATE
|
||||||
|
# naut_piece naut_peer naut_metainfo naut_tracker naut_dht naut_system
|
||||||
|
# naut_session)
|
||||||
|
|
||||||
# --- daemon + CLI: Phase 7 extensibility surface ---------------------------
|
# --- daemon + CLI: Phase 7 extensibility surface ---------------------------
|
||||||
add_executable(nautd apps/nautd/main.c)
|
add_executable(nautd apps/nautd/main.c)
|
||||||
target_link_libraries(nautd PRIVATE
|
target_link_libraries(nautd PRIVATE
|
||||||
naut_plugin naut_script naut_rpc naut_session naut_torrents naut_metainfo)
|
naut_plugin naut_script naut_rpc naut_session naut_metainfo naut_swarm_engine)
|
||||||
|
|
||||||
add_executable(nautctl apps/nautctl/main.c)
|
add_executable(nautctl apps/nautctl/main.c)
|
||||||
target_link_libraries(nautctl PRIVATE naut_rpc)
|
target_link_libraries(nautctl PRIVATE naut_rpc)
|
||||||
|
|
@ -202,7 +301,7 @@ target_link_libraries(test_extension PRIVATE naut_peer)
|
||||||
add_test(NAME test_extension COMMAND test_extension)
|
add_test(NAME test_extension COMMAND test_extension)
|
||||||
|
|
||||||
add_executable(test_mse tests/unit/test_mse.c)
|
add_executable(test_mse tests/unit/test_mse.c)
|
||||||
target_link_libraries(test_mse PRIVATE naut_peer)
|
target_link_libraries(test_mse PRIVATE naut_mse)
|
||||||
add_test(NAME test_mse COMMAND test_mse)
|
add_test(NAME test_mse COMMAND test_mse)
|
||||||
|
|
||||||
add_executable(test_tracker tests/unit/test_tracker.c)
|
add_executable(test_tracker tests/unit/test_tracker.c)
|
||||||
|
|
@ -234,38 +333,46 @@ target_link_libraries(test_picker PRIVATE naut_piece)
|
||||||
add_test(NAME test_picker COMMAND test_picker)
|
add_test(NAME test_picker COMMAND test_picker)
|
||||||
|
|
||||||
# Phase 3 interop gate: download from a real libtorrent seed (SKIPs without it).
|
# Phase 3 interop gate: download from a real libtorrent seed (SKIPs without it).
|
||||||
add_test(NAME interop_leech
|
# These gates exercise the standalone naut_leech/naut_swarm/naut_echo binaries,
|
||||||
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_interop.sh $<TARGET_FILE:naut_leech>)
|
# which are currently folded into the daemon — register them only when built.
|
||||||
set_tests_properties(interop_leech PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180)
|
if(TARGET naut_leech)
|
||||||
|
add_test(NAME interop_leech
|
||||||
|
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_interop.sh $<TARGET_FILE:naut_leech>)
|
||||||
|
set_tests_properties(interop_leech PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 180)
|
||||||
|
|
||||||
add_test(NAME interop_mse
|
add_test(NAME interop_mse
|
||||||
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_mse.sh $<TARGET_FILE:naut_leech>)
|
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_mse.sh $<TARGET_FILE:naut_leech>)
|
||||||
set_tests_properties(interop_mse PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
set_tests_properties(interop_mse PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_test(NAME interop_magnet_dht
|
if(TARGET naut_swarm)
|
||||||
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_magnet_dht.sh
|
add_test(NAME interop_magnet_dht
|
||||||
$<TARGET_FILE:naut_swarm>)
|
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_magnet_dht.sh
|
||||||
set_tests_properties(interop_magnet_dht PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
$<TARGET_FILE:naut_swarm>)
|
||||||
|
set_tests_properties(interop_magnet_dht PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||||
|
|
||||||
# Phase 4 interop gate: both libtorrent seeds must contribute to one download.
|
# Phase 4 interop gate: both libtorrent seeds must contribute to one download.
|
||||||
add_test(NAME interop_swarm
|
add_test(NAME interop_swarm
|
||||||
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_swarm.sh $<TARGET_FILE:naut_swarm>)
|
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_swarm.sh $<TARGET_FILE:naut_swarm>)
|
||||||
set_tests_properties(interop_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
set_tests_properties(interop_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||||
|
|
||||||
add_test(NAME interop_tracker_swarm
|
add_test(NAME interop_tracker_swarm
|
||||||
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_tracker_swarm.sh
|
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_tracker_swarm.sh
|
||||||
$<TARGET_FILE:naut_swarm> http)
|
$<TARGET_FILE:naut_swarm> http)
|
||||||
set_tests_properties(interop_tracker_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
set_tests_properties(interop_tracker_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||||
|
|
||||||
add_test(NAME interop_udp_tracker_swarm
|
add_test(NAME interop_udp_tracker_swarm
|
||||||
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_tracker_swarm.sh
|
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_tracker_swarm.sh
|
||||||
$<TARGET_FILE:naut_swarm> udp)
|
$<TARGET_FILE:naut_swarm> udp)
|
||||||
set_tests_properties(interop_udp_tracker_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
set_tests_properties(interop_udp_tracker_swarm PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_test(NAME interop_echo_scale
|
if(TARGET naut_echo)
|
||||||
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_echo_scale.sh
|
add_test(NAME interop_echo_scale
|
||||||
$<TARGET_FILE:naut_echo>)
|
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_echo_scale.sh
|
||||||
set_tests_properties(interop_echo_scale PROPERTIES TIMEOUT 30)
|
$<TARGET_FILE:naut_echo>)
|
||||||
|
set_tests_properties(interop_echo_scale PROPERTIES TIMEOUT 30)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_test(NAME phase7_extensibility
|
add_test(NAME phase7_extensibility
|
||||||
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_phase7.sh
|
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/integration/run_phase7.sh
|
||||||
|
|
@ -273,6 +380,7 @@ add_test(NAME phase7_extensibility
|
||||||
$<TARGET_FILE:naut_example>
|
$<TARGET_FILE:naut_example>
|
||||||
${CMAKE_SOURCE_DIR}/tests/fixtures/phase7.lua)
|
${CMAKE_SOURCE_DIR}/tests/fixtures/phase7.lua)
|
||||||
set_tests_properties(phase7_extensibility PROPERTIES TIMEOUT 15)
|
set_tests_properties(phase7_extensibility PROPERTIES TIMEOUT 15)
|
||||||
|
set_tests_properties(phase7_extensibility PROPERTIES SKIP_RETURN_CODE 77)
|
||||||
|
|
||||||
# Example Lua scripts: parser battery + end-to-end sort path building. Only
|
# Example Lua scripts: parser battery + end-to-end sort path building. Only
|
||||||
# registered when a standalone lua interpreter is available.
|
# registered when a standalone lua interpreter is available.
|
||||||
|
|
|
||||||
113
README.md
113
README.md
|
|
@ -25,6 +25,8 @@ src/peer/ wire protocol, MSE/RC4, BEP-10, ut_metadata, and PEX
|
||||||
apps/echo/ Phase 1 gate: io_uring echo server on the buffer pool
|
apps/echo/ Phase 1 gate: io_uring echo server on the buffer pool
|
||||||
apps/leech/ Phase 3 gate: verified single-peer download
|
apps/leech/ Phase 3 gate: verified single-peer download
|
||||||
apps/swarm/ tracker/DHT discovery, magnets, and concurrent peers
|
apps/swarm/ tracker/DHT discovery, magnets, and concurrent peers
|
||||||
|
apps/nautctl/ thin CLI frontend over daemon RPC
|
||||||
|
plugins/webui/ daemon plugin that serves ../torrent-ui as the web panel
|
||||||
tests/unit/ unit + concurrency tests
|
tests/unit/ unit + concurrency tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -35,6 +37,13 @@ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||||
ninja -C build
|
ninja -C build
|
||||||
ctest --test-dir build --output-on-failure
|
ctest --test-dir build --output-on-failure
|
||||||
|
|
||||||
|
# portable daemon/client build with Jansson and Lua embedded
|
||||||
|
cmake -S . -B build-standalone -G Ninja \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release -DNAUT_STANDALONE=ON
|
||||||
|
ninja -C build-standalone nautd nautctl
|
||||||
|
ldd build-standalone/nautd
|
||||||
|
ldd build-standalone/nautctl
|
||||||
|
|
||||||
# sanitizer build (address|thread|undefined)
|
# sanitizer build (address|thread|undefined)
|
||||||
cmake -S . -B build-tsan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DNAUT_SAN=thread
|
cmake -S . -B build-tsan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DNAUT_SAN=thread
|
||||||
ninja -C build-tsan && ./build-tsan/test_buf
|
ninja -C build-tsan && ./build-tsan/test_buf
|
||||||
|
|
@ -42,13 +51,16 @@ ninja -C build-tsan && ./build-tsan/test_buf
|
||||||
# run the Phase 1 echo gate
|
# run the Phase 1 echo gate
|
||||||
./build/naut_echo 9000
|
./build/naut_echo 9000
|
||||||
|
|
||||||
# download from explicit peers, or omit them to use the torrent's trackers
|
# start the engine, then add and inspect downloads through its RPC frontend
|
||||||
./build/naut_swarm file.torrent output/ 192.0.2.10:6881 192.0.2.11:6881
|
./build/nautd
|
||||||
./build/naut_swarm file.torrent output/
|
./build/nautctl add file.torrent output/
|
||||||
|
./build/nautctl list
|
||||||
|
./build/nautctl show 1
|
||||||
|
./build/nautctl events
|
||||||
|
|
||||||
# trackerless magnet start through DHT (override bootstraps when needed)
|
# explicit peers and trackerless magnets use the same daemon workflow
|
||||||
./build/naut_swarm 'magnet:?xt=urn:btih:...' output/
|
./build/nautctl add file.torrent output/ 192.0.2.10:6881
|
||||||
NAUT_DHT_BOOTSTRAP=127.0.0.1:6881 ./build/naut_swarm 'magnet:?xt=urn:btih:...' output/
|
./build/nautctl add 'magnet:?xt=urn:btih:...' output/
|
||||||
|
|
||||||
# force an encrypted single-peer MSE/RC4 connection
|
# force an encrypted single-peer MSE/RC4 connection
|
||||||
./build/naut_leech --mse file.torrent output/ 192.0.2.10 6881
|
./build/naut_leech --mse file.torrent output/ 192.0.2.10 6881
|
||||||
|
|
@ -59,41 +71,86 @@ NAUT_DHT_BOOTSTRAP=127.0.0.1:6881 ./build/naut_swarm 'magnet:?xt=urn:btih:...' o
|
||||||
bash tests/integration/run_echo_scale.sh ./build/naut_echo
|
bash tests/integration/run_echo_scale.sh ./build/naut_echo
|
||||||
|
|
||||||
# optional data-path tuning
|
# optional data-path tuning
|
||||||
NAUT_DIRECT_IO=1 NAUT_WORKERS=8 ./build/naut_swarm file.torrent output/
|
NAUT_DIRECT_IO=1 NAUT_WORKERS=8 ./build/nautd
|
||||||
NAUT_CPU=2 NAUT_SQPOLL=1 NAUT_HUGEPAGES=1 NAUT_NUMA_NODE=0 ./build/naut_echo 9000
|
NAUT_CPU=2 NAUT_SQPOLL=1 NAUT_HUGEPAGES=1 NAUT_NUMA_NODE=0 ./build/naut_echo 9000
|
||||||
```
|
```
|
||||||
|
|
||||||
Requirements: Linux ≥ 6.0, `liburing` (≥ 2.x), OpenSSL `libcrypto`, Jansson,
|
Requirements: Linux ≥ 6.0, `liburing` (≥ 2.x), OpenSSL `libcrypto`, Jansson,
|
||||||
Lua, CMake ≥ 3.20, gcc/clang, Ninja.
|
Lua, CMake ≥ 3.20, gcc/clang, Ninja.
|
||||||
|
|
||||||
|
`NAUT_STANDALONE=ON` downloads hash-pinned Jansson 2.14.1 and Lua 5.4.8
|
||||||
|
sources at configure time and statically embeds them in `nautd` and `nautctl`.
|
||||||
|
The resulting executables still use the host's glibc/ELF loader intentionally:
|
||||||
|
fully static glibc breaks normal DNS/NSS behavior and native `.so` plugins.
|
||||||
|
Release builds target a portable CPU baseline. Use `-DNAUT_NATIVE=ON` only for
|
||||||
|
a local build that will run on the same CPU family as the build machine.
|
||||||
|
|
||||||
## Daemon, RPC, plugins, and scripts
|
## Daemon, RPC, plugins, and scripts
|
||||||
|
|
||||||
Phase 7 adds a headless control process and thin CLI over a versioned,
|
`nautd` is the application engine: it owns torrent workers, storage, scripts,
|
||||||
length-prefixed JSON protocol on a Unix socket:
|
plugins, progress, and lifecycle. `nautctl` is one thin frontend over a
|
||||||
|
versioned, length-prefixed JSON protocol on a Unix socket; a desktop or web
|
||||||
|
panel can use the same RPC surface.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./build/nautd \
|
./build/nautd \
|
||||||
--socket /tmp/nautd.sock \
|
--socket /tmp/nautd.sock \
|
||||||
--plugin ./build/naut_example.so \
|
--plugin ./build/naut_example.so
|
||||||
--script ./tests/fixtures/phase7.lua
|
|
||||||
|
|
||||||
./build/nautctl ping
|
./build/nautctl ping
|
||||||
./build/nautctl plugins
|
./build/nautctl plugins
|
||||||
./build/nautctl status
|
./build/nautctl status
|
||||||
|
./build/nautctl script ./examples/anime_sort.lua
|
||||||
|
./build/nautctl add show.torrent /downloads/show
|
||||||
|
./build/nautctl list
|
||||||
./build/nautctl events
|
./build/nautctl events
|
||||||
```
|
```
|
||||||
|
|
||||||
`nautctl` accepts an optional JSON value after the method:
|
The convenience commands cover normal operation:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# register a torrent's storage so a move command can resolve + relocate its files
|
./build/nautctl add file.torrent output/ [IP:PORT ...]
|
||||||
./build/nautctl add_torrent \
|
./build/nautctl list
|
||||||
'{"torrent_id":7,"torrent":"file.torrent","root":"output/"}'
|
./build/nautctl show 1
|
||||||
./build/nautctl emit \
|
./build/nautctl remove 1
|
||||||
'{"type":"torrent_finished","torrent_id":7}'
|
./build/nautctl script rules.lua
|
||||||
|
./build/nautctl unscript
|
||||||
./build/nautctl shutdown
|
./build/nautctl shutdown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For tooling and plugin methods, the generic form remains
|
||||||
|
`nautctl METHOD [PARAMS_JSON]`.
|
||||||
|
|
||||||
|
### Web panel
|
||||||
|
|
||||||
|
The web panel is a daemon plugin, not part of `nautctl`. It serves the static
|
||||||
|
frontend from `../torrent-ui/public` by default and adapts that UI's `/api/*`
|
||||||
|
contract to Naut's daemon RPC surface:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
NAUT_WEBUI_ROOT=../torrent-ui/public \
|
||||||
|
NAUT_AUTH_PASSWORD='change-me' \
|
||||||
|
./build/nautd --socket /tmp/nautd.sock --plugin ./build/naut_webui.so
|
||||||
|
# open http://127.0.0.1:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
NAUT_WEBUI_HOST=127.0.0.1 # default
|
||||||
|
NAUT_WEBUI_PORT=8080 # default
|
||||||
|
NAUT_WEBUI_ROOT=../torrent-ui/public
|
||||||
|
NAUT_AUTH_USER=admin # default
|
||||||
|
NAUT_AUTH_PASSWORD=change-me # generated and logged if omitted
|
||||||
|
NAUT_WEBUI_SAVE_PATH=/downloads # default add-torrent destination
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin implements the stable `torrent-ui` API surface: cookie login,
|
||||||
|
`/api/snapshot`, `/api/stream` Server-Sent Events, `/api/meta`, torrent detail
|
||||||
|
tabs, add/remove, and `/api/plugins` loading ES modules from
|
||||||
|
`public/plugins/plugins.json`. Some advanced qBittorrent-style controls in the
|
||||||
|
UI are accepted as no-ops until Naut grows matching daemon RPC methods.
|
||||||
|
|
||||||
The native ABI is declared in `include/naut/naut_plugin.h`. Plugins export
|
The native ABI is declared in `include/naut/naut_plugin.h`. Plugins export
|
||||||
`naut_plugin_register()`, receive the versioned host API, and may register RPC
|
`naut_plugin_register()`, receive the versioned host API, and may register RPC
|
||||||
methods, storage backends, and event handlers. `plugins/example/example.c`
|
methods, storage backends, and event handlers. `plugins/example/example.c`
|
||||||
|
|
@ -105,12 +162,11 @@ hooks are `on_torrent_added`, `on_piece_complete`, `on_file_complete`,
|
||||||
filesystem, process, package-loading, debug, and raw chunk-loading globals
|
filesystem, process, package-loading, debug, and raw chunk-loading globals
|
||||||
(`os`, `io`, `package`/`require`, `debug`, `dofile`/`loadfile`, and
|
(`os`, `io`, `package`/`require`, `debug`, `dofile`/`loadfile`, and
|
||||||
`load`/`loadstring` — the bytecode loaders are denied so a crafted binary chunk
|
`load`/`loadstring` — the bytecode loaders are denied so a crafted binary chunk
|
||||||
can't escape the VM). `naut.move_file()` submits a bounded command from the
|
can't escape the VM). `naut.move_file()` submits a bounded command to the
|
||||||
script thread to the daemon owner thread; the owner resolves it through the
|
worker that owns the torrent. That worker performs
|
||||||
torrent registry (`naut_session`) and performs the relocate with
|
`naut_storage_relocate()` and keeps tracking the file at its new path.
|
||||||
`naut_storage_relocate()`. Register a torrent's storage first with the
|
`phase7_extensibility` drives a real daemon-owned download end to end and
|
||||||
`add_torrent` RPC so the id resolves. `phase7_extensibility` drives this
|
asserts the moved file byte-for-byte.
|
||||||
end to end and asserts the file actually moves on disk.
|
|
||||||
|
|
||||||
The full script-visible surface — every event hook, the `event` object's
|
The full script-visible surface — every event hook, the `event` object's
|
||||||
fields, and the `naut` API table — is documented in
|
fields, and the `naut` API table — is documented in
|
||||||
|
|
@ -211,9 +267,8 @@ embedded Anitomy-style filename parser ([`examples/`](examples/)).
|
||||||
piece verifies — before the torrent finishes — and `naut_storage_relocate()`
|
piece verifies — before the torrent finishes — and `naut_storage_relocate()`
|
||||||
moves that file out safely (even mid-download, while other files' pieces are
|
moves that file out safely (even mid-download, while other files' pieces are
|
||||||
still arriving). `test_filemove` proves a file is relocated mid-download with
|
still arriving). `test_filemove` proves a file is relocated mid-download with
|
||||||
no corruption. The scripting layer (Phase 7) forwards the event to an
|
no corruption. The scripting layer forwards the event to an
|
||||||
`on_file_complete` hook and exposes `move_file`; the daemon resolves the
|
`on_file_complete` hook and exposes `move_file`; the daemon queues the command
|
||||||
command through the `naut_session` torrent registry (`src/session/session.c`)
|
back to the worker that owns the torrent's storage.
|
||||||
and calls `naut_storage_relocate()` on its owner thread. `phase7_extensibility`
|
`phase7_extensibility` exercises the whole chain — download worker → script
|
||||||
exercises the whole chain — script thread → bounded queue → owner thread →
|
thread → bounded command queue → download worker — and checks the moved bytes.
|
||||||
storage — and asserts the file moves on disk.
|
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,16 @@
|
||||||
|
|
||||||
static void usage(const char *program) {
|
static void usage(const char *program) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"usage: %s [--socket PATH] METHOD [PARAMS_JSON]\n"
|
"usage: %s [--socket PATH] COMMAND [ARGS]\n"
|
||||||
" %s [--socket PATH] events\n", program, program);
|
"\n"
|
||||||
|
"commands:\n"
|
||||||
|
" add SOURCE OUTPUT [IP:PORT ...]\n"
|
||||||
|
" list\n"
|
||||||
|
" show TORRENT_ID\n"
|
||||||
|
" remove TORRENT_ID\n"
|
||||||
|
" script PATH | unscript\n"
|
||||||
|
" status | events | shutdown\n"
|
||||||
|
" METHOD [PARAMS_JSON] (raw RPC)\n", program);
|
||||||
}
|
}
|
||||||
|
|
||||||
static json_t *parse_params(const char *text) {
|
static json_t *parse_params(const char *text) {
|
||||||
|
|
@ -54,6 +62,15 @@ static int stream_events(const char *socket_path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool parse_id(const char *text, json_int_t *id) {
|
||||||
|
if (!text || !*text || *text == '-') return false;
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long long value = strtoull(text, &end, 10);
|
||||||
|
if (!end || *end || value > (unsigned long long)INT64_MAX) return false;
|
||||||
|
*id = (json_int_t)value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
const char *socket_path = getenv("NAUT_SOCKET");
|
const char *socket_path = getenv("NAUT_SOCKET");
|
||||||
if (!socket_path || !*socket_path) socket_path = DEFAULT_SOCKET;
|
if (!socket_path || !*socket_path) socket_path = DEFAULT_SOCKET;
|
||||||
|
|
@ -66,7 +83,7 @@ int main(int argc, char **argv) {
|
||||||
socket_path = argv[arg + 1];
|
socket_path = argv[arg + 1];
|
||||||
arg += 2;
|
arg += 2;
|
||||||
}
|
}
|
||||||
if (arg >= argc || arg + 2 < argc) {
|
if (arg >= argc) {
|
||||||
usage(argv[0]);
|
usage(argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +91,49 @@ int main(int argc, char **argv) {
|
||||||
const char *method = argv[arg++];
|
const char *method = argv[arg++];
|
||||||
if (strcmp(method, "events") == 0)
|
if (strcmp(method, "events") == 0)
|
||||||
return stream_events(socket_path);
|
return stream_events(socket_path);
|
||||||
json_t *params = parse_params(arg < argc ? argv[arg] : NULL);
|
|
||||||
|
json_t *params = NULL;
|
||||||
|
if (strcmp(method, "add") == 0) {
|
||||||
|
if (arg + 1 >= argc) { usage(argv[0]); return 2; }
|
||||||
|
method = "add_torrent";
|
||||||
|
params = json_pack("{s:s,s:s}", "source", argv[arg],
|
||||||
|
"output", argv[arg + 1]);
|
||||||
|
arg += 2;
|
||||||
|
json_t *peers = json_array();
|
||||||
|
if (!params || !peers) {
|
||||||
|
json_decref(params);
|
||||||
|
json_decref(peers);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
while (arg < argc)
|
||||||
|
json_array_append_new(peers, json_string(argv[arg++]));
|
||||||
|
json_object_set_new(params, "peers", peers);
|
||||||
|
} else if (strcmp(method, "list") == 0) {
|
||||||
|
if (arg != argc) { usage(argv[0]); return 2; }
|
||||||
|
method = "torrents";
|
||||||
|
params = json_object();
|
||||||
|
} else if (strcmp(method, "show") == 0 ||
|
||||||
|
strcmp(method, "remove") == 0) {
|
||||||
|
json_int_t id;
|
||||||
|
if (arg + 1 != argc || !parse_id(argv[arg], &id)) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
method = strcmp(method, "show") == 0 ? "torrent" :
|
||||||
|
"remove_torrent";
|
||||||
|
params = json_pack("{s:I}", "torrent_id", id);
|
||||||
|
} else if (strcmp(method, "script") == 0) {
|
||||||
|
if (arg + 1 != argc) { usage(argv[0]); return 2; }
|
||||||
|
method = "load_script";
|
||||||
|
params = json_pack("{s:s}", "path", argv[arg]);
|
||||||
|
} else if (strcmp(method, "unscript") == 0) {
|
||||||
|
if (arg != argc) { usage(argv[0]); return 2; }
|
||||||
|
method = "unload_script";
|
||||||
|
params = json_object();
|
||||||
|
} else {
|
||||||
|
if (arg + 1 < argc) { usage(argv[0]); return 2; }
|
||||||
|
params = parse_params(arg < argc ? argv[arg] : NULL);
|
||||||
|
}
|
||||||
if (!params) {
|
if (!params) {
|
||||||
fprintf(stderr, "nautctl: invalid JSON parameters\n");
|
fprintf(stderr, "nautctl: invalid JSON parameters\n");
|
||||||
return 2;
|
return 2;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
#include "naut/event.h"
|
#include "naut/event.h"
|
||||||
#include "naut/log.h"
|
#include "naut/log.h"
|
||||||
#include "naut/metainfo.h"
|
|
||||||
#include "naut/plugin.h"
|
#include "naut/plugin.h"
|
||||||
#include "naut/rpc.h"
|
#include "naut/rpc.h"
|
||||||
#include "naut/script.h"
|
#include "naut/script.h"
|
||||||
#include "naut/session.h"
|
|
||||||
#include "naut/storage.h"
|
#include "naut/storage.h"
|
||||||
|
#include "naut/swarm.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
|
@ -23,37 +22,221 @@
|
||||||
#define DEFAULT_SOCKET "/tmp/nautd.sock"
|
#define DEFAULT_SOCKET "/tmp/nautd.sock"
|
||||||
#define MOVE_QUEUE_CAPACITY 64
|
#define MOVE_QUEUE_CAPACITY 64
|
||||||
#define MAX_SUBSCRIBERS 64
|
#define MAX_SUBSCRIBERS 64
|
||||||
|
#define MAX_TORRENTS 128
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint64_t torrent_id;
|
|
||||||
uint32_t file_index;
|
uint32_t file_index;
|
||||||
char destination[PATH_MAX];
|
char destination[PATH_MAX];
|
||||||
} move_command;
|
} move_command;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
TORRENT_QUEUED,
|
||||||
|
TORRENT_RUNNING,
|
||||||
|
TORRENT_COMPLETE,
|
||||||
|
TORRENT_STOPPING,
|
||||||
|
TORRENT_STOPPED,
|
||||||
|
TORRENT_ERROR,
|
||||||
|
} torrent_state;
|
||||||
|
|
||||||
|
typedef struct daemon_state daemon_state;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
naut_event_bus *events;
|
daemon_state *daemon;
|
||||||
naut_rpc_registry *rpc;
|
uint64_t id;
|
||||||
naut_plugin_manager *plugins;
|
char *source;
|
||||||
naut_script *script;
|
bool source_is_temp; /* source is a daemon-owned upload; unlink on destroy */
|
||||||
naut_session *session;
|
char *output_dir;
|
||||||
pthread_mutex_t move_lock;
|
char **peers;
|
||||||
|
size_t num_peers;
|
||||||
|
pthread_t thread;
|
||||||
|
bool thread_started;
|
||||||
|
bool thread_done;
|
||||||
|
pthread_mutex_t lock;
|
||||||
|
torrent_state state;
|
||||||
|
naut_err result;
|
||||||
|
bool stop_requested;
|
||||||
|
bool remove_requested;
|
||||||
|
naut_swarm_stats stats;
|
||||||
move_command moves[MOVE_QUEUE_CAPACITY];
|
move_command moves[MOVE_QUEUE_CAPACITY];
|
||||||
size_t move_head;
|
size_t move_head;
|
||||||
size_t move_count;
|
size_t move_count;
|
||||||
uint64_t moves_processed;
|
uint64_t moves_processed;
|
||||||
|
uint64_t moves_failed;
|
||||||
|
} torrent_task;
|
||||||
|
|
||||||
|
struct daemon_state {
|
||||||
|
naut_event_bus *events;
|
||||||
|
naut_rpc_registry *rpc;
|
||||||
|
naut_plugin_manager *plugins;
|
||||||
|
naut_script *script;
|
||||||
|
pthread_mutex_t torrent_lock;
|
||||||
|
torrent_task *torrents[MAX_TORRENTS];
|
||||||
|
size_t torrent_count;
|
||||||
|
uint64_t next_torrent_id;
|
||||||
pthread_mutex_t subscriber_lock;
|
pthread_mutex_t subscriber_lock;
|
||||||
int subscribers[MAX_SUBSCRIBERS];
|
int subscribers[MAX_SUBSCRIBERS];
|
||||||
size_t subscriber_count;
|
size_t subscriber_count;
|
||||||
bool stopping;
|
bool stopping;
|
||||||
} daemon_state;
|
};
|
||||||
|
|
||||||
static volatile sig_atomic_t interrupted;
|
static volatile sig_atomic_t interrupted;
|
||||||
|
|
||||||
|
static naut_err queue_move(void *opaque, uint64_t torrent_id,
|
||||||
|
uint32_t file_index, const char *destination);
|
||||||
|
|
||||||
static void on_signal(int signal_number) {
|
static void on_signal(int signal_number) {
|
||||||
(void)signal_number;
|
(void)signal_number;
|
||||||
interrupted = 1;
|
interrupted = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *torrent_state_name(torrent_state state) {
|
||||||
|
static const char *names[] = {
|
||||||
|
[TORRENT_QUEUED] = "queued",
|
||||||
|
[TORRENT_RUNNING] = "downloading",
|
||||||
|
[TORRENT_COMPLETE] = "complete",
|
||||||
|
[TORRENT_STOPPING] = "stopping",
|
||||||
|
[TORRENT_STOPPED] = "stopped",
|
||||||
|
[TORRENT_ERROR] = "error",
|
||||||
|
};
|
||||||
|
return (size_t)state < NAUT_ARRAY_LEN(names) ? names[state] : "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
static torrent_task *find_torrent_locked(daemon_state *state, uint64_t id) {
|
||||||
|
for (size_t i = 0; i < state->torrent_count; i++)
|
||||||
|
if (state->torrents[i]->id == id) return state->torrents[i];
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void torrent_progress(void *opaque, const naut_swarm_stats *stats) {
|
||||||
|
torrent_task *task = opaque;
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
task->stats = *stats;
|
||||||
|
if (stats->total_pieces > 0 &&
|
||||||
|
stats->pieces_done == stats->total_pieces)
|
||||||
|
task->state = TORRENT_COMPLETE;
|
||||||
|
else if (task->state == TORRENT_QUEUED)
|
||||||
|
task->state = TORRENT_RUNNING;
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool torrent_should_stop(void *opaque) {
|
||||||
|
torrent_task *task = opaque;
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
bool stop = task->stop_requested;
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
return stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void torrent_control(void *opaque, naut_storage *storage) {
|
||||||
|
torrent_task *task = opaque;
|
||||||
|
for (;;) {
|
||||||
|
move_command command;
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
if (task->move_count == 0) {
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
command = task->moves[task->move_head];
|
||||||
|
task->move_head = (task->move_head + 1) % MOVE_QUEUE_CAPACITY;
|
||||||
|
task->move_count--;
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
|
||||||
|
naut_err error = naut_storage_relocate(
|
||||||
|
storage, command.file_index, command.destination);
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
if (error == NAUT_OK)
|
||||||
|
task->moves_processed++;
|
||||||
|
else
|
||||||
|
task->moves_failed++;
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
if (error == NAUT_OK)
|
||||||
|
NAUT_INFO("moved torrent=%llu file=%u -> %s",
|
||||||
|
(unsigned long long)task->id, command.file_index,
|
||||||
|
command.destination);
|
||||||
|
else
|
||||||
|
NAUT_WARN("move torrent=%llu file=%u -> %s failed: %s",
|
||||||
|
(unsigned long long)task->id, command.file_index,
|
||||||
|
command.destination, naut_strerror(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *torrent_worker(void *opaque) {
|
||||||
|
torrent_task *task = opaque;
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
task->state = TORRENT_RUNNING;
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
|
||||||
|
naut_swarm_config config = {
|
||||||
|
.source = task->source,
|
||||||
|
.output_dir = task->output_dir,
|
||||||
|
.peers = (const char *const *)task->peers,
|
||||||
|
.num_peers = task->num_peers,
|
||||||
|
.torrent_id = task->id,
|
||||||
|
.events = task->daemon->events,
|
||||||
|
.keep_alive = true,
|
||||||
|
.on_progress = torrent_progress,
|
||||||
|
.on_control = torrent_control,
|
||||||
|
.should_stop = torrent_should_stop,
|
||||||
|
.context = task,
|
||||||
|
};
|
||||||
|
naut_err result = naut_swarm_run(&config);
|
||||||
|
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
task->result = result;
|
||||||
|
if (result == NAUT_OK)
|
||||||
|
task->state = TORRENT_COMPLETE;
|
||||||
|
else if (task->stop_requested)
|
||||||
|
task->state = TORRENT_STOPPED;
|
||||||
|
else
|
||||||
|
task->state = TORRENT_ERROR;
|
||||||
|
task->thread_done = true;
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static json_t *torrent_json(torrent_task *task) {
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
json_t *result = json_object();
|
||||||
|
if (result) {
|
||||||
|
json_object_set_new(result, "torrent_id",
|
||||||
|
json_integer((json_int_t)task->id));
|
||||||
|
json_object_set_new(result, "source", json_string(task->source));
|
||||||
|
json_object_set_new(result, "output",
|
||||||
|
json_string(task->output_dir));
|
||||||
|
json_object_set_new(result, "state",
|
||||||
|
json_string(torrent_state_name(task->state)));
|
||||||
|
json_object_set_new(result, "bytes_done",
|
||||||
|
json_integer((json_int_t)task->stats.bytes_done));
|
||||||
|
json_object_set_new(result, "total_bytes",
|
||||||
|
json_integer((json_int_t)task->stats.total_bytes));
|
||||||
|
json_object_set_new(result, "pieces_done",
|
||||||
|
json_integer(task->stats.pieces_done));
|
||||||
|
json_object_set_new(result, "total_pieces",
|
||||||
|
json_integer(task->stats.total_pieces));
|
||||||
|
json_object_set_new(result, "peers",
|
||||||
|
json_integer(task->stats.peers_active));
|
||||||
|
json_object_set_new(result, "peers_discovered",
|
||||||
|
json_integer(task->stats.peers_total));
|
||||||
|
json_object_set_new(result, "peers_connecting",
|
||||||
|
json_integer(task->stats.peers_connecting));
|
||||||
|
json_object_set_new(result, "peers_failed",
|
||||||
|
json_integer(task->stats.peers_failed));
|
||||||
|
json_object_set_new(result, "elapsed_seconds",
|
||||||
|
json_real(task->stats.elapsed_seconds));
|
||||||
|
json_object_set_new(result, "pending_moves",
|
||||||
|
json_integer((json_int_t)task->move_count));
|
||||||
|
json_object_set_new(result, "moves_processed",
|
||||||
|
json_integer((json_int_t)task->moves_processed));
|
||||||
|
json_object_set_new(result, "moves_failed",
|
||||||
|
json_integer((json_int_t)task->moves_failed));
|
||||||
|
if (task->state == TORRENT_ERROR)
|
||||||
|
json_object_set_new(result, "error",
|
||||||
|
json_string(naut_strerror(task->result)));
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
static json_t *rpc_ping(void *opaque, const json_t *params, naut_err *error) {
|
static json_t *rpc_ping(void *opaque, const json_t *params, naut_err *error) {
|
||||||
(void)opaque;
|
(void)opaque;
|
||||||
(void)params;
|
(void)params;
|
||||||
|
|
@ -74,10 +257,23 @@ static json_t *rpc_status(void *opaque, const json_t *params,
|
||||||
daemon_state *state = opaque;
|
daemon_state *state = opaque;
|
||||||
naut_script_stats stats = {0};
|
naut_script_stats stats = {0};
|
||||||
if (state->script) naut_script_get_stats(state->script, &stats);
|
if (state->script) naut_script_get_stats(state->script, &stats);
|
||||||
pthread_mutex_lock(&state->move_lock);
|
size_t torrent_count;
|
||||||
uint64_t moves = state->moves_processed;
|
size_t active = 0;
|
||||||
size_t pending = state->move_count;
|
uint64_t moves = 0;
|
||||||
pthread_mutex_unlock(&state->move_lock);
|
size_t pending = 0;
|
||||||
|
pthread_mutex_lock(&state->torrent_lock);
|
||||||
|
torrent_count = state->torrent_count;
|
||||||
|
for (size_t i = 0; i < torrent_count; i++) {
|
||||||
|
torrent_task *task = state->torrents[i];
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
if (task->state == TORRENT_RUNNING ||
|
||||||
|
task->state == TORRENT_STOPPING)
|
||||||
|
active++;
|
||||||
|
moves += task->moves_processed;
|
||||||
|
pending += task->move_count;
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
json_t *result = json_object();
|
json_t *result = json_object();
|
||||||
json_t *script = json_object();
|
json_t *script = json_object();
|
||||||
if (!result || !script) {
|
if (!result || !script) {
|
||||||
|
|
@ -93,6 +289,12 @@ static json_t *rpc_status(void *opaque, const json_t *params,
|
||||||
json_object_set_new(result, "storage_backends",
|
json_object_set_new(result, "storage_backends",
|
||||||
json_integer((json_int_t)naut_plugin_storage_count(
|
json_integer((json_int_t)naut_plugin_storage_count(
|
||||||
state->plugins)));
|
state->plugins)));
|
||||||
|
json_object_set_new(result, "torrents",
|
||||||
|
json_integer((json_int_t)torrent_count));
|
||||||
|
json_object_set_new(result, "active_torrents",
|
||||||
|
json_integer((json_int_t)active));
|
||||||
|
json_object_set_new(result, "script_loaded",
|
||||||
|
json_boolean(state->script != NULL));
|
||||||
json_object_set_new(script, "queued", json_integer(stats.queued));
|
json_object_set_new(script, "queued", json_integer(stats.queued));
|
||||||
json_object_set_new(script, "handled", json_integer(stats.handled));
|
json_object_set_new(script, "handled", json_integer(stats.handled));
|
||||||
json_object_set_new(script, "dropped", json_integer(stats.dropped));
|
json_object_set_new(script, "dropped", json_integer(stats.dropped));
|
||||||
|
|
@ -178,122 +380,328 @@ static json_t *rpc_shutdown(void *opaque, const json_t *params,
|
||||||
return json_true();
|
return json_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t *read_file(const char *path, size_t *len) {
|
static int b64_val(int c) {
|
||||||
FILE *f = fopen(path, "rb");
|
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||||
if (!f) return NULL;
|
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||||
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; }
|
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||||||
long n = ftell(f);
|
if (c == '+') return 62;
|
||||||
if (n < 0 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); return NULL; }
|
if (c == '/') return 63;
|
||||||
uint8_t *buf = malloc((size_t)n);
|
return -1; /* padding / whitespace / invalid -> skipped */
|
||||||
if (!buf) { fclose(f); return NULL; }
|
}
|
||||||
if (fread(buf, 1, (size_t)n, f) != (size_t)n) {
|
|
||||||
free(buf); fclose(f); return NULL;
|
/* Decode standard base64 (padding optional, whitespace ignored). */
|
||||||
}
|
static unsigned char *b64_decode(const char *in, size_t *out_len) {
|
||||||
fclose(f);
|
size_t cap = strlen(in) / 4 * 3 + 4;
|
||||||
*len = (size_t)n;
|
unsigned char *out = malloc(cap);
|
||||||
return buf;
|
if (!out) return NULL;
|
||||||
|
size_t o = 0;
|
||||||
|
int acc = 0, bits = 0;
|
||||||
|
for (const char *p = in; *p; p++) {
|
||||||
|
if (*p == '=') break;
|
||||||
|
int v = b64_val((unsigned char)*p);
|
||||||
|
if (v < 0) continue;
|
||||||
|
acc = (acc << 6) | v;
|
||||||
|
bits += 6;
|
||||||
|
if (bits >= 8) { bits -= 8; out[o++] = (unsigned char)((acc >> bits) & 0xff); }
|
||||||
|
}
|
||||||
|
*out_len = o;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool write_all_fd(int fd, const void *buf, size_t len) {
|
||||||
|
const char *p = buf;
|
||||||
|
while (len) {
|
||||||
|
ssize_t n = write(fd, p, len);
|
||||||
|
if (n < 0) { if (errno == EINTR) continue; return false; }
|
||||||
|
p += n;
|
||||||
|
len -= (size_t)n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A browser uploads a .torrent's bytes as base64 in "data"; the daemon writes
|
||||||
|
* its own temp file and owns its lifecycle (no shared path with the client).
|
||||||
|
* Returns the temp path in `out` (size cap) or sets *error. */
|
||||||
|
static bool add_torrent_write_upload(const char *data_b64, char *out,
|
||||||
|
size_t cap, naut_err *error) {
|
||||||
|
size_t raw_len = 0;
|
||||||
|
unsigned char *raw = b64_decode(data_b64, &raw_len);
|
||||||
|
if (!raw || raw_len == 0) { free(raw); *error = NAUT_ERR_INVAL; return false; }
|
||||||
|
char tmpl[] = "/tmp/naut-upload-XXXXXX";
|
||||||
|
int fd = mkstemp(tmpl);
|
||||||
|
if (fd < 0) { free(raw); *error = NAUT_ERR_IO; return false; }
|
||||||
|
bool ok = write_all_fd(fd, raw, raw_len);
|
||||||
|
close(fd);
|
||||||
|
free(raw);
|
||||||
|
if (!ok) { unlink(tmpl); *error = NAUT_ERR_IO; return false; }
|
||||||
|
snprintf(out, cap, "%s", tmpl);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* add_torrent {torrent_id, torrent: <.torrent path>, root: <output dir>} opens
|
|
||||||
* the torrent's storage and registers it so move_file can later relocate one of
|
|
||||||
* its files. This is the control-plane seam that binds a script's move command
|
|
||||||
* to a concrete naut_storage; it runs on the daemon owner thread. */
|
|
||||||
static json_t *rpc_add_torrent(void *opaque, const json_t *params,
|
static json_t *rpc_add_torrent(void *opaque, const json_t *params,
|
||||||
naut_err *error) {
|
naut_err *error) {
|
||||||
daemon_state *state = opaque;
|
daemon_state *state = opaque;
|
||||||
if (!json_is_object(params)) { *error = NAUT_ERR_INVAL; return NULL; }
|
if (!json_is_object(params)) { *error = NAUT_ERR_INVAL; return NULL; }
|
||||||
json_int_t torrent_id =
|
|
||||||
json_integer_value(json_object_get(params, "torrent_id"));
|
const char *source =
|
||||||
const char *torrent_path =
|
json_string_value(json_object_get(params, "source"));
|
||||||
json_string_value(json_object_get(params, "torrent"));
|
if (!source)
|
||||||
const char *root = json_string_value(json_object_get(params, "root"));
|
source = json_string_value(json_object_get(params, "torrent"));
|
||||||
if (torrent_id < 0 || !torrent_path || !root) {
|
const char *data_b64 =
|
||||||
|
json_string_value(json_object_get(params, "data"));
|
||||||
|
const char *output =
|
||||||
|
json_string_value(json_object_get(params, "output"));
|
||||||
|
if (!output)
|
||||||
|
output = json_string_value(json_object_get(params, "root"));
|
||||||
|
json_t *peers_json = json_object_get(params, "peers");
|
||||||
|
/* need an output and either a source (path/magnet) or uploaded bytes */
|
||||||
|
if (!output || !*output ||
|
||||||
|
((!source || !*source) && (!data_b64 || !*data_b64)) ||
|
||||||
|
(peers_json && !json_is_array(peers_json))) {
|
||||||
*error = NAUT_ERR_INVAL;
|
*error = NAUT_ERR_INVAL;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (naut_session_has(state->session, (uint64_t)torrent_id)) {
|
|
||||||
|
/* materialize an upload into a daemon-owned temp .torrent */
|
||||||
|
char temp_source[PATH_MAX];
|
||||||
|
bool is_temp = false;
|
||||||
|
if ((!source || !*source) && data_b64 && *data_b64) {
|
||||||
|
if (!add_torrent_write_upload(data_b64, temp_source, sizeof temp_source,
|
||||||
|
error))
|
||||||
|
return NULL;
|
||||||
|
source = temp_source;
|
||||||
|
is_temp = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
torrent_task *task = calloc(1, sizeof(*task));
|
||||||
|
if (!task) {
|
||||||
|
if (is_temp) unlink(source);
|
||||||
|
*error = NAUT_ERR_NOMEM;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
task->daemon = state;
|
||||||
|
task->state = TORRENT_QUEUED;
|
||||||
|
task->result = NAUT_ERR_AGAIN;
|
||||||
|
task->source = strdup(source);
|
||||||
|
task->source_is_temp = is_temp;
|
||||||
|
task->output_dir = strdup(output);
|
||||||
|
if (!task->source || !task->output_dir) {
|
||||||
|
if (is_temp) unlink(temp_source);
|
||||||
|
free(task->source);
|
||||||
|
free(task->output_dir);
|
||||||
|
free(task);
|
||||||
|
*error = NAUT_ERR_NOMEM;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (pthread_mutex_init(&task->lock, NULL) != 0) {
|
||||||
|
if (is_temp) unlink(temp_source);
|
||||||
|
free(task->source);
|
||||||
|
free(task->output_dir);
|
||||||
|
free(task);
|
||||||
|
*error = NAUT_ERR_NOMEM;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
task->num_peers = peers_json ? json_array_size(peers_json) : 0;
|
||||||
|
if (task->num_peers) {
|
||||||
|
task->peers = calloc(task->num_peers, sizeof(*task->peers));
|
||||||
|
if (!task->peers) { *error = NAUT_ERR_NOMEM; goto fail_task; }
|
||||||
|
for (size_t i = 0; i < task->num_peers; i++) {
|
||||||
|
const char *peer =
|
||||||
|
json_string_value(json_array_get(peers_json, i));
|
||||||
|
if (!peer || !*peer) { *error = NAUT_ERR_INVAL; goto fail_task; }
|
||||||
|
task->peers[i] = strdup(peer);
|
||||||
|
if (!task->peers[i]) { *error = NAUT_ERR_NOMEM; goto fail_task; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json_t *id_json = json_object_get(params, "torrent_id");
|
||||||
|
pthread_mutex_lock(&state->torrent_lock);
|
||||||
|
if (state->torrent_count == MAX_TORRENTS) {
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
*error = NAUT_ERR_FULL;
|
||||||
|
goto fail_task;
|
||||||
|
}
|
||||||
|
if (id_json) {
|
||||||
|
if (!json_is_integer(id_json) || json_integer_value(id_json) < 0) {
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
*error = NAUT_ERR_INVAL;
|
||||||
|
goto fail_task;
|
||||||
|
}
|
||||||
|
task->id = (uint64_t)json_integer_value(id_json);
|
||||||
|
if (task->id < (uint64_t)INT64_MAX &&
|
||||||
|
task->id >= state->next_torrent_id)
|
||||||
|
state->next_torrent_id = task->id + 1;
|
||||||
|
} else {
|
||||||
|
while (find_torrent_locked(state, state->next_torrent_id))
|
||||||
|
state->next_torrent_id++;
|
||||||
|
if (state->next_torrent_id > (uint64_t)INT64_MAX) {
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
*error = NAUT_ERR_FULL;
|
||||||
|
goto fail_task;
|
||||||
|
}
|
||||||
|
task->id = state->next_torrent_id++;
|
||||||
|
}
|
||||||
|
if (find_torrent_locked(state, task->id)) {
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
*error = NAUT_ERR_INVAL;
|
||||||
|
goto fail_task;
|
||||||
|
}
|
||||||
|
state->torrents[state->torrent_count++] = task;
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
|
||||||
|
if (pthread_create(&task->thread, NULL, torrent_worker, task) != 0) {
|
||||||
|
pthread_mutex_lock(&state->torrent_lock);
|
||||||
|
state->torrent_count--;
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
*error = NAUT_ERR_NOMEM;
|
||||||
|
goto fail_task;
|
||||||
|
}
|
||||||
|
task->thread_started = true;
|
||||||
|
*error = NAUT_OK;
|
||||||
|
return torrent_json(task);
|
||||||
|
|
||||||
|
fail_task:
|
||||||
|
if (is_temp) unlink(temp_source);
|
||||||
|
for (size_t i = 0; i < task->num_peers; i++) free(task->peers[i]);
|
||||||
|
free(task->peers);
|
||||||
|
free(task->source);
|
||||||
|
free(task->output_dir);
|
||||||
|
pthread_mutex_destroy(&task->lock);
|
||||||
|
free(task);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static json_t *rpc_torrents(void *opaque, const json_t *params,
|
||||||
|
naut_err *error) {
|
||||||
|
(void)params;
|
||||||
|
daemon_state *state = opaque;
|
||||||
|
json_t *result = json_array();
|
||||||
|
if (!result) { *error = NAUT_ERR_NOMEM; return NULL; }
|
||||||
|
pthread_mutex_lock(&state->torrent_lock);
|
||||||
|
for (size_t i = 0; i < state->torrent_count; i++) {
|
||||||
|
json_t *item = torrent_json(state->torrents[i]);
|
||||||
|
if (!item || json_array_append_new(result, item) != 0) {
|
||||||
|
json_decref(item);
|
||||||
|
json_decref(result);
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
*error = NAUT_ERR_NOMEM;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
*error = NAUT_OK;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parse_torrent_id(const json_t *params, uint64_t *id) {
|
||||||
|
if (!json_is_object(params)) return false;
|
||||||
|
json_t *value = json_object_get(params, "torrent_id");
|
||||||
|
if (!json_is_integer(value) || json_integer_value(value) < 0) return false;
|
||||||
|
*id = (uint64_t)json_integer_value(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static json_t *rpc_torrent(void *opaque, const json_t *params,
|
||||||
|
naut_err *error) {
|
||||||
|
daemon_state *state = opaque;
|
||||||
|
uint64_t id;
|
||||||
|
if (!parse_torrent_id(params, &id)) {
|
||||||
*error = NAUT_ERR_INVAL;
|
*error = NAUT_ERR_INVAL;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
size_t len = 0;
|
pthread_mutex_lock(&state->torrent_lock);
|
||||||
uint8_t *raw = read_file(torrent_path, &len);
|
torrent_task *task = find_torrent_locked(state, id);
|
||||||
if (!raw) { *error = NAUT_ERR_IO; return NULL; }
|
json_t *result = task ? torrent_json(task) : NULL;
|
||||||
naut_metainfo mi;
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
naut_err err = naut_metainfo_parse(raw, len, &mi);
|
*error = task ? (result ? NAUT_OK : NAUT_ERR_NOMEM) : NAUT_ERR_NOTFOUND;
|
||||||
free(raw);
|
return result;
|
||||||
if (err != NAUT_OK) { *error = err; return NULL; }
|
}
|
||||||
naut_storage *storage =
|
|
||||||
naut_storage_open(mi.files, mi.num_files, root, &err);
|
static json_t *rpc_remove_torrent(void *opaque, const json_t *params,
|
||||||
if (!storage) {
|
naut_err *error) {
|
||||||
naut_metainfo_free(&mi);
|
daemon_state *state = opaque;
|
||||||
*error = err != NAUT_OK ? err : NAUT_ERR_IO;
|
uint64_t id;
|
||||||
|
if (!parse_torrent_id(params, &id)) {
|
||||||
|
*error = NAUT_ERR_INVAL;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
naut_metainfo_free(&mi);
|
pthread_mutex_lock(&state->torrent_lock);
|
||||||
err = naut_session_add(state->session, (uint64_t)torrent_id, storage);
|
torrent_task *task = find_torrent_locked(state, id);
|
||||||
if (err != NAUT_OK) {
|
if (task) {
|
||||||
naut_storage_close(storage);
|
pthread_mutex_lock(&task->lock);
|
||||||
*error = err;
|
task->stop_requested = true;
|
||||||
|
task->remove_requested = true;
|
||||||
|
if (!task->thread_done)
|
||||||
|
task->state = TORRENT_STOPPING;
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
if (!task) {
|
||||||
|
*error = NAUT_ERR_NOTFOUND;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
*error = NAUT_OK;
|
*error = NAUT_OK;
|
||||||
|
return torrent_json(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
static json_t *rpc_load_script(void *opaque, const json_t *params,
|
||||||
|
naut_err *error) {
|
||||||
|
daemon_state *state = opaque;
|
||||||
|
const char *path = json_is_object(params)
|
||||||
|
? json_string_value(json_object_get(params, "path")) : NULL;
|
||||||
|
if (!path || !*path) { *error = NAUT_ERR_INVAL; return NULL; }
|
||||||
|
naut_script *script = naut_script_create(
|
||||||
|
state->events, path, 256, queue_move, state, error);
|
||||||
|
if (!script) return NULL;
|
||||||
|
naut_script *old = state->script;
|
||||||
|
state->script = script;
|
||||||
|
naut_script_destroy(old);
|
||||||
|
*error = NAUT_OK;
|
||||||
|
return json_string(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static json_t *rpc_unload_script(void *opaque, const json_t *params,
|
||||||
|
naut_err *error) {
|
||||||
|
(void)params;
|
||||||
|
daemon_state *state = opaque;
|
||||||
|
naut_script *old = state->script;
|
||||||
|
state->script = NULL;
|
||||||
|
naut_script_destroy(old);
|
||||||
|
*error = NAUT_OK;
|
||||||
return json_true();
|
return json_true();
|
||||||
}
|
}
|
||||||
|
|
||||||
static naut_err queue_move(void *opaque, uint64_t torrent_id,
|
static naut_err queue_move(void *opaque, uint64_t torrent_id,
|
||||||
uint32_t file_index, const char *destination) {
|
uint32_t file_index, const char *destination) {
|
||||||
daemon_state *state = opaque;
|
daemon_state *state = opaque;
|
||||||
pthread_mutex_lock(&state->move_lock);
|
if (!destination || strlen(destination) >= PATH_MAX)
|
||||||
if (state->move_count == MOVE_QUEUE_CAPACITY) {
|
return NAUT_ERR_RANGE;
|
||||||
pthread_mutex_unlock(&state->move_lock);
|
pthread_mutex_lock(&state->torrent_lock);
|
||||||
|
torrent_task *task = find_torrent_locked(state, torrent_id);
|
||||||
|
if (!task) {
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
return NAUT_ERR_NOTFOUND;
|
||||||
|
}
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
if (task->remove_requested) {
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
return NAUT_ERR_NOTFOUND;
|
||||||
|
}
|
||||||
|
if (task->move_count == MOVE_QUEUE_CAPACITY) {
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
return NAUT_ERR_FULL;
|
return NAUT_ERR_FULL;
|
||||||
}
|
}
|
||||||
size_t tail = (state->move_head + state->move_count) %
|
size_t tail = (task->move_head + task->move_count) % MOVE_QUEUE_CAPACITY;
|
||||||
MOVE_QUEUE_CAPACITY;
|
task->moves[tail].file_index = file_index;
|
||||||
state->moves[tail] = (move_command) {
|
snprintf(task->moves[tail].destination,
|
||||||
.torrent_id = torrent_id,
|
sizeof task->moves[tail].destination, "%s", destination);
|
||||||
.file_index = file_index,
|
task->move_count++;
|
||||||
};
|
pthread_mutex_unlock(&task->lock);
|
||||||
snprintf(state->moves[tail].destination,
|
|
||||||
sizeof state->moves[tail].destination, "%s", destination);
|
|
||||||
state->move_count++;
|
|
||||||
pthread_mutex_unlock(&state->move_lock);
|
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void drain_moves(daemon_state *state) {
|
|
||||||
/* Copy each pending command out under the lock, then perform the relocate
|
|
||||||
* with the lock released (so the script thread can keep enqueuing). All
|
|
||||||
* relocates run on this, the owner thread, as the session requires. */
|
|
||||||
for (;;) {
|
|
||||||
move_command command;
|
|
||||||
pthread_mutex_lock(&state->move_lock);
|
|
||||||
if (state->move_count == 0) {
|
|
||||||
pthread_mutex_unlock(&state->move_lock);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
command = state->moves[state->move_head];
|
|
||||||
state->move_head = (state->move_head + 1) % MOVE_QUEUE_CAPACITY;
|
|
||||||
state->move_count--;
|
|
||||||
state->moves_processed++;
|
|
||||||
pthread_mutex_unlock(&state->move_lock);
|
|
||||||
|
|
||||||
naut_err err = naut_session_move_file(state->session,
|
|
||||||
command.torrent_id,
|
|
||||||
command.file_index,
|
|
||||||
command.destination);
|
|
||||||
if (err == NAUT_OK)
|
|
||||||
NAUT_INFO("moved torrent=%llu file=%u -> %s",
|
|
||||||
(unsigned long long)command.torrent_id,
|
|
||||||
command.file_index, command.destination);
|
|
||||||
else
|
|
||||||
NAUT_WARN("move torrent=%llu file=%u -> %s failed: %s",
|
|
||||||
(unsigned long long)command.torrent_id,
|
|
||||||
command.file_index, command.destination,
|
|
||||||
naut_strerror(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void broadcast_event(void *opaque, const naut_event *event) {
|
static void broadcast_event(void *opaque, const naut_event *event) {
|
||||||
daemon_state *state = opaque;
|
daemon_state *state = opaque;
|
||||||
json_t *payload = naut_rpc_event_json(event);
|
json_t *payload = naut_rpc_event_json(event);
|
||||||
|
|
@ -406,9 +814,68 @@ static bool register_commands(daemon_state *state) {
|
||||||
naut_rpc_register(state->rpc, "plugins", rpc_plugins, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "plugins", rpc_plugins, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "emit", rpc_emit, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "emit", rpc_emit, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "add_torrent", rpc_add_torrent, state) == NAUT_OK &&
|
naut_rpc_register(state->rpc, "add_torrent", rpc_add_torrent, state) == NAUT_OK &&
|
||||||
|
naut_rpc_register(state->rpc, "torrents", rpc_torrents, state) == NAUT_OK &&
|
||||||
|
naut_rpc_register(state->rpc, "torrent", rpc_torrent, state) == NAUT_OK &&
|
||||||
|
naut_rpc_register(state->rpc, "remove_torrent", rpc_remove_torrent, state) == NAUT_OK &&
|
||||||
|
naut_rpc_register(state->rpc, "load_script", rpc_load_script, state) == NAUT_OK &&
|
||||||
|
naut_rpc_register(state->rpc, "unload_script", rpc_unload_script, state) == NAUT_OK &&
|
||||||
naut_rpc_register(state->rpc, "shutdown", rpc_shutdown, state) == NAUT_OK;
|
naut_rpc_register(state->rpc, "shutdown", rpc_shutdown, state) == NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void stop_torrents(daemon_state *state) {
|
||||||
|
pthread_mutex_lock(&state->torrent_lock);
|
||||||
|
for (size_t i = 0; i < state->torrent_count; i++) {
|
||||||
|
torrent_task *task = state->torrents[i];
|
||||||
|
pthread_mutex_lock(&task->lock);
|
||||||
|
task->stop_requested = true;
|
||||||
|
if (task->state == TORRENT_RUNNING)
|
||||||
|
task->state = TORRENT_STOPPING;
|
||||||
|
pthread_mutex_unlock(&task->lock);
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void destroy_torrent(torrent_task *task) {
|
||||||
|
if (task->thread_started) pthread_join(task->thread, NULL);
|
||||||
|
/* uploaded torrents live in a daemon-owned temp file; remove it now that
|
||||||
|
* the worker has finished reading it */
|
||||||
|
if (task->source_is_temp && task->source) unlink(task->source);
|
||||||
|
for (size_t p = 0; p < task->num_peers; p++) free(task->peers[p]);
|
||||||
|
free(task->peers);
|
||||||
|
free(task->source);
|
||||||
|
free(task->output_dir);
|
||||||
|
pthread_mutex_destroy(&task->lock);
|
||||||
|
free(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reap_torrents(daemon_state *state) {
|
||||||
|
for (;;) {
|
||||||
|
torrent_task *task = NULL;
|
||||||
|
pthread_mutex_lock(&state->torrent_lock);
|
||||||
|
for (size_t i = 0; i < state->torrent_count; i++) {
|
||||||
|
torrent_task *candidate = state->torrents[i];
|
||||||
|
pthread_mutex_lock(&candidate->lock);
|
||||||
|
bool reap = candidate->remove_requested &&
|
||||||
|
candidate->thread_done;
|
||||||
|
pthread_mutex_unlock(&candidate->lock);
|
||||||
|
if (!reap) continue;
|
||||||
|
task = candidate;
|
||||||
|
state->torrents[i] =
|
||||||
|
state->torrents[--state->torrent_count];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&state->torrent_lock);
|
||||||
|
if (!task) return;
|
||||||
|
destroy_torrent(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void destroy_torrents(daemon_state *state) {
|
||||||
|
for (size_t i = 0; i < state->torrent_count; i++)
|
||||||
|
destroy_torrent(state->torrents[i]);
|
||||||
|
state->torrent_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void usage(const char *program) {
|
static void usage(const char *program) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"usage: %s [--socket PATH] [--plugin PATH]... [--script PATH]\n",
|
"usage: %s [--socket PATH] [--plugin PATH]... [--script PATH]\n",
|
||||||
|
|
@ -438,13 +905,13 @@ int main(int argc, char **argv) {
|
||||||
signal(SIGTERM, on_signal);
|
signal(SIGTERM, on_signal);
|
||||||
signal(SIGPIPE, SIG_IGN);
|
signal(SIGPIPE, SIG_IGN);
|
||||||
daemon_state state = {0};
|
daemon_state state = {0};
|
||||||
pthread_mutex_init(&state.move_lock, NULL);
|
state.next_torrent_id = 1;
|
||||||
|
pthread_mutex_init(&state.torrent_lock, NULL);
|
||||||
pthread_mutex_init(&state.subscriber_lock, NULL);
|
pthread_mutex_init(&state.subscriber_lock, NULL);
|
||||||
state.events = naut_event_bus_create();
|
state.events = naut_event_bus_create();
|
||||||
state.rpc = naut_rpc_registry_create();
|
state.rpc = naut_rpc_registry_create();
|
||||||
state.plugins = naut_plugin_manager_create(state.rpc, state.events);
|
state.plugins = naut_plugin_manager_create(state.rpc, state.events);
|
||||||
state.session = naut_session_create();
|
if (!state.events || !state.rpc || !state.plugins ||
|
||||||
if (!state.events || !state.rpc || !state.plugins || !state.session ||
|
|
||||||
!register_commands(&state)) {
|
!register_commands(&state)) {
|
||||||
fprintf(stderr, "nautd: failed to initialize control plane\n");
|
fprintf(stderr, "nautd: failed to initialize control plane\n");
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -485,23 +952,24 @@ int main(int argc, char **argv) {
|
||||||
} else if (ready < 0 && errno != EINTR) {
|
} else if (ready < 0 && errno != EINTR) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
drain_moves(&state);
|
reap_torrents(&state);
|
||||||
}
|
}
|
||||||
|
|
||||||
close(listener);
|
close(listener);
|
||||||
unlink(socket_path);
|
unlink(socket_path);
|
||||||
|
naut_script_destroy(state.script);
|
||||||
|
state.script = NULL;
|
||||||
|
stop_torrents(&state);
|
||||||
|
destroy_torrents(&state);
|
||||||
naut_event_unsubscribe(state.events, event_subscription);
|
naut_event_unsubscribe(state.events, event_subscription);
|
||||||
pthread_mutex_lock(&state.subscriber_lock);
|
pthread_mutex_lock(&state.subscriber_lock);
|
||||||
for (size_t i = 0; i < state.subscriber_count; i++)
|
for (size_t i = 0; i < state.subscriber_count; i++)
|
||||||
close(state.subscribers[i]);
|
close(state.subscribers[i]);
|
||||||
pthread_mutex_unlock(&state.subscriber_lock);
|
pthread_mutex_unlock(&state.subscriber_lock);
|
||||||
naut_script_destroy(state.script); /* joins the script thread */
|
|
||||||
drain_moves(&state); /* flush any moves it left queued */
|
|
||||||
naut_plugin_manager_destroy(state.plugins);
|
naut_plugin_manager_destroy(state.plugins);
|
||||||
naut_rpc_registry_destroy(state.rpc);
|
naut_rpc_registry_destroy(state.rpc);
|
||||||
naut_session_destroy(state.session);
|
|
||||||
naut_event_bus_destroy(state.events);
|
naut_event_bus_destroy(state.events);
|
||||||
pthread_mutex_destroy(&state.subscriber_lock);
|
pthread_mutex_destroy(&state.subscriber_lock);
|
||||||
pthread_mutex_destroy(&state.move_lock);
|
pthread_mutex_destroy(&state.torrent_lock);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,13 @@
|
||||||
#include "naut/log.h"
|
#include "naut/log.h"
|
||||||
#include "naut/pipeline.h"
|
#include "naut/pipeline.h"
|
||||||
#include "naut/system.h"
|
#include "naut/system.h"
|
||||||
|
#include "naut/swarm.h"
|
||||||
#include "naut/worker.h"
|
#include "naut/worker.h"
|
||||||
|
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
@ -33,6 +36,7 @@
|
||||||
#include <netinet/tcp.h>
|
#include <netinet/tcp.h>
|
||||||
|
|
||||||
#define REQUEST_TIMEOUT 15.0
|
#define REQUEST_TIMEOUT 15.0
|
||||||
|
#define CONNECT_TIMEOUT_MS 5000
|
||||||
#define EXT_RESERVED 0x0000000000100000ULL
|
#define EXT_RESERVED 0x0000000000100000ULL
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -61,6 +65,92 @@ typedef struct {
|
||||||
|
|
||||||
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec*1e-9; }
|
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec*1e-9; }
|
||||||
|
|
||||||
|
static void random_bytes(uint8_t *output, size_t length) {
|
||||||
|
int fd = open("/dev/urandom", O_RDONLY);
|
||||||
|
size_t offset = 0;
|
||||||
|
while (fd >= 0 && offset < length) {
|
||||||
|
ssize_t count = read(fd, output + offset, length - offset);
|
||||||
|
if (count > 0) {
|
||||||
|
offset += (size_t)count;
|
||||||
|
} else if (count < 0 && errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fd >= 0) close(fd);
|
||||||
|
uint64_t fallback = (uint64_t)(now() * 1e9) ^
|
||||||
|
(uint64_t)(uintptr_t)output ^
|
||||||
|
(uint64_t)getpid();
|
||||||
|
while (offset < length) {
|
||||||
|
fallback ^= fallback << 13;
|
||||||
|
fallback ^= fallback >> 7;
|
||||||
|
fallback ^= fallback << 17;
|
||||||
|
output[offset++] = (uint8_t)fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void emit_event(const naut_swarm_config *config, naut_event_type type,
|
||||||
|
uint32_t index, const char *message, const char *path) {
|
||||||
|
if (!config->events) return;
|
||||||
|
naut_event event = {
|
||||||
|
.type = type,
|
||||||
|
.torrent_id = config->torrent_id,
|
||||||
|
.index = index,
|
||||||
|
.message = message,
|
||||||
|
.path = path,
|
||||||
|
};
|
||||||
|
naut_event_emit(config->events, &event);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_file_complete(void *opaque, uint32_t index, const char *path) {
|
||||||
|
const naut_swarm_config *config = opaque;
|
||||||
|
char full_path[PATH_MAX];
|
||||||
|
const char *event_path = path;
|
||||||
|
if (path && path[0] != '/') {
|
||||||
|
int length = snprintf(full_path, sizeof full_path, "%s/%s",
|
||||||
|
config->output_dir, path);
|
||||||
|
if (length >= 0 && (size_t)length < sizeof full_path)
|
||||||
|
event_path = full_path;
|
||||||
|
}
|
||||||
|
emit_event(config, NAUT_EVENT_FILE_COMPLETE, index, NULL, event_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_piece_complete(void *opaque, uint32_t index) {
|
||||||
|
const naut_swarm_config *config = opaque;
|
||||||
|
emit_event(config, NAUT_EVENT_PIECE_COMPLETE, index, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void report_progress(const naut_swarm_config *config,
|
||||||
|
const naut_download *download,
|
||||||
|
const naut_metainfo *metainfo,
|
||||||
|
uint32_t peers_total, uint32_t peers_connecting,
|
||||||
|
uint32_t peers_active, uint32_t peers_failed,
|
||||||
|
double started_at) {
|
||||||
|
if (!config->on_progress) return;
|
||||||
|
naut_swarm_stats stats = {
|
||||||
|
.total_bytes = (uint64_t)metainfo->total_length,
|
||||||
|
.bytes_done = download ? naut_download_bytes_done(download) : 0,
|
||||||
|
.total_pieces = metainfo->num_pieces,
|
||||||
|
.pieces_done = download ? naut_download_pieces_done(download) : 0,
|
||||||
|
.peers_total = peers_total,
|
||||||
|
.peers_connecting = peers_connecting,
|
||||||
|
.peers_active = peers_active,
|
||||||
|
.peers_failed = peers_failed,
|
||||||
|
.elapsed_seconds = now() - started_at,
|
||||||
|
};
|
||||||
|
config->on_progress(config->context, &stats);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool stop_requested(const naut_swarm_config *config) {
|
||||||
|
return config->should_stop && config->should_stop(config->context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void service_control(const naut_swarm_config *config,
|
||||||
|
naut_storage *storage) {
|
||||||
|
if (config->on_control) config->on_control(config->context, storage);
|
||||||
|
}
|
||||||
|
|
||||||
static uint8_t *slurp(const char *path, size_t *len) {
|
static uint8_t *slurp(const char *path, size_t *len) {
|
||||||
FILE *f = fopen(path, "rb"); if (!f) return NULL;
|
FILE *f = fopen(path, "rb"); if (!f) return NULL;
|
||||||
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
|
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
|
||||||
|
|
@ -138,6 +228,7 @@ static bool parse_udp_tracker(const char *url, char *host, size_t hostsz,
|
||||||
|
|
||||||
static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length,
|
static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length,
|
||||||
char *const *trackers, size_t num_trackers,
|
char *const *trackers, size_t num_trackers,
|
||||||
|
const uint32_t *tracker_tiers,
|
||||||
const uint8_t peerid[20],
|
const uint8_t peerid[20],
|
||||||
endpoint_t **eps, size_t *neps, size_t *cap) {
|
endpoint_t **eps, size_t *neps, size_t *cap) {
|
||||||
naut_announce_req req;
|
naut_announce_req req;
|
||||||
|
|
@ -148,37 +239,62 @@ static bool discover_trackers(const uint8_t info_hash[20], uint64_t total_length
|
||||||
req.left = total_length;
|
req.left = total_length;
|
||||||
req.event = NAUT_TEV_STARTED;
|
req.event = NAUT_TEV_STARTED;
|
||||||
req.numwant = 100;
|
req.numwant = 100;
|
||||||
req.key = (uint32_t)rand();
|
memcpy(&req.key, peerid + 8, sizeof req.key);
|
||||||
|
|
||||||
for (size_t i = 0; i < num_trackers; i++) {
|
size_t tier_start = 0;
|
||||||
const char *tracker = trackers[i];
|
while (tier_start < num_trackers) {
|
||||||
naut_tracker_response response;
|
uint32_t tier = tracker_tiers
|
||||||
naut_err e = NAUT_ERR_INVAL;
|
? tracker_tiers[tier_start] : (uint32_t)tier_start;
|
||||||
if (strncmp(tracker, "http://", 7) == 0) {
|
size_t tier_end = tier_start + 1;
|
||||||
char url[4096];
|
if (tracker_tiers)
|
||||||
if (naut_tracker_http_url(tracker, &req, url, sizeof url) != 0)
|
while (tier_end < num_trackers &&
|
||||||
e = naut_tracker_announce_http(url, &response);
|
tracker_tiers[tier_end] == tier)
|
||||||
} else if (strncmp(tracker, "udp://", 6) == 0) {
|
tier_end++;
|
||||||
char host[256];
|
|
||||||
uint16_t port;
|
size_t tier_count = tier_end - tier_start;
|
||||||
if (parse_udp_tracker(tracker, host, sizeof host, &port))
|
uint32_t random = 0;
|
||||||
e = naut_tracker_announce_udp(host, port, &req, &response);
|
random_bytes((uint8_t *)&random, sizeof random);
|
||||||
} else {
|
size_t first = tier_count ? random % tier_count : 0;
|
||||||
NAUT_WARN("tracker scheme unsupported: %s", tracker);
|
bool tier_succeeded = false;
|
||||||
continue;
|
for (size_t n = 0; n < tier_count; n++) {
|
||||||
}
|
size_t i = tier_start + (first + n) % tier_count;
|
||||||
if (e != NAUT_OK) {
|
const char *tracker = trackers[i];
|
||||||
NAUT_WARN("tracker announce failed: %s", tracker);
|
naut_tracker_response response;
|
||||||
continue;
|
naut_err e = NAUT_ERR_INVAL;
|
||||||
}
|
if (strncmp(tracker, "http://", 7) == 0) {
|
||||||
NAUT_INFO("tracker %s returned %zu peers", tracker, response.num_peers);
|
char url[4096];
|
||||||
for (size_t p = 0; p < response.num_peers; p++) {
|
if (naut_tracker_http_url(tracker, &req, url,
|
||||||
if (!endpoint_add(eps, neps, cap, &response.peers[p])) {
|
sizeof url) != 0)
|
||||||
naut_tracker_response_free(&response);
|
e = naut_tracker_announce_http(url, &response);
|
||||||
return false;
|
} else if (strncmp(tracker, "udp://", 6) == 0) {
|
||||||
|
char host[256];
|
||||||
|
uint16_t port;
|
||||||
|
if (parse_udp_tracker(tracker, host, sizeof host, &port))
|
||||||
|
e = naut_tracker_announce_udp(host, port, &req, &response);
|
||||||
|
} else {
|
||||||
|
NAUT_WARN("tracker scheme unsupported: %s", tracker);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
if (e != NAUT_OK) {
|
||||||
|
NAUT_WARN("tracker announce failed: %s", tracker);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tier_succeeded = true;
|
||||||
|
NAUT_INFO("tracker %s returned %zu peers",
|
||||||
|
tracker, response.num_peers);
|
||||||
|
for (size_t p = 0; p < response.num_peers; p++) {
|
||||||
|
if (!endpoint_add(eps, neps, cap, &response.peers[p])) {
|
||||||
|
naut_tracker_response_free(&response);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
naut_tracker_response_free(&response);
|
||||||
|
/* Trackers within a tier are alternatives, not a fan-out set.
|
||||||
|
* Once one accepts the announce, do not load the rest. */
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
naut_tracker_response_free(&response);
|
if (tier_succeeded) break;
|
||||||
|
tier_start = tier_end;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -321,18 +437,81 @@ static void expire_requests(naut_download *d, peer_t *p, double t) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static int connect_to(const endpoint_t *ep) {
|
static int connect_start(const endpoint_t *ep, bool *connected) {
|
||||||
|
*connected = false;
|
||||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
if (fd < 0) return -1;
|
if (fd < 0) return -1;
|
||||||
|
int flags = fcntl(fd, F_GETFL, 0);
|
||||||
|
if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) != 0) {
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
struct sockaddr_in a; memset(&a, 0, sizeof a);
|
struct sockaddr_in a; memset(&a, 0, sizeof a);
|
||||||
a.sin_family = AF_INET;
|
a.sin_family = AF_INET;
|
||||||
a.sin_port = htons(ep->addr.port);
|
a.sin_port = htons(ep->addr.port);
|
||||||
memcpy(&a.sin_addr, ep->addr.ip, sizeof ep->addr.ip);
|
memcpy(&a.sin_addr, ep->addr.ip, sizeof ep->addr.ip);
|
||||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { close(fd); return -1; }
|
if (connect(fd, (struct sockaddr *)&a, sizeof a) == 0) {
|
||||||
int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
|
*connected = true;
|
||||||
|
} else if (errno != EINPROGRESS) {
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool connect_finish(int fd) {
|
||||||
|
int error = 0;
|
||||||
|
socklen_t length = sizeof error;
|
||||||
|
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &length) != 0 ||
|
||||||
|
error != 0)
|
||||||
|
return false;
|
||||||
|
int flags = fcntl(fd, F_GETFL, 0);
|
||||||
|
if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) != 0)
|
||||||
|
return false;
|
||||||
|
int one = 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool peer_start(naut_download *download, const naut_metainfo *metainfo,
|
||||||
|
const uint8_t peerid[20], const endpoint_t *endpoint,
|
||||||
|
peer_t *peer, int fd) {
|
||||||
|
peer->fd = fd;
|
||||||
|
snprintf(peer->name, sizeof peer->name, "%s", endpoint->name);
|
||||||
|
peer->peer_choking = true;
|
||||||
|
naut_pipeline_init(&peer->pipeline, NAUT_BLOCK, 4, 1024, 32);
|
||||||
|
peer->rcap = 1 << 18;
|
||||||
|
peer->rbuf = malloc(peer->rcap);
|
||||||
|
if (!peer->rbuf ||
|
||||||
|
naut_bitfield_init(&peer->have, metainfo->num_pieces) != NAUT_OK) {
|
||||||
|
free(peer->rbuf);
|
||||||
|
peer->rbuf = NULL;
|
||||||
|
close(fd);
|
||||||
|
peer->fd = -1;
|
||||||
|
peer->dead = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint8_t handshake[NAUT_HANDSHAKE_LEN];
|
||||||
|
naut_peer_handshake_build(handshake, metainfo->infohash_v1, peerid,
|
||||||
|
EXT_RESERVED);
|
||||||
|
uint8_t interested[5];
|
||||||
|
naut_peer_msg_simple(interested, NAUT_MSG_INTERESTED);
|
||||||
|
uint8_t *extension = NULL;
|
||||||
|
size_t extension_length = 0;
|
||||||
|
naut_err extension_error = naut_ext_build_handshake(
|
||||||
|
NAUT_EXT_UT_METADATA, NAUT_EXT_UT_PEX, 0, 0, &extension,
|
||||||
|
&extension_length);
|
||||||
|
bool sent = extension_error == NAUT_OK &&
|
||||||
|
send_all(fd, handshake, sizeof handshake) &&
|
||||||
|
send_all(fd, extension, extension_length) &&
|
||||||
|
send_all(fd, interested, sizeof interested);
|
||||||
|
free(extension);
|
||||||
|
if (!sent) {
|
||||||
|
peer_drop(download, peer);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/* process all complete messages currently buffered for peer p */
|
/* process all complete messages currently buffered for peer p */
|
||||||
static void cancel_block(naut_download *d, peer_t *peers, int npeers,
|
static void cancel_block(naut_download *d, peer_t *peers, int npeers,
|
||||||
peer_t *source, uint32_t piece, uint32_t begin) {
|
peer_t *source, uint32_t piece, uint32_t begin) {
|
||||||
|
|
@ -481,50 +660,46 @@ parsed:
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
naut_err naut_swarm_run(const naut_swarm_config *config) {
|
||||||
if (argc < 3) {
|
if (!config || !config->source || !*config->source ||
|
||||||
fprintf(stderr,
|
!config->output_dir || !*config->output_dir)
|
||||||
"usage: %s <file.torrent|magnet-uri> <out-dir> [ip:port ...]\n",
|
return NAUT_ERR_INVAL;
|
||||||
argv[0]);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
naut_log_set_level(NAUT_LOG_INFO);
|
|
||||||
|
|
||||||
uint8_t peerid[20]; memcpy(peerid, "-NT0001-", 8);
|
uint8_t peerid[20];
|
||||||
srand((unsigned)time(NULL) ^ (unsigned)getpid());
|
memcpy(peerid, "-NT0001-", 8);
|
||||||
for (int i = 8; i < 20; i++) peerid[i] = (uint8_t)(rand() & 0xff);
|
random_bytes(peerid + 8, sizeof peerid - 8);
|
||||||
|
|
||||||
bool from_magnet = strncmp(argv[1], "magnet:?", 8) == 0;
|
bool from_magnet = strncmp(config->source, "magnet:?", 8) == 0;
|
||||||
naut_metainfo mi;
|
naut_metainfo mi;
|
||||||
memset(&mi, 0, sizeof mi);
|
memset(&mi, 0, sizeof mi);
|
||||||
naut_magnet magnet;
|
naut_magnet magnet;
|
||||||
memset(&magnet, 0, sizeof magnet);
|
memset(&magnet, 0, sizeof magnet);
|
||||||
if (from_magnet) {
|
if (from_magnet) {
|
||||||
if (naut_magnet_parse(argv[1], &magnet) != NAUT_OK ||
|
if (naut_magnet_parse(config->source, &magnet) != NAUT_OK ||
|
||||||
!magnet.has_v1) {
|
!magnet.has_v1) {
|
||||||
NAUT_ERROR("magnet must contain a v1 btih hash");
|
NAUT_ERROR("magnet must contain a v1 btih hash");
|
||||||
naut_magnet_free(&magnet);
|
naut_magnet_free(&magnet);
|
||||||
return 1;
|
return NAUT_ERR_INVAL;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
size_t tlen;
|
size_t tlen;
|
||||||
uint8_t *tor = slurp(argv[1], &tlen);
|
uint8_t *tor = slurp(config->source, &tlen);
|
||||||
if (!tor) { NAUT_ERROR("read torrent"); return 1; }
|
if (!tor) { NAUT_ERROR("read torrent"); return NAUT_ERR_IO; }
|
||||||
if (naut_metainfo_parse(tor, tlen, &mi) != NAUT_OK) {
|
if (naut_metainfo_parse(tor, tlen, &mi) != NAUT_OK) {
|
||||||
NAUT_ERROR("parse torrent");
|
NAUT_ERROR("parse torrent");
|
||||||
free(tor);
|
free(tor);
|
||||||
return 1;
|
return NAUT_ERR_PROTO;
|
||||||
}
|
}
|
||||||
free(tor);
|
free(tor);
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint_t *endpoints = NULL;
|
endpoint_t *endpoints = NULL;
|
||||||
size_t neps = 0, epcap = 0;
|
size_t neps = 0, epcap = 0;
|
||||||
if (argc > 3) {
|
if (config->num_peers > 0) {
|
||||||
for (int i = 3; i < argc; i++) {
|
for (size_t i = 0; i < config->num_peers; i++) {
|
||||||
naut_peer_addr addr;
|
naut_peer_addr addr;
|
||||||
if (!endpoint_parse(argv[i], &addr)) {
|
if (!endpoint_parse(config->peers[i], &addr)) {
|
||||||
NAUT_WARN("invalid peer address: %s", argv[i]);
|
NAUT_WARN("invalid peer address: %s", config->peers[i]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!endpoint_add(&endpoints, &neps, &epcap, &addr)) {
|
if (!endpoint_add(&endpoints, &neps, &epcap, &addr)) {
|
||||||
|
|
@ -532,7 +707,7 @@ int main(int argc, char **argv) {
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
naut_magnet_free(&magnet);
|
naut_magnet_free(&magnet);
|
||||||
free(endpoints);
|
free(endpoints);
|
||||||
return 1;
|
return NAUT_ERR_NOMEM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -540,18 +715,21 @@ int main(int argc, char **argv) {
|
||||||
from_magnet ? magnet.infohash_v1 : mi.infohash_v1;
|
from_magnet ? magnet.infohash_v1 : mi.infohash_v1;
|
||||||
char *const *trackers =
|
char *const *trackers =
|
||||||
from_magnet ? magnet.trackers : mi.trackers;
|
from_magnet ? magnet.trackers : mi.trackers;
|
||||||
|
const uint32_t *tracker_tiers =
|
||||||
|
from_magnet ? NULL : mi.tracker_tiers;
|
||||||
size_t num_trackers =
|
size_t num_trackers =
|
||||||
from_magnet ? magnet.num_trackers : mi.num_trackers;
|
from_magnet ? magnet.num_trackers : mi.num_trackers;
|
||||||
uint64_t total = from_magnet ? 0 : (uint64_t)mi.total_length;
|
uint64_t total = from_magnet ? 0 : (uint64_t)mi.total_length;
|
||||||
if (!discover_trackers(hash, total, trackers, num_trackers, peerid,
|
if (!discover_trackers(hash, total, trackers, num_trackers,
|
||||||
&endpoints, &neps, &epcap) ||
|
tracker_tiers, peerid, &endpoints, &neps,
|
||||||
|
&epcap) ||
|
||||||
(neps == 0 &&
|
(neps == 0 &&
|
||||||
!discover_dht(hash, &endpoints, &neps, &epcap))) {
|
!discover_dht(hash, &endpoints, &neps, &epcap))) {
|
||||||
NAUT_ERROR("out of memory collecting discovered peers");
|
NAUT_ERROR("out of memory collecting discovered peers");
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
naut_magnet_free(&magnet);
|
naut_magnet_free(&magnet);
|
||||||
free(endpoints);
|
free(endpoints);
|
||||||
return 1;
|
return NAUT_ERR_NOMEM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -574,7 +752,8 @@ int main(int argc, char **argv) {
|
||||||
free(info);
|
free(info);
|
||||||
naut_magnet_free(&magnet);
|
naut_magnet_free(&magnet);
|
||||||
free(endpoints);
|
free(endpoints);
|
||||||
return 1;
|
return metadata_error != NAUT_OK ? metadata_error :
|
||||||
|
NAUT_ERR_PROTO;
|
||||||
}
|
}
|
||||||
free(info);
|
free(info);
|
||||||
NAUT_INFO("magnet metadata verified: %u pieces, %lld bytes",
|
NAUT_INFO("magnet metadata verified: %u pieces, %lld bytes",
|
||||||
|
|
@ -583,11 +762,11 @@ int main(int argc, char **argv) {
|
||||||
naut_magnet_free(&magnet);
|
naut_magnet_free(&magnet);
|
||||||
|
|
||||||
if (neps == 0) {
|
if (neps == 0) {
|
||||||
NAUT_ERROR(argc > 3 ? "no valid peer addresses" :
|
NAUT_ERROR(config->num_peers > 0 ? "no valid peer addresses" :
|
||||||
"tracker and DHT discovery returned no peers");
|
"tracker and DHT discovery returned no peers");
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
free(endpoints);
|
free(endpoints);
|
||||||
return 1;
|
return NAUT_ERR_NOTFOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
naut_err err;
|
naut_err err;
|
||||||
|
|
@ -596,20 +775,22 @@ int main(int argc, char **argv) {
|
||||||
.preallocate = true,
|
.preallocate = true,
|
||||||
};
|
};
|
||||||
naut_storage *st = naut_storage_open_opts(
|
naut_storage *st = naut_storage_open_opts(
|
||||||
mi.files, mi.num_files, argv[2], &storage_opts, &err);
|
mi.files, mi.num_files, config->output_dir, &storage_opts, &err);
|
||||||
if (!st) {
|
if (!st) {
|
||||||
NAUT_ERROR("storage: %s", naut_strerror(err));
|
NAUT_ERROR("storage: %s", naut_strerror(err));
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
free(endpoints);
|
free(endpoints);
|
||||||
return 1;
|
return err != NAUT_OK ? err : NAUT_ERR_IO;
|
||||||
}
|
}
|
||||||
naut_download *d = naut_download_create(&mi, st);
|
naut_download *d = naut_download_create(&mi, st);
|
||||||
if (!d) {
|
if (!d) {
|
||||||
naut_storage_close(st);
|
naut_storage_close(st);
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
free(endpoints);
|
free(endpoints);
|
||||||
return 1;
|
return NAUT_ERR_NOMEM;
|
||||||
}
|
}
|
||||||
|
naut_download_set_file_cb(d, on_file_complete, (void *)config);
|
||||||
|
naut_download_set_piece_cb(d, on_piece_complete, (void *)config);
|
||||||
int online_cpus = naut_online_cpus();
|
int online_cpus = naut_online_cpus();
|
||||||
uint32_t worker_count =
|
uint32_t worker_count =
|
||||||
(uint32_t)NAUT_MAX(1, NAUT_MIN(8, online_cpus / 2));
|
(uint32_t)NAUT_MAX(1, NAUT_MIN(8, online_cpus / 2));
|
||||||
|
|
@ -628,7 +809,7 @@ int main(int argc, char **argv) {
|
||||||
naut_storage_close(st);
|
naut_storage_close(st);
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
free(endpoints);
|
free(endpoints);
|
||||||
return 1;
|
return NAUT_ERR_NOMEM;
|
||||||
}
|
}
|
||||||
naut_download_set_worker_pool(d, workers);
|
naut_download_set_worker_pool(d, workers);
|
||||||
|
|
||||||
|
|
@ -644,61 +825,109 @@ int main(int argc, char **argv) {
|
||||||
naut_storage_close(st);
|
naut_storage_close(st);
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
free(endpoints);
|
free(endpoints);
|
||||||
return 1;
|
return NAUT_ERR_NOMEM;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < npeers; i++) peers[i].fd = -1;
|
for (int i = 0; i < npeers; i++) peers[i].fd = -1;
|
||||||
|
|
||||||
int active = 0;
|
double t0 = now();
|
||||||
|
naut_err run_error = NAUT_OK;
|
||||||
|
bool cancelled = false;
|
||||||
|
uint32_t connecting = 0;
|
||||||
|
uint32_t active = 0;
|
||||||
|
uint32_t failed = 0;
|
||||||
|
struct pollfd *connect_fds = pfd;
|
||||||
|
for (int i = 0; i < npeers; i++) connect_fds[i].fd = -1;
|
||||||
|
report_progress(config, d, &mi, (uint32_t)npeers, 0, 0, 0, t0);
|
||||||
for (int i = 0; i < npeers; i++) {
|
for (int i = 0; i < npeers; i++) {
|
||||||
int fd = connect_to(&endpoints[i]);
|
bool connected = false;
|
||||||
|
int fd = connect_start(&endpoints[i], &connected);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
NAUT_WARN("connect %s failed", endpoints[i].name);
|
NAUT_WARN("connect %s failed", endpoints[i].name);
|
||||||
peers[i].dead = true;
|
peers[i].dead = true;
|
||||||
|
failed++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
peer_t *p = &peers[i];
|
peers[i].fd = fd;
|
||||||
p->fd = fd;
|
if (connected) {
|
||||||
snprintf(p->name, sizeof p->name, "%s", endpoints[i].name);
|
if (!connect_finish(fd) ||
|
||||||
p->peer_choking = true;
|
!peer_start(d, &mi, peerid, &endpoints[i], &peers[i], fd)) {
|
||||||
naut_pipeline_init(&p->pipeline, NAUT_BLOCK, 4, 1024, 32);
|
NAUT_WARN("connect %s failed", endpoints[i].name);
|
||||||
p->rcap = 1 << 18;
|
if (peers[i].fd >= 0) close(peers[i].fd);
|
||||||
p->rbuf = malloc(p->rcap);
|
peers[i].fd = -1;
|
||||||
if (!p->rbuf || naut_bitfield_init(&p->have, mi.num_pieces) != NAUT_OK) {
|
peers[i].dead = true;
|
||||||
free(p->rbuf);
|
failed++;
|
||||||
p->rbuf = NULL;
|
continue;
|
||||||
close(fd);
|
}
|
||||||
p->fd = -1;
|
active++;
|
||||||
p->dead = true;
|
emit_event(config, NAUT_EVENT_PEER_CONNECTED, 0,
|
||||||
continue;
|
peers[i].name, NULL);
|
||||||
|
} else {
|
||||||
|
connect_fds[i].fd = fd;
|
||||||
|
connect_fds[i].events = POLLOUT;
|
||||||
|
connecting++;
|
||||||
}
|
}
|
||||||
uint8_t hs[NAUT_HANDSHAKE_LEN];
|
}
|
||||||
naut_peer_handshake_build(hs, mi.infohash_v1, peerid, EXT_RESERVED);
|
double connect_deadline = now() + CONNECT_TIMEOUT_MS / 1000.0;
|
||||||
uint8_t intr[5]; naut_peer_msg_simple(intr, NAUT_MSG_INTERESTED);
|
while (connecting > 0 && now() < connect_deadline &&
|
||||||
uint8_t *ext = NULL;
|
!stop_requested(config)) {
|
||||||
size_t ext_len = 0;
|
report_progress(config, d, &mi, (uint32_t)npeers, connecting,
|
||||||
naut_err ext_error = naut_ext_build_handshake(
|
active, failed, t0);
|
||||||
NAUT_EXT_UT_METADATA, NAUT_EXT_UT_PEX, 0, 0, &ext, &ext_len);
|
int ready = poll(connect_fds, (nfds_t)neps, 100);
|
||||||
bool sent = ext_error == NAUT_OK &&
|
if (ready < 0) {
|
||||||
send_all(fd, hs, sizeof hs) &&
|
if (errno == EINTR) continue;
|
||||||
send_all(fd, ext, ext_len) &&
|
run_error = NAUT_ERR_IO;
|
||||||
send_all(fd, intr, 5);
|
break;
|
||||||
free(ext);
|
|
||||||
if (!sent) {
|
|
||||||
peer_drop(d, p);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
active++;
|
if (ready == 0) continue;
|
||||||
|
for (int i = 0; i < npeers; i++) {
|
||||||
|
if (connect_fds[i].fd < 0 ||
|
||||||
|
!(connect_fds[i].revents &
|
||||||
|
(POLLOUT | POLLERR | POLLHUP | POLLNVAL)))
|
||||||
|
continue;
|
||||||
|
int fd = connect_fds[i].fd;
|
||||||
|
connect_fds[i].fd = -1;
|
||||||
|
connecting--;
|
||||||
|
if (!connect_finish(fd) ||
|
||||||
|
!peer_start(d, &mi, peerid, &endpoints[i], &peers[i], fd)) {
|
||||||
|
NAUT_WARN("connect %s failed", endpoints[i].name);
|
||||||
|
if (peers[i].fd >= 0) close(peers[i].fd);
|
||||||
|
peers[i].fd = -1;
|
||||||
|
peers[i].dead = true;
|
||||||
|
failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
active++;
|
||||||
|
emit_event(config, NAUT_EVENT_PEER_CONNECTED, 0,
|
||||||
|
peers[i].name, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < npeers; i++) {
|
||||||
|
if (connect_fds[i].fd < 0) continue;
|
||||||
|
close(connect_fds[i].fd);
|
||||||
|
connect_fds[i].fd = -1;
|
||||||
|
peers[i].fd = -1;
|
||||||
|
peers[i].dead = true;
|
||||||
|
connecting--;
|
||||||
|
failed++;
|
||||||
|
NAUT_WARN("connect %s timed out", endpoints[i].name);
|
||||||
}
|
}
|
||||||
free(endpoints);
|
free(endpoints);
|
||||||
if (!active) {
|
if (!active) {
|
||||||
NAUT_ERROR("no peers reachable");
|
NAUT_ERROR("no peers reachable");
|
||||||
|
if (run_error == NAUT_OK) run_error = NAUT_ERR_IO;
|
||||||
goto done;
|
goto done;
|
||||||
}
|
}
|
||||||
NAUT_INFO("swarm: %d peers, %u pieces, %lld bytes", active, mi.num_pieces, (long long)mi.total_length);
|
NAUT_INFO("swarm: %u/%d peers connected, %u pieces, %lld bytes",
|
||||||
|
active, npeers, mi.num_pieces, (long long)mi.total_length);
|
||||||
|
|
||||||
double t0 = now();
|
emit_event(config, NAUT_EVENT_TORRENT_ADDED, 0, NULL, NULL);
|
||||||
naut_err run_error = NAUT_OK;
|
report_progress(config, d, &mi, (uint32_t)npeers, 0, active, failed, t0);
|
||||||
while (!naut_download_complete(d) && run_error == NAUT_OK) {
|
while (!naut_download_complete(d) && run_error == NAUT_OK) {
|
||||||
|
service_control(config, st);
|
||||||
|
if (stop_requested(config)) {
|
||||||
|
cancelled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
int nf = 0;
|
int nf = 0;
|
||||||
for (int i = 0; i < npeers; i++) {
|
for (int i = 0; i < npeers; i++) {
|
||||||
if (peers[i].dead) continue;
|
if (peers[i].dead) continue;
|
||||||
|
|
@ -717,10 +946,18 @@ int main(int argc, char **argv) {
|
||||||
if (naut_download_complete(d)) break;
|
if (naut_download_complete(d)) break;
|
||||||
NAUT_ERROR("all peers gone (%.0f%% done)",
|
NAUT_ERROR("all peers gone (%.0f%% done)",
|
||||||
100.0 * naut_download_pieces_done(d) / mi.num_pieces);
|
100.0 * naut_download_pieces_done(d) / mi.num_pieces);
|
||||||
|
run_error = NAUT_ERR_IO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
report_progress(config, d, &mi, (uint32_t)npeers, 0,
|
||||||
|
(uint32_t)live_peers,
|
||||||
|
(uint32_t)npeers - (uint32_t)live_peers, t0);
|
||||||
|
int r = poll(pfd, nf, 200);
|
||||||
|
if (r < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
run_error = NAUT_ERR_IO;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
int r = poll(pfd, nf, 2000);
|
|
||||||
if (r < 0) { if (errno == EINTR) continue; break; }
|
|
||||||
|
|
||||||
for (int k = 0; k < nf; k++) {
|
for (int k = 0; k < nf; k++) {
|
||||||
if (idx_map[k] < 0) {
|
if (idx_map[k] < 0) {
|
||||||
|
|
@ -769,6 +1006,13 @@ int main(int argc, char **argv) {
|
||||||
NAUT_INFO("COMPLETE: %u/%u pieces from swarm in %.2fs (%.1f MB/s), all SHA-1 verified%s",
|
NAUT_INFO("COMPLETE: %u/%u pieces from swarm in %.2fs (%.1f MB/s), all SHA-1 verified%s",
|
||||||
naut_download_pieces_done(d), mi.num_pieces, dt, mb/dt,
|
naut_download_pieces_done(d), mi.num_pieces, dt, mb/dt,
|
||||||
naut_download_in_endgame(d) ? " (passed through endgame)" : "");
|
naut_download_in_endgame(d) ? " (passed through endgame)" : "");
|
||||||
|
report_progress(config, d, &mi, (uint32_t)npeers, 0, 0,
|
||||||
|
(uint32_t)npeers, t0);
|
||||||
|
emit_event(config, NAUT_EVENT_TORRENT_FINISHED, 0, NULL, NULL);
|
||||||
|
while (config->keep_alive && !stop_requested(config)) {
|
||||||
|
service_control(config, st);
|
||||||
|
usleep(100000);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (run_error != NAUT_OK)
|
if (run_error != NAUT_OK)
|
||||||
NAUT_ERROR("swarm stopped: %s", naut_strerror(run_error));
|
NAUT_ERROR("swarm stopped: %s", naut_strerror(run_error));
|
||||||
|
|
@ -777,6 +1021,7 @@ int main(int argc, char **argv) {
|
||||||
|
|
||||||
done:
|
done:
|
||||||
ok = naut_download_complete(d);
|
ok = naut_download_complete(d);
|
||||||
|
service_control(config, st);
|
||||||
naut_storage_sync(st);
|
naut_storage_sync(st);
|
||||||
for (int i = 0; i < npeers; i++) {
|
for (int i = 0; i < npeers; i++) {
|
||||||
if (peers[i].blocks_received)
|
if (peers[i].blocks_received)
|
||||||
|
|
@ -797,5 +1042,26 @@ done:
|
||||||
free(peers); free(pfd); free(idx_map);
|
free(peers); free(pfd); free(idx_map);
|
||||||
naut_worker_pool_destroy(workers);
|
naut_worker_pool_destroy(workers);
|
||||||
naut_download_destroy(d); naut_storage_close(st); naut_metainfo_free(&mi);
|
naut_download_destroy(d); naut_storage_close(st); naut_metainfo_free(&mi);
|
||||||
return ok ? 0 : 1;
|
if (ok) return NAUT_OK;
|
||||||
|
if (cancelled) return NAUT_ERR_AGAIN;
|
||||||
|
return run_error != NAUT_OK ? run_error : NAUT_ERR_IO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef NAUT_SWARM_LIBRARY
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 3) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"usage: %s <file.torrent|magnet-uri> <out-dir> [ip:port ...]\n",
|
||||||
|
argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
naut_log_set_level(NAUT_LOG_INFO);
|
||||||
|
naut_swarm_config config = {
|
||||||
|
.source = argv[1],
|
||||||
|
.output_dir = argv[2],
|
||||||
|
.peers = argc > 3 ? (const char *const *)&argv[3] : NULL,
|
||||||
|
.num_peers = argc > 3 ? (size_t)(argc - 3) : 0,
|
||||||
|
};
|
||||||
|
return naut_swarm_run(&config) == NAUT_OK ? 0 : 1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -16,18 +16,19 @@ them, and the `naut` API table.
|
||||||
|
|
||||||
## 1. Loading a script
|
## 1. Loading a script
|
||||||
|
|
||||||
Pass a script to the daemon with `--script`:
|
Load a script through the control API:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nautd --socket /tmp/nautd.sock --script ./my-rules.lua
|
nautctl --socket /tmp/nautd.sock script ./my-rules.lua
|
||||||
```
|
```
|
||||||
|
|
||||||
- **One script per daemon.** If `--script` is given more than once, the last
|
- **One active script per daemon.** Loading another script replaces the current
|
||||||
path wins.
|
one. `nautctl unscript` unloads it. `nautd --script PATH` is also available
|
||||||
- The file is **loaded and executed once at startup**, on the main thread,
|
for startup configuration.
|
||||||
before the event worker starts. Use this top-level run to define your hook
|
- The file is **loaded and executed once**, synchronously on the control thread,
|
||||||
functions (and any state they need). If the file fails to load or its
|
before its event worker starts. Use this top-level run to define your hook
|
||||||
top-level code raises, the daemon refuses to start and prints the Lua error.
|
functions (and any state they need). A load or top-level error rejects the
|
||||||
|
request (or prevents startup when `--script` is used).
|
||||||
- After startup the script is **event-driven**: the functions you defined are
|
- After startup the script is **event-driven**: the functions you defined are
|
||||||
called as matching events occur.
|
called as matching events occur.
|
||||||
|
|
||||||
|
|
@ -161,9 +162,9 @@ Move one **completed** file of a torrent to `destination` (the headline
|
||||||
**Return value:** none on success.
|
**Return value:** none on success.
|
||||||
|
|
||||||
**Asynchronous semantics — important.** `move_file` does **not** perform the
|
**Asynchronous semantics — important.** `move_file` does **not** perform the
|
||||||
move inline on the script thread. It enqueues a bounded command that the daemon
|
move inline on the script thread. It enqueues a bounded command that the
|
||||||
**owner thread** executes shortly after (this preserves the engine's
|
torrent's **owner thread** executes shortly after (the script thread never
|
||||||
shared-nothing threading: the script thread never touches storage directly).
|
touches storage directly).
|
||||||
So a successful call means *"the move was accepted"*, not *"the file has moved."*
|
So a successful call means *"the move was accepted"*, not *"the file has moved."*
|
||||||
The actual relocate (a `rename`, or copy+unlink across filesystems) runs later;
|
The actual relocate (a `rename`, or copy+unlink across filesystems) runs later;
|
||||||
its success or failure is reported in the **daemon log** and reflected in the
|
its success or failure is reported in the **daemon log** and reflected in the
|
||||||
|
|
@ -189,19 +190,15 @@ function on_file_complete(event)
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
**Prerequisite — register the torrent's storage first.** `move_file` resolves
|
**Prerequisite — the torrent must be loaded.** Add the torrent to the daemon;
|
||||||
`torrent_id` through the daemon's torrent registry (`naut_session`). Register a
|
the same worker that downloads it owns and executes its move commands:
|
||||||
torrent's storage with the `add_torrent` RPC before any move command can take
|
|
||||||
effect:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nautctl --socket /tmp/nautd.sock add_torrent \
|
nautctl --socket /tmp/nautd.sock add file.torrent /downloads/42
|
||||||
'{"torrent_id":42,"torrent":"file.torrent","root":"/downloads/42"}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If `torrent_id` is unknown when the move drains, the relocate fails with
|
If `torrent_id` is unknown or is being removed, `naut.move_file` raises a
|
||||||
"not found" in the daemon log (the Lua call itself still succeeded, because it
|
`move_file failed` error in the hook.
|
||||||
only *queued* the command).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -283,7 +280,7 @@ end
|
||||||
Run it:
|
Run it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nautd --socket /tmp/nautd.sock --script ./archive-on-finish.lua &
|
nautd --socket /tmp/nautd.sock &
|
||||||
nautctl --socket /tmp/nautd.sock add_torrent \
|
nautctl --socket /tmp/nautd.sock script ./archive-on-finish.lua
|
||||||
'{"torrent_id":1,"torrent":"big.torrent","root":"/downloads/1"}'
|
nautctl --socket /tmp/nautd.sock add big.torrent /downloads/1
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,10 @@ Movies (no detectable episode) go to `<SORTED_ROOT>/<Title>/<Title> (year).<ext>
|
||||||
### Use it
|
### Use it
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# 1. edit SORTED_ROOT (and options) at the top of the script
|
# Start the engine, load the script, and add a download.
|
||||||
# 2. start the daemon with the script
|
nautd --socket /tmp/nautd.sock
|
||||||
nautd --socket /tmp/nautd.sock --script examples/anime_sort.lua
|
nautctl --socket /tmp/nautd.sock script examples/anime_sort.lua
|
||||||
|
nautctl --socket /tmp/nautd.sock add show.torrent /downloads/1
|
||||||
# 3. register each torrent's storage so the move can resolve + relocate its files
|
|
||||||
nautctl --socket /tmp/nautd.sock add_torrent \
|
|
||||||
'{"torrent_id":1,"torrent":"show.torrent","root":"/downloads/1"}'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
As each file completes you'll see, e.g.:
|
As each file completes you'll see, e.g.:
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@
|
||||||
-- without waiting for the rest of the torrent.
|
-- without waiting for the rest of the torrent.
|
||||||
--
|
--
|
||||||
-- Install:
|
-- Install:
|
||||||
-- nautd --socket /tmp/nautd.sock --script examples/anime_sort.lua
|
-- nautd --socket /tmp/nautd.sock
|
||||||
-- nautctl --socket /tmp/nautd.sock add_torrent \
|
-- nautctl --socket /tmp/nautd.sock script examples/anime_sort.lua
|
||||||
-- '{"torrent_id":1,"torrent":"show.torrent","root":"/downloads/1"}'
|
-- nautctl --socket /tmp/nautd.sock add show.torrent /downloads/1
|
||||||
--
|
--
|
||||||
-- The parser below is a VERBATIM COPY of examples/anitomy.lua (the sandbox has
|
-- The parser below is a VERBATIM COPY of examples/anitomy.lua (the sandbox has
|
||||||
-- no `require`, so it must be inlined). Keep the two in sync; examples/
|
-- no `require`, so it must be inlined). Keep the two in sync; examples/
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
-- CONFIG — edit these
|
-- CONFIG — edit these
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
local SORTED_ROOT = "/sorted" -- destination library root
|
local SORTED_ROOT = "/workspaces/source/ai-garbo/Naut-Torrent/sorted" -- destination library root
|
||||||
local ONLY_VIDEO = true -- skip non-video files (subs, nfo, samples)
|
local ONLY_VIDEO = true -- skip non-video files (subs, nfo, samples)
|
||||||
local KEEP_ORIGINAL_NAME = false -- true: keep the original filename;
|
local KEEP_ORIGINAL_NAME = false -- true: keep the original filename;
|
||||||
-- false: rename to "Title - SNNENN.ext"
|
-- false: rename to "Title - SNNENN.ext"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ dofile(dir .. "anime_sort.lua") -- defines on_file_complete + _G.anime_sort
|
||||||
|
|
||||||
_G.print = realprint
|
_G.print = realprint
|
||||||
|
|
||||||
|
local configured_root = _G.anime_sort.destination({
|
||||||
|
title = "__ROOT_PROBE__",
|
||||||
|
file_name = "__ROOT_PROBE__.mkv",
|
||||||
|
extension = "mkv",
|
||||||
|
}):match("^(.*)/__ROOT_PROBE__/__ROOT_PROBE__%.mkv$")
|
||||||
|
assert(configured_root, "could not derive SORTED_ROOT from anime_sort.lua")
|
||||||
|
|
||||||
local fails = 0
|
local fails = 0
|
||||||
local function fire(path)
|
local function fire(path)
|
||||||
captured = nil
|
captured = nil
|
||||||
|
|
@ -35,21 +42,21 @@ end
|
||||||
|
|
||||||
-- episodes
|
-- episodes
|
||||||
expect("/downloads/1/[HorribleSubs] Boku no Hero Academia - 12 [1080p].mkv",
|
expect("/downloads/1/[HorribleSubs] Boku no Hero Academia - 12 [1080p].mkv",
|
||||||
"/sorted/Boku no Hero Academia/Season 01/Boku no Hero Academia - S01E12.mkv")
|
configured_root .. "/Boku no Hero Academia/Season 01/Boku no Hero Academia - S01E12.mkv")
|
||||||
expect("/downloads/2/[Group] Attack on Titan S04E28 [BD 1080p HEVC].mkv",
|
expect("/downloads/2/[Group] Attack on Titan S04E28 [BD 1080p HEVC].mkv",
|
||||||
"/sorted/Attack on Titan/Season 04/Attack on Titan - S04E28.mkv")
|
configured_root .. "/Attack on Titan/Season 04/Attack on Titan - S04E28.mkv")
|
||||||
expect("/dl/[Commie] Steins;Gate 0 - 12 [BD 1080p].mkv",
|
expect("/dl/[Commie] Steins;Gate 0 - 12 [BD 1080p].mkv",
|
||||||
"/sorted/Steins;Gate 0/Season 01/Steins;Gate 0 - S01E12.mkv")
|
configured_root .. "/Steins;Gate 0/Season 01/Steins;Gate 0 - S01E12.mkv")
|
||||||
expect("Demon.Slayer.Kimetsu.no.Yaiba.S03E11.1080p.mkv",
|
expect("Demon.Slayer.Kimetsu.no.Yaiba.S03E11.1080p.mkv",
|
||||||
"/sorted/Demon Slayer Kimetsu no Yaiba/Season 03/Demon Slayer Kimetsu no Yaiba - S03E11.mkv")
|
configured_root .. "/Demon Slayer Kimetsu no Yaiba/Season 03/Demon Slayer Kimetsu no Yaiba - S03E11.mkv")
|
||||||
expect("/x/[Erai-raws] Jujutsu Kaisen 2nd Season - 17 [1080p].mkv",
|
expect("/x/[Erai-raws] Jujutsu Kaisen 2nd Season - 17 [1080p].mkv",
|
||||||
"/sorted/Jujutsu Kaisen/Season 02/Jujutsu Kaisen - S02E17.mkv")
|
configured_root .. "/Jujutsu Kaisen/Season 02/Jujutsu Kaisen - S02E17.mkv")
|
||||||
|
|
||||||
-- movies (no episode)
|
-- movies (no episode)
|
||||||
expect("/m/[Group] A Silent Voice [BD 1080p FLAC].mkv",
|
expect("/m/[Group] A Silent Voice [BD 1080p FLAC].mkv",
|
||||||
"/sorted/A Silent Voice/A Silent Voice.mkv")
|
configured_root .. "/A Silent Voice/A Silent Voice.mkv")
|
||||||
expect("/m/Spirited.Away.2001.1080p.BluRay.x264.mkv",
|
expect("/m/Spirited.Away.2001.1080p.BluRay.x264.mkv",
|
||||||
"/sorted/Spirited Away/Spirited Away (2001).mkv")
|
configured_root .. "/Spirited Away/Spirited Away (2001).mkv")
|
||||||
|
|
||||||
-- non-video file must be skipped (no move queued)
|
-- non-video file must be skipped (no move queued)
|
||||||
do
|
do
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@ typedef struct naut_metainfo {
|
||||||
const uint8_t *piece_hashes;
|
const uint8_t *piece_hashes;
|
||||||
|
|
||||||
naut_file *files; size_t num_files;
|
naut_file *files; size_t num_files;
|
||||||
char **trackers; size_t num_trackers; /* announce + announce-list, flattened */
|
char **trackers; size_t num_trackers;
|
||||||
|
/* Parallel to trackers. Equal values belong to one BEP-12 tier. */
|
||||||
|
uint32_t *tracker_tiers;
|
||||||
|
|
||||||
/* internals kept alive so piece_hashes/name stay valid */
|
/* internals kept alive so piece_hashes/name stay valid */
|
||||||
void *_owned;
|
void *_owned;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ typedef naut_err (*naut_plugin_rpc_fn)(void *context,
|
||||||
char **response_json);
|
char **response_json);
|
||||||
typedef void (*naut_plugin_event_fn)(void *context,
|
typedef void (*naut_plugin_event_fn)(void *context,
|
||||||
const naut_event *event);
|
const naut_event *event);
|
||||||
|
typedef naut_err (*naut_plugin_shutdown_fn)(void);
|
||||||
|
|
||||||
typedef struct naut_host_api {
|
typedef struct naut_host_api {
|
||||||
uint32_t abi_version;
|
uint32_t abi_version;
|
||||||
|
|
@ -41,6 +42,8 @@ typedef struct naut_host_api {
|
||||||
void *context);
|
void *context);
|
||||||
void (*emit_event)(void *host_context, const naut_event *event);
|
void (*emit_event)(void *host_context, const naut_event *event);
|
||||||
void (*log)(void *host_context, int level, const char *message);
|
void (*log)(void *host_context, int level, const char *message);
|
||||||
|
naut_err (*call_rpc)(void *host_context, const char *method,
|
||||||
|
const char *request_json, char **response_json);
|
||||||
} naut_host_api;
|
} naut_host_api;
|
||||||
|
|
||||||
/* Every plugin exports this exact symbol. */
|
/* Every plugin exports this exact symbol. */
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,11 @@ typedef void (*naut_file_complete_cb)(void *ctx, uint32_t file_index, const char
|
||||||
void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx);
|
void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx);
|
||||||
bool naut_download_file_complete(const naut_download *d, uint32_t file_index);
|
bool naut_download_file_complete(const naut_download *d, uint32_t file_index);
|
||||||
|
|
||||||
|
/* Optional owner-thread notification after a piece verifies and is persisted. */
|
||||||
|
typedef void (*naut_piece_complete_cb)(void *ctx, uint32_t piece_index);
|
||||||
|
void naut_download_set_piece_cb(naut_download *d, naut_piece_complete_cb cb,
|
||||||
|
void *ctx);
|
||||||
|
|
||||||
/* Hand out the next block to request. false => nothing left to hand out right
|
/* Hand out the next block to request. false => nothing left to hand out right
|
||||||
* now (all blocks have been requested). */
|
* now (all blocks have been requested). */
|
||||||
bool naut_download_next_request(naut_download *d,
|
bool naut_download_next_request(naut_download *d,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
#include <jansson.h>
|
#include <jansson.h>
|
||||||
|
|
||||||
#define NAUT_RPC_VERSION 1
|
#define NAUT_RPC_VERSION 1
|
||||||
#define NAUT_RPC_MAX_PAYLOAD (1u << 20)
|
/* Generous enough to carry a base64-encoded .torrent upload in add_torrent. */
|
||||||
|
#define NAUT_RPC_MAX_PAYLOAD (8u << 20)
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
NAUT_RPC_REQUEST = 1,
|
NAUT_RPC_REQUEST = 1,
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,12 @@ naut_err naut_storage_write(naut_storage *s, int64_t offset, const void *buf, si
|
||||||
naut_err naut_storage_read(naut_storage *s, int64_t offset, void *buf, size_t len);
|
naut_err naut_storage_read(naut_storage *s, int64_t offset, void *buf, size_t len);
|
||||||
naut_err naut_storage_sync(naut_storage *s);
|
naut_err naut_storage_sync(naut_storage *s);
|
||||||
|
|
||||||
/* Move one completed file out to `dest` (rename, or copy+unlink across file
|
/* Move one file to `dest` (rename, or copy+unlink across file systems) and keep
|
||||||
* systems). The caller must guarantee the file is complete — every piece
|
* tracking it there: the slot's path is updated and its fd reopened, so the
|
||||||
* overlapping it verified — so no further writes target it. After this the slot
|
* engine can still read/write/seed the file at its new location. Typically
|
||||||
* is "externalized": subsequent I/O to its region returns NAUT_ERR_RANGE. This
|
* called the moment a file completes (the storage half of "move files as they
|
||||||
* is the storage half of the "move files as they finish" feature. */
|
* finish"), but safe at any time. The owning process therefore never loses
|
||||||
|
* track of a moved file. Returns NAUT_ERR_IO if the move or reopen fails. */
|
||||||
naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest);
|
naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest);
|
||||||
|
|
||||||
int64_t naut_storage_total(const naut_storage *s);
|
int64_t naut_storage_total(const naut_storage *s);
|
||||||
|
|
|
||||||
52
include/naut/swarm.h
Normal file
52
include/naut/swarm.h
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
/* swarm.h - reusable multi-peer download driver.
|
||||||
|
*
|
||||||
|
* naut_swarm is a small CLI wrapper around this API. Long-running applications
|
||||||
|
* such as nautd own the worker thread and use callbacks for progress, control
|
||||||
|
* commands, cancellation, and event delivery.
|
||||||
|
*/
|
||||||
|
#ifndef NAUT_SWARM_H
|
||||||
|
#define NAUT_SWARM_H
|
||||||
|
|
||||||
|
#include "naut/common.h"
|
||||||
|
#include "naut/event.h"
|
||||||
|
#include "naut/storage.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint64_t total_bytes;
|
||||||
|
uint64_t bytes_done;
|
||||||
|
uint32_t total_pieces;
|
||||||
|
uint32_t pieces_done;
|
||||||
|
uint32_t peers_total; /* discovered endpoints */
|
||||||
|
uint32_t peers_connecting;
|
||||||
|
uint32_t peers_active;
|
||||||
|
uint32_t peers_failed;
|
||||||
|
double elapsed_seconds;
|
||||||
|
} naut_swarm_stats;
|
||||||
|
|
||||||
|
typedef void (*naut_swarm_progress_cb)(void *context,
|
||||||
|
const naut_swarm_stats *stats);
|
||||||
|
|
||||||
|
/* Called on the swarm owner thread. The callback may safely operate on storage,
|
||||||
|
* including relocating completed files. */
|
||||||
|
typedef void (*naut_swarm_control_cb)(void *context, naut_storage *storage);
|
||||||
|
|
||||||
|
typedef bool (*naut_swarm_stop_cb)(void *context);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *source; /* .torrent path or magnet URI */
|
||||||
|
const char *output_dir;
|
||||||
|
const char *const *peers; /* optional explicit ip:port endpoints */
|
||||||
|
size_t num_peers;
|
||||||
|
uint64_t torrent_id;
|
||||||
|
naut_event_bus *events; /* optional */
|
||||||
|
bool keep_alive; /* retain completed storage until stopped */
|
||||||
|
naut_swarm_progress_cb on_progress;
|
||||||
|
naut_swarm_control_cb on_control;
|
||||||
|
naut_swarm_stop_cb should_stop;
|
||||||
|
void *context;
|
||||||
|
} naut_swarm_config;
|
||||||
|
|
||||||
|
/* Blocks until the torrent completes, is cancelled, or fails. */
|
||||||
|
naut_err naut_swarm_run(const naut_swarm_config *config);
|
||||||
|
|
||||||
|
#endif /* NAUT_SWARM_H */
|
||||||
1117
plugins/webui/webui.c
Normal file
1117
plugins/webui/webui.c
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -20,9 +20,37 @@ static char *dup_cstr(const uint8_t *p, size_t n) {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* collect a single announce string or an announce-list (list of tiers) */
|
static naut_err add_tracker(naut_metainfo *mi, size_t *capacity,
|
||||||
static void collect_trackers(const naut_bc *root, naut_metainfo *mi) {
|
const uint8_t *url, size_t url_len,
|
||||||
size_t cap = 0;
|
uint32_t tier) {
|
||||||
|
for (size_t i = 0; i < mi->num_trackers; i++)
|
||||||
|
if (strlen(mi->trackers[i]) == url_len &&
|
||||||
|
memcmp(mi->trackers[i], url, url_len) == 0)
|
||||||
|
return NAUT_OK;
|
||||||
|
if (mi->num_trackers == *capacity) {
|
||||||
|
size_t next_capacity = *capacity ? *capacity * 2 : 8;
|
||||||
|
char **next_trackers =
|
||||||
|
realloc(mi->trackers, next_capacity * sizeof(*next_trackers));
|
||||||
|
if (!next_trackers) return NAUT_ERR_NOMEM;
|
||||||
|
mi->trackers = next_trackers;
|
||||||
|
uint32_t *next_tiers =
|
||||||
|
realloc(mi->tracker_tiers,
|
||||||
|
next_capacity * sizeof(*next_tiers));
|
||||||
|
if (!next_tiers) return NAUT_ERR_NOMEM;
|
||||||
|
mi->tracker_tiers = next_tiers;
|
||||||
|
*capacity = next_capacity;
|
||||||
|
}
|
||||||
|
char *copy = dup_cstr(url, url_len);
|
||||||
|
if (!copy) return NAUT_ERR_NOMEM;
|
||||||
|
mi->trackers[mi->num_trackers] = copy;
|
||||||
|
mi->tracker_tiers[mi->num_trackers] = tier;
|
||||||
|
mi->num_trackers++;
|
||||||
|
return NAUT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preserve the outer announce-list as BEP-12 failover tiers. */
|
||||||
|
static naut_err collect_trackers(const naut_bc *root, naut_metainfo *mi) {
|
||||||
|
size_t capacity = 0;
|
||||||
const naut_bc *al = naut_bc_dict_get(root, "announce-list");
|
const naut_bc *al = naut_bc_dict_get(root, "announce-list");
|
||||||
if (al && al->type == NAUT_BC_LIST) {
|
if (al && al->type == NAUT_BC_LIST) {
|
||||||
for (size_t t = 0; t < al->v.list.count; t++) {
|
for (size_t t = 0; t < al->v.list.count; t++) {
|
||||||
|
|
@ -32,21 +60,20 @@ static void collect_trackers(const naut_bc *root, naut_metainfo *mi) {
|
||||||
const naut_bc *url = naut_bc_list_at(tier, u);
|
const naut_bc *url = naut_bc_list_at(tier, u);
|
||||||
const uint8_t *p; size_t n;
|
const uint8_t *p; size_t n;
|
||||||
if (!naut_bc_get_str(url, &p, &n)) continue;
|
if (!naut_bc_get_str(url, &p, &n)) continue;
|
||||||
if (mi->num_trackers == cap) {
|
naut_err error =
|
||||||
cap = cap ? cap * 2 : 8;
|
add_tracker(mi, &capacity, p, n, (uint32_t)t);
|
||||||
mi->trackers = realloc(mi->trackers, cap * sizeof(char *));
|
if (error != NAUT_OK) return error;
|
||||||
}
|
|
||||||
mi->trackers[mi->num_trackers++] = dup_cstr(p, n);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mi->num_trackers == 0) {
|
if (mi->num_trackers == 0) {
|
||||||
const uint8_t *p; size_t n;
|
const uint8_t *p; size_t n;
|
||||||
if (naut_bc_get_str(naut_bc_dict_get(root, "announce"), &p, &n)) {
|
if (naut_bc_get_str(naut_bc_dict_get(root, "announce"), &p, &n)) {
|
||||||
mi->trackers = malloc(sizeof(char *));
|
naut_err error = add_tracker(mi, &capacity, p, n, 0);
|
||||||
mi->trackers[mi->num_trackers++] = dup_cstr(p, n);
|
if (error != NAUT_OK) return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* v1 file list: single-file (info.length) or multi-file (info.files[]) */
|
/* v1 file list: single-file (info.length) or multi-file (info.files[]) */
|
||||||
|
|
@ -220,7 +247,12 @@ naut_err naut_metainfo_parse(const uint8_t *data, size_t len, naut_metainfo *out
|
||||||
} else {
|
} else {
|
||||||
collect_files_v2(info, out); /* v2-only: walk the file tree */
|
collect_files_v2(info, out); /* v2-only: walk the file tree */
|
||||||
}
|
}
|
||||||
collect_trackers(root, out);
|
e = collect_trackers(root, out);
|
||||||
|
if (e != NAUT_OK) {
|
||||||
|
out->_owned = o;
|
||||||
|
naut_metainfo_free(out);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
out->_owned = o;
|
out->_owned = o;
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
|
|
@ -245,7 +277,9 @@ naut_err naut_metainfo_parse_info(const uint8_t *info, size_t info_len,
|
||||||
|
|
||||||
if (num_trackers) {
|
if (num_trackers) {
|
||||||
out->trackers = calloc(num_trackers, sizeof(*out->trackers));
|
out->trackers = calloc(num_trackers, sizeof(*out->trackers));
|
||||||
if (!out->trackers) {
|
out->tracker_tiers =
|
||||||
|
calloc(num_trackers, sizeof(*out->tracker_tiers));
|
||||||
|
if (!out->trackers || !out->tracker_tiers) {
|
||||||
naut_metainfo_free(out);
|
naut_metainfo_free(out);
|
||||||
return NAUT_ERR_NOMEM;
|
return NAUT_ERR_NOMEM;
|
||||||
}
|
}
|
||||||
|
|
@ -257,6 +291,9 @@ naut_err naut_metainfo_parse_info(const uint8_t *info, size_t info_len,
|
||||||
naut_metainfo_free(out);
|
naut_metainfo_free(out);
|
||||||
return NAUT_ERR_NOMEM;
|
return NAUT_ERR_NOMEM;
|
||||||
}
|
}
|
||||||
|
/* Magnet tr= parameters have no tier metadata. Treat them as
|
||||||
|
* ordered failover entries instead of announcing to all at once. */
|
||||||
|
out->tracker_tiers[i] = (uint32_t)i;
|
||||||
}
|
}
|
||||||
out->num_trackers = num_trackers;
|
out->num_trackers = num_trackers;
|
||||||
}
|
}
|
||||||
|
|
@ -270,6 +307,7 @@ void naut_metainfo_free(naut_metainfo *mi) {
|
||||||
free(mi->files);
|
free(mi->files);
|
||||||
for (size_t i = 0; i < mi->num_trackers; i++) free(mi->trackers[i]);
|
for (size_t i = 0; i < mi->num_trackers; i++) free(mi->trackers[i]);
|
||||||
free(mi->trackers);
|
free(mi->trackers);
|
||||||
|
free(mi->tracker_tiers);
|
||||||
if (mi->_owned) {
|
if (mi->_owned) {
|
||||||
owned *o = mi->_owned;
|
owned *o = mi->_owned;
|
||||||
naut_bc_free(o->doc);
|
naut_bc_free(o->doc);
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,8 @@ struct naut_download {
|
||||||
bool *file_done;
|
bool *file_done;
|
||||||
naut_file_complete_cb file_cb;
|
naut_file_complete_cb file_cb;
|
||||||
void *file_cb_ctx;
|
void *file_cb_ctx;
|
||||||
|
naut_piece_complete_cb piece_cb;
|
||||||
|
void *piece_cb_ctx;
|
||||||
};
|
};
|
||||||
|
|
||||||
static bool bget(const uint8_t *a, uint32_t i) { return (a[i>>3] >> (i&7)) & 1; }
|
static bool bget(const uint8_t *a, uint32_t i) { return (a[i>>3] >> (i&7)) & 1; }
|
||||||
|
|
@ -157,6 +159,11 @@ void naut_download_set_worker_pool(naut_download *d, naut_worker_pool *pool) {
|
||||||
void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx) {
|
void naut_download_set_file_cb(naut_download *d, naut_file_complete_cb cb, void *ctx) {
|
||||||
d->file_cb = cb; d->file_cb_ctx = ctx;
|
d->file_cb = cb; d->file_cb_ctx = ctx;
|
||||||
}
|
}
|
||||||
|
void naut_download_set_piece_cb(naut_download *d, naut_piece_complete_cb cb,
|
||||||
|
void *ctx) {
|
||||||
|
d->piece_cb = cb;
|
||||||
|
d->piece_cb_ctx = ctx;
|
||||||
|
}
|
||||||
bool naut_download_file_complete(const naut_download *d, uint32_t f) {
|
bool naut_download_file_complete(const naut_download *d, uint32_t f) {
|
||||||
return f < d->num_files && d->file_done[f];
|
return f < d->num_files && d->file_done[f];
|
||||||
}
|
}
|
||||||
|
|
@ -300,6 +307,7 @@ static naut_err finish_verified(naut_download *d, uint32_t p,
|
||||||
d->bytes_done += ps;
|
d->bytes_done += ps;
|
||||||
free_ps(d, p);
|
free_ps(d, p);
|
||||||
*done = true;
|
*done = true;
|
||||||
|
if (d->piece_cb) d->piece_cb(d->piece_cb_ctx, p);
|
||||||
notify_files(d, p);
|
notify_files(d, p);
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ typedef struct {
|
||||||
void *handle;
|
void *handle;
|
||||||
char *path;
|
char *path;
|
||||||
char *name;
|
char *name;
|
||||||
|
naut_plugin_shutdown_fn shutdown;
|
||||||
uint64_t *subscriptions;
|
uint64_t *subscriptions;
|
||||||
size_t subscription_count;
|
size_t subscription_count;
|
||||||
size_t subscription_capacity;
|
size_t subscription_capacity;
|
||||||
|
|
@ -209,6 +210,36 @@ static void host_log(void *opaque, int level, const char *message) {
|
||||||
else NAUT_INFO("plugin: %s", message);
|
else NAUT_INFO("plugin: %s", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static naut_err host_call_rpc(void *opaque, const char *method,
|
||||||
|
const char *request_json,
|
||||||
|
char **response_json) {
|
||||||
|
naut_plugin_manager *manager = opaque;
|
||||||
|
if (!manager || !method || !*method || !response_json)
|
||||||
|
return NAUT_ERR_INVAL;
|
||||||
|
*response_json = NULL;
|
||||||
|
json_t *params = NULL;
|
||||||
|
if (request_json && *request_json) {
|
||||||
|
json_error_t json_error;
|
||||||
|
params = json_loads(request_json,
|
||||||
|
JSON_REJECT_DUPLICATES | JSON_DECODE_ANY,
|
||||||
|
&json_error);
|
||||||
|
if (!params) return NAUT_ERR_PROTO;
|
||||||
|
}
|
||||||
|
naut_err error = NAUT_OK;
|
||||||
|
json_t *result = naut_rpc_dispatch(manager->rpc, method, params, &error);
|
||||||
|
json_decref(params);
|
||||||
|
if (error != NAUT_OK) {
|
||||||
|
json_decref(result);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
char *text = json_dumps(result ? result : json_null(),
|
||||||
|
JSON_COMPACT | JSON_ENCODE_ANY);
|
||||||
|
json_decref(result);
|
||||||
|
if (!text) return NAUT_ERR_NOMEM;
|
||||||
|
*response_json = text;
|
||||||
|
return NAUT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
naut_plugin_manager *naut_plugin_manager_create(
|
naut_plugin_manager *naut_plugin_manager_create(
|
||||||
naut_rpc_registry *rpc, naut_event_bus *events) {
|
naut_rpc_registry *rpc, naut_event_bus *events) {
|
||||||
if (!rpc || !events) return NULL;
|
if (!rpc || !events) return NULL;
|
||||||
|
|
@ -225,6 +256,7 @@ void naut_plugin_manager_destroy(naut_plugin_manager *manager) {
|
||||||
naut_rpc_unregister(manager->rpc, manager->rpc_adapters[i]->method);
|
naut_rpc_unregister(manager->rpc, manager->rpc_adapters[i]->method);
|
||||||
for (size_t i = 0; i < manager->plugin_count; i++) {
|
for (size_t i = 0; i < manager->plugin_count; i++) {
|
||||||
loaded_plugin *plugin = &manager->plugins[i];
|
loaded_plugin *plugin = &manager->plugins[i];
|
||||||
|
if (plugin->shutdown) plugin->shutdown();
|
||||||
for (size_t s = 0; s < plugin->subscription_count; s++)
|
for (size_t s = 0; s < plugin->subscription_count; s++)
|
||||||
naut_event_unsubscribe(manager->events, plugin->subscriptions[s]);
|
naut_event_unsubscribe(manager->events, plugin->subscriptions[s]);
|
||||||
free(plugin->subscriptions);
|
free(plugin->subscriptions);
|
||||||
|
|
@ -280,6 +312,10 @@ naut_err naut_plugin_load(naut_plugin_manager *manager, const char *path) {
|
||||||
memset(plugin, 0, sizeof(*plugin));
|
memset(plugin, 0, sizeof(*plugin));
|
||||||
return NAUT_ERR_PROTO;
|
return NAUT_ERR_PROTO;
|
||||||
}
|
}
|
||||||
|
dlerror();
|
||||||
|
plugin->shutdown = (naut_plugin_shutdown_fn)dlsym(plugin->handle,
|
||||||
|
"naut_plugin_shutdown");
|
||||||
|
dlerror();
|
||||||
naut_host_api host = {
|
naut_host_api host = {
|
||||||
.abi_version = NAUT_PLUGIN_ABI_VERSION,
|
.abi_version = NAUT_PLUGIN_ABI_VERSION,
|
||||||
.struct_size = sizeof(host),
|
.struct_size = sizeof(host),
|
||||||
|
|
@ -290,6 +326,7 @@ naut_err naut_plugin_load(naut_plugin_manager *manager, const char *path) {
|
||||||
.subscribe_event = host_subscribe_event,
|
.subscribe_event = host_subscribe_event,
|
||||||
.emit_event = host_emit_event,
|
.emit_event = host_emit_event,
|
||||||
.log = host_log,
|
.log = host_log,
|
||||||
|
.call_rpc = host_call_rpc,
|
||||||
};
|
};
|
||||||
size_t rpc_start = manager->rpc_count;
|
size_t rpc_start = manager->rpc_count;
|
||||||
size_t event_start = manager->event_count;
|
size_t event_start = manager->event_count;
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ typedef struct {
|
||||||
int direct_fd;
|
int direct_fd;
|
||||||
int64_t start; /* global offset of this file's first byte */
|
int64_t start; /* global offset of this file's first byte */
|
||||||
int64_t length;
|
int64_t length;
|
||||||
char *path; /* full on-disk path (for relocate) */
|
char *path; /* current on-disk path (updated by relocate) */
|
||||||
bool externalized; /* moved out; region no longer backed here */
|
|
||||||
} file_slot;
|
} file_slot;
|
||||||
|
|
||||||
struct naut_storage {
|
struct naut_storage {
|
||||||
|
|
@ -140,7 +139,6 @@ static naut_err io_at(naut_storage *s, int64_t offset, void *buf, size_t len, bo
|
||||||
while (len > 0) {
|
while (len > 0) {
|
||||||
const file_slot *f = locate(s, offset);
|
const file_slot *f = locate(s, offset);
|
||||||
if (!f) return NAUT_ERR_RANGE; /* zero-length file region */
|
if (!f) return NAUT_ERR_RANGE; /* zero-length file region */
|
||||||
if (f->externalized) return NAUT_ERR_RANGE; /* moved out; not backed here */
|
|
||||||
off_t fo = (off_t)(offset - f->start);
|
off_t fo = (off_t)(offset - f->start);
|
||||||
size_t chunk = len;
|
size_t chunk = len;
|
||||||
int64_t avail = f->length - fo;
|
int64_t avail = f->length - fo;
|
||||||
|
|
@ -193,11 +191,31 @@ done:
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static naut_err reopen_slot(file_slot *file, const char *path,
|
||||||
|
bool direct_io) {
|
||||||
|
file->fd = open(path, O_RDWR);
|
||||||
|
if (file->fd < 0) return NAUT_ERR_IO;
|
||||||
|
#ifdef O_DIRECT
|
||||||
|
if (direct_io && file->length > 0) {
|
||||||
|
file->direct_fd = open(path, O_RDWR | O_DIRECT);
|
||||||
|
if (file->direct_fd < 0)
|
||||||
|
NAUT_WARN("O_DIRECT reopen unavailable for %s: %s", path,
|
||||||
|
strerror(errno));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void)direct_io;
|
||||||
|
#endif
|
||||||
|
return NAUT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest) {
|
naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *dest) {
|
||||||
|
if (!s || !dest || !*dest) return NAUT_ERR_INVAL;
|
||||||
if (file_index >= s->nfiles) return NAUT_ERR_RANGE;
|
if (file_index >= s->nfiles) return NAUT_ERR_RANGE;
|
||||||
file_slot *f = &s->files[file_index];
|
file_slot *f = &s->files[file_index];
|
||||||
if (f->externalized) return NAUT_ERR_INVAL;
|
|
||||||
|
|
||||||
|
char *newpath = strdup(dest);
|
||||||
|
if (!newpath) return NAUT_ERR_NOMEM;
|
||||||
|
bool had_direct = f->direct_fd >= 0;
|
||||||
if (f->direct_fd >= 0) {
|
if (f->direct_fd >= 0) {
|
||||||
fsync(f->direct_fd);
|
fsync(f->direct_fd);
|
||||||
close(f->direct_fd);
|
close(f->direct_fd);
|
||||||
|
|
@ -207,17 +225,45 @@ naut_err naut_storage_relocate(naut_storage *s, size_t file_index, const char *d
|
||||||
|
|
||||||
/* ensure the destination directory exists */
|
/* ensure the destination directory exists */
|
||||||
char dcopy[4096];
|
char dcopy[4096];
|
||||||
if ((size_t)snprintf(dcopy, sizeof dcopy, "%s", dest) >= sizeof dcopy) return NAUT_ERR_INVAL;
|
if ((size_t)snprintf(dcopy, sizeof dcopy, "%s", dest) >= sizeof dcopy) {
|
||||||
if (make_parents(dcopy) != NAUT_OK) return NAUT_ERR_IO;
|
free(newpath);
|
||||||
|
(void)reopen_slot(f, f->path, had_direct);
|
||||||
|
return NAUT_ERR_INVAL;
|
||||||
|
}
|
||||||
|
if (make_parents(dcopy) != NAUT_OK) {
|
||||||
|
free(newpath);
|
||||||
|
(void)reopen_slot(f, f->path, had_direct);
|
||||||
|
return NAUT_ERR_IO;
|
||||||
|
}
|
||||||
|
|
||||||
if (rename(f->path, dest) != 0) {
|
if (rename(f->path, dest) != 0) {
|
||||||
if (errno != EXDEV) { NAUT_ERROR("rename %s -> %s: %s", f->path, dest, strerror(errno)); return NAUT_ERR_IO; }
|
if (errno != EXDEV) {
|
||||||
|
NAUT_ERROR("rename %s -> %s: %s", f->path, dest,
|
||||||
|
strerror(errno));
|
||||||
|
free(newpath);
|
||||||
|
(void)reopen_slot(f, f->path, had_direct);
|
||||||
|
return NAUT_ERR_IO;
|
||||||
|
}
|
||||||
naut_err e = copy_file(f->path, dest); /* cross-filesystem */
|
naut_err e = copy_file(f->path, dest); /* cross-filesystem */
|
||||||
if (e != NAUT_OK) return e;
|
if (e != NAUT_OK) {
|
||||||
|
free(newpath);
|
||||||
|
(void)reopen_slot(f, f->path, had_direct);
|
||||||
|
return e;
|
||||||
|
}
|
||||||
if (unlink(f->path) != 0) NAUT_WARN("unlink %s after copy: %s", f->path, strerror(errno));
|
if (unlink(f->path) != 0) NAUT_WARN("unlink %s after copy: %s", f->path, strerror(errno));
|
||||||
}
|
}
|
||||||
f->externalized = true;
|
|
||||||
NAUT_INFO("relocated file %zu -> %s", file_index, dest);
|
/* Keep tracking the file at its new home: update the path and reopen so the
|
||||||
|
* engine can still read/write/seed it from the new location (no externalize,
|
||||||
|
* so the owning process never loses track of a moved file). */
|
||||||
|
free(f->path);
|
||||||
|
f->path = newpath;
|
||||||
|
|
||||||
|
if (reopen_slot(f, dest, had_direct) != NAUT_OK) {
|
||||||
|
NAUT_ERROR("reopen %s after move: %s", dest, strerror(errno));
|
||||||
|
return NAUT_ERR_IO;
|
||||||
|
}
|
||||||
|
NAUT_INFO("relocated file %zu -> %s (still tracked)", file_index, dest);
|
||||||
return NAUT_OK;
|
return NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
tests/fixtures/phase7.lua
vendored
2
tests/fixtures/phase7.lua
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
function on_torrent_finished(event)
|
function on_torrent_finished(event)
|
||||||
naut.move_file(event.torrent_id, 0, "/tmp/naut-phase7-finished")
|
print("torrent " .. event.torrent_id .. " finished")
|
||||||
end
|
end
|
||||||
|
|
||||||
function on_file_complete(event)
|
function on_file_complete(event)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,11 @@ tmp=$(mktemp -d)
|
||||||
socket="$tmp/nautd.sock"
|
socket="$tmp/nautd.sock"
|
||||||
daemon_log="$tmp/nautd.log"
|
daemon_log="$tmp/nautd.log"
|
||||||
events_log="$tmp/events.log"
|
events_log="$tmp/events.log"
|
||||||
|
seeder_log="$tmp/seeder.log"
|
||||||
|
root_dir=$(cd "$(dirname "$script")/../.." && pwd)
|
||||||
|
seeder="$root_dir/tests/integration/seeder.py"
|
||||||
|
torrent="$(dirname "$script")/single_v1.torrent"
|
||||||
|
seed_data="$(dirname "$script")/data"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
result=$?
|
result=$?
|
||||||
|
|
@ -20,16 +25,36 @@ cleanup() {
|
||||||
kill "$events_pid" 2>/dev/null || true
|
kill "$events_pid" 2>/dev/null || true
|
||||||
wait "$events_pid" 2>/dev/null || true
|
wait "$events_pid" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
if [[ -n "${seeder_pid:-}" ]]; then
|
||||||
|
kill "$seeder_pid" 2>/dev/null || true
|
||||||
|
wait "$seeder_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
if [[ "$result" -ne 0 ]]; then
|
if [[ "$result" -ne 0 ]]; then
|
||||||
cat "$daemon_log" >&2 2>/dev/null || true
|
cat "$daemon_log" >&2 2>/dev/null || true
|
||||||
cat "$events_log" >&2 2>/dev/null || true
|
cat "$events_log" >&2 2>/dev/null || true
|
||||||
|
cat "$seeder_log" >&2 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
rm -rf "$tmp"
|
rm -rf "$tmp"
|
||||||
return "$result"
|
return "$result"
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
"$daemon" --socket "$socket" --plugin "$plugin" --script "$script" \
|
python3 -c 'import libtorrent' 2>/dev/null || {
|
||||||
|
echo "SKIP: python libtorrent not available"
|
||||||
|
exit 77
|
||||||
|
}
|
||||||
|
|
||||||
|
python3 "$seeder" "$torrent" "$seed_data" >"$seeder_log" 2>&1 &
|
||||||
|
seeder_pid=$!
|
||||||
|
port=
|
||||||
|
for _ in $(seq 1 100); do
|
||||||
|
port=$(grep -oP 'PORT \K[0-9]+' "$seeder_log" 2>/dev/null || true)
|
||||||
|
[[ -n "$port" && "$port" != 0 ]] && break
|
||||||
|
sleep 0.05
|
||||||
|
done
|
||||||
|
[[ -n "$port" && "$port" != 0 ]]
|
||||||
|
|
||||||
|
"$daemon" --socket "$socket" --plugin "$plugin" \
|
||||||
>"$daemon_log" 2>&1 &
|
>"$daemon_log" 2>&1 &
|
||||||
daemon_pid=$!
|
daemon_pid=$!
|
||||||
|
|
||||||
|
|
@ -41,24 +66,28 @@ done
|
||||||
|
|
||||||
"$ctl" --socket "$socket" ping | grep -q '"service": "nautd"'
|
"$ctl" --socket "$socket" ping | grep -q '"service": "nautd"'
|
||||||
"$ctl" --socket "$socket" plugins | grep -q '"memory"'
|
"$ctl" --socket "$socket" plugins | grep -q '"memory"'
|
||||||
|
"$ctl" --socket "$socket" script "$script" | grep -q '"ok": true'
|
||||||
|
|
||||||
timeout 5 "$ctl" --socket "$socket" events >"$events_log" &
|
timeout 5 "$ctl" --socket "$socket" events >"$events_log" &
|
||||||
events_pid=$!
|
events_pid=$!
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
"$ctl" --socket "$socket" emit \
|
root="$tmp/torrent-data"
|
||||||
'{"type":"torrent_finished","torrent_id":7}' >/dev/null
|
"$ctl" --socket "$socket" add "$torrent" "$root" "127.0.0.1:$port" \
|
||||||
|
| grep -q '"ok": true'
|
||||||
|
|
||||||
status=
|
listing=
|
||||||
for _ in $(seq 1 100); do
|
for _ in $(seq 1 200); do
|
||||||
status=$("$ctl" --socket "$socket" status)
|
listing=$("$ctl" --socket "$socket" list)
|
||||||
if grep -q '"move_commands": 1' <<<"$status" &&
|
grep -q '"state": "complete"' <<<"$listing" &&
|
||||||
grep -q '"handled": 1' <<<"$status"; then
|
[[ -f "$root/single.bin.moved" ]] && break
|
||||||
break
|
sleep 0.05
|
||||||
fi
|
|
||||||
sleep 0.02
|
|
||||||
done
|
done
|
||||||
|
grep -q '"state": "complete"' <<<"$listing"
|
||||||
|
cmp "$root/single.bin.moved" "$seed_data/single.bin"
|
||||||
|
|
||||||
|
status=$("$ctl" --socket "$socket" status)
|
||||||
grep -q '"move_commands": 1' <<<"$status"
|
grep -q '"move_commands": 1' <<<"$status"
|
||||||
grep -q '"handled": 1' <<<"$status"
|
grep -q '"handled": 2' <<<"$status"
|
||||||
grep -q '"errors": 0' <<<"$status"
|
grep -q '"errors": 0' <<<"$status"
|
||||||
|
|
||||||
plugin_status=$("$ctl" --socket "$socket" example.events)
|
plugin_status=$("$ctl" --socket "$socket" example.events)
|
||||||
|
|
@ -70,28 +99,13 @@ for _ in $(seq 1 100); do
|
||||||
done
|
done
|
||||||
grep -q '"event": "torrent_finished"' "$events_log"
|
grep -q '"event": "torrent_finished"' "$events_log"
|
||||||
|
|
||||||
# --- end-to-end move-as-you-finish: register a real torrent's storage, fire a
|
"$ctl" --socket "$socket" remove 1 >/dev/null
|
||||||
# file_complete event, and confirm the script-driven move actually relocates the
|
|
||||||
# file on disk (script thread -> bounded queue -> owner thread -> storage). ----
|
|
||||||
fixtures=$(dirname "$script")
|
|
||||||
root="$tmp/torrent-data"
|
|
||||||
"$ctl" --socket "$socket" add_torrent \
|
|
||||||
"{\"torrent_id\":42,\"torrent\":\"$fixtures/single_v1.torrent\",\"root\":\"$root\"}" \
|
|
||||||
| grep -q '"ok": true'
|
|
||||||
|
|
||||||
src=$(find "$root" -type f | head -n1)
|
|
||||||
[[ -n "$src" ]]
|
|
||||||
|
|
||||||
"$ctl" --socket "$socket" emit \
|
|
||||||
"{\"type\":\"file_complete\",\"torrent_id\":42,\"index\":0,\"path\":\"$src\"}" \
|
|
||||||
>/dev/null
|
|
||||||
|
|
||||||
for _ in $(seq 1 100); do
|
for _ in $(seq 1 100); do
|
||||||
[[ -f "$src.moved" ]] && break
|
listing=$("$ctl" --socket "$socket" list)
|
||||||
|
grep -q '"result": \[\]' <<<"$listing" && break
|
||||||
sleep 0.02
|
sleep 0.02
|
||||||
done
|
done
|
||||||
[[ -f "$src.moved" ]]
|
grep -q '"result": \[\]' <<<"$listing"
|
||||||
[[ ! -f "$src" ]]
|
|
||||||
|
|
||||||
"$ctl" --socket "$socket" shutdown >/dev/null
|
"$ctl" --socket "$socket" shutdown >/dev/null
|
||||||
wait "$daemon_pid"
|
wait "$daemon_pid"
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ if [ "$MODE" = "udp" ]; then
|
||||||
else
|
else
|
||||||
announce="http://127.0.0.1:$tracker_port/announce"
|
announce="http://127.0.0.1:$tracker_port/announce"
|
||||||
fi
|
fi
|
||||||
python3 - "$SOURCE_TOR" "$tor" "$announce" <<'PY'
|
backup="http://127.0.0.1:1/lower-tier-should-not-be-contacted"
|
||||||
|
python3 - "$SOURCE_TOR" "$tor" "$announce" "$backup" <<'PY'
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,7 +91,8 @@ def skip(data, pos):
|
||||||
return colon + 1 + size
|
return colon + 1 + size
|
||||||
|
|
||||||
|
|
||||||
source, target, announce = sys.argv[1], sys.argv[2], sys.argv[3].encode()
|
source, target = sys.argv[1], sys.argv[2]
|
||||||
|
announce, backup = sys.argv[3].encode(), sys.argv[4].encode()
|
||||||
data = open(source, "rb").read()
|
data = open(source, "rb").read()
|
||||||
pos = 1
|
pos = 1
|
||||||
raw_info = None
|
raw_info = None
|
||||||
|
|
@ -107,6 +109,8 @@ while data[pos] != ord("e"):
|
||||||
assert raw_info is not None
|
assert raw_info is not None
|
||||||
rewritten = (
|
rewritten = (
|
||||||
b"d8:announce" + str(len(announce)).encode() + b":" + announce
|
b"d8:announce" + str(len(announce)).encode() + b":" + announce
|
||||||
|
+ b"13:announce-listll" + str(len(announce)).encode() + b":" + announce
|
||||||
|
+ b"el" + str(len(backup)).encode() + b":" + backup + b"ee"
|
||||||
+ b"4:info" + raw_info + b"e"
|
+ b"4:info" + raw_info + b"e"
|
||||||
)
|
)
|
||||||
open(target, "wb").write(rewritten)
|
open(target, "wb").write(rewritten)
|
||||||
|
|
@ -125,5 +129,10 @@ if ! grep -q "REQUEST" "$tracker_log"; then
|
||||||
echo "FAIL: tracker received no announce"
|
echo "FAIL: tracker received no announce"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if grep -q "$backup" "$swarm_log"; then
|
||||||
|
echo "FAIL: lower tracker tier was contacted after primary success"
|
||||||
|
cat "$swarm_log"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "PASS: $MODE tracker discovery produced byte-identical output"
|
echo "PASS: $MODE tracker discovery produced byte-identical output"
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ int main(void) {
|
||||||
CHECK_EQ(mi.total_length, 200000);
|
CHECK_EQ(mi.total_length, 200000);
|
||||||
CHECK_EQ(mi.num_files, 1);
|
CHECK_EQ(mi.num_files, 1);
|
||||||
CHECK_EQ(mi.num_trackers, 2);
|
CHECK_EQ(mi.num_trackers, 2);
|
||||||
|
CHECK(mi.tracker_tiers != NULL);
|
||||||
|
CHECK_EQ(mi.tracker_tiers[0], 0);
|
||||||
|
CHECK_EQ(mi.tracker_tiers[1], 1);
|
||||||
char hex[41]; naut_infohash_v1_hex(&mi, hex);
|
char hex[41]; naut_infohash_v1_hex(&mi, hex);
|
||||||
CHECK(strcmp(hex, "7f2555dfd18ba1c4a024264e939d7a5f8b25311b") == 0);
|
CHECK(strcmp(hex, "7f2555dfd18ba1c4a024264e939d7a5f8b25311b") == 0);
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
|
|
@ -134,6 +137,7 @@ int main(void) {
|
||||||
"7f2555dfd18ba1c4a024264e939d7a5f8b25311b", 20));
|
"7f2555dfd18ba1c4a024264e939d7a5f8b25311b", 20));
|
||||||
CHECK(mi.num_trackers == 1 &&
|
CHECK(mi.num_trackers == 1 &&
|
||||||
strcmp(mi.trackers[0], trackers[0]) == 0);
|
strcmp(mi.trackers[0], trackers[0]) == 0);
|
||||||
|
CHECK(mi.tracker_tiers && mi.tracker_tiers[0] == 0);
|
||||||
naut_metainfo_free(&mi);
|
naut_metainfo_free(&mi);
|
||||||
naut_bc_free(doc);
|
naut_bc_free(doc);
|
||||||
free(torrent);
|
free(torrent);
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,13 @@ int main(void) {
|
||||||
.torrent_id = 42,
|
.torrent_id = 42,
|
||||||
};
|
};
|
||||||
naut_event_emit(events, &event);
|
naut_event_emit(events, &event);
|
||||||
for (unsigned i = 0; i < 100 && atomic_load(&capture.calls) == 0; i++)
|
for (unsigned i = 0; i < 100; i++) {
|
||||||
|
naut_script_stats pending;
|
||||||
|
naut_script_get_stats(script, &pending);
|
||||||
|
if (pending.handled == 1) break;
|
||||||
usleep(1000);
|
usleep(1000);
|
||||||
CHECK_EQ(atomic_load(&capture.calls), 1);
|
}
|
||||||
CHECK(!pthread_equal(owner, capture.caller));
|
CHECK_EQ(atomic_load(&capture.calls), 0);
|
||||||
CHECK_EQ(capture.torrent_id, 42);
|
|
||||||
CHECK_EQ(capture.file_index, 0);
|
|
||||||
CHECK(strcmp(capture.destination, "/tmp/naut-phase7-finished") == 0);
|
|
||||||
|
|
||||||
event = (naut_event) {
|
event = (naut_event) {
|
||||||
.type = NAUT_EVENT_FILE_COMPLETE,
|
.type = NAUT_EVENT_FILE_COMPLETE,
|
||||||
|
|
@ -59,9 +59,11 @@ int main(void) {
|
||||||
.path = "/tmp/completed-file",
|
.path = "/tmp/completed-file",
|
||||||
};
|
};
|
||||||
naut_event_emit(events, &event);
|
naut_event_emit(events, &event);
|
||||||
for (unsigned i = 0; i < 100 && atomic_load(&capture.calls) < 2; i++)
|
for (unsigned i = 0; i < 100 && atomic_load(&capture.calls) < 1; i++)
|
||||||
usleep(1000);
|
usleep(1000);
|
||||||
CHECK_EQ(atomic_load(&capture.calls), 2);
|
CHECK_EQ(atomic_load(&capture.calls), 1);
|
||||||
|
CHECK(!pthread_equal(owner, capture.caller));
|
||||||
|
CHECK_EQ(capture.torrent_id, 42);
|
||||||
CHECK_EQ(capture.file_index, 3);
|
CHECK_EQ(capture.file_index, 3);
|
||||||
CHECK(strcmp(capture.destination, "/tmp/completed-file.moved") == 0);
|
CHECK(strcmp(capture.destination, "/tmp/completed-file.moved") == 0);
|
||||||
|
|
||||||
|
|
@ -70,7 +72,7 @@ int main(void) {
|
||||||
CHECK_EQ(stats.queued, 2);
|
CHECK_EQ(stats.queued, 2);
|
||||||
CHECK_EQ(stats.handled, 2);
|
CHECK_EQ(stats.handled, 2);
|
||||||
CHECK_EQ(stats.errors, 0);
|
CHECK_EQ(stats.errors, 0);
|
||||||
CHECK_EQ(stats.move_requests, 2);
|
CHECK_EQ(stats.move_requests, 1);
|
||||||
|
|
||||||
naut_script_destroy(script);
|
naut_script_destroy(script);
|
||||||
naut_event_bus_destroy(events);
|
naut_event_bus_destroy(events);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue