feat: add asynchronous cancellable image import
This commit is contained in:
parent
f2efe418cb
commit
8bb90ed05a
10 changed files with 1044 additions and 44 deletions
|
|
@ -33,3 +33,5 @@ La variable `LARDON3D_PROJECTS_ROOT` permet de choisir un autre répertoire raci
|
||||||
L'import accepte les images JPEG, PNG, TIFF et HEIC présentes directement dans
|
L'import accepte les images JPEG, PNG, TIFF et HEIC présentes directement dans
|
||||||
le dossier choisi, sans parcourir ses sous-dossiers. Elles sont copiées vers
|
le dossier choisi, sans parcourir ses sous-dossiers. Elles sont copiées vers
|
||||||
`images/originals` et répertoriées dans `images/manifest.tsv`.
|
`images/originals` et répertoriées dans `images/manifest.tsv`.
|
||||||
|
L'import s'exécute en arrière-plan afin que la TUI reste réactive. Pendant une
|
||||||
|
opération, la touche `C` demande son annulation.
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,44 @@ typedef struct {
|
||||||
size_t ignored;
|
size_t ignored;
|
||||||
} Lardon3DImportResult;
|
} Lardon3DImportResult;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
size_t total;
|
||||||
|
size_t processed;
|
||||||
|
size_t copied;
|
||||||
|
size_t already_present;
|
||||||
|
size_t ignored;
|
||||||
|
const char *message;
|
||||||
|
} Lardon3DImportProgress;
|
||||||
|
|
||||||
|
typedef bool (*Lardon3DImportCancelled)(void *context);
|
||||||
|
typedef void (*Lardon3DImportProgressed)(
|
||||||
|
void *context,
|
||||||
|
const Lardon3DImportProgress *progress
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
void *context;
|
||||||
|
Lardon3DImportCancelled is_cancelled;
|
||||||
|
Lardon3DImportProgressed progressed;
|
||||||
|
} Lardon3DImportControl;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_IMPORT_FAILED = 0,
|
||||||
|
LARDON3D_IMPORT_SUCCEEDED,
|
||||||
|
LARDON3D_IMPORT_CANCELLED
|
||||||
|
} Lardon3DImportOutcome;
|
||||||
|
|
||||||
bool lardon3d_import_directory(
|
bool lardon3d_import_directory(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
const char *source_directory,
|
const char *source_directory,
|
||||||
Lardon3DImportResult *result
|
Lardon3DImportResult *result
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Lardon3DImportOutcome lardon3d_import_directory_controlled(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *source_directory,
|
||||||
|
Lardon3DImportResult *result,
|
||||||
|
const Lardon3DImportControl *control
|
||||||
|
);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
44
include/lardon3d/import_task.h
Normal file
44
include/lardon3d/import_task.h
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#ifndef LARDON3D_IMPORT_TASK_H
|
||||||
|
#define LARDON3D_IMPORT_TASK_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include <lardon3d/app_state.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
LARDON3D_IMPORT_TASK_IDLE = 0,
|
||||||
|
LARDON3D_IMPORT_TASK_RUNNING,
|
||||||
|
LARDON3D_IMPORT_TASK_SUCCEEDED,
|
||||||
|
LARDON3D_IMPORT_TASK_CANCELLED,
|
||||||
|
LARDON3D_IMPORT_TASK_FAILED
|
||||||
|
} Lardon3DImportTaskStatus;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
Lardon3DImportTaskStatus status;
|
||||||
|
size_t total;
|
||||||
|
size_t processed;
|
||||||
|
size_t copied;
|
||||||
|
size_t already_present;
|
||||||
|
size_t ignored;
|
||||||
|
char message[256];
|
||||||
|
} Lardon3DImportTaskSnapshot;
|
||||||
|
|
||||||
|
typedef struct Lardon3DImportTask Lardon3DImportTask;
|
||||||
|
|
||||||
|
Lardon3DImportTask *lardon3d_import_task_create(void);
|
||||||
|
bool lardon3d_import_task_start(
|
||||||
|
Lardon3DImportTask *task,
|
||||||
|
const Lardon3DAppState *state,
|
||||||
|
const char *source_directory
|
||||||
|
);
|
||||||
|
void lardon3d_import_task_request_cancel(Lardon3DImportTask *task);
|
||||||
|
bool lardon3d_import_task_snapshot(
|
||||||
|
Lardon3DImportTask *task,
|
||||||
|
Lardon3DImportTaskSnapshot *snapshot
|
||||||
|
);
|
||||||
|
bool lardon3d_import_task_is_finished(Lardon3DImportTask *task);
|
||||||
|
bool lardon3d_import_task_join(Lardon3DImportTask *task);
|
||||||
|
void lardon3d_import_task_destroy(Lardon3DImportTask *task);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -2,11 +2,13 @@
|
||||||
#define LARDON3D_LAYOUT_H
|
#define LARDON3D_LAYOUT_H
|
||||||
|
|
||||||
#include <lardon3d/app_state.h>
|
#include <lardon3d/app_state.h>
|
||||||
|
#include <lardon3d/import_task.h>
|
||||||
|
|
||||||
void lardon3d_layout_draw(
|
void lardon3d_layout_draw(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
const char *input_text,
|
const char *input_text,
|
||||||
const char *input_label,
|
const char *input_label,
|
||||||
|
const Lardon3DImportTaskSnapshot *import_snapshot,
|
||||||
int rows,
|
int rows,
|
||||||
int cols
|
int cols
|
||||||
);
|
);
|
||||||
|
|
|
||||||
18
meson.build
18
meson.build
|
|
@ -19,6 +19,7 @@ add_project_arguments(
|
||||||
)
|
)
|
||||||
|
|
||||||
ncursesw = dependency('ncursesw', required: true)
|
ncursesw = dependency('ncursesw', required: true)
|
||||||
|
threads = dependency('threads')
|
||||||
|
|
||||||
executable(
|
executable(
|
||||||
'lardon3d',
|
'lardon3d',
|
||||||
|
|
@ -29,10 +30,11 @@ executable(
|
||||||
'src/tui.c',
|
'src/tui.c',
|
||||||
'src/layout.c',
|
'src/layout.c',
|
||||||
'src/import.c',
|
'src/import.c',
|
||||||
|
'src/import_task.c',
|
||||||
'src/project.c',
|
'src/project.c',
|
||||||
],
|
],
|
||||||
include_directories: include_directories('include'),
|
include_directories: include_directories('include'),
|
||||||
dependencies: [ncursesw],
|
dependencies: [ncursesw, threads],
|
||||||
)
|
)
|
||||||
|
|
||||||
import_test = executable(
|
import_test = executable(
|
||||||
|
|
@ -46,3 +48,17 @@ import_test = executable(
|
||||||
)
|
)
|
||||||
|
|
||||||
test('import', import_test)
|
test('import', import_test)
|
||||||
|
|
||||||
|
import_task_test = executable(
|
||||||
|
'test-import-task',
|
||||||
|
sources: [
|
||||||
|
'tests/test_import_task.c',
|
||||||
|
'src/app_state.c',
|
||||||
|
'src/import.c',
|
||||||
|
'src/import_task.c',
|
||||||
|
],
|
||||||
|
include_directories: include_directories('include'),
|
||||||
|
dependencies: [threads],
|
||||||
|
)
|
||||||
|
|
||||||
|
test('import-task', import_task_test, timeout: 30)
|
||||||
|
|
|
||||||
225
src/import.c
225
src/import.c
|
|
@ -37,6 +37,13 @@ typedef struct {
|
||||||
size_t capacity;
|
size_t capacity;
|
||||||
} CandidateList;
|
} CandidateList;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
COPY_IMAGE_ERROR = -1,
|
||||||
|
COPY_IMAGE_COLLISION = 0,
|
||||||
|
COPY_IMAGE_CREATED = 1,
|
||||||
|
COPY_IMAGE_CANCELLED = 2,
|
||||||
|
} CopyImageOutcome;
|
||||||
|
|
||||||
static void
|
static void
|
||||||
set_status(Lardon3DAppState *state, const char *message)
|
set_status(Lardon3DAppState *state, const char *message)
|
||||||
{
|
{
|
||||||
|
|
@ -48,6 +55,37 @@ set_status(Lardon3DAppState *state, const char *message)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
import_is_cancelled(const Lardon3DImportControl *control)
|
||||||
|
{
|
||||||
|
return control && control->is_cancelled
|
||||||
|
&& control->is_cancelled(control->context);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
publish_progress(
|
||||||
|
const Lardon3DImportControl *control,
|
||||||
|
const Lardon3DImportResult *result,
|
||||||
|
size_t processed,
|
||||||
|
const char *message
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!control || !control->progressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Lardon3DImportProgress progress = {
|
||||||
|
.total = result->admissible_found,
|
||||||
|
.processed = processed <= result->admissible_found
|
||||||
|
? processed
|
||||||
|
: result->admissible_found,
|
||||||
|
.copied = result->copied,
|
||||||
|
.already_present = result->already_present,
|
||||||
|
.ignored = result->ignored,
|
||||||
|
.message = message,
|
||||||
|
};
|
||||||
|
control->progressed(control->context, &progress);
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
join_path(
|
join_path(
|
||||||
char destination[PATH_MAX],
|
char destination[PATH_MAX],
|
||||||
|
|
@ -416,7 +454,8 @@ static int
|
||||||
copy_image(
|
copy_image(
|
||||||
const char *source_path,
|
const char *source_path,
|
||||||
const char *destination_path,
|
const char *destination_path,
|
||||||
off_t *destination_size
|
off_t *destination_size,
|
||||||
|
const Lardon3DImportControl *control
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
int source = open(source_path, O_RDONLY | O_NOFOLLOW);
|
int source = open(source_path, O_RDONLY | O_NOFOLLOW);
|
||||||
|
|
@ -445,8 +484,14 @@ copy_image(
|
||||||
}
|
}
|
||||||
|
|
||||||
bool success = true;
|
bool success = true;
|
||||||
|
bool cancelled = false;
|
||||||
char buffer[64 * 1024];
|
char buffer[64 * 1024];
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
if (import_is_cancelled(control)) {
|
||||||
|
cancelled = true;
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
ssize_t read_count = read(source, buffer, sizeof(buffer));
|
ssize_t read_count = read(source, buffer, sizeof(buffer));
|
||||||
if (read_count == 0) {
|
if (read_count == 0) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -461,6 +506,11 @@ copy_image(
|
||||||
|
|
||||||
ssize_t written_total = 0;
|
ssize_t written_total = 0;
|
||||||
while (written_total < read_count) {
|
while (written_total < read_count) {
|
||||||
|
if (import_is_cancelled(control)) {
|
||||||
|
cancelled = true;
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
ssize_t written = write(
|
ssize_t written = write(
|
||||||
destination,
|
destination,
|
||||||
buffer + written_total,
|
buffer + written_total,
|
||||||
|
|
@ -491,11 +541,11 @@ copy_image(
|
||||||
}
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
(void)unlink(destination_path);
|
(void)unlink(destination_path);
|
||||||
return -1;
|
return cancelled ? COPY_IMAGE_CANCELLED : COPY_IMAGE_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
*destination_size = source_info.st_size;
|
*destination_size = source_info.st_size;
|
||||||
return 1;
|
return COPY_IMAGE_CREATED;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
|
|
@ -504,7 +554,9 @@ analyze_source_directory(
|
||||||
const char *absolute_source,
|
const char *absolute_source,
|
||||||
const char *originals_path,
|
const char *originals_path,
|
||||||
Lardon3DImportResult *result,
|
Lardon3DImportResult *result,
|
||||||
CandidateList *candidates
|
CandidateList *candidates,
|
||||||
|
const Lardon3DImportControl *control,
|
||||||
|
bool *cancelled
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
DIR *directory = opendir(absolute_source);
|
DIR *directory = opendir(absolute_source);
|
||||||
|
|
@ -515,6 +567,10 @@ analyze_source_directory(
|
||||||
|
|
||||||
bool success = true;
|
bool success = true;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
if (import_is_cancelled(control)) {
|
||||||
|
*cancelled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
errno = 0;
|
errno = 0;
|
||||||
struct dirent *entry = readdir(directory);
|
struct dirent *entry = readdir(directory);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
|
|
@ -600,7 +656,7 @@ rollback_created_files(
|
||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static void
|
||||||
fail_import(
|
fail_import(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
Lardon3DImportResult *result,
|
Lardon3DImportResult *result,
|
||||||
|
|
@ -638,47 +694,80 @@ fail_import(
|
||||||
reason
|
reason
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
static Lardon3DImportOutcome
|
||||||
lardon3d_import_directory(
|
cancel_import(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
Lardon3DImportResult *result,
|
||||||
|
size_t processed,
|
||||||
|
ManifestWriter *manifest,
|
||||||
|
CandidateList *candidates,
|
||||||
|
const char *originals_path,
|
||||||
|
const Lardon3DImportControl *control
|
||||||
|
)
|
||||||
|
{
|
||||||
|
manifest_abort(manifest);
|
||||||
|
size_t removed = rollback_created_files(candidates, originals_path);
|
||||||
|
result->copied -= removed;
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Import annulé : %zu sur %zu fichiers traités.",
|
||||||
|
processed,
|
||||||
|
result->admissible_found
|
||||||
|
);
|
||||||
|
publish_progress(control, result, processed, state->status_message);
|
||||||
|
free(candidates->items);
|
||||||
|
return LARDON3D_IMPORT_CANCELLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DImportOutcome
|
||||||
|
lardon3d_import_directory_controlled(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
const char *source_directory,
|
const char *source_directory,
|
||||||
Lardon3DImportResult *result
|
Lardon3DImportResult *result,
|
||||||
|
const Lardon3DImportControl *control
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (!state || !result) {
|
if (!state || !result) {
|
||||||
return false;
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
*result = (Lardon3DImportResult) {0};
|
*result = (Lardon3DImportResult) {0};
|
||||||
|
publish_progress(control, result, 0, "Analyse du dossier source...");
|
||||||
|
|
||||||
if (!state->project_loaded) {
|
if (!state->project_loaded) {
|
||||||
set_status(state, "Aucun projet chargé.");
|
set_status(state, "Aucun projet chargé.");
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
char trimmed_source[PATH_MAX];
|
char trimmed_source[PATH_MAX];
|
||||||
char absolute_source[PATH_MAX];
|
char absolute_source[PATH_MAX];
|
||||||
if (!trim_source_path(state, source_directory, trimmed_source)) {
|
if (!trim_source_path(state, source_directory, trimmed_source)) {
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
if (!resolve_source_path(state, trimmed_source, absolute_source)) {
|
if (!resolve_source_path(state, trimmed_source, absolute_source)) {
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
if (has_forbidden_manifest_character(absolute_source)) {
|
if (has_forbidden_manifest_character(absolute_source)) {
|
||||||
set_status(state, "Erreur : chemin source incompatible avec le manifeste.");
|
set_status(state, "Erreur : chemin source incompatible avec le manifeste.");
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct stat source_directory_info;
|
struct stat source_directory_info;
|
||||||
if (lstat(absolute_source, &source_directory_info) != 0) {
|
if (lstat(absolute_source, &source_directory_info) != 0) {
|
||||||
set_status(state, "Erreur : dossier source inexistant.");
|
set_status(state, "Erreur : dossier source inexistant.");
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
if (!S_ISDIR(source_directory_info.st_mode)) {
|
if (!S_ISDIR(source_directory_info.st_mode)) {
|
||||||
set_status(state, "Erreur : la source n'est pas un dossier.");
|
set_status(state, "Erreur : la source n'est pas un dossier.");
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
char images_path[PATH_MAX];
|
char images_path[PATH_MAX];
|
||||||
|
|
@ -686,35 +775,62 @@ lardon3d_import_directory(
|
||||||
if (!join_path(images_path, state->project_path, "images")
|
if (!join_path(images_path, state->project_path, "images")
|
||||||
|| !join_path(originals_path, images_path, "originals")) {
|
|| !join_path(originals_path, images_path, "originals")) {
|
||||||
set_status(state, "Erreur : chemin du projet trop long.");
|
set_status(state, "Erreur : chemin du projet trop long.");
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
CandidateList candidates = {0};
|
CandidateList candidates = {0};
|
||||||
|
bool analysis_cancelled = false;
|
||||||
if (!analyze_source_directory(
|
if (!analyze_source_directory(
|
||||||
state,
|
state,
|
||||||
absolute_source,
|
absolute_source,
|
||||||
originals_path,
|
originals_path,
|
||||||
result,
|
result,
|
||||||
&candidates
|
&candidates,
|
||||||
|
control,
|
||||||
|
&analysis_cancelled
|
||||||
)) {
|
)) {
|
||||||
free(candidates.items);
|
free(candidates.items);
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
|
}
|
||||||
|
if (analysis_cancelled) {
|
||||||
|
free(candidates.items);
|
||||||
|
set_status(state, "Import annulé : 0 fichier traité.");
|
||||||
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_CANCELLED;
|
||||||
|
}
|
||||||
|
publish_progress(control, result, 0, "Import en cours...");
|
||||||
|
|
||||||
|
if (import_is_cancelled(control)) {
|
||||||
|
free(candidates.items);
|
||||||
|
set_status(state, "Import annulé : 0 fichier traité.");
|
||||||
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_CANCELLED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ensure_originals_directory(state, images_path, originals_path)) {
|
if (!ensure_originals_directory(state, images_path, originals_path)) {
|
||||||
free(candidates.items);
|
free(candidates.items);
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
ManifestWriter manifest;
|
ManifestWriter manifest;
|
||||||
if (!manifest_begin(state, images_path, &manifest)) {
|
if (!manifest_begin(state, images_path, &manifest)) {
|
||||||
free(candidates.items);
|
free(candidates.items);
|
||||||
return false;
|
publish_progress(control, result, 0, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool success = true;
|
bool success = true;
|
||||||
|
bool cancelled = false;
|
||||||
|
size_t processed = 0;
|
||||||
const char *failure_reason = NULL;
|
const char *failure_reason = NULL;
|
||||||
for (size_t index = 0; index < candidates.count; ++index) {
|
for (size_t index = 0; index < candidates.count; ++index) {
|
||||||
|
if (import_is_cancelled(control)) {
|
||||||
|
cancelled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
ImportCandidate *candidate = &candidates.items[index];
|
ImportCandidate *candidate = &candidates.items[index];
|
||||||
char source_path[PATH_MAX];
|
char source_path[PATH_MAX];
|
||||||
char destination_path[PATH_MAX];
|
char destination_path[PATH_MAX];
|
||||||
|
|
@ -730,13 +846,22 @@ lardon3d_import_directory(
|
||||||
}
|
}
|
||||||
|
|
||||||
off_t size = 0;
|
off_t size = 0;
|
||||||
int copied = copy_image(source_path, destination_path, &size);
|
int copied = copy_image(
|
||||||
if (copied < 0) {
|
source_path,
|
||||||
|
destination_path,
|
||||||
|
&size,
|
||||||
|
control
|
||||||
|
);
|
||||||
|
if (copied == COPY_IMAGE_CANCELLED) {
|
||||||
|
cancelled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (copied == COPY_IMAGE_ERROR) {
|
||||||
success = false;
|
success = false;
|
||||||
failure_reason = "copie d'une image impossible";
|
failure_reason = "copie d'une image impossible";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (copied == 0) {
|
if (copied == COPY_IMAGE_COLLISION) {
|
||||||
++result->already_present;
|
++result->already_present;
|
||||||
struct stat destination_info;
|
struct stat destination_info;
|
||||||
bool destination_is_regular = false;
|
bool destination_is_regular = false;
|
||||||
|
|
@ -746,6 +871,8 @@ lardon3d_import_directory(
|
||||||
destination_is_regular = true;
|
destination_is_regular = true;
|
||||||
}
|
}
|
||||||
if (!destination_is_regular) {
|
if (!destination_is_regular) {
|
||||||
|
++processed;
|
||||||
|
publish_progress(control, result, processed, "Import en cours...");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -773,8 +900,21 @@ lardon3d_import_directory(
|
||||||
failure_reason = "écriture du manifeste impossible";
|
failure_reason = "écriture du manifeste impossible";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
++processed;
|
||||||
|
publish_progress(control, result, processed, "Import en cours...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return cancel_import(
|
||||||
|
state,
|
||||||
|
result,
|
||||||
|
processed,
|
||||||
|
&manifest,
|
||||||
|
&candidates,
|
||||||
|
originals_path,
|
||||||
|
control
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!success) {
|
if (!success) {
|
||||||
manifest_abort(&manifest);
|
manifest_abort(&manifest);
|
||||||
size_t removed = rollback_created_files(
|
size_t removed = rollback_created_files(
|
||||||
|
|
@ -782,7 +922,20 @@ lardon3d_import_directory(
|
||||||
originals_path
|
originals_path
|
||||||
);
|
);
|
||||||
free(candidates.items);
|
free(candidates.items);
|
||||||
return fail_import(state, result, removed, failure_reason);
|
(void)fail_import(state, result, removed, failure_reason);
|
||||||
|
publish_progress(control, result, processed, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
|
}
|
||||||
|
if (import_is_cancelled(control)) {
|
||||||
|
return cancel_import(
|
||||||
|
state,
|
||||||
|
result,
|
||||||
|
processed,
|
||||||
|
&manifest,
|
||||||
|
&candidates,
|
||||||
|
originals_path,
|
||||||
|
control
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!manifest_commit(state, &manifest)) {
|
if (!manifest_commit(state, &manifest)) {
|
||||||
size_t removed = rollback_created_files(
|
size_t removed = rollback_created_files(
|
||||||
|
|
@ -790,12 +943,14 @@ lardon3d_import_directory(
|
||||||
originals_path
|
originals_path
|
||||||
);
|
);
|
||||||
free(candidates.items);
|
free(candidates.items);
|
||||||
return fail_import(
|
(void)fail_import(
|
||||||
state,
|
state,
|
||||||
result,
|
result,
|
||||||
removed,
|
removed,
|
||||||
"mise à jour du manifeste impossible"
|
"mise à jour du manifeste impossible"
|
||||||
);
|
);
|
||||||
|
publish_progress(control, result, processed, state->status_message);
|
||||||
|
return LARDON3D_IMPORT_FAILED;
|
||||||
}
|
}
|
||||||
free(candidates.items);
|
free(candidates.items);
|
||||||
|
|
||||||
|
|
@ -808,5 +963,21 @@ lardon3d_import_directory(
|
||||||
result->already_present,
|
result->already_present,
|
||||||
result->already_present == 1 ? "" : "s"
|
result->already_present == 1 ? "" : "s"
|
||||||
);
|
);
|
||||||
return true;
|
publish_progress(control, result, processed, "Import terminé.");
|
||||||
|
return LARDON3D_IMPORT_SUCCEEDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_import_directory(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *source_directory,
|
||||||
|
Lardon3DImportResult *result
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return lardon3d_import_directory_controlled(
|
||||||
|
state,
|
||||||
|
source_directory,
|
||||||
|
result,
|
||||||
|
NULL
|
||||||
|
) == LARDON3D_IMPORT_SUCCEEDED;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
229
src/import_task.c
Normal file
229
src/import_task.c
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <lardon3d/import.h>
|
||||||
|
#include <lardon3d/import_task.h>
|
||||||
|
|
||||||
|
struct Lardon3DImportTask {
|
||||||
|
pthread_mutex_t mutex;
|
||||||
|
pthread_t thread;
|
||||||
|
bool thread_started;
|
||||||
|
bool joined;
|
||||||
|
bool cancel_requested;
|
||||||
|
Lardon3DAppState worker_state;
|
||||||
|
char source_directory[PATH_MAX];
|
||||||
|
Lardon3DImportTaskSnapshot snapshot;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
copy_message(char destination[256], const char *message)
|
||||||
|
{
|
||||||
|
(void)snprintf(destination, 256, "%s", message ? message : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
task_is_cancelled(void *context)
|
||||||
|
{
|
||||||
|
Lardon3DImportTask *task = context;
|
||||||
|
bool cancelled;
|
||||||
|
(void)pthread_mutex_lock(&task->mutex);
|
||||||
|
cancelled = task->cancel_requested;
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
return cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
task_progressed(void *context, const Lardon3DImportProgress *progress)
|
||||||
|
{
|
||||||
|
Lardon3DImportTask *task = context;
|
||||||
|
(void)pthread_mutex_lock(&task->mutex);
|
||||||
|
task->snapshot.total = progress->total;
|
||||||
|
task->snapshot.processed = progress->processed;
|
||||||
|
task->snapshot.copied = progress->copied;
|
||||||
|
task->snapshot.already_present = progress->already_present;
|
||||||
|
task->snapshot.ignored = progress->ignored;
|
||||||
|
copy_message(task->snapshot.message, progress->message);
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *
|
||||||
|
run_import(void *argument)
|
||||||
|
{
|
||||||
|
Lardon3DImportTask *task = argument;
|
||||||
|
Lardon3DImportResult result;
|
||||||
|
const Lardon3DImportControl control = {
|
||||||
|
.context = task,
|
||||||
|
.is_cancelled = task_is_cancelled,
|
||||||
|
.progressed = task_progressed,
|
||||||
|
};
|
||||||
|
Lardon3DImportOutcome outcome = lardon3d_import_directory_controlled(
|
||||||
|
&task->worker_state,
|
||||||
|
task->source_directory,
|
||||||
|
&result,
|
||||||
|
&control
|
||||||
|
);
|
||||||
|
|
||||||
|
(void)pthread_mutex_lock(&task->mutex);
|
||||||
|
task->snapshot.status = outcome == LARDON3D_IMPORT_SUCCEEDED
|
||||||
|
? LARDON3D_IMPORT_TASK_SUCCEEDED
|
||||||
|
: outcome == LARDON3D_IMPORT_CANCELLED
|
||||||
|
? LARDON3D_IMPORT_TASK_CANCELLED
|
||||||
|
: LARDON3D_IMPORT_TASK_FAILED;
|
||||||
|
task->snapshot.total = result.admissible_found;
|
||||||
|
task->snapshot.copied = result.copied;
|
||||||
|
task->snapshot.already_present = result.already_present;
|
||||||
|
copy_message(task->snapshot.message, task->worker_state.status_message);
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DImportTask *
|
||||||
|
lardon3d_import_task_create(void)
|
||||||
|
{
|
||||||
|
Lardon3DImportTask *task = calloc(1, sizeof(*task));
|
||||||
|
if (!task) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (pthread_mutex_init(&task->mutex, NULL) != 0) {
|
||||||
|
free(task);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
task->snapshot.status = LARDON3D_IMPORT_TASK_IDLE;
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_import_task_start(
|
||||||
|
Lardon3DImportTask *task,
|
||||||
|
const Lardon3DAppState *state,
|
||||||
|
const char *source_directory
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!task || !state || !source_directory) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)pthread_mutex_lock(&task->mutex);
|
||||||
|
if (task->thread_started) {
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int written = snprintf(
|
||||||
|
task->source_directory,
|
||||||
|
sizeof(task->source_directory),
|
||||||
|
"%s",
|
||||||
|
source_directory
|
||||||
|
);
|
||||||
|
if (written < 0 || (size_t)written >= sizeof(task->source_directory)) {
|
||||||
|
copy_message(task->snapshot.message, "Erreur : chemin source trop long.");
|
||||||
|
task->snapshot.status = LARDON3D_IMPORT_TASK_FAILED;
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
task->worker_state = *state;
|
||||||
|
task->cancel_requested = false;
|
||||||
|
task->joined = false;
|
||||||
|
task->snapshot = (Lardon3DImportTaskSnapshot) {
|
||||||
|
.status = LARDON3D_IMPORT_TASK_RUNNING,
|
||||||
|
.message = "Analyse du dossier source...",
|
||||||
|
};
|
||||||
|
task->thread_started = true;
|
||||||
|
int error = pthread_create(&task->thread, NULL, run_import, task);
|
||||||
|
if (error != 0) {
|
||||||
|
task->thread_started = false;
|
||||||
|
task->snapshot.status = LARDON3D_IMPORT_TASK_FAILED;
|
||||||
|
copy_message(
|
||||||
|
task->snapshot.message,
|
||||||
|
"Erreur : impossible de lancer la tâche d'import."
|
||||||
|
);
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
lardon3d_import_task_request_cancel(Lardon3DImportTask *task)
|
||||||
|
{
|
||||||
|
if (!task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_lock(&task->mutex);
|
||||||
|
if (task->snapshot.status == LARDON3D_IMPORT_TASK_RUNNING) {
|
||||||
|
task->cancel_requested = true;
|
||||||
|
copy_message(task->snapshot.message, "Annulation demandée...");
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_import_task_snapshot(
|
||||||
|
Lardon3DImportTask *task,
|
||||||
|
Lardon3DImportTaskSnapshot *snapshot
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!task || !snapshot) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_lock(&task->mutex);
|
||||||
|
*snapshot = task->snapshot;
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_import_task_is_finished(Lardon3DImportTask *task)
|
||||||
|
{
|
||||||
|
Lardon3DImportTaskSnapshot snapshot;
|
||||||
|
if (!lardon3d_import_task_snapshot(task, &snapshot)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return snapshot.status == LARDON3D_IMPORT_TASK_SUCCEEDED
|
||||||
|
|| snapshot.status == LARDON3D_IMPORT_TASK_CANCELLED
|
||||||
|
|| snapshot.status == LARDON3D_IMPORT_TASK_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
lardon3d_import_task_join(Lardon3DImportTask *task)
|
||||||
|
{
|
||||||
|
if (!task) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)pthread_mutex_lock(&task->mutex);
|
||||||
|
bool started = task->thread_started;
|
||||||
|
bool joined = task->joined;
|
||||||
|
bool should_join = started && !joined;
|
||||||
|
pthread_t thread = task->thread;
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
if (!should_join) {
|
||||||
|
return started && joined;
|
||||||
|
}
|
||||||
|
if (pthread_join(thread, NULL) != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_lock(&task->mutex);
|
||||||
|
task->joined = true;
|
||||||
|
(void)pthread_mutex_unlock(&task->mutex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
lardon3d_import_task_destroy(Lardon3DImportTask *task)
|
||||||
|
{
|
||||||
|
if (!task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (task->thread_started && !task->joined) {
|
||||||
|
lardon3d_import_task_request_cancel(task);
|
||||||
|
if (!lardon3d_import_task_join(task)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(void)pthread_mutex_destroy(&task->mutex);
|
||||||
|
free(task);
|
||||||
|
}
|
||||||
76
src/layout.c
76
src/layout.c
|
|
@ -1,4 +1,5 @@
|
||||||
#include <ncurses.h>
|
#include <ncurses.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include <lardon3d/layout.h>
|
#include <lardon3d/layout.h>
|
||||||
|
|
@ -134,9 +135,62 @@ static void
|
||||||
draw_import_screen(
|
draw_import_screen(
|
||||||
const char *input_text,
|
const char *input_text,
|
||||||
const char *input_label,
|
const char *input_label,
|
||||||
|
const Lardon3DImportTaskSnapshot *snapshot,
|
||||||
int columns
|
int columns
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
if (snapshot && snapshot->status == LARDON3D_IMPORT_TASK_RUNNING) {
|
||||||
|
draw_text(6, 4, columns - 6, "Import en cours");
|
||||||
|
char line[128];
|
||||||
|
(void)snprintf(
|
||||||
|
line,
|
||||||
|
sizeof(line),
|
||||||
|
"Fichiers : %zu / %zu",
|
||||||
|
snapshot->processed,
|
||||||
|
snapshot->total
|
||||||
|
);
|
||||||
|
draw_text(7, 4, columns - 6, line);
|
||||||
|
(void)snprintf(line, sizeof(line), "Copiés : %zu", snapshot->copied);
|
||||||
|
draw_text(8, 4, columns - 6, line);
|
||||||
|
(void)snprintf(
|
||||||
|
line,
|
||||||
|
sizeof(line),
|
||||||
|
"Déjà présents : %zu",
|
||||||
|
snapshot->already_present
|
||||||
|
);
|
||||||
|
draw_text(9, 4, columns - 6, line);
|
||||||
|
(void)snprintf(line, sizeof(line), "Ignorés : %zu", snapshot->ignored);
|
||||||
|
draw_text(10, 4, columns - 6, line);
|
||||||
|
|
||||||
|
int percent = snapshot->total == 0
|
||||||
|
? 0
|
||||||
|
: (int)((snapshot->processed * 100) / snapshot->total);
|
||||||
|
if (percent > 100) {
|
||||||
|
percent = 100;
|
||||||
|
}
|
||||||
|
int bar_width = columns - 18;
|
||||||
|
if (bar_width > 40) {
|
||||||
|
bar_width = 40;
|
||||||
|
}
|
||||||
|
if (bar_width < 1) {
|
||||||
|
bar_width = 1;
|
||||||
|
}
|
||||||
|
int filled = (bar_width * percent) / 100;
|
||||||
|
char bar[64];
|
||||||
|
bar[0] = '[';
|
||||||
|
for (int index = 0; index < bar_width; ++index) {
|
||||||
|
bar[index + 1] = index < filled ? '#' : '-';
|
||||||
|
}
|
||||||
|
(void)snprintf(
|
||||||
|
bar + bar_width + 1,
|
||||||
|
sizeof(bar) - (size_t)bar_width - 1,
|
||||||
|
"] %d %%",
|
||||||
|
percent
|
||||||
|
);
|
||||||
|
draw_text(11, 4, columns - 6, bar);
|
||||||
|
draw_text(12, 4, columns - 6, "C : Annuler l'import");
|
||||||
|
return;
|
||||||
|
}
|
||||||
draw_text(7, 4, columns - 6, "I : Importer des images");
|
draw_text(7, 4, columns - 6, "I : Importer des images");
|
||||||
draw_text(8, 4, columns - 6, "ESC : Accueil");
|
draw_text(8, 4, columns - 6, "ESC : Accueil");
|
||||||
draw_text(9, 4, columns - 6, "Q : Quitter");
|
draw_text(9, 4, columns - 6, "Q : Quitter");
|
||||||
|
|
@ -148,6 +202,7 @@ draw_content(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
const char *input_text,
|
const char *input_text,
|
||||||
const char *input_label,
|
const char *input_label,
|
||||||
|
const Lardon3DImportTaskSnapshot *import_snapshot,
|
||||||
int rows,
|
int rows,
|
||||||
int columns
|
int columns
|
||||||
)
|
)
|
||||||
|
|
@ -156,6 +211,11 @@ draw_content(
|
||||||
const char *content;
|
const char *content;
|
||||||
const char *footer;
|
const char *footer;
|
||||||
screen_texts(state->screen, &title, &content, &footer);
|
screen_texts(state->screen, &title, &content, &footer);
|
||||||
|
bool import_running = import_snapshot
|
||||||
|
&& import_snapshot->status == LARDON3D_IMPORT_TASK_RUNNING;
|
||||||
|
if (import_running) {
|
||||||
|
footer = "C Annuler l'import Q désactivé";
|
||||||
|
}
|
||||||
int title_length = (int)strlen(title);
|
int title_length = (int)strlen(title);
|
||||||
int content_length = (int)strlen(content);
|
int content_length = (int)strlen(content);
|
||||||
int journal_row = rows - 7;
|
int journal_row = rows - 7;
|
||||||
|
|
@ -176,7 +236,12 @@ draw_content(
|
||||||
columns
|
columns
|
||||||
);
|
);
|
||||||
} else if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
} else if (state->screen == LARDON3D_SCREEN_IMPORT) {
|
||||||
draw_import_screen(input_text, input_label, columns);
|
draw_import_screen(
|
||||||
|
input_text,
|
||||||
|
input_label,
|
||||||
|
import_snapshot,
|
||||||
|
columns
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
draw_text(
|
draw_text(
|
||||||
(3 + journal_row) / 2,
|
(3 + journal_row) / 2,
|
||||||
|
|
@ -186,7 +251,12 @@ draw_content(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
draw_text(journal_row + 1, 2, columns - 4, "Journal");
|
draw_text(journal_row + 1, 2, columns - 4, "Journal");
|
||||||
draw_text(journal_row + 2, 4, columns - 6, state->status_message);
|
draw_text(
|
||||||
|
journal_row + 2,
|
||||||
|
4,
|
||||||
|
columns - 6,
|
||||||
|
import_running ? import_snapshot->message : state->status_message
|
||||||
|
);
|
||||||
draw_text(rows - 2, 2, columns - 4, footer);
|
draw_text(rows - 2, 2, columns - 4, footer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,6 +265,7 @@ lardon3d_layout_draw(
|
||||||
const Lardon3DAppState *state,
|
const Lardon3DAppState *state,
|
||||||
const char *input_text,
|
const char *input_text,
|
||||||
const char *input_label,
|
const char *input_label,
|
||||||
|
const Lardon3DImportTaskSnapshot *import_snapshot,
|
||||||
int rows,
|
int rows,
|
||||||
int columns
|
int columns
|
||||||
)
|
)
|
||||||
|
|
@ -209,6 +280,7 @@ lardon3d_layout_draw(
|
||||||
state,
|
state,
|
||||||
input_text,
|
input_text,
|
||||||
input_label,
|
input_label,
|
||||||
|
import_snapshot,
|
||||||
rows,
|
rows,
|
||||||
columns
|
columns
|
||||||
);
|
);
|
||||||
|
|
|
||||||
134
src/tui.c
134
src/tui.c
|
|
@ -4,7 +4,7 @@
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
#include <lardon3d/import.h>
|
#include <lardon3d/import_task.h>
|
||||||
#include <lardon3d/layout.h>
|
#include <lardon3d/layout.h>
|
||||||
#include <lardon3d/project.h>
|
#include <lardon3d/project.h>
|
||||||
#include <lardon3d/tui.h>
|
#include <lardon3d/tui.h>
|
||||||
|
|
@ -28,7 +28,11 @@ typedef struct {
|
||||||
} TuiInput;
|
} TuiInput;
|
||||||
|
|
||||||
static void
|
static void
|
||||||
redraw(const Lardon3DAppState *state, const TuiInput *input)
|
redraw(
|
||||||
|
const Lardon3DAppState *state,
|
||||||
|
const TuiInput *input,
|
||||||
|
Lardon3DImportTask *task
|
||||||
|
)
|
||||||
{
|
{
|
||||||
int rows;
|
int rows;
|
||||||
int columns;
|
int columns;
|
||||||
|
|
@ -41,7 +45,19 @@ redraw(const Lardon3DAppState *state, const TuiInput *input)
|
||||||
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
||||||
label = "Dossier source :";
|
label = "Dossier source :";
|
||||||
}
|
}
|
||||||
lardon3d_layout_draw(state, text, label, rows, columns);
|
Lardon3DImportTaskSnapshot snapshot;
|
||||||
|
const Lardon3DImportTaskSnapshot *displayed_snapshot = NULL;
|
||||||
|
if (task && lardon3d_import_task_snapshot(task, &snapshot)) {
|
||||||
|
displayed_snapshot = &snapshot;
|
||||||
|
}
|
||||||
|
lardon3d_layout_draw(
|
||||||
|
state,
|
||||||
|
text,
|
||||||
|
label,
|
||||||
|
displayed_snapshot,
|
||||||
|
rows,
|
||||||
|
columns
|
||||||
|
);
|
||||||
(void)curs_set(
|
(void)curs_set(
|
||||||
input->mode != INPUT_NONE
|
input->mode != INPUT_NONE
|
||||||
&& rows >= MINIMUM_ROWS
|
&& rows >= MINIMUM_ROWS
|
||||||
|
|
@ -51,10 +67,48 @@ redraw(const Lardon3DAppState *state, const TuiInput *input)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
start_import_task(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
TuiInput *input,
|
||||||
|
Lardon3DImportTask **task
|
||||||
|
)
|
||||||
|
{
|
||||||
|
*task = lardon3d_import_task_create();
|
||||||
|
if (!*task) {
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Erreur : impossible de créer la tâche d'import."
|
||||||
|
);
|
||||||
|
input->mode = INPUT_NONE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!lardon3d_import_task_start(*task, state, input->text)) {
|
||||||
|
Lardon3DImportTaskSnapshot snapshot;
|
||||||
|
if (lardon3d_import_task_snapshot(*task, &snapshot)
|
||||||
|
&& snapshot.message[0]) {
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"%s",
|
||||||
|
snapshot.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lardon3d_import_task_destroy(*task);
|
||||||
|
*task = NULL;
|
||||||
|
input->mode = INPUT_NONE;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
input->mode = INPUT_NONE;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
handle_active_input(
|
handle_active_input(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
TuiInput *input,
|
TuiInput *input,
|
||||||
|
Lardon3DImportTask **task,
|
||||||
int key
|
int key
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
@ -83,8 +137,7 @@ handle_active_input(
|
||||||
if (input->mode == INPUT_PROJECT_OPEN) {
|
if (input->mode == INPUT_PROJECT_OPEN) {
|
||||||
(void)lardon3d_project_open(state, input->text);
|
(void)lardon3d_project_open(state, input->text);
|
||||||
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
||||||
Lardon3DImportResult result;
|
(void)start_import_task(state, input, task);
|
||||||
(void)lardon3d_import_directory(state, input->text, &result);
|
|
||||||
} else {
|
} else {
|
||||||
(void)lardon3d_project_create(state, input->text);
|
(void)lardon3d_project_create(state, input->text);
|
||||||
}
|
}
|
||||||
|
|
@ -105,8 +158,11 @@ handle_active_input(
|
||||||
if (input->mode == INPUT_PROJECT_OPEN) {
|
if (input->mode == INPUT_PROJECT_OPEN) {
|
||||||
(void)lardon3d_project_open(state, input->text);
|
(void)lardon3d_project_open(state, input->text);
|
||||||
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
} else if (input->mode == INPUT_IMPORT_DIRECTORY) {
|
||||||
Lardon3DImportResult result;
|
(void)snprintf(
|
||||||
(void)lardon3d_import_directory(state, input->text, &result);
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Erreur : chemin source trop long."
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
(void)lardon3d_project_create(state, input->text);
|
(void)lardon3d_project_create(state, input->text);
|
||||||
}
|
}
|
||||||
|
|
@ -127,9 +183,29 @@ static bool
|
||||||
handle_normal_input(
|
handle_normal_input(
|
||||||
Lardon3DAppState *state,
|
Lardon3DAppState *state,
|
||||||
TuiInput *input,
|
TuiInput *input,
|
||||||
|
Lardon3DImportTask *task,
|
||||||
int key
|
int key
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
if (task) {
|
||||||
|
if (key == 'c' || key == 'C') {
|
||||||
|
lardon3d_import_task_request_cancel(task);
|
||||||
|
} else if (key == 'q' || key == 'Q') {
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Quitter est désactivé pendant l'import."
|
||||||
|
);
|
||||||
|
} else if (key == 27) {
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"Accueil indisponible pendant l'import."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return key == KEY_RESIZE || key == 'c' || key == 'C'
|
||||||
|
|| key == 'q' || key == 'Q' || key == 27;
|
||||||
|
}
|
||||||
if (key == 'q' || key == 'Q') {
|
if (key == 'q' || key == 'Q') {
|
||||||
state->running = false;
|
state->running = false;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -218,6 +294,7 @@ lardon3d_tui_init(void)
|
||||||
endwin();
|
endwin();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
timeout(75);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -230,23 +307,52 @@ lardon3d_tui_run(Lardon3DAppState *state)
|
||||||
}
|
}
|
||||||
|
|
||||||
TuiInput input = {0};
|
TuiInput input = {0};
|
||||||
redraw(state, &input);
|
Lardon3DImportTask *task = NULL;
|
||||||
|
redraw(state, &input, task);
|
||||||
|
|
||||||
while (state->running) {
|
while (state->running) {
|
||||||
int key = getch();
|
int key = getch();
|
||||||
if (key == ERR) {
|
|
||||||
return false;
|
bool should_redraw = task != NULL;
|
||||||
|
if (task && lardon3d_import_task_is_finished(task)) {
|
||||||
|
Lardon3DImportTaskSnapshot snapshot;
|
||||||
|
if (!lardon3d_import_task_join(task)
|
||||||
|
|| !lardon3d_import_task_snapshot(task, &snapshot)) {
|
||||||
|
lardon3d_import_task_destroy(task);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
(void)snprintf(
|
||||||
|
state->status_message,
|
||||||
|
sizeof(state->status_message),
|
||||||
|
"%s",
|
||||||
|
snapshot.message
|
||||||
|
);
|
||||||
|
lardon3d_import_task_destroy(task);
|
||||||
|
task = NULL;
|
||||||
|
should_redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool should_redraw = input.mode != INPUT_NONE
|
if (key != ERR) {
|
||||||
? handle_active_input(state, &input, key)
|
should_redraw = (input.mode != INPUT_NONE
|
||||||
: handle_normal_input(state, &input, key);
|
? handle_active_input(state, &input, &task, key)
|
||||||
|
: handle_normal_input(state, &input, task, key))
|
||||||
|
|| should_redraw;
|
||||||
|
}
|
||||||
|
|
||||||
if (should_redraw) {
|
if (should_redraw) {
|
||||||
redraw(state, &input);
|
redraw(state, &input, task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (task) {
|
||||||
|
lardon3d_import_task_request_cancel(task);
|
||||||
|
if (!lardon3d_import_task_join(task)) {
|
||||||
|
lardon3d_import_task_destroy(task);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lardon3d_import_task_destroy(task);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
324
tests/test_import_task.c
Normal file
324
tests/test_import_task.c
Normal file
|
|
@ -0,0 +1,324 @@
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.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/app_state.h>
|
||||||
|
#include <lardon3d/import_task.h>
|
||||||
|
|
||||||
|
#define CHECK(condition) \
|
||||||
|
do { \
|
||||||
|
if (!(condition)) { \
|
||||||
|
(void)fprintf(stderr, "Échec ligne %d : %s\n", __LINE__, #condition); \
|
||||||
|
return false; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static bool
|
||||||
|
join_path(char destination[PATH_MAX], const char *parent, const char *child)
|
||||||
|
{
|
||||||
|
int written = snprintf(destination, PATH_MAX, "%s/%s", parent, child);
|
||||||
|
return written >= 0 && (size_t)written < PATH_MAX;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
write_all(int descriptor, const void *data, size_t size)
|
||||||
|
{
|
||||||
|
const char *bytes = data;
|
||||||
|
size_t total = 0;
|
||||||
|
while (total < size) {
|
||||||
|
ssize_t written = write(descriptor, bytes + total, size - total);
|
||||||
|
if (written < 0 && errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (written <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
total += (size_t)written;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
create_file(const char *path, size_t size)
|
||||||
|
{
|
||||||
|
int descriptor = open(path, O_WRONLY | O_CREAT | O_EXCL, 0644);
|
||||||
|
if (descriptor < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char block[64 * 1024];
|
||||||
|
(void)memset(block, 'L', sizeof(block));
|
||||||
|
bool success = true;
|
||||||
|
while (size > 0) {
|
||||||
|
size_t chunk = size < sizeof(block) ? size : sizeof(block);
|
||||||
|
if (!write_all(descriptor, block, chunk)) {
|
||||||
|
success = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
size -= chunk;
|
||||||
|
}
|
||||||
|
if (close(descriptor) != 0) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
read_file(const char *path, char *content, size_t capacity)
|
||||||
|
{
|
||||||
|
int descriptor = open(path, O_RDONLY);
|
||||||
|
if (descriptor < 0 || capacity == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
size_t total = 0;
|
||||||
|
while (total + 1 < capacity) {
|
||||||
|
ssize_t count = read(descriptor, content + total, capacity - total - 1);
|
||||||
|
if (count == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (count < 0 && errno == EINTR) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (count < 0) {
|
||||||
|
(void)close(descriptor);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
total += (size_t)count;
|
||||||
|
}
|
||||||
|
content[total] = '\0';
|
||||||
|
return close(descriptor) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
remove_tree(const char *path)
|
||||||
|
{
|
||||||
|
struct stat info;
|
||||||
|
if (lstat(path, &info) != 0) {
|
||||||
|
return errno == ENOENT;
|
||||||
|
}
|
||||||
|
if (!S_ISDIR(info.st_mode)) {
|
||||||
|
return unlink(path) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
DIR *directory = opendir(path);
|
||||||
|
if (!directory) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool success = true;
|
||||||
|
for (struct dirent *entry = readdir(directory);
|
||||||
|
entry;
|
||||||
|
entry = readdir(directory)) {
|
||||||
|
if (strcmp(entry->d_name, ".") == 0
|
||||||
|
|| strcmp(entry->d_name, "..") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
char child[PATH_MAX];
|
||||||
|
if (!join_path(child, path, entry->d_name) || !remove_tree(child)) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closedir(directory) != 0 || rmdir(path) != 0) {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
has_temporary_file(const char *images_path)
|
||||||
|
{
|
||||||
|
DIR *directory = opendir(images_path);
|
||||||
|
if (!directory) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool found = false;
|
||||||
|
for (struct dirent *entry = readdir(directory);
|
||||||
|
entry;
|
||||||
|
entry = readdir(directory)) {
|
||||||
|
if (strncmp(entry->d_name, ".manifest.tsv.tmp.", 18) == 0) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (closedir(directory) != 0) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
wait_until_finished(
|
||||||
|
Lardon3DImportTask *task,
|
||||||
|
Lardon3DImportTaskSnapshot *snapshot
|
||||||
|
)
|
||||||
|
{
|
||||||
|
const struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
||||||
|
for (size_t attempt = 0; attempt < 10000; ++attempt) {
|
||||||
|
if (!lardon3d_import_task_snapshot(task, snapshot)
|
||||||
|
|| snapshot->processed > snapshot->total) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (snapshot->status != LARDON3D_IMPORT_TASK_RUNNING) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(void)nanosleep(&pause, NULL);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
run_success_tests(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *source,
|
||||||
|
const char *manifest
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Lardon3DImportTask *unused = lardon3d_import_task_create();
|
||||||
|
CHECK(unused);
|
||||||
|
CHECK(!lardon3d_import_task_start(NULL, state, source));
|
||||||
|
CHECK(!lardon3d_import_task_start(unused, NULL, source));
|
||||||
|
CHECK(!lardon3d_import_task_start(unused, state, NULL));
|
||||||
|
lardon3d_import_task_destroy(unused);
|
||||||
|
|
||||||
|
Lardon3DImportTask *task = lardon3d_import_task_create();
|
||||||
|
CHECK(task);
|
||||||
|
CHECK(lardon3d_import_task_start(task, state, source));
|
||||||
|
CHECK(!lardon3d_import_task_start(task, state, source));
|
||||||
|
Lardon3DImportTaskSnapshot snapshot;
|
||||||
|
CHECK(wait_until_finished(task, &snapshot));
|
||||||
|
CHECK(snapshot.status == LARDON3D_IMPORT_TASK_SUCCEEDED);
|
||||||
|
CHECK(snapshot.total == 2);
|
||||||
|
CHECK(snapshot.processed == 2);
|
||||||
|
CHECK(snapshot.copied == 2);
|
||||||
|
CHECK(snapshot.already_present == 0);
|
||||||
|
CHECK(lardon3d_import_task_join(task));
|
||||||
|
CHECK(lardon3d_import_task_join(task));
|
||||||
|
lardon3d_import_task_destroy(task);
|
||||||
|
CHECK(access(manifest, F_OK) == 0);
|
||||||
|
|
||||||
|
task = lardon3d_import_task_create();
|
||||||
|
CHECK(task && lardon3d_import_task_start(task, state, source));
|
||||||
|
CHECK(wait_until_finished(task, &snapshot));
|
||||||
|
CHECK(snapshot.status == LARDON3D_IMPORT_TASK_SUCCEEDED);
|
||||||
|
CHECK(snapshot.copied == 0);
|
||||||
|
CHECK(snapshot.already_present == 2);
|
||||||
|
CHECK(lardon3d_import_task_join(task));
|
||||||
|
lardon3d_import_task_destroy(task);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
run_cancellation_test(
|
||||||
|
Lardon3DAppState *state,
|
||||||
|
const char *source,
|
||||||
|
const char *images,
|
||||||
|
const char *originals,
|
||||||
|
const char *manifest
|
||||||
|
)
|
||||||
|
{
|
||||||
|
char manifest_before[8192];
|
||||||
|
CHECK(read_file(manifest, manifest_before, sizeof(manifest_before)));
|
||||||
|
|
||||||
|
Lardon3DImportTask *task = lardon3d_import_task_create();
|
||||||
|
CHECK(task && lardon3d_import_task_start(task, state, source));
|
||||||
|
Lardon3DImportTaskSnapshot snapshot;
|
||||||
|
const struct timespec pause = {.tv_sec = 0, .tv_nsec = 1000000};
|
||||||
|
bool copy_observed = false;
|
||||||
|
for (size_t attempt = 0; attempt < 10000; ++attempt) {
|
||||||
|
CHECK(lardon3d_import_task_snapshot(task, &snapshot));
|
||||||
|
CHECK(snapshot.processed <= snapshot.total);
|
||||||
|
if (snapshot.status != LARDON3D_IMPORT_TASK_RUNNING) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (snapshot.copied > 0) {
|
||||||
|
copy_observed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
(void)nanosleep(&pause, NULL);
|
||||||
|
}
|
||||||
|
CHECK(copy_observed);
|
||||||
|
lardon3d_import_task_request_cancel(task);
|
||||||
|
CHECK(wait_until_finished(task, &snapshot));
|
||||||
|
CHECK(snapshot.status == LARDON3D_IMPORT_TASK_CANCELLED);
|
||||||
|
CHECK(lardon3d_import_task_join(task));
|
||||||
|
lardon3d_import_task_destroy(task);
|
||||||
|
|
||||||
|
char manifest_after[8192];
|
||||||
|
CHECK(read_file(manifest, manifest_after, sizeof(manifest_after)));
|
||||||
|
CHECK(strcmp(manifest_before, manifest_after) == 0);
|
||||||
|
CHECK(!has_temporary_file(images));
|
||||||
|
for (size_t index = 0; index < 8; ++index) {
|
||||||
|
char filename[64];
|
||||||
|
CHECK(snprintf(filename, sizeof(filename), "large-%zu.jpg", index) > 0);
|
||||||
|
char destination[PATH_MAX];
|
||||||
|
CHECK(join_path(destination, originals, filename));
|
||||||
|
CHECK(access(destination, F_OK) != 0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
run_test(void)
|
||||||
|
{
|
||||||
|
char base[] = "/tmp/lardon3d-import-task-test.XXXXXX";
|
||||||
|
CHECK(mkdtemp(base));
|
||||||
|
char project[PATH_MAX];
|
||||||
|
char images[PATH_MAX];
|
||||||
|
char originals[PATH_MAX];
|
||||||
|
char source[PATH_MAX];
|
||||||
|
char large_source[PATH_MAX];
|
||||||
|
char manifest[PATH_MAX];
|
||||||
|
CHECK(join_path(project, base, "project"));
|
||||||
|
CHECK(join_path(images, project, "images"));
|
||||||
|
CHECK(join_path(originals, images, "originals"));
|
||||||
|
CHECK(join_path(source, base, "source"));
|
||||||
|
CHECK(join_path(large_source, base, "large-source"));
|
||||||
|
CHECK(join_path(manifest, images, "manifest.tsv"));
|
||||||
|
CHECK(mkdir(project, 0755) == 0);
|
||||||
|
CHECK(mkdir(images, 0755) == 0);
|
||||||
|
CHECK(mkdir(source, 0755) == 0);
|
||||||
|
CHECK(mkdir(large_source, 0755) == 0);
|
||||||
|
|
||||||
|
char path[PATH_MAX];
|
||||||
|
CHECK(join_path(path, source, "one.jpg"));
|
||||||
|
CHECK(create_file(path, 3));
|
||||||
|
CHECK(join_path(path, source, "two.png"));
|
||||||
|
CHECK(create_file(path, 7));
|
||||||
|
for (size_t index = 0; index < 8; ++index) {
|
||||||
|
char filename[64];
|
||||||
|
CHECK(snprintf(filename, sizeof(filename), "large-%zu.jpg", index) > 0);
|
||||||
|
CHECK(join_path(path, large_source, filename));
|
||||||
|
CHECK(create_file(path, 16 * 1024 * 1024));
|
||||||
|
}
|
||||||
|
|
||||||
|
Lardon3DAppState state;
|
||||||
|
lardon3d_app_state_init(&state);
|
||||||
|
state.project_loaded = true;
|
||||||
|
CHECK(snprintf(
|
||||||
|
state.project_path,
|
||||||
|
sizeof(state.project_path),
|
||||||
|
"%s",
|
||||||
|
project
|
||||||
|
) > 0);
|
||||||
|
CHECK(run_success_tests(&state, source, manifest));
|
||||||
|
CHECK(run_cancellation_test(
|
||||||
|
&state,
|
||||||
|
large_source,
|
||||||
|
images,
|
||||||
|
originals,
|
||||||
|
manifest
|
||||||
|
));
|
||||||
|
CHECK(remove_tree(base));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main(void)
|
||||||
|
{
|
||||||
|
return run_test() ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue