diff --git a/ISSUES.md b/ISSUES.md index efac3a8..65317ed 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -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. \ No newline at end of file diff --git a/apps/nautd/main.c b/apps/nautd/main.c index 5b4c3c5..990d5d4 100644 --- a/apps/nautd/main.c +++ b/apps/nautd/main.c @@ -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); diff --git a/apps/swarm/main.c b/apps/swarm/main.c index 45a062b..755320f 100644 --- a/apps/swarm/main.c +++ b/apps/swarm/main.c @@ -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, diff --git a/include/naut/swarm.h b/include/naut/swarm.h index a14bd18..dab0b3b 100644 --- a/include/naut/swarm.h +++ b/include/naut/swarm.h @@ -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; diff --git a/plugins/webui/webui.c b/plugins/webui/webui.c index 1393068..74ac401 100644 --- a/plugins/webui/webui.c +++ b/plugins/webui/webui.c @@ -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";