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:
parent
7bbc1ee22c
commit
4708db151d
5 changed files with 80 additions and 25 deletions
|
|
@ -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.
|
||||
|
|
@ -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,14 +183,18 @@ static void torrent_progress(void *opaque, const naut_swarm_stats *stats) {
|
|||
torrent_task *task = opaque;
|
||||
pthread_mutex_lock(&task->lock);
|
||||
task->stats = *stats;
|
||||
if (stats->total_pieces > 0 &&
|
||||
stats->pieces_done == stats->total_pieces)
|
||||
task->state = TORRENT_COMPLETE;
|
||||
else if (stats->stalled)
|
||||
task->state = TORRENT_STALLED;
|
||||
else if (task->state == TORRENT_QUEUED ||
|
||||
task->state == TORRENT_STALLED)
|
||||
task->state = TORRENT_RUNNING;
|
||||
/* 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;
|
||||
else if (stats->stalled)
|
||||
task->state = TORRENT_STALLED;
|
||||
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;
|
||||
/* 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)
|
||||
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. */
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue