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:
ookami125 2026-06-17 00:21:48 -04:00
parent 50a357968a
commit 8dde48c05a
27 changed files with 2706 additions and 403 deletions

View file

@ -20,9 +20,37 @@ static char *dup_cstr(const uint8_t *p, size_t n) {
return s;
}
/* collect a single announce string or an announce-list (list of tiers) */
static void collect_trackers(const naut_bc *root, naut_metainfo *mi) {
size_t cap = 0;
static naut_err add_tracker(naut_metainfo *mi, size_t *capacity,
const uint8_t *url, size_t url_len,
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");
if (al && al->type == NAUT_BC_LIST) {
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 uint8_t *p; size_t n;
if (!naut_bc_get_str(url, &p, &n)) continue;
if (mi->num_trackers == cap) {
cap = cap ? cap * 2 : 8;
mi->trackers = realloc(mi->trackers, cap * sizeof(char *));
}
mi->trackers[mi->num_trackers++] = dup_cstr(p, n);
naut_err error =
add_tracker(mi, &capacity, p, n, (uint32_t)t);
if (error != NAUT_OK) return error;
}
}
}
if (mi->num_trackers == 0) {
const uint8_t *p; size_t n;
if (naut_bc_get_str(naut_bc_dict_get(root, "announce"), &p, &n)) {
mi->trackers = malloc(sizeof(char *));
mi->trackers[mi->num_trackers++] = dup_cstr(p, n);
naut_err error = add_tracker(mi, &capacity, p, n, 0);
if (error != NAUT_OK) return error;
}
}
return NAUT_OK;
}
/* 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 {
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;
return NAUT_OK;
@ -245,7 +277,9 @@ naut_err naut_metainfo_parse_info(const uint8_t *info, size_t info_len,
if (num_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);
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);
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;
}
@ -270,6 +307,7 @@ void naut_metainfo_free(naut_metainfo *mi) {
free(mi->files);
for (size_t i = 0; i < mi->num_trackers; i++) free(mi->trackers[i]);
free(mi->trackers);
free(mi->tracker_tiers);
if (mi->_owned) {
owned *o = mi->_owned;
naut_bc_free(o->doc);

View file

@ -49,6 +49,8 @@ struct naut_download {
bool *file_done;
naut_file_complete_cb file_cb;
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; }
@ -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) {
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) {
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;
free_ps(d, p);
*done = true;
if (d->piece_cb) d->piece_cb(d->piece_cb_ctx, p);
notify_files(d, p);
return NAUT_OK;
}

View file

@ -20,6 +20,7 @@ typedef struct {
void *handle;
char *path;
char *name;
naut_plugin_shutdown_fn shutdown;
uint64_t *subscriptions;
size_t subscription_count;
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);
}
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_rpc_registry *rpc, naut_event_bus *events) {
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);
for (size_t i = 0; i < manager->plugin_count; i++) {
loaded_plugin *plugin = &manager->plugins[i];
if (plugin->shutdown) plugin->shutdown();
for (size_t s = 0; s < plugin->subscription_count; s++)
naut_event_unsubscribe(manager->events, plugin->subscriptions[s]);
free(plugin->subscriptions);
@ -280,6 +312,10 @@ naut_err naut_plugin_load(naut_plugin_manager *manager, const char *path) {
memset(plugin, 0, sizeof(*plugin));
return NAUT_ERR_PROTO;
}
dlerror();
plugin->shutdown = (naut_plugin_shutdown_fn)dlsym(plugin->handle,
"naut_plugin_shutdown");
dlerror();
naut_host_api host = {
.abi_version = NAUT_PLUGIN_ABI_VERSION,
.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,
.emit_event = host_emit_event,
.log = host_log,
.call_rpc = host_call_rpc,
};
size_t rpc_start = manager->rpc_count;
size_t event_start = manager->event_count;

View file

@ -15,8 +15,7 @@ typedef struct {
int direct_fd;
int64_t start; /* global offset of this file's first byte */
int64_t length;
char *path; /* full on-disk path (for relocate) */
bool externalized; /* moved out; region no longer backed here */
char *path; /* current on-disk path (updated by relocate) */
} file_slot;
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) {
const file_slot *f = locate(s, offset);
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);
size_t chunk = len;
int64_t avail = f->length - fo;
@ -193,11 +191,31 @@ done:
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) {
if (!s || !dest || !*dest) return NAUT_ERR_INVAL;
if (file_index >= s->nfiles) return NAUT_ERR_RANGE;
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) {
fsync(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 */
char dcopy[4096];
if ((size_t)snprintf(dcopy, sizeof dcopy, "%s", dest) >= sizeof dcopy) return NAUT_ERR_INVAL;
if (make_parents(dcopy) != NAUT_OK) return NAUT_ERR_IO;
if ((size_t)snprintf(dcopy, sizeof dcopy, "%s", dest) >= sizeof dcopy) {
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 (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 */
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));
}
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;
}