nautd: hash-check paused torrents (check-only worker)

A paused torrent never ran a worker, so it never verified on-disk data
and showed no progress. Add a one-shot check-only swarm mode (open +
resume scan + report, no peers/engine/download). The reconciler runs it
for a paused torrent flagged needs_check (set on paused-add and recheck);
the worker stays paused afterward. New TORRENT_CHECKING state -> webui
checkingDL/UP. Mark #11 done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-22 01:22:10 -04:00
parent 7bbc1ee22c
commit 4708db151d
5 changed files with 80 additions and 25 deletions

View file

@ -8,5 +8,5 @@
- ✅ Categories aren't saved across restart.
- ✅ Pausing a torrent will go back into Downloading and Seeding.
- ✅ A torrents data could overlap with another existing torrent. This should be blocked to avoid
- A paused torrent should still do a full piece check.
- A paused torrent should still do a full piece check.
- ✅ Something appears to have broken the peers info tab, nothing shows up.

View file

@ -47,6 +47,7 @@ typedef enum {
TORRENT_STOPPED,
TORRENT_ERROR,
TORRENT_PAUSED,
TORRENT_CHECKING,
} torrent_state;
typedef struct daemon_state daemon_state;
@ -73,6 +74,8 @@ typedef struct {
bool paused; /* user-paused: never auto-activated (persisted) */
bool force_start; /* bypass the queue cap (persisted) */
bool restart_requested; /* one-shot stop->start (recheck) */
bool needs_check; /* run a one-shot hash check (paused add/recheck) */
bool checking; /* a check-only worker is currently running */
int queue_pos; /* ordering within the download queue (persisted) */
uint64_t rate_share; /* engine download cap for this torrent, bytes/sec */
naut_swarm_stats stats;
@ -165,6 +168,7 @@ static const char *torrent_state_name(torrent_state state) {
[TORRENT_STOPPED] = "stopped",
[TORRENT_ERROR] = "error",
[TORRENT_PAUSED] = "paused",
[TORRENT_CHECKING] = "checking",
};
return (size_t)state < NAUT_ARRAY_LEN(names) ? names[state] : "unknown";
}
@ -179,6 +183,9 @@ static void torrent_progress(void *opaque, const naut_swarm_stats *stats) {
torrent_task *task = opaque;
pthread_mutex_lock(&task->lock);
task->stats = *stats;
/* A check-only run just reports verified progress; the worker decides the
* final state (it stays paused), so don't flip it to complete/running here. */
if (!task->checking) {
if (stats->total_pieces > 0 &&
stats->pieces_done == stats->total_pieces)
task->state = TORRENT_COMPLETE;
@ -187,6 +194,7 @@ static void torrent_progress(void *opaque, const naut_swarm_stats *stats) {
else if (task->state == TORRENT_QUEUED ||
task->state == TORRENT_STALLED)
task->state = TORRENT_RUNNING;
}
pthread_mutex_unlock(&task->lock);
}
@ -508,7 +516,8 @@ static void *torrent_worker(void *opaque) {
naut_swarm_file_location *locations = NULL;
size_t num_locations = 0;
pthread_mutex_lock(&task->lock);
task->state = TORRENT_RUNNING;
bool check_only = task->checking;
task->state = check_only ? TORRENT_CHECKING : TORRENT_RUNNING;
if (task->num_locations &&
(locations = calloc(task->num_locations, sizeof *locations))) {
for (size_t i = 0; i < task->num_locations; i++) {
@ -531,6 +540,7 @@ static void *torrent_worker(void *opaque) {
.torrent_id = task->id,
.events = task->daemon->events,
.keep_alive = true,
.check_only = check_only,
.on_progress = torrent_progress,
.on_control = torrent_control,
.should_stop = torrent_should_stop,
@ -546,16 +556,23 @@ static void *torrent_worker(void *opaque) {
pthread_mutex_lock(&task->lock);
task->result = result;
if (check_only) {
/* One-shot hash check finished: progress is recorded; return to the
* paused state (or queued if the user resumed mid-check). */
task->checking = false;
task->needs_check = false;
task->state = task->paused ? TORRENT_PAUSED : TORRENT_QUEUED;
} else if (task->stop_requested) {
/* A requested stop wins over the run result: a completed torrent returns
* NAUT_OK even when paused/stopped, and marking it COMPLETE would make the
* lifecycle reconciler immediately relaunch its keep-alive worker (clearing
* `paused`) i.e. pause wouldn't stick for seeding torrents. */
if (task->stop_requested)
* NAUT_OK even when paused/stopped, and marking it COMPLETE would make
* the lifecycle reconciler immediately relaunch its keep-alive worker
* (clearing `paused`) i.e. pause wouldn't stick for seeding torrents. */
task->state = task->paused ? TORRENT_PAUSED : TORRENT_STOPPED;
else if (result == NAUT_OK)
} else if (result == NAUT_OK) {
task->state = TORRENT_COMPLETE;
else
} else {
task->state = TORRENT_ERROR;
}
task->thread_done = true;
pthread_mutex_unlock(&task->lock);
return NULL;
@ -1493,6 +1510,9 @@ static torrent_task *spawn_torrent(daemon_state *state, const char *source,
task->state = start_paused ? TORRENT_PAUSED : TORRENT_QUEUED;
task->result = NAUT_ERR_AGAIN;
task->paused = start_paused;
/* A paused torrent never runs a download worker, so hash-check its data once
* (via a check-only worker) to report accurate progress. */
task->needs_check = start_paused;
task->force_start = force_start;
task->source = strdup(source);
task->source_is_temp = source_is_temp;
@ -1815,8 +1835,13 @@ static void apply_resume(torrent_task *task, const json_t *params) {
static void apply_recheck(torrent_task *task, const json_t *params) {
(void)params;
/* Force a stop->start cycle; a fresh run re-hashes via the resume scan. */
task->paused = false;
if (task->paused) {
/* Recheck a paused torrent: hash-verify in place and stay paused (a
* check-only worker runs via the reconciler). */
task->needs_check = true;
return;
}
/* Active torrent: stop->start so the fresh run re-hashes via the resume scan. */
task->restart_requested = true;
if (!task->thread_done) {
task->stop_requested = true;
@ -2312,7 +2337,9 @@ static bool register_commands(daemon_state *state) {
/* (Re)start a torrent's worker. The task must have no live worker
* (thread_done). Joins any prior thread first. Main thread only. */
static bool start_worker(torrent_task *task) {
/* Start the worker. `check` => a one-shot hash-check pass that keeps the torrent
* paused (no download); otherwise a normal download run (clears paused). */
static bool start_worker_ex(torrent_task *task, bool check) {
if (task->thread_started) {
pthread_join(task->thread, NULL);
task->thread_started = false;
@ -2321,14 +2348,16 @@ static bool start_worker(torrent_task *task) {
task->stop_requested = false;
task->restart_requested = false;
task->thread_done = false;
task->paused = false;
task->checking = check;
if (!check) task->paused = false;
task->result = NAUT_ERR_AGAIN;
task->state = TORRENT_RUNNING;
task->state = check ? TORRENT_CHECKING : TORRENT_RUNNING;
pthread_mutex_unlock(&task->lock);
if (pthread_create(&task->thread, NULL, torrent_worker, task) != 0) {
pthread_mutex_lock(&task->lock);
task->state = TORRENT_ERROR;
task->thread_done = true;
task->checking = false;
pthread_mutex_unlock(&task->lock);
return false;
}
@ -2336,6 +2365,10 @@ static bool start_worker(torrent_task *task) {
return true;
}
static bool start_worker(torrent_task *task) {
return start_worker_ex(task, false);
}
/* Ask a running worker to stop; it exits asynchronously (revisited next tick). */
static void request_stop(torrent_task *task) {
pthread_mutex_lock(&task->lock);
@ -2346,7 +2379,7 @@ static void request_stop(torrent_task *task) {
pthread_mutex_unlock(&task->lock);
}
typedef enum { ACT_NONE, ACT_START, ACT_STOP, ACT_SETSTATE } lifecycle_act;
typedef enum { ACT_NONE, ACT_START, ACT_STOP, ACT_SETSTATE, ACT_CHECK } lifecycle_act;
/* Reconcile desired vs actual run-state for every torrent: enforce the
* max-active download queue, honor pause/force-start, run recheck restarts, and
@ -2416,12 +2449,15 @@ static void service_lifecycle(daemon_state *state) {
bool removed = t->remove_requested;
bool started = t->thread_started, done = t->thread_done;
bool stopping = t->stop_requested, restart = t->restart_requested;
bool paused = t->paused;
bool paused = t->paused, checking = t->checking;
bool needs_check = t->needs_check;
torrent_state st = t->state;
pthread_mutex_unlock(&t->lock);
if (removed) continue;
bool running = started && !done;
if (restart) {
if (checking && running) {
act[i] = ACT_NONE; /* let the one-shot hash check finish */
} else if (restart) {
if (running && !stopping) act[i] = ACT_STOP;
else if (done) act[i] = ACT_START;
} else if (want_run[i]) {
@ -2429,6 +2465,8 @@ static void service_lifecycle(daemon_state *state) {
} else {
if (running && !stopping) {
act[i] = ACT_STOP;
} else if (done && paused && needs_check) {
act[i] = ACT_CHECK; /* verify a paused torrent's data */
} else if (done) {
int want = paused ? TORRENT_PAUSED : TORRENT_QUEUED;
if ((int)st != want) { act[i] = ACT_SETSTATE; target[i] = want; }
@ -2441,6 +2479,7 @@ static void service_lifecycle(daemon_state *state) {
for (size_t i = 0; i < n; i++) {
switch (act[i]) {
case ACT_START: start_worker(tasks[i]); break;
case ACT_CHECK: start_worker_ex(tasks[i], true); break;
case ACT_STOP: request_stop(tasks[i]); break;
case ACT_SETSTATE:
pthread_mutex_lock(&tasks[i]->lock);

View file

@ -713,6 +713,18 @@ naut_err naut_swarm_run(const naut_swarm_config *config) {
naut_download_pieces_done(d), mi.num_pieces,
(unsigned long long)resumed_left);
/* Check-only: the resume scan above already hash-verified every piece on
* disk. Report the result and stop no peers, no engine, no download. */
if (config->check_only) {
report_progress(config, NULL, 0, d, &mi, tracker_stats, tracker_count,
now());
naut_download_destroy(d);
naut_storage_close(st);
naut_metainfo_free(&mi);
free(endpoints);
return NAUT_OK;
}
if (!from_magnet && config->num_peers == 0 && !naut_download_complete(d)) {
if (!discover_trackers(mi.infohash_v1, (uint64_t)mi.total_length,
mi.trackers, mi.num_trackers,

View file

@ -109,6 +109,8 @@ typedef struct {
uint64_t torrent_id;
naut_event_bus *events; /* optional */
bool keep_alive; /* retain completed storage until stopped */
bool check_only; /* hash-verify existing data + report, then return;
* no peers, no engine, no download (paused recheck)*/
naut_swarm_progress_cb on_progress;
naut_swarm_control_cb on_control;
naut_swarm_stop_cb should_stop;

View file

@ -499,6 +499,8 @@ static const char *ui_state(const char *state, double progress) {
if (strcmp(state, "stopped") == 0)
return progress >= 1.0 ? "pausedUP" : "pausedDL";
if (strcmp(state, "stopping") == 0) return "pausedDL";
if (strcmp(state, "checking") == 0)
return progress >= 1.0 ? "checkingUP" : "checkingDL";
if (strcmp(state, "stalled") == 0)
return progress >= 1.0 ? "stalledUP" : "stalledDL";
if (strcmp(state, "queued") == 0) return "queuedDL";