lardon3d/src/project.c

1230 lines
49 KiB
C

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <lardon3d/image_catalog.h>
#include <lardon3d/image_view.h>
#include <lardon3d/import_task.h>
#include <lardon3d/project.h>
#include <lardon3d/project_db.h>
#include <lardon3d/task_checkpoint.h>
#include <lardon3d/task_queue.h>
enum {
MAX_CREATED_DIRECTORIES = 16,
INI_LINE_CAPACITY = 512,
};
typedef struct {
char paths[MAX_CREATED_DIRECTORIES][PATH_MAX];
size_t count;
} CreatedDirectories;
typedef struct {
char name[128];
char stable_id[LARDON3D_PROJECT_DB_ID_CAPACITY];
unsigned int version;
} ProjectMetadata;
static void set_status(Lardon3DAppState *state, const char *message) {
(void)snprintf(state->status_message, sizeof(state->status_message), "%s", message);
}
static void store_recovery_summary(Lardon3DAppState *state,
const Lardon3DProjectRecoverySummary *summary) {
state->recovery_inspected = summary ? summary->inspected : 0;
state->recovery_resumed = summary ? summary->resumed : 0;
state->recovery_skipped = summary ? summary->skipped : 0;
state->recovery_failed = summary ? summary->failed : 0;
state->recovery_published_not_durable = summary ? summary->published_not_durable : 0;
state->recovery_queue_full = summary && summary->queue_full;
}
static void set_database_status(Lardon3DAppState *state, Lardon3DProjectDb *database,
const char *open_error, const char *fallback) {
char detail[LARDON3D_PROJECT_DB_ERROR_CAPACITY] = "";
if (database) {
(void)lardon3d_project_db_last_error(database, detail);
} else if (open_error) {
(void)snprintf(detail, sizeof(detail), "%s", open_error);
}
(void)snprintf(state->status_message, sizeof(state->status_message), "Erreur project.db : %.220s",
detail[0] ? detail : fallback);
}
static void clear_catalog(Lardon3DAppState *state) {
lardon3d_image_view_destroy(state->image_view);
state->image_view = NULL;
lardon3d_image_catalog_destroy(state->image_catalog);
state->image_catalog = NULL;
}
static bool normalize_name(Lardon3DAppState *state, const char *input, char *output,
size_t output_size) {
if (!input) {
set_status(state, "Erreur : le nom du projet est vide.");
return false;
}
const char *start = input;
while (*start && isspace((unsigned char)*start)) {
++start;
}
const char *end = input + strlen(input);
while (end > start && isspace((unsigned char)end[-1])) {
--end;
}
size_t length = (size_t)(end - start);
if (length == 0) {
set_status(state, "Erreur : le nom du projet est vide.");
return false;
}
if (length >= output_size) {
set_status(state, "Erreur : le nom du projet est trop long.");
return false;
}
if ((length == 1 && start[0] == '.') || (length == 2 && start[0] == '.' && start[1] == '.')) {
set_status(state, "Erreur : nom de projet interdit.");
return false;
}
for (const char *character = start; character < end; ++character) {
if (*character == '/' || *character == '\\' || iscntrl((unsigned char)*character)) {
set_status(state, "Erreur : nom de projet interdit.");
return false;
}
}
(void)memcpy(output, start, length);
output[length] = '\0';
return true;
}
static bool copy_path(char *destination, size_t size, const char *source) {
int written = snprintf(destination, size, "%s", source);
return written >= 0 && (size_t)written < size;
}
static bool join_path(char *destination, size_t size, const char *parent, const char *child) {
int written = snprintf(destination, size, "%s/%s", parent, child);
return written >= 0 && (size_t)written < size;
}
static bool generate_stable_id(char output[LARDON3D_PROJECT_DB_ID_CAPACITY]) {
unsigned char bytes[16];
int descriptor = open("/dev/urandom", O_RDONLY | O_CLOEXEC);
if (descriptor < 0) {
return false;
}
size_t used = 0;
while (used < sizeof(bytes)) {
ssize_t amount = read(descriptor, bytes + used, sizeof(bytes) - used);
if (amount < 0 && errno == EINTR) {
continue;
}
if (amount <= 0) {
(void)close(descriptor);
return false;
}
used += (size_t)amount;
}
if (close(descriptor) != 0) {
return false;
}
for (size_t index = 0; index < sizeof(bytes); ++index) {
(void)snprintf(output + index * 2, 3, "%02x", bytes[index]);
}
return true;
}
static bool resolve_projects_root(Lardon3DAppState *state, char root[PATH_MAX]) {
const char *configured = getenv("LARDON3D_PROJECTS_ROOT");
if (configured && configured[0]) {
if (configured[0] != '/' || !copy_path(root, PATH_MAX, configured)) {
set_status(state, "Erreur : répertoire racine invalide.");
return false;
}
} else {
const char *home = getenv("HOME");
if (!home || home[0] != '/') {
set_status(state, "Erreur : HOME est absent ou invalide.");
return false;
}
int written = snprintf(root, PATH_MAX, "%s/Documents/Lardon/Projets3D", home);
if (written < 0 || (size_t)written >= PATH_MAX) {
set_status(state, "Erreur : chemin racine trop long.");
return false;
}
}
size_t length = strlen(root);
while (length > 1 && root[length - 1] == '/') {
root[--length] = '\0';
}
return true;
}
static void cleanup_directories(CreatedDirectories *created) {
while (created->count > 0) {
--created->count;
(void)rmdir(created->paths[created->count]);
}
}
static bool ensure_directory(Lardon3DAppState *state, const char *path,
CreatedDirectories *created) {
struct stat info;
if (lstat(path, &info) == 0) {
if (!S_ISDIR(info.st_mode)) {
set_status(state, "Erreur : un élément du chemin n'est pas un dossier.");
return false;
}
return true;
}
if (errno != ENOENT || created->count >= MAX_CREATED_DIRECTORIES) {
set_status(state, "Erreur : impossible de préparer le répertoire racine.");
return false;
}
if (mkdir(path, 0755) != 0) {
set_status(state, "Erreur : impossible de créer un dossier.");
return false;
}
if (!copy_path(created->paths[created->count], sizeof(created->paths[created->count]), path)) {
(void)rmdir(path);
set_status(state, "Erreur : chemin de dossier trop long.");
return false;
}
++created->count;
return true;
}
static bool ensure_directory_tree(Lardon3DAppState *state, const char *path,
CreatedDirectories *created) {
char partial[PATH_MAX];
if (!copy_path(partial, sizeof(partial), path)) {
set_status(state, "Erreur : chemin racine trop long.");
return false;
}
for (char *separator = partial + 1; *separator; ++separator) {
if (*separator != '/') {
continue;
}
*separator = '\0';
bool success = ensure_directory(state, partial, created);
*separator = '/';
if (!success) {
return false;
}
}
return ensure_directory(state, partial, created);
}
static bool write_project_ini(Lardon3DAppState *state, const char *project_path,
const char *project_name, const char *stable_id) {
char temporary_path[PATH_MAX];
char final_path[PATH_MAX];
if (!join_path(final_path, sizeof(final_path), project_path, "project.ini") ||
!join_path(temporary_path, sizeof(temporary_path), project_path, ".project.ini.tmp.XXXXXX")) {
set_status(state, "Erreur : chemin de project.ini trop long.");
return false;
}
int descriptor = mkstemp(temporary_path);
if (descriptor < 0) {
set_status(state, "Erreur : impossible de créer project.ini.");
return false;
}
FILE *file = fdopen(descriptor, "w");
if (!file) {
(void)close(descriptor);
(void)unlink(temporary_path);
set_status(state, "Erreur : impossible d'écrire project.ini.");
return false;
}
bool success =
fprintf(file, "[project]\nname=%s\nstable_id=%s\nversion=2\n", project_name, stable_id) >= 0;
if (success) {
success = fflush(file) == 0;
}
if (success) {
success = fsync(fileno(file)) == 0;
}
if (fclose(file) != 0) {
success = false;
}
if (success) {
success = rename(temporary_path, final_path) == 0;
}
if (!success) {
(void)unlink(temporary_path);
set_status(state, "Erreur : impossible d'écrire project.ini.");
}
return success;
}
bool lardon3d_project_create(Lardon3DAppState *state, const char *name) {
if (!state) {
return false;
}
if (state->project_loaded || state->project_db) {
set_status(state, "Erreur : un projet est déjà ouvert.");
return false;
}
char normalized_name[sizeof(state->project_name)];
char root[PATH_MAX];
char project_path[PATH_MAX];
char stable_id[LARDON3D_PROJECT_DB_ID_CAPACITY];
if (!normalize_name(state, name, normalized_name, sizeof(normalized_name))) {
return false;
}
if (!resolve_projects_root(state, root)) {
return false;
}
if (!generate_stable_id(stable_id)) {
set_status(state, "Erreur : impossible de créer l'identité du projet.");
return false;
}
if (!join_path(project_path, sizeof(project_path), root, normalized_name)) {
set_status(state, "Erreur : chemin du projet trop long.");
return false;
}
CreatedDirectories created = {0};
if (!ensure_directory_tree(state, root, &created)) {
cleanup_directories(&created);
return false;
}
struct stat info;
if (lstat(project_path, &info) == 0 || errno != ENOENT) {
cleanup_directories(&created);
set_status(state, "Erreur : ce projet existe déjà.");
return false;
}
if (!ensure_directory(state, project_path, &created)) {
cleanup_directories(&created);
return false;
}
const char *subdirectories[] = {
"images", "reconstruction", "exports", "logs", ".lardon3d", ".lardon3d/checkpoints",
};
for (size_t index = 0; index < sizeof(subdirectories) / sizeof(subdirectories[0]); ++index) {
char path[PATH_MAX];
if (!join_path(path, sizeof(path), project_path, subdirectories[index])) {
set_status(state, "Erreur : chemin de dossier trop long.");
cleanup_directories(&created);
return false;
}
if (!ensure_directory(state, path, &created)) {
cleanup_directories(&created);
return false;
}
}
if (!write_project_ini(state, project_path, normalized_name, stable_id)) {
cleanup_directories(&created);
return false;
}
char database_path[PATH_MAX];
char ini_path[PATH_MAX];
if (!join_path(database_path, sizeof(database_path), project_path, "project.db") ||
!join_path(ini_path, sizeof(ini_path), project_path, "project.ini")) {
cleanup_directories(&created);
set_status(state, "Erreur : chemin de base projet trop long.");
return false;
}
Lardon3DProjectDb *database = NULL;
char database_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDbResult database_result =
lardon3d_project_db_open(database_path, &database, database_error);
struct timespec now;
if (database_result == LARDON3D_PROJECT_DB_OK && clock_gettime(CLOCK_REALTIME, &now) == 0) {
Lardon3DProjectDbProject project = {
.created_at = now.tv_sec,
.updated_at = now.tv_sec,
};
(void)snprintf(project.stable_id, sizeof(project.stable_id), "%s", stable_id);
(void)snprintf(project.name, sizeof(project.name), "%s", normalized_name);
database_result = lardon3d_project_db_set_project(database, &project);
} else if (database_result == LARDON3D_PROJECT_DB_OK) {
database_result = LARDON3D_PROJECT_DB_IO_ERROR;
}
if (database_result != LARDON3D_PROJECT_DB_OK) {
set_database_status(state, database, database_error, "initialisation impossible");
lardon3d_project_db_close(database);
(void)unlink(database_path);
(void)unlink(ini_path);
cleanup_directories(&created);
return false;
}
clear_catalog(state);
state->project_loaded = true;
store_recovery_summary(state, NULL);
state->project_db = database;
(void)copy_path(state->project_name, sizeof(state->project_name), normalized_name);
(void)copy_path(state->project_path, sizeof(state->project_path), project_path);
(void)snprintf(state->project_stable_id, sizeof(state->project_stable_id), "%s", stable_id);
(void)snprintf(state->status_message, sizeof(state->status_message), "Projet créé : %s",
state->project_name);
return true;
}
static bool read_project_ini(Lardon3DAppState *state, const char *path, ProjectMetadata *metadata) {
int descriptor = open(path, O_RDONLY | O_NOFOLLOW);
if (descriptor < 0) {
set_status(state, "Erreur : project.ini est absent ou inaccessible.");
return false;
}
struct stat info;
if (fstat(descriptor, &info) != 0 || !S_ISREG(info.st_mode)) {
(void)close(descriptor);
set_status(state, "Erreur : project.ini n'est pas un fichier régulier.");
return false;
}
FILE *file = fdopen(descriptor, "r");
if (!file) {
(void)close(descriptor);
set_status(state, "Erreur : impossible de lire project.ini.");
return false;
}
bool in_project_section = false;
bool section_found = false;
bool name_found = false;
bool stable_id_found = false;
bool version_found = false;
bool valid = true;
char line[INI_LINE_CAPACITY];
while (valid && fgets(line, sizeof(line), file)) {
size_t length = strlen(line);
if (length > 0 && line[length - 1] == '\n') {
line[--length] = '\0';
} else if (!feof(file)) {
valid = false;
break;
}
if (length > 0 && line[length - 1] == '\r') {
line[--length] = '\0';
}
if (strcmp(line, "[project]") == 0) {
in_project_section = true;
section_found = true;
} else if (line[0] == '[') {
in_project_section = false;
} else if (in_project_section && strncmp(line, "name=", 5) == 0) {
if (name_found ||
!normalize_name(state, line + 5, metadata->name, sizeof(state->project_name))) {
valid = false;
}
name_found = true;
} else if (in_project_section && strncmp(line, "stable_id=", 10) == 0) {
size_t id_length = strnlen(line + 10, sizeof(metadata->stable_id));
if (stable_id_found || id_length != 32) {
valid = false;
} else {
for (size_t index = 0; index < id_length; ++index) {
char character = line[10 + index];
if (!((character >= '0' && character <= '9') || (character >= 'a' && character <= 'f'))) {
valid = false;
}
}
if (valid) {
/* The v2 project identity is exactly 128 lowercase hexadecimal bits. Copying the
* validated width makes the no-truncation contract explicit to both readers and
* compiler diagnostics; project identity must never be accepted by prefix. */
memcpy(metadata->stable_id, line + 10, id_length);
metadata->stable_id[id_length] = '\0';
}
}
stable_id_found = true;
} else if (in_project_section && strncmp(line, "version=", 8) == 0) {
if (version_found) {
valid = false;
} else if (strcmp(line, "version=1") == 0) {
metadata->version = 1;
} else if (strcmp(line, "version=2") == 0) {
metadata->version = 2;
} else {
valid = false;
}
version_found = true;
}
}
if (ferror(file)) {
valid = false;
}
if (fclose(file) != 0) {
valid = false;
}
if (!valid || !section_found || !name_found || !version_found ||
(metadata->version == 2 && !stable_id_found) || (metadata->version == 1 && stable_id_found)) {
set_status(state, "Erreur : project.ini invalide.");
return false;
}
return true;
}
bool lardon3d_project_open(Lardon3DAppState *state, const char *directory_name) {
if (!state) {
return false;
}
if (state->project_loaded || state->project_db) {
set_status(state, "Erreur : un projet est déjà ouvert.");
return false;
}
char normalized_directory[sizeof(state->project_name)];
char root[PATH_MAX];
char project_path[PATH_MAX];
char ini_path[PATH_MAX];
char database_path[PATH_MAX];
if (!normalize_name(state, directory_name, normalized_directory, sizeof(normalized_directory))) {
return false;
}
if (!resolve_projects_root(state, root)) {
return false;
}
if (!join_path(project_path, sizeof(project_path), root, normalized_directory) ||
!join_path(ini_path, sizeof(ini_path), project_path, "project.ini") ||
!join_path(database_path, sizeof(database_path), project_path, "project.db")) {
set_status(state, "Erreur : chemin du projet trop long.");
return false;
}
struct stat info;
if (lstat(project_path, &info) != 0 || !S_ISDIR(info.st_mode)) {
set_status(state, "Erreur : dossier projet absent ou invalide.");
return false;
}
ProjectMetadata metadata = {0};
if (!read_project_ini(state, ini_path, &metadata)) {
return false;
}
bool database_existed = false;
if (lstat(database_path, &info) == 0) {
if (!S_ISREG(info.st_mode)) {
set_status(state, "Erreur : project.db n'est pas un fichier régulier.");
return false;
}
database_existed = true;
} else if (errno != ENOENT) {
set_status(state, "Erreur : project.db est inaccessible.");
return false;
}
CreatedDirectories created = {0};
char internal_path[PATH_MAX];
char checkpoint_path[PATH_MAX];
if (!join_path(internal_path, sizeof(internal_path), project_path, ".lardon3d") ||
!join_path(checkpoint_path, sizeof(checkpoint_path), internal_path, "checkpoints") ||
!ensure_directory(state, internal_path, &created) ||
!ensure_directory(state, checkpoint_path, &created)) {
cleanup_directories(&created);
return false;
}
Lardon3DProjectDb *database = NULL;
char database_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
Lardon3DProjectDbResult database_result =
lardon3d_project_db_open(database_path, &database, database_error);
if (database_result != LARDON3D_PROJECT_DB_OK) {
cleanup_directories(&created);
set_database_status(state, NULL, database_error, "ouverture impossible");
return false;
}
Lardon3DProjectDbProject db_project;
database_result = lardon3d_project_db_get_project(database, &db_project);
bool rewrite_metadata = metadata.version == 1;
struct timespec now;
if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
database_result = LARDON3D_PROJECT_DB_IO_ERROR;
} else if (database_result == LARDON3D_PROJECT_DB_OK) {
if (metadata.stable_id[0] && strcmp(metadata.stable_id, db_project.stable_id) != 0) {
database_result = LARDON3D_PROJECT_DB_CONSTRAINT;
} else if (!metadata.stable_id[0]) {
(void)snprintf(metadata.stable_id, sizeof(metadata.stable_id), "%s", db_project.stable_id);
rewrite_metadata = true;
}
if (database_result == LARDON3D_PROJECT_DB_OK) {
db_project.updated_at = now.tv_sec;
(void)snprintf(db_project.name, sizeof(db_project.name), "%s", metadata.name);
database_result = lardon3d_project_db_set_project(database, &db_project);
}
} else if (database_result == LARDON3D_PROJECT_DB_NOT_FOUND) {
if (database_existed && !metadata.stable_id[0]) {
database_result = LARDON3D_PROJECT_DB_CONSTRAINT;
} else {
if (!metadata.stable_id[0]) {
if (!generate_stable_id(metadata.stable_id)) {
database_result = LARDON3D_PROJECT_DB_IO_ERROR;
}
rewrite_metadata = true;
}
if (database_result != LARDON3D_PROJECT_DB_IO_ERROR) {
Lardon3DProjectDbProject new_project = {
.created_at = now.tv_sec,
.updated_at = now.tv_sec,
};
(void)snprintf(new_project.stable_id, sizeof(new_project.stable_id), "%s",
metadata.stable_id);
(void)snprintf(new_project.name, sizeof(new_project.name), "%s", metadata.name);
database_result = lardon3d_project_db_set_project(database, &new_project);
}
}
}
if (database_result == LARDON3D_PROJECT_DB_OK && rewrite_metadata &&
!write_project_ini(state, project_path, metadata.name, metadata.stable_id)) {
database_result = LARDON3D_PROJECT_DB_IO_ERROR;
}
if (database_result != LARDON3D_PROJECT_DB_OK) {
if (database_result == LARDON3D_PROJECT_DB_CONSTRAINT) {
set_status(state, "Erreur : identité project.ini/project.db incohérente.");
} else {
set_database_status(state, database, database_error, "initialisation impossible");
}
lardon3d_project_db_close(database);
if (!database_existed) {
(void)unlink(database_path);
}
cleanup_directories(&created);
return false;
}
clear_catalog(state);
state->project_loaded = true;
state->project_db = database;
(void)copy_path(state->project_name, sizeof(state->project_name), metadata.name);
(void)snprintf(state->project_stable_id, sizeof(state->project_stable_id), "%s",
metadata.stable_id);
(void)copy_path(state->project_path, sizeof(state->project_path), project_path);
store_recovery_summary(state, NULL);
if (state->task_queue && state->resource_governor) {
Lardon3DProjectRecoverySummary summary;
(void)lardon3d_project_resume_recoverable_tasks(state, lardon3d_task_kind_registry_production(),
&summary);
(void)snprintf(state->status_message, sizeof(state->status_message),
"Projet ouvert — %zu tâche(s) reprise(s), %zu ignorée(s), %zu en échec%s.",
summary.resumed, summary.skipped, summary.failed,
summary.queue_full ? ", fenêtre de reprise saturée" : "");
} else {
(void)snprintf(state->status_message, sizeof(state->status_message), "Projet ouvert : %s",
state->project_name);
}
return true;
}
void lardon3d_project_close(Lardon3DAppState *state) {
if (!state) {
return;
}
if (!state->project_loaded) {
set_status(state, "Aucun projet à fermer.");
return;
}
clear_catalog(state);
lardon3d_project_db_close(state->project_db);
state->project_db = NULL;
state->project_loaded = false;
state->project_name[0] = '\0';
state->project_path[0] = '\0';
state->project_stable_id[0] = '\0';
store_recovery_summary(state, NULL);
set_status(state, "Projet fermé.");
}
static bool checkpoint_paths(const Lardon3DAppState *state, uint64_t task_id,
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY],
char absolute[PATH_MAX]) {
int written = snprintf(relative, LARDON3D_PROJECT_DB_PATH_CAPACITY,
".lardon3d/checkpoints/%" PRIu64 ".chk", task_id);
return written > 0 && (size_t)written < LARDON3D_PROJECT_DB_PATH_CAPACITY &&
join_path(absolute, PATH_MAX, state->project_path, relative);
}
static int checkpoint_lock_acquire(const char *checkpoint_path) {
char lock_path[PATH_MAX];
int written = snprintf(lock_path, sizeof(lock_path), "%s.lock", checkpoint_path);
if (written < 0 || (size_t)written >= sizeof(lock_path)) {
return -1;
}
int lock = open(lock_path, O_CREAT | O_RDWR | O_CLOEXEC, 0600);
if (lock < 0 || flock(lock, LOCK_EX) != 0) {
if (lock >= 0) {
(void)close(lock);
}
return -1;
}
return lock;
}
#ifdef LARDON3D_CHECKPOINT_TESTING
static void checkpoint_test_rendezvous(const char *variable) {
const char *value = getenv(variable);
if (!value || !value[0]) {
return;
}
char *end = NULL;
errno = 0;
long descriptor = strtol(value, &end, 10);
if (errno != 0 || !end || *end != '\0' || descriptor < 0 || descriptor > INT_MAX) {
return;
}
unsigned char token = 1;
if (write((int)descriptor, &token, sizeof(token)) == (ssize_t)sizeof(token)) {
(void)read((int)descriptor, &token, sizeof(token));
}
}
#endif
static Lardon3DProjectTaskCheckpointResult
checkpoint_task_internal(Lardon3DAppState *state, const Lardon3DTask *task,
const char *image_import_source, uint64_t image_import_scanset_id,
const Lardon3DProjectDbFeatureExtractTask *feature_parameters,
const Lardon3DProjectDbSiftExtractTask *sift_parameters,
const Lardon3DProjectDbVisualIndexUpdateTask *visual_parameters,
const Lardon3DProjectDbCandidatePairGenerateTask *candidate_parameters,
const Lardon3DProjectDbMatcherTask *matcher_parameters,
const Lardon3DProjectDbGeometricVerifierTask *geometric_parameters,
const Lardon3DProjectDbAcquisitionCampaignTask *campaign_parameters,
const Lardon3DProjectDbPhotoQualityTask *quality_parameters,
const Lardon3DProjectDbRawDevelopmentTask *raw_parameters,
const Lardon3DProjectDbRawDevelopmentBatchTask *raw_batch_parameters,
const Lardon3DProjectDbFeatureExtractBatchTask *feature_batch_parameters) {
if (!state || !state->project_loaded || !state->project_db) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT;
}
Lardon3DTaskDurableSnapshot snapshot;
if (!lardon3d_task_durable_snapshot(task, &snapshot) || snapshot.id == 0) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
}
char task_kind[LARDON3D_TASK_KIND_CAPACITY];
uint32_t task_kind_version = 0;
if (!lardon3d_task_kind(task, task_kind, &task_kind_version)) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
}
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
char absolute[PATH_MAX];
if (!checkpoint_paths(state, snapshot.id, relative, absolute)) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR;
}
/* Serialize the fixed .next slot per task through stage, DB commit, and
* promotion. The advisory lock is process-owned and is released by kernel
* close on crash, so it cannot become a durable recovery dependency. */
int checkpoint_lock = checkpoint_lock_acquire(absolute);
if (checkpoint_lock < 0) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR;
}
#define CHECKPOINT_RETURN(value) do { (void)close(checkpoint_lock); return (value); } while (0)
/* DB and filesystem durability are independent: changing their write order
* cannot make S1 atomic. Keep .next durable across the DB commit window so
* recovery always has an explicitly published representation to compare. */
Lardon3DTaskCheckpointResult saved = lardon3d_task_checkpoint_stage(absolute, &snapshot);
if (saved == LARDON3D_TASK_CHECKPOINT_INVALID) {
CHECKPOINT_RETURN(LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK);
}
/* S1 must be directory-durable before SQLite may publish S1. A staged
* namespace with an uncertain parent-directory sync is not a safe recovery
* representation, even when its file descriptor itself was synced. */
if (saved == LARDON3D_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE) {
CHECKPOINT_RETURN(LARDON3D_PROJECT_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE);
}
if (saved != LARDON3D_TASK_CHECKPOINT_OK) {
CHECKPOINT_RETURN(LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR);
}
struct timespec now;
if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
CHECKPOINT_RETURN(LARDON3D_PROJECT_TASK_CHECKPOINT_DB_ERROR);
}
Lardon3DProjectDbCheckpoint checkpoint = {
.format_version = LARDON3D_TASK_CHECKPOINT_VERSION,
/* A fully synced .next is the durable checkpoint representation during
* the DB-to-canonical handoff, even though checkpoint.path names the
* legacy canonical location for v22 compatibility. */
.durability = LARDON3D_DB_CHECKPOINT_DURABLE,
.updated_at = now.tv_sec,
};
(void)snprintf(checkpoint.path, sizeof(checkpoint.path), "%s", relative);
Lardon3DProjectDbResult recorded =
image_import_source
? lardon3d_project_db_record_image_import_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
image_import_source, image_import_scanset_id, now.tv_sec)
: feature_parameters
? lardon3d_project_db_record_feature_extract_task(state->project_db, &snapshot, task_kind,
task_kind_version, &checkpoint,
feature_parameters, now.tv_sec)
: sift_parameters
? lardon3d_project_db_record_sift_extract_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
sift_parameters, now.tv_sec)
: visual_parameters
? lardon3d_project_db_record_visual_index_update_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
visual_parameters, now.tv_sec)
: candidate_parameters
? lardon3d_project_db_record_candidate_pair_generate_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
candidate_parameters, now.tv_sec)
: matcher_parameters
? lardon3d_project_db_record_matcher_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
matcher_parameters, now.tv_sec)
: geometric_parameters
? lardon3d_project_db_record_geometric_verifier_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
geometric_parameters, now.tv_sec)
: campaign_parameters
? lardon3d_project_db_record_acquisition_campaign_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
campaign_parameters, now.tv_sec)
: quality_parameters
? lardon3d_project_db_record_photo_quality_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
quality_parameters, now.tv_sec)
: raw_parameters
? lardon3d_project_db_record_raw_development_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
raw_parameters, now.tv_sec)
: raw_batch_parameters
? lardon3d_project_db_record_raw_development_batch_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
raw_batch_parameters, now.tv_sec)
: feature_batch_parameters
? lardon3d_project_db_record_feature_extract_batch_task(
state->project_db, &snapshot, task_kind, task_kind_version, &checkpoint,
feature_batch_parameters, now.tv_sec)
: lardon3d_project_db_record_task(state->project_db, &snapshot, task_kind,
task_kind_version, &checkpoint, now.tv_sec);
if (recorded == LARDON3D_PROJECT_DB_BUSY) {
CHECKPOINT_RETURN(LARDON3D_PROJECT_TASK_CHECKPOINT_DB_BUSY);
}
if (recorded != LARDON3D_PROJECT_DB_OK) {
CHECKPOINT_RETURN(LARDON3D_PROJECT_TASK_CHECKPOINT_DB_ERROR);
}
#ifdef LARDON3D_CHECKPOINT_TESTING
/* Deterministic crash boundary: production recovery must select .next after
* the SQLite FULL transaction is durable but before canonical promotion. */
const char *stop_after_db_commit = getenv("LARDON3D_TEST_PROJECT_CHECKPOINT_AFTER_DB_COMMIT");
if (stop_after_db_commit && strcmp(stop_after_db_commit, "1") == 0) {
CHECKPOINT_RETURN(LARDON3D_PROJECT_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE);
}
#endif
Lardon3DTaskCheckpointResult promoted = lardon3d_task_checkpoint_promote_staged(absolute);
Lardon3DProjectTaskCheckpointResult result = promoted == LARDON3D_TASK_CHECKPOINT_OK
? LARDON3D_PROJECT_TASK_CHECKPOINT_OK
: LARDON3D_PROJECT_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE;
#undef CHECKPOINT_RETURN
(void)close(checkpoint_lock);
return result;
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(Lardon3DAppState *state,
const Lardon3DTask *task) {
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult
lardon3d_project_checkpoint_image_import_task(Lardon3DAppState *state, const Lardon3DTask *task,
const char *source_path, uint64_t scanset_id) {
if (!source_path || !source_path[0] || scanset_id == 0) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
}
return checkpoint_task_internal(
state, task, source_path, scanset_id, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_acquisition_campaign_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbAcquisitionCampaignTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL,
parameters, NULL, NULL, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_photo_quality_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbPhotoQualityTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
parameters, NULL, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_raw_development_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbRawDevelopmentTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, parameters, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbFeatureExtractTask *parameters) {
if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
}
return checkpoint_task_internal(state, task, NULL, 0, parameters, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_sift_extract_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbSiftExtractTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, parameters, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_visual_index_update_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbVisualIndexUpdateTask *parameters) {
if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
}
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, parameters, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_candidate_pair_generate_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbCandidatePairGenerateTask *parameters) {
if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
}
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, parameters, NULL, NULL,
NULL, NULL, NULL, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_matcher_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbMatcherTask *parameters) {
if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
}
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, parameters, NULL,
NULL, NULL, NULL, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_geometric_verifier_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbGeometricVerifierTask *parameters) {
if (!parameters) {
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
}
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL,
parameters, NULL, NULL, NULL, NULL, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_raw_development_batch_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbRawDevelopmentBatchTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, parameters, NULL);
}
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_feature_extract_batch_task(
Lardon3DAppState *state, const Lardon3DTask *task,
const Lardon3DProjectDbFeatureExtractBatchTask *parameters) {
if (!parameters) return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
return checkpoint_task_internal(state, task, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, parameters);
}
static bool coherent_recovery(const Lardon3DProjectDbTask *database_task,
const Lardon3DTaskDurableSnapshot *snapshot) {
return snapshot->id == database_task->task_id &&
strcmp(snapshot->name, database_task->name) == 0 &&
snapshot->saved_state == database_task->saved_state &&
snapshot->recovery_state == database_task->recovery_state &&
snapshot->progress == database_task->progress &&
snapshot->sequence_count == database_task->sequence_count;
}
Lardon3DProjectDbResult lardon3d_project_list_recoverable(Lardon3DAppState *state,
const Lardon3DTaskKindRegistry *registry,
uint64_t after_task_id,
Lardon3DProjectRecoveryEntry *entries,
size_t capacity, size_t *count) {
if (count) {
*count = 0;
}
if (!state || !state->project_loaded || !state->project_db || !registry || !entries || !count ||
capacity == 0 || capacity > LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
enum { RECOVERY_CHUNK = 8 };
Lardon3DProjectDbTask tasks[RECOVERY_CHUNK];
uint64_t cursor = after_task_id;
while (*count < capacity) {
size_t requested = capacity - *count;
if (requested > RECOVERY_CHUNK) {
requested = RECOVERY_CHUNK;
}
size_t task_count = 0;
Lardon3DProjectDbResult result = lardon3d_project_db_list_recoverable(
state->project_db, cursor, tasks, requested, &task_count);
if (result != LARDON3D_PROJECT_DB_OK) {
return result;
}
for (size_t index = 0; index < task_count; ++index) {
Lardon3DProjectRecoveryEntry *entry = &entries[*count];
memset(entry, 0, sizeof(*entry));
entry->task_id = tasks[index].task_id;
entry->durability = tasks[index].checkpoint.durability;
(void)snprintf(entry->name, sizeof(entry->name), "%s", tasks[index].name);
if (tasks[index].has_task_kind) {
(void)snprintf(entry->task_kind, sizeof(entry->task_kind), "%s", tasks[index].task_kind);
entry->task_kind_version = tasks[index].task_kind_version;
}
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
char absolute[PATH_MAX];
const Lardon3DTaskKindDescriptor *descriptor = NULL;
Lardon3DTaskKindResult kind_result =
tasks[index].has_task_kind
? lardon3d_task_kind_registry_lookup(registry, tasks[index].task_kind,
tasks[index].task_kind_version, &descriptor)
: LARDON3D_TASK_KIND_UNKNOWN;
if (!tasks[index].has_task_kind) {
entry->status = LARDON3D_PROJECT_RECOVERY_LEGACY_UNTYPED;
} else if (kind_result == LARDON3D_TASK_KIND_UNKNOWN) {
entry->status = LARDON3D_PROJECT_RECOVERY_UNKNOWN_TASK_KIND;
} else if (kind_result == LARDON3D_TASK_KIND_UNSUPPORTED_VERSION) {
entry->status = LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_TASK_KIND_VERSION;
} else if (kind_result != LARDON3D_TASK_KIND_OK) {
entry->status = LARDON3D_PROJECT_RECOVERY_UNKNOWN_TASK_KIND;
} else if (!checkpoint_paths(state, entry->task_id, relative, absolute) ||
strcmp(relative, tasks[index].checkpoint.path) != 0) {
entry->status = LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT;
} else {
/* Writer and recovery take this lock before touching SQLite. Recovery
* must hold it across staged validation and promotion: otherwise a
* writer could replace .next after it matched the DB-stored task
* summary fields.
* Reload the DB row only after acquiring the lock, because the paged
* row above may have become stale while recovery waited for a writer. */
#ifdef LARDON3D_CHECKPOINT_TESTING
/* Test-only rendezvous proves that the paged row is never used after
* another writer advances it before recovery obtains .chk.lock. */
checkpoint_test_rendezvous("LARDON3D_TEST_RECOVERY_BEFORE_CHECKPOINT_LOCK_FD");
#endif
int checkpoint_lock = checkpoint_lock_acquire(absolute);
if (checkpoint_lock < 0) {
entry->status = LARDON3D_PROJECT_RECOVERY_CHECKPOINT_IO_ERROR;
cursor = entry->task_id;
++*count;
continue;
}
Lardon3DProjectDbTask durable_task;
Lardon3DProjectDbResult loaded_task =
lardon3d_project_db_load_task(state->project_db, entry->task_id, &durable_task);
if (loaded_task != LARDON3D_PROJECT_DB_OK) {
(void)close(checkpoint_lock);
return loaded_task;
}
/* Pagination is only a candidate discovery mechanism. A writer may
* have made this task terminal while recovery waited, so do not emit a
* recoverable entry unless the locked, authoritative row is pending. */
if (durable_task.recovery_state != TASK_PENDING || !durable_task.has_checkpoint ||
!durable_task.has_task_kind || strcmp(relative, durable_task.checkpoint.path) != 0) {
(void)close(checkpoint_lock);
cursor = entry->task_id;
continue;
}
entry->durability = durable_task.checkpoint.durability;
(void)snprintf(entry->name, sizeof(entry->name), "%s", durable_task.name);
if (durable_task.has_task_kind) {
(void)snprintf(entry->task_kind, sizeof(entry->task_kind), "%s", durable_task.task_kind);
entry->task_kind_version = durable_task.task_kind_version;
}
uint32_t version = 0;
Lardon3DTaskCheckpointResult promotion = LARDON3D_TASK_CHECKPOINT_OK;
/* A codec-valid canonical checkpoint whose DB-stored task-summary
* fields match wins. A stale or corrupt .next is ignored in that
* case; the DB deliberately does not duplicate the full snapshot. */
Lardon3DTaskCheckpointResult loaded =
lardon3d_task_checkpoint_load(absolute, &entry->snapshot, &version);
bool canonical_match = loaded == LARDON3D_TASK_CHECKPOINT_OK
&& version == durable_task.checkpoint.format_version
&& coherent_recovery(&durable_task, &entry->snapshot);
if (!canonical_match) {
Lardon3DTaskDurableSnapshot staged_snapshot;
uint32_t staged_version = 0;
Lardon3DTaskCheckpointResult staged = lardon3d_task_checkpoint_load_staged(
absolute, &staged_snapshot, &staged_version);
if (staged == LARDON3D_TASK_CHECKPOINT_OK
&& staged_version == durable_task.checkpoint.format_version
&& coherent_recovery(&durable_task, &staged_snapshot)) {
entry->snapshot = staged_snapshot;
#ifdef LARDON3D_CHECKPOINT_TESTING
checkpoint_test_rendezvous("LARDON3D_TEST_RECOVERY_AFTER_STAGED_MATCH_FD");
#endif
/* The same lock protects this re-read/promote helper from a
* replacement of .next between DB-summary validation and rename. */
promotion = lardon3d_task_checkpoint_promote_staged(absolute);
if (promotion == LARDON3D_TASK_CHECKPOINT_OK ||
promotion == LARDON3D_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE) {
loaded = LARDON3D_TASK_CHECKPOINT_OK;
version = staged_version;
canonical_match = true;
}
}
}
if (!canonical_match && loaded == LARDON3D_TASK_CHECKPOINT_NOT_FOUND) {
entry->status = LARDON3D_PROJECT_RECOVERY_MISSING_CHECKPOINT;
} else if (!canonical_match && loaded == LARDON3D_TASK_CHECKPOINT_UNSUPPORTED_VERSION) {
entry->status = LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_CHECKPOINT;
} else if (!canonical_match && loaded == LARDON3D_TASK_CHECKPOINT_INVALID) {
entry->status = LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT;
} else if (!canonical_match) {
entry->status = loaded == LARDON3D_TASK_CHECKPOINT_IO_ERROR
? LARDON3D_PROJECT_RECOVERY_CHECKPOINT_IO_ERROR
: LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT;
} else {
entry->status = promotion == LARDON3D_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE ||
entry->durability == LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE
? LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE
: LARDON3D_PROJECT_RECOVERABLE;
}
(void)close(checkpoint_lock);
}
cursor = entry->task_id;
++*count;
}
if (task_count < requested) {
break;
}
}
return LARDON3D_PROJECT_DB_OK;
}
Lardon3DProjectDbResult
lardon3d_project_resume_recoverable_tasks(Lardon3DAppState *state,
const Lardon3DTaskKindRegistry *registry,
Lardon3DProjectRecoverySummary *summary) {
if (summary) {
memset(summary, 0, sizeof(*summary));
}
if (!state || !state->project_loaded || !state->project_db || !state->task_queue ||
!state->resource_governor || !registry || !summary) {
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
}
enum { RECOVERY_PAGE_SIZE = 8 };
uint64_t cursor = 0;
Lardon3DProjectDbResult result = LARDON3D_PROJECT_DB_OK;
bool stop = false;
do {
Lardon3DProjectRecoveryEntry entries[RECOVERY_PAGE_SIZE];
size_t count = 0;
result = lardon3d_project_list_recoverable(state, registry, cursor, entries, RECOVERY_PAGE_SIZE,
&count);
if (result != LARDON3D_PROJECT_DB_OK) {
++summary->failed;
break;
}
for (size_t index = 0; index < count; ++index) {
Lardon3DProjectRecoveryEntry *entry = &entries[index];
cursor = entry->task_id;
++summary->inspected;
bool recoverable = entry->status == LARDON3D_PROJECT_RECOVERABLE ||
entry->status == LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE;
if (!recoverable || entry->snapshot.recovery_state != TASK_PENDING) {
++summary->skipped;
continue;
}
if (entry->status == LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE) {
++summary->published_not_durable;
}
Lardon3DTaskSnapshot existing;
if (lardon3d_task_queue_get(state->task_queue, entry->task_id, &existing)) {
++summary->skipped;
continue;
}
Lardon3DImageImportReconstructionContext context = {
.project_path = state->project_path,
.project_db = state->project_db,
.resource_governor = state->resource_governor,
.orb_vulkan_backend = state->orb_vulkan_backend,
};
Lardon3DTask *task = NULL;
Lardon3DTaskKindResult restored = lardon3d_task_kind_registry_restore(
registry, entry->task_kind, entry->task_kind_version, &entry->snapshot, &context, &task);
if (restored != LARDON3D_TASK_KIND_OK || !task) {
++summary->failed;
continue;
}
Lardon3DTaskQueueAddResult added =
lardon3d_task_queue_try_add_ex(state->task_queue, task, NULL);
if (added == LARDON3D_TASK_QUEUE_ADD_OK) {
++summary->resumed;
continue;
}
lardon3d_task_destroy(task);
if (added == LARDON3D_TASK_QUEUE_ADD_DUPLICATE_ID) {
++summary->skipped;
} else if (added == LARDON3D_TASK_QUEUE_ADD_FULL) {
++summary->skipped;
} else {
++summary->failed;
}
if (added == LARDON3D_TASK_QUEUE_ADD_FULL || added == LARDON3D_TASK_QUEUE_ADD_STOPPING) {
summary->queue_full = added == LARDON3D_TASK_QUEUE_ADD_FULL;
stop = true;
break;
}
}
if (stop || count < RECOVERY_PAGE_SIZE) {
break;
}
} while (cursor > 0);
store_recovery_summary(state, summary);
return result;
}
bool lardon3d_project_last_recovery_summary(const Lardon3DAppState *state,
Lardon3DProjectRecoverySummary *summary) {
if (!state || !summary) {
return false;
}
*summary = (Lardon3DProjectRecoverySummary){
.inspected = state->recovery_inspected,
.resumed = state->recovery_resumed,
.skipped = state->recovery_skipped,
.failed = state->recovery_failed,
.published_not_durable = state->recovery_published_not_durable,
.queue_full = state->recovery_queue_full,
};
return true;
}