From 6ae72907b0faf2cfbae7c30ad3ab22ae775ca0a7 Mon Sep 17 00:00:00 2001 From: ookami125 Date: Wed, 17 Jun 2026 01:29:44 -0400 Subject: [PATCH] Expose peer and piece state to web UI --- apps/nautd/main.c | 35 ++++++++++++++++ apps/swarm/main.c | 93 ++++++++++++++++++++++++++++++++++++++++--- include/naut/piece.h | 2 + include/naut/swarm.h | 21 ++++++++++ plugins/webui/webui.c | 59 ++++++++++++++++++++++++--- src/piece/piece.c | 14 +++++++ 6 files changed, 213 insertions(+), 11 deletions(-) diff --git a/apps/nautd/main.c b/apps/nautd/main.c index b8c8e95..e99b421 100644 --- a/apps/nautd/main.c +++ b/apps/nautd/main.c @@ -213,6 +213,16 @@ static json_t *torrent_json(torrent_task *task) { json_integer(task->stats.pieces_done)); json_object_set_new(result, "total_pieces", json_integer(task->stats.total_pieces)); + json_t *piece_states = json_array(); + if (piece_states) { + uint32_t state_count = task->stats.piece_state_count; + if (state_count > NAUT_SWARM_MAX_PIECE_STATS) + state_count = NAUT_SWARM_MAX_PIECE_STATS; + for (uint32_t i = 0; i < state_count; i++) + json_array_append_new(piece_states, + json_integer(task->stats.piece_states[i])); + json_object_set_new(result, "piece_states", piece_states); + } json_object_set_new(result, "peers", json_integer(task->stats.peers_active)); json_object_set_new(result, "peers_discovered", @@ -221,6 +231,31 @@ static json_t *torrent_json(torrent_task *task) { json_integer(task->stats.peers_connecting)); json_object_set_new(result, "peers_failed", json_integer(task->stats.peers_failed)); + json_t *peer_list = json_array(); + if (peer_list) { + uint32_t peer_count = task->stats.peer_count; + if (peer_count > NAUT_SWARM_MAX_PEER_STATS) + peer_count = NAUT_SWARM_MAX_PEER_STATS; + for (uint32_t i = 0; i < peer_count; i++) { + const naut_swarm_peer_stats *peer = + &task->stats.peer_stats[i]; + json_t *item = json_pack( + "{s:s,s:i,s:s,s:s,s:s,s:f,s:f,s:I,s:I,s:I,s:I}", + "ip", peer->ip, + "port", (int)peer->port, + "client", peer->client, + "connection", peer->connection, + "flags", peer->flags, + "progress", peer->progress, + "relevance", peer->relevance, + "downloaded", (json_int_t)peer->downloaded, + "uploaded", (json_int_t)peer->uploaded, + "dlspeed", (json_int_t)peer->dlspeed, + "upspeed", (json_int_t)peer->upspeed); + if (item) json_array_append_new(peer_list, item); + } + json_object_set_new(result, "peer_list", peer_list); + } json_object_set_new(result, "elapsed_seconds", json_real(task->stats.elapsed_seconds)); json_object_set_new(result, "pending_moves", diff --git a/apps/swarm/main.c b/apps/swarm/main.c index 6bc1304..ae39c64 100644 --- a/apps/swarm/main.c +++ b/apps/swarm/main.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -51,12 +52,16 @@ typedef struct { typedef struct { int fd; + naut_peer_addr addr; char name[40]; + uint8_t peer_id[20]; + bool have_peer_id; uint8_t *rbuf; size_t rcap, rlen; bool hs_done, peer_choking; naut_bitfield have; req_t *inflight; size_t nflight, cflight; uint64_t blocks_received; + uint64_t bytes_received; naut_ext_handshake extensions; uint64_t pex_received; naut_pipeline pipeline; @@ -121,11 +126,77 @@ static void on_piece_complete(void *opaque, uint32_t index) { emit_event(config, NAUT_EVENT_PIECE_COMPLETE, index, NULL, NULL); } +static void peer_client_label(const peer_t *peer, char out[64]) { + if (!peer || !peer->have_peer_id) { + snprintf(out, 64, "Unknown"); + return; + } + if (peer->peer_id[0] == '-' && peer->peer_id[7] == '-') { + char code[3] = { + (char)peer->peer_id[1], + (char)peer->peer_id[2], + 0, + }; + char version[5]; + memcpy(version, peer->peer_id + 3, 4); + version[4] = 0; + for (size_t i = 0; i < sizeof version - 1; i++) + if (!isprint((unsigned char)version[i])) version[i] = '?'; + snprintf(out, 64, "%s %s", code, version); + return; + } + char id[21]; + memcpy(id, peer->peer_id, sizeof peer->peer_id); + id[20] = 0; + for (size_t i = 0; i < sizeof id - 1; i++) + if (!isprint((unsigned char)id[i])) id[i] = '.'; + snprintf(out, 64, "%s", id); +} + +static void peer_flags(const peer_t *peer, char out[16]) { + size_t n = 0; + if (peer && !peer->peer_choking && n + 1 < 16) out[n++] = 'D'; + if (peer && (peer->extensions.ut_pex || peer->pex_received) && n + 1 < 16) + out[n++] = 'X'; + out[n] = 0; +} + +static void snapshot_peer_stats(naut_swarm_stats *stats, + const peer_t *peers, int npeers, + uint32_t total_pieces) { + if (!stats || !peers || npeers <= 0) return; + for (int i = 0; i < npeers && + stats->peer_count < NAUT_SWARM_MAX_PEER_STATS; i++) { + const peer_t *peer = &peers[i]; + if (peer->dead || !peer->hs_done) continue; + naut_swarm_peer_stats *out = + &stats->peer_stats[stats->peer_count++]; + snprintf(out->ip, sizeof out->ip, "%u.%u.%u.%u", + peer->addr.ip[0], peer->addr.ip[1], + peer->addr.ip[2], peer->addr.ip[3]); + out->port = peer->addr.port; + peer_client_label(peer, out->client); + snprintf(out->connection, sizeof out->connection, "TCP"); + peer_flags(peer, out->flags); + size_t have = peer->have.words ? naut_bitfield_count(&peer->have) : 0; + double ratio = total_pieces + ? (double)have / (double)total_pieces : 0.0; + if (ratio > 1.0) ratio = 1.0; + out->progress = ratio; + out->relevance = ratio; + out->dlspeed = peer->pipeline.bytes_per_second; + out->upspeed = 0.0; + out->downloaded = peer->bytes_received; + out->uploaded = 0; + } +} + 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, + const peer_t *peers, int npeers, double started_at) { if (!config->on_progress) return; naut_swarm_stats stats = { @@ -139,6 +210,11 @@ static void report_progress(const naut_swarm_config *config, .peers_failed = peers_failed, .elapsed_seconds = now() - started_at, }; + snapshot_peer_stats(&stats, peers, npeers, metainfo->num_pieces); + if (download) { + stats.piece_state_count = (uint32_t)naut_download_piece_states( + download, stats.piece_states, NAUT_SWARM_MAX_PIECE_STATS); + } config->on_progress(config->context, &stats); } @@ -476,6 +552,7 @@ 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; + peer->addr = endpoint->addr; snprintf(peer->name, sizeof peer->name, "%s", endpoint->name); peer->peer_choking = true; naut_pipeline_init(&peer->pipeline, NAUT_BLOCK, 4, 1024, 32); @@ -580,6 +657,8 @@ static naut_err peer_process(naut_download *d, const naut_metainfo *mi, p->dead = true; return NAUT_OK; } + memcpy(p->peer_id, pid, sizeof p->peer_id); + p->have_peer_id = true; pos = NAUT_HANDSHAKE_LEN; p->hs_done = true; } @@ -642,6 +721,7 @@ static naut_err peer_process(naut_download *d, const naut_metainfo *mi, (uint32_t)m.payload_len, &pdone); if (e == NAUT_OK) { p->blocks_received++; + p->bytes_received += m.payload_len; cancel_block(d, peers, npeers, p, m.index, m.begin); } else if (e == NAUT_ERR_PROTO) { if (expected) cancel_piece(d, peers, npeers, m.index); @@ -837,7 +917,8 @@ naut_err naut_swarm_run(const naut_swarm_config *config) { 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); + report_progress(config, d, &mi, (uint32_t)npeers, 0, 0, 0, + peers, npeers, t0); for (int i = 0; i < npeers; i++) { bool connected = false; int fd = connect_start(&endpoints[i], &connected); @@ -871,7 +952,7 @@ naut_err naut_swarm_run(const naut_swarm_config *config) { while (connecting > 0 && now() < connect_deadline && !stop_requested(config)) { report_progress(config, d, &mi, (uint32_t)npeers, connecting, - active, failed, t0); + active, failed, peers, npeers, t0); int ready = poll(connect_fds, (nfds_t)neps, 100); if (ready < 0) { if (errno == EINTR) continue; @@ -921,7 +1002,8 @@ naut_err naut_swarm_run(const naut_swarm_config *config) { active, npeers, mi.num_pieces, (long long)mi.total_length); emit_event(config, NAUT_EVENT_TORRENT_ADDED, 0, NULL, NULL); - report_progress(config, d, &mi, (uint32_t)npeers, 0, active, failed, t0); + report_progress(config, d, &mi, (uint32_t)npeers, 0, active, failed, + peers, npeers, t0); while (!naut_download_complete(d) && run_error == NAUT_OK) { service_control(config, st); if (stop_requested(config)) { @@ -951,7 +1033,8 @@ naut_err naut_swarm_run(const naut_swarm_config *config) { } report_progress(config, d, &mi, (uint32_t)npeers, 0, (uint32_t)live_peers, - (uint32_t)npeers - (uint32_t)live_peers, t0); + (uint32_t)npeers - (uint32_t)live_peers, + peers, npeers, t0); int r = poll(pfd, nf, 200); if (r < 0) { if (errno == EINTR) continue; @@ -1007,7 +1090,7 @@ naut_err naut_swarm_run(const naut_swarm_config *config) { naut_download_pieces_done(d), mi.num_pieces, dt, mb/dt, naut_download_in_endgame(d) ? " (passed through endgame)" : ""); report_progress(config, d, &mi, (uint32_t)npeers, 0, 0, - (uint32_t)npeers, t0); + (uint32_t)npeers, peers, npeers, t0); emit_event(config, NAUT_EVENT_TORRENT_FINISHED, 0, NULL, NULL); while (config->keep_alive && !stop_requested(config)) { service_control(config, st); diff --git a/include/naut/piece.h b/include/naut/piece.h index 5aae65d..b4423b9 100644 --- a/include/naut/piece.h +++ b/include/naut/piece.h @@ -87,5 +87,7 @@ bool naut_download_complete(const naut_download *d); uint32_t naut_download_num_pieces(const naut_download *d); uint32_t naut_download_pieces_done(const naut_download *d); uint64_t naut_download_bytes_done(const naut_download *d); +size_t naut_download_piece_states(const naut_download *d, uint8_t *out, + size_t capacity); #endif /* NAUT_PIECE_H */ diff --git a/include/naut/swarm.h b/include/naut/swarm.h index 995e1b0..952c80e 100644 --- a/include/naut/swarm.h +++ b/include/naut/swarm.h @@ -11,6 +11,23 @@ #include "naut/event.h" #include "naut/storage.h" +#define NAUT_SWARM_MAX_PEER_STATS 64 +#define NAUT_SWARM_MAX_PIECE_STATS 4000 + +typedef struct { + char ip[46]; + uint16_t port; + char client[64]; + char connection[16]; + char flags[16]; + double progress; + double relevance; + double dlspeed; + double upspeed; + uint64_t downloaded; + uint64_t uploaded; +} naut_swarm_peer_stats; + typedef struct { uint64_t total_bytes; uint64_t bytes_done; @@ -21,6 +38,10 @@ typedef struct { uint32_t peers_active; uint32_t peers_failed; double elapsed_seconds; + uint32_t peer_count; + naut_swarm_peer_stats peer_stats[NAUT_SWARM_MAX_PEER_STATS]; + uint32_t piece_state_count; + uint8_t piece_states[NAUT_SWARM_MAX_PIECE_STATS]; } naut_swarm_stats; typedef void (*naut_swarm_progress_cb)(void *context, diff --git a/plugins/webui/webui.c b/plugins/webui/webui.c index 9e87c6d..817c1c0 100644 --- a/plugins/webui/webui.c +++ b/plugins/webui/webui.c @@ -442,6 +442,12 @@ static uint64_t json_u64(const json_t *obj, const char *key) { ? (uint64_t)json_integer_value(value) : 0; } +static double json_number_or(const json_t *obj, const char *key, + double fallback) { + json_t *value = json_object_get(obj, key); + return json_is_number(value) ? json_number_value(value) : fallback; +} + static const char *base_name(const char *path) { if (!path || !*path) return "torrent"; const char *slash = strrchr(path, '/'); @@ -568,6 +574,33 @@ static json_t *tracker_hosts(json_t *trackers) { return hosts; } +static json_t *map_peer_list(json_t *torrent) { + json_t *out = json_array(); + json_t *peers = json_object_get(torrent, "peer_list"); + if (!out || !json_is_array(peers)) return out; + size_t index; + json_t *peer; + json_array_foreach(peers, index, peer) { + if (!json_is_object(peer)) continue; + json_t *item = json_pack( + "{s:s,s:s,s:i,s:s,s:s,s:s,s:f,s:i,s:i,s:I,s:I,s:f}", + "country", "", + "ip", json_string_or(peer, "ip", ""), + "port", (int)json_u64(peer, "port"), + "client", json_string_or(peer, "client", "Unknown"), + "connection", json_string_or(peer, "connection", "TCP"), + "flags", json_string_or(peer, "flags", ""), + "progress", json_number_or(peer, "progress", 0.0), + "dlspeed", (int)json_u64(peer, "dlspeed"), + "upspeed", (int)json_u64(peer, "upspeed"), + "downloaded", (json_int_t)json_u64(peer, "downloaded"), + "uploaded", (json_int_t)json_u64(peer, "uploaded"), + "relevance", json_number_or(peer, "relevance", 0.0)); + if (item) json_array_append_new(out, item); + } + return out; +} + static double torrent_progress_ratio(json_t *torrent, uint64_t *done_out, uint64_t *total_out) { uint64_t done = json_u64(torrent, "bytes_done"); @@ -781,7 +814,7 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) { "{s:s,s:I,s:f,s:i,s:f}", "name", name, "size", (json_int_t)total, "progress", progress, "priority", 1, "availability", 1.0)); - peers_list = json_array(); + peers_list = map_peer_list(torrent); hosts = tracker_hosts(trackers); } @@ -844,6 +877,11 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) { json_string(json_string_or(torrent, "source", ""))); json_object_set_new(out, "pieceCount", json_integer((json_int_t)pieces)); json_object_set_new(out, "piecesDone", json_integer((json_int_t)pieces_done)); + json_t *piece_states = json_object_get(torrent, "piece_states"); + json_object_set_new(out, "pieceStates", + json_is_array(piece_states) + ? json_deep_copy(piece_states) + : json_array()); json_object_set_new(out, "trackers", trackers ? trackers : json_array()); json_object_set_new(out, "peersList", peers_list ? peers_list : json_array()); json_object_set_new(out, "files", files ? files : json_array()); @@ -1056,9 +1094,12 @@ static void api_torrent_detail(int fd, const char *tail) { } else if (strcmp(tab, "pieces") == 0) { uint64_t count = json_u64(torrent, "pieceCount"); uint64_t done = json_u64(torrent, "piecesDone"); - json_t *pieces = json_array(); - for (uint64_t i = 0; pieces && i < count && i < 4000; i++) - json_array_append_new(pieces, json_integer(i < done ? 2 : 0)); + json_t *states = json_object_get(torrent, "pieceStates"); + json_t *pieces = json_is_array(states) ? json_deep_copy(states) + : json_array(); + if (pieces && json_array_size(pieces) == 0) + for (uint64_t i = 0; i < count && i < 4000; i++) + json_array_append_new(pieces, json_integer(i < done ? 2 : 0)); json_t *value = json_pack("{s:I,s:I,s:o}", "pieceSize", json_u64(torrent, "pieceSize"), "pieceCount", count, "pieces", pieces); @@ -1085,9 +1126,15 @@ static void api_add(int fd, const char *body, size_t len) { if (!save_path || !*save_path) save_path = "."; if (!source) source = magnet; if ((!source || !*source) && (!data || !*data)) { + /* A request carrying only parsed metadata (name/files) but no bytes is + * the tell-tale of a stale UI that predates base64 upload support. */ + bool looks_stale = json_object_get(req, "name") || + json_object_get(req, "files"); json_decref(req); - http_text(fd, 400, "Bad Request", - "torrent-ui must send a magnet, source, or torrent data"); + http_text(fd, 400, "Bad Request", looks_stale + ? "no torrent bytes in request: the page is running an old UI. " + "Hard-reload the browser (Ctrl+Shift+R) and add the file again." + : "send a magnet, a source path, or uploaded torrent bytes"); return; } json_t *params = json_object(); diff --git a/src/piece/piece.c b/src/piece/piece.c index ca49eb4..1b365d3 100644 --- a/src/piece/piece.c +++ b/src/piece/piece.c @@ -391,3 +391,17 @@ bool naut_download_in_endgame(const naut_download *d) { return d->endgame; } uint32_t naut_download_num_pieces(const naut_download *d) { return d->num_pieces; } uint32_t naut_download_pieces_done(const naut_download *d) { return d->pieces_done; } uint64_t naut_download_bytes_done(const naut_download *d) { return d->bytes_done; } +size_t naut_download_piece_states(const naut_download *d, uint8_t *out, + size_t capacity) { + if (!d || !out || capacity == 0) return 0; + size_t count = NAUT_MIN((size_t)d->num_pieces, capacity); + for (size_t i = 0; i < count; i++) { + if (naut_bitfield_test(&d->have, i)) + out[i] = 2; + else if (d->ps[i]) + out[i] = 1; + else + out[i] = 0; + } + return count; +}