From 6026cc027599ff8e0ffa1af1bb3f71f551b08ab7 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Sun, 2 Nov 2025 15:46:50 -0500 Subject: Add semaphore wrapper --- src/lib/core/CMakeLists.txt | 1 + src/lib/core/include/sem.h | 17 +++++++++++++++++ src/lib/core/semaphore.c | 26 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 src/lib/core/include/sem.h create mode 100644 src/lib/core/semaphore.c diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt index e3e3d4b..7b33cf9 100644 --- a/src/lib/core/CMakeLists.txt +++ b/src/lib/core/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(stasis_core STATIC template_func_proto.c envctl.c multiprocessing.c + semaphore.c ) target_include_directories(stasis_core PRIVATE ${core_INCLUDE} diff --git a/src/lib/core/include/sem.h b/src/lib/core/include/sem.h new file mode 100644 index 0000000..868f33d --- /dev/null +++ b/src/lib/core/include/sem.h @@ -0,0 +1,17 @@ +#ifndef STASIS_SEMAPHORE_H +#define STASIS_SEMAPHORE_H + +#include + +struct Semaphore { + sem_t *sem; + char name[255]; +}; + +int semaphore_init(struct Semaphore *s, const char *name, int value); +int semaphore_wait(struct Semaphore *s); +int semaphore_post(struct Semaphore *s); + +void semaphore_destroy(struct Semaphore *s); + +#endif //STASIS_SEMAPHORE_H \ No newline at end of file diff --git a/src/lib/core/semaphore.c b/src/lib/core/semaphore.c new file mode 100644 index 0000000..9ea4c9c --- /dev/null +++ b/src/lib/core/semaphore.c @@ -0,0 +1,26 @@ +#include +#include +#include "sem.h" + +int semaphore_init(struct Semaphore *s, const char *name, const int value) { + snprintf(s->name, sizeof(s->name), "/%s", name); + s->sem = sem_open(s->name, O_CREAT, 0644, value); + if (s->sem == SEM_FAILED) { + perror("sem_open"); + return -1; + } + return 0; +} + +int semaphore_wait(struct Semaphore *s) { + return sem_wait(s->sem); +} + +int semaphore_post(struct Semaphore *s) { + return sem_post(s->sem); +} + +void semaphore_destroy(struct Semaphore *s) { + sem_close(s->sem); + sem_unlink(s->name); +} -- cgit From dacc7e84c7b1c9f68a39f8bc0545bf4e73f5af25 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Sun, 2 Nov 2025 15:48:01 -0500 Subject: Integrate semaphore --- src/lib/core/include/multiprocessing.h | 3 ++- src/lib/core/multiprocessing.c | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/lib/core/include/multiprocessing.h b/src/lib/core/include/multiprocessing.h index ff674e9..6477818 100644 --- a/src/lib/core/include/multiprocessing.h +++ b/src/lib/core/include/multiprocessing.h @@ -3,9 +3,9 @@ #define STASIS_MULTIPROCESSING_H #include "core.h" +#include "sem.h" #include #include -#include #include #include #include @@ -38,6 +38,7 @@ struct MultiProcessingPool { char ident[255]; ///< Identity of task pool char log_root[PATH_MAX]; ///< Base directory to store stderr/stdout log files int status_interval; ///< Report a pooled task is "running" every n seconds + struct Semaphore semaphore; }; /// A multiprocessing task's initial state (i.e. "FAIL") diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index d59a7cd..74eb95e 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -80,14 +80,20 @@ int parent(struct MultiProcessingPool *pool, struct MultiProcessingTask *task, p static int mp_task_fork(struct MultiProcessingPool *pool, struct MultiProcessingTask *task) { SYSDEBUG("Preparing to fork() child task %s:%s", pool->ident, task->ident); + semaphore_wait(&pool->semaphore); pid_t pid = fork(); + int parent_status = 0; int child_status = 0; if (pid == -1) { return -1; - } else if (pid == 0) { + } + if (pid == 0) { child(pool, task); + } else { + parent_status = parent(pool, task, pid, &child_status); } - return parent(pool, task, pid, &child_status); + semaphore_post(&pool->semaphore); + return parent_status; } struct MultiProcessingTask *mp_pool_task(struct MultiProcessingPool *pool, const char *ident, char *working_dir, char *cmd) { @@ -398,6 +404,7 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { pool_deadlocked: puts(""); + return failures; } @@ -441,12 +448,20 @@ struct MultiProcessingPool *mp_pool_init(const char *ident, const char *log_root return NULL; } + if (semaphore_init(&pool->semaphore, "stasis_pool_lock", 2) != 0) { + fprintf(stderr, "unable to initialize semaphore\n"); + mp_pool_free(&pool); + return NULL; + } + return pool; } void mp_pool_free(struct MultiProcessingPool **pool) { for (size_t i = 0; i < (*pool)->num_alloc; i++) { } + semaphore_destroy(&(*pool)->semaphore); + // Unmap all pool tasks if ((*pool)->task) { if ((*pool)->task->cmd) { -- cgit From 0230268fb96f100e6747206364fdae35c451f7cf Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 3 Nov 2025 06:54:58 -0500 Subject: Integrate semaphore --- src/lib/core/semaphore.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/lib/core/semaphore.c b/src/lib/core/semaphore.c index 9ea4c9c..a884469 100644 --- a/src/lib/core/semaphore.c +++ b/src/lib/core/semaphore.c @@ -1,6 +1,9 @@ #include #include + +#include "core_message.h" #include "sem.h" +#include "utils.h" int semaphore_init(struct Semaphore *s, const char *name, const int value) { snprintf(s->name, sizeof(s->name), "/%s", name); @@ -9,18 +12,26 @@ int semaphore_init(struct Semaphore *s, const char *name, const int value) { perror("sem_open"); return -1; } + SYSDEBUG("%s", s->name); return 0; } int semaphore_wait(struct Semaphore *s) { + int state = 0; + sem_getvalue(s->sem, &state); + SYSDEBUG("%s", s->name); return sem_wait(s->sem); } int semaphore_post(struct Semaphore *s) { + int state = 0; + sem_getvalue(s->sem, &state); + SYSDEBUG("%s", s->name); return sem_post(s->sem); } void semaphore_destroy(struct Semaphore *s) { + SYSDEBUG("%s", s->name); sem_close(s->sem); sem_unlink(s->name); } -- cgit From 0771a8593fea16e089c50f2ce853423b1450da7c Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 3 Nov 2025 08:24:53 -0500 Subject: Integrate semaphore --- src/lib/core/multiprocessing.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index 74eb95e..b475111 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -448,7 +448,9 @@ struct MultiProcessingPool *mp_pool_init(const char *ident, const char *log_root return NULL; } - if (semaphore_init(&pool->semaphore, "stasis_pool_lock", 2) != 0) { + char semaphore_name[255] = {0}; + snprintf(semaphore_name, sizeof(semaphore_name), "stasis_mp_semaphore_%s", ident); + if (semaphore_init(&pool->semaphore, semaphore_name, 2) != 0) { fprintf(stderr, "unable to initialize semaphore\n"); mp_pool_free(&pool); return NULL; -- cgit From 63e3b849777aaa70df06876878b4ec460a43080a Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 3 Nov 2025 10:16:45 -0500 Subject: Flush buffers --- src/lib/core/multiprocessing.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index b475111..889ec3f 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -91,6 +91,8 @@ static int mp_task_fork(struct MultiProcessingPool *pool, struct MultiProcessing child(pool, task); } else { parent_status = parent(pool, task, pid, &child_status); + fflush(stdout); + fflush(stderr); } semaphore_post(&pool->semaphore); return parent_status; @@ -215,6 +217,7 @@ static int show_log_contents(FILE *stream, struct MultiProcessingTask *task) { memset(buf, 0, sizeof(buf)); } fprintf(stream, "\n"); + fflush(stream); fclose(fp); return 0; } -- cgit From b674adfebf5d7b756f9d996a5229e7aa733a7797 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 3 Nov 2025 10:17:36 -0500 Subject: Decrease length of semaphore name --- src/lib/core/multiprocessing.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index 889ec3f..c9d22a1 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -452,7 +452,7 @@ struct MultiProcessingPool *mp_pool_init(const char *ident, const char *log_root } char semaphore_name[255] = {0}; - snprintf(semaphore_name, sizeof(semaphore_name), "stasis_mp_semaphore_%s", ident); + snprintf(semaphore_name, sizeof(semaphore_name), "stasis_mp_%s", ident); if (semaphore_init(&pool->semaphore, semaphore_name, 2) != 0) { fprintf(stderr, "unable to initialize semaphore\n"); mp_pool_free(&pool); -- cgit From 212e56f1256b8ff3e9dacb0b867e48fcb9d2a315 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 11 Nov 2025 15:52:55 -0500 Subject: Add --no-task-logging CLI argument --- src/cli/stasis/args.c | 2 ++ src/cli/stasis/include/args.h | 1 + src/lib/core/globals.c | 1 + src/lib/core/include/core.h | 1 + 4 files changed, 5 insertions(+) diff --git a/src/cli/stasis/args.c b/src/cli/stasis/args.c index f3ce823..fbda494 100644 --- a/src/cli/stasis/args.c +++ b/src/cli/stasis/args.c @@ -20,6 +20,7 @@ struct option long_options[] = { {"no-artifactory-upload", no_argument, 0, OPT_NO_ARTIFACTORY_UPLOAD}, {"no-testing", no_argument, 0, OPT_NO_TESTING}, {"no-parallel", no_argument, 0, OPT_NO_PARALLEL}, + {"no-task-logging", no_argument, 0, OPT_NO_TASK_LOGGING}, {"no-rewrite", no_argument, 0, OPT_NO_REWRITE_SPEC_STAGE_2}, {0, 0, 0, 0}, }; @@ -43,6 +44,7 @@ const char *long_options_help[] = { "Do not upload artifacts to Artifactory (dry-run)", "Do not execute test scripts", "Do not execute tests in parallel", + "Do not log task output (write to stdout)", "Do not rewrite paths and URLs in output files", NULL, }; diff --git a/src/cli/stasis/include/args.h b/src/cli/stasis/include/args.h index 5bad752..850be4a 100644 --- a/src/cli/stasis/include/args.h +++ b/src/cli/stasis/include/args.h @@ -17,6 +17,7 @@ #define OPT_FAIL_FAST 1009 #define OPT_NO_PARALLEL 1010 #define OPT_POOL_STATUS_INTERVAL 1011 +#define OPT_NO_TASK_LOGGING 1012 extern struct option long_options[]; void usage(char *progname); diff --git a/src/lib/core/globals.c b/src/lib/core/globals.c index d84e799..a262d6c 100644 --- a/src/lib/core/globals.c +++ b/src/lib/core/globals.c @@ -41,6 +41,7 @@ struct STASIS_GLOBAL globals = { .enable_testing = true, ///< Toggle [test] block "script" execution. "script_setup" always executes. .enable_rewrite_spec_stage_2 = true, ///< Leave template stings in output files .enable_parallel = true, ///< Toggle testing in parallel + .enable_task_logging = true, ///< Toggle logging for multiprocess tasks .parallel_fail_fast = false, ///< Kill ALL multiprocessing tasks immediately on error .pool_status_interval = 30, ///< Report "Task is running" }; diff --git a/src/lib/core/include/core.h b/src/lib/core/include/core.h index 92969d2..e96e010 100644 --- a/src/lib/core/include/core.h +++ b/src/lib/core/include/core.h @@ -42,6 +42,7 @@ struct STASIS_GLOBAL { bool enable_overwrite; //!< Enable release file clobbering bool enable_rewrite_spec_stage_2; //!< Enable automatic @STR@ replacement in output files bool enable_parallel; //!< Enable testing in parallel + bool enable_task_logging; //!< Enable logging task output to a file long cpu_limit; //!< Limit parallel processing to n cores (default: max - 1) long parallel_fail_fast; //!< Fail immediately on error int pool_status_interval; //!< Report "Task is running" every n seconds -- cgit From 36964de6ce26241e40693509efeefa613bc49a28 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 11 Nov 2025 15:53:14 -0500 Subject: Integrate --no-task-logging CLI argument --- src/cli/stasis/stasis_main.c | 3 +++ src/lib/core/multiprocessing.c | 35 ++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/cli/stasis/stasis_main.c b/src/cli/stasis/stasis_main.c index 2ce6831..967ecaf 100644 --- a/src/cli/stasis/stasis_main.c +++ b/src/cli/stasis/stasis_main.c @@ -586,6 +586,9 @@ int main(int argc, char *argv[]) { case OPT_NO_PARALLEL: globals.enable_parallel = false; break; + case OPT_NO_TASK_LOGGING: + globals.enable_task_logging = false; + break; case '?': default: exit(1); diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index c9d22a1..69719e8 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -27,8 +27,11 @@ int child(struct MultiProcessingPool *pool, struct MultiProcessingTask *task) { // Redirect stdout and stderr to the log file fflush(stdout); fflush(stderr); + // Set log file name - sprintf(task->log_file + strlen(task->log_file), "task-%zu-%d.log", mp_global_task_count, task->parent_pid); + if (globals.enable_task_logging) { + sprintf(task->log_file + strlen(task->log_file), "task-%zu-%d.log", mp_global_task_count, task->parent_pid); + } fp_log = freopen(task->log_file, "w+", stdout); if (!fp_log) { fprintf(stderr, "unable to open '%s' for writing: %s\n", task->log_file, strerror(errno)); @@ -118,8 +121,12 @@ struct MultiProcessingTask *mp_pool_task(struct MultiProcessingPool *pool, const // Set log file path memset(slot->log_file, 0, sizeof(*slot->log_file)); - strcat(slot->log_file, pool->log_root); - strcat(slot->log_file, "/"); + if (globals.enable_task_logging) { + strcat(slot->log_file, pool->log_root); + strcat(slot->log_file, "/"); + } else { + strcpy(slot->log_file, "/dev/stdout"); + } // Set working directory if (isempty(working_dir)) { @@ -251,9 +258,11 @@ int mp_pool_kill(struct MultiProcessingPool *pool, int signum) { } } } - if (!access(slot->log_file, F_OK)) { - SYSDEBUG("Removing log file: %s", slot->log_file); - remove(slot->log_file); + if (globals.enable_task_logging) { + if (!access(slot->log_file, F_OK)) { + SYSDEBUG("Removing log file: %s", slot->log_file); + remove(slot->log_file); + } } if (!access(slot->parent_script, F_OK)) { SYSDEBUG("Removing runner script: %s", slot->parent_script); @@ -340,9 +349,11 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { fprintf(stderr, "%s Task state is unknown (0x%04X)\n", progress, status); } - // Show the log (always) - if (show_log_contents(stdout, slot)) { - perror(slot->log_file); + if (globals.enable_task_logging) { + // Show the log (always) + if (show_log_contents(stdout, slot)) { + perror(slot->log_file); + } } // Record the task stop time @@ -364,8 +375,10 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { } // Clean up logs and scripts left behind by the task - if (remove(slot->log_file)) { - fprintf(stderr, "%s Unable to remove log file: '%s': %s\n", progress, slot->parent_script, strerror(errno)); + if (globals.enable_task_logging) { + if (remove(slot->log_file)) { + fprintf(stderr, "%s Unable to remove log file: '%s': %s\n", progress, slot->parent_script, strerror(errno)); + } } if (remove(slot->parent_script)) { fprintf(stderr, "%s Unable to remove temporary script '%s': %s\n", progress, slot->parent_script, strerror(errno)); -- cgit From ce09eafdd23a23f1b6389cd2536dd73a6642196b Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 17 Nov 2025 16:11:35 -0500 Subject: Document semaphore usage --- src/lib/core/include/sem.h | 41 ++++++++++++++++++++++++++++++++++++++++- src/lib/core/semaphore.c | 3 +++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/lib/core/include/sem.h b/src/lib/core/include/sem.h index 868f33d..947f9e8 100644 --- a/src/lib/core/include/sem.h +++ b/src/lib/core/include/sem.h @@ -1,13 +1,52 @@ +/** +* @file sem.h +*/ #ifndef STASIS_SEMAPHORE_H #define STASIS_SEMAPHORE_H +#include "core.h" #include struct Semaphore { sem_t *sem; - char name[255]; + char name[STASIS_NAME_MAX]; }; +/** + * Initialize a cross-platform semaphore (Linux/Darwin) + * + * @code c + * #include "sem.h" + * + * int main(int argc, char *argv[]) { + * struct Semaphore s; + * if (semaphore_init(&s, "mysem", 1)) { + * perror("semaphore_init failed"); + * exit(1); + * } + * if (semaphore_wait(&s)) { + * perror("semaphore_wait failed"); + * exit(1); + * } + * + * // + * // Critical section + * // CODE HERE + * // + * + * if (semaphore_post(&s)) { + * perror("semaphore_post failed"); + * exit(1); + * } + * } + * @endcode + * + * @param s a pointer to `Semaphore` + * @param name of the semaphore + * @param value initial value of the semaphore + * @return -1 on error + * @return 0 on success + */ int semaphore_init(struct Semaphore *s, const char *name, int value); int semaphore_wait(struct Semaphore *s); int semaphore_post(struct Semaphore *s); diff --git a/src/lib/core/semaphore.c b/src/lib/core/semaphore.c index a884469..60a928a 100644 --- a/src/lib/core/semaphore.c +++ b/src/lib/core/semaphore.c @@ -1,3 +1,6 @@ +/** +* @file semaphore.c +*/ #include #include -- cgit From 46cf2462a09dcb5bdee27523494a180b3a99c2e9 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 17 Nov 2025 16:12:10 -0500 Subject: Do not print error from within function --- src/lib/core/semaphore.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/core/semaphore.c b/src/lib/core/semaphore.c index 60a928a..2913c9c 100644 --- a/src/lib/core/semaphore.c +++ b/src/lib/core/semaphore.c @@ -12,7 +12,6 @@ int semaphore_init(struct Semaphore *s, const char *name, const int value) { snprintf(s->name, sizeof(s->name), "/%s", name); s->sem = sem_open(s->name, O_CREAT, 0644, value); if (s->sem == SEM_FAILED) { - perror("sem_open"); return -1; } SYSDEBUG("%s", s->name); -- cgit From 76919bc69006fd799fe5c3c979ed4e28e0a09092 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 17 Nov 2025 16:12:35 -0500 Subject: Remove debugging code --- src/lib/core/semaphore.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib/core/semaphore.c b/src/lib/core/semaphore.c index 2913c9c..6a24726 100644 --- a/src/lib/core/semaphore.c +++ b/src/lib/core/semaphore.c @@ -19,15 +19,11 @@ int semaphore_init(struct Semaphore *s, const char *name, const int value) { } int semaphore_wait(struct Semaphore *s) { - int state = 0; - sem_getvalue(s->sem, &state); SYSDEBUG("%s", s->name); return sem_wait(s->sem); } int semaphore_post(struct Semaphore *s) { - int state = 0; - sem_getvalue(s->sem, &state); SYSDEBUG("%s", s->name); return sem_post(s->sem); } -- cgit From 13340a31c0f89508aba655d6f790d0e4cc4a9fef Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 17 Nov 2025 16:15:34 -0500 Subject: Enforce maximum buffer length of `name` --- src/lib/core/semaphore.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib/core/semaphore.c b/src/lib/core/semaphore.c index 6a24726..5c0e904 100644 --- a/src/lib/core/semaphore.c +++ b/src/lib/core/semaphore.c @@ -9,7 +9,14 @@ #include "utils.h" int semaphore_init(struct Semaphore *s, const char *name, const int value) { - snprintf(s->name, sizeof(s->name), "/%s", name); +#if defined(STASIS_OS_DARWIN) + // see: sem_open(2) + const size_t max_namelen = PSEMNAMLEN; +#else + // see: sem_open(3) + const size_t max_namelen = STASIS_NAME_MAX; +#endif + snprintf(s->name, max_namelen, "/%s", name); s->sem = sem_open(s->name, O_CREAT, 0644, value); if (s->sem == SEM_FAILED) { return -1; -- cgit From 3d7e1eab65e815bf72b6101aee59d7b22e17c08b Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 18 Nov 2025 08:45:29 -0500 Subject: Add destroy to example --- src/lib/core/include/sem.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/core/include/sem.h b/src/lib/core/include/sem.h index 947f9e8..63d5ee3 100644 --- a/src/lib/core/include/sem.h +++ b/src/lib/core/include/sem.h @@ -38,6 +38,8 @@ struct Semaphore { * perror("semaphore_post failed"); * exit(1); * } + * + * semaphore_destroy(&s); * } * @endcode * -- cgit From df4c6e0b0df7b9839b11da53f03f69ed6d0c9244 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 18 Nov 2025 08:45:48 -0500 Subject: Remove blank line --- src/lib/core/include/sem.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/core/include/sem.h b/src/lib/core/include/sem.h index 63d5ee3..583770a 100644 --- a/src/lib/core/include/sem.h +++ b/src/lib/core/include/sem.h @@ -52,7 +52,6 @@ struct Semaphore { int semaphore_init(struct Semaphore *s, const char *name, int value); int semaphore_wait(struct Semaphore *s); int semaphore_post(struct Semaphore *s); - void semaphore_destroy(struct Semaphore *s); #endif //STASIS_SEMAPHORE_H \ No newline at end of file -- cgit From 67c290158cdb12b755c17b404f0eb63bc40eac73 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Wed, 24 Dec 2025 10:04:28 -0500 Subject: Fix undefined PSEMNAMLEN on Darwin --- src/lib/core/include/sem.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/core/include/sem.h b/src/lib/core/include/sem.h index 583770a..b8f9a39 100644 --- a/src/lib/core/include/sem.h +++ b/src/lib/core/include/sem.h @@ -6,6 +6,11 @@ #include "core.h" #include +#if defined(STASIS_OS_DARWIN) +// Darwin's sem_open() limits the path length to PSEMNAMLEN +// even though it isn't used directly. +#include // PSEMNAMLEN +#endif struct Semaphore { sem_t *sem; -- cgit From 18b29bd58d1daa1752e981488445e0fcb100f2a7 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 30 Dec 2025 11:28:36 -0500 Subject: Implement task timeout * Add argument: --task-timeout=1[s,m,h] * Timed out tasks are SIGKILL'd * If killing a task fails, the entire program ends --- src/cli/stasis/args.c | 28 ++++++++++++++++++++++++++++ src/cli/stasis/include/args.h | 5 +++++ src/cli/stasis/stasis_main.c | 12 ++++++++++++ src/lib/core/globals.c | 1 + src/lib/core/include/core.h | 1 + src/lib/core/include/multiprocessing.h | 1 + src/lib/core/multiprocessing.c | 3 +++ tests/test_multiprocessing.c | 1 + 8 files changed, 52 insertions(+) diff --git a/src/cli/stasis/args.c b/src/cli/stasis/args.c index fbda494..9410958 100644 --- a/src/cli/stasis/args.c +++ b/src/cli/stasis/args.c @@ -13,6 +13,7 @@ struct option long_options[] = { {"unbuffered", no_argument, 0, 'U'}, {"update-base", no_argument, 0, OPT_ALWAYS_UPDATE_BASE}, {"fail-fast", no_argument, 0, OPT_FAIL_FAST}, + {"task-timeout", required_argument, 0, OPT_TASK_TIMEOUT}, {"overwrite", no_argument, 0, OPT_OVERWRITE}, {"no-docker", no_argument, 0, OPT_NO_DOCKER}, {"no-artifactory", no_argument, 0, OPT_NO_ARTIFACTORY}, @@ -37,6 +38,7 @@ const char *long_options_help[] = { "Disable line buffering", "Update conda installation prior to STASIS environment creation", "On error, immediately terminate all tasks", + "Terminate task after timeout is reached (#s, #m, #h)", "Overwrite an existing release", "Do not build docker images", "Do not upload artifacts to Artifactory", @@ -104,3 +106,29 @@ void usage(char *progname) { puts(output); } } + +int str_to_timeout(char *s) { + if (!s) { + return 0; // no timeout + } + + char *scale = NULL; + int value = (int) strtol(s, &scale, 10); + if (scale) { + if (*scale == 's') { + value *= 1; // seconds, no-op + } else if (*scale == 'm') { + value *= 60; // minutes + } else if (*scale == 'h') { + value *= 3200; // hours + } else { + return STR_TO_TIMEOUT_INVALID_TIME_SCALE; // invalid time scale + } + } + + if (value < 0) { + return STR_TO_TIMEOUT_NEGATIVE; // cannot be negative + } + return value; +} + diff --git a/src/cli/stasis/include/args.h b/src/cli/stasis/include/args.h index 850be4a..d75fe29 100644 --- a/src/cli/stasis/include/args.h +++ b/src/cli/stasis/include/args.h @@ -18,8 +18,13 @@ #define OPT_NO_PARALLEL 1010 #define OPT_POOL_STATUS_INTERVAL 1011 #define OPT_NO_TASK_LOGGING 1012 +#define OPT_TASK_TIMEOUT 1013 extern struct option long_options[]; void usage(char *progname); +#define STR_TO_TIMEOUT_NEGATIVE (-1) +#define STR_TO_TIMEOUT_INVALID_TIME_SCALE (-2) +int str_to_timeout(char *s); + #endif //STASIS_ARGS_H diff --git a/src/cli/stasis/stasis_main.c b/src/cli/stasis/stasis_main.c index 967ecaf..44ee6d7 100644 --- a/src/cli/stasis/stasis_main.c +++ b/src/cli/stasis/stasis_main.c @@ -540,6 +540,18 @@ int main(int argc, char *argv[]) { case OPT_FAIL_FAST: globals.parallel_fail_fast = true; break; + case OPT_TASK_TIMEOUT: + globals.task_timeout = str_to_timeout(optarg); + if (globals.task_timeout < 0) { + fprintf(stderr, "Invalid timeout: %s\n", optarg); + if (globals.task_timeout == STR_TO_TIMEOUT_INVALID_TIME_SCALE) { + fprintf(stderr, "Use format '#s' (seconds), '#m' (minutes), '#h' (hours)\n"); + } else if (globals.task_timeout == STR_TO_TIMEOUT_NEGATIVE) { + fprintf(stderr, "Timeout cannot be negative\n"); + } + exit(1); + } + break; case OPT_POOL_STATUS_INTERVAL: globals.pool_status_interval = (int) strtol(optarg, NULL, 10); if (globals.pool_status_interval < 1) { diff --git a/src/lib/core/globals.c b/src/lib/core/globals.c index a262d6c..834213b 100644 --- a/src/lib/core/globals.c +++ b/src/lib/core/globals.c @@ -44,6 +44,7 @@ struct STASIS_GLOBAL globals = { .enable_task_logging = true, ///< Toggle logging for multiprocess tasks .parallel_fail_fast = false, ///< Kill ALL multiprocessing tasks immediately on error .pool_status_interval = 30, ///< Report "Task is running" + .task_timeout = 0, ///< Time in seconds before task is terminated }; void globals_free() { diff --git a/src/lib/core/include/core.h b/src/lib/core/include/core.h index e96e010..5a3fa85 100644 --- a/src/lib/core/include/core.h +++ b/src/lib/core/include/core.h @@ -51,6 +51,7 @@ struct STASIS_GLOBAL { char *tmpdir; //!< Path to temporary storage directory char *conda_install_prefix; //!< Path to install conda char *sysconfdir; //!< Path where STASIS reads its configuration files (mission directory, etc) + int task_timeout; ///< Time in seconds before task is terminated struct { char *tox_posargs; char *conda_reactivate; diff --git a/src/lib/core/include/multiprocessing.h b/src/lib/core/include/multiprocessing.h index 6477818..ab7b416 100644 --- a/src/lib/core/include/multiprocessing.h +++ b/src/lib/core/include/multiprocessing.h @@ -15,6 +15,7 @@ struct MultiProcessingTask { pid_t parent_pid; ///< Program PID (parent process) int status; ///< Child process exit status int signaled_by; ///< Last signal received, if any + int timeout; ///< Seconds to elapse before killing the process time_t _now; ///< Current time time_t _seconds; ///< Time elapsed since status interval (used by MultiprocessingPool.status_interval) time_t _startup; ///< Time elapsed since task started diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index 69719e8..ff4453c 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -167,6 +167,9 @@ struct MultiProcessingTask *mp_pool_task(struct MultiProcessingPool *pool, const memset(slot->cmd, 0, slot->cmd_len); strncpy(slot->cmd, cmd, slot->cmd_len); + // Set task timeout + slot->timeout = globals.task_timeout; + return slot; } diff --git a/tests/test_multiprocessing.c b/tests/test_multiprocessing.c index 7c9d695..b10f530 100644 --- a/tests/test_multiprocessing.c +++ b/tests/test_multiprocessing.c @@ -214,6 +214,7 @@ int main(int argc, char *argv[]) { test_mp_fail_fast, test_mp_stop_continue }; + globals.task_timeout = 60; STASIS_TEST_RUN(tests); STASIS_TEST_END_MAIN(); } -- cgit From 059519c39bb7d452d651e7dc440594251966c88b Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Tue, 30 Dec 2025 22:38:24 -0500 Subject: Fix broken header guard --- src/lib/core/include/copy.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/core/include/copy.h b/src/lib/core/include/copy.h index 0f92ddd..1eb5219 100644 --- a/src/lib/core/include/copy.h +++ b/src/lib/core/include/copy.h @@ -1,5 +1,6 @@ //! @file copy.h #ifndef STASIS_COPY_H +#define STASIS_COPY_H #include #include -- cgit From 1d071010c2bec860e62371cfb70f2e12a7d00563 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Wed, 31 Dec 2025 11:51:35 -0500 Subject: Update multiprocessing to use semaphore * Introduce a small public domain timespec handling library * Renamed MultiProcessingTask members _now and _seconds to _interval_start and _interval_stop * Added interval_elapsed member * Change elapsed member from type int to double * mp_pool_free no longer tries to free semaphores with empty names * semaphore_init uses the correct default value of 1 instead of 2. The bug was related to calling semaphore_wait before the child() process started and semaphore_post after the parent exited. Now we post before the child to prevent a deadlock * Replace sleep with usleep in mp_pool_join. Set to 100ms. * Replace time() with clock_gettime() (helper functions created to prevent mistakes) * Stop recording time when the process ends. This fixes a bug where a process ends and up to 3 different elapsed times were reported to the user. * Progress output is now always available, not only when pid > 0 * Implement seconds_to_human_readable and hook it up to progress reporting calls. Breaking down thousands of seconds in my head after a long run was mentally exhausting. * Cleaned up some if-statements; removed else-if for clarity * Implemented a global timeout for pool tasks * Add register_semaphore() and semaphore_handle_exit() to aid with clean up. On Darwin a dangling shared memory file will lead to unexpected failures. These are destroyed via atexit(). --- src/lib/core/CMakeLists.txt | 1 + src/lib/core/include/multiprocessing.h | 9 +- src/lib/core/include/timespec.h | 71 +++ src/lib/core/multiprocessing.c | 207 ++++--- src/lib/core/semaphore.c | 35 +- src/lib/core/timespec.c | 979 +++++++++++++++++++++++++++++++++ 6 files changed, 1233 insertions(+), 69 deletions(-) create mode 100644 src/lib/core/include/timespec.h create mode 100644 src/lib/core/timespec.c diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt index 7b33cf9..eb7a908 100644 --- a/src/lib/core/CMakeLists.txt +++ b/src/lib/core/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(stasis_core STATIC globals.c + timespec.c str.c strlist.c ini.c diff --git a/src/lib/core/include/multiprocessing.h b/src/lib/core/include/multiprocessing.h index ab7b416..933f7d6 100644 --- a/src/lib/core/include/multiprocessing.h +++ b/src/lib/core/include/multiprocessing.h @@ -4,11 +4,13 @@ #include "core.h" #include "sem.h" +#include "timespec.h" #include #include #include #include #include +#include struct MultiProcessingTask { pid_t pid; ///< Program PID @@ -16,10 +18,11 @@ struct MultiProcessingTask { int status; ///< Child process exit status int signaled_by; ///< Last signal received, if any int timeout; ///< Seconds to elapse before killing the process - time_t _now; ///< Current time - time_t _seconds; ///< Time elapsed since status interval (used by MultiprocessingPool.status_interval) + struct timespec _interval_start; ///< Current time, start of interval + struct timespec _interval_stop; ///< Time elapsed since status interval (used by MultiprocessingPool.status_interval) + double interval_elapsed; time_t _startup; ///< Time elapsed since task started - long elapsed; ///< Total time elapsed in seconds + double elapsed; ///< Total time elapsed in seconds char ident[255]; ///< Identity of the pool task char *cmd; ///< Shell command(s) to be executed size_t cmd_len; ///< Length of command string (for mmap/munmap) diff --git a/src/lib/core/include/timespec.h b/src/lib/core/include/timespec.h new file mode 100644 index 0000000..3f4b9a7 --- /dev/null +++ b/src/lib/core/include/timespec.h @@ -0,0 +1,71 @@ +/* Functions for working with timespec structures + * Written by Daniel Collins (2017-2021) + * timespec_mod by Alex Forencich (2019) + * Various contributions by Ingo Albrecht (2021) + * + * This is free and unencumbered software released into the public domain. + * + * Anyone is free to copy, modify, publish, use, compile, sell, or + * distribute this software, either in source code form or as a compiled + * binary, for any purpose, commercial or non-commercial, and by any + * means. + * + * In jurisdictions that recognize copyright laws, the author or authors + * of this software dedicate any and all copyright interest in the + * software to the public domain. We make this dedication for the benefit + * of the public at large and to the detriment of our heirs and + * successors. We intend this dedication to be an overt act of + * relinquishment in perpetuity of all present and future rights to this + * software under copyright law. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * For more information, please refer to +*/ + +#ifndef DAN_TIMESPEC_H +#define DAN_TIMESPEC_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct timespec timespec_add(struct timespec ts1, struct timespec ts2); +struct timespec timespec_sub(struct timespec ts1, struct timespec ts2); +struct timespec timespec_mod(struct timespec ts1, struct timespec ts2); + +struct timespec timespec_min(struct timespec ts1, struct timespec ts2); +struct timespec timespec_max(struct timespec ts1, struct timespec ts2); +struct timespec timespec_clamp(struct timespec ts1, struct timespec min, struct timespec max); + +int timespec_cmp(struct timespec ts1, struct timespec ts2); +bool timespec_eq(struct timespec ts1, struct timespec ts2); +bool timespec_gt(struct timespec ts1, struct timespec ts2); +bool timespec_ge(struct timespec ts1, struct timespec ts2); +bool timespec_lt(struct timespec ts1, struct timespec ts2); +bool timespec_le(struct timespec ts1, struct timespec ts2); + +struct timespec timespec_from_double(double s); +double timespec_to_double(struct timespec ts); +struct timespec timespec_from_timeval(struct timeval tv); +struct timeval timespec_to_timeval(struct timespec ts); +struct timespec timespec_from_ms(long milliseconds); +long timespec_to_ms(struct timespec ts); + +struct timespec timespec_normalise(struct timespec ts); + +#ifdef __cplusplus +} +#endif + +#endif /* !DAN_TIMESPEC_H */ diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index ff4453c..43f3e07 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -4,6 +4,74 @@ /// The sum of all tasks started by mp_task() size_t mp_global_task_count = 0; +static char *seconds_to_human_readable(const int v) { + static char result[255] = {0}; + const int hours = v / 3600; + const int minutes = (v % 3600) / 60; + const int seconds = v % 60; + + memset(result, '\0', sizeof(result)); + if (hours) { + snprintf(result + strlen(result), sizeof(result), "%dh ", hours); + } + if (minutes) { + snprintf(result + strlen(result), sizeof(result), "%dm ", minutes); + } + snprintf(result + strlen(result), sizeof(result), "%ds", seconds); + + return result; +} + +static double get_duration(const struct timespec stop, const struct timespec start) { + const struct timespec result = timespec_sub(stop, start); + return timespec_to_double(result); +} + +static double get_task_duration(const struct MultiProcessingTask *task) { + const struct timespec *start = &task->time_data.t_start; + const struct timespec *stop = &task->time_data.t_stop; + return get_duration(*stop, *start); +} + +static double get_task_interval_duration(const struct MultiProcessingTask *task) { + const struct timespec *start = &task->_interval_start; + const struct timespec *stop = &task->_interval_stop; + return get_duration(*stop, *start); +} + +static void update_task_interval_start(struct MultiProcessingTask *task) { + // Record the task stop time + if (clock_gettime(CLOCK_REALTIME, &task->_interval_start) < 0) { + perror("clock_gettime"); + exit(1); + } +} + +static void update_task_interval_elapsed(struct MultiProcessingTask *task) { + // Record the interval stop time + if (clock_gettime(CLOCK_REALTIME, &task->_interval_stop) < 0) { + perror("clock_gettime"); + exit(1); + } + task->interval_elapsed = get_task_interval_duration(task); +} + +static void update_task_start(struct MultiProcessingTask *task) { + // Record the task start time + if (clock_gettime(CLOCK_REALTIME, &task->time_data.t_start) < 0) { + perror("clock_gettime"); + exit(1); + } +} +static void update_task_elapsed(struct MultiProcessingTask *task) { + // Record the task stop time + if (clock_gettime(CLOCK_REALTIME, &task->time_data.t_stop) < 0) { + perror("clock_gettime"); + exit(1); + } + task->elapsed = get_task_duration(task); +} + static struct MultiProcessingTask *mp_pool_next_available(struct MultiProcessingPool *pool) { return &pool->task[pool->num_used]; } @@ -18,12 +86,6 @@ int child(struct MultiProcessingPool *pool, struct MultiProcessingTask *task) { exit(1); } - // Record the task start time - if (clock_gettime(CLOCK_REALTIME, &task->time_data.t_start) < 0) { - perror("clock_gettime"); - exit(1); - } - // Redirect stdout and stderr to the log file fflush(stdout); fflush(stderr); @@ -64,13 +126,18 @@ int child(struct MultiProcessingPool *pool, struct MultiProcessingTask *task) { } int parent(struct MultiProcessingPool *pool, struct MultiProcessingTask *task, pid_t pid, int *child_status) { + // Record the task start time + update_task_start(task); + printf("[%s:%s] Task started (pid: %d)\n", pool->ident, task->ident, pid); // Give the child process access to our PID value task->pid = pid; task->parent_pid = pid; + semaphore_wait(&pool->semaphore); mp_global_task_count++; + semaphore_post(&pool->semaphore); // Check child's status pid_t code = waitpid(pid, child_status, WUNTRACED | WCONTINUED | WNOHANG); @@ -91,13 +158,13 @@ static int mp_task_fork(struct MultiProcessingPool *pool, struct MultiProcessing return -1; } if (pid == 0) { + semaphore_post(&pool->semaphore); child(pool, task); } else { parent_status = parent(pool, task, pid, &child_status); fflush(stdout); fflush(stderr); } - semaphore_post(&pool->semaphore); return parent_status; } @@ -173,24 +240,11 @@ struct MultiProcessingTask *mp_pool_task(struct MultiProcessingPool *pool, const return slot; } -static void get_task_duration(struct MultiProcessingTask *task, struct timespec *result) { - // based on the timersub() macro in time.h - // This implementation uses timespec and increases the resolution from microseconds to nanoseconds. - struct timespec *start = &task->time_data.t_start; - struct timespec *stop = &task->time_data.t_stop; - result->tv_sec = (stop->tv_sec - start->tv_sec); - result->tv_nsec = (stop->tv_nsec - start->tv_nsec); - if (result->tv_nsec < 0) { - --result->tv_sec; - result->tv_nsec += 1000000000L; - } -} - void mp_pool_show_summary(struct MultiProcessingPool *pool) { print_banner("=", 79); printf("Pool execution summary for \"%s\"\n", pool->ident); print_banner("=", 79); - printf("STATUS PID DURATION IDENT\n"); + printf("STATUS PID DURATION IDENT\n"); for (size_t i = 0; i < pool->num_used; i++) { struct MultiProcessingTask *task = &pool->task[i]; char status_str[10] = {0}; @@ -208,10 +262,8 @@ void mp_pool_show_summary(struct MultiProcessingPool *pool) { strcpy(status_str, "FAIL"); } - struct timespec duration; - get_task_duration(task, &duration); - long diff = duration.tv_sec + duration.tv_nsec / 1000000000L; - printf("%-4s %10d %7lds %-10s\n", status_str, task->parent_pid, diff, task->ident) ; + printf("%-4s %10d %10s %-10s\n", status_str, task->parent_pid, seconds_to_human_readable(task->elapsed), task->ident) ; + //printf("%-4s %10d %7lds %-10s\n", status_str, task->parent_pid, task->elapsed, task->ident) ; } puts(""); } @@ -243,18 +295,18 @@ int mp_pool_kill(struct MultiProcessingPool *pool, int signum) { if (slot->pid > 0) { int status; printf("Sending signal %d to task '%s' (pid: %d)\n", signum, slot->ident, slot->pid); + semaphore_wait(&pool->semaphore); status = kill(slot->pid, signum); + semaphore_post(&pool->semaphore); if (status && errno != ESRCH) { fprintf(stderr, "Task '%s' (pid: %d) did not respond: %s\n", slot->ident, slot->pid, strerror(errno)); } else { // Wait for process to handle the signal, then set the status accordingly if (waitpid(slot->pid, &status, 0) >= 0) { slot->signaled_by = WTERMSIG(status); - // Record the task stop time - if (clock_gettime(CLOCK_REALTIME, &slot->time_data.t_stop) < 0) { - perror("clock_gettime"); - exit(1); - } + semaphore_wait(&pool->semaphore); + update_task_elapsed(slot); + semaphore_post(&pool->semaphore); // We are short-circuiting the normal flow, and the process is now dead, so mark it as such SYSDEBUG("Marking slot %zu: UNUSED", i); slot->pid = MP_POOL_PID_UNUSED; @@ -262,15 +314,20 @@ int mp_pool_kill(struct MultiProcessingPool *pool, int signum) { } } if (globals.enable_task_logging) { + semaphore_wait(&pool->semaphore); if (!access(slot->log_file, F_OK)) { SYSDEBUG("Removing log file: %s", slot->log_file); remove(slot->log_file); } + semaphore_post(&pool->semaphore); } + + semaphore_wait(&pool->semaphore); if (!access(slot->parent_script, F_OK)) { SYSDEBUG("Removing runner script: %s", slot->parent_script); remove(slot->parent_script); } + semaphore_post(&pool->semaphore); } return 0; } @@ -317,32 +374,49 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { // Is the process finished? pid_t pid = waitpid(slot->pid, &status, WNOHANG | WUNTRACED | WCONTINUED); - int task_ended = WIFEXITED(status); - int task_ended_by_signal = WIFSIGNALED(status); - int task_stopped = WIFSTOPPED(status); - int task_continued = WIFCONTINUED(status); - int status_exit = WEXITSTATUS(status); - int status_signal = WTERMSIG(status); - int status_stopped = WSTOPSIG(status); + + char progress[1024] = {0}; + const double percent = ((double) (tasks_complete + 1) / (double) pool->num_used) * 100; + snprintf(progress, sizeof(progress) - 1, "[%s:%s] [%3.1f%%]", pool->ident, slot->ident, percent); + + int task_timed_out = false; + if (slot->timeout) { + task_timed_out = slot->elapsed >= (double) slot->timeout; + if (task_timed_out && pid == 0 && slot->pid != 0) { + printf("%s Task timed out after %s (pid: %d)\n", progress, seconds_to_human_readable(slot->timeout), slot->pid); + if (kill(slot->pid, SIGKILL) == 0) { + status = SIGKILL; + } else { + SYSERROR("Timeout reached, however pid %d could not be killed.", slot->pid); + return -1; + } + } + } + + const int task_ended = WIFEXITED(status); + const int task_ended_by_signal = WIFSIGNALED(status); + const int task_stopped = WIFSTOPPED(status); + const int task_continued = WIFCONTINUED(status); + const int status_exit = WEXITSTATUS(status); + const int status_signal = WTERMSIG(status); + const int status_stopped = WSTOPSIG(status); // Update status slot->status = status_exit; slot->signaled_by = status_signal; - char progress[1024] = {0}; if (pid > 0) { - double percent = ((double) (tasks_complete + 1) / (double) pool->num_used) * 100; - snprintf(progress, sizeof(progress) - 1, "[%s:%s] [%3.1f%%]", pool->ident, slot->ident, percent); - // The process ended in one the following ways // Note: SIGSTOP nor SIGCONT will not increment the tasks_complete counter if (task_stopped) { printf("%s Task was suspended (%d)\n", progress, status_stopped); continue; - } else if (task_continued) { + } + if (task_continued) { printf("%s Task was resumed\n", progress); continue; - } else if (task_ended_by_signal) { + } + if (task_ended_by_signal) { printf("%s Task ended by signal %d (%s)\n", progress, status_signal, strsignal(status_signal)); tasks_complete++; } else if (task_ended) { @@ -359,14 +433,11 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { } } - // Record the task stop time - if (clock_gettime(CLOCK_REALTIME, &slot->time_data.t_stop) < 0) { - perror("clock_gettime"); - exit(1); - } - if (status >> 8 != 0 || (status & 0xff) != 0) { - fprintf(stderr, "%s Task failed after %lus\n", progress, slot->elapsed); + semaphore_wait(&pool->semaphore); + update_task_elapsed(slot); + semaphore_post(&pool->semaphore); + fprintf(stderr, "%s Task failed after %s\n", progress, seconds_to_human_readable(slot->elapsed)); failures++; if (flags & MP_POOL_FAIL_FAST && pool->num_used > 1) { @@ -374,7 +445,7 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { return -2; } } else { - printf("%s Task finished after %lus\n", progress, slot->elapsed); + printf("%s Task finished after %s\n", progress, seconds_to_human_readable(slot->elapsed)); } // Clean up logs and scripts left behind by the task @@ -395,17 +466,25 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { } else { // Track the number of seconds elapsed for each task. // When a task has executed for longer than status_intervals, print a status update - // _seconds represents the time between intervals, not the total runtime of the task - slot->_seconds = time(NULL) - slot->_now; - if (slot->_seconds > pool->status_interval) { - slot->_now = time(NULL); - slot->_seconds = 0; + // interval_elapsed represents the time between intervals, not the total runtime of the task + semaphore_wait(&pool->semaphore); + if (fabs(slot->interval_elapsed) > pool->status_interval) { + slot->interval_elapsed = 0.0; } - if (slot->_seconds == 0) { - printf("[%s:%s] Task is running (pid: %d, elapsed: %lus)\n", pool->ident, slot->ident, slot->parent_pid, slot->elapsed); + if (slot->interval_elapsed == 0.0) { + printf("[%s:%s] Task is running (pid: %d, elapsed: %s)\n", pool->ident, slot->ident, slot->parent_pid, seconds_to_human_readable(slot->elapsed)); + update_task_interval_start(slot); } + + update_task_interval_elapsed(slot); + semaphore_post(&pool->semaphore); + } + + if (!task_ended || !task_ended_by_signal) { + semaphore_wait(&pool->semaphore); + update_task_elapsed(slot); + semaphore_post(&pool->semaphore); } - slot->elapsed = time(NULL) - slot->_startup; } if (tasks_complete == pool->num_used) { @@ -418,7 +497,7 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { } // Poll again after a short delay - sleep(1); + usleep(100000); } while (1); pool_deadlocked: @@ -469,19 +548,21 @@ struct MultiProcessingPool *mp_pool_init(const char *ident, const char *log_root char semaphore_name[255] = {0}; snprintf(semaphore_name, sizeof(semaphore_name), "stasis_mp_%s", ident); - if (semaphore_init(&pool->semaphore, semaphore_name, 2) != 0) { + if (semaphore_init(&pool->semaphore, semaphore_name, 1) != 0) { fprintf(stderr, "unable to initialize semaphore\n"); mp_pool_free(&pool); return NULL; } + pool->status_interval = 3; + return pool; } void mp_pool_free(struct MultiProcessingPool **pool) { - for (size_t i = 0; i < (*pool)->num_alloc; i++) { + if (!isempty((*pool)->semaphore.name)) { + semaphore_destroy(&(*pool)->semaphore); } - semaphore_destroy(&(*pool)->semaphore); // Unmap all pool tasks if ((*pool)->task) { diff --git a/src/lib/core/semaphore.c b/src/lib/core/semaphore.c index 5c0e904..579479a 100644 --- a/src/lib/core/semaphore.c +++ b/src/lib/core/semaphore.c @@ -8,6 +8,28 @@ #include "sem.h" #include "utils.h" +struct Semaphore *semaphores[1000] = {0}; +bool semaphore_handle_exit_ready = false; + +void semaphore_handle_exit() { + for (size_t i = 0; i < sizeof(semaphores) / sizeof(*semaphores); ++i) { + if (semaphores[i]) { + SYSDEBUG("%s", semaphores[i]->name); + semaphore_destroy(semaphores[i]); + } + } +} + +static void register_semaphore(struct Semaphore *s) { + struct Semaphore **cur = semaphores; + size_t i = 0; + while (i < sizeof(semaphores) / sizeof(*semaphores) && cur != NULL) { + cur++; + i++; + } + cur = &s; +} + int semaphore_init(struct Semaphore *s, const char *name, const int value) { #if defined(STASIS_OS_DARWIN) // see: sem_open(2) @@ -22,21 +44,28 @@ int semaphore_init(struct Semaphore *s, const char *name, const int value) { return -1; } SYSDEBUG("%s", s->name); + register_semaphore(s); + if (!semaphore_handle_exit_ready) { + atexit(semaphore_handle_exit); + } + return 0; } int semaphore_wait(struct Semaphore *s) { - SYSDEBUG("%s", s->name); return sem_wait(s->sem); } int semaphore_post(struct Semaphore *s) { - SYSDEBUG("%s", s->name); return sem_post(s->sem); } void semaphore_destroy(struct Semaphore *s) { + if (!s) { + SYSDEBUG("%s", "would have crashed"); + return; + } SYSDEBUG("%s", s->name); sem_close(s->sem); sem_unlink(s->name); -} +} \ No newline at end of file diff --git a/src/lib/core/timespec.c b/src/lib/core/timespec.c new file mode 100644 index 0000000..bd33993 --- /dev/null +++ b/src/lib/core/timespec.c @@ -0,0 +1,979 @@ +/* Functions for working with timespec structures + * Written by Daniel Collins (2017-2021) + * timespec_mod by Alex Forencich (2019) + * Various contributions by Ingo Albrecht (2021) + * + * This is free and unencumbered software released into the public domain. + * + * Anyone is free to copy, modify, publish, use, compile, sell, or + * distribute this software, either in source code form or as a compiled + * binary, for any purpose, commercial or non-commercial, and by any + * means. + * + * In jurisdictions that recognize copyright laws, the author or authors + * of this software dedicate any and all copyright interest in the + * software to the public domain. We make this dedication for the benefit + * of the public at large and to the detriment of our heirs and + * successors. We intend this dedication to be an overt act of + * relinquishment in perpetuity of all present and future rights to this + * software under copyright law. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * For more information, please refer to +*/ + +/** \file timespec.c + * \brief Functions for working with timespec structures. + * + * This library aims to provide a comprehensive set of functions with + * well-defined behaviour that handle all edge cases (e.g. negative values) in + * a sensible manner. + * + * Negative values are allowed in the tv_sec and/or tv_usec field of timespec + * structures, tv_usec is always relative to tv_sec, so mixing positive and + * negative values will produce consistent results: + * + *
+ * { tv_sec = 1,  tv_nsec = 500000000  } ==  1.5 seconds
+ * { tv_sec = 1,  tv_nsec = 0          } ==  1.0 seconds
+ * { tv_sec = 1,  tv_nsec = -500000000 } ==  0.5 seconds
+ * { tv_sec = 0,  tv_nsec = 500000000  } ==  0.5 seconds
+ * { tv_sec = 0,  tv_nsec = 0          } ==  0.0 seconds
+ * { tv_sec = 0,  tv_nsec = -500000000 } == -0.5 seconds
+ * { tv_sec = -1, tv_nsec = 500000000  } == -0.5 seconds
+ * { tv_sec = -1, tv_nsec = 0          } == -1.0 seconds
+ * { tv_sec = -1, tv_nsec = -500000000 } == -1.5 seconds
+ * 
+ * + * Furthermore, any timespec structure processed or returned by library functions + * is normalised according to the rules in timespec_normalise(). +*/ + +#include +#include +#include +#include + +#include "timespec.h" + +#define NSEC_PER_SEC 1000000000 + +/** \fn struct timespec timespec_add(struct timespec ts1, struct timespec ts2) + * \brief Returns the result of adding two timespec structures. +*/ +struct timespec timespec_add(struct timespec ts1, struct timespec ts2) +{ + /* Normalise inputs to prevent tv_nsec rollover if whole-second values + * are packed in it. + */ + ts1 = timespec_normalise(ts1); + ts2 = timespec_normalise(ts2); + + ts1.tv_sec += ts2.tv_sec; + ts1.tv_nsec += ts2.tv_nsec; + + return timespec_normalise(ts1); +} + +/** \fn struct timespec timespec_sub(struct timespec ts1, struct timespec ts2) + * \brief Returns the result of subtracting ts2 from ts1. +*/ +struct timespec timespec_sub(struct timespec ts1, struct timespec ts2) +{ + /* Normalise inputs to prevent tv_nsec rollover if whole-second values + * are packed in it. + */ + ts1 = timespec_normalise(ts1); + ts2 = timespec_normalise(ts2); + + ts1.tv_sec -= ts2.tv_sec; + ts1.tv_nsec -= ts2.tv_nsec; + + return timespec_normalise(ts1); +} + +/** \fn struct timespec timespec_mod(struct timespec ts1, struct timespec ts2) + * \brief Returns the remainder left over after dividing ts1 by ts2 (ts1%ts2). +*/ +struct timespec timespec_mod(struct timespec ts1, struct timespec ts2) +{ + int i = 0; + bool neg1 = false; + bool neg2 = false; + + /* Normalise inputs to prevent tv_nsec rollover if whole-second values + * are packed in it. + */ + ts1 = timespec_normalise(ts1); + ts2 = timespec_normalise(ts2); + + /* If ts2 is zero, just return ts1 + */ + if (ts2.tv_sec == 0 && ts2.tv_nsec == 0) + { + return ts1; + } + + /* If inputs are negative, flip and record sign + */ + if (ts1.tv_sec < 0 || ts1.tv_nsec < 0) + { + neg1 = true; + ts1.tv_sec = -ts1.tv_sec; + ts1.tv_nsec = -ts1.tv_nsec; + } + + if (ts2.tv_sec < 0 || ts2.tv_nsec < 0) + { + neg2 = true; + ts2.tv_sec = -ts2.tv_sec; + ts2.tv_nsec = -ts2.tv_nsec; + } + + /* Shift ts2 until it is larger than ts1 or is about to overflow + */ + while ((ts2.tv_sec < (LONG_MAX >> 1)) && timespec_ge(ts1, ts2)) + { + i++; + ts2.tv_nsec <<= 1; + ts2.tv_sec <<= 1; + if (ts2.tv_nsec > NSEC_PER_SEC) + { + ts2.tv_nsec -= NSEC_PER_SEC; + ts2.tv_sec++; + } + } + + /* Division by repeated subtraction + */ + while (i >= 0) + { + if (timespec_ge(ts1, ts2)) + { + ts1 = timespec_sub(ts1, ts2); + } + + if (i == 0) + { + break; + } + + i--; + if (ts2.tv_sec & 1) + { + ts2.tv_nsec += NSEC_PER_SEC; + } + ts2.tv_nsec >>= 1; + ts2.tv_sec >>= 1; + } + + /* If signs differ and result is nonzero, subtract once more to cross zero + */ + if (neg1 ^ neg2 && (ts1.tv_sec != 0 || ts1.tv_nsec != 0)) + { + ts1 = timespec_sub(ts1, ts2); + } + + /* Restore sign + */ + if (neg1) + { + ts1.tv_sec = -ts1.tv_sec; + ts1.tv_nsec = -ts1.tv_nsec; + } + + return ts1; +} + +/** \fn struct timespec timespec_min(struct timespec ts1, struct timespec ts2) + * \brief Return the lesser one of the two given timespec values. +*/ +struct timespec timespec_min(struct timespec ts1, struct timespec ts2) { + if(timespec_le(ts1, ts2)) { + return ts1; + } else { + return ts2; + } +} + +/** \fn struct timespec timespec_max(struct timespec ts1, struct timespec ts2) + * \brief Return the greater one of the two given timespec values. +*/ +struct timespec timespec_max(struct timespec ts1, struct timespec ts2) { + if(timespec_ge(ts1, ts2)) { + return ts1; + } else { + return ts2; + } +} + +/** \fn struct timespec timespec_clamp(struct timespec ts, struct timespec min, struct timespec max) + * \brief Clamp the value of TS between MIN and MAX. +*/ +struct timespec timespec_clamp(struct timespec ts, struct timespec min, struct timespec max) { + if(timespec_gt(ts, max)) { + return max; + } + if(timespec_lt(ts, min)) { + return min; + } + return ts; +} + +/** \fn int timespec_cmp(struct timespec ts1, struct timespec ts2) + * \brief Returns (1, 0, -1) if ts1 is (greater than, equal to, less than) to ts2. +*/ +int timespec_cmp(struct timespec ts1, struct timespec ts2) +{ + ts1 = timespec_normalise(ts1); + ts2 = timespec_normalise(ts2); + + if(ts1.tv_sec == ts2.tv_sec && ts1.tv_nsec == ts2.tv_nsec) + { + return 0; + } + else if((ts1.tv_sec > ts2.tv_sec) + || (ts1.tv_sec == ts2.tv_sec && ts1.tv_nsec > ts2.tv_nsec)) + { + return 1; + } + else { + return -1; + } +} + +/** \fn bool timespec_eq(struct timespec ts1, struct timespec ts2) + * \brief Returns true if the two timespec structures are equal. +*/ +bool timespec_eq(struct timespec ts1, struct timespec ts2) +{ + ts1 = timespec_normalise(ts1); + ts2 = timespec_normalise(ts2); + + return (ts1.tv_sec == ts2.tv_sec && ts1.tv_nsec == ts2.tv_nsec); +} + +/** \fn bool timespec_gt(struct timespec ts1, struct timespec ts2) + * \brief Returns true if ts1 is greater than ts2. +*/ +bool timespec_gt(struct timespec ts1, struct timespec ts2) +{ + ts1 = timespec_normalise(ts1); + ts2 = timespec_normalise(ts2); + + return (ts1.tv_sec > ts2.tv_sec || (ts1.tv_sec == ts2.tv_sec && ts1.tv_nsec > ts2.tv_nsec)); +} + +/** \fn bool timespec_ge(struct timespec ts1, struct timespec ts2) + * \brief Returns true if ts1 is greater than or equal to ts2. +*/ +bool timespec_ge(struct timespec ts1, struct timespec ts2) +{ + ts1 = timespec_normalise(ts1); + ts2 = timespec_normalise(ts2); + + return (ts1.tv_sec > ts2.tv_sec || (ts1.tv_sec == ts2.tv_sec && ts1.tv_nsec >= ts2.tv_nsec)); +} + +/** \fn bool timespec_lt(struct timespec ts1, struct timespec ts2) + * \brief Returns true if ts1 is less than ts2. +*/ +bool timespec_lt(struct timespec ts1, struct timespec ts2) +{ + ts1 = timespec_normalise(ts1); + ts2 = timespec_normalise(ts2); + + return (ts1.tv_sec < ts2.tv_sec || (ts1.tv_sec == ts2.tv_sec && ts1.tv_nsec < ts2.tv_nsec)); +} + +/** \fn bool timespec_le(struct timespec ts1, struct timespec ts2) + * \brief Returns true if ts1 is less than or equal to ts2. +*/ +bool timespec_le(struct timespec ts1, struct timespec ts2) +{ + ts1 = timespec_normalise(ts1); + ts2 = timespec_normalise(ts2); + + return (ts1.tv_sec < ts2.tv_sec || (ts1.tv_sec == ts2.tv_sec && ts1.tv_nsec <= ts2.tv_nsec)); +} + +/** \fn struct timespec timespec_from_double(double s) + * \brief Converts a fractional number of seconds to a timespec. +*/ +struct timespec timespec_from_double(double s) +{ + struct timespec ts = { + .tv_sec = s, + .tv_nsec = (s - (long)(s)) * NSEC_PER_SEC, + }; + + return timespec_normalise(ts); +} + +/** \fn double timespec_to_double(struct timespec ts) + * \brief Converts a timespec to a fractional number of seconds. +*/ +double timespec_to_double(struct timespec ts) +{ + return ((double)(ts.tv_sec) + ((double)(ts.tv_nsec) / NSEC_PER_SEC)); +} + +/** \fn struct timespec timespec_from_timeval(struct timeval tv) + * \brief Converts a timeval to a timespec. +*/ +struct timespec timespec_from_timeval(struct timeval tv) +{ + struct timespec ts = { + .tv_sec = tv.tv_sec, + .tv_nsec = tv.tv_usec * 1000 + }; + + return timespec_normalise(ts); +} + +/** \fn struct timeval timespec_to_timeval(struct timespec ts) + * \brief Converts a timespec to a timeval. +*/ +struct timeval timespec_to_timeval(struct timespec ts) +{ + ts = timespec_normalise(ts); + + struct timeval tv = { + .tv_sec = ts.tv_sec, + .tv_usec = ts.tv_nsec / 1000, + }; + + return tv; +} + +/** \fn struct timespec timespec_from_ms(long milliseconds) + * \brief Converts an integer number of milliseconds to a timespec. +*/ +struct timespec timespec_from_ms(long milliseconds) +{ + struct timespec ts = { + .tv_sec = (milliseconds / 1000), + .tv_nsec = (milliseconds % 1000) * 1000000, + }; + + return timespec_normalise(ts); +} + +/** \fn long timespec_to_ms(struct timespec ts) + * \brief Converts a timespec to an integer number of milliseconds. +*/ +long timespec_to_ms(struct timespec ts) +{ + return (ts.tv_sec * 1000) + (ts.tv_nsec / 1000000); +} + +/** \fn struct timespec timespec_normalise(struct timespec ts) + * \brief Normalises a timespec structure. + * + * Returns a normalised version of a timespec structure, according to the + * following rules: + * + * 1) If tv_nsec is >=1,000,000,00 or <=-1,000,000,000, flatten the surplus + * nanoseconds into the tv_sec field. + * + * 2) If tv_nsec is negative, decrement tv_sec and roll tv_nsec up to represent + * the same value attainable by ADDING nanoseconds to tv_sec. +*/ +struct timespec timespec_normalise(struct timespec ts) +{ + while(ts.tv_nsec >= NSEC_PER_SEC) + { + ++(ts.tv_sec); + ts.tv_nsec -= NSEC_PER_SEC; + } + + while(ts.tv_nsec <= -NSEC_PER_SEC) + { + --(ts.tv_sec); + ts.tv_nsec += NSEC_PER_SEC; + } + + if(ts.tv_nsec < 0) + { + /* Negative nanoseconds isn't valid according to POSIX. + * Decrement tv_sec and roll tv_nsec over. + */ + + --(ts.tv_sec); + ts.tv_nsec = (NSEC_PER_SEC + ts.tv_nsec); + } + + return ts; +} + +#ifdef TEST +#include + +#define TEST_NORMALISE(ts_sec, ts_nsec, expect_sec, expect_nsec) { \ + struct timespec in = { .tv_sec = ts_sec, .tv_nsec = ts_nsec }; \ + struct timespec got = timespec_normalise(in); \ + if(got.tv_sec != expect_sec || got.tv_nsec != expect_nsec) \ + { \ + printf("%s:%d: timespec_normalise({%ld, %ld}) returned wrong values\n", __FILE__, __LINE__, \ + (long)(ts_sec), (long)(ts_nsec)); \ + printf(" Expected: {%ld, %ld}\n", (long)(expect_sec), (long)(expect_nsec)); \ + printf(" Got: {%ld, %ld}\n", (long)(got.tv_sec), (long)(got.tv_nsec)); \ + ++result; \ + } \ +} + +#define TEST_BINOP(func, ts1_sec, ts1_nsec, ts2_sec, ts2_nsec, expect_sec, expect_nsec) { \ + struct timespec ts1 = { .tv_sec = ts1_sec, .tv_nsec = ts1_nsec }; \ + struct timespec ts2 = { .tv_sec = ts2_sec, .tv_nsec = ts2_nsec }; \ + struct timespec got = func(ts1, ts2); \ + if(got.tv_sec != expect_sec || got.tv_nsec != expect_nsec) \ + { \ + printf(#func "({%ld, %ld}, {%ld, %ld}) returned wrong values\n", \ + (long)(ts1_sec), (long)(ts1_nsec), (long)(ts2_sec), (long)(ts2_nsec)); \ + printf(" Expected: {%ld, %ld}\n", (long)(expect_sec), (long)(expect_nsec)); \ + printf(" Got: {%ld, %ld}\n", (long)(got.tv_sec), (long)(got.tv_nsec)); \ + ++result; \ + } \ +} + +#define TEST_TRINOP(func, ts1_sec, ts1_nsec, ts2_sec, ts2_nsec, ts3_sec, ts3_nsec, expect_sec, expect_nsec) { \ + struct timespec ts1 = { .tv_sec = ts1_sec, .tv_nsec = ts1_nsec }; \ + struct timespec ts2 = { .tv_sec = ts2_sec, .tv_nsec = ts2_nsec }; \ + struct timespec ts3 = { .tv_sec = ts3_sec, .tv_nsec = ts3_nsec }; \ + struct timespec got = func(ts1, ts2, ts3); \ + if(got.tv_sec != expect_sec || got.tv_nsec != expect_nsec) \ + { \ + printf(#func "({%ld, %ld}, {%ld, %ld}, {%ld, %ld}) returned wrong values\n", \ + (long)(ts1_sec), (long)(ts1_nsec), \ + (long)(ts2_sec), (long)(ts2_nsec), \ + (long)(ts3_sec), (long)(ts3_nsec)); \ + printf(" Expected: {%ld, %ld}\n", (long)(expect_sec), (long)(expect_nsec)); \ + printf(" Got: {%ld, %ld}\n", (long)(got.tv_sec), (long)(got.tv_nsec)); \ + ++result; \ + } \ +} + +#define TEST_TEST_FUNC(func, ts1_sec, ts1_nsec, ts2_sec, ts2_nsec, expect) { \ + struct timespec ts1 = { .tv_sec = ts1_sec, .tv_nsec = ts1_nsec }; \ + struct timespec ts2 = { .tv_sec = ts2_sec, .tv_nsec = ts2_nsec }; \ + int got = func(ts1, ts2); \ + if(got != expect) \ + { \ + printf("%s:%d: " #func "({%ld, %ld}, {%ld, %ld}) returned %d, expected %s\n", __FILE__, __LINE__, \ + (long)(ts1_sec), (long)(ts1_nsec), (long)(ts2_sec), (long)(ts2_nsec), \ + got, #expect); \ + ++result; \ + } \ +} + +#define TEST_FROM_DOUBLE(d_secs, expect_sec, expect_nsec) { \ + struct timespec got = timespec_from_double(d_secs); \ + if(got.tv_sec != expect_sec || got.tv_nsec != expect_nsec) \ + { \ + printf("%s:%d: timespec_from_double(%f) returned wrong values\n", __FILE__, __LINE__, (double)(d_secs)); \ + printf(" Expected: {%ld, %ld}\n", (long)(expect_sec), (long)(expect_nsec)); \ + printf(" Got: {%ld, %ld}\n", (long)(got.tv_sec), (long)(got.tv_nsec)); \ + ++result; \ + } \ +} + +#define TEST_TO_DOUBLE(ts_sec, ts_nsec, expect) { \ + struct timespec ts = { .tv_sec = ts_sec, .tv_nsec = ts_nsec }; \ + double got = timespec_to_double(ts); \ + if(got != expect) { \ + printf("%s:%d: timespec_to_double({%ld, %ld}) returned wrong value\n", __FILE__, __LINE__, \ + (long)(ts_sec), (long)(ts_nsec)); \ + printf(" Expected: %f\n", (double)(expect)); \ + printf(" Got: %f\n", got); \ + ++result; \ + } \ +} + +#define TEST_FROM_TIMEVAL(in_sec, in_usec, expect_sec, expect_nsec) { \ + struct timeval tv = { .tv_sec = in_sec, .tv_usec = in_usec }; \ + struct timespec got = timespec_from_timeval(tv); \ + if(got.tv_sec != expect_sec || got.tv_nsec != expect_nsec) \ + { \ + printf("%s:%d: timespec_from_timeval({%ld, %ld}) returned wrong values\n", __FILE__, __LINE__, \ + (long)(in_sec), (long)(in_usec)); \ + printf(" Expected: {%ld, %ld}\n", (long)(expect_sec), (long)(expect_nsec)); \ + printf(" Got: {%ld, %ld}\n", (long)(got.tv_sec), (long)(got.tv_nsec)); \ + ++result; \ + } \ +} + +#define TEST_TO_TIMEVAL(ts_sec, ts_nsec, expect_sec, expect_usec) { \ + struct timespec ts = { .tv_sec = ts_sec, .tv_nsec = ts_nsec }; \ + struct timeval got = timespec_to_timeval(ts); \ + if(got.tv_sec != expect_sec || got.tv_usec != expect_usec) \ + { \ + printf("%s:%d: timespec_to_timeval({%ld, %ld}) returned wrong values\n", __FILE__, __LINE__, \ + (long)(ts_sec), (long)(ts_nsec)); \ + printf(" Expected: {%ld, %ld}\n", (long)(expect_sec), (long)(expect_usec)); \ + printf(" Got: {%ld, %ld}\n", (long)(got.tv_sec), (long)(got.tv_usec)); \ + ++result; \ + } \ +} + +#define TEST_FROM_MS(msecs, expect_sec, expect_nsec) { \ + struct timespec got = timespec_from_ms(msecs); \ + if(got.tv_sec != expect_sec || got.tv_nsec != expect_nsec) \ + { \ + printf("%s:%d: timespec_from_ms(%ld) returned wrong values\n", __FILE__, __LINE__, (long)(msecs)); \ + printf(" Expected: {%ld, %ld}\n", (long)(expect_sec), (long)(expect_nsec)); \ + printf(" Got: {%ld, %ld}\n", (long)(got.tv_sec), (long)(got.tv_nsec)); \ + ++result; \ + } \ +} + +#define TEST_TO_MS(ts_sec, ts_nsec, expect) { \ + struct timespec ts = { .tv_sec = ts_sec, .tv_nsec = ts_nsec }; \ + long got = timespec_to_ms(ts); \ + if(got != expect) { \ + printf("%s:%d: timespec_to_ms({%ld, %ld}) returned wrong value\n", __FILE__, __LINE__, \ + (long)(ts_sec), (long)(ts_nsec)); \ + printf(" Expected: %ld\n", (long)(expect)); \ + printf(" Got: %ld\n", got); \ + ++result; \ + } \ +} + +int main() +{ + int result = 0; + + // timespec_add + + TEST_BINOP(timespec_add, 0,0, 0,0, 0,0); + TEST_BINOP(timespec_add, 0,0, 1,0, 1,0); + TEST_BINOP(timespec_add, 1,0, 0,0, 1,0); + TEST_BINOP(timespec_add, 1,0, 1,0, 2,0); + TEST_BINOP(timespec_add, 1,500000000, 1,0, 2,500000000); + TEST_BINOP(timespec_add, 1,0, 1,500000000, 2,500000000); + TEST_BINOP(timespec_add, 1,500000000, 1,500000000, 3,0); + TEST_BINOP(timespec_add, 1,500000000, 1,499999999, 2,999999999); + TEST_BINOP(timespec_add, 1,500000000, 1,500000000, 3,0); + TEST_BINOP(timespec_add, 1,999999999, 1,999999999, 3,999999998); + TEST_BINOP(timespec_add, 0,500000000, 1,500000000, 2,0); + TEST_BINOP(timespec_add, 1,500000000, 0,500000000, 2,0); + + // timespec_sub + + TEST_BINOP(timespec_sub, 0,0, 0,0, 0,0); + TEST_BINOP(timespec_sub, 1,0, 0,0, 1,0); + TEST_BINOP(timespec_sub, 1,0, 1,0, 0,0); + TEST_BINOP(timespec_sub, 1,500000000, 0,500000000, 1,0); + TEST_BINOP(timespec_sub, 5,500000000, 2,999999999, 2,500000001); + TEST_BINOP(timespec_sub, 0,0, 1,0, -1,0); + TEST_BINOP(timespec_sub, 0,500000000, 1,500000000, -1,0); + TEST_BINOP(timespec_sub, 0,0, 1,500000000, -2,500000000); + TEST_BINOP(timespec_sub, 1,0, 1,500000000, -1,500000000); + TEST_BINOP(timespec_sub, 1,0, 1,499999999, -1,500000001); + + // timespec_mod + + TEST_BINOP(timespec_mod, 0,0, 0,0, 0,0); + TEST_BINOP(timespec_mod, 0,0, 1,0, 0,0); + TEST_BINOP(timespec_mod, 1,0, 0,0, 1,0); + TEST_BINOP(timespec_mod, 1,0, 1,0, 0,0); + TEST_BINOP(timespec_mod, 10,0, 1,0, 0,0); + TEST_BINOP(timespec_mod, 10,0, 3,0, 1,0); + TEST_BINOP(timespec_mod, 10,0, -3,0, -2,0); + TEST_BINOP(timespec_mod, -10,0, 3,0, 2,0); + TEST_BINOP(timespec_mod, -10,0, -3,0, -1,0); + TEST_BINOP(timespec_mod, 10,0, 5,0, 0,0); + TEST_BINOP(timespec_mod, 10,0, -5,0, 0,0); + TEST_BINOP(timespec_mod, -10,0, 5,0, 0,0); + TEST_BINOP(timespec_mod, -10,0, -5,0, 0,0); + TEST_BINOP(timespec_mod, 1,500000000, 0,500000000, 0,0); + TEST_BINOP(timespec_mod, 5,500000000, 2,999999999, 2,500000001); + TEST_BINOP(timespec_mod, 0,500000000, 1,500000000, 0,500000000); + TEST_BINOP(timespec_mod, 0,0, 1,500000000, 0,0); + TEST_BINOP(timespec_mod, 1,0, 1,500000000, 1,0); + TEST_BINOP(timespec_mod, 1,0, 0,1, 0,0); + TEST_BINOP(timespec_mod, 1,123456789, 0,1000, 0,789); + TEST_BINOP(timespec_mod, 1,0, 0,9999999, 0,100); + TEST_BINOP(timespec_mod, 12345,54321, 0,100001, 0,5555); + TEST_BINOP(timespec_mod, LONG_MAX,0, 0,1, 0,0); + TEST_BINOP(timespec_mod, LONG_MAX,0, LONG_MAX,1, LONG_MAX,0); + + // timespec_clamp + + TEST_TRINOP(timespec_clamp, 0,0, 0,0, 0,0, 0,0); + + TEST_TRINOP(timespec_clamp, 1000,0, 2000,0, 3000,0, 2000,0); + TEST_TRINOP(timespec_clamp, 1500,0, 2000,0, 3000,0, 2000,0); + TEST_TRINOP(timespec_clamp, 1999,0, 2000,0, 3000,0, 2000,0); + TEST_TRINOP(timespec_clamp, 2000,0, 2000,0, 3000,0, 2000,0); + TEST_TRINOP(timespec_clamp, 2001,0, 2000,0, 3000,0, 2001,0); + TEST_TRINOP(timespec_clamp, 2250,0, 2000,0, 3000,0, 2250,0); + TEST_TRINOP(timespec_clamp, 2500,0, 2000,0, 3000,0, 2500,0); + TEST_TRINOP(timespec_clamp, 2750,0, 2000,0, 3000,0, 2750,0); + TEST_TRINOP(timespec_clamp, 2999,0, 2000,0, 3000,0, 2999,0); + TEST_TRINOP(timespec_clamp, 3000,0, 2000,0, 3000,0, 3000,0); + TEST_TRINOP(timespec_clamp, 3001,0, 2000,0, 3000,0, 3000,0); + TEST_TRINOP(timespec_clamp, 3500,0, 2000,0, 3000,0, 3000,0); + TEST_TRINOP(timespec_clamp, 4000,0, 2000,0, 3000,0, 3000,0); + + TEST_TRINOP(timespec_clamp, 0,1000, 0,2000, 0,3000, 0,2000); + TEST_TRINOP(timespec_clamp, 0,1500, 0,2000, 0,3000, 0,2000); + TEST_TRINOP(timespec_clamp, 0,1999, 0,2000, 0,3000, 0,2000); + TEST_TRINOP(timespec_clamp, 0,2000, 0,2000, 0,3000, 0,2000); + TEST_TRINOP(timespec_clamp, 0,2001, 0,2000, 0,3000, 0,2001); + TEST_TRINOP(timespec_clamp, 0,2250, 0,2000, 0,3000, 0,2250); + TEST_TRINOP(timespec_clamp, 0,2500, 0,2000, 0,3000, 0,2500); + TEST_TRINOP(timespec_clamp, 0,2750, 0,2000, 0,3000, 0,2750); + TEST_TRINOP(timespec_clamp, 0,2999, 0,2000, 0,3000, 0,2999); + TEST_TRINOP(timespec_clamp, 0,3000, 0,2000, 0,3000, 0,3000); + TEST_TRINOP(timespec_clamp, 0,3001, 0,2000, 0,3000, 0,3000); + TEST_TRINOP(timespec_clamp, 0,3500, 0,2000, 0,3000, 0,3000); + TEST_TRINOP(timespec_clamp, 0,4000, 0,2000, 0,3000, 0,3000); + + TEST_TRINOP(timespec_clamp,0,-1000, 0,-3000, 0,-2000, 0,-2000); + TEST_TRINOP(timespec_clamp,0,-1500, 0,-3000, 0,-2000, 0,-2000); + TEST_TRINOP(timespec_clamp,0,-1999, 0,-3000, 0,-2000, 0,-2000); + TEST_TRINOP(timespec_clamp,0,-3000, 0,-3000, 0,-2000, 0,-3000); + TEST_TRINOP(timespec_clamp,0,-2001, 0,-3000, 0,-2000, 0,-2001); + TEST_TRINOP(timespec_clamp,0,-2250, 0,-3000, 0,-2000, 0,-2250); + TEST_TRINOP(timespec_clamp,0,-2500, 0,-3000, 0,-2000, 0,-2500); + TEST_TRINOP(timespec_clamp,0,-2750, 0,-3000, 0,-2000, 0,-2750); + TEST_TRINOP(timespec_clamp,0,-2999, 0,-3000, 0,-2000, 0,-2999); + TEST_TRINOP(timespec_clamp,0,-2000, 0,-3000, 0,-2000, 0,-2000); + TEST_TRINOP(timespec_clamp,0,-3001, 0,-3000, 0,-2000, 0,-3000); + TEST_TRINOP(timespec_clamp,0,-3500, 0,-3000, 0,-2000, 0,-3000); + TEST_TRINOP(timespec_clamp,0,-2000, 0,-3000, 0,-2000, 0,-2000); + + TEST_TRINOP(timespec_clamp,0,-4000, 0,-3000, 0,3000, 0,-3000); + TEST_TRINOP(timespec_clamp,0,-3001, 0,-3000, 0,3000, 0,-3000); + TEST_TRINOP(timespec_clamp,0,-3000, 0,-3000, 0,3000, 0,-3000); + TEST_TRINOP(timespec_clamp,0,-2999, 0,-3000, 0,3000, 0,-2999); + TEST_TRINOP(timespec_clamp,0,-1500, 0,-3000, 0,3000, 0,-1500); + TEST_TRINOP(timespec_clamp,0, -1, 0,-3000, 0,3000, 0, -1); + TEST_TRINOP(timespec_clamp,0, 0, 0,-3000, 0,3000, 0, 0); + TEST_TRINOP(timespec_clamp,0, 1, 0,-3000, 0,3000, 0, 1); + TEST_TRINOP(timespec_clamp,0, 1500, 0,-3000, 0,3000, 0, 1500); + TEST_TRINOP(timespec_clamp,0, 2999, 0,-3000, 0,3000, 0, 2999); + TEST_TRINOP(timespec_clamp,0, 3000, 0,-3000, 0,3000, 0, 3000); + TEST_TRINOP(timespec_clamp,0, 3001, 0,-3000, 0,3000, 0, 3000); + TEST_TRINOP(timespec_clamp,0, 4000, 0,-3000, 0,3000, 0, 3000); + + // timespec_min + + TEST_BINOP(timespec_min, 0,0, 0,0, 0,0); + TEST_BINOP(timespec_min, 0,0, 1,0, 0,0); + TEST_BINOP(timespec_min, 1,0, 0,0, 0,0); + TEST_BINOP(timespec_min, 1,0, 1,0, 1,0); + TEST_BINOP(timespec_min, 10,0, 1,0, 1,0); + TEST_BINOP(timespec_min, 10,0, 3,0, 3,0); + TEST_BINOP(timespec_min, 10,0, -3,0, -3,0); + TEST_BINOP(timespec_min, -10,0, 3,0, -10,0); + TEST_BINOP(timespec_min, -10,0, -3,0, -10,0); + TEST_BINOP(timespec_min, 10,0, 5,0, 5,0); + TEST_BINOP(timespec_min, 10,0, -5,0, -5,0); + TEST_BINOP(timespec_min, -10,0, 5,0, -10,0); + TEST_BINOP(timespec_min, -10,0, -5,0, -10,0); + TEST_BINOP(timespec_min, 1,500000000, 0,500000000, 0,500000000); + TEST_BINOP(timespec_min, 5,500000000, 2,999999999, 2,999999999); + TEST_BINOP(timespec_min, 0,500000000, 1,500000000, 0,500000000); + TEST_BINOP(timespec_min, 0,0, 1,500000000, 0,0); + TEST_BINOP(timespec_min, 1,0, 1,500000000, 1,0); + TEST_BINOP(timespec_min, 1,0, 0,1, 0,1); + TEST_BINOP(timespec_min, 1,123456789, 0,1000, 0,1000); + TEST_BINOP(timespec_min, 1,0, 0,9999999, 0,9999999); + TEST_BINOP(timespec_min, 12345,54321, 0,100001, 0,100001); + TEST_BINOP(timespec_min, LONG_MIN,0, 0,1, LONG_MIN,0); + TEST_BINOP(timespec_min, LONG_MIN,0, 0,-1, LONG_MIN,0); + TEST_BINOP(timespec_min, LONG_MIN,0, LONG_MAX,0, LONG_MIN,0); + TEST_BINOP(timespec_min, LONG_MIN,0, LONG_MIN,0, LONG_MIN,0); + TEST_BINOP(timespec_min, LONG_MAX,0, 0,1, 0,1); + TEST_BINOP(timespec_min, LONG_MAX,0, 0,-1, 0,-1); + TEST_BINOP(timespec_min, LONG_MAX,0, LONG_MAX,0, LONG_MAX,0); + TEST_BINOP(timespec_min, LONG_MAX,0, LONG_MIN,0, LONG_MIN,0); + + // timespec_max + + TEST_BINOP(timespec_max, 0,0, 0,0, 0,0); + TEST_BINOP(timespec_max, 0,0, 1,0, 1,0); + TEST_BINOP(timespec_max, 1,0, 0,0, 1,0); + TEST_BINOP(timespec_max, 1,0, 1,0, 1,0); + TEST_BINOP(timespec_max, 10,0, 1,0, 10,0); + TEST_BINOP(timespec_max, 10,0, 3,0, 10,0); + TEST_BINOP(timespec_max, 10,0, -3,0, 10,0); + TEST_BINOP(timespec_max, -10,0, 3,0, 3,0); + TEST_BINOP(timespec_max, -10,0, -3,0, -3,0); + TEST_BINOP(timespec_max, 10,0, 5,0, 10,0); + TEST_BINOP(timespec_max, 10,0, -5,0, 10,0); + TEST_BINOP(timespec_max, -10,0, 5,0, 5,0); + TEST_BINOP(timespec_max, -10,0, -5,0, -5,0); + TEST_BINOP(timespec_max, 1,500000000, 0,500000000, 1,500000000); + TEST_BINOP(timespec_max, 5,500000000, 2,999999999, 5,500000000); + TEST_BINOP(timespec_max, 0,500000000, 1,500000000, 1,500000000); + TEST_BINOP(timespec_max, 0,0, 1,500000000, 1,500000000); + TEST_BINOP(timespec_max, 1,0, 1,500000000, 1,500000000); + TEST_BINOP(timespec_max, 1,0, 0,1, 1,0); + TEST_BINOP(timespec_max, 1,123456789, 0,1000, 1,123456789); + TEST_BINOP(timespec_max, 1,0, 0,9999999, 1,0); + TEST_BINOP(timespec_max, 12345,54321, 0,100001, 12345,54321); + TEST_BINOP(timespec_max, LONG_MIN,0, 0,1, 0,1); + TEST_BINOP(timespec_max, LONG_MIN,0, 0,-1, 0,-1); + TEST_BINOP(timespec_max, LONG_MIN,0, LONG_MAX,0, LONG_MAX,0); + TEST_BINOP(timespec_max, LONG_MIN,0, LONG_MIN,0, LONG_MIN,0); + TEST_BINOP(timespec_max, LONG_MAX,0, 0,1, LONG_MAX,0); + TEST_BINOP(timespec_max, LONG_MAX,0, 0,-1, LONG_MAX,0); + TEST_BINOP(timespec_max, LONG_MAX,0, LONG_MAX,0, LONG_MAX,0); + TEST_BINOP(timespec_max, LONG_MAX,0, LONG_MIN,0, LONG_MAX,0); + + // timespec_cmp + + TEST_TEST_FUNC(timespec_cmp, 0,0, 0,0, 0); + TEST_TEST_FUNC(timespec_cmp, 100,0, 100,0, 0); + TEST_TEST_FUNC(timespec_cmp, -100,0, -100,0, 0); + + TEST_TEST_FUNC(timespec_cmp, 1,0, 0,0, 1); + TEST_TEST_FUNC(timespec_cmp, 0,0, 1,0, -1); + TEST_TEST_FUNC(timespec_cmp, 0,1, 0,0, 1); + TEST_TEST_FUNC(timespec_cmp, 0,0, 0,1, -1); + TEST_TEST_FUNC(timespec_cmp, 1,0, 0,100, 1); + TEST_TEST_FUNC(timespec_cmp, 0,100 , 1,0, -1); + + TEST_TEST_FUNC(timespec_cmp, -0,-0, 0,0, 0); + TEST_TEST_FUNC(timespec_cmp, -10,-500000000, -11,500000000, 0); + TEST_TEST_FUNC(timespec_cmp, -10,-500000001, -11,499999999, 0); + TEST_TEST_FUNC(timespec_cmp, -10,-500000001, -11,500000001, -1); + TEST_TEST_FUNC(timespec_cmp, -11,500000001, -10,-500000001, 1); + + // timespec_eq + + TEST_TEST_FUNC(timespec_eq, 0,0, 0,0, true); + TEST_TEST_FUNC(timespec_eq, 100,0, 100,0, true); + TEST_TEST_FUNC(timespec_eq, -200,0, -200,0, true); + TEST_TEST_FUNC(timespec_eq, 0,300, 0,300, true); + TEST_TEST_FUNC(timespec_eq, 0,-400, 0,-400, true); + + TEST_TEST_FUNC(timespec_eq, 100,1, 100,0, false); + TEST_TEST_FUNC(timespec_eq, 101,0, 100,0, false); + TEST_TEST_FUNC(timespec_eq, -100,0, 100,0, false); + TEST_TEST_FUNC(timespec_eq, 0,10, 0,-10, false); + + TEST_TEST_FUNC(timespec_eq, -0,-0, 0,0, true); + TEST_TEST_FUNC(timespec_eq, -10,-500000000, -11,500000000, true); + TEST_TEST_FUNC(timespec_eq, -10,-500000001, -11,499999999, true); + TEST_TEST_FUNC(timespec_eq, -10,-500000001, -11,500000001, false); + + // timespec_gt + + TEST_TEST_FUNC(timespec_gt, 1,0, 0,0, true); + TEST_TEST_FUNC(timespec_gt, 0,0, -1,0, true); + TEST_TEST_FUNC(timespec_gt, 0,1, 0,0, true); + TEST_TEST_FUNC(timespec_gt, 0,0, 0,-1, true); + + TEST_TEST_FUNC(timespec_gt, 1,0, 1,0, false); + TEST_TEST_FUNC(timespec_gt, 1,1, 1,1, false); + TEST_TEST_FUNC(timespec_gt, -1,0, 0,0, false); + TEST_TEST_FUNC(timespec_gt, 0,-1, 0,0, false); + + TEST_TEST_FUNC(timespec_gt, 0,0, -0,-0, false); + TEST_TEST_FUNC(timespec_gt, -10,-500000000, -11,500000000, false); + TEST_TEST_FUNC(timespec_gt, -11,500000000, -10,-500000000, false); + TEST_TEST_FUNC(timespec_gt, -10,-500000001, -11,499999999, false); + TEST_TEST_FUNC(timespec_gt, -11,499999999, -11,499999999, false); + TEST_TEST_FUNC(timespec_gt, -10,-500000001, -11,500000001, false); + TEST_TEST_FUNC(timespec_gt, -11,500000001, -10,-500000001, true); + + // timespec_ge + + TEST_TEST_FUNC(timespec_ge, 1,0, 0,0, true); + TEST_TEST_FUNC(timespec_ge, 0,0, -1,0, true); + TEST_TEST_FUNC(timespec_ge, 0,1, 0,0, true); + TEST_TEST_FUNC(timespec_ge, 0,0, 0,-1, true); + TEST_TEST_FUNC(timespec_ge, 1,0, 1,0, true); + TEST_TEST_FUNC(timespec_ge, 1,1, 1,1, true); + + TEST_TEST_FUNC(timespec_ge, -1,0, 0,0, false); + TEST_TEST_FUNC(timespec_ge, 0,-1, 0,0, false); + + TEST_TEST_FUNC(timespec_ge, 0,0, -0,-0, true); + TEST_TEST_FUNC(timespec_ge, -10,-500000000, -11,500000000, true); + TEST_TEST_FUNC(timespec_ge, -11,500000000, -10,-500000000, true); + TEST_TEST_FUNC(timespec_ge, -10,-500000001, -11,499999999, true); + TEST_TEST_FUNC(timespec_ge, -11,499999999, -11,499999999, true); + TEST_TEST_FUNC(timespec_ge, -10,-500000001, -11,500000001, false); + TEST_TEST_FUNC(timespec_ge, -11,500000001, -10,-500000001, true); + + // timespec_lt + + TEST_TEST_FUNC(timespec_lt, 0,0, 1,0, true); + TEST_TEST_FUNC(timespec_lt, -1,0, 0,0, true); + TEST_TEST_FUNC(timespec_lt, 0,0, 0,1, true); + TEST_TEST_FUNC(timespec_lt, 0,-1, 0,0, true); + + TEST_TEST_FUNC(timespec_lt, 1,0, 1,0, false); + TEST_TEST_FUNC(timespec_lt, 1,1, 1,1, false); + TEST_TEST_FUNC(timespec_lt, 0,0, -1,0, false); + TEST_TEST_FUNC(timespec_lt, 0,0, 0,-1, false); + + TEST_TEST_FUNC(timespec_lt, 0,0, -0,-0, false); + TEST_TEST_FUNC(timespec_lt, -10,-500000000, -11,500000000, false); + TEST_TEST_FUNC(timespec_lt, -11,500000000, -10,-500000000, false); + TEST_TEST_FUNC(timespec_lt, -10,-500000001, -11,499999999, false); + TEST_TEST_FUNC(timespec_lt, -11,499999999, -11,499999999, false); + TEST_TEST_FUNC(timespec_lt, -10,-500000001, -11,500000001, true); + TEST_TEST_FUNC(timespec_lt, -11,500000001, -10,-500000001, false); + + // timespec_le + + TEST_TEST_FUNC(timespec_le, 0,0, 1,0, true); + TEST_TEST_FUNC(timespec_le, -1,0, 0,0, true); + TEST_TEST_FUNC(timespec_le, 0,0, 0,1, true); + TEST_TEST_FUNC(timespec_le, 0,-1, 0,0, true); + TEST_TEST_FUNC(timespec_le, 1,0, 1,0, true); + TEST_TEST_FUNC(timespec_le, 1,1, 1,1, true); + + TEST_TEST_FUNC(timespec_le, 0,0, -1,0, false); + TEST_TEST_FUNC(timespec_le, 0,0, 0,-1, false); + + TEST_TEST_FUNC(timespec_le, 0,0, -0,-0, true); + TEST_TEST_FUNC(timespec_le, -10,-500000000, -11,500000000, true); + TEST_TEST_FUNC(timespec_le, -11,500000000, -10,-500000000, true); + TEST_TEST_FUNC(timespec_le, -10,-500000001, -11,499999999, true); + TEST_TEST_FUNC(timespec_le, -11,499999999, -11,499999999, true); + TEST_TEST_FUNC(timespec_le, -10,-500000001, -11,500000001, true); + TEST_TEST_FUNC(timespec_le, -11,500000001, -10,-500000001, false); + + // timespec_from_double + + TEST_FROM_DOUBLE(0.0, 0,0); + TEST_FROM_DOUBLE(10.0, 10,0); + TEST_FROM_DOUBLE(-10.0, -10,0); + TEST_FROM_DOUBLE(0.5, 0,500000000); + TEST_FROM_DOUBLE(-0.5, -1,500000000); + TEST_FROM_DOUBLE(10.5, 10,500000000); + TEST_FROM_DOUBLE(-10.5, -11,500000000); + + // timespec_to_double + + TEST_TO_DOUBLE(0,0, 0.0); + TEST_TO_DOUBLE(10,0, 10.0); + TEST_TO_DOUBLE(-10,0, -10.0); + TEST_TO_DOUBLE(0,500000000, 0.5); + TEST_TO_DOUBLE(0,-500000000, -0.5); + TEST_TO_DOUBLE(10,500000000, 10.5); + TEST_TO_DOUBLE(10,-500000000, 9.5); + TEST_TO_DOUBLE(-10,500000000, -9.5); + TEST_TO_DOUBLE(-10,-500000000, -10.5); + + // timespec_from_timeval + + TEST_FROM_TIMEVAL(0,0, 0,0); + TEST_FROM_TIMEVAL(1,0, 1,0); + TEST_FROM_TIMEVAL(1000,0, 1000,0); + TEST_FROM_TIMEVAL(0,0, 0,0); + TEST_FROM_TIMEVAL(-1,0, -1,0); + TEST_FROM_TIMEVAL(-1000,0, -1000,0); + + TEST_FROM_TIMEVAL(1,1, 1,1000); + TEST_FROM_TIMEVAL(1,1000, 1,1000000); + TEST_FROM_TIMEVAL(1,-1, 0,999999000); + TEST_FROM_TIMEVAL(1,-1000, 0,999000000); + TEST_FROM_TIMEVAL(-1,-1, -2,999999000); + TEST_FROM_TIMEVAL(-1,-1000, -2,999000000); + + // timespec_to_timeval + + TEST_TO_TIMEVAL(0,0, 0,0); + TEST_TO_TIMEVAL(1,0, 1,0); + TEST_TO_TIMEVAL(10,0, 10,0); + TEST_TO_TIMEVAL(-1,0, -1,0); + TEST_TO_TIMEVAL(-10,0, -10,0); + + TEST_TO_TIMEVAL(1,1, 1,0); + TEST_TO_TIMEVAL(1,999, 1,0); + TEST_TO_TIMEVAL(1,1000, 1,1); + TEST_TO_TIMEVAL(1,1001, 1,1); + TEST_TO_TIMEVAL(1,2000, 1,2); + TEST_TO_TIMEVAL(1,2000000, 1,2000); + + TEST_TO_TIMEVAL(1,-1, 0,999999); + TEST_TO_TIMEVAL(1,-999, 0,999999); + TEST_TO_TIMEVAL(1,-1000, 0,999999); + TEST_TO_TIMEVAL(1,-1001, 0,999998); + TEST_TO_TIMEVAL(1,-2000, 0,999998); + TEST_TO_TIMEVAL(1,-2000000, 0,998000); + + TEST_TO_TIMEVAL(-1,-1, -2,999999); + TEST_TO_TIMEVAL(-1,-999, -2,999999); + TEST_TO_TIMEVAL(-1,-1000, -2,999999); + TEST_TO_TIMEVAL(-1,-1001, -2,999998); + TEST_TO_TIMEVAL(-1,-2000, -2,999998); + TEST_TO_TIMEVAL(-1,-2000000, -2,998000); + + TEST_TO_TIMEVAL(1,1500000000, 2,500000); + TEST_TO_TIMEVAL(1,-1500000000, -1,500000); + TEST_TO_TIMEVAL(-1,-1500000000, -3,500000); + + // timespec_from_ms + + TEST_FROM_MS(0, 0,0); + TEST_FROM_MS(1, 0,1000000); + TEST_FROM_MS(-1, -1,999000000); + TEST_FROM_MS(1500, 1,500000000); + TEST_FROM_MS(-1000, -1,0); + TEST_FROM_MS(-1500, -2,500000000); + + // timespec_to_ms + + TEST_TO_MS(0,0, 0); + TEST_TO_MS(10,0, 10000); + TEST_TO_MS(-10,0, -10000); + TEST_TO_MS(0,500000000, 500); + TEST_TO_MS(0,-500000000, -500); + TEST_TO_MS(10,500000000, 10500); + TEST_TO_MS(10,-500000000, 9500); + TEST_TO_MS(-10,500000000, -9500); + TEST_TO_MS(-10,-500000000, -10500); + + // timespec_normalise + + TEST_NORMALISE(0,0, 0,0); + + TEST_NORMALISE(0,1000000000, 1,0); + TEST_NORMALISE(0,1500000000, 1,500000000); + TEST_NORMALISE(0,-1000000000, -1,0); + TEST_NORMALISE(0,-1500000000, -2,500000000); + + TEST_NORMALISE(5,1000000000, 6,0); + TEST_NORMALISE(5,1500000000, 6,500000000); + TEST_NORMALISE(-5,-1000000000, -6,0); + TEST_NORMALISE(-5,-1500000000, -7,500000000); + + TEST_NORMALISE(0,2000000000, 2,0); + TEST_NORMALISE(0,2100000000, 2,100000000); + TEST_NORMALISE(0,-2000000000, -2,0); + TEST_NORMALISE(0,-2100000000, -3,900000000); + + TEST_NORMALISE(1,-500000001, 0,499999999); + TEST_NORMALISE(1,-500000000, 0,500000000); + TEST_NORMALISE(1,-499999999, 0,500000001); + TEST_NORMALISE(0,-499999999, -1,500000001); + + TEST_NORMALISE(-1,500000000, -1,500000000); + TEST_NORMALISE(-1,499999999, -1,499999999); + + if(result > 0) + { + printf("%d tests failed\n", result); + } + else{ + printf("All tests passed\n"); + } + + return !!result; /* Don't overflow the exit status */ +} +#endif -- cgit From 9be1567765803341e252e87262dc43d790d8e770 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Fri, 2 Jan 2026 16:23:28 -0500 Subject: Move utility functions to utils.c --- src/cli/stasis/args.c | 26 ------------------------- src/cli/stasis/include/args.h | 4 ---- src/lib/core/include/utils.h | 6 ++++++ src/lib/core/multiprocessing.c | 18 ----------------- src/lib/core/utils.c | 44 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 48 deletions(-) diff --git a/src/cli/stasis/args.c b/src/cli/stasis/args.c index 9410958..172981a 100644 --- a/src/cli/stasis/args.c +++ b/src/cli/stasis/args.c @@ -106,29 +106,3 @@ void usage(char *progname) { puts(output); } } - -int str_to_timeout(char *s) { - if (!s) { - return 0; // no timeout - } - - char *scale = NULL; - int value = (int) strtol(s, &scale, 10); - if (scale) { - if (*scale == 's') { - value *= 1; // seconds, no-op - } else if (*scale == 'm') { - value *= 60; // minutes - } else if (*scale == 'h') { - value *= 3200; // hours - } else { - return STR_TO_TIMEOUT_INVALID_TIME_SCALE; // invalid time scale - } - } - - if (value < 0) { - return STR_TO_TIMEOUT_NEGATIVE; // cannot be negative - } - return value; -} - diff --git a/src/cli/stasis/include/args.h b/src/cli/stasis/include/args.h index d75fe29..5536735 100644 --- a/src/cli/stasis/include/args.h +++ b/src/cli/stasis/include/args.h @@ -23,8 +23,4 @@ extern struct option long_options[]; void usage(char *progname); -#define STR_TO_TIMEOUT_NEGATIVE (-1) -#define STR_TO_TIMEOUT_INVALID_TIME_SCALE (-2) -int str_to_timeout(char *s); - #endif //STASIS_ARGS_H diff --git a/src/lib/core/include/utils.h b/src/lib/core/include/utils.h index a9bcd2f..2476d4e 100644 --- a/src/lib/core/include/utils.h +++ b/src/lib/core/include/utils.h @@ -464,4 +464,10 @@ int is_git_sha(char const *hash); int check_python_package_dependencies(const char *srcdir); +char *seconds_to_human_readable(int v); + +#define STR_TO_TIMEOUT_NEGATIVE (-1) +#define STR_TO_TIMEOUT_INVALID_TIME_SCALE (-2) +int str_to_timeout(char *s); + #endif //STASIS_UTILS_H diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index 43f3e07..91b668a 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -4,24 +4,6 @@ /// The sum of all tasks started by mp_task() size_t mp_global_task_count = 0; -static char *seconds_to_human_readable(const int v) { - static char result[255] = {0}; - const int hours = v / 3600; - const int minutes = (v % 3600) / 60; - const int seconds = v % 60; - - memset(result, '\0', sizeof(result)); - if (hours) { - snprintf(result + strlen(result), sizeof(result), "%dh ", hours); - } - if (minutes) { - snprintf(result + strlen(result), sizeof(result), "%dm ", minutes); - } - snprintf(result + strlen(result), sizeof(result), "%ds", seconds); - - return result; -} - static double get_duration(const struct timespec stop, const struct timespec start) { const struct timespec result = timespec_sub(stop, start); return timespec_to_double(result); diff --git a/src/lib/core/utils.c b/src/lib/core/utils.c index 62f3bec..0871787 100644 --- a/src/lib/core/utils.c +++ b/src/lib/core/utils.c @@ -1078,3 +1078,47 @@ int check_python_package_dependencies(const char *srcdir) { } return 0; } + +int str_to_timeout(char *s) { + if (!s) { + return 0; // no timeout + } + + char *scale = NULL; + int value = (int) strtol(s, &scale, 10); + if (scale) { + if (*scale == 's') { + value *= 1; // seconds, no-op + } else if (*scale == 'm') { + value *= 60; // minutes + } else if (*scale == 'h') { + value *= 3200; // hours + } else { + return STR_TO_TIMEOUT_INVALID_TIME_SCALE; // invalid time scale + } + } + + if (value < 0) { + return STR_TO_TIMEOUT_NEGATIVE; // cannot be negative + } + return value; +} + +char *seconds_to_human_readable(const int v) { + static char result[255] = {0}; + const int hours = v / 3600; + const int minutes = (v % 3600) / 60; + const int seconds = v % 60; + + memset(result, '\0', sizeof(result)); + if (hours) { + snprintf(result + strlen(result), sizeof(result), "%dh ", hours); + } + if (hours || minutes) { + snprintf(result + strlen(result), sizeof(result), "%dm ", minutes); + } + snprintf(result + strlen(result), sizeof(result), "%ds", seconds); + + return result; +} + -- cgit From 69ad7329284d9d5af6d23a4fb4a6f605228ea52a Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Fri, 2 Jan 2026 16:29:10 -0500 Subject: Consolidate timer data * Add MultiProcessingTimer struct * Replace raw timespec and double counters with MultiProcessingTimer(s) --- src/lib/core/include/multiprocessing.h | 16 ++++++++-------- src/lib/core/multiprocessing.c | 29 +++++++++++++++-------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/lib/core/include/multiprocessing.h b/src/lib/core/include/multiprocessing.h index 933f7d6..874777c 100644 --- a/src/lib/core/include/multiprocessing.h +++ b/src/lib/core/include/multiprocessing.h @@ -12,27 +12,27 @@ #include #include +struct MultiProcessingTimer { + struct timespec t_start; + struct timespec t_stop; + double duration; +}; + struct MultiProcessingTask { pid_t pid; ///< Program PID pid_t parent_pid; ///< Program PID (parent process) int status; ///< Child process exit status int signaled_by; ///< Last signal received, if any int timeout; ///< Seconds to elapse before killing the process - struct timespec _interval_start; ///< Current time, start of interval - struct timespec _interval_stop; ///< Time elapsed since status interval (used by MultiprocessingPool.status_interval) - double interval_elapsed; time_t _startup; ///< Time elapsed since task started - double elapsed; ///< Total time elapsed in seconds char ident[255]; ///< Identity of the pool task char *cmd; ///< Shell command(s) to be executed size_t cmd_len; ///< Length of command string (for mmap/munmap) char working_dir[PATH_MAX]; ///< Path to directory `cmd` should be executed in char log_file[PATH_MAX]; ///< Full path to stdout/stderr log file char parent_script[PATH_MAX]; ///< Path to temporary script executing the task - struct { - struct timespec t_start; - struct timespec t_stop; - } time_data; ///< Wall-time counters + struct MultiProcessingTimer time_data; ///< Wall-time counters + struct MultiProcessingTimer interval_data; ///< Progress report counters }; struct MultiProcessingPool { diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index 91b668a..f39f74b 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -16,14 +16,14 @@ static double get_task_duration(const struct MultiProcessingTask *task) { } static double get_task_interval_duration(const struct MultiProcessingTask *task) { - const struct timespec *start = &task->_interval_start; - const struct timespec *stop = &task->_interval_stop; + const struct timespec *start = &task->interval_data.t_start; + const struct timespec *stop = &task->interval_data.t_stop; return get_duration(*stop, *start); } static void update_task_interval_start(struct MultiProcessingTask *task) { // Record the task stop time - if (clock_gettime(CLOCK_REALTIME, &task->_interval_start) < 0) { + if (clock_gettime(CLOCK_REALTIME, &task->interval_data.t_start) < 0) { perror("clock_gettime"); exit(1); } @@ -31,11 +31,11 @@ static void update_task_interval_start(struct MultiProcessingTask *task) { static void update_task_interval_elapsed(struct MultiProcessingTask *task) { // Record the interval stop time - if (clock_gettime(CLOCK_REALTIME, &task->_interval_stop) < 0) { + if (clock_gettime(CLOCK_REALTIME, &task->interval_data.t_stop) < 0) { perror("clock_gettime"); exit(1); } - task->interval_elapsed = get_task_interval_duration(task); + task->interval_data.duration = get_task_interval_duration(task); } static void update_task_start(struct MultiProcessingTask *task) { @@ -51,7 +51,7 @@ static void update_task_elapsed(struct MultiProcessingTask *task) { perror("clock_gettime"); exit(1); } - task->elapsed = get_task_duration(task); + task->time_data.duration = get_task_duration(task); } static struct MultiProcessingTask *mp_pool_next_available(struct MultiProcessingPool *pool) { @@ -244,7 +244,7 @@ void mp_pool_show_summary(struct MultiProcessingPool *pool) { strcpy(status_str, "FAIL"); } - printf("%-4s %10d %10s %-10s\n", status_str, task->parent_pid, seconds_to_human_readable(task->elapsed), task->ident) ; + printf("%-4s %10d %10s %-10s\n", status_str, task->parent_pid, seconds_to_human_readable(task->time_data.duration), task->ident) ; //printf("%-4s %10d %7lds %-10s\n", status_str, task->parent_pid, task->elapsed, task->ident) ; } puts(""); @@ -363,7 +363,7 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { int task_timed_out = false; if (slot->timeout) { - task_timed_out = slot->elapsed >= (double) slot->timeout; + task_timed_out = slot->time_data.duration >= (double) slot->timeout; if (task_timed_out && pid == 0 && slot->pid != 0) { printf("%s Task timed out after %s (pid: %d)\n", progress, seconds_to_human_readable(slot->timeout), slot->pid); if (kill(slot->pid, SIGKILL) == 0) { @@ -419,7 +419,7 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { semaphore_wait(&pool->semaphore); update_task_elapsed(slot); semaphore_post(&pool->semaphore); - fprintf(stderr, "%s Task failed after %s\n", progress, seconds_to_human_readable(slot->elapsed)); + fprintf(stderr, "%s Task failed after %s\n", progress, seconds_to_human_readable(slot->time_data.duration)); failures++; if (flags & MP_POOL_FAIL_FAST && pool->num_used > 1) { @@ -427,7 +427,7 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { return -2; } } else { - printf("%s Task finished after %s\n", progress, seconds_to_human_readable(slot->elapsed)); + printf("%s Task finished after %s\n", progress, seconds_to_human_readable(slot->time_data.duration)); } // Clean up logs and scripts left behind by the task @@ -450,11 +450,12 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { // When a task has executed for longer than status_intervals, print a status update // interval_elapsed represents the time between intervals, not the total runtime of the task semaphore_wait(&pool->semaphore); - if (fabs(slot->interval_elapsed) > pool->status_interval) { - slot->interval_elapsed = 0.0; + if (fabs(slot->interval_data.duration) > pool->status_interval) { + slot->interval_data.duration = 0.0; } - if (slot->interval_elapsed == 0.0) { - printf("[%s:%s] Task is running (pid: %d, elapsed: %s)\n", pool->ident, slot->ident, slot->parent_pid, seconds_to_human_readable(slot->elapsed)); + if (slot->interval_data.duration == 0.0) { + printf("[%s:%s] Task is running (pid: %d, elapsed: %s)\n", + pool->ident, slot->ident, slot->parent_pid, seconds_to_human_readable(slot->time_data.duration)); update_task_interval_start(slot); } -- cgit From 11e4e32a37e61d7e3168adbdfaf507aa58cb43f0 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Fri, 2 Jan 2026 16:29:45 -0500 Subject: Add tests * test_mp_timeout() * test_mp_seconds_to_human_readable() --- tests/test_multiprocessing.c | 54 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/tests/test_multiprocessing.c b/tests/test_multiprocessing.c index b10f530..4a68688 100644 --- a/tests/test_multiprocessing.c +++ b/tests/test_multiprocessing.c @@ -171,6 +171,42 @@ void test_mp_fail_fast() { mp_pool_free(&p); } +static void test_mp_timeout() { + struct MultiProcessingPool *p = NULL; + p = mp_pool_init("timeout", "timeoutlogs"); + p->status_interval = 1; + struct MultiProcessingTask *task = mp_pool_task(p, "timeout", NULL, "sleep 5"); + int timeout = 3; + task->timeout = timeout; + mp_pool_join(p, 1, 0); + STASIS_ASSERT((task->time_data.duration >= (double) timeout && task->time_data.duration < (double) timeout + 1), "Timeout occurred out of desired range"); + mp_pool_show_summary(p); + mp_pool_free(&p); +} + +static void test_mp_seconds_to_human_readable() { + struct testcase { + int seconds; + const char *expected; + } tc[] = { + {.seconds = -1, "-1s"}, + {.seconds = 0, "0s"}, + {.seconds = 10, "10s"}, + {.seconds = 20, "20s"}, + {.seconds = 30, "30s"}, + {.seconds = 60, "1m 0s"}, + {.seconds = 125, "2m 5s"}, + {.seconds = 3600, "1h 0m 0s"}, + {.seconds = 86399, "23h 59m 59s"}, + {.seconds = 86400, "24h 0m 0s"}, + }; + for (size_t i = 0; i < sizeof(tc) / sizeof(tc[0]); i++) { + char *result = seconds_to_human_readable(tc[i].seconds); + printf("seconds=%d, expected: %s, got: %s\n", tc[i].seconds, tc[i].expected, result); + STASIS_ASSERT(strcmp(result, tc[i].expected) == 0, "bad output"); + } +} + pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static void *pool_container(void *data) { char *commands_sc[] = { @@ -206,15 +242,19 @@ void test_mp_stop_continue() { int main(int argc, char *argv[]) { STASIS_TEST_BEGIN_MAIN(); STASIS_TEST_FUNC *tests[] = { - test_mp_pool_init, - test_mp_task, - test_mp_pool_join, - test_mp_pool_free, - test_mp_pool_workflow, - test_mp_fail_fast, - test_mp_stop_continue + //test_mp_pool_init, + //test_mp_task, + //test_mp_pool_join, + //test_mp_pool_free, + //test_mp_pool_workflow, + //test_mp_fail_fast, + test_mp_timeout, + test_mp_seconds_to_human_readable, + //test_mp_stop_continue }; + globals.task_timeout = 60; + STASIS_TEST_RUN(tests); STASIS_TEST_END_MAIN(); } -- cgit From 5c1f18bf3a98ad71d3674f4fd935feae73e0963a Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Sat, 3 Jan 2026 02:01:22 -0500 Subject: Trying to fix a buffer overflow reported by the CI * Locally ASAN wasn't complaining. --- src/lib/core/include/utils.h | 2 +- src/lib/core/multiprocessing.c | 17 ++++++++++++----- src/lib/core/utils.c | 13 +++++-------- tests/test_multiprocessing.c | 5 +++-- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/lib/core/include/utils.h b/src/lib/core/include/utils.h index 2476d4e..ea98faf 100644 --- a/src/lib/core/include/utils.h +++ b/src/lib/core/include/utils.h @@ -464,7 +464,7 @@ int is_git_sha(char const *hash); int check_python_package_dependencies(const char *srcdir); -char *seconds_to_human_readable(int v); +void seconds_to_human_readable(int v, char *result, size_t maxlen); #define STR_TO_TIMEOUT_NEGATIVE (-1) #define STR_TO_TIMEOUT_INVALID_TIME_SCALE (-2) diff --git a/src/lib/core/multiprocessing.c b/src/lib/core/multiprocessing.c index f39f74b..298484a 100644 --- a/src/lib/core/multiprocessing.c +++ b/src/lib/core/multiprocessing.c @@ -244,7 +244,9 @@ void mp_pool_show_summary(struct MultiProcessingPool *pool) { strcpy(status_str, "FAIL"); } - printf("%-4s %10d %10s %-10s\n", status_str, task->parent_pid, seconds_to_human_readable(task->time_data.duration), task->ident) ; + char duration[255] = {0}; + seconds_to_human_readable(task->time_data.duration, duration, sizeof(duration)); + printf("%-4s %10d %10s %-10s\n", status_str, task->parent_pid, duration, task->ident) ; //printf("%-4s %10d %7lds %-10s\n", status_str, task->parent_pid, task->elapsed, task->ident) ; } puts(""); @@ -329,6 +331,7 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { } for (size_t i = lower_i; i < upper_i; i++) { + char duration[255] = {0}; struct MultiProcessingTask *slot = &pool->task[i]; if (slot->status == MP_POOL_TASK_STATUS_INITIAL) { slot->_startup = time(NULL); @@ -365,7 +368,8 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { if (slot->timeout) { task_timed_out = slot->time_data.duration >= (double) slot->timeout; if (task_timed_out && pid == 0 && slot->pid != 0) { - printf("%s Task timed out after %s (pid: %d)\n", progress, seconds_to_human_readable(slot->timeout), slot->pid); + seconds_to_human_readable(slot->timeout, duration, sizeof(duration)); + printf("%s Task timed out after %s (pid: %d)\n", progress, duration, slot->pid); if (kill(slot->pid, SIGKILL) == 0) { status = SIGKILL; } else { @@ -419,7 +423,8 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { semaphore_wait(&pool->semaphore); update_task_elapsed(slot); semaphore_post(&pool->semaphore); - fprintf(stderr, "%s Task failed after %s\n", progress, seconds_to_human_readable(slot->time_data.duration)); + seconds_to_human_readable(slot->time_data.duration, duration, sizeof(duration)); + fprintf(stderr, "%s Task failed after %s\n", progress, duration); failures++; if (flags & MP_POOL_FAIL_FAST && pool->num_used > 1) { @@ -427,7 +432,8 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { return -2; } } else { - printf("%s Task finished after %s\n", progress, seconds_to_human_readable(slot->time_data.duration)); + seconds_to_human_readable(slot->time_data.duration, duration, sizeof(duration)); + printf("%s Task finished after %s\n", progress, duration); } // Clean up logs and scripts left behind by the task @@ -454,8 +460,9 @@ int mp_pool_join(struct MultiProcessingPool *pool, size_t jobs, size_t flags) { slot->interval_data.duration = 0.0; } if (slot->interval_data.duration == 0.0) { + seconds_to_human_readable(slot->time_data.duration, duration, sizeof(duration)); printf("[%s:%s] Task is running (pid: %d, elapsed: %s)\n", - pool->ident, slot->ident, slot->parent_pid, seconds_to_human_readable(slot->time_data.duration)); + pool->ident, slot->ident, slot->parent_pid, duration); update_task_interval_start(slot); } diff --git a/src/lib/core/utils.c b/src/lib/core/utils.c index 0871787..a8b1c73 100644 --- a/src/lib/core/utils.c +++ b/src/lib/core/utils.c @@ -1104,21 +1104,18 @@ int str_to_timeout(char *s) { return value; } -char *seconds_to_human_readable(const int v) { - static char result[255] = {0}; +void seconds_to_human_readable(const int v, char *result, const size_t maxlen) { const int hours = v / 3600; const int minutes = (v % 3600) / 60; const int seconds = v % 60; - memset(result, '\0', sizeof(result)); + memset(result, '\0', maxlen); if (hours) { - snprintf(result + strlen(result), sizeof(result), "%dh ", hours); + snprintf(result + strlen(result), maxlen, "%dh ", hours); } if (hours || minutes) { - snprintf(result + strlen(result), sizeof(result), "%dm ", minutes); + snprintf(result + strlen(result), maxlen, "%dm ", minutes); } - snprintf(result + strlen(result), sizeof(result), "%ds", seconds); - - return result; + snprintf(result + strlen(result), maxlen, "%ds", seconds); } diff --git a/tests/test_multiprocessing.c b/tests/test_multiprocessing.c index 4a68688..1db8716 100644 --- a/tests/test_multiprocessing.c +++ b/tests/test_multiprocessing.c @@ -185,7 +185,7 @@ static void test_mp_timeout() { } static void test_mp_seconds_to_human_readable() { - struct testcase { + const struct testcase { int seconds; const char *expected; } tc[] = { @@ -201,7 +201,8 @@ static void test_mp_seconds_to_human_readable() { {.seconds = 86400, "24h 0m 0s"}, }; for (size_t i = 0; i < sizeof(tc) / sizeof(tc[0]); i++) { - char *result = seconds_to_human_readable(tc[i].seconds); + char result[255] = {0}; + seconds_to_human_readable(tc[i].seconds, result, sizeof(result)); printf("seconds=%d, expected: %s, got: %s\n", tc[i].seconds, tc[i].expected, result); STASIS_ASSERT(strcmp(result, tc[i].expected) == 0, "bad output"); } -- cgit From b840c9737effa4b9c5e6550efab468c53e86cf11 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Sat, 3 Jan 2026 08:56:31 -0500 Subject: Enable tests --- tests/test_multiprocessing.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_multiprocessing.c b/tests/test_multiprocessing.c index 1db8716..60aa4f6 100644 --- a/tests/test_multiprocessing.c +++ b/tests/test_multiprocessing.c @@ -243,15 +243,15 @@ void test_mp_stop_continue() { int main(int argc, char *argv[]) { STASIS_TEST_BEGIN_MAIN(); STASIS_TEST_FUNC *tests[] = { - //test_mp_pool_init, - //test_mp_task, - //test_mp_pool_join, - //test_mp_pool_free, - //test_mp_pool_workflow, - //test_mp_fail_fast, + test_mp_pool_init, + test_mp_task, + test_mp_pool_join, + test_mp_pool_free, + test_mp_pool_workflow, + test_mp_fail_fast, test_mp_timeout, test_mp_seconds_to_human_readable, - //test_mp_stop_continue + test_mp_stop_continue }; globals.task_timeout = 60; -- cgit From 272231cf29b8ce348d53b6950247fd7faec2d372 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Sat, 3 Jan 2026 11:41:47 -0500 Subject: Add TESTS_VERBOSE cmake option * Defines STASIS_TEST_VERBOSE --- CMakeLists.txt | 10 ++++++++++ tests/CMakeLists.txt | 3 +++ 2 files changed, 13 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f5dfa9..074c2ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,12 +54,22 @@ message(CHECK_START "Run unit tests") if (TESTS) message(CHECK_PASS "yes") enable_testing() + + message(CHECK_START "Verbose test output") + option(TESTS_VERBOSE OFF) + if (TESTS_VERBOSE) + message(CHECK_PASS "yes") + else() + message(CHECK_PASS "no") + endif() + message(CHECK_START "Run regression tests") if (TESTS_RT) message(CHECK_PASS "yes") else() message(CHECK_PASS "no") endif() + add_subdirectory(tests) else() message(CHECK_PASS "no") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2b09e9e..08ef833 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -30,6 +30,9 @@ foreach(source_file ${source_files}) elseif (CMAKE_C_COMPILER_ID STREQUAL "MSVC") target_compile_options(${test_executable} PRIVATE ${win_cflags} ${win_msvc_cflags}) endif() + if (TESTS_VERBOSE) + target_compile_definitions(${test_executable} PRIVATE STASIS_TEST_VERBOSE=1) + endif () target_include_directories(${test_executable} PRIVATE ${core_INCLUDE} ${delivery_INCLUDE} -- cgit From c948d5020091cc40fe8eb7f0a21e51a2c1dc3ef5 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Sat, 3 Jan 2026 11:42:24 -0500 Subject: Add STASIS_TEST_MSG macro --- tests/include/testing.h | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/include/testing.h b/tests/include/testing.h index ab24115..6fa5ca6 100644 --- a/tests/include/testing.h +++ b/tests/include/testing.h @@ -9,6 +9,15 @@ #define __FILE_NAME__ __FILE__ #endif +#ifdef STASIS_TEST_VERBOSE +#define STASIS_TEST_MSG(MSG, ...) do { \ +fprintf(stderr, "%s:%d:%s(): ", path_basename(__FILE__), __LINE__, __FUNCTION__); \ +fprintf(stderr, MSG LINE_SEP, __VA_ARGS__); \ +} while (0) +#else +#define STASIS_TEST_MSG(MSG, ...) do {} while (0) +#endif + typedef void(STASIS_TEST_FUNC)(); struct stasis_test_result_t { const char *filename; -- cgit From ba5a5fda9c3fcc5490c97ddb6b1beef41da7c8e2 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Sat, 3 Jan 2026 11:42:41 -0500 Subject: First use of STASIS_TEST_MSG --- tests/test_multiprocessing.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_multiprocessing.c b/tests/test_multiprocessing.c index 60aa4f6..3a462f1 100644 --- a/tests/test_multiprocessing.c +++ b/tests/test_multiprocessing.c @@ -161,7 +161,7 @@ void test_mp_fail_fast() { if (task->status == 0) result.total_status_success++; if (task->pid == MP_POOL_PID_UNUSED && task->status == MP_POOL_TASK_STATUS_INITIAL) result.total_unused++; } - fprintf(stderr, "total_status_fail = %d\ntotal_status_success = %d\ntotal_signaled = %d\ntotal_unused = %d\n", + STASIS_TEST_MSG("\ntotal_status_fail = %d\ntotal_status_success = %d\ntotal_signaled = %d\ntotal_unused = %d", result.total_status_fail, result.total_status_success, result.total_signaled, result.total_unused); STASIS_ASSERT(result.total_status_fail, "Should have failures"); STASIS_ASSERT(result.total_status_success, "Should have successes"); @@ -203,7 +203,7 @@ static void test_mp_seconds_to_human_readable() { for (size_t i = 0; i < sizeof(tc) / sizeof(tc[0]); i++) { char result[255] = {0}; seconds_to_human_readable(tc[i].seconds, result, sizeof(result)); - printf("seconds=%d, expected: %s, got: %s\n", tc[i].seconds, tc[i].expected, result); + STASIS_TEST_MSG("seconds=%d, expected: %s, got: %s", tc[i].seconds, tc[i].expected, result); STASIS_ASSERT(strcmp(result, tc[i].expected) == 0, "bad output"); } } -- cgit From a32d167a62f54ddc35690b7cdbee28ab26c27dae Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Sat, 3 Jan 2026 12:27:20 -0500 Subject: CI: Enable verbose testing --- .github/workflows/cmake-multi-platform.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index ee601b1..055fa0e 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -58,6 +58,7 @@ jobs: -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DTESTS=ON + -DTESTS_VERBOSE=ON -DTESTS_RT=ON -DDEBUG_MESSAGES=ON -S ${{ github.workspace }} -- cgit From 0ad1fc6e37835bd2a7bfbda41be1bf22f8f6bc5e Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 5 Jan 2026 09:43:49 -0500 Subject: Update README --- README.md | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 14f7ddb..f6a89bd 100644 --- a/README.md +++ b/README.md @@ -147,28 +147,30 @@ stasis mydelivery.ini ## Command Line Options -| Long Option | Short Option | Purpose | -|:---------------------------|:------------:|:---------------------------------------------------------------| -| --help | -h | Display usage statement | -| --version | -V | Display program version | -| --continue-on-error | -C | Allow tests to fail | -| --config ARG | -c ARG | Read STASIS configuration file | -| --cpu-limit ARG | -l ARG | Number of processes to spawn concurrently (default: cpus - 1) | -| --pool-status-interval ARG | n/a | Report task status every n seconds (default: 30) | -| --python ARG | -p ARG | Override version of Python in configuration | -| --verbose | -v | Increase output verbosity | -| --unbuffered | -U | Disable line buffering | -| --update-base | n/a | Update conda installation prior to STATIS environment creation | -| --fail-fast | n/a | On test error, terminate all tasks | -| --overwrite | n/a | Overwrite an existing release | -| --no-docker | n/a | Do not build docker images | -| --no-artifactory | n/a | Do not upload artifacts to Artifactory | -| --no-artifactory-build-info| n/a | Do not upload build info objects to Artifactory | -| --no-artifactory-upload | n/a | Do not upload artifacts to Artifactory (dry-run) | -| --no-testing | n/a | Do not execute test scripts | -| --no-parallel | n/a | Do not execute tests in parallel | -| --no-rewrite | n/a | Do not rewrite paths and URLs in output files | -| DELIVERY_FILE | n/a | STASIS delivery file | +| Long Option | Short Option | Purpose | +|:----------------------------|:------------:|:---------------------------------------------------------------| +| --help | -h | Display usage statement | +| --version | -V | Display program version | +| --continue-on-error | -C | Allow tests to fail | +| --config ARG | -c ARG | Read STASIS configuration file | +| --cpu-limit ARG | -l ARG | Number of processes to spawn concurrently (default: cpus - 1) | +| --pool-status-interval ARG | n/a | Report task status every n seconds (default: 30) | +| --python ARG | -p ARG | Override version of Python in configuration | +| --verbose | -v | Increase output verbosity | +| --unbuffered | -U | Disable line buffering | +| --update-base | n/a | Update conda installation prior to STATIS environment creation | +| --fail-fast | n/a | On test error, terminate all tasks | +| --task-timeout ARG | n/a | Terminate task after timeout is reached (#s, #m, #h) | +| --overwrite | n/a | Overwrite an existing release | +| --no-docker | n/a | Do not build docker images | +| --no-artifactory | n/a | Do not upload artifacts to Artifactory | +| --no-artifactory-build-info | n/a | Do not upload build info objects to Artifactory | +| --no-artifactory-upload | n/a | Do not upload artifacts to Artifactory (dry-run) | +| --no-testing | n/a | Do not execute test scripts | +| --no-parallel | n/a | Do not execute tests in parallel | +| --no-task-logging | n/a | Do not log task output (write to stdout) | +| --no-rewrite | n/a | Do not rewrite paths and URLs in output files | +| DELIVERY_FILE | n/a | STASIS delivery file | ## Indexer Command Line Options -- cgit