From 59cbd54b974bccd5093f0cdd65c62258b8acc624 Mon Sep 17 00:00:00 2001 From: Vratislav Podzimek Date: Thu, 22 Apr 2021 13:17:22 +0200 Subject: [PATCH 1/2] Add MapRemoveSoft() function Allowing values being removed from the map without being destroyed (keys are always owned by the map and thus destroyed). Ticket: CFE-3572 Changelog: None --- libutils/array_map.c | 20 ++++++++++++++++++++ libutils/array_map_priv.h | 1 + libutils/hash_map.c | 33 +++++++++++++++++++++++++++++++++ libutils/hash_map_priv.h | 1 + libutils/map.c | 14 ++++++++++++++ libutils/map.h | 6 ++++++ tests/unit/map_test.c | 24 ++++++++++++++++++++++++ 7 files changed, 99 insertions(+) diff --git a/libutils/array_map.c b/libutils/array_map.c index 3ea5092f..ee0553a6 100644 --- a/libutils/array_map.c +++ b/libutils/array_map.c @@ -86,6 +86,26 @@ bool ArrayMapRemove(ArrayMap *map, const void *key) return false; } +bool ArrayMapRemoveSoft(ArrayMap *map, const void *key) +{ + assert(map != NULL); + + for (int i = 0; i < map->size; ++i) + { + if (map->equal_fn(map->values[i].key, key)) + { + map->destroy_key_fn(map->values[i].key); + + memmove(map->values + i, map->values + i + 1, + sizeof(MapKeyValue) * (map->size - i - 1)); + + map->size--; + return true; + } + } + return false; +} + MapKeyValue *ArrayMapGet(const ArrayMap *map, const void *key) { for (int i = 0; i < map->size; ++i) diff --git a/libutils/array_map_priv.h b/libutils/array_map_priv.h index cb3d7fbe..3e9067ef 100644 --- a/libutils/array_map_priv.h +++ b/libutils/array_map_priv.h @@ -55,6 +55,7 @@ ArrayMap *ArrayMapNew(MapKeyEqualFn equal_fn, int ArrayMapInsert(ArrayMap *map, void *key, void *value); bool ArrayMapRemove(ArrayMap *map, const void *key); +bool ArrayMapRemoveSoft(ArrayMap *map, const void *key); MapKeyValue *ArrayMapGet(const ArrayMap *map, const void *key); void ArrayMapClear(ArrayMap *map); void ArrayMapSoftDestroy(ArrayMap *map); diff --git a/libutils/hash_map.c b/libutils/hash_map.c index 020c4f7a..0f2e61ea 100644 --- a/libutils/hash_map.c +++ b/libutils/hash_map.c @@ -172,6 +172,39 @@ bool HashMapRemove(HashMap *map, const void *key) return false; } +bool HashMapRemoveSoft(HashMap *map, const void *key) +{ + assert(map != NULL); + + unsigned bucket = HashMapGetBucket(map, key); + + /* + * prev points to a previous "next" pointer to rewrite it in case value need + * to be deleted + */ + + for (BucketListItem **prev = &map->buckets[bucket]; + *prev != NULL; + prev = &((*prev)->next)) + { + BucketListItem *cur = *prev; + if (map->equal_fn(cur->value.key, key)) + { + map->destroy_key_fn(cur->value.key); + *prev = cur->next; + free(cur); + map->load--; + if ((map->load < map->min_threshold) && (map->size > map->init_size)) + { + HashMapResize(map, map->size >> 1); + } + return true; + } + } + + return false; +} + MapKeyValue *HashMapGet(const HashMap *map, const void *key) { unsigned bucket = HashMapGetBucket(map, key); diff --git a/libutils/hash_map_priv.h b/libutils/hash_map_priv.h index fe3e6ad4..054af143 100644 --- a/libutils/hash_map_priv.h +++ b/libutils/hash_map_priv.h @@ -63,6 +63,7 @@ HashMap *HashMapNew(MapHashFn hash_fn, MapKeyEqualFn equal_fn, bool HashMapInsert(HashMap *map, void *key, void *value); bool HashMapRemove(HashMap *map, const void *key); +bool HashMapRemoveSoft(HashMap *map, const void *key); MapKeyValue *HashMapGet(const HashMap *map, const void *key); void HashMapClear(HashMap *map); void HashMapSoftDestroy(HashMap *map); diff --git a/libutils/map.c b/libutils/map.c index 786f7399..c831d40a 100644 --- a/libutils/map.c +++ b/libutils/map.c @@ -237,6 +237,20 @@ bool MapRemove(Map *map, const void *key) } } +bool MapRemoveSoft(Map *map, const void *key) +{ + assert(map != NULL); + + if (IsArrayMap(map)) + { + return ArrayMapRemoveSoft(map->arraymap, key); + } + else + { + return HashMapRemoveSoft(map->hashmap, key); + } +} + void MapClear(Map *map) { assert(map != NULL); diff --git a/libutils/map.h b/libutils/map.h index d5a04f08..5ce4bf69 100644 --- a/libutils/map.h +++ b/libutils/map.h @@ -63,6 +63,12 @@ void *MapGet(Map *map, const void *key); */ bool MapRemove(Map *map, const void *key); +/* + * Remove key/value pair from the map without destroying the value. Returns + * 'true' if key was present in the map. + */ +bool MapRemoveSoft(Map *map, const void *key); + size_t MapSize(const Map *map); /* diff --git a/tests/unit/map_test.c b/tests/unit/map_test.c index 4e93dc20..2e98ab0d 100644 --- a/tests/unit/map_test.c +++ b/tests/unit/map_test.c @@ -116,6 +116,29 @@ static void test_remove(void) HashMapDestroy(hashmap); } +static void test_remove_soft(void) +{ + HashMap *hashmap = HashMapNew(ConstHash, StringEqual_untyped, free, free, + HASH_MAP_INIT_SIZE); + + char *key = xstrdup("a"); + char *value = xstrdup("b"); + HashMapInsert(hashmap, key, value); + + MapKeyValue *item = HashMapGet(hashmap, "a"); + assert_string_equal(item->key, "a"); + assert_string_equal(item->value, "b"); + + /* Soft-remove, the value should still be available afterwards and needs to + * be free()'d explicitly. */ + assert_true(HashMapRemoveSoft(hashmap, "a")); + assert_int_equal(HashMapGet(hashmap, "a"), NULL); + assert_string_equal(value, "b"); + free(value); + + HashMapDestroy(hashmap); +} + static void test_add_n_as_to_map(HashMap *hashmap, unsigned int i) { char s[i+1]; @@ -628,6 +651,7 @@ int main() unit_test(test_insert), unit_test(test_insert_jumbo), unit_test(test_remove), + unit_test(test_remove_soft), unit_test(test_grow), unit_test(test_shrink), unit_test(test_no_shrink_below_init_size), From 042b7b9492a9030bf86fd5e66961a14c25c1b16e Mon Sep 17 00:00:00 2001 From: Vratislav Podzimek Date: Tue, 20 Apr 2021 12:03:32 +0200 Subject: [PATCH 2/2] Add a process manager "class" In many places there's a need to keep track of sub-processes spawned by the main process. Such processes need to be looked up by various parameters and interacted with. Having a common, reliable and fast implementation with high test coverage that holds the various pieces of information about sub-processes together is beneficial. In the future, we should extend this new API with the process spawning and termination functionality. Ticket: CFE-3572 Changelog: None --- libutils/Makefile.am | 1 + libutils/proc_manager.c | 618 ++++++++++++++++ libutils/proc_manager.h | 139 ++++ tests/unit/Makefile.am | 4 +- .../unit/proc_manager_force_terminate_test.c | 130 ++++ tests/unit/proc_manager_test.c | 660 ++++++++++++++++++ 6 files changed, 1551 insertions(+), 1 deletion(-) create mode 100644 libutils/proc_manager.c create mode 100644 libutils/proc_manager.h create mode 100644 tests/unit/proc_manager_force_terminate_test.c create mode 100644 tests/unit/proc_manager_test.c diff --git a/libutils/Makefile.am b/libutils/Makefile.am index 6974ee73..b78c88dd 100644 --- a/libutils/Makefile.am +++ b/libutils/Makefile.am @@ -63,6 +63,7 @@ libutils_la_SOURCES = \ platform.h condition_macros.h \ printsize.h \ proc_keyvalue.c proc_keyvalue.h \ + proc_manager.c proc_manager.h \ queue.c queue.h \ rb-tree.c rb-tree.h \ refcount.c refcount.h \ diff --git a/libutils/proc_manager.c b/libutils/proc_manager.c new file mode 100644 index 00000000..26842c31 --- /dev/null +++ b/libutils/proc_manager.c @@ -0,0 +1,618 @@ +/* + Copyright 2021 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#include + +#include /* Map* */ +#include /* xmalloc, xasprintf */ +#include /* StringHash_untyped, StringEqual_untyped */ +#include +#include + +#ifndef __MINGW32__ +#include +#include /* kill, SIGKILL */ +#endif + +#ifdef __MINGW32__ +#define fileno _fileno +#endif + +#define SIGKILL_TERMINATION_TIMEOUT 5 /* seconds */ + +struct ProcManager_ { + Map *procs_by_id; /** primary storage for the subprocesses info (owns the data) */ + Map *procs_by_fd; /** a different view on the data to speed up lookup by FD/stream */ + Map *procs_by_pid; /** a different view on the data to speed up lookup by PID */ +}; + +static unsigned int HashFD_untyped(const void *fd, ARG_UNUSED unsigned int seed) +{ + assert(fd != NULL); + return *((unsigned int*) fd); +} + +static bool FDEqual_untyped(const void *fd1, const void *fd2) +{ + assert((fd1 != NULL) && (fd2 != NULL)); + return (*((int*) fd1) == *((int*) fd2)); +} + +static unsigned int HashPID_untyped(const void *pid, ARG_UNUSED unsigned int seed) +{ + assert(pid != NULL); + return *((unsigned int*) pid); +} + +static bool PIDEqual_untyped(const void *pid1, const void *pid2) +{ + assert((pid1 != NULL) && (pid2 != NULL)); + return (*((pid_t*) pid1) == *((pid_t*) pid2)); +} + +static void NoopDestroy(ARG_UNUSED void *p) +{ + /* not destroying anything here */ +} + +ProcManager *ProcManagerNew() +{ + ProcManager *manager = xmalloc(sizeof(ProcManager)); + + /* keys are just pointers into data, this map actually holds the data */ + manager->procs_by_id = MapNew(StringHash_untyped, StringEqual_untyped, + NoopDestroy, (MapDestroyDataFn) SubprocessDestroy); + + /* keys are allocated, data belongs to the procs_by_id map */ + manager->procs_by_fd = MapNew(HashFD_untyped, FDEqual_untyped, + free, NoopDestroy); + + /* keys are just pointers into data, data belongs to the procs_by_id map */ + manager->procs_by_pid = MapNew(HashPID_untyped, PIDEqual_untyped, + NoopDestroy, NoopDestroy); + + return manager; +} + +void ProcManagerDestroy(ProcManager *manager) +{ + if (manager != NULL) + { + /* procs_by_id owns the data */ + MapSoftDestroy(manager->procs_by_fd); + MapSoftDestroy(manager->procs_by_pid); + MapDestroy(manager->procs_by_id); + free(manager); + } +} + +static inline Subprocess *SubprocessNew(char *id, char *cmd, char *description, + pid_t pid, FILE *input, FILE *output, char lookup_io) +{ + Subprocess *ret = xmalloc(sizeof(Subprocess)); + ret->id = id; + ret->cmd = cmd; + ret->description = description; + ret->pid = pid; + ret->input = input; + ret->output = output; + ret->lookup_io = lookup_io; + + return ret; +} + +void SubprocessDestroy(Subprocess *proc) +{ + if (proc != NULL) + { + free(proc->id); + free(proc->cmd); + free(proc->description); + free(proc); + } +} + +static inline int *DupInt(int val) +{ + int *ret = xmalloc(sizeof(int)); + *ret = val; + return ret; +} + +bool ProcManagerAddProcess(ProcManager *manager, + char *id, char *cmd, char *description, pid_t pid, + FILE *input, FILE *output, char lookup_io) +{ + assert(manager != NULL); + + if (id == NULL) + { + NDEBUG_UNUSED int ret = xasprintf(&id, "%jd", (intmax_t) pid); + assert(ret > 0); + } + + int *lookup_fd = NULL; + if (lookup_io == 'i') + { + assert(input != NULL); + int fd = fileno(input); + if (fd >= 0) + { + lookup_fd = DupInt(fd); + } + else + { + Log(LOG_LEVEL_ERR, "Failed to get FD for input file stream for the process '%s'", + description != NULL ? description : id); + } + } + else if (lookup_io == 'o') + { + assert(output != NULL); + int fd = fileno(output); + if (fd >= 0) + { + lookup_fd = DupInt(fd); + } + else + { + Log(LOG_LEVEL_ERR, "Failed to get FD for output file stream for the process '%s'", + description != NULL ? description : id); + } + } + else + { + assert(lookup_io == '\0'); + } + + if (MapHasKey(manager->procs_by_id, id) || + MapHasKey(manager->procs_by_pid, &pid) || + ((lookup_fd != NULL) && MapHasKey(manager->procs_by_fd, lookup_fd))) + { + Log(LOG_LEVEL_ERR, + "Attempt to insert the process '%s:%jd:%d' ['%s'] ('%s')' twice into the same process manager", + id, (intmax_t) pid, *lookup_fd, description, cmd); + + free(id); + free(cmd); + free(description); + free(lookup_fd); + + return false; + } + + Subprocess *proc = SubprocessNew(id, cmd, description, pid, input, output, lookup_io); + MapInsert(manager->procs_by_id, id, proc); + MapInsert(manager->procs_by_pid, &(proc->pid), proc); + + if (lookup_fd != NULL) + { + MapInsert(manager->procs_by_fd, lookup_fd, proc); + } + + return true; +} + + +Subprocess *ProcManagerGetProcessByPID(ProcManager *manager, pid_t pid) +{ + assert(manager != NULL); + return (Subprocess*) MapGet(manager->procs_by_pid, &pid); +} + +Subprocess *ProcManagerGetProcessById(ProcManager *manager, const char *id) +{ + assert(manager != NULL); + return (Subprocess*) MapGet(manager->procs_by_id, id); +} + +Subprocess *ProcManagerGetProcessByFD(ProcManager *manager, int fd) +{ + assert(manager != NULL); + return (Subprocess*) MapGet(manager->procs_by_fd, &fd); +} + +Subprocess *ProcManagerGetProcessByStream(ProcManager *manager, FILE *stream) +{ + assert(manager != NULL); + int fd = fileno(stream); + return (Subprocess*) MapGet(manager->procs_by_fd, &fd); +} + + +static inline void SoftRemoveProcFromMaps(ProcManager *manager, Subprocess *proc) +{ + assert(manager != NULL); + assert(proc != NULL); + + MapRemoveSoft(manager->procs_by_id, proc->id); + MapRemoveSoft(manager->procs_by_pid, &(proc->pid)); + + int lookup_fd = -1; + if (proc->lookup_io == 'i') + { + lookup_fd = fileno(proc->input); + assert(lookup_fd != -1); + } + else if (proc->lookup_io == 'o') + { + lookup_fd = fileno(proc->output); + assert(lookup_fd != -1); + } + + if (lookup_fd != -1) + { + MapRemoveSoft(manager->procs_by_fd, &lookup_fd); + } +} + +Subprocess *ProcManagerPopProcessByPID(ProcManager *manager, pid_t pid) +{ + assert(manager != NULL); + + Subprocess *proc = MapGet(manager->procs_by_pid, &pid); + SoftRemoveProcFromMaps(manager, proc); + return proc; +} + +Subprocess *ProcManagerPopProcessById(ProcManager *manager, const char *id) +{ + assert(manager != NULL); + + Subprocess *proc = MapGet(manager->procs_by_id, id); + SoftRemoveProcFromMaps(manager, proc); + return proc; +} + +Subprocess *ProcManagerPopProcessByFD(ProcManager *manager, int fd) +{ + assert(manager != NULL); + + Subprocess *proc = MapGet(manager->procs_by_fd, &fd); + SoftRemoveProcFromMaps(manager, proc); + return proc; +} + +Subprocess *ProcManagerPopProcessByStream(ProcManager *manager, FILE *stream) +{ + assert(manager != NULL); + + int fd = fileno(stream); + Subprocess *proc = MapGet(manager->procs_by_fd, &fd); + SoftRemoveProcFromMaps(manager, proc); + return proc; +} + + +/** + * @param lookup_fd needed because proc->input and proc->output are expected to be already closed + * and thus not valid for fileno() + */ +static inline void RemoveProcFromMaps(ProcManager *manager, Subprocess *proc, int lookup_fd) +{ + assert(manager != NULL); + assert(proc != NULL); + + MapRemoveSoft(manager->procs_by_pid, &(proc->pid)); + + if (lookup_fd != -1) + { + MapRemoveSoft(manager->procs_by_fd, &lookup_fd); + } + MapRemove(manager->procs_by_id, proc->id); +} + +static bool ForceTermination(Subprocess *proc) +{ + assert(proc != NULL); + + if (proc->input != NULL) + { + int ret = fclose(proc->input); + if (ret != 0) + { + Log(LOG_LEVEL_ERR, "Failed to close input to the process '%s:%jd' (FD: %d)", + proc->id, (intmax_t) proc->pid, fileno(proc->input)); + } + } + if (proc->output != NULL) + { + int ret = fclose(proc->output); + if (ret != 0) + { + Log(LOG_LEVEL_ERR, "Failed to close output from the process '%s:%jd' (FD: %d)", + proc->id, (intmax_t) proc->pid, fileno(proc->output)); + } + } + +#ifndef __MINGW32__ + int ret = kill(proc->pid, SIGKILL); + if (ret != 0) + { + Log(LOG_LEVEL_ERR, "Failed to send SIGKILL to the process '%s:%jd'", + proc->id, (intmax_t) proc->pid); + } + + bool retry = true; + const int started = time(NULL); + while (retry) + { + pid_t pid = waitpid(proc->pid, NULL, WNOHANG); + if (pid > 0) + { + /* successfully reaped */ + retry = false; + } + else if (pid == 0) + { + /* still not terminated */ + int now = time(NULL); + if ((now - started) > SIGKILL_TERMINATION_TIMEOUT) + { + Log(LOG_LEVEL_ERR, "Failed to terminate process '%s:%jd'", + proc->id, (intmax_t) proc->pid); + return false; + } + /* else retry */ + } + else if ((pid == -1) && (errno != EINTR)) + { + Log(LOG_LEVEL_ERR, "Failed to wait for the process '%s:%jd'", + proc->id, (intmax_t) proc->pid); + return false; + } + /* else retry */ + } + return true; +#else + /* TODO: This will require spawning to happen via the ProcManager too. */ + Log(LOG_LEVEL_NOTICE, "Forceful termination of processes not implemented on Windows"); + return false; +#endif +} + +bool ProcManagerTerminateProcessByPID(ProcManager *manager, pid_t pid, + ProcessTerminator Terminator, void *data) +{ + assert(manager != NULL); + + Subprocess *proc = ProcManagerGetProcessByPID(manager, pid); + if (proc == NULL) + { + Log(LOG_LEVEL_ERR, "No process with PID '%jd' to terminate", (intmax_t) pid); + return false; + } + + /* Determine lookup_fd now before termination closes the streams. */ + int lookup_fd = -1; + if (proc->lookup_io == 'i') + { + lookup_fd = fileno(proc->input); + assert(lookup_fd != -1); + } + else if (proc->lookup_io == 'o') + { + lookup_fd = fileno(proc->output); + assert(lookup_fd != -1); + } + + bool terminated = Terminator(proc, data); + if (!terminated) + { + Log(LOG_LEVEL_NOTICE, "Failed to terminate the process '%s:%jd' gracefully", + proc->id, (intmax_t) proc->pid); + terminated = ForceTermination(proc); + } + + if (terminated) + { + RemoveProcFromMaps(manager, proc, lookup_fd); + } + + return terminated; +} + +bool ProcManagerTerminateProcessById(ProcManager *manager, const char *id, + ProcessTerminator Terminator, void *data) +{ + assert(manager != NULL); + + Subprocess *proc = ProcManagerGetProcessById(manager, id); + if (proc == NULL) + { + Log(LOG_LEVEL_ERR, "No process with ID '%s' to terminate", id); + return false; + } + + /* Determine lookup_fd now before termination closes the streams. */ + int lookup_fd = -1; + if (proc->lookup_io == 'i') + { + lookup_fd = fileno(proc->input); + assert(lookup_fd != -1); + } + else if (proc->lookup_io == 'o') + { + lookup_fd = fileno(proc->output); + assert(lookup_fd != -1); + } + + bool terminated = Terminator(proc, data); + if (!terminated) + { + Log(LOG_LEVEL_NOTICE, "Failed to terminate the process '%s:%jd' gracefully", + proc->id, (intmax_t) proc->pid); + terminated = ForceTermination(proc); + } + + if (terminated) + { + RemoveProcFromMaps(manager, proc, lookup_fd); + } + + return terminated; +} + +bool ProcManagerTerminateProcessByStream(ProcManager *manager, FILE *stream, + ProcessTerminator Terminator, void *data) +{ + assert(manager != NULL); + + Subprocess *proc = ProcManagerGetProcessByStream(manager, stream); + if (proc == NULL) + { + Log(LOG_LEVEL_ERR, "No process to terminate found for given stream (fd: %d)", + fileno(stream)); + return false; + } + + /* Determine lookup_fd now before termination closes the streams. */ + int lookup_fd = -1; + if (proc->lookup_io == 'i') + { + lookup_fd = fileno(proc->input); + assert(lookup_fd != -1); + } + else if (proc->lookup_io == 'o') + { + lookup_fd = fileno(proc->output); + assert(lookup_fd != -1); + } + + bool terminated = Terminator(proc, data); + if (!terminated) + { + Log(LOG_LEVEL_NOTICE, "Failed to terminate the process '%s:%jd' gracefully", + proc->id, (intmax_t) proc->pid); + terminated = ForceTermination(proc); + } + + if (terminated) + { + RemoveProcFromMaps(manager, proc, lookup_fd); + } + + return terminated; +} + +bool ProcManagerTerminateProcessByFD(ProcManager *manager, int fd, + ProcessTerminator Terminator, void *data) +{ + assert(manager != NULL); + + Subprocess *proc = ProcManagerGetProcessByFD(manager, fd); + if (proc == NULL) + { + Log(LOG_LEVEL_ERR, "No process to terminate found for FD %d", fd); + return false; + } + + /* Determine lookup_fd now before termination closes the streams. */ + int lookup_fd = -1; + if (proc->lookup_io == 'i') + { + lookup_fd = fileno(proc->input); + assert(lookup_fd != -1); + } + else if (proc->lookup_io == 'o') + { + lookup_fd = fileno(proc->output); + assert(lookup_fd != -1); + } + + bool terminated = Terminator(proc, data); + if (!terminated) + { + Log(LOG_LEVEL_NOTICE, "Failed to terminate the process '%s:%jd' gracefully", + proc->id, (intmax_t) proc->pid); + terminated = ForceTermination(proc); + } + + if (terminated) + { + RemoveProcFromMaps(manager, proc, lookup_fd); + } + + return terminated; +} + +bool ProcManagerTerminateAllProcesses(ProcManager *manager, + ProcessTerminator Terminator, void *data) +{ + assert(manager != NULL); + + /* Avoid removing items from the map while iterating over it. */ + Seq *to_remove = SeqNew(MapSize(manager->procs_by_id), NULL); + + bool all_terminated = true; + MapIterator iter = MapIteratorInit(manager->procs_by_id); + MapKeyValue *item; + while ((item = MapIteratorNext(&iter)) != NULL) + { + Subprocess *proc = item->value; + + /* Determine lookup_fd now before termination closes the streams. */ + int lookup_fd = -1; + if (proc->lookup_io == 'i') + { + lookup_fd = fileno(proc->input); + assert(lookup_fd != -1); + } + else if (proc->lookup_io == 'o') + { + lookup_fd = fileno(proc->output); + assert(lookup_fd != -1); + } + + bool terminated = Terminator(proc, data); + if (!terminated) + { + Log(LOG_LEVEL_NOTICE, "Failed to terminate the process '%s:%jd' gracefully", + proc->id, (intmax_t) proc->pid); + terminated = ForceTermination(proc); + } + + if (terminated) + { + SeqAppend(to_remove, proc); + + /* Remove from map by FD here so that we don't have to remember the + * FDs anywhere. */ + MapRemoveSoft(manager->procs_by_fd, &lookup_fd); + } + else + { + all_terminated = false; + } + } + + const size_t length = SeqLength(to_remove); + for (size_t i = 0; i < length; i++) + { + RemoveProcFromMaps(manager, (Subprocess *) SeqAt(to_remove, i), -1); + } + SeqDestroy(to_remove); + + return all_terminated; +} diff --git a/libutils/proc_manager.h b/libutils/proc_manager.h new file mode 100644 index 00000000..5354b569 --- /dev/null +++ b/libutils/proc_manager.h @@ -0,0 +1,139 @@ +/* + Copyright 2021 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#ifndef CFENGINE_PROC_MANAGER_H +#define CFENGINE_PROC_MANAGER_H + +#include + +/** + * Struct to hold information about a subprocess. + */ +struct Subprocess_ { + char *id; /** unique ID of the subprocess [always not %NULL] */ + char *cmd; /** command used to create the subprocess [can be %NULL] */ + char *description; /** human-friendly description of the subprocess [can be %NULL] */ + pid_t pid; /** PID of the subprocess [always >= 0] */ + FILE *input; /** stream of the input to the subprocess [can be %NULL] */ + FILE *output; /** stream of the output from the subprocess [can be %NULL] */ + char lookup_io; /** which stream/FD to use for lookup by stream/FD ['i', 'o' or '\0'] */ +}; +typedef struct Subprocess_ Subprocess; + +/** + * Destroy a subprocess. + */ +void SubprocessDestroy(Subprocess *proc); + +typedef struct ProcManager_ ProcManager; + +/** + * Function to terminate a subprocess. + * + * @param proc the subprocess to terminate + * @param data custom data passed to the function through the termination functions + * @return whether the termination was successful or not + */ +typedef bool (*ProcessTerminator) (Subprocess *proc, void *data); + +/** + * Get a new ProcManager. + */ +ProcManager *ProcManagerNew(); + +/** + * Destroy a ProcManager. + */ +void ProcManagerDestroy(); + +/** + * Add a subprocess to a ProcManager. + * + * @param manager the ProcManager to add the process to [cannot be %NULL] + * @param id ID of the process to add [string(pid) is used if %NULL] + * @param cmd command used to create the subprocess [can be %NULL] + * @param description human-friendly description of the subprocess [can be %NULL] + * @param pid PID of the subprocess [required to be >=0] + * @param input stream of the input to the subprocess [can be %NULL] + * @param output stream of the output from the subprocess [-1 if N/A] + * @param lookup_io which stream/FD to use for lookup by stream/FD ['i', 'o' or '\0'] + * + * @return whether the subprocess was successfully added or not (e.g. in case of + * the process with the same id, PID or lookup stream/FD added before) + * + * @note Ownership of #id, #cmd and #description is transferred to the #manager. + */ +bool ProcManagerAddProcess(ProcManager *manager, + char *id, char *cmd, char *description, pid_t pid, + FILE *input, FILE *output, char lookup_io); + +/** + * Getter functions for #ProcManager. + * + * @return %NULL if not found + */ +Subprocess *ProcManagerGetProcessByPID(ProcManager *manager, pid_t pid); +Subprocess *ProcManagerGetProcessById(ProcManager *manager, const char *id); +Subprocess *ProcManagerGetProcessByStream(ProcManager *manager, FILE *stream); +Subprocess *ProcManagerGetProcessByFD(ProcManager *manager, int fd); + +/** + * Functions to get and remove subprocesses from a #ProcManager. + * + * @return %NULL if not found + * @note Destroy the returned #Subprocess with SubprocessDestroy(). + */ +Subprocess *ProcManagerPopProcessByPID(ProcManager *manager, pid_t pid); +Subprocess *ProcManagerPopProcessById(ProcManager *manager, const char *id); +Subprocess *ProcManagerPopProcessByStream(ProcManager *manager, FILE *stream); +Subprocess *ProcManagerPopProcessByFD(ProcManager *manager, int fd); + +/** + * Termination functions for ProcManager. + * + * @param Terminator terminator function called on the matched process(es) + * @param data arbitrary data passed to the #Terminator function + * + * @return %true if terminated successfully, %false if not (e.g. when not found, errors are logged) + * + * @note If the #Terminator function returns %false, the default hard termination is + * attempted (closing FDs and doing kill -9). Once the subprocess is terminated, + * or the hard termination is attempted, the subprocess is removed from the + * #ProcManager. + */ +bool ProcManagerTerminateProcessByPID(ProcManager *manager, pid_t pid, + ProcessTerminator Terminator, void *data); +bool ProcManagerTerminateProcessById(ProcManager *manager, const char *id, + ProcessTerminator Terminator, void *data); +bool ProcManagerTerminateProcessByStream(ProcManager *manager, FILE *stream, + ProcessTerminator Terminator, void *data); +bool ProcManagerTerminateProcessByFD(ProcManager *manager, int fd, + ProcessTerminator Terminator, void *data); +bool ProcManagerTerminateAllProcesses(ProcManager *manager, + ProcessTerminator Terminator, void *data); + +/* TODO: */ +/* Functions for spawning processes (see cfengine/core/libpromises/pipes.h) */ + +#endif /* CFENGINE_PROC_MANAGER_H */ diff --git a/tests/unit/Makefile.am b/tests/unit/Makefile.am index 59f4e5d1..5715ea8e 100644 --- a/tests/unit/Makefile.am +++ b/tests/unit/Makefile.am @@ -108,7 +108,9 @@ check_PROGRAMS = \ version_comparison_test \ ring_buffer_test \ libcompat_test \ - definitions_test + definitions_test \ + proc_manager_test \ + proc_manager_force_terminate_test if WITH_OPENSSL check_PROGRAMS += \ diff --git a/tests/unit/proc_manager_force_terminate_test.c b/tests/unit/proc_manager_force_terminate_test.c new file mode 100644 index 00000000..25e77dda --- /dev/null +++ b/tests/unit/proc_manager_force_terminate_test.c @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +/** + * This file contains ProcManager tests that need to fork() a child process + * which totally confuses the unit test framework used by the other tests. + */ + +static bool failure = false; + +#define assert_int_equal(expr1, expr2) \ + if ((expr1) != (expr2)) \ + { \ + fprintf(stderr, "FAIL: "#expr1" != "#expr2" [%d != %d] (%s:%d)\n", (expr1), (expr2), __FILE__, __LINE__); \ + failure = true; \ + } + +#define assert_true(expr) \ + if (!(expr)) \ + { \ + fprintf(stderr, "FAIL: "#expr" is FALSE (%s:%d)\n", __FILE__, __LINE__); \ + failure = true; \ + } + +#define assert_false(expr) \ + if ((expr)) \ + { \ + fprintf(stderr, "FAIL: "#expr" is TRUE (%s:%d)\n", __FILE__, __LINE__); \ + failure = true; \ + } + +static bool TerminateWithSIGTERM(Subprocess *proc, void *data) +{ + assert_true(proc->pid > 0); + + /* Not closing the pipes here, the force-termination should do that after + * this graceful termination fails. */ + int ret = kill(proc->pid, SIGTERM); + assert_int_equal(ret, 0); + + /* Give child time to terminate (it shouldn't). */ + sleep(1); + ret = waitpid(proc->pid, NULL, WNOHANG); + assert_int_equal(ret, 0); + + bool *failed = data; + *failed = true; + + /* Attempt failed, leave the process to the force-termination. */ + return false; +} + +int main() +{ + int parent_write_child_read[2]; + int ret = pipe(parent_write_child_read); + assert_int_equal(ret, 0); + int parent_read_child_write[2]; + ret = pipe(parent_read_child_write); + assert_int_equal(ret, 0); + + pid_t pid = fork(); + assert_true(pid >= 0); + + if (pid == 0) + { + /* child */ + close(parent_write_child_read[1]); + close(parent_read_child_write[0]); + + /* Also close the ends the child should normally use, we don't need them. */ + close(parent_write_child_read[0]); + close(parent_read_child_write[1]); + + /* Ignore SIGTERM */ + signal(SIGTERM, SIG_IGN); + + /* Wait to be killed. */ + sleep(3600); + } + /* parent just continues below */ + + close(parent_write_child_read[0]); + close(parent_read_child_write[1]); + + /* Give child time to start ignoring SIGTERM */ + sleep(1); + + FILE *input = fdopen(parent_write_child_read[1], "w"); + assert_true(input != NULL); + FILE *output = fdopen(parent_read_child_write[0], "r"); + assert_true(output != NULL); + + ProcManager *manager = ProcManagerNew(); + bool success = ProcManagerAddProcess(manager, + xstrdup("test-process"), xstrdup("/some/cmd arg1"), xstrdup("test process"), + pid, input, output, 'o'); + assert_true(success); + + bool graceful_termination_failed = false;; + success = ProcManagerTerminateProcessByFD(manager, fileno(output), + TerminateWithSIGTERM, &graceful_termination_failed); + assert_true(success); + assert_true(graceful_termination_failed); + + ret = kill(pid, 0); + assert_int_equal(ret, -1); /* there should be no such process anymore */ + + ProcManagerDestroy(manager); + + if (failure) + { + fprintf(stderr, "FAILED\n"); + return 1; + } + else + { + fprintf(stderr, "SUCCESS\n"); + return 0; + } +} diff --git a/tests/unit/proc_manager_test.c b/tests/unit/proc_manager_test.c new file mode 100644 index 00000000..9e68c89a --- /dev/null +++ b/tests/unit/proc_manager_test.c @@ -0,0 +1,660 @@ +/* + Copyright 2021 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the + Free Software Foundation; version 3. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#include + +#include +#include +#include +#include + +#include + +#if defined(__ANDROID__) +# define TEMP_DIR "/data/data/com.termux/files/usr/tmp" +#else +# define TEMP_DIR "/tmp/proc_manager_test" +#endif +#define TEST_FILE "proc_manager_test.txt" + +static void test_add_get_process_destroy(void) +{ + /* We just need some FILE streams. */ + FILE *input = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input != NULL); + FILE *output = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output != NULL); + + ProcManager *manager = ProcManagerNew(); + assert_true(manager != NULL); + + bool ret = ProcManagerAddProcess(manager, + xstrdup("test-process"), xstrdup("/some/command arg1 arg2"), xstrdup("test process"), + (pid_t) 1234, input, output, 'i'); + assert_true(ret); + + Subprocess *proc = ProcManagerGetProcessByPID(manager, 1234); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 1234); + assert_true(proc->input == input); + assert_true(proc->output == output); + + proc = ProcManagerGetProcessById(manager, "test-process"); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 1234); + assert_true(proc->input == input); + assert_true(proc->output == output); + + proc = ProcManagerGetProcessByStream(manager, input); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 1234); + assert_true(proc->input == input); + assert_true(proc->output == output); + + proc = ProcManagerGetProcessByFD(manager, fileno(input)); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 1234); + assert_true(proc->input == input); + assert_true(proc->output == output); + + /* lookup_io was 'i', so shouldn't be possible to get it from the *output* stream/FD */ + proc = ProcManagerGetProcessByStream(manager, output); + assert_true(proc == NULL); + proc = ProcManagerGetProcessByFD(manager, fileno(output)); + assert_true(proc == NULL); + + fclose(input); + fclose(output); + + ProcManagerDestroy(manager); +} + +static void test_add_get_process_multi_destroy(void) +{ + /* We just need some FILE streams. */ + FILE *input = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input != NULL); + FILE *output = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output != NULL); + + ProcManager *manager = ProcManagerNew(); + assert_true(manager != NULL); + + bool ret = ProcManagerAddProcess(manager, + xstrdup("test-process"), xstrdup("/some/command arg1 arg2"), xstrdup("test process"), + (pid_t) 0xAA, input, output, 'i'); + assert_true(ret); + + /* Add another process with a different spec and lookup_io set to 'o' this time. */ + FILE *input2 = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input2 != NULL); + FILE *output2 = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output2 != NULL); + + ret = ProcManagerAddProcess(manager, + xstrdup("test-process2"), xstrdup("/some/command2 arg1 arg2"), xstrdup("test process2"), + (pid_t) 0xAB, input2, output2, 'o'); + assert_true(ret); + + /* Add yet another process with a different spec, with a PID that should + * make a collision in the internal Map by PID */ + FILE *input3 = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input3 != NULL); + FILE *output3 = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output3 != NULL); + + ret = ProcManagerAddProcess(manager, + xstrdup("test-process3"), xstrdup("/some/command3 arg1 arg2"), xstrdup("test process3"), + (pid_t) 0x010000AB, input3, output3, 'o'); + assert_true(ret); + + Subprocess *proc = ProcManagerGetProcessByPID(manager, 0xAA); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 0xAA); + assert_true(proc->input == input); + assert_true(proc->output == output); + + proc = ProcManagerGetProcessById(manager, "test-process"); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 0xAA); + assert_true(proc->input == input); + assert_true(proc->output == output); + + proc = ProcManagerGetProcessByStream(manager, input); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 0xAA); + assert_true(proc->input == input); + assert_true(proc->output == output); + + proc = ProcManagerGetProcessByFD(manager, fileno(input)); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 0xAA); + assert_true(proc->input == input); + assert_true(proc->output == output); + + /* lookup_io was 'i', so shouldn't be possible to get it from the *output* stream/FD */ + proc = ProcManagerGetProcessByStream(manager, output); + assert_true(proc == NULL); + proc = ProcManagerGetProcessByFD(manager, fileno(output)); + assert_true(proc == NULL); + + + proc = ProcManagerGetProcessByPID(manager, 0xAB); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process2"); + assert_string_equal(proc->cmd, "/some/command2 arg1 arg2"); + assert_string_equal(proc->description, "test process2"); + assert_int_equal(proc->pid, 0xAB); + assert_true(proc->input == input2); + assert_true(proc->output == output2); + + proc = ProcManagerGetProcessById(manager, "test-process2"); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process2"); + assert_string_equal(proc->cmd, "/some/command2 arg1 arg2"); + assert_string_equal(proc->description, "test process2"); + assert_int_equal(proc->pid, 0xAB); + assert_true(proc->input == input2); + assert_true(proc->output == output2); + + proc = ProcManagerGetProcessByStream(manager, output2); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process2"); + assert_string_equal(proc->cmd, "/some/command2 arg1 arg2"); + assert_string_equal(proc->description, "test process2"); + assert_int_equal(proc->pid, 0xAB); + assert_true(proc->input == input2); + assert_true(proc->output == output2); + + proc = ProcManagerGetProcessByFD(manager, fileno(output2)); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process2"); + assert_string_equal(proc->cmd, "/some/command2 arg1 arg2"); + assert_string_equal(proc->description, "test process2"); + assert_int_equal(proc->pid, 0xAB); + assert_true(proc->input == input2); + assert_true(proc->output == output2); + + /* lookup_io was 'o', so shouldn't be possible to get it from the *input* stream/FD */ + proc = ProcManagerGetProcessByStream(manager, input2); + assert_true(proc == NULL); + proc = ProcManagerGetProcessByFD(manager, fileno(input2)); + assert_true(proc == NULL); + + + proc = ProcManagerGetProcessByPID(manager, 0x010000AB); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process3"); + assert_string_equal(proc->cmd, "/some/command3 arg1 arg2"); + assert_string_equal(proc->description, "test process3"); + assert_int_equal(proc->pid, 0x010000AB); + assert_true(proc->input == input3); + assert_true(proc->output == output3); + + proc = ProcManagerGetProcessById(manager, "test-process3"); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process3"); + assert_string_equal(proc->cmd, "/some/command3 arg1 arg2"); + assert_string_equal(proc->description, "test process3"); + assert_int_equal(proc->pid, 0x010000AB); + assert_true(proc->input == input3); + assert_true(proc->output == output3); + + proc = ProcManagerGetProcessByStream(manager, output3); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process3"); + assert_string_equal(proc->cmd, "/some/command3 arg1 arg2"); + assert_string_equal(proc->description, "test process3"); + assert_int_equal(proc->pid, 0x010000AB); + assert_true(proc->input == input3); + assert_true(proc->output == output3); + + proc = ProcManagerGetProcessByFD(manager, fileno(output3)); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process3"); + assert_string_equal(proc->cmd, "/some/command3 arg1 arg2"); + assert_string_equal(proc->description, "test process3"); + assert_int_equal(proc->pid, 0x010000AB); + assert_true(proc->input == input3); + assert_true(proc->output == output3); + + /* lookup_io was 'o', so shouldn't be possible to get it from the *input* stream/FD */ + proc = ProcManagerGetProcessByStream(manager, input3); + assert_true(proc == NULL); + proc = ProcManagerGetProcessByFD(manager, fileno(input3)); + assert_true(proc == NULL); + + + fclose(input); + fclose(output); + fclose(input2); + fclose(output2); + fclose(input3); + fclose(output3); + + ProcManagerDestroy(manager); +} + +static void test_pop(void) +{ + /* We just need some FILE streams. */ + FILE *input = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input != NULL); + FILE *output = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output != NULL); + + ProcManager *manager = ProcManagerNew(); + assert_true(manager != NULL); + + bool ret = ProcManagerAddProcess(manager, + xstrdup("test-process"), xstrdup("/some/command arg1 arg2"), xstrdup("test process"), + (pid_t) 1234, input, output, 'i'); + assert_true(ret); + + Subprocess *proc = ProcManagerPopProcessByPID(manager, 1234); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 1234); + assert_true(proc->input == input); + assert_true(proc->output == output); + + SubprocessDestroy(proc); + + /* The process should no longer be there. */ + proc = ProcManagerGetProcessById(manager, "test-process"); + assert_true(proc == NULL); + proc = ProcManagerGetProcessByStream(manager, input); + assert_true(proc == NULL); + proc = ProcManagerGetProcessByFD(manager, fileno(input)); + assert_true(proc == NULL); + proc = ProcManagerGetProcessByFD(manager, fileno(output)); + assert_true(proc == NULL); + + fclose(input); + fclose(output); + + ProcManagerDestroy(manager); +} + +static void test_add_process_twice(void) +{ + /* We just need some FILE streams. */ + FILE *input = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input != NULL); + FILE *output = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output != NULL); + + ProcManager *manager = ProcManagerNew(); + assert_true(manager != NULL); + + bool ret = ProcManagerAddProcess(manager, + xstrdup("test-process"), xstrdup("/some/command arg1 arg2"), xstrdup("test process"), + (pid_t) 1234, input, output, 'i'); + assert_true(ret); + + /* Now try to insert the process again (with a different command and + * description). This shouldn't be allowed. */ + ret = ProcManagerAddProcess(manager, + xstrdup("test-process"), xstrdup("/some/command2 arg1 arg2"), xstrdup("better test process"), + (pid_t) 1234, input, output, 'i'); + assert_false(ret); + + /* But the original process should still be there. */ + Subprocess *proc = ProcManagerGetProcessByPID(manager, 1234); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 1234); + assert_true(proc->input == input); + assert_true(proc->output == output); + + proc = ProcManagerGetProcessById(manager, "test-process"); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 1234); + assert_true(proc->input == input); + assert_true(proc->output == output); + + proc = ProcManagerGetProcessByStream(manager, input); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 1234); + assert_true(proc->input == input); + assert_true(proc->output == output); + + proc = ProcManagerGetProcessByFD(manager, fileno(input)); + assert_true(proc != NULL); + assert_string_equal(proc->id, "test-process"); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, 1234); + assert_true(proc->input == input); + assert_true(proc->output == output); + + /* lookup_io was 'i', so shouldn't be possible to get it from the *output* stream/FD */ + proc = ProcManagerGetProcessByStream(manager, output); + assert_true(proc == NULL); + proc = ProcManagerGetProcessByFD(manager, fileno(output)); + assert_true(proc == NULL); + + fclose(input); + fclose(output); + + ProcManagerDestroy(manager); +} + +void ItemDestroy_fclose(void *data) +{ + FILE *stream = data; + fclose(stream); +} + +static void test_add_get_process_many_destroy(void) +{ + ProcManager *manager = ProcManagerNew(); + assert_true(manager != NULL); + + const size_t n_procs = 150; + Seq *inputs = SeqNew(n_procs, ItemDestroy_fclose); + Seq *outputs = SeqNew(n_procs, ItemDestroy_fclose); + Seq *ids = SeqNew(n_procs, NULL); + + for (size_t i = 0; i < n_procs; i++) + { + FILE *input = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input != NULL); + SeqAppend(inputs, input); + FILE *output = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output != NULL); + SeqAppend(outputs, output); + char *id = NULL; + xasprintf(&id, "test-process%zd", i); + assert_true(id != NULL); + SeqAppend(ids, id); + + bool ret = ProcManagerAddProcess(manager, + id, xstrdup("/some/command arg1 arg2"), xstrdup("test process"), + (pid_t) 1234 + i, input, output, (i % 2) == 0 ? 'i' : 'o'); + assert_true(ret); + } + + for (size_t i = 0; i < n_procs; i++) + { + Subprocess *proc = ProcManagerGetProcessByPID(manager, (pid_t) 1234 + i); + assert_true(proc != NULL); + assert_string_equal(proc->id, SeqAt(ids, i)); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, (pid_t) 1234 + i); + assert_true(proc->input == SeqAt(inputs, i)); + assert_true(proc->output == SeqAt(outputs, i)); + + proc = ProcManagerGetProcessById(manager, SeqAt(ids, i)); + assert_true(proc != NULL); + assert_string_equal(proc->id, SeqAt(ids, i)); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, (pid_t) 1234 + i); + assert_true(proc->input == SeqAt(inputs, i)); + assert_true(proc->output == SeqAt(outputs, i)); + + proc = ProcManagerGetProcessByStream(manager, (i % 2) == 0 ? SeqAt(inputs, i) : SeqAt(outputs, i)); + assert_true(proc != NULL); + assert_string_equal(proc->id, SeqAt(ids, i)); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, (pid_t) 1234 + i); + assert_true(proc->input == SeqAt(inputs, i)); + assert_true(proc->output == SeqAt(outputs, i)); + + proc = ProcManagerGetProcessByFD(manager, fileno((i % 2) == 0 ? SeqAt(inputs, i) : SeqAt(outputs, i))); + assert_true(proc != NULL); + assert_string_equal(proc->id, SeqAt(ids, i)); + assert_string_equal(proc->cmd, "/some/command arg1 arg2"); + assert_string_equal(proc->description, "test process"); + assert_int_equal(proc->pid, (pid_t) 1234 + i); + assert_true(proc->input == SeqAt(inputs, i)); + assert_true(proc->output == SeqAt(outputs, i)); + + proc = ProcManagerGetProcessByStream(manager, (i % 2) == 0 ? SeqAt(outputs, i) : SeqAt(inputs, i)); + assert_true(proc == NULL); + proc = ProcManagerGetProcessByFD(manager, fileno((i % 2) == 0 ? SeqAt(outputs, i) : SeqAt(inputs, i))); + assert_true(proc == NULL); + } + + SeqDestroy(inputs); + SeqDestroy(outputs); + SeqDestroy(ids); + + ProcManagerDestroy(manager); +} + +static bool TestTerminator(Subprocess *proc, void *data) +{ + int *terminator_call_counter = data; + (*terminator_call_counter)++; + + /* just close the descriptor, we have fake data, no real process(es) */ + fclose(proc->input); + fclose(proc->output); + + return true; +} + +static void test_terminate(void) +{ + ProcManager *manager = ProcManagerNew(); + assert_true(manager != NULL); + + /* We just need some FILE streams. */ + FILE *input = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input != NULL); + FILE *output = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output != NULL); + + bool ret = ProcManagerAddProcess(manager, + xstrdup("test-process"), xstrdup("/some/command arg1 arg2"), xstrdup("test process"), + (pid_t) 1234, input, output, 'i'); + assert_true(ret); + + FILE *input2 = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input2 != NULL); + FILE *output2 = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output2 != NULL); + + ret = ProcManagerAddProcess(manager, + xstrdup("test-process2"), xstrdup("/some/command2 arg1 arg2"), xstrdup("test process 2"), + (pid_t) 1235, input2, output2, 'i'); + assert_true(ret); + + FILE *input3 = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input3 != NULL); + FILE *output3 = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output3 != NULL); + + ret = ProcManagerAddProcess(manager, + xstrdup("test-process3"), xstrdup("/some/command3 arg1 arg2"), xstrdup("test process 3"), + (pid_t) 1236, input3, output3, 'i'); + assert_true(ret); + + FILE *input4 = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input4 != NULL); + FILE *output4 = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output4 != NULL); + + ret = ProcManagerAddProcess(manager, + xstrdup("test-process4"), xstrdup("/some/command4 arg1 arg2"), xstrdup("test process 4"), + (pid_t) 1237, input4, output4, 'i'); + assert_true(ret); + + int terminator_call_counter = 0; + ret = ProcManagerTerminateProcessByPID(manager, 1234, TestTerminator, &terminator_call_counter); + assert_true(ret); + assert_int_equal(terminator_call_counter, 1); + + ret = ProcManagerTerminateProcessById(manager, "test-process2", TestTerminator, &terminator_call_counter); + assert_true(ret); + assert_int_equal(terminator_call_counter, 2); + + ret = ProcManagerTerminateProcessByStream(manager, input3, TestTerminator, &terminator_call_counter); + assert_true(ret); + assert_int_equal(terminator_call_counter, 3); + + ret = ProcManagerTerminateProcessByFD(manager, fileno(input4), TestTerminator, &terminator_call_counter); + assert_true(ret); + assert_int_equal(terminator_call_counter, 4); + + ProcManagerDestroy(manager); +} + +static void test_terminate_all(void) +{ + ProcManager *manager = ProcManagerNew(); + assert_true(manager != NULL); + + /* We just need some FILE streams. */ + FILE *input = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input != NULL); + FILE *output = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output != NULL); + + bool ret = ProcManagerAddProcess(manager, + xstrdup("test-process"), xstrdup("/some/command arg1 arg2"), xstrdup("test process"), + (pid_t) 1234, input, output, 'i'); + assert_true(ret); + + FILE *input2 = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input2 != NULL); + FILE *output2 = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output2 != NULL); + + ret = ProcManagerAddProcess(manager, + xstrdup("test-process2"), xstrdup("/some/command2 arg1 arg2"), xstrdup("test process 2"), + (pid_t) 1235, input2, output2, 'i'); + assert_true(ret); + + FILE *input3 = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input3 != NULL); + FILE *output3 = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output3 != NULL); + + ret = ProcManagerAddProcess(manager, + xstrdup("test-process3"), xstrdup("/some/command3 arg1 arg2"), xstrdup("test process 3"), + (pid_t) 1236, input3, output3, 'i'); + assert_true(ret); + + FILE *input4 = fopen(TEMP_DIR"/"TEST_FILE, "w"); + assert_true(input4 != NULL); + FILE *output4 = fopen(TEMP_DIR"/"TEST_FILE, "r"); + assert_true(output4 != NULL); + + ret = ProcManagerAddProcess(manager, + xstrdup("test-process4"), xstrdup("/some/command4 arg1 arg2"), xstrdup("test process 4"), + (pid_t) 1237, input4, output4, 'i'); + assert_true(ret); + + int terminator_call_counter = 0; + ret = ProcManagerTerminateAllProcesses(manager, TestTerminator, &terminator_call_counter); + assert_true(ret); + assert_int_equal(terminator_call_counter, 4); + + ProcManagerDestroy(manager); +} + +static void init(void) +{ + int ret = mkdir(TEMP_DIR, 0755); + if (ret != 0) + { + printf("Failed to create directory for tests!\n"); + exit(EXIT_FAILURE); + } + + ret = open(TEMP_DIR "/" TEST_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (ret == -1) + { + printf("Failed to create test file for tests!\n"); + exit(EXIT_FAILURE); + } +} + +static void cleanup(void) +{ + unlink(TEMP_DIR"/"TEST_FILE); + rmdir(TEMP_DIR); +} + +int main() +{ + PRINT_TEST_BANNER(); + + init(); + + const UnitTest tests[] = { + unit_test(test_add_get_process_destroy), + unit_test(test_add_get_process_multi_destroy), + unit_test(test_pop), + unit_test(test_add_process_twice), + unit_test(test_add_get_process_many_destroy), + unit_test(test_terminate), + unit_test(test_terminate_all), + }; + + int ret = run_tests(tests); + + cleanup(); + return ret; +} +