summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDennis Camera <skonfig@dtnr.ch>2022-08-22 00:27:47 +0200
committerDennis Camera <skonfig@dtnr.ch>2022-08-22 00:27:47 +0200
commit2f425cb9e9d28c8e7f017a4fbe1203c7431e745b (patch)
tree9ac97abaebdbe3363ee71c6a9f3210799d4494ab
parent580b957769a5ee4ab7522e474f3996c03426a286 (diff)
downloadskonfig-c-2f425cb9e9d28c8e7f017a4fbe1203c7431e745b.tar.gz
skonfig-c-2f425cb9e9d28c8e7f017a4fbe1203c7431e745b.zip
Implement command execution
-rw-r--r--Makefile.am15
-rw-r--r--configure.ac5
-rw-r--r--src/rcopy.c43
-rw-r--r--src/rcopy.h17
-rw-r--r--src/rexec.c48
-rw-r--r--src/rexec.h18
-rw-r--r--src/run.c58
-rw-r--r--src/skonfig.c1
-rw-r--r--src/util/envp.c182
-rw-r--r--src/util/envp.h111
-rw-r--r--src/util/exec.c97
-rw-r--r--src/util/exec.h23
-rw-r--r--src/util/fs.c66
-rw-r--r--src/util/fs.h18
-rw-r--r--src/util/string.h24
15 files changed, 715 insertions, 11 deletions
diff --git a/Makefile.am b/Makefile.am
index 7f8a228..faf7197 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -4,6 +4,15 @@ AM_CFLAGS += -Wall -Werror
AM_CFLAGS += -include config.h
bin_PROGRAMS = skonfig
-skonfig_SOURCES = src/skonfig.c \
- src/config.c src/log.c src/run.c \
- src/util/array.c src/util/fs.c src/util/string.c
+skonfig_SOURCES = \
+ src/config.c \
+ src/log.c \
+ src/rcopy.c \
+ src/rexec.c \
+ src/run.c \
+ src/skonfig.c \
+ src/util/array.c \
+ src/util/envp.c \
+ src/util/exec.c \
+ src/util/fs.c \
+ src/util/string.c
diff --git a/configure.ac b/configure.ac
index 37560ea..8780d5b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -84,9 +84,10 @@ check_set_ccopt -fstack-clash-protection \
# Checks for header files.
AC_HEADER_STDC
AC_HEADER_STDBOOL
-AC_CHECK_HEADERS([sys/stat.h])
AC_HEADER_STAT
-AC_CHECK_HEADERS([stdarg.h])
+
+AC_CHECK_HEADERS_ONCE([stdarg.h])
+AC_CHECK_HEADERS_ONCE([linux/limits.h])
# Checks for typedefs, structures, and compiler characteristics.
AC_C_CONST
diff --git a/src/rcopy.c b/src/rcopy.c
new file mode 100644
index 0000000..8858f07
--- /dev/null
+++ b/src/rcopy.c
@@ -0,0 +1,43 @@
+#if HAVE_STDBOOL_H
+#include <stdbool.h>
+#endif
+
+#include "rexec.h"
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+
+static char *alloc_cmd(const char *restrict format, ...) {
+ va_list ap;
+ va_start(ap, format);
+ int len = vsnprintf(NULL, 0, format, ap);
+ va_end(ap);
+
+ char *buf = malloc(len+1);
+ va_start(ap, format);
+ (void)vsnprintf(buf, len+1, format, ap);
+ va_end(ap);
+
+ return buf;
+}
+
+int sk_rcopy_file(
+ const char *restrict src, const char *restrict target_host,
+ const char *restrict dst) {
+ char *remote_cmd = alloc_cmd("cat >%s && chmod 0700 %s", dst, dst);
+ FILE *sfil = fopen(src, "r");
+
+ /* TODO: set correct stdout and stderr */
+ int rv = sk_rexec_command(
+ target_host, remote_cmd,
+ fileno(sfil), fileno(stdout), fileno(stderr),
+ NULL);
+
+ /* clean up */
+ fclose(sfil);
+ free(remote_cmd);
+
+ return rv;
+}
diff --git a/src/rcopy.h b/src/rcopy.h
new file mode 100644
index 0000000..d816f4a
--- /dev/null
+++ b/src/rcopy.h
@@ -0,0 +1,17 @@
+#ifndef _SK_RCOPY_H
+#define _SK_RCOPY_H
+
+/**
+ * sk_rcopy_file:
+ * copies a local file to a remote machine.
+ *
+ * @param src: the pathname to the local file.
+ * @param target_host: the name of the remote machine.
+ * @param dst: the pathname where to store the file on @target_host.
+ * @return 0 on success, other value otherwise.
+ */
+int sk_rcopy_file(
+ const char *restrict src, const char *restrict target_host,
+ const char *restrict dst);
+
+#endif
diff --git a/src/rexec.c b/src/rexec.c
new file mode 100644
index 0000000..56b0b4e
--- /dev/null
+++ b/src/rexec.c
@@ -0,0 +1,48 @@
+#include "rexec.h"
+
+#include "log.h"
+
+#include "util/exec.h"
+
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/wait.h>
+
+int sk_rexec_command(
+ const char *restrict target_host, char *restrict cmd,
+ int stdin, int stdout, int stderr, envp_t *const extra_envp) {
+ envp_t *envp = envp_dup();
+
+ /* FIXME: set actual values */
+ envp_set(envp, "__target_host", target_host, true);
+ envp_set(envp, "__target_fqdn", target_host, true);
+ envp_set(envp, "__target_hostname", target_host, true);
+
+ if (NULL != envp) {
+ envp_merge(envp, extra_envp);
+ }
+
+ /* TODO: use remote_exec instead of ssh */
+ /* FIXME: Remove echo */
+ pid_t pid = exec_subcmd(
+ (char *const[]){"echo", "ssh", (char *)target_host, cmd},
+ stdin, stdout, stderr, true, NULL, envp);
+
+ int rv = -1;
+ if (0 > pid) {
+ /* command could not be launched */
+ sk_error("failed to execute command (%s) on target: %s",
+ cmd, strerror(errno));
+ goto ret;
+ }
+
+ int cmd_status = 0;
+ while (0 >= waitpid(pid, &cmd_status, 0));
+ if (WIFEXITED(cmd_status)) { rv = WEXITSTATUS(cmd_status); }
+
+ ret:
+ envp_free(envp);
+
+ return rv;
+}
diff --git a/src/rexec.h b/src/rexec.h
new file mode 100644
index 0000000..128241c
--- /dev/null
+++ b/src/rexec.h
@@ -0,0 +1,18 @@
+#ifndef _SK_REXEC_H
+#define _SK_REXEC_H
+
+#include "util/envp.h"
+
+/**
+ * sk_rexec_command:
+ * executes a command on a remote machine.
+ *
+ * @param target_host: the name of the remote machine.
+ * @param cmd: the command to execute.
+ * @return the exit status of the command.
+ */
+int sk_rexec_command(
+ const char *restrict target_host, char *restrict cmd,
+ int stdin, int stdout, int stderr, envp_t *const extra_envp);
+
+#endif
diff --git a/src/run.c b/src/run.c
index 3589467..eef9874 100644
--- a/src/run.c
+++ b/src/run.c
@@ -2,6 +2,8 @@
#include "log.h"
+#include "rcopy.h"
+
#include "util/array.h"
#include "util/fs.h"
@@ -249,6 +251,8 @@ sk_run_error sk_run_install_type_cmds(const char *local_hostdir) {
}
}
+ (void)closedir(dirp);
+
return SK_RUN_OK;
}
@@ -284,18 +288,60 @@ sk_run_error sk_run_setup_object_marker_file(
sk_run_error sk_run_exec_global_explorers(
const char *target_host, bool dry_run, const char *local_hostdir,
struct sk_config *config) {
- char explorers_dir[PATH_MAX+1];
+ char local_explorers_dir[PATH_MAX+1], remote_explorers_dir[PATH_MAX+1];
+
+ if (PATH_MAX < pathjoin_r(
+ local_explorers_dir, PATH_MAX+1,
+ local_hostdir, "conf", "explorer")) {
+ sk_error("local global explorers path is longer than PATH_MAX");
+ return SK_RUN_GENERIC_ERROR;
+ }
+
if (PATH_MAX < pathjoin_r(
- explorers_dir, PATH_MAX+1,
- local_hostdir, "explorer")) {
- sk_error("global explorers path is longer than PATH_MAX");
+ remote_explorers_dir, PATH_MAX+1,
+ config->global.remote_out_path, "conf", "explorer")) {
+ sk_error("remote global explorers path is longer than PATH_MAX");
return SK_RUN_GENERIC_ERROR;
}
sk_debug("Running global explorers");
+ sk_trace("local explorers dir = %s", local_explorers_dir);
- /* transfer global exploers to target */
- /* TODO */
+ /* transfer global explorers to target */
+ DIR *dirp = opendir(local_explorers_dir);
+ if (NULL == dirp) {
+ sk_error("cannot open directory (%s): %s",
+ local_explorers_dir, strerror(errno));
+ return SK_RUN_GENERIC_ERROR;
+ }
+
+ struct dirent *dp;
+ while (NULL != (dp = readdir(dirp))) {
+ if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) {
+ continue;
+ }
+
+ char explorer_src[PATH_MAX+1], explorer_dst[PATH_MAX+1];
+ if (PATH_MAX < pathjoin_r(
+ explorer_src, PATH_MAX+1,
+ local_explorers_dir, dp->d_name)) {
+ sk_error("explorer path too long: %s/%s",
+ local_explorers_dir, dp->d_name);
+ continue;
+ }
+ if (PATH_MAX < pathjoin_r(
+ explorer_dst, PATH_MAX+1,
+ remote_explorers_dir, dp->d_name)) {
+ sk_error("explorer destination too long: %s/%s",
+ remote_explorers_dir, dp->d_name);
+ continue;
+ }
+
+ sk_trace("copying explorer %s to %s:%s",
+ explorer_src, target_host, explorer_dst);
+ sk_rcopy_file(explorer_src, target_host, explorer_dst);
+ }
+ (void)closedir(dirp);
/* execute global explorers */
/* TODO: implement multiple jobs */
diff --git a/src/skonfig.c b/src/skonfig.c
index 4b1002e..0d2341b 100644
--- a/src/skonfig.c
+++ b/src/skonfig.c
@@ -237,6 +237,7 @@ int main(int argc, char *argv[]) {
sk_init_config(config, NULL);
/* update config from command line arguments */
+ free(config->global.init_manifest);
config->global.init_manifest = strdup(options.init_manifest);
config->global.jobs = options.jobs;
diff --git a/src/util/envp.c b/src/util/envp.c
new file mode 100644
index 0000000..808d9b8
--- /dev/null
+++ b/src/util/envp.c
@@ -0,0 +1,182 @@
+#include "envp.h"
+
+#include "string.h"
+
+#include <stdlib.h>
+
+extern char **environ;
+
+static inline bool _envp_string_match_key(
+ const char *restrict string, const char *restrict key) {
+ for (size_t i = 0; ; ++i) {
+ if ('\0' == key[i]) {
+ return ('=' == string[i]);
+ } else if ('\0' == string[i]) {
+ return false;
+ } else if (string[i] != key[i]) {
+ return false;
+ }
+ }
+}
+
+
+static int _envp_append(envp_t *envp, char *string) {
+ /* find the end */
+ envp_t p = *envp;
+ size_t i = 0;
+ while (NULL != p[i]) i++;
+
+ /* make space for one more element */
+ p = realloc(p, ((i+2) * sizeof(char *)));
+ if (NULL == p) {
+ return -1;
+ }
+ *envp = p;
+
+ /* append element */
+ p[i+1] = 0;
+ p[i] = string;
+
+ return 0;
+}
+
+static int _envp_replace(char **old, char *new) {
+ char *oldv = *old;
+ *old = new;
+ free(oldv);
+
+ return 0;
+}
+
+
+envp_t *envp_alloc(void) {
+ envp_t envp = calloc(1, sizeof(*envp));
+ envp_t *envpp = malloc(sizeof(envpp));
+ *envpp = envp;
+ return envpp;
+}
+
+envp_t *envp_dup(void) {
+ envp_t *envp = envp_alloc();
+
+ for (size_t i = 0; NULL != environ[i]; ++i) {
+ (void)_envp_append(envp, strdup(environ[i]));
+ }
+
+ return envp;
+}
+
+static char **_envp_find_elem(envp_t envp, const char *restrict name) {
+ for (size_t i = 0; NULL != envp[i]; ++i) {
+ if (_envp_string_match_key(envp[i], name)) {
+ return &envp[i];
+ }
+ }
+
+ return NULL;
+}
+
+static char *_envp_get_elem(envp_t envp, const char *restrict name) {
+ char **elem = _envp_find_elem(envp, name);
+ return (elem ? *elem : NULL);
+}
+
+char *envp_get(envp_t *envp, const char *name) {
+ if (NULL == envp) return NULL;
+
+ char *elem = _envp_get_elem(*envp, name);
+
+ if (NULL != elem) {
+ elem = strchr(elem, '=');
+ if (NULL != elem) elem++; /* skip over = */
+ }
+
+ return elem;
+}
+
+
+int envp_put(envp_t *envp, char *string) {
+ char envname[strchr(string, '=')-string+1];
+ strncpy(envname, string, sizeof(envname));
+
+ char **elem = _envp_find_elem(*envp, envname);
+
+ if (NULL != elem) {
+ return _envp_replace(elem, string);
+ } else {
+ return _envp_append(envp, string);
+ }
+}
+
+int envp_set(
+ envp_t *envp, const char *restrict envname, const char *restrict envval,
+ bool overwrite) {
+ char **elem = _envp_find_elem(*envp, envname);
+
+ char *string = NULL;
+ if (NULL == elem || overwrite) {
+ size_t slen = (strlen(envname) + strlen (envval) + 2);
+ string = malloc(slen * sizeof(char));
+ snprintf(string, slen, "%s=%s", envname, envval);
+ }
+
+ if (NULL != elem) {
+ if (overwrite) {
+ return _envp_replace(elem, string);
+ } else {
+ /* no change */
+ return 0;
+ }
+ } else {
+ return _envp_append(envp, string);
+ }
+}
+
+int envp_unset(envp_t *envp, const char *restrict name) {
+ size_t i;
+
+ /* find element */
+ for (i = 0; NULL != (*envp)[i]; ++i) {
+ if (_envp_string_match_key((*envp)[i], name)) {
+ break;
+ }
+ }
+
+ char *elem = (*envp)[i];
+ if (NULL == elem) {
+ /* not found -> ok */
+ return 0;
+ }
+
+ /* shift rest of elements */
+ for (; NULL != (*envp)[i]; ++i) {
+ (*envp)[i] = (*envp)[i+1];
+ }
+
+ /* delete match */
+ free(elem);
+
+ return 0;
+}
+
+void envp_merge(envp_t *dest, envp_t *const src) {
+ if (NULL == src) return;
+
+ for (size_t i = 0; NULL != (*src)[i]; ++i) {
+ envp_put(dest, strdup((*src)[i]));
+ }
+}
+
+void envp_print(envp_t *envp, FILE *restrict stream) {
+ for (size_t i = 0; NULL != (*envp)[i]; ++i) {
+ fprintf(stream, "%s\n", (*envp)[i]);
+ }
+}
+
+void envp_free(envp_t *envp) {
+ for (size_t i = 0; NULL != (*envp)[i]; ++i) {
+ free((*envp)[i]);
+ }
+ free(*envp);
+ free(envp);
+}
diff --git a/src/util/envp.h b/src/util/envp.h
new file mode 100644
index 0000000..37871f9
--- /dev/null
+++ b/src/util/envp.h
@@ -0,0 +1,111 @@
+#ifndef _SK_UTIL_ENVP_H
+#define _SK_UTIL_ENVP_H
+
+#if HAVE_STDBOOL_H
+#include <stdbool.h>
+#endif
+
+#include <stdio.h>
+
+typedef char ** envp_t;
+
+/**
+ * envp_alloc:
+ * allocates a new, empty envp_t.
+ *
+ * the returned pointer should be passed to envp_free() when done.
+ *
+ * @return new, empty envp_t.
+ */
+envp_t *envp_alloc(void);
+
+/**
+ * envp_dup:
+ * allocates a new envp_t with the contents of the process’ environ.
+ *
+ * the returned pointer should be passed to envp_free() when done.
+ *
+ * @return new envp_t.
+ */
+envp_t *envp_dup(void);
+
+/**
+ * envp_get:
+ * returns the value of an environment variable in an envp.
+ *
+ * @param envp: the envp to query.
+ * @param name: the name of the environment variable to return.
+ * @return the value of the environment variable if found, NULL otherwise.
+ */
+char *envp_get(envp_t *envp, const char *name);
+
+/**
+ * envp_put:
+ * puts an environment variable into an envp.
+ *
+ * NOTE that the @string pointer will be included in the envp environment as is,
+ * i.e. it must be a string pointer which can be passed to free().
+ * Moreover, the @string will be freed when the @envp is passed to envp_free().
+ *
+ * If an environment variable with the same name already exists, its value will
+ * be replaced with @string.
+ *
+ * @param envp: the envp to modify.
+ * @param string: a string of the form "name=value".
+ * @return 0 on success, -1 otherwise and sets errno.
+ */
+int envp_put(envp_t *envp, char *string);
+
+/**
+ * envp_set:
+ * sets a new environment variable in an envp.
+ *
+ * @param envp: the envp to modify.
+ * @param envname: the name of the environment variable.
+ * @param envval: the value of @envname.
+ * @param overwrite: if an environment variable with @envname already exists in
+ * @envp, replace its value. If @overwrite is false, the function returns
+ * 0 and will not update @envp.
+ * @return 0 on success, -1 otherwise and sets errno.
+ */
+int envp_set(
+ envp_t *envp, const char *restrict envname, const char *restrict envval,
+ bool overwrite);
+
+/**
+ * envp_unset:
+ * removes an environment variable from an envp.
+ *
+ * @param envp: the envp to modify.
+ * @param name: the name of the environment variable to remove.
+ * @return 0
+ */
+int envp_unset(envp_t *envp, const char *restrict name);
+
+/**
+ * envp_merge:
+ * add all items from an envp to another one.
+ *
+ * @param dest: the envp to modify.
+ * @param src: the source envp.
+ */
+void envp_merge(envp_t *dest, envp_t *const src);
+
+/**
+ * envp_print:
+ * prints the contents of an envp to an output stream.
+ *
+ * @param envp: the envp to print.
+ * @param stream: the stream to output to.
+ */
+void envp_print(envp_t *envp, FILE *restrict stream);
+
+/**
+ * envp_free:
+ * frees a given envp_t and all of its contents.
+ *
+ * @param envp: the structure to free.
+ */
+void envp_free(envp_t *envp);
+
+#endif
diff --git a/src/util/exec.c b/src/util/exec.c
new file mode 100644
index 0000000..687cfbd
--- /dev/null
+++ b/src/util/exec.c
@@ -0,0 +1,97 @@
+#include "exec.h"
+
+#if HAVE_STDBOOL_H
+#include <stdbool.h>
+#endif
+
+#include "fs.h"
+
+#include "../log.h"
+
+#include <errno.h>
+
+#include <limits.h>
+#if HAVE_LINUX_LIMITS_H
+#include <linux/limits.h>
+#endif
+
+#include <unistd.h>
+
+static int max_fd() {
+ int fd_max = 0;
+
+#ifdef NR_OPEN /* from <linux/limits.h> */
+ fd_max = NR_OPEN;
+ if (0 < fd_max) return fd_max;
+#endif
+#ifdef _SC_OPEN_MAX /* from <unistd.h> */
+ fd_max = sysconf(_SC_OPEN_MAX);
+ if (0 < fd_max) return fd_max;
+#endif
+#ifdef _POSIX_OPEN_MAX /* from <limits.h> */
+ fd_max = _POSIX_OPEN_MAX;
+ if (0 < fd_max) return fd_max;
+#endif
+
+ return 256; /* random guess falllback value */
+}
+
+pid_t exec_subcmd(
+ char *const argv[], int stdin, int stdout, int stderr,
+ bool close_fds, const char *cwd, envp_t *const envp) {
+ pid_t pid = fork();
+ if (0 != pid) {
+ /* parent */
+ return pid;
+ }
+
+ /* child */
+
+ /* replace "standard" FDs */
+ if (-1 == dup2(stdin, 0)) {
+ sk_error("failed to replace stdin for subcmd: %s", strerror(errno));
+ return -1;
+ }
+ if (-1 == dup2(stdout, 1)) {
+ sk_error("failed to replace stdout for subcmd: %s", strerror(errno));
+ return -1;
+ }
+ if (-1 == dup2(stderr, 2)) {
+ sk_error("failed to replace stderr for subcmd: %s", strerror(errno));
+ return -1;
+ }
+
+ if (close_fds) {
+ /* close all "non-standard" FDs */
+ for (int i = 3, m = max_fd(); i < m; ++i) {
+ (void)close(i);
+ }
+ }
+
+ /* chdir */
+ if (NULL != cwd) {
+ if (-1 == chdir(cwd)) {
+ sk_error(
+ "error in subcommand execution: chdir(%s) failed: %s",
+ cwd, strerror(errno));
+ exit(-1);
+ }
+ }
+
+ char executable[PATH_MAX+1];
+ which(argv[0], executable, envp_get(envp, "PATH"));
+
+ if (is_empty_string(executable)) {
+ /* no such command found */
+ /* TODO: handle error */
+ exit(-1);
+ }
+
+ if (NULL != envp) {
+ execve(executable, argv, *envp);
+ } else {
+ execv(executable, argv);
+ }
+
+ exit(-1);
+}
diff --git a/src/util/exec.h b/src/util/exec.h
new file mode 100644
index 0000000..4572a43
--- /dev/null
+++ b/src/util/exec.h
@@ -0,0 +1,23 @@
+#ifndef _SK_UTIL_EXEC_H
+#define _SK_UTIL_EXEC_H
+
+#if HAVE_STDBOOL_H
+#include <stdbool.h>
+#endif
+
+#include "envp.h"
+
+#include <sys/types.h>
+
+/**
+ * exec_subcmd:
+ * ...
+ *
+ * @param
+ * @return ...
+ */
+pid_t exec_subcmd(
+ char *const argv[], int stdin, int stdout, int stderr,
+ bool close_fds, const char *cwd, envp_t *const envp);
+
+#endif
diff --git a/src/util/fs.c b/src/util/fs.c
index 72c87f3..762c9a5 100644
--- a/src/util/fs.c
+++ b/src/util/fs.c
@@ -7,6 +7,8 @@
#include <sys/stat.h>
#include <fcntl.h>
#include <ftw.h>
+#include <string.h>
+#include <unistd.h>
bool is_dir(const char *restrict pathname) {
#ifdef STAT_MACROS_BROKEN
@@ -122,3 +124,67 @@ static int __rmtree_fn(
int rmtree(const char *dir) {
return nftw(dir, __rmtree_fn, 16, FTW_DEPTH | FTW_PHYS);
}
+
+void which(
+ const char *restrict name, char *restrict resolved_path,
+ const char *restrict search_path) {
+ char *path = NULL;
+
+ /* check if name is a path */
+ if (realpath(name, resolved_path)) {
+ if (access(resolved_path, X_OK)) {
+ return;
+ }
+ }
+
+ resolved_path[0] = '\0';
+
+ if (NULL == search_path) {
+ /* use PATH from environment */
+ search_path = getenv("PATH");
+ }
+ if (NULL != search_path) {
+ path = strdup(search_path);
+ }
+#ifdef _CS_PATH
+ if (NULL == search_path) {
+ /* fall back to POSIX path */
+ size_t pathlen = confstr(_CS_PATH, NULL, 0);
+ path = malloc(pathlen);
+ if (NULL != path) {
+ (void)confstr(_CS_PATH, path, pathlen);
+ }
+ }
+#endif
+
+ if (NULL == path) {
+ /* cannot determine PATH */
+ return;
+ }
+
+ char *saveptr = NULL;
+ char *path_tok = strtok_r(path, stringize(PATHSEP), &saveptr);
+ do {
+ if (PATH_MAX < pathjoin_r(resolved_path, PATH_MAX+1, path_tok, name)) {
+ /* TODO: handle error */
+ continue;
+ }
+
+ if (access(resolved_path, X_OK)) {
+ /* no match */
+ resolved_path[0] = '\0';
+ continue;
+ }
+
+ /* match */
+ break;
+ } while ((path_tok = strtok_r(NULL, stringize(PATHSEP), &saveptr)));
+
+ if (!is_empty_string(resolved_path)) {
+ sk_debug("resolved command %s: %s", name, resolved_path);
+ } else {
+ sk_warn("could not resolve command: %s", name);
+ }
+
+ free(path);
+}
diff --git a/src/util/fs.h b/src/util/fs.h
index ad0a449..69849b9 100644
--- a/src/util/fs.h
+++ b/src/util/fs.h
@@ -72,4 +72,22 @@ char *mktempdir(char *dest);
*/
int rmtree(const char *dir);
+/**
+ * which:
+ * find an executable in a PATH.
+ *
+ * this function searches @search_path (or the "PATH" environment variable if
+ * @search_path is NULL) for an executable with @name.
+ * The full path of the resolved executable will be stored in @resolved_path.
+ * If no match could be found, the content of @resolved_path will be the empty
+ * string.
+ *
+ * @param name: name of the executable to search for.
+ * @param resolved_path: a pointer to a buffer of at least PATH_MAX+1 length.
+ * @param search_path: PATH to search in (defaults to the "PATH" env variable).
+ */
+void which(
+ const char *restrict name, char *restrict resolved_path,
+ const char *restrict search_path);
+
#endif
diff --git a/src/util/string.h b/src/util/string.h
index 1ef07d1..b75e783 100644
--- a/src/util/string.h
+++ b/src/util/string.h
@@ -1,9 +1,14 @@
#ifndef _SK_UTIL_STRING_H
#define _SK_UTIL_STRING_H
+#if HAVE_STDBOOL_H
+#include <stdbool.h>
+#endif
+
#include "array.h"
#include <stddef.h>
+#include <string.h>
/**
* stringize:
@@ -36,6 +41,25 @@
#define boolstr(b) (b ? "true" : "false")
/**
+ * is_empty_string:
+ * Checks if the given string is empty ("").
+ *
+ * @param s: the string to check
+ * @return: true if @s is the empty string (""), false otherwise.
+ */
+#define is_empty_string(s) ('\0' == *s)
+
+/**
+ * string_starts_with:
+ * Checks if the given string starts with a given prefix.
+ *
+ * @param s: the string to check.
+ * @param p: the prefix.
+ * @return true if @s starts with @p, false otherwise.
+ */
+#define string_starts_with(s, p) (strncmp(s, p, strlen(p)))
+
+/**
* strdup:
* like strdup(3p).
*/