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.
|
- ✅ Categories aren't saved across restart.
|
||||||
- ✅ Pausing a torrent will go back into Downloading and Seeding.
|
- ✅ 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 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.
|
- ✅ Something appears to have broken the peers info tab, nothing shows up.
|
||||||
|
|
@ -47,6 +47,7 @@ typedef enum {
|
||||||
TORRENT_STOPPED,
|
TORRENT_STOPPED,
|
||||||
TORRENT_ERROR,
|
TORRENT_ERROR,
|
||||||
TORRENT_PAUSED,
|
TORRENT_PAUSED,
|
||||||
|
TORRENT_CHECKING,
|
||||||
} torrent_state;
|
} torrent_state;
|
||||||
|
|
||||||
typedef struct daemon_state daemon_state;
|
typedef struct daemon_state daemon_state;
|
||||||
|
|
@ -73,6 +74,8 @@ typedef struct {
|
||||||
bool paused; /* user-paused: never auto-activated (persisted) */
|
bool paused; /* user-paused: never auto-activated (persisted) */
|
||||||
bool force_start; /* bypass the queue cap (persisted) */
|
bool force_start; /* bypass the queue cap (persisted) */
|
||||||
bool restart_requested; /* one-shot stop->start (recheck) */
|
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) */
|
int queue_pos; /* ordering within the download queue (persisted) */
|
||||||
uint64_t rate_share; /* engine download cap for this torrent, bytes/sec */
|
uint64_t rate_share; /* engine download cap for this torrent, bytes/sec */
|
||||||
naut_swarm_stats stats;
|
naut_swarm_stats stats;
|
||||||
|
|
@ -165,6 +168,7 @@ static const char *torrent_state_name(torrent_state state) {
|
||||||
[TORRENT_STOPPED] = "stopped",
|
[TORRENT_STOPPED] = "stopped",
|
||||||
[TORRENT_ERROR] = "error",
|
[TORRENT_ERROR] = "error",
|
||||||
[TORRENT_PAUSED] = "paused",
|
[TORRENT_PAUSED] = "paused",
|
||||||
|
[TORRENT_CHECKING] = "checking",
|
||||||
};
|
};
|
||||||
return (size_t)state < NAUT_ARRAY_LEN(names) ? names[state] : "unknown";
|
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;
|
torrent_task *task = opaque;
|
||||||
pthread_mutex_lock(&task->lock);
|
pthread_mutex_lock(&task->lock);
|
||||||
task->stats = *stats;
|
task->stats = *stats;
|
||||||
if (stats->total_pieces > 0 &&
|
/* A check-only run just reports verified progress; the worker decides the
|
||||||
stats->pieces_done == stats->total_pieces)
|
* final state (it stays paused), so don't flip it to complete/running here. */
|
||||||
task->state = TORRENT_COMPLETE;
|
if (!task->checking) {
|
||||||
else if (stats->stalled)
|
if (stats->total_pieces > 0 &&
|
||||||
task->state = TORRENT_STALLED;
|
stats->pieces_done == stats->total_pieces)
|
||||||
else if (task->state == TORRENT_QUEUED ||
|
task->state = TORRENT_COMPLETE;
|
||||||
task->state == TORRENT_STALLED)
|
else if (stats->stalled)
|
||||||
task->state = TORRENT_RUNNING;
|
task->state = TORRENT_STALLED;
|
||||||
|
else if (task->state == TORRENT_QUEUED ||
|
||||||
|
task->state == TORRENT_STALLED)
|
||||||
|
task->state = TORRENT_RUNNING;
|
||||||
|
}
|
||||||
pthread_mutex_unlock(&task->lock);
|
pthread_mutex_unlock(&task->lock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -508,7 +516,8 @@ static void *torrent_worker(void *opaque) {
|
||||||
naut_swarm_file_location *locations = NULL;
|
naut_swarm_file_location *locations = NULL;
|
||||||
size_t num_locations = 0;
|
size_t num_locations = 0;
|
||||||
pthread_mutex_lock(&task->lock);
|
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 &&
|
if (task->num_locations &&
|
||||||
(locations = calloc(task->num_locations, sizeof *locations))) {
|
(locations = calloc(task->num_locations, sizeof *locations))) {
|
||||||
for (size_t i = 0; i < task->num_locations; i++) {
|
for (size_t i = 0; i < task->num_locations; i++) {
|
||||||
|
|
@ -531,6 +540,7 @@ static void *torrent_worker(void *opaque) {
|
||||||
.torrent_id = task->id,
|
.torrent_id = task->id,
|
||||||
.events = task->daemon->events,
|
.events = task->daemon->events,
|
||||||
.keep_alive = true,
|
.keep_alive = true,
|
||||||
|
.check_only = check_only,
|
||||||
.on_progress = torrent_progress,
|
.on_progress = torrent_progress,
|
||||||
.on_control = torrent_control,
|
.on_control = torrent_control,
|
||||||
.should_stop = torrent_should_stop,
|
.should_stop = torrent_should_stop,
|
||||||
|
|
@ -546,16 +556,23 @@ static void *torrent_worker(void *opaque) {
|
||||||
|
|
||||||
pthread_mutex_lock(&task->lock);
|
pthread_mutex_lock(&task->lock);
|
||||||
task->result = result;
|
task->result = result;
|
||||||
/* A requested stop wins over the run result: a completed torrent returns
|
if (check_only) {
|
||||||
* NAUT_OK even when paused/stopped, and marking it COMPLETE would make the
|
/* One-shot hash check finished: progress is recorded; return to the
|
||||||
* lifecycle reconciler immediately relaunch its keep-alive worker (clearing
|
* paused state (or queued if the user resumed mid-check). */
|
||||||
* `paused`) — i.e. pause wouldn't stick for seeding torrents. */
|
task->checking = false;
|
||||||
if (task->stop_requested)
|
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;
|
task->state = task->paused ? TORRENT_PAUSED : TORRENT_STOPPED;
|
||||||
else if (result == NAUT_OK)
|
} else if (result == NAUT_OK) {
|
||||||
task->state = TORRENT_COMPLETE;
|
task->state = TORRENT_COMPLETE;
|
||||||
else
|
} else {
|
||||||
task->state = TORRENT_ERROR;
|
task->state = TORRENT_ERROR;
|
||||||
|
}
|
||||||
task->thread_done = true;
|
task->thread_done = true;
|
||||||
pthread_mutex_unlock(&task->lock);
|
pthread_mutex_unlock(&task->lock);
|
||||||
return NULL;
|
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->state = start_paused ? TORRENT_PAUSED : TORRENT_QUEUED;
|
||||||
task->result = NAUT_ERR_AGAIN;
|
task->result = NAUT_ERR_AGAIN;
|
||||||
task->paused = start_paused;
|
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->force_start = force_start;
|
||||||
task->source = strdup(source);
|
task->source = strdup(source);
|
||||||
task->source_is_temp = source_is_temp;
|
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) {
|
static void apply_recheck(torrent_task *task, const json_t *params) {
|
||||||
(void)params;
|
(void)params;
|
||||||
/* Force a stop->start cycle; a fresh run re-hashes via the resume scan. */
|
if (task->paused) {
|
||||||
task->paused = false;
|
/* 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;
|
task->restart_requested = true;
|
||||||
if (!task->thread_done) {
|
if (!task->thread_done) {
|
||||||
task->stop_requested = true;
|
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
|
/* (Re)start a torrent's worker. The task must have no live worker
|
||||||
* (thread_done). Joins any prior thread first. Main thread only. */
|
* (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) {
|
if (task->thread_started) {
|
||||||
pthread_join(task->thread, NULL);
|
pthread_join(task->thread, NULL);
|
||||||
task->thread_started = false;
|
task->thread_started = false;
|
||||||
|
|
@ -2321,14 +2348,16 @@ static bool start_worker(torrent_task *task) {
|
||||||
task->stop_requested = false;
|
task->stop_requested = false;
|
||||||
task->restart_requested = false;
|
task->restart_requested = false;
|
||||||
task->thread_done = false;
|
task->thread_done = false;
|
||||||
task->paused = false;
|
task->checking = check;
|
||||||
|
if (!check) task->paused = false;
|
||||||
task->result = NAUT_ERR_AGAIN;
|
task->result = NAUT_ERR_AGAIN;
|
||||||
task->state = TORRENT_RUNNING;
|
task->state = check ? TORRENT_CHECKING : TORRENT_RUNNING;
|
||||||
pthread_mutex_unlock(&task->lock);
|
pthread_mutex_unlock(&task->lock);
|
||||||
if (pthread_create(&task->thread, NULL, torrent_worker, task) != 0) {
|
if (pthread_create(&task->thread, NULL, torrent_worker, task) != 0) {
|
||||||
pthread_mutex_lock(&task->lock);
|
pthread_mutex_lock(&task->lock);
|
||||||
task->state = TORRENT_ERROR;
|
task->state = TORRENT_ERROR;
|
||||||
task->thread_done = true;
|
task->thread_done = true;
|
||||||
|
task->checking = false;
|
||||||
pthread_mutex_unlock(&task->lock);
|
pthread_mutex_unlock(&task->lock);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -2336,6 +2365,10 @@ static bool start_worker(torrent_task *task) {
|
||||||
return true;
|
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). */
|
/* Ask a running worker to stop; it exits asynchronously (revisited next tick). */
|
||||||
static void request_stop(torrent_task *task) {
|
static void request_stop(torrent_task *task) {
|
||||||
pthread_mutex_lock(&task->lock);
|
pthread_mutex_lock(&task->lock);
|
||||||
|
|
@ -2346,7 +2379,7 @@ static void request_stop(torrent_task *task) {
|
||||||
pthread_mutex_unlock(&task->lock);
|
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
|
/* Reconcile desired vs actual run-state for every torrent: enforce the
|
||||||
* max-active download queue, honor pause/force-start, run recheck restarts, and
|
* 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 removed = t->remove_requested;
|
||||||
bool started = t->thread_started, done = t->thread_done;
|
bool started = t->thread_started, done = t->thread_done;
|
||||||
bool stopping = t->stop_requested, restart = t->restart_requested;
|
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;
|
torrent_state st = t->state;
|
||||||
pthread_mutex_unlock(&t->lock);
|
pthread_mutex_unlock(&t->lock);
|
||||||
if (removed) continue;
|
if (removed) continue;
|
||||||
bool running = started && !done;
|
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;
|
if (running && !stopping) act[i] = ACT_STOP;
|
||||||
else if (done) act[i] = ACT_START;
|
else if (done) act[i] = ACT_START;
|
||||||
} else if (want_run[i]) {
|
} else if (want_run[i]) {
|
||||||
|
|
@ -2429,6 +2465,8 @@ static void service_lifecycle(daemon_state *state) {
|
||||||
} else {
|
} else {
|
||||||
if (running && !stopping) {
|
if (running && !stopping) {
|
||||||
act[i] = ACT_STOP;
|
act[i] = ACT_STOP;
|
||||||
|
} else if (done && paused && needs_check) {
|
||||||
|
act[i] = ACT_CHECK; /* verify a paused torrent's data */
|
||||||
} else if (done) {
|
} else if (done) {
|
||||||
int want = paused ? TORRENT_PAUSED : TORRENT_QUEUED;
|
int want = paused ? TORRENT_PAUSED : TORRENT_QUEUED;
|
||||||
if ((int)st != want) { act[i] = ACT_SETSTATE; target[i] = want; }
|
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++) {
|
for (size_t i = 0; i < n; i++) {
|
||||||
switch (act[i]) {
|
switch (act[i]) {
|
||||||
case ACT_START: start_worker(tasks[i]); break;
|
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_STOP: request_stop(tasks[i]); break;
|
||||||
case ACT_SETSTATE:
|
case ACT_SETSTATE:
|
||||||
pthread_mutex_lock(&tasks[i]->lock);
|
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,
|
naut_download_pieces_done(d), mi.num_pieces,
|
||||||
(unsigned long long)resumed_left);
|
(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 (!from_magnet && config->num_peers == 0 && !naut_download_complete(d)) {
|
||||||
if (!discover_trackers(mi.infohash_v1, (uint64_t)mi.total_length,
|
if (!discover_trackers(mi.infohash_v1, (uint64_t)mi.total_length,
|
||||||
mi.trackers, mi.num_trackers,
|
mi.trackers, mi.num_trackers,
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,8 @@ typedef struct {
|
||||||
uint64_t torrent_id;
|
uint64_t torrent_id;
|
||||||
naut_event_bus *events; /* optional */
|
naut_event_bus *events; /* optional */
|
||||||
bool keep_alive; /* retain completed storage until stopped */
|
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_progress_cb on_progress;
|
||||||
naut_swarm_control_cb on_control;
|
naut_swarm_control_cb on_control;
|
||||||
naut_swarm_stop_cb should_stop;
|
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)
|
if (strcmp(state, "stopped") == 0)
|
||||||
return progress >= 1.0 ? "pausedUP" : "pausedDL";
|
return progress >= 1.0 ? "pausedUP" : "pausedDL";
|
||||||
if (strcmp(state, "stopping") == 0) return "pausedDL";
|
if (strcmp(state, "stopping") == 0) return "pausedDL";
|
||||||
|
if (strcmp(state, "checking") == 0)
|
||||||
|
return progress >= 1.0 ? "checkingUP" : "checkingDL";
|
||||||
if (strcmp(state, "stalled") == 0)
|
if (strcmp(state, "stalled") == 0)
|
||||||
return progress >= 1.0 ? "stalledUP" : "stalledDL";
|
return progress >= 1.0 ? "stalledUP" : "stalledDL";
|
||||||
if (strcmp(state, "queued") == 0) return "queuedDL";
|
if (strcmp(state, "queued") == 0) return "queuedDL";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue