A maintainable, extensible BitTorrent client (C11, Linux/io_uring) targeting 10 GbE saturation. All torrent functionality is built from scratch; liburing is the only linked third-party dependency on the data path. Implements Phases 1-7 of the roadmap: - core: page-aligned buffer pool, MPMC/Treiber queues, bitfields, worker pool - crypto: SHA-1/256 (SHA-NI + scalar), Merkle (BEP-52), RC4 (MSE) - bencode/metainfo: zero-copy parser, v1/v2/hybrid .torrent + magnet - peer: sans-IO wire codec, MSE/PE handshake state machine, BEP-10, ut_metadata, PEX - piece/storage: block-level multi-peer engine, rarest-first + endgame, per-file completion events + single-file relocate (move-as-you-finish) - tracker/dht: HTTP + UDP (BEP-15) trackers, BEP-5 KRPC iterative lookup - platform: io_uring reactor (SQPOLL, registered buffers, SEND_ZC) - surface: versioned RPC, native plugin ABI, sandboxed Lua scripting, nautd/nautctl Verified against libtorrent (single/multi/hybrid, MSE, magnet-via-DHT, swarm); unit + interop tests green; ASan/UBSan/TSan clean. Scripting reference in docs/scripting.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
312 lines
11 KiB
C
312 lines
11 KiB
C
#include "naut/script.h"
|
|
|
|
#include <lua.h>
|
|
#include <lauxlib.h>
|
|
#include <lualib.h>
|
|
|
|
#include <limits.h>
|
|
#include <pthread.h>
|
|
#include <stdatomic.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
typedef struct {
|
|
naut_event event;
|
|
char message[256];
|
|
char path[PATH_MAX];
|
|
} script_job;
|
|
|
|
struct naut_script {
|
|
naut_event_bus *events;
|
|
uint64_t subscription;
|
|
lua_State *lua;
|
|
pthread_t thread;
|
|
pthread_mutex_t lock;
|
|
pthread_cond_t ready;
|
|
script_job *queue;
|
|
size_t capacity;
|
|
size_t head;
|
|
size_t count;
|
|
bool stopping;
|
|
naut_script_move_file_cb move_file;
|
|
void *move_context;
|
|
_Atomic uint64_t queued;
|
|
_Atomic uint64_t handled;
|
|
_Atomic uint64_t dropped;
|
|
_Atomic uint64_t errors;
|
|
_Atomic uint64_t move_requests;
|
|
char last_error[256];
|
|
};
|
|
|
|
static const char *hook_names[] = {
|
|
[NAUT_EVENT_TORRENT_ADDED] = "on_torrent_added",
|
|
[NAUT_EVENT_PIECE_COMPLETE] = "on_piece_complete",
|
|
[NAUT_EVENT_FILE_COMPLETE] = "on_file_complete",
|
|
[NAUT_EVENT_TORRENT_FINISHED] = "on_torrent_finished",
|
|
[NAUT_EVENT_PEER_CONNECTED] = "on_peer_connected",
|
|
[NAUT_EVENT_ALERT] = "on_alert",
|
|
};
|
|
|
|
static void set_last_error(naut_script *script, const char *message) {
|
|
pthread_mutex_lock(&script->lock);
|
|
snprintf(script->last_error, sizeof script->last_error, "%s",
|
|
message ? message : "unknown Lua error");
|
|
pthread_mutex_unlock(&script->lock);
|
|
}
|
|
|
|
static naut_script *lua_script(lua_State *lua) {
|
|
return lua_touserdata(lua, lua_upvalueindex(1));
|
|
}
|
|
|
|
static int lua_move_file(lua_State *lua) {
|
|
naut_script *script = lua_script(lua);
|
|
lua_Integer torrent_id = luaL_checkinteger(lua, 1);
|
|
lua_Integer file_index = luaL_checkinteger(lua, 2);
|
|
const char *destination = luaL_checkstring(lua, 3);
|
|
if (torrent_id < 0 || file_index < 0 ||
|
|
(uint64_t)file_index > UINT32_MAX)
|
|
return luaL_error(lua, "move_file arguments out of range");
|
|
if (!script->move_file)
|
|
return luaL_error(lua, "move_file is unavailable");
|
|
naut_err error = script->move_file(script->move_context,
|
|
(uint64_t)torrent_id,
|
|
(uint32_t)file_index,
|
|
destination);
|
|
if (error != NAUT_OK)
|
|
return luaL_error(lua, "move_file failed: %d", error);
|
|
atomic_fetch_add_explicit(&script->move_requests, 1,
|
|
memory_order_relaxed);
|
|
return 0;
|
|
}
|
|
|
|
static void sandbox(lua_State *lua) {
|
|
/* Remove every documented route to the filesystem, subprocesses, native
|
|
* module loading, and raw chunk compilation. `load`/`loadstring` are
|
|
* blocked too: with the default "bt" mode they accept *binary* chunks, and
|
|
* a crafted bytecode chunk can escape the VM entirely — so even though
|
|
* scripts are operator-supplied, we deny the bytecode-loader as
|
|
* defense-in-depth. */
|
|
static const char *blocked[] = {
|
|
"debug", "dofile", "io", "load", "loadfile", "loadstring",
|
|
"os", "package", "require",
|
|
};
|
|
for (size_t i = 0; i < NAUT_ARRAY_LEN(blocked); i++) {
|
|
lua_pushnil(lua);
|
|
lua_setglobal(lua, blocked[i]);
|
|
}
|
|
}
|
|
|
|
static void install_api(naut_script *script) {
|
|
lua_State *lua = script->lua;
|
|
lua_newtable(lua);
|
|
lua_pushlightuserdata(lua, script);
|
|
lua_pushcclosure(lua, lua_move_file, 1);
|
|
lua_setfield(lua, -2, "move_file");
|
|
lua_setglobal(lua, "naut");
|
|
}
|
|
|
|
static void push_event(lua_State *lua, const naut_event *event) {
|
|
lua_createtable(lua, 0, 5);
|
|
lua_pushstring(lua, naut_event_type_name(event->type));
|
|
lua_setfield(lua, -2, "type");
|
|
lua_pushinteger(lua, (lua_Integer)event->torrent_id);
|
|
lua_setfield(lua, -2, "torrent_id");
|
|
lua_pushinteger(lua, (lua_Integer)event->index);
|
|
lua_setfield(lua, -2, "index");
|
|
if (event->message) {
|
|
lua_pushstring(lua, event->message);
|
|
lua_setfield(lua, -2, "message");
|
|
}
|
|
if (event->path) {
|
|
lua_pushstring(lua, event->path);
|
|
lua_setfield(lua, -2, "path");
|
|
}
|
|
}
|
|
|
|
static void run_hook(naut_script *script, const naut_event *event) {
|
|
if ((size_t)event->type >= NAUT_ARRAY_LEN(hook_names)) {
|
|
atomic_fetch_add_explicit(&script->errors, 1, memory_order_relaxed);
|
|
set_last_error(script, "unknown event type");
|
|
return;
|
|
}
|
|
const char *hook = hook_names[event->type];
|
|
lua_getglobal(script->lua, hook);
|
|
if (lua_isnil(script->lua, -1)) {
|
|
lua_pop(script->lua, 1);
|
|
return;
|
|
}
|
|
if (!lua_isfunction(script->lua, -1)) {
|
|
lua_pop(script->lua, 1);
|
|
atomic_fetch_add_explicit(&script->errors, 1, memory_order_relaxed);
|
|
set_last_error(script, "event hook is not a function");
|
|
return;
|
|
}
|
|
push_event(script->lua, event);
|
|
if (lua_pcall(script->lua, 1, 0, 0) != LUA_OK) {
|
|
atomic_fetch_add_explicit(&script->errors, 1, memory_order_relaxed);
|
|
set_last_error(script, lua_tostring(script->lua, -1));
|
|
lua_pop(script->lua, 1);
|
|
return;
|
|
}
|
|
atomic_fetch_add_explicit(&script->handled, 1, memory_order_relaxed);
|
|
}
|
|
|
|
static bool pop_job(naut_script *script, script_job *job) {
|
|
pthread_mutex_lock(&script->lock);
|
|
while (!script->stopping && script->count == 0)
|
|
pthread_cond_wait(&script->ready, &script->lock);
|
|
if (script->count == 0) {
|
|
pthread_mutex_unlock(&script->lock);
|
|
return false;
|
|
}
|
|
*job = script->queue[script->head];
|
|
if (job->event.message) job->event.message = job->message;
|
|
if (job->event.path) job->event.path = job->path;
|
|
script->head = (script->head + 1) % script->capacity;
|
|
script->count--;
|
|
pthread_mutex_unlock(&script->lock);
|
|
return true;
|
|
}
|
|
|
|
static void *script_worker(void *opaque) {
|
|
naut_script *script = opaque;
|
|
script_job job;
|
|
while (pop_job(script, &job))
|
|
run_hook(script, &job.event);
|
|
return NULL;
|
|
}
|
|
|
|
static void queue_event(void *opaque, const naut_event *event) {
|
|
naut_script *script = opaque;
|
|
pthread_mutex_lock(&script->lock);
|
|
if (script->stopping || script->count == script->capacity) {
|
|
pthread_mutex_unlock(&script->lock);
|
|
atomic_fetch_add_explicit(&script->dropped, 1, memory_order_relaxed);
|
|
return;
|
|
}
|
|
size_t tail = (script->head + script->count) % script->capacity;
|
|
script_job *job = &script->queue[tail];
|
|
memset(job, 0, sizeof(*job));
|
|
job->event = *event;
|
|
if (event->message) {
|
|
snprintf(job->message, sizeof job->message, "%s", event->message);
|
|
job->event.message = job->message;
|
|
}
|
|
if (event->path) {
|
|
snprintf(job->path, sizeof job->path, "%s", event->path);
|
|
job->event.path = job->path;
|
|
}
|
|
script->count++;
|
|
pthread_cond_signal(&script->ready);
|
|
pthread_mutex_unlock(&script->lock);
|
|
atomic_fetch_add_explicit(&script->queued, 1, memory_order_relaxed);
|
|
}
|
|
|
|
naut_script *naut_script_create(naut_event_bus *events,
|
|
const char *script_path,
|
|
size_t queue_capacity,
|
|
naut_script_move_file_cb move_file,
|
|
void *move_context,
|
|
naut_err *error) {
|
|
if (error) *error = NAUT_ERR_INVAL;
|
|
if (!events || !script_path || !*script_path || queue_capacity == 0)
|
|
return NULL;
|
|
naut_script *script = calloc(1, sizeof(*script));
|
|
if (!script) {
|
|
if (error) *error = NAUT_ERR_NOMEM;
|
|
return NULL;
|
|
}
|
|
script->events = events;
|
|
script->capacity = queue_capacity;
|
|
script->move_file = move_file;
|
|
script->move_context = move_context;
|
|
script->queue = calloc(queue_capacity, sizeof(*script->queue));
|
|
if (!script->queue) {
|
|
if (error) *error = NAUT_ERR_NOMEM;
|
|
free(script);
|
|
return NULL;
|
|
}
|
|
if (pthread_mutex_init(&script->lock, NULL) != 0) {
|
|
if (error) *error = NAUT_ERR_NOMEM;
|
|
free(script->queue);
|
|
free(script);
|
|
return NULL;
|
|
}
|
|
if (pthread_cond_init(&script->ready, NULL) != 0) {
|
|
if (error) *error = NAUT_ERR_NOMEM;
|
|
pthread_mutex_destroy(&script->lock);
|
|
free(script->queue);
|
|
free(script);
|
|
return NULL;
|
|
}
|
|
script->lua = luaL_newstate();
|
|
if (!script->lua) goto fail;
|
|
luaL_openlibs(script->lua);
|
|
sandbox(script->lua);
|
|
install_api(script);
|
|
if (luaL_loadfile(script->lua, script_path) != LUA_OK ||
|
|
lua_pcall(script->lua, 0, 0, 0) != LUA_OK) {
|
|
set_last_error(script, lua_tostring(script->lua, -1));
|
|
if (error) *error = NAUT_ERR_PROTO;
|
|
goto fail;
|
|
}
|
|
if (naut_event_subscribe(events, queue_event, script,
|
|
&script->subscription) != NAUT_OK)
|
|
goto fail;
|
|
if (pthread_create(&script->thread, NULL, script_worker, script) != 0) {
|
|
naut_event_unsubscribe(events, script->subscription);
|
|
script->subscription = 0;
|
|
goto fail;
|
|
}
|
|
if (error) *error = NAUT_OK;
|
|
return script;
|
|
|
|
fail:
|
|
if (error && *error == NAUT_ERR_INVAL) *error = NAUT_ERR_NOMEM;
|
|
if (script->lua) lua_close(script->lua);
|
|
pthread_cond_destroy(&script->ready);
|
|
pthread_mutex_destroy(&script->lock);
|
|
free(script->queue);
|
|
free(script);
|
|
return NULL;
|
|
}
|
|
|
|
void naut_script_destroy(naut_script *script) {
|
|
if (!script) return;
|
|
naut_event_unsubscribe(script->events, script->subscription);
|
|
pthread_mutex_lock(&script->lock);
|
|
script->stopping = true;
|
|
pthread_cond_broadcast(&script->ready);
|
|
pthread_mutex_unlock(&script->lock);
|
|
pthread_join(script->thread, NULL);
|
|
lua_close(script->lua);
|
|
pthread_cond_destroy(&script->ready);
|
|
pthread_mutex_destroy(&script->lock);
|
|
free(script->queue);
|
|
free(script);
|
|
}
|
|
|
|
void naut_script_get_stats(const naut_script *script,
|
|
naut_script_stats *stats) {
|
|
if (!script || !stats) return;
|
|
*stats = (naut_script_stats) {
|
|
.queued = atomic_load_explicit(&script->queued, memory_order_relaxed),
|
|
.handled = atomic_load_explicit(&script->handled,
|
|
memory_order_relaxed),
|
|
.dropped = atomic_load_explicit(&script->dropped,
|
|
memory_order_relaxed),
|
|
.errors = atomic_load_explicit(&script->errors, memory_order_relaxed),
|
|
.move_requests = atomic_load_explicit(&script->move_requests,
|
|
memory_order_relaxed),
|
|
};
|
|
}
|
|
|
|
const char *naut_script_last_error(naut_script *script) {
|
|
if (!script) return "";
|
|
pthread_mutex_lock(&script->lock);
|
|
static _Thread_local char copy[256];
|
|
snprintf(copy, sizeof copy, "%s", script->last_error);
|
|
pthread_mutex_unlock(&script->lock);
|
|
return copy;
|
|
}
|