feat(project): wire project database and task recovery inventory
This commit is contained in:
parent
2b6e9bf444
commit
efa556a824
19 changed files with 1786 additions and 54 deletions
|
|
@ -20,13 +20,14 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble
|
|||
|
||||
### Briques implémentées (IMPLEMENTED)
|
||||
|
||||
- **Project** : gestion persistante des projets
|
||||
- **Project** : cycle de vie persistant, identité stable et Project Database ouverte
|
||||
- **Import** : import asynchrone et annulable d'images
|
||||
- **Import Task** : wrapper asynchrone avec états et progression
|
||||
- **Image Catalog** : indexage des métadonnées d'images
|
||||
- **Image View** : vues triées et filtrées pour la TUI
|
||||
- **Task** : moteur de tâches avec pause/reprise, annulation et séquences
|
||||
- **Task Checkpoint v1** : snapshot durable, fichier atomique et reprise sûre
|
||||
- **Project Database v1** : identité, tâches/checkpoints et inventaire d'artefacts SQLite
|
||||
- **Task Queue** : file FIFO avec sélection adaptative et backpressure
|
||||
- **Hardware Profile** : détection des capacités matérielles
|
||||
- **Resource Snapshot** : capture instantanée des ressources
|
||||
|
|
@ -39,7 +40,7 @@ Lardon3D ne vise pas simplement "dossier de photos → objet 3D", mais "ensemble
|
|||
|
||||
### Briques prévues (PLANNED)
|
||||
|
||||
- Branchement des checkpoints aux pipelines et reprise globale
|
||||
- Reconstruction des callbacks métier et resoumission des tâches récupérables
|
||||
- DAG de dépendances
|
||||
- Pools de workers multiples (CPU/GPU/IO)
|
||||
- Publication live validée
|
||||
|
|
|
|||
|
|
@ -108,13 +108,46 @@ par le `time_t` local avant conversion. Le format reste donc lisible entre
|
|||
plateformes uniquement pour les valeurs communes à leurs domaines `size_t` et
|
||||
`time_t`.
|
||||
|
||||
## Project Database v1
|
||||
|
||||
SQLite contient l'état logique interrogable et les références aux fichiers ;
|
||||
les checkpoints et artefacts volumineux restent externes. L'enregistrement du
|
||||
résumé de tâche et de sa référence checkpoint est une transaction unique. Un
|
||||
artefact est d'abord publié et vérifié comme fichier régulier, puis seulement
|
||||
marqué `READY` en DB. Le chemin inverse est interdit.
|
||||
|
||||
### Protocole checkpoint projet
|
||||
|
||||
Le protocole réel n'est pas une transaction distribuée :
|
||||
|
||||
1. capture locale du snapshot sous le mutex de tâche puis déverrouillage ;
|
||||
2. publication atomique du fichier sous
|
||||
`.lardon3d/checkpoints/<task_id>.chk` ;
|
||||
3. transaction SQLite sur `tasks` et `checkpoints` avec chemin relatif.
|
||||
|
||||
Une erreur avant publication ne modifie pas la DB. `PUBLISHED_NOT_DURABLE` est
|
||||
conservé comme tel en DB. Si la publication réussit puis que SQLite retourne
|
||||
`BUSY` ou une erreur, le fichier valide reste sur disque, la DB conserve son
|
||||
ancienne vérité et le nouveau fichier est un orphelin à réconcilier plus tard.
|
||||
Il n'est pas supprimé et aucune atomicité FS+SQLite n'est revendiquée.
|
||||
|
||||
L'inventaire distingue checkpoint récupérable durable, récupérable mais publié
|
||||
non durable, absent, invalide, version inconnue et erreur d'I/O. Aucune réparation
|
||||
ou suppression silencieuse n'est effectuée.
|
||||
|
||||
## Statut
|
||||
|
||||
**IMPLEMENTED** — modèle durable, codec v1, lecture validée, publication
|
||||
atomique et restauration sûre d'une tâche isolée.
|
||||
|
||||
**NOT_YET_WIRED** — les pipelines et le scheduler ne déclenchent pas encore les
|
||||
sauvegardes et ne rechargent pas globalement les tâches au démarrage.
|
||||
**IMPLEMENTED** — Project Database v1 pour identité, résumés de tâches,
|
||||
références checkpoint et artefacts génériques.
|
||||
|
||||
**PLANNED** — Project Database SQLite, catalogue d'artefacts réels, transactions
|
||||
entre métadonnées et artefacts, migration de formats et reprise globale.
|
||||
**IMPLEMENTED** — API projet de sauvegarde fichier+DB et inventaire validé au
|
||||
redémarrage.
|
||||
|
||||
**NOT_YET_WIRED** — autosave complet, reconstruction des callbacks métier,
|
||||
resoumission scheduler et réconciliation des fichiers orphelins.
|
||||
|
||||
**PLANNED** — catalogue d'artefacts photogrammétriques réels, migrations v2+ et
|
||||
reprise globale.
|
||||
|
|
|
|||
|
|
@ -162,9 +162,90 @@ validation ; elle ne stockera jamais les objets pthread, callbacks, pointeurs,
|
|||
contrats ou réservations. Le fichier par tâche est une fondation, pas une
|
||||
Project Database miniature.
|
||||
|
||||
La stratégie v1 retient un résumé logique interrogable dans SQLite et une
|
||||
référence vers le fichier checkpoint. Le fichier checkpoint validé reste la
|
||||
source complète pour `lardon3d_task_restore()` ; la DB seule ne reconstruit
|
||||
jamais une tâche. Un écart ou un fichier invalide interdit la reprise.
|
||||
|
||||
## Schéma v1 implémenté
|
||||
|
||||
- `metadata(key PRIMARY KEY, value)` contient uniquement `schema_version=1`.
|
||||
- `project(singleton=1, stable_id UNIQUE, name, created_at, updated_at)` décrit
|
||||
l'unique identité logique de la DB.
|
||||
- `tasks(task_id PRIMARY KEY, name, saved_state, recovery_state, progress,
|
||||
sequence_count, started_sec/nsec, finished_sec/nsec, updated_at)` contient le
|
||||
résumé durable. Les IDs v1 sont compris entre 1 et `INT64_MAX`.
|
||||
- `checkpoints(task_id PRIMARY KEY REFERENCES tasks ON DELETE CASCADE, path,
|
||||
format_version, durability, updated_at)` représente `DURABLE` ou
|
||||
`PUBLISHED_NOT_DURABLE`.
|
||||
- `artifacts(artifact_id PRIMARY KEY, kind, path, state, size_bytes,
|
||||
producer_task_id REFERENCES tasks, created_at, updated_at)` inventorie des
|
||||
fichiers externes. Les états v1 sont `STAGED` et `READY`.
|
||||
|
||||
Les indexes portent uniquement sur `tasks(recovery_state, task_id)`,
|
||||
`artifacts(state, artifact_id)` et `artifacts(producer_task_id)`.
|
||||
|
||||
## Ouverture et migrations
|
||||
|
||||
Une DB vide reçoit le schéma v1 dans une transaction `BEGIN IMMEDIATE`. Une DB
|
||||
v1 est validée puis ouverte. Une version future est refusée et une DB contenant
|
||||
des tables sans métadonnée de version est considérée corrompue. La fonction
|
||||
interne de migration ne connaît que `0 → 1`; aucune migration fictive ou
|
||||
destructive n'est présente.
|
||||
|
||||
Configuration v1 : `foreign_keys=ON`, `journal_mode=DELETE`,
|
||||
`synchronous=FULL`, `busy_timeout=5000`. Le mode DELETE convient au propriétaire
|
||||
unique actuel, évite les fichiers WAL/SHM durables et conserve la synchronisation
|
||||
forte. Le timeout borne l'attente d'un verrou externe à cinq secondes.
|
||||
|
||||
## Concurrence et ownership
|
||||
|
||||
Une connexion opaque est sérialisée par un mutex interne. Chaque opération
|
||||
composée possède sa transaction entière ; aucune transaction publique ne peut
|
||||
rester ouverte entre deux appels. Aucune I/O d'artefact n'a lieu sous le mutex :
|
||||
le module confirme que le fichier publié et validé par l'appelant est régulier
|
||||
avant la mise à jour `READY`. Fermer la DB pendant un
|
||||
appel concurrent est interdit au propriétaire.
|
||||
|
||||
Les records et chaînes sont copiés dans des buffers fournis par l'appelant ;
|
||||
aucun pointeur SQLite n'en sort. Tous les statements sont finalisés dans
|
||||
l'appel. La liste de reprise utilise des pages fournies par l'appelant, limitées
|
||||
à 256 entrées. Elle ne retourne que les tâches normalisées `PENDING` possédant
|
||||
une référence checkpoint ; le fichier doit encore être chargé et validé.
|
||||
|
||||
## Branchement au projet
|
||||
|
||||
`Lardon3DAppState` est l'instance projet runtime actuelle et possède exactement
|
||||
une `Lardon3DProjectDb *` pendant que `project_loaded` est vrai. La DB canonique
|
||||
est `<project_root>/project.db`. Elle est ouverte à la création/ouverture du
|
||||
projet et fermée une seule fois par `lardon3d_project_close()`. À l'arrêt de
|
||||
l'application, la task queue est arrêtée avant la fermeture du projet et de sa
|
||||
DB. Une fermeture concurrente à un appel projet/DB est interdite au propriétaire.
|
||||
|
||||
`project.ini` v2 contient `name`, `stable_id` hexadécimal sur 128 bits et
|
||||
`version=2`. La même identité est enregistrée dans la table `project`. Toute
|
||||
divergence est une erreur. Un INI v1 sans identité adopte l'identité d'une DB
|
||||
existante ; sans DB, une identité est générée une seule fois puis l'INI est
|
||||
migré atomiquement avant de devenir la référence des ouvertures suivantes. Une
|
||||
DB existante sans ligne projet ne peut être initialisée que si l'INI possède
|
||||
déjà son identité.
|
||||
|
||||
Les checkpoints sont référencés par chemins relatifs portables :
|
||||
`.lardon3d/checkpoints/<task_id>.chk`. L'inventaire projet pagine les tâches DB,
|
||||
résout ce chemin sous la racine, charge le checkpoint et vérifie la cohérence du
|
||||
snapshot avec le résumé DB.
|
||||
|
||||
## Statut
|
||||
|
||||
**NOT_YET_WIRED** — le modèle de checkpoint est prêt à être consommé.
|
||||
**IMPLEMENTED** — SQLite système, schéma/migration v1, identité projet,
|
||||
transactions tâche+checkpoint, pagination de reprise et artefacts génériques.
|
||||
|
||||
**PLANNED** — schéma SQLite, migrations, transactions, inventaire d'artefacts,
|
||||
chargement global et coordination avec le scheduler.
|
||||
**IMPLEMENTED** — ouverture/fermeture avec le projet, identité INI/DB cohérente,
|
||||
publication de checkpoints par le projet et inventaire de reprise validé.
|
||||
|
||||
**NOT_YET_WIRED** — reconstruction des callbacks/userdata métier, resoumission
|
||||
scheduler, autosave à toutes les transitions et réconciliation des checkpoints
|
||||
orphelins, ScanSet et catalogue image persistants, Feature Store et Visual Index.
|
||||
|
||||
**PLANNED** — migrations v2+, dépendances d'artefacts, graphe géométrique et
|
||||
reconstruction incrémentale.
|
||||
|
|
|
|||
|
|
@ -75,11 +75,24 @@ qu'à la terminaison de cette tentative.
|
|||
|
||||
**IMPLEMENTED** — snapshot, codec v1 et restauration isolée.
|
||||
|
||||
**NOT_YET_WIRED** — sauvegarde périodique, chargement de projet et resoumission
|
||||
automatique.
|
||||
**NOT_YET_WIRED** — sauvegarde périodique, reconstruction métier des tâches et
|
||||
resoumission automatique.
|
||||
|
||||
**PLANNED** — reprise globale du scheduler via la Project Database.
|
||||
|
||||
## Accès Project Database
|
||||
|
||||
**IMPLEMENTED** — une connexion SQLite opaque sérialisée par mutex interne ;
|
||||
les opérations multi-tables sont transactionnelles et bornées.
|
||||
|
||||
**IMPLEMENTED** — le cycle de vie projet ouvre/crée `project.db`, vérifie
|
||||
l'identité et ferme la connexion. L'application arrête la task queue avant la
|
||||
fermeture finale du projet.
|
||||
|
||||
**NOT_YET_WIRED** — aucune reconstruction de callback/userdata ni resoumission
|
||||
automatique. Une future resoumission devra aussi permettre à la queue de
|
||||
conserver l'identifiant stable restauré au lieu d'en assigner un nouveau.
|
||||
|
||||
## Invariants
|
||||
|
||||
- ncurses appartient exclusivement au thread principal
|
||||
|
|
|
|||
|
|
@ -19,40 +19,33 @@ l'ordre FIFO et consulte le gouverneur via le scheduler.
|
|||
|
||||
## Types principaux
|
||||
|
||||
```c
|
||||
typedef struct task_queue task_queue_t;
|
||||
|
||||
typedef void (*task_callback_fn)(task_t *task, task_result_t *result, void *userdata);
|
||||
```
|
||||
Le type public réel est l'opaque `Lardon3DTaskQueue`.
|
||||
|
||||
## API publique
|
||||
|
||||
| Fonction | Description |
|
||||
|---|---|
|
||||
| `task_queue_create()` | Alloue et initialise une file vide |
|
||||
| `task_queue_destroy()` | Libère la file et toutes les tâches restantes |
|
||||
| `task_queue_submit()` | Soumet une tâche à la file (FIFO) |
|
||||
| `task_queue_next()` | Sélectionne la prochaine tâche admissible |
|
||||
| `task_queue_pop()` | Retire et retourne la tâche sélectionnée |
|
||||
| `task_queue_cancel()` | Annule une tâche spécifique dans la file |
|
||||
| `task_queue_cancel_all()` | Annule toutes les tâches en attente |
|
||||
| `task_queue_size()` | Retourne le nombre de tâches en attente |
|
||||
| `task_queue_is_empty()` | Vérifie si la file est vide |
|
||||
| `task_queue_set_callback()` | Définit le callback pour les résultats |
|
||||
| `lardon3d_task_queue_create()` | Crée une file bornée et son worker |
|
||||
| `lardon3d_task_queue_destroy()` | Arrête, annule, attend puis libère la file |
|
||||
| `lardon3d_task_queue_add()` | Ajoute avec backpressure bloquante |
|
||||
| `lardon3d_task_queue_try_add()` | Ajoute sans bloquer si une place existe |
|
||||
| `lardon3d_task_queue_cancel()` | Demande l'annulation par ID |
|
||||
| `lardon3d_task_queue_remove()` | Retire une tâche terminale |
|
||||
| `lardon3d_task_queue_snapshot()` | Copie une vue bornée de la file |
|
||||
|
||||
## Comportement FIFO
|
||||
|
||||
1. `task_queue_submit()` ajoute la tâche en fin de file.
|
||||
2. `task_queue_next()` parcourt la file du début vers la fin.
|
||||
3. La première tâche en état `QUEUED` (non en pause, non annulée) est
|
||||
1. `lardon3d_task_queue_add()` ajoute la tâche en fin de file.
|
||||
2. Le sélecteur interne parcourt la file du début vers la fin.
|
||||
3. La première tâche `PENDING` admissible (non terminale) est
|
||||
retournée.
|
||||
4. Si aucune tâche n'est admissible, `task_queue_next()` retourne `NULL`.
|
||||
4. Si aucune tâche n'est admissible, le worker attend un changement.
|
||||
5. L'ordre de soumission est toujours respecté entre tâches de même priorité.
|
||||
|
||||
## Sélection adaptative
|
||||
|
||||
`task_queue_next()` saute les tâches en état `WAIT` (en attente de
|
||||
ressources) et retourne la première tâche réellement admissible. Cela évite
|
||||
Le sélecteur saute les tâches pour lesquelles le governor répond `WAIT` et
|
||||
retourne la première tâche réellement admissible. Cela évite
|
||||
le blocage par la tête de file lorsqu'une tâche ne peut pas démarrer.
|
||||
|
||||
## Invariants
|
||||
|
|
@ -71,9 +64,9 @@ le blocage par la tête de file lorsqu'une tâche ne peut pas démarrer.
|
|||
|
||||
## Interactions
|
||||
|
||||
- **task** : chaque entrée de la file est un `task_t` avec son état et sa
|
||||
- **task** : chaque entrée de la file est un `Lardon3DTask` avec son état et sa
|
||||
progression.
|
||||
- **scheduler** : le scheduler appelle `task_queue_submit()` et orchestre
|
||||
- **scheduler** : la file applique la soumission FIFO et orchestre
|
||||
l'exécution via le worker.
|
||||
- **resource_governor** : la file consulte le gouverneur (via le scheduler)
|
||||
avant d'exécuter chaque tâche.
|
||||
|
|
@ -90,6 +83,6 @@ annulation coopératives.
|
|||
- Worker unique : pas de parallélisme interne.
|
||||
- Pas de DAG ni de dépendances inter-tâches.
|
||||
- Pas de priorités (FIFO strict).
|
||||
- Pas de persistance des tâches après arrêt.
|
||||
- La reprise depuis Project Database n'est pas encore branchée.
|
||||
- Pas de pool de workers CPU/IO/GPU.
|
||||
- Pas de contre-pression (backpressure) entre étapes.
|
||||
- La backpressure borne les producteurs à la capacité configurée.
|
||||
|
|
|
|||
|
|
@ -75,6 +75,17 @@ adaptatives et fondation de checkpoints persistants isolés.
|
|||
|
||||
**NOT_YET_WIRED** — sauvegarde automatique et restauration par la file.
|
||||
|
||||
La Project Database v1 peut enregistrer transactionnellement un résumé
|
||||
`Lardon3DTaskDurableSnapshot` et la référence de son checkpoint. Elle ne stocke
|
||||
ni estimation sérialisée complète, ni callback, ni réservation, et ne remplace
|
||||
pas la validation du fichier checkpoint avant `task_restore()`.
|
||||
|
||||
`lardon3d_project_checkpoint_task()` est la frontière runtime : elle capture le
|
||||
snapshot, publie le fichier hors mutex de tâche, puis met à jour la DB. L'API
|
||||
d'inventaire retourne des snapshots durables validés, mais ne peut pas appeler
|
||||
`task_restore()` tant que le type métier ne sait pas reconstruire callback et
|
||||
userdata.
|
||||
|
||||
## Limites
|
||||
|
||||
- Aucune priorité interne : l'ordre est uniquement FIFO.
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ Lardon3D suit une feuille de route ordonnée qui privilégie la stabilité et la
|
|||
|
||||
### Phase 3 : Persistance
|
||||
- ✅ Fondation versionnée des checkpoints de tâches
|
||||
- 📋 Branchement des checkpoints aux pipelines et reprise globale
|
||||
- 📋 Project Database v1 (SQLite)
|
||||
- ✅ Project Database v1 (SQLite)
|
||||
- ✅ Branchement Project Database au cycle de vie projet et inventaire de reprise
|
||||
- 📋 Reconstruction métier et resoumission des tâches récupérables
|
||||
- 📋 ScanSet et Image Catalog persistants
|
||||
|
||||
### Phase 4 : Pipeline avancé
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ typedef struct Lardon3DImageCatalog Lardon3DImageCatalog;
|
|||
typedef struct Lardon3DImageView Lardon3DImageView;
|
||||
typedef struct Lardon3DTaskQueue Lardon3DTaskQueue;
|
||||
typedef struct Lardon3DResourceGovernor Lardon3DResourceGovernor;
|
||||
typedef struct Lardon3DProjectDb Lardon3DProjectDb;
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_SCREEN_HOME = 0,
|
||||
|
|
@ -25,12 +26,14 @@ typedef struct {
|
|||
bool project_loaded;
|
||||
char project_name[128];
|
||||
char project_path[PATH_MAX];
|
||||
char project_stable_id[65];
|
||||
char status_message[256];
|
||||
Lardon3DImageCatalog *image_catalog;
|
||||
Lardon3DImageView *image_view;
|
||||
Lardon3DTaskQueue *task_queue;
|
||||
Lardon3DHardwareProfile hardware_profile;
|
||||
Lardon3DResourceGovernor *resource_governor;
|
||||
Lardon3DProjectDb *project_db;
|
||||
} Lardon3DAppState;
|
||||
|
||||
void lardon3d_app_state_init(Lardon3DAppState *state);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,34 @@
|
|||
#include <stdbool.h>
|
||||
|
||||
#include <lardon3d/app_state.h>
|
||||
#include <lardon3d/project_db.h>
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_PROJECT_TASK_CHECKPOINT_OK = 0,
|
||||
LARDON3D_PROJECT_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE,
|
||||
LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT,
|
||||
LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK,
|
||||
LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR,
|
||||
LARDON3D_PROJECT_TASK_CHECKPOINT_DB_BUSY,
|
||||
LARDON3D_PROJECT_TASK_CHECKPOINT_DB_ERROR
|
||||
} Lardon3DProjectTaskCheckpointResult;
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_PROJECT_RECOVERABLE = 0,
|
||||
LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE,
|
||||
LARDON3D_PROJECT_RECOVERY_MISSING_CHECKPOINT,
|
||||
LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT,
|
||||
LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_CHECKPOINT,
|
||||
LARDON3D_PROJECT_RECOVERY_CHECKPOINT_IO_ERROR
|
||||
} Lardon3DProjectRecoveryStatus;
|
||||
|
||||
typedef struct {
|
||||
uint64_t task_id;
|
||||
char name[LARDON3D_TASK_NAME_CAPACITY];
|
||||
Lardon3DProjectRecoveryStatus status;
|
||||
Lardon3DProjectDbCheckpointDurability durability;
|
||||
Lardon3DTaskDurableSnapshot snapshot;
|
||||
} Lardon3DProjectRecoveryEntry;
|
||||
|
||||
bool lardon3d_project_create(
|
||||
Lardon3DAppState *state,
|
||||
|
|
@ -16,5 +44,16 @@ bool lardon3d_project_open(
|
|||
);
|
||||
|
||||
void lardon3d_project_close(Lardon3DAppState *state);
|
||||
Lardon3DProjectTaskCheckpointResult lardon3d_project_checkpoint_task(
|
||||
Lardon3DAppState *state,
|
||||
const Lardon3DTask *task
|
||||
);
|
||||
Lardon3DProjectDbResult lardon3d_project_list_recoverable(
|
||||
Lardon3DAppState *state,
|
||||
uint64_t after_task_id,
|
||||
Lardon3DProjectRecoveryEntry *entries,
|
||||
size_t capacity,
|
||||
size_t *count
|
||||
);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
136
include/lardon3d/project_db.h
Normal file
136
include/lardon3d/project_db.h
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
#ifndef LARDON3D_PROJECT_DB_H
|
||||
#define LARDON3D_PROJECT_DB_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <lardon3d/task.h>
|
||||
|
||||
enum {
|
||||
LARDON3D_PROJECT_DB_SCHEMA_VERSION = 1,
|
||||
LARDON3D_PROJECT_DB_ID_CAPACITY = 65,
|
||||
LARDON3D_PROJECT_DB_KIND_CAPACITY = 65,
|
||||
LARDON3D_PROJECT_DB_PATH_CAPACITY = 4096,
|
||||
LARDON3D_PROJECT_DB_ERROR_CAPACITY = 256,
|
||||
LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX = 256,
|
||||
};
|
||||
|
||||
typedef struct Lardon3DProjectDb Lardon3DProjectDb;
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_PROJECT_DB_OK = 0,
|
||||
LARDON3D_PROJECT_DB_INVALID_ARGUMENT,
|
||||
LARDON3D_PROJECT_DB_NOT_FOUND,
|
||||
LARDON3D_PROJECT_DB_BUSY,
|
||||
LARDON3D_PROJECT_DB_UNSUPPORTED_SCHEMA,
|
||||
LARDON3D_PROJECT_DB_CORRUPT,
|
||||
LARDON3D_PROJECT_DB_CONSTRAINT,
|
||||
LARDON3D_PROJECT_DB_IO_ERROR
|
||||
} Lardon3DProjectDbResult;
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_DB_CHECKPOINT_DURABLE = 0,
|
||||
LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE
|
||||
} Lardon3DProjectDbCheckpointDurability;
|
||||
|
||||
typedef enum {
|
||||
LARDON3D_DB_ARTIFACT_STAGED = 0,
|
||||
LARDON3D_DB_ARTIFACT_READY
|
||||
} Lardon3DProjectDbArtifactState;
|
||||
|
||||
typedef struct {
|
||||
char stable_id[LARDON3D_PROJECT_DB_ID_CAPACITY];
|
||||
char name[LARDON3D_TASK_NAME_CAPACITY];
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
} Lardon3DProjectDbProject;
|
||||
|
||||
typedef struct {
|
||||
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||
uint32_t format_version;
|
||||
Lardon3DProjectDbCheckpointDurability durability;
|
||||
int64_t updated_at;
|
||||
} Lardon3DProjectDbCheckpoint;
|
||||
|
||||
typedef struct {
|
||||
uint64_t task_id;
|
||||
char name[LARDON3D_TASK_NAME_CAPACITY];
|
||||
Lardon3DTaskState saved_state;
|
||||
Lardon3DTaskState recovery_state;
|
||||
unsigned int progress;
|
||||
unsigned int sequence_count;
|
||||
struct timespec started_at;
|
||||
struct timespec finished_at;
|
||||
int64_t updated_at;
|
||||
bool has_checkpoint;
|
||||
Lardon3DProjectDbCheckpoint checkpoint;
|
||||
} Lardon3DProjectDbTask;
|
||||
|
||||
typedef struct {
|
||||
char artifact_id[LARDON3D_PROJECT_DB_ID_CAPACITY];
|
||||
char kind[LARDON3D_PROJECT_DB_KIND_CAPACITY];
|
||||
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||
Lardon3DProjectDbArtifactState state;
|
||||
uint64_t size_bytes;
|
||||
bool has_producer_task;
|
||||
uint64_t producer_task_id;
|
||||
int64_t created_at;
|
||||
int64_t updated_at;
|
||||
} Lardon3DProjectDbArtifact;
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_open(
|
||||
const char *path,
|
||||
Lardon3DProjectDb **database,
|
||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]
|
||||
);
|
||||
void lardon3d_project_db_close(Lardon3DProjectDb *database);
|
||||
bool lardon3d_project_db_last_error(
|
||||
Lardon3DProjectDb *database,
|
||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY]
|
||||
);
|
||||
unsigned int lardon3d_project_db_schema_version(Lardon3DProjectDb *database);
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_set_project(
|
||||
Lardon3DProjectDb *database,
|
||||
const Lardon3DProjectDbProject *project
|
||||
);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_get_project(
|
||||
Lardon3DProjectDb *database,
|
||||
Lardon3DProjectDbProject *project
|
||||
);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_record_task(
|
||||
Lardon3DProjectDb *database,
|
||||
const Lardon3DTaskDurableSnapshot *snapshot,
|
||||
const Lardon3DProjectDbCheckpoint *checkpoint,
|
||||
int64_t updated_at
|
||||
);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_load_task(
|
||||
Lardon3DProjectDb *database,
|
||||
uint64_t task_id,
|
||||
Lardon3DProjectDbTask *task
|
||||
);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_list_recoverable(
|
||||
Lardon3DProjectDb *database,
|
||||
uint64_t after_task_id,
|
||||
Lardon3DProjectDbTask *tasks,
|
||||
size_t capacity,
|
||||
size_t *count
|
||||
);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_create_artifact(
|
||||
Lardon3DProjectDb *database,
|
||||
const Lardon3DProjectDbArtifact *artifact
|
||||
);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_mark_artifact_ready(
|
||||
Lardon3DProjectDb *database,
|
||||
const char *artifact_id,
|
||||
int64_t updated_at
|
||||
);
|
||||
Lardon3DProjectDbResult lardon3d_project_db_load_artifact(
|
||||
Lardon3DProjectDb *database,
|
||||
const char *artifact_id,
|
||||
Lardon3DProjectDbArtifact *artifact
|
||||
);
|
||||
|
||||
#endif
|
||||
41
meson.build
41
meson.build
|
|
@ -20,6 +20,7 @@ add_project_arguments(
|
|||
|
||||
ncursesw = dependency('ncursesw', required: true)
|
||||
threads = dependency('threads')
|
||||
sqlite3 = dependency('sqlite3', required: true)
|
||||
|
||||
executable(
|
||||
'lardon3d',
|
||||
|
|
@ -34,6 +35,7 @@ executable(
|
|||
'src/image_catalog.c',
|
||||
'src/image_view.c',
|
||||
'src/project.c',
|
||||
'src/project_db.c',
|
||||
'src/task.c',
|
||||
'src/task_checkpoint.c',
|
||||
'src/task_queue.c',
|
||||
|
|
@ -42,7 +44,7 @@ executable(
|
|||
'src/hardware_profile.c',
|
||||
],
|
||||
include_directories: include_directories('include'),
|
||||
dependencies: [ncursesw, threads],
|
||||
dependencies: [ncursesw, threads, sqlite3],
|
||||
)
|
||||
|
||||
import_test = executable(
|
||||
|
|
@ -126,6 +128,43 @@ task_checkpoint_test = executable(
|
|||
|
||||
test('task-checkpoint', task_checkpoint_test, timeout: 30)
|
||||
|
||||
project_db_test = executable(
|
||||
'test-project-db',
|
||||
sources: [
|
||||
'tests/test_project_db.c',
|
||||
'src/project_db.c',
|
||||
],
|
||||
c_args: ['-DLARDON3D_PROJECT_DB_TESTING'],
|
||||
include_directories: include_directories('include'),
|
||||
dependencies: [threads, sqlite3],
|
||||
)
|
||||
|
||||
test('project-db', project_db_test, timeout: 30)
|
||||
|
||||
project_test = executable(
|
||||
'test-project',
|
||||
sources: [
|
||||
'tests/test_project.c',
|
||||
'src/app_state.c',
|
||||
'src/project.c',
|
||||
'src/project_db.c',
|
||||
'src/task.c',
|
||||
'src/task_checkpoint.c',
|
||||
'src/image_catalog.c',
|
||||
'src/image_view.c',
|
||||
'src/resource_governor.c',
|
||||
'src/resource_snapshot.c',
|
||||
],
|
||||
c_args: [
|
||||
'-DLARDON3D_PROJECT_DB_TESTING',
|
||||
'-DLARDON3D_CHECKPOINT_TESTING',
|
||||
],
|
||||
include_directories: include_directories('include'),
|
||||
dependencies: [threads, sqlite3],
|
||||
)
|
||||
|
||||
test('project', project_test, timeout: 30)
|
||||
|
||||
sequential_task_test = executable(
|
||||
'test-sequential-task',
|
||||
sources: [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <lardon3d/app_state.h>
|
||||
#include <lardon3d/image_catalog.h>
|
||||
#include <lardon3d/image_view.h>
|
||||
#include <lardon3d/project.h>
|
||||
#include <lardon3d/resource_governor.h>
|
||||
#include <lardon3d/task_queue.h>
|
||||
#include <lardon3d/tui.h>
|
||||
|
|
@ -51,10 +52,12 @@ lardon3d_app_run(void)
|
|||
}
|
||||
|
||||
bool success = lardon3d_tui_run(&state);
|
||||
lardon3d_image_view_destroy(state.image_view);
|
||||
lardon3d_image_catalog_destroy(state.image_catalog);
|
||||
lardon3d_tui_shutdown();
|
||||
lardon3d_task_queue_destroy(state.task_queue);
|
||||
state.task_queue = NULL;
|
||||
if (state.project_loaded) {
|
||||
lardon3d_project_close(&state);
|
||||
}
|
||||
lardon3d_resource_governor_destroy(state.resource_governor);
|
||||
|
||||
return success ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ lardon3d_app_state_init(Lardon3DAppState *state)
|
|||
.project_loaded = false,
|
||||
.project_name = "",
|
||||
.project_path = "",
|
||||
.project_stable_id = "",
|
||||
.status_message = "Bienvenue dans Lardon3D",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
480
src/project.c
480
src/project.c
|
|
@ -1,17 +1,21 @@
|
|||
#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/types.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <lardon3d/project.h>
|
||||
#include <lardon3d/image_catalog.h>
|
||||
#include <lardon3d/image_view.h>
|
||||
#include <lardon3d/project_db.h>
|
||||
#include <lardon3d/task_checkpoint.h>
|
||||
|
||||
enum {
|
||||
MAX_CREATED_DIRECTORIES = 16,
|
||||
|
|
@ -23,6 +27,12 @@ typedef struct {
|
|||
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)
|
||||
{
|
||||
|
|
@ -34,6 +44,28 @@ set_status(Lardon3DAppState *state, const char *message)
|
|||
);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
@ -113,6 +145,35 @@ join_path(
|
|||
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])
|
||||
{
|
||||
|
|
@ -224,7 +285,8 @@ static bool
|
|||
write_project_ini(
|
||||
Lardon3DAppState *state,
|
||||
const char *project_path,
|
||||
const char *project_name
|
||||
const char *project_name,
|
||||
const char *stable_id
|
||||
)
|
||||
{
|
||||
char temporary_path[PATH_MAX];
|
||||
|
|
@ -256,8 +318,9 @@ write_project_ini(
|
|||
|
||||
bool success = fprintf(
|
||||
file,
|
||||
"[project]\nname=%s\nversion=1\n",
|
||||
project_name
|
||||
"[project]\nname=%s\nstable_id=%s\nversion=2\n",
|
||||
project_name,
|
||||
stable_id
|
||||
) >= 0;
|
||||
if (success) {
|
||||
success = fflush(file) == 0;
|
||||
|
|
@ -284,10 +347,15 @@ 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,
|
||||
|
|
@ -299,6 +367,10 @@ lardon3d_project_create(Lardon3DAppState *state, const char *name)
|
|||
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),
|
||||
|
|
@ -331,6 +403,8 @@ lardon3d_project_create(Lardon3DAppState *state, const char *name)
|
|||
"reconstruction",
|
||||
"exports",
|
||||
"logs",
|
||||
".lardon3d",
|
||||
".lardon3d/checkpoints",
|
||||
};
|
||||
for (size_t index = 0;
|
||||
index < sizeof(subdirectories) / sizeof(subdirectories[0]);
|
||||
|
|
@ -352,13 +426,56 @@ lardon3d_project_create(Lardon3DAppState *state, const char *name)
|
|||
}
|
||||
}
|
||||
|
||||
if (!write_project_ini(state, project_path, normalized_name)) {
|
||||
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;
|
||||
state->project_db = database;
|
||||
(void)copy_path(
|
||||
state->project_name,
|
||||
sizeof(state->project_name),
|
||||
|
|
@ -369,6 +486,12 @@ lardon3d_project_create(Lardon3DAppState *state, const char *name)
|
|||
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),
|
||||
|
|
@ -382,7 +505,7 @@ static bool
|
|||
read_project_ini(
|
||||
Lardon3DAppState *state,
|
||||
const char *path,
|
||||
char project_name[128]
|
||||
ProjectMetadata *metadata
|
||||
)
|
||||
{
|
||||
int descriptor = open(path, O_RDONLY | O_NOFOLLOW);
|
||||
|
|
@ -408,6 +531,7 @@ read_project_ini(
|
|||
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];
|
||||
|
|
@ -433,14 +557,45 @@ read_project_ini(
|
|||
if (name_found || !normalize_name(
|
||||
state,
|
||||
line + 5,
|
||||
project_name,
|
||||
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) {
|
||||
(void)snprintf(
|
||||
metadata->stable_id,
|
||||
sizeof(metadata->stable_id),
|
||||
"%s",
|
||||
line + 10
|
||||
);
|
||||
}
|
||||
}
|
||||
stable_id_found = true;
|
||||
} else if (in_project_section && strncmp(line, "version=", 8) == 0) {
|
||||
if (version_found || strcmp(line, "version=1") != 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;
|
||||
|
|
@ -453,7 +608,9 @@ read_project_ini(
|
|||
if (fclose(file) != 0) {
|
||||
valid = false;
|
||||
}
|
||||
if (!valid || !section_found || !name_found || !version_found) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -469,11 +626,16 @@ lardon3d_project_open(
|
|||
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,
|
||||
|
|
@ -491,7 +653,8 @@ lardon3d_project_open(
|
|||
root,
|
||||
normalized_directory
|
||||
)
|
||||
|| !join_path(ini_path, sizeof(ini_path), project_path, "project.ini")) {
|
||||
|| !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;
|
||||
}
|
||||
|
|
@ -502,17 +665,133 @@ lardon3d_project_open(
|
|||
return false;
|
||||
}
|
||||
|
||||
char project_name[sizeof(state->project_name)];
|
||||
if (!read_project_ini(state, ini_path, project_name)) {
|
||||
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),
|
||||
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,
|
||||
|
|
@ -541,8 +820,185 @@ lardon3d_project_close(Lardon3DAppState *state)
|
|||
}
|
||||
|
||||
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';
|
||||
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);
|
||||
}
|
||||
|
||||
Lardon3DProjectTaskCheckpointResult
|
||||
lardon3d_project_checkpoint_task(
|
||||
Lardon3DAppState *state,
|
||||
const Lardon3DTask *task
|
||||
)
|
||||
{
|
||||
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 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;
|
||||
}
|
||||
Lardon3DTaskCheckpointResult saved = lardon3d_task_checkpoint_save(
|
||||
absolute,
|
||||
&snapshot
|
||||
);
|
||||
if (saved == LARDON3D_TASK_CHECKPOINT_INVALID) {
|
||||
return LARDON3D_PROJECT_TASK_CHECKPOINT_INVALID_TASK;
|
||||
}
|
||||
if (saved != LARDON3D_TASK_CHECKPOINT_OK
|
||||
&& saved != LARDON3D_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE) {
|
||||
return LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR;
|
||||
}
|
||||
struct timespec now;
|
||||
if (clock_gettime(CLOCK_REALTIME, &now) != 0) {
|
||||
return LARDON3D_PROJECT_TASK_CHECKPOINT_DB_ERROR;
|
||||
}
|
||||
Lardon3DProjectDbCheckpoint checkpoint = {
|
||||
.format_version = LARDON3D_TASK_CHECKPOINT_VERSION,
|
||||
.durability = saved == LARDON3D_TASK_CHECKPOINT_OK
|
||||
? LARDON3D_DB_CHECKPOINT_DURABLE
|
||||
: LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE,
|
||||
.updated_at = now.tv_sec,
|
||||
};
|
||||
(void)snprintf(checkpoint.path, sizeof(checkpoint.path), "%s", relative);
|
||||
Lardon3DProjectDbResult recorded = lardon3d_project_db_record_task(
|
||||
state->project_db,
|
||||
&snapshot,
|
||||
&checkpoint,
|
||||
now.tv_sec
|
||||
);
|
||||
if (recorded == LARDON3D_PROJECT_DB_BUSY) {
|
||||
return LARDON3D_PROJECT_TASK_CHECKPOINT_DB_BUSY;
|
||||
}
|
||||
if (recorded != LARDON3D_PROJECT_DB_OK) {
|
||||
return LARDON3D_PROJECT_TASK_CHECKPOINT_DB_ERROR;
|
||||
}
|
||||
return saved == LARDON3D_TASK_CHECKPOINT_OK
|
||||
? LARDON3D_PROJECT_TASK_CHECKPOINT_OK
|
||||
: LARDON3D_PROJECT_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE;
|
||||
}
|
||||
|
||||
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,
|
||||
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 || !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);
|
||||
char relative[LARDON3D_PROJECT_DB_PATH_CAPACITY];
|
||||
char absolute[PATH_MAX];
|
||||
if (!checkpoint_paths(state, entry->task_id, relative, absolute)
|
||||
|| strcmp(relative, tasks[index].checkpoint.path) != 0) {
|
||||
entry->status = LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT;
|
||||
} else {
|
||||
uint32_t version = 0;
|
||||
Lardon3DTaskCheckpointResult loaded = lardon3d_task_checkpoint_load(
|
||||
absolute,
|
||||
&entry->snapshot,
|
||||
&version
|
||||
);
|
||||
if (loaded == LARDON3D_TASK_CHECKPOINT_NOT_FOUND) {
|
||||
entry->status = LARDON3D_PROJECT_RECOVERY_MISSING_CHECKPOINT;
|
||||
} else if (loaded == LARDON3D_TASK_CHECKPOINT_UNSUPPORTED_VERSION) {
|
||||
entry->status = LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_CHECKPOINT;
|
||||
} else if (loaded == LARDON3D_TASK_CHECKPOINT_INVALID) {
|
||||
entry->status = LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT;
|
||||
} else if (loaded != LARDON3D_TASK_CHECKPOINT_OK
|
||||
|| version != tasks[index].checkpoint.format_version
|
||||
|| !coherent_recovery(&tasks[index], &entry->snapshot)) {
|
||||
entry->status = loaded == LARDON3D_TASK_CHECKPOINT_IO_ERROR
|
||||
? LARDON3D_PROJECT_RECOVERY_CHECKPOINT_IO_ERROR
|
||||
: LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT;
|
||||
} else {
|
||||
entry->status = entry->durability
|
||||
== LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE
|
||||
? LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE
|
||||
: LARDON3D_PROJECT_RECOVERABLE;
|
||||
}
|
||||
}
|
||||
cursor = entry->task_id;
|
||||
++*count;
|
||||
}
|
||||
if (task_count < requested) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return LARDON3D_PROJECT_DB_OK;
|
||||
}
|
||||
|
|
|
|||
504
src/project_db.c
Normal file
504
src/project_db.c
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <pthread.h>
|
||||
#include <sqlite3.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <lardon3d/project_db.h>
|
||||
|
||||
struct Lardon3DProjectDb {
|
||||
sqlite3 *connection;
|
||||
pthread_mutex_t mutex;
|
||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||
};
|
||||
|
||||
static const char schema_v1[] =
|
||||
"CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||
"INSERT INTO metadata(key,value) VALUES('schema_version',1);"
|
||||
"CREATE TABLE project(singleton INTEGER PRIMARY KEY CHECK(singleton=1),"
|
||||
"stable_id TEXT NOT NULL UNIQUE,name TEXT NOT NULL,created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
||||
"CREATE TABLE tasks(task_id INTEGER PRIMARY KEY CHECK(task_id>0),name TEXT NOT NULL,"
|
||||
"saved_state INTEGER NOT NULL CHECK(saved_state BETWEEN 0 AND 5),"
|
||||
"recovery_state INTEGER NOT NULL CHECK(recovery_state BETWEEN 0 AND 5),"
|
||||
"progress INTEGER NOT NULL CHECK(progress BETWEEN 0 AND 100),sequence_count INTEGER NOT NULL CHECK(sequence_count>=0),"
|
||||
"started_sec INTEGER NOT NULL,started_nsec INTEGER NOT NULL CHECK(started_nsec BETWEEN 0 AND 999999999),"
|
||||
"finished_sec INTEGER NOT NULL,finished_nsec INTEGER NOT NULL CHECK(finished_nsec BETWEEN 0 AND 999999999),updated_at INTEGER NOT NULL);"
|
||||
"CREATE INDEX tasks_recovery_state_idx ON tasks(recovery_state,task_id);"
|
||||
"CREATE TABLE checkpoints(task_id INTEGER PRIMARY KEY REFERENCES tasks(task_id) ON DELETE CASCADE,"
|
||||
"path TEXT NOT NULL,format_version INTEGER NOT NULL CHECK(format_version>0),"
|
||||
"durability INTEGER NOT NULL CHECK(durability BETWEEN 0 AND 1),updated_at INTEGER NOT NULL);"
|
||||
"CREATE TABLE artifacts(artifact_id TEXT PRIMARY KEY,kind TEXT NOT NULL,path TEXT NOT NULL,"
|
||||
"state INTEGER NOT NULL CHECK(state BETWEEN 0 AND 1),size_bytes INTEGER NOT NULL CHECK(size_bytes>=0),"
|
||||
"producer_task_id INTEGER REFERENCES tasks(task_id),created_at INTEGER NOT NULL,updated_at INTEGER NOT NULL);"
|
||||
"CREATE INDEX artifacts_state_idx ON artifacts(state,artifact_id);"
|
||||
"CREATE INDEX artifacts_producer_idx ON artifacts(producer_task_id);";
|
||||
|
||||
static void
|
||||
copy_error(char destination[LARDON3D_PROJECT_DB_ERROR_CAPACITY], const char *text)
|
||||
{
|
||||
if (destination) {
|
||||
(void)snprintf(destination, LARDON3D_PROJECT_DB_ERROR_CAPACITY, "%s", text ? text : "");
|
||||
}
|
||||
}
|
||||
|
||||
static Lardon3DProjectDbResult
|
||||
sqlite_result(Lardon3DProjectDb *database, int code, const char *context)
|
||||
{
|
||||
(void)snprintf(database->error, sizeof(database->error), "%s: %s", context, sqlite3_errmsg(database->connection));
|
||||
if (code == SQLITE_BUSY || code == SQLITE_LOCKED) return LARDON3D_PROJECT_DB_BUSY;
|
||||
if (code == SQLITE_CONSTRAINT) return LARDON3D_PROJECT_DB_CONSTRAINT;
|
||||
if (code == SQLITE_CORRUPT || code == SQLITE_NOTADB) return LARDON3D_PROJECT_DB_CORRUPT;
|
||||
return LARDON3D_PROJECT_DB_IO_ERROR;
|
||||
}
|
||||
|
||||
static bool
|
||||
bounded_text(const char *text, size_t capacity, bool allow_empty)
|
||||
{
|
||||
if (!text) return false;
|
||||
size_t length = strnlen(text, capacity);
|
||||
return length < capacity && (allow_empty || length > 0);
|
||||
}
|
||||
|
||||
static bool
|
||||
valid_task_id(uint64_t id)
|
||||
{
|
||||
return id > 0 && id <= INT64_MAX;
|
||||
}
|
||||
|
||||
static bool
|
||||
valid_state(Lardon3DTaskState state)
|
||||
{
|
||||
return state >= TASK_PENDING && state <= TASK_COMPLETED;
|
||||
}
|
||||
|
||||
static bool
|
||||
database_time(sqlite3_int64 value, time_t *output)
|
||||
{
|
||||
if (value < 0) return false;
|
||||
time_t converted = (time_t)value;
|
||||
if (converted < 0 || (uint64_t)converted != (uint64_t)value) return false;
|
||||
*output = converted;
|
||||
return true;
|
||||
}
|
||||
|
||||
static Lardon3DProjectDbResult
|
||||
execute(Lardon3DProjectDb *database, const char *sql, const char *context)
|
||||
{
|
||||
char *message = NULL;
|
||||
int code = sqlite3_exec(database->connection, sql, NULL, NULL, &message);
|
||||
if (code == SQLITE_OK) return LARDON3D_PROJECT_DB_OK;
|
||||
(void)snprintf(database->error, sizeof(database->error), "%s: %s", context, message ? message : sqlite3_errmsg(database->connection));
|
||||
sqlite3_free(message);
|
||||
return sqlite_result(database, code, context);
|
||||
}
|
||||
|
||||
static Lardon3DProjectDbResult
|
||||
prepare(Lardon3DProjectDb *database, const char *sql, sqlite3_stmt **statement)
|
||||
{
|
||||
int code = sqlite3_prepare_v2(database->connection, sql, -1, statement, NULL);
|
||||
return code == SQLITE_OK ? LARDON3D_PROJECT_DB_OK : sqlite_result(database, code, "prepare");
|
||||
}
|
||||
|
||||
static bool
|
||||
table_exists(sqlite3 *connection, const char *name)
|
||||
{
|
||||
sqlite3_stmt *statement = NULL;
|
||||
if (sqlite3_prepare_v2(connection, "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1", -1, &statement, NULL) != SQLITE_OK) return false;
|
||||
(void)sqlite3_bind_text(statement, 1, name, -1, SQLITE_STATIC);
|
||||
bool exists = sqlite3_step(statement) == SQLITE_ROW;
|
||||
(void)sqlite3_finalize(statement);
|
||||
return exists;
|
||||
}
|
||||
|
||||
static Lardon3DProjectDbResult
|
||||
migrate(Lardon3DProjectDb *database, unsigned int from_version)
|
||||
{
|
||||
if (from_version > LARDON3D_PROJECT_DB_SCHEMA_VERSION) {
|
||||
copy_error(database->error, "Version de schéma future non supportée.");
|
||||
return LARDON3D_PROJECT_DB_UNSUPPORTED_SCHEMA;
|
||||
}
|
||||
if (from_version == 1) return LARDON3D_PROJECT_DB_OK;
|
||||
if (from_version != 0) return LARDON3D_PROJECT_DB_CORRUPT;
|
||||
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin migration");
|
||||
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, schema_v1, "create schema v1");
|
||||
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit migration");
|
||||
if (result != LARDON3D_PROJECT_DB_OK) (void)execute(database, "ROLLBACK", "rollback migration");
|
||||
return result;
|
||||
}
|
||||
|
||||
static Lardon3DProjectDbResult
|
||||
read_schema_version(Lardon3DProjectDb *database, unsigned int *version)
|
||||
{
|
||||
if (!table_exists(database->connection, "metadata")) {
|
||||
sqlite3_stmt *statement = NULL;
|
||||
int code = sqlite3_prepare_v2(database->connection, "SELECT 1 FROM sqlite_master WHERE type='table' LIMIT 1", -1, &statement, NULL);
|
||||
if (code != SQLITE_OK) return sqlite_result(database, code, "inspect schema");
|
||||
code = sqlite3_step(statement);
|
||||
bool empty = code == SQLITE_DONE;
|
||||
(void)sqlite3_finalize(statement);
|
||||
if (!empty) {
|
||||
copy_error(database->error, "Base incohérente sans metadata.");
|
||||
return LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
*version = 0;
|
||||
return LARDON3D_PROJECT_DB_OK;
|
||||
}
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(database, "SELECT value FROM metadata WHERE key='schema_version'", &statement);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) return result;
|
||||
int code = sqlite3_step(statement);
|
||||
if (code != SQLITE_ROW || sqlite3_column_type(statement, 0) != SQLITE_INTEGER) {
|
||||
(void)sqlite3_finalize(statement);
|
||||
copy_error(database->error, "schema_version absente ou invalide.");
|
||||
return LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
sqlite3_int64 value = sqlite3_column_int64(statement, 0);
|
||||
code = sqlite3_step(statement);
|
||||
(void)sqlite3_finalize(statement);
|
||||
if (code != SQLITE_DONE || value < 1 || value > UINT_MAX) return LARDON3D_PROJECT_DB_CORRUPT;
|
||||
*version = (unsigned int)value;
|
||||
return LARDON3D_PROJECT_DB_OK;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_open(const char *path, Lardon3DProjectDb **output, char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY])
|
||||
{
|
||||
if (output) *output = NULL;
|
||||
if (!path || !path[0] || !output || strnlen(path, LARDON3D_PROJECT_DB_PATH_CAPACITY) >= LARDON3D_PROJECT_DB_PATH_CAPACITY) {
|
||||
copy_error(error, "Chemin de base invalide.");
|
||||
return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
}
|
||||
Lardon3DProjectDb *database = calloc(1, sizeof(*database));
|
||||
if (!database) { copy_error(error, "Allocation impossible."); return LARDON3D_PROJECT_DB_IO_ERROR; }
|
||||
if (pthread_mutex_init(&database->mutex, NULL) != 0) { free(database); copy_error(error, "Mutex impossible."); return LARDON3D_PROJECT_DB_IO_ERROR; }
|
||||
int code = sqlite3_open_v2(path, &database->connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX, NULL);
|
||||
if (code != SQLITE_OK) {
|
||||
copy_error(error, database->connection ? sqlite3_errmsg(database->connection) : "Ouverture SQLite impossible.");
|
||||
if (database->connection) (void)sqlite3_close(database->connection);
|
||||
(void)pthread_mutex_destroy(&database->mutex); free(database);
|
||||
return code == SQLITE_CANTOPEN ? LARDON3D_PROJECT_DB_IO_ERROR : LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
Lardon3DProjectDbResult result = execute(database, "PRAGMA foreign_keys=ON;PRAGMA journal_mode=DELETE;PRAGMA synchronous=FULL;PRAGMA busy_timeout=5000", "configure SQLite");
|
||||
unsigned int version = 0;
|
||||
if (result == LARDON3D_PROJECT_DB_OK) result = read_schema_version(database, &version);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) result = migrate(database, version);
|
||||
if (result == LARDON3D_PROJECT_DB_OK && version == 1) {
|
||||
const char *required[] = {"project", "tasks", "checkpoints", "artifacts"};
|
||||
for (size_t index = 0; index < 4 && result == LARDON3D_PROJECT_DB_OK; ++index) {
|
||||
if (!table_exists(database->connection, required[index])) {
|
||||
copy_error(database->error, "Schéma v1 incomplet.");
|
||||
result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result != LARDON3D_PROJECT_DB_OK) {
|
||||
copy_error(error, database->error); (void)sqlite3_close(database->connection);
|
||||
(void)pthread_mutex_destroy(&database->mutex); free(database); return result;
|
||||
}
|
||||
database->error[0] = '\0'; copy_error(error, ""); *output = database;
|
||||
return LARDON3D_PROJECT_DB_OK;
|
||||
}
|
||||
|
||||
void lardon3d_project_db_close(Lardon3DProjectDb *database)
|
||||
{
|
||||
if (!database) return;
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
(void)sqlite3_close(database->connection);
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
(void)pthread_mutex_destroy(&database->mutex);
|
||||
free(database);
|
||||
}
|
||||
|
||||
bool
|
||||
lardon3d_project_db_last_error(Lardon3DProjectDb *database, char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY])
|
||||
{
|
||||
if (!database || !error) return false;
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
copy_error(error, database->error);
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return true;
|
||||
}
|
||||
unsigned int lardon3d_project_db_schema_version(Lardon3DProjectDb *database) { return database ? 1U : 0U; }
|
||||
|
||||
static Lardon3DProjectDbResult
|
||||
step_done(Lardon3DProjectDb *database, sqlite3_stmt *statement, const char *context)
|
||||
{
|
||||
int code = sqlite3_step(statement);
|
||||
if (code != SQLITE_DONE) {
|
||||
Lardon3DProjectDbResult result = sqlite_result(database, code, context);
|
||||
(void)sqlite3_finalize(statement);
|
||||
return result;
|
||||
}
|
||||
code = sqlite3_finalize(statement);
|
||||
return code == SQLITE_OK ? LARDON3D_PROJECT_DB_OK : sqlite_result(database, code, context);
|
||||
}
|
||||
|
||||
static bool
|
||||
copy_column(sqlite3_stmt *statement, int column, char *destination, size_t capacity)
|
||||
{
|
||||
if (sqlite3_column_type(statement, column) != SQLITE_TEXT) return false;
|
||||
int bytes = sqlite3_column_bytes(statement, column);
|
||||
const unsigned char *text = sqlite3_column_text(statement, column);
|
||||
if (!text || bytes < 0 || (size_t)bytes >= capacity) return false;
|
||||
memcpy(destination, text, (size_t)bytes);
|
||||
destination[bytes] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_set_project(Lardon3DProjectDb *database, const Lardon3DProjectDbProject *project)
|
||||
{
|
||||
if (!database || !project
|
||||
|| !bounded_text(project->stable_id, sizeof(project->stable_id), false)
|
||||
|| !bounded_text(project->name, sizeof(project->name), false)
|
||||
|| project->created_at < 0 || project->updated_at < project->created_at) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(database,
|
||||
"INSERT INTO project(singleton,stable_id,name,created_at,updated_at) VALUES(1,?1,?2,?3,?4) "
|
||||
"ON CONFLICT(singleton) DO UPDATE SET name=excluded.name,updated_at=excluded.updated_at "
|
||||
"WHERE project.stable_id=excluded.stable_id AND project.created_at=excluded.created_at", &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
(void)sqlite3_bind_text(statement, 1, project->stable_id, -1, SQLITE_TRANSIENT);
|
||||
(void)sqlite3_bind_text(statement, 2, project->name, -1, SQLITE_TRANSIENT);
|
||||
(void)sqlite3_bind_int64(statement, 3, project->created_at);
|
||||
(void)sqlite3_bind_int64(statement, 4, project->updated_at);
|
||||
result = step_done(database, statement, "set project");
|
||||
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) {
|
||||
copy_error(database->error, "Identité de projet contradictoire.");
|
||||
result = LARDON3D_PROJECT_DB_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_get_project(Lardon3DProjectDb *database, Lardon3DProjectDbProject *project)
|
||||
{
|
||||
if (!database || !project) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(database, "SELECT stable_id,name,created_at,updated_at FROM project WHERE singleton=1", &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
int code = sqlite3_step(statement);
|
||||
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
|
||||
else if (code != SQLITE_ROW || !copy_column(statement, 0, project->stable_id, sizeof(project->stable_id))
|
||||
|| !copy_column(statement, 1, project->name, sizeof(project->name))) result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
else { project->created_at = sqlite3_column_int64(statement, 2); project->updated_at = sqlite3_column_int64(statement, 3); }
|
||||
(void)sqlite3_finalize(statement);
|
||||
}
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool
|
||||
valid_checkpoint(const Lardon3DProjectDbCheckpoint *checkpoint)
|
||||
{
|
||||
return checkpoint && bounded_text(checkpoint->path, sizeof(checkpoint->path), false)
|
||||
&& checkpoint->format_version > 0 && checkpoint->updated_at >= 0
|
||||
&& checkpoint->durability >= LARDON3D_DB_CHECKPOINT_DURABLE
|
||||
&& checkpoint->durability <= LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE;
|
||||
}
|
||||
|
||||
static bool
|
||||
valid_durable_task(const Lardon3DTaskDurableSnapshot *snapshot, int64_t updated_at)
|
||||
{
|
||||
return snapshot && valid_task_id(snapshot->id)
|
||||
&& bounded_text(snapshot->name, sizeof(snapshot->name), false)
|
||||
&& valid_state(snapshot->saved_state) && valid_state(snapshot->recovery_state)
|
||||
&& snapshot->recovery_state == ((snapshot->saved_state == TASK_RUNNING || snapshot->saved_state == TASK_PAUSED) ? TASK_PENDING : snapshot->saved_state)
|
||||
&& snapshot->progress <= 100 && snapshot->sequence_count <= INT_MAX
|
||||
&& snapshot->started_at.tv_sec >= 0 && snapshot->started_at.tv_sec <= INT64_MAX
|
||||
&& snapshot->started_at.tv_nsec >= 0 && snapshot->started_at.tv_nsec < 1000000000L
|
||||
&& snapshot->finished_at.tv_sec >= 0 && snapshot->finished_at.tv_sec <= INT64_MAX
|
||||
&& snapshot->finished_at.tv_nsec >= 0 && snapshot->finished_at.tv_nsec < 1000000000L
|
||||
&& updated_at >= 0;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_record_task(Lardon3DProjectDb *database, const Lardon3DTaskDurableSnapshot *snapshot,
|
||||
const Lardon3DProjectDbCheckpoint *checkpoint, int64_t updated_at)
|
||||
{
|
||||
if (!database || !valid_durable_task(snapshot, updated_at) || (checkpoint && !valid_checkpoint(checkpoint))) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||
const char *forced_busy = getenv("LARDON3D_TEST_PROJECT_DB_BUSY_CHECKPOINT");
|
||||
if (forced_busy && checkpoint && strcmp(forced_busy, "1") == 0) {
|
||||
copy_error(database->error, "Verrou DB injecté pour test.");
|
||||
return LARDON3D_PROJECT_DB_BUSY;
|
||||
}
|
||||
#endif
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
Lardon3DProjectDbResult result = execute(database, "BEGIN IMMEDIATE", "begin task record");
|
||||
sqlite3_stmt *statement = NULL;
|
||||
if (result == LARDON3D_PROJECT_DB_OK) result = prepare(database,
|
||||
"INSERT INTO tasks(task_id,name,saved_state,recovery_state,progress,sequence_count,started_sec,started_nsec,finished_sec,finished_nsec,updated_at)"
|
||||
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11) ON CONFLICT(task_id) DO UPDATE SET name=excluded.name,saved_state=excluded.saved_state,"
|
||||
"recovery_state=excluded.recovery_state,progress=excluded.progress,sequence_count=excluded.sequence_count,started_sec=excluded.started_sec,"
|
||||
"started_nsec=excluded.started_nsec,finished_sec=excluded.finished_sec,finished_nsec=excluded.finished_nsec,updated_at=excluded.updated_at", &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id); (void)sqlite3_bind_text(statement, 2, snapshot->name, -1, SQLITE_TRANSIENT);
|
||||
(void)sqlite3_bind_int(statement, 3, (int)snapshot->saved_state); (void)sqlite3_bind_int(statement, 4, (int)snapshot->recovery_state);
|
||||
(void)sqlite3_bind_int(statement, 5, (int)snapshot->progress); (void)sqlite3_bind_int(statement, 6, (int)snapshot->sequence_count);
|
||||
(void)sqlite3_bind_int64(statement, 7, snapshot->started_at.tv_sec); (void)sqlite3_bind_int64(statement, 8, snapshot->started_at.tv_nsec);
|
||||
(void)sqlite3_bind_int64(statement, 9, snapshot->finished_at.tv_sec); (void)sqlite3_bind_int64(statement, 10, snapshot->finished_at.tv_nsec);
|
||||
(void)sqlite3_bind_int64(statement, 11, updated_at); result = step_done(database, statement, "upsert task");
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK && checkpoint) {
|
||||
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||
const char *forced_failure = getenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT");
|
||||
if (forced_failure && strcmp(forced_failure, "1") == 0) {
|
||||
result = execute(database, "INSERT INTO missing_test_table VALUES(1)", "forced checkpoint failure");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK && checkpoint) {
|
||||
result = prepare(database, "INSERT INTO checkpoints(task_id,path,format_version,durability,updated_at) VALUES(?1,?2,?3,?4,?5) "
|
||||
"ON CONFLICT(task_id) DO UPDATE SET path=excluded.path,format_version=excluded.format_version,durability=excluded.durability,updated_at=excluded.updated_at", &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
(void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)snapshot->id); (void)sqlite3_bind_text(statement, 2, checkpoint->path, -1, SQLITE_TRANSIENT);
|
||||
(void)sqlite3_bind_int64(statement, 3, checkpoint->format_version); (void)sqlite3_bind_int(statement, 4, (int)checkpoint->durability);
|
||||
(void)sqlite3_bind_int64(statement, 5, checkpoint->updated_at); result = step_done(database, statement, "upsert checkpoint");
|
||||
}
|
||||
}
|
||||
if (result == LARDON3D_PROJECT_DB_OK) result = execute(database, "COMMIT", "commit task record");
|
||||
if (result != LARDON3D_PROJECT_DB_OK) (void)execute(database, "ROLLBACK", "rollback task record");
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool
|
||||
read_task(sqlite3_stmt *statement, Lardon3DProjectDbTask *task)
|
||||
{
|
||||
memset(task, 0, sizeof(*task));
|
||||
sqlite3_int64 id = sqlite3_column_int64(statement, 0);
|
||||
int progress = sqlite3_column_int(statement, 4);
|
||||
sqlite3_int64 sequence_count = sqlite3_column_int64(statement, 5);
|
||||
sqlite3_int64 started_nsec = sqlite3_column_int64(statement, 7);
|
||||
sqlite3_int64 finished_nsec = sqlite3_column_int64(statement, 9);
|
||||
if (id <= 0 || !copy_column(statement, 1, task->name, sizeof(task->name))) return false;
|
||||
task->task_id = (uint64_t)id; task->saved_state = (Lardon3DTaskState)sqlite3_column_int(statement, 2);
|
||||
task->recovery_state = (Lardon3DTaskState)sqlite3_column_int(statement, 3);
|
||||
if (progress < 0 || progress > 100 || sequence_count < 0 || sequence_count > UINT_MAX
|
||||
|| started_nsec < 0 || started_nsec >= 1000000000 || finished_nsec < 0 || finished_nsec >= 1000000000
|
||||
|| !database_time(sqlite3_column_int64(statement, 6), &task->started_at.tv_sec)
|
||||
|| !database_time(sqlite3_column_int64(statement, 8), &task->finished_at.tv_sec)) return false;
|
||||
task->progress = (unsigned int)progress; task->sequence_count = (unsigned int)sequence_count;
|
||||
task->started_at.tv_nsec = (long)started_nsec; task->finished_at.tv_nsec = (long)finished_nsec; task->updated_at = sqlite3_column_int64(statement, 10);
|
||||
if (sqlite3_column_type(statement, 11) != SQLITE_NULL) {
|
||||
task->has_checkpoint = true;
|
||||
if (!copy_column(statement, 11, task->checkpoint.path, sizeof(task->checkpoint.path))) return false;
|
||||
sqlite3_int64 format_version = sqlite3_column_int64(statement, 12);
|
||||
if (format_version <= 0 || format_version > UINT32_MAX) return false;
|
||||
task->checkpoint.format_version = (uint32_t)format_version;
|
||||
task->checkpoint.durability = (Lardon3DProjectDbCheckpointDurability)sqlite3_column_int(statement, 13);
|
||||
task->checkpoint.updated_at = sqlite3_column_int64(statement, 14);
|
||||
if (task->checkpoint.durability < LARDON3D_DB_CHECKPOINT_DURABLE
|
||||
|| task->checkpoint.durability > LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE) return false;
|
||||
}
|
||||
return valid_state(task->saved_state) && valid_state(task->recovery_state) && task->progress <= 100;
|
||||
}
|
||||
|
||||
static const char task_select[] = "SELECT t.task_id,t.name,t.saved_state,t.recovery_state,t.progress,t.sequence_count,t.started_sec,t.started_nsec,"
|
||||
"t.finished_sec,t.finished_nsec,t.updated_at,c.path,c.format_version,c.durability,c.updated_at FROM tasks t LEFT JOIN checkpoints c ON c.task_id=t.task_id ";
|
||||
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_load_task(Lardon3DProjectDb *database, uint64_t task_id, Lardon3DProjectDbTask *task)
|
||||
{
|
||||
if (!database || !valid_task_id(task_id) || !task) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
(void)pthread_mutex_lock(&database->mutex); sqlite3_stmt *statement = NULL;
|
||||
char sql[768]; (void)snprintf(sql, sizeof(sql), "%s WHERE t.task_id=?1", task_select);
|
||||
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) { (void)sqlite3_bind_int64(statement, 1, (sqlite3_int64)task_id); int code = sqlite3_step(statement);
|
||||
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND; else if (code != SQLITE_ROW || !read_task(statement, task)) result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
(void)sqlite3_finalize(statement); }
|
||||
(void)pthread_mutex_unlock(&database->mutex); return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_list_recoverable(Lardon3DProjectDb *database, uint64_t after_task_id, Lardon3DProjectDbTask *tasks, size_t capacity, size_t *count)
|
||||
{
|
||||
if (count) *count = 0;
|
||||
if (!database || !tasks || !count || after_task_id > INT64_MAX || capacity == 0 || capacity > LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
(void)pthread_mutex_lock(&database->mutex); sqlite3_stmt *statement = NULL; char sql[896];
|
||||
(void)snprintf(sql, sizeof(sql), "%s WHERE t.recovery_state=?1 AND c.task_id IS NOT NULL AND t.task_id>?2 ORDER BY t.task_id LIMIT ?3", task_select);
|
||||
Lardon3DProjectDbResult result = prepare(database, sql, &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) {
|
||||
(void)sqlite3_bind_int(statement, 1, TASK_PENDING); (void)sqlite3_bind_int64(statement, 2, (sqlite3_int64)after_task_id); (void)sqlite3_bind_int64(statement, 3, (sqlite3_int64)capacity);
|
||||
int code = SQLITE_DONE; while (*count < capacity && (code = sqlite3_step(statement)) == SQLITE_ROW) { if (!read_task(statement, &tasks[*count])) { result = LARDON3D_PROJECT_DB_CORRUPT; break; } ++*count; }
|
||||
if (result == LARDON3D_PROJECT_DB_OK && *count < capacity && code != SQLITE_DONE) result = sqlite_result(database, code, "list recoverable");
|
||||
(void)sqlite3_finalize(statement);
|
||||
}
|
||||
(void)pthread_mutex_unlock(&database->mutex); return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_create_artifact(Lardon3DProjectDb *database, const Lardon3DProjectDbArtifact *artifact)
|
||||
{
|
||||
if (!database || !artifact || !bounded_text(artifact->artifact_id, sizeof(artifact->artifact_id), false)
|
||||
|| !bounded_text(artifact->kind, sizeof(artifact->kind), false) || !bounded_text(artifact->path, sizeof(artifact->path), false)
|
||||
|| artifact->state != LARDON3D_DB_ARTIFACT_STAGED || artifact->size_bytes != 0 || artifact->created_at < 0
|
||||
|| artifact->updated_at < artifact->created_at || (artifact->has_producer_task && !valid_task_id(artifact->producer_task_id))) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
(void)pthread_mutex_lock(&database->mutex); sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(database, "INSERT INTO artifacts(artifact_id,kind,path,state,size_bytes,producer_task_id,created_at,updated_at) VALUES(?1,?2,?3,0,0,?4,?5,?6)", &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) { (void)sqlite3_bind_text(statement, 1, artifact->artifact_id, -1, SQLITE_TRANSIENT); (void)sqlite3_bind_text(statement, 2, artifact->kind, -1, SQLITE_TRANSIENT);
|
||||
(void)sqlite3_bind_text(statement, 3, artifact->path, -1, SQLITE_TRANSIENT); if (artifact->has_producer_task) (void)sqlite3_bind_int64(statement, 4, (sqlite3_int64)artifact->producer_task_id); else (void)sqlite3_bind_null(statement, 4);
|
||||
(void)sqlite3_bind_int64(statement, 5, artifact->created_at); (void)sqlite3_bind_int64(statement, 6, artifact->updated_at); result = step_done(database, statement, "create artifact"); }
|
||||
(void)pthread_mutex_unlock(&database->mutex); return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_mark_artifact_ready(Lardon3DProjectDb *database, const char *artifact_id, int64_t updated_at)
|
||||
{
|
||||
if (!database || !bounded_text(artifact_id, LARDON3D_PROJECT_DB_ID_CAPACITY, false) || updated_at < 0) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
char path[LARDON3D_PROJECT_DB_PATH_CAPACITY]; Lardon3DProjectDbArtifact artifact;
|
||||
Lardon3DProjectDbResult result = lardon3d_project_db_load_artifact(database, artifact_id, &artifact);
|
||||
if (result != LARDON3D_PROJECT_DB_OK) return result; (void)snprintf(path, sizeof(path), "%s", artifact.path);
|
||||
struct stat information; if (stat(path, &information) != 0 || !S_ISREG(information.st_mode) || information.st_size < 0) return LARDON3D_PROJECT_DB_IO_ERROR;
|
||||
(void)pthread_mutex_lock(&database->mutex); sqlite3_stmt *statement = NULL;
|
||||
result = prepare(database, "UPDATE artifacts SET state=1,size_bytes=?1,updated_at=?2 WHERE artifact_id=?3 AND path=?4", &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) { (void)sqlite3_bind_int64(statement, 1, information.st_size); (void)sqlite3_bind_int64(statement, 2, updated_at);
|
||||
(void)sqlite3_bind_text(statement, 3, artifact_id, -1, SQLITE_TRANSIENT); (void)sqlite3_bind_text(statement, 4, path, -1, SQLITE_TRANSIENT); result = step_done(database, statement, "mark artifact ready");
|
||||
if (result == LARDON3D_PROJECT_DB_OK && sqlite3_changes(database->connection) != 1) result = LARDON3D_PROJECT_DB_NOT_FOUND; }
|
||||
(void)pthread_mutex_unlock(&database->mutex); return result;
|
||||
}
|
||||
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_load_artifact(Lardon3DProjectDb *database, const char *artifact_id, Lardon3DProjectDbArtifact *artifact)
|
||||
{
|
||||
if (!database || !artifact || !bounded_text(artifact_id, LARDON3D_PROJECT_DB_ID_CAPACITY, false)) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
(void)pthread_mutex_lock(&database->mutex); sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(database, "SELECT artifact_id,kind,path,state,size_bytes,producer_task_id,created_at,updated_at FROM artifacts WHERE artifact_id=?1", &statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) { (void)sqlite3_bind_text(statement, 1, artifact_id, -1, SQLITE_TRANSIENT); int code = sqlite3_step(statement);
|
||||
if (code == SQLITE_DONE) result = LARDON3D_PROJECT_DB_NOT_FOUND;
|
||||
else if (code != SQLITE_ROW || !copy_column(statement, 0, artifact->artifact_id, sizeof(artifact->artifact_id)) || !copy_column(statement, 1, artifact->kind, sizeof(artifact->kind)) || !copy_column(statement, 2, artifact->path, sizeof(artifact->path))) result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
else { sqlite3_int64 size = sqlite3_column_int64(statement, 4); sqlite3_int64 producer = sqlite3_column_int64(statement, 5);
|
||||
artifact->state = (Lardon3DProjectDbArtifactState)sqlite3_column_int(statement, 3); artifact->size_bytes = size >= 0 ? (uint64_t)size : 0;
|
||||
artifact->has_producer_task = sqlite3_column_type(statement, 5) != SQLITE_NULL; artifact->producer_task_id = artifact->has_producer_task && producer > 0 ? (uint64_t)producer : 0;
|
||||
artifact->created_at = sqlite3_column_int64(statement, 6); artifact->updated_at = sqlite3_column_int64(statement, 7); }
|
||||
(void)sqlite3_finalize(statement); }
|
||||
if (result == LARDON3D_PROJECT_DB_OK && (artifact->state < LARDON3D_DB_ARTIFACT_STAGED
|
||||
|| artifact->state > LARDON3D_DB_ARTIFACT_READY || (artifact->has_producer_task && artifact->producer_task_id == 0))) result = LARDON3D_PROJECT_DB_CORRUPT;
|
||||
(void)pthread_mutex_unlock(&database->mutex); return result;
|
||||
}
|
||||
|
||||
#ifdef LARDON3D_PROJECT_DB_TESTING
|
||||
Lardon3DProjectDbResult
|
||||
lardon3d_project_db_test_orphan_checkpoint(Lardon3DProjectDb *database)
|
||||
{
|
||||
if (!database) return LARDON3D_PROJECT_DB_INVALID_ARGUMENT;
|
||||
(void)pthread_mutex_lock(&database->mutex);
|
||||
sqlite3_stmt *statement = NULL;
|
||||
Lardon3DProjectDbResult result = prepare(database,
|
||||
"INSERT INTO checkpoints(task_id,path,format_version,durability,updated_at) VALUES(9223372036854775807,'orphan',1,0,0)",
|
||||
&statement);
|
||||
if (result == LARDON3D_PROJECT_DB_OK) result = step_done(database, statement, "orphan checkpoint");
|
||||
(void)pthread_mutex_unlock(&database->mutex);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
|
@ -291,6 +291,16 @@ lardon3d_task_checkpoint_save(
|
|||
if (!path || !path[0] || !valid_snapshot(snapshot)) {
|
||||
return LARDON3D_TASK_CHECKPOINT_INVALID;
|
||||
}
|
||||
#ifdef LARDON3D_CHECKPOINT_TESTING
|
||||
const char *forced_prepublication_failure = getenv(
|
||||
"LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE"
|
||||
);
|
||||
if (forced_prepublication_failure
|
||||
&& strcmp(forced_prepublication_failure, "1") == 0) {
|
||||
errno = EIO;
|
||||
return LARDON3D_TASK_CHECKPOINT_IO_ERROR;
|
||||
}
|
||||
#endif
|
||||
char temporary[4096];
|
||||
int length = snprintf(temporary, sizeof(temporary), "%s.tmp.XXXXXX", path);
|
||||
if (length < 0 || (size_t)length >= sizeof(temporary)) {
|
||||
|
|
|
|||
211
tests/test_project.c
Normal file
211
tests/test_project.c
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <lardon3d/project.h>
|
||||
#include <lardon3d/task_checkpoint.h>
|
||||
|
||||
#define CHECK(condition) do { if (!(condition)) { \
|
||||
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); return false; \
|
||||
} } while (0)
|
||||
|
||||
static bool
|
||||
unused_callback(Lardon3DTask *task, void *userdata)
|
||||
{
|
||||
(void)task;
|
||||
(void)userdata;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
write_ini(const char *path, const char *name, const char *stable_id, unsigned int version)
|
||||
{
|
||||
FILE *file = fopen(path, "w");
|
||||
if (!file) return false;
|
||||
bool written = version == 1
|
||||
? fprintf(file, "[project]\nname=%s\nversion=1\n", name) > 0
|
||||
: fprintf(file, "[project]\nname=%s\nstable_id=%s\nversion=2\n", name, stable_id) > 0;
|
||||
return fclose(file) == 0 && written;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
Lardon3DAppState *state;
|
||||
Lardon3DTask *task;
|
||||
Lardon3DProjectTaskCheckpointResult result;
|
||||
} CheckpointThread;
|
||||
|
||||
static void *
|
||||
checkpoint_thread(void *userdata)
|
||||
{
|
||||
CheckpointThread *context = userdata;
|
||||
context->result = lardon3d_project_checkpoint_task(context->state, context->task);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool
|
||||
run_test(void)
|
||||
{
|
||||
char root[] = "/tmp/lardon3d-project-life-XXXXXX";
|
||||
CHECK(mkdtemp(root));
|
||||
CHECK(setenv("LARDON3D_PROJECTS_ROOT", root, 1) == 0);
|
||||
Lardon3DAppState state;
|
||||
lardon3d_app_state_init(&state);
|
||||
CHECK(lardon3d_project_create(&state, "Projet Cycle"));
|
||||
CHECK(state.project_loaded && state.project_db && strlen(state.project_stable_id) == 32);
|
||||
|
||||
char project_path[512], database_path[512], ini_path[512], checkpoint_path[512];
|
||||
CHECK(snprintf(project_path, sizeof(project_path), "%s/Projet Cycle", root) > 0);
|
||||
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", project_path) > 0);
|
||||
CHECK(snprintf(ini_path, sizeof(ini_path), "%s/project.ini", project_path) > 0);
|
||||
CHECK(snprintf(checkpoint_path, sizeof(checkpoint_path), "%s/.lardon3d/checkpoints/1.chk", project_path) > 0);
|
||||
struct stat information;
|
||||
CHECK(stat(database_path, &information) == 0 && S_ISREG(information.st_mode));
|
||||
char stable_id[65]; (void)snprintf(stable_id, sizeof(stable_id), "%s", state.project_stable_id);
|
||||
for (size_t index = 0; index < 32; ++index) {
|
||||
CHECK((stable_id[index] >= '0' && stable_id[index] <= '9')
|
||||
|| (stable_id[index] >= 'a' && stable_id[index] <= 'f'));
|
||||
}
|
||||
Lardon3DProjectDbProject db_project;
|
||||
CHECK(lardon3d_project_db_get_project(state.project_db, &db_project) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(strcmp(db_project.stable_id, stable_id) == 0);
|
||||
|
||||
lardon3d_project_close(&state);
|
||||
CHECK(!state.project_loaded && !state.project_db && !state.project_stable_id[0]);
|
||||
CHECK(lardon3d_project_open(&state, "Projet Cycle"));
|
||||
CHECK(strcmp(state.project_stable_id, stable_id) == 0);
|
||||
lardon3d_project_close(&state);
|
||||
CHECK(write_ini(ini_path, "Projet Cycle", "A0000000000000000000000000000000", 2));
|
||||
CHECK(!lardon3d_project_open(&state, "Projet Cycle"));
|
||||
CHECK(!state.project_loaded && !state.project_db);
|
||||
CHECK(strstr(state.status_message, "project.ini invalide") != NULL);
|
||||
CHECK(write_ini(ini_path, "Projet Cycle", "", 1));
|
||||
CHECK(lardon3d_project_open(&state, "Projet Cycle"));
|
||||
CHECK(strcmp(state.project_stable_id, stable_id) == 0);
|
||||
lardon3d_project_close(&state);
|
||||
CHECK(unlink(database_path) == 0);
|
||||
CHECK(lardon3d_project_open(&state, "Projet Cycle"));
|
||||
CHECK(strcmp(state.project_stable_id, stable_id) == 0);
|
||||
|
||||
const Lardon3DResourceEstimate estimate = {
|
||||
.minimum_batch_size = 1,
|
||||
.maximum_batch_size = 1,
|
||||
.desired_cpu_threads = 1,
|
||||
};
|
||||
Lardon3DTask *task = lardon3d_task_create("Persistée", &estimate, unused_callback, NULL);
|
||||
CHECK(task && lardon3d_task_assign_id(task, 1));
|
||||
CHECK(lardon3d_task_set_progress(task, 10, "frontière 10"));
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||
CHECK(stat(checkpoint_path, &information) == 0 && S_ISREG(information.st_mode));
|
||||
Lardon3DProjectDbTask db_task;
|
||||
Lardon3DProjectRecoveryEntry entries[2]; size_t count = 0;
|
||||
Lardon3DTaskDurableSnapshot disk_snapshot; uint32_t version = 0;
|
||||
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(db_task.progress == 10 && strcmp(db_task.checkpoint.path, ".lardon3d/checkpoints/1.chk") == 0);
|
||||
|
||||
CHECK(setenv("LARDON3D_TEST_CHECKPOINT_SYNC_DIRECTORY_FAILURE", "1", 1) == 0);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_PUBLISHED_NOT_DURABLE);
|
||||
CHECK(unsetenv("LARDON3D_TEST_CHECKPOINT_SYNC_DIRECTORY_FAILURE") == 0);
|
||||
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(db_task.checkpoint.durability == LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE);
|
||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERABLE_PUBLISHED_NOT_DURABLE);
|
||||
|
||||
CHECK(lardon3d_task_set_progress(task, 20, "frontière 20"));
|
||||
CHECK(setenv("LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE", "1", 1) == 0);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR);
|
||||
CHECK(unsetenv("LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE") == 0);
|
||||
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK && db_task.progress == 10);
|
||||
Lardon3DTask *unpublished = lardon3d_task_create("Non publiée", &estimate, unused_callback, NULL);
|
||||
CHECK(unpublished && lardon3d_task_assign_id(unpublished, 3));
|
||||
CHECK(setenv("LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE", "1", 1) == 0);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, unpublished) == LARDON3D_PROJECT_TASK_CHECKPOINT_IO_ERROR);
|
||||
CHECK(unsetenv("LARDON3D_TEST_CHECKPOINT_PREPUBLICATION_FAILURE") == 0);
|
||||
CHECK(lardon3d_project_db_load_task(state.project_db, 3, &db_task) == LARDON3D_PROJECT_DB_NOT_FOUND);
|
||||
lardon3d_task_destroy(unpublished);
|
||||
|
||||
CHECK(lardon3d_task_set_progress(task, 25, "frontière 25"));
|
||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_BUSY_CHECKPOINT", "1", 1) == 0);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_DB_BUSY);
|
||||
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_BUSY_CHECKPOINT") == 0);
|
||||
CHECK(lardon3d_task_checkpoint_load(checkpoint_path, &disk_snapshot, &version) == LARDON3D_TASK_CHECKPOINT_OK);
|
||||
CHECK(disk_snapshot.progress == 25);
|
||||
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK && db_task.progress == 10);
|
||||
CHECK(lardon3d_task_set_progress(task, 20, "frontière 20"));
|
||||
|
||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT", "1", 1) == 0);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_DB_ERROR);
|
||||
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT") == 0);
|
||||
CHECK(lardon3d_task_checkpoint_load(checkpoint_path, &disk_snapshot, &version) == LARDON3D_TASK_CHECKPOINT_OK);
|
||||
CHECK(disk_snapshot.progress == 20);
|
||||
CHECK(lardon3d_project_db_load_task(state.project_db, 1, &db_task) == LARDON3D_PROJECT_DB_OK && db_task.progress == 10);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||
|
||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERABLE && entries[0].snapshot.progress == 20);
|
||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 257, &count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||
|
||||
CHECK(unlink(checkpoint_path) == 0);
|
||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERY_MISSING_CHECKPOINT);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||
int descriptor = open(checkpoint_path, O_WRONLY | O_TRUNC); CHECK(descriptor >= 0);
|
||||
CHECK(write(descriptor, "corrupt", 7) == 7 && close(descriptor) == 0);
|
||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(entries[0].status == LARDON3D_PROJECT_RECOVERY_INVALID_CHECKPOINT);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||
descriptor = open(checkpoint_path, O_RDWR); CHECK(descriptor >= 0);
|
||||
unsigned char future_version[4] = {2, 0, 0, 0};
|
||||
CHECK(pwrite(descriptor, future_version, sizeof(future_version), 8) == 4 && close(descriptor) == 0);
|
||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(entries[0].status == LARDON3D_PROJECT_RECOVERY_UNSUPPORTED_CHECKPOINT);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||
|
||||
CheckpointThread contexts[2] = {{.state = &state, .task = task}, {.state = &state, .task = task}};
|
||||
pthread_t threads[2]; CHECK(pthread_create(&threads[0], NULL, checkpoint_thread, &contexts[0]) == 0);
|
||||
CHECK(pthread_create(&threads[1], NULL, checkpoint_thread, &contexts[1]) == 0);
|
||||
CHECK(pthread_join(threads[0], NULL) == 0 && pthread_join(threads[1], NULL) == 0);
|
||||
CHECK(contexts[0].result == LARDON3D_PROJECT_TASK_CHECKPOINT_OK && contexts[1].result == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||
|
||||
Lardon3DTask *terminal = lardon3d_task_create("Terminale", &estimate, unused_callback, NULL);
|
||||
CHECK(terminal && lardon3d_task_assign_id(terminal, 2)); lardon3d_task_request_cancel(terminal);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, terminal) == LARDON3D_PROJECT_TASK_CHECKPOINT_OK);
|
||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK && count == 1);
|
||||
lardon3d_task_destroy(terminal);
|
||||
|
||||
lardon3d_project_close(&state);
|
||||
CHECK(lardon3d_project_checkpoint_task(&state, task) == LARDON3D_PROJECT_TASK_CHECKPOINT_NO_PROJECT);
|
||||
CHECK(lardon3d_project_open(&state, "Projet Cycle"));
|
||||
CHECK(lardon3d_project_list_recoverable(&state, 0, entries, 2, &count) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(count == 1 && entries[0].status == LARDON3D_PROJECT_RECOVERABLE);
|
||||
lardon3d_project_close(&state);
|
||||
|
||||
CHECK(write_ini(ini_path, "Projet Cycle", "00000000000000000000000000000000", 2));
|
||||
CHECK(!lardon3d_project_open(&state, "Projet Cycle"));
|
||||
CHECK(!state.project_loaded && !state.project_db);
|
||||
CHECK(write_ini(ini_path, "Projet Cycle", stable_id, 2));
|
||||
descriptor = open(database_path, O_WRONLY | O_TRUNC); CHECK(descriptor >= 0);
|
||||
CHECK(write(descriptor, "not sqlite", 10) == 10 && close(descriptor) == 0);
|
||||
CHECK(!lardon3d_project_open(&state, "Projet Cycle"));
|
||||
CHECK(!state.project_loaded && !state.project_db);
|
||||
lardon3d_task_destroy(task);
|
||||
|
||||
char terminal_checkpoint[512]; CHECK(snprintf(terminal_checkpoint, sizeof(terminal_checkpoint), "%s/.lardon3d/checkpoints/2.chk", project_path) > 0);
|
||||
CHECK(unlink(checkpoint_path) == 0); CHECK(unlink(terminal_checkpoint) == 0);
|
||||
CHECK(unlink(database_path) == 0); CHECK(unlink(ini_path) == 0);
|
||||
char path[512];
|
||||
CHECK(snprintf(path, sizeof(path), "%s/.lardon3d/checkpoints", project_path) > 0 && rmdir(path) == 0);
|
||||
CHECK(snprintf(path, sizeof(path), "%s/.lardon3d", project_path) > 0 && rmdir(path) == 0);
|
||||
const char *directories[] = {"images", "reconstruction", "exports", "logs"};
|
||||
for (size_t index = 0; index < 4; ++index) { CHECK(snprintf(path, sizeof(path), "%s/%s", project_path, directories[index]) > 0); CHECK(rmdir(path) == 0); }
|
||||
CHECK(rmdir(project_path) == 0); CHECK(rmdir(root) == 0);
|
||||
CHECK(unsetenv("LARDON3D_PROJECTS_ROOT") == 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(void) { return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; }
|
||||
196
tests/test_project_db.c
Normal file
196
tests/test_project_db.c
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <sqlite3.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <lardon3d/project_db.h>
|
||||
|
||||
#define CHECK(condition) do { if (!(condition)) { \
|
||||
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); return false; \
|
||||
} } while (0)
|
||||
|
||||
Lardon3DProjectDbResult lardon3d_project_db_test_orphan_checkpoint(Lardon3DProjectDb *database);
|
||||
|
||||
static Lardon3DTaskDurableSnapshot
|
||||
task_snapshot(uint64_t id, Lardon3DTaskState saved)
|
||||
{
|
||||
Lardon3DTaskDurableSnapshot snapshot = {
|
||||
.id = id,
|
||||
.estimate = {.minimum_batch_size = 1, .maximum_batch_size = 1, .desired_cpu_threads = 1},
|
||||
.progress = saved == TASK_COMPLETED ? 100 : 25,
|
||||
.saved_state = saved,
|
||||
.recovery_state = saved == TASK_RUNNING || saved == TASK_PAUSED ? TASK_PENDING : saved,
|
||||
.started_at = {.tv_sec = 10, .tv_nsec = 20},
|
||||
.finished_at = {.tv_sec = 30, .tv_nsec = 40},
|
||||
.sequence_count = 2,
|
||||
};
|
||||
(void)snprintf(snapshot.name, sizeof(snapshot.name), "Tâche %llu", (unsigned long long)id);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
typedef struct { Lardon3DProjectDb *database; bool success; } ThreadContext;
|
||||
|
||||
static void *
|
||||
read_thread(void *userdata)
|
||||
{
|
||||
ThreadContext *context = userdata;
|
||||
context->success = true;
|
||||
for (size_t index = 0; index < 100; ++index) {
|
||||
Lardon3DProjectDbTask task;
|
||||
if (lardon3d_project_db_load_task(context->database, 1, &task) != LARDON3D_PROJECT_DB_OK) {
|
||||
context->success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool
|
||||
create_future_database(const char *path)
|
||||
{
|
||||
sqlite3 *connection = NULL;
|
||||
if (sqlite3_open(path, &connection) != SQLITE_OK) return false;
|
||||
bool ok = sqlite3_exec(connection, "CREATE TABLE metadata(key TEXT PRIMARY KEY,value INTEGER NOT NULL);"
|
||||
"INSERT INTO metadata VALUES('schema_version',2);", NULL, NULL, NULL) == SQLITE_OK;
|
||||
return sqlite3_close(connection) == SQLITE_OK && ok;
|
||||
}
|
||||
|
||||
static bool
|
||||
query_integer(const char *path, const char *sql, sqlite3_int64 expected)
|
||||
{
|
||||
sqlite3 *connection = NULL;
|
||||
sqlite3_stmt *statement = NULL;
|
||||
if (sqlite3_open_v2(path, &connection, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK
|
||||
|| sqlite3_prepare_v2(connection, sql, -1, &statement, NULL) != SQLITE_OK) {
|
||||
if (statement) (void)sqlite3_finalize(statement);
|
||||
if (connection) (void)sqlite3_close(connection);
|
||||
return false;
|
||||
}
|
||||
bool matches = sqlite3_step(statement) == SQLITE_ROW
|
||||
&& sqlite3_column_int64(statement, 0) == expected
|
||||
&& sqlite3_step(statement) == SQLITE_DONE;
|
||||
return sqlite3_finalize(statement) == SQLITE_OK
|
||||
&& sqlite3_close(connection) == SQLITE_OK && matches;
|
||||
}
|
||||
|
||||
static bool
|
||||
run_test(void)
|
||||
{
|
||||
char directory[] = "/tmp/lardon3d-project-db-XXXXXX";
|
||||
CHECK(mkdtemp(directory));
|
||||
char database_path[512], artifact_path[512], future_path[512], corrupt_path[512];
|
||||
CHECK(snprintf(database_path, sizeof(database_path), "%s/project.db", directory) > 0);
|
||||
CHECK(snprintf(artifact_path, sizeof(artifact_path), "%s/artifact.bin", directory) > 0);
|
||||
CHECK(snprintf(future_path, sizeof(future_path), "%s/future.db", directory) > 0);
|
||||
CHECK(snprintf(corrupt_path, sizeof(corrupt_path), "%s/corrupt.db", directory) > 0);
|
||||
|
||||
char error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||
Lardon3DProjectDb *database = NULL;
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(database && lardon3d_project_db_schema_version(database) == 1);
|
||||
|
||||
Lardon3DProjectDbProject project = {.created_at = 100, .updated_at = 100};
|
||||
(void)snprintf(project.stable_id, sizeof(project.stable_id), "project-0001");
|
||||
(void)snprintf(project.name, sizeof(project.name), "Projet test");
|
||||
CHECK(lardon3d_project_db_set_project(database, &project) == LARDON3D_PROJECT_DB_OK);
|
||||
Lardon3DProjectDbProject loaded_project;
|
||||
CHECK(lardon3d_project_db_get_project(database, &loaded_project) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(strcmp(loaded_project.stable_id, project.stable_id) == 0);
|
||||
project.updated_at = 101; (void)snprintf(project.name, sizeof(project.name), "Projet renommé");
|
||||
CHECK(lardon3d_project_db_set_project(database, &project) == LARDON3D_PROJECT_DB_OK);
|
||||
Lardon3DProjectDbProject conflict = project; (void)snprintf(conflict.stable_id, sizeof(conflict.stable_id), "other");
|
||||
CHECK(lardon3d_project_db_set_project(database, &conflict) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||
char copied_error[LARDON3D_PROJECT_DB_ERROR_CAPACITY];
|
||||
CHECK(lardon3d_project_db_last_error(database, copied_error));
|
||||
CHECK(copied_error[0] != '\0');
|
||||
project = loaded_project; memset(project.name, 'x', sizeof(project.name));
|
||||
CHECK(lardon3d_project_db_set_project(database, &project) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||
|
||||
Lardon3DTaskDurableSnapshot running = task_snapshot(1, TASK_RUNNING);
|
||||
Lardon3DProjectDbCheckpoint checkpoint = {.format_version = 1, .durability = LARDON3D_DB_CHECKPOINT_DURABLE, .updated_at = 200};
|
||||
(void)snprintf(checkpoint.path, sizeof(checkpoint.path), "%s/checkpoints/task-1.chk", directory);
|
||||
CHECK(lardon3d_project_db_record_task(database, &running, &checkpoint, 200) == LARDON3D_PROJECT_DB_OK);
|
||||
running.progress = 30; running.sequence_count = 3;
|
||||
CHECK(lardon3d_project_db_record_task(database, &running, &checkpoint, 201) == LARDON3D_PROJECT_DB_OK);
|
||||
Lardon3DProjectDbTask task;
|
||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(task.recovery_state == TASK_PENDING && task.progress == 30 && task.sequence_count == 3);
|
||||
CHECK(task.has_checkpoint && strcmp(task.checkpoint.path, checkpoint.path) == 0);
|
||||
checkpoint.durability = LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE;
|
||||
CHECK(lardon3d_project_db_record_task(database, &running, &checkpoint, 201) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(task.checkpoint.durability == LARDON3D_DB_CHECKPOINT_PUBLISHED_NOT_DURABLE);
|
||||
|
||||
Lardon3DTaskDurableSnapshot completed = task_snapshot(2, TASK_COMPLETED);
|
||||
CHECK(lardon3d_project_db_record_task(database, &completed, NULL, 202) == LARDON3D_PROJECT_DB_OK);
|
||||
Lardon3DTaskDurableSnapshot no_checkpoint = task_snapshot(4, TASK_PENDING);
|
||||
CHECK(lardon3d_project_db_record_task(database, &no_checkpoint, NULL, 202) == LARDON3D_PROJECT_DB_OK);
|
||||
Lardon3DProjectDbTask page[1]; size_t count = 0;
|
||||
CHECK(lardon3d_project_db_list_recoverable(database, 0, page, 1, &count) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(count == 1 && page[0].task_id == 1);
|
||||
CHECK(lardon3d_project_db_list_recoverable(database, 1, page, 1, &count) == LARDON3D_PROJECT_DB_OK && count == 0);
|
||||
CHECK(lardon3d_project_db_list_recoverable(database, 0, page, LARDON3D_PROJECT_DB_RECOVERY_PAGE_MAX + 1, &count) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||
|
||||
Lardon3DTaskDurableSnapshot rollback_task = task_snapshot(3, TASK_PENDING);
|
||||
CHECK(setenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT", "1", 1) == 0);
|
||||
CHECK(lardon3d_project_db_record_task(database, &rollback_task, &checkpoint, 203) == LARDON3D_PROJECT_DB_IO_ERROR);
|
||||
CHECK(unsetenv("LARDON3D_TEST_PROJECT_DB_FAIL_CHECKPOINT") == 0);
|
||||
CHECK(lardon3d_project_db_load_task(database, 3, &task) == LARDON3D_PROJECT_DB_NOT_FOUND);
|
||||
CHECK(lardon3d_project_db_test_orphan_checkpoint(database) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||
|
||||
Lardon3DProjectDbArtifact artifact = {.state = LARDON3D_DB_ARTIFACT_STAGED, .has_producer_task = true, .producer_task_id = 1, .created_at = 300, .updated_at = 300};
|
||||
(void)snprintf(artifact.artifact_id, sizeof(artifact.artifact_id), "artifact-1");
|
||||
(void)snprintf(artifact.kind, sizeof(artifact.kind), "generic-test");
|
||||
(void)snprintf(artifact.path, sizeof(artifact.path), "%s", artifact_path);
|
||||
CHECK(lardon3d_project_db_create_artifact(database, &artifact) == LARDON3D_PROJECT_DB_OK);
|
||||
Lardon3DProjectDbArtifact loaded_artifact;
|
||||
CHECK(lardon3d_project_db_load_artifact(database, artifact.artifact_id, &loaded_artifact) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(loaded_artifact.state == LARDON3D_DB_ARTIFACT_STAGED);
|
||||
CHECK(lardon3d_project_db_mark_artifact_ready(database, artifact.artifact_id, 301) == LARDON3D_PROJECT_DB_IO_ERROR);
|
||||
int descriptor = open(artifact_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); CHECK(descriptor >= 0);
|
||||
const char payload[] = "validated"; CHECK(write(descriptor, payload, sizeof(payload)) == (ssize_t)sizeof(payload)); CHECK(close(descriptor) == 0);
|
||||
CHECK(lardon3d_project_db_mark_artifact_ready(database, artifact.artifact_id, 301) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_load_artifact(database, artifact.artifact_id, &loaded_artifact) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(loaded_artifact.state == LARDON3D_DB_ARTIFACT_READY && loaded_artifact.size_bytes == sizeof(payload));
|
||||
CHECK(lardon3d_project_db_create_artifact(database, &artifact) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||
artifact.producer_task_id = 999;
|
||||
(void)snprintf(artifact.artifact_id, sizeof(artifact.artifact_id), "orphan-artifact");
|
||||
CHECK(lardon3d_project_db_create_artifact(database, &artifact) == LARDON3D_PROJECT_DB_CONSTRAINT);
|
||||
artifact = loaded_artifact; artifact.state = LARDON3D_DB_ARTIFACT_STAGED; artifact.size_bytes = 0;
|
||||
memset(artifact.path, 'x', sizeof(artifact.path));
|
||||
CHECK(lardon3d_project_db_create_artifact(database, &artifact) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||
|
||||
ThreadContext contexts[2] = {{.database = database}, {.database = database}}; pthread_t threads[2];
|
||||
CHECK(pthread_create(&threads[0], NULL, read_thread, &contexts[0]) == 0);
|
||||
CHECK(pthread_create(&threads[1], NULL, read_thread, &contexts[1]) == 0);
|
||||
CHECK(pthread_join(threads[0], NULL) == 0 && pthread_join(threads[1], NULL) == 0);
|
||||
CHECK(contexts[0].success && contexts[1].success);
|
||||
|
||||
char too_long[LARDON3D_PROJECT_DB_PATH_CAPACITY + 1]; memset(too_long, 'x', sizeof(too_long)); too_long[sizeof(too_long) - 1] = '\0';
|
||||
CHECK(lardon3d_project_db_open(too_long, &database, error) == LARDON3D_PROJECT_DB_INVALID_ARGUMENT);
|
||||
lardon3d_project_db_close(contexts[0].database); database = NULL;
|
||||
CHECK(query_integer(database_path, "SELECT value FROM metadata WHERE key='schema_version'", 1));
|
||||
CHECK(query_integer(database_path, "SELECT count(*) FROM tasks WHERE task_id=1", 1));
|
||||
CHECK(lardon3d_project_db_open(database_path, &database, error) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_load_task(database, 1, &task) == LARDON3D_PROJECT_DB_OK);
|
||||
CHECK(lardon3d_project_db_load_artifact(database, "artifact-1", &loaded_artifact) == LARDON3D_PROJECT_DB_OK);
|
||||
lardon3d_project_db_close(database);
|
||||
|
||||
CHECK(create_future_database(future_path));
|
||||
CHECK(lardon3d_project_db_open(future_path, &database, error) == LARDON3D_PROJECT_DB_UNSUPPORTED_SCHEMA);
|
||||
descriptor = open(corrupt_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); CHECK(descriptor >= 0);
|
||||
CHECK(write(descriptor, "not sqlite", 10) == 10 && close(descriptor) == 0);
|
||||
CHECK(lardon3d_project_db_open(corrupt_path, &database, error) == LARDON3D_PROJECT_DB_CORRUPT);
|
||||
|
||||
CHECK(unlink(artifact_path) == 0); CHECK(unlink(database_path) == 0); CHECK(unlink(future_path) == 0); CHECK(unlink(corrupt_path) == 0);
|
||||
CHECK(rmdir(directory) == 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(void) { return run_test() ? EXIT_SUCCESS : EXIT_FAILURE; }
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
|
@ -326,7 +327,7 @@ run_test(void)
|
|||
CHECK(check_restored_state(TASK_PAUSED, TASK_PENDING));
|
||||
|
||||
Lardon3DHardwareProfile profile = {
|
||||
.logical_cpu_count = 2,
|
||||
.logical_cpu_count = UINT_MAX,
|
||||
.page_size_bytes = 4096,
|
||||
.memory_total_bytes = UINT64_MAX,
|
||||
.cpu_architecture = "test",
|
||||
|
|
|
|||
Loading…
Reference in a new issue