Expose peer and piece state to web UI

This commit is contained in:
ookami125 2026-06-17 01:29:44 -04:00
parent f4f4e86be4
commit 6ae72907b0
6 changed files with 213 additions and 11 deletions

View file

@ -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",

View file

@ -26,6 +26,7 @@
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -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);

View file

@ -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 */

View file

@ -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,

View file

@ -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,8 +1094,11 @@ 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_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"),
@ -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();

View file

@ -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;
}