Expose peer and piece state to web UI
This commit is contained in:
parent
f4f4e86be4
commit
6ae72907b0
6 changed files with 213 additions and 11 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue