engines: Adding exec engine
[fio.git] / engines / exec.c
diff --git a/engines/exec.c b/engines/exec.c
new file mode 100644 (file)
index 0000000..e1cc0b8
--- /dev/null
@@ -0,0 +1,361 @@
+/*
+ * Exec engine
+ *
+ * Doesn't transfer any data, merely run 3rd party tools
+ *
+ */
+#include "../fio.h"
+#include "../optgroup.h"
+#include <signal.h>
+
+struct exec_options {
+       void *pad;
+       char *program;
+       char *arguments;
+       int grace_time;
+       unsigned int std_redirect;
+       pid_t pid;
+};
+
+static struct fio_option options[] = {
+       {
+        .name = "program",
+        .lname = "Program",
+        .type = FIO_OPT_STR_STORE,
+        .off1 = offsetof(struct exec_options, program),
+        .help = "Program to execute",
+        .category = FIO_OPT_C_ENGINE,
+        .group = FIO_OPT_G_INVALID,
+        },
+       {
+        .name = "arguments",
+        .lname = "Arguments",
+        .type = FIO_OPT_STR_STORE,
+        .off1 = offsetof(struct exec_options, arguments),
+        .help = "Arguments to pass",
+        .category = FIO_OPT_C_ENGINE,
+        .group = FIO_OPT_G_INVALID,
+        },
+       {
+        .name = "grace_time",
+        .lname = "Grace time",
+        .type = FIO_OPT_INT,
+        .minval = 0,
+        .def = "1",
+        .off1 = offsetof(struct exec_options, grace_time),
+        .help = "Grace time before sending a SIGKILL",
+        .category = FIO_OPT_C_ENGINE,
+        .group = FIO_OPT_G_INVALID,
+        },
+       {
+        .name = "std_redirect",
+        .lname = "Std redirect",
+        .type = FIO_OPT_BOOL,
+        .def = "1",
+        .off1 = offsetof(struct exec_options, std_redirect),
+        .help = "Redirect stdout & stderr to files",
+        .category = FIO_OPT_C_ENGINE,
+        .group = FIO_OPT_G_INVALID,
+        },
+       {
+        .name = NULL,
+        },
+};
+
+char *str_replace(char *orig, const char *rep, const char *with)
+{
+       /*
+          Replace a substring by another.
+
+          Returns the new string if occurences were found
+          Returns orig if no occurence is found
+        */
+       char *result, *insert, *tmp;
+       int len_rep, len_with, len_front, count;
+
+       // sanity checks and initialization
+       if (!orig || !rep)
+               return orig;
+
+       len_rep = strlen(rep);
+       if (len_rep == 0)
+               return orig;
+
+       if (!with)
+               with = "";
+       len_with = strlen(with);
+
+       insert = orig;
+       for (count = 0; (tmp = strstr(insert, rep)); ++count) {
+               insert = tmp + len_rep;
+       }
+
+       tmp = result = malloc(strlen(orig) + (len_with - len_rep) * count + 1);
+
+       if (!result)
+               return orig;
+
+       while (count--) {
+               insert = strstr(orig, rep);
+               len_front = insert - orig;
+               tmp = strncpy(tmp, orig, len_front) + len_front;
+               tmp = strcpy(tmp, with) + len_with;
+               orig += len_front + len_rep;
+       }
+       strcpy(tmp, orig);
+       return result;
+}
+
+char *expand_variables(struct thread_options *o, char *arguments)
+{
+       char str[16];
+       char *expanded_runtime, *expanded_name;
+       snprintf(str, sizeof(str), "%lld", o->timeout / 1000000);
+
+       /* %r is replaced by the runtime in seconds */
+       expanded_runtime = str_replace(arguments, "%r", str);
+
+       /* %n is replaced by the name of the running job */
+       expanded_name = str_replace(expanded_runtime, "%n", o->name);
+
+       return expanded_name;
+}
+
+static int exec_background(struct thread_options *o, struct exec_options *eo)
+{
+       char *outfilename = NULL, *errfilename = NULL;
+       int outfd = 0, errfd = 0;
+       pid_t pid;
+       char *expanded_arguments = NULL;
+       /* For the arguments splitting */
+       char **arguments_array = NULL;
+       char *p;
+       char *exec_cmd = NULL;
+       size_t arguments_nb_items = 0, q;
+
+       if (asprintf(&outfilename, "%s.stdout", o->name) < 0)
+               return -1;
+
+       if (asprintf(&errfilename, "%s.stderr", o->name) < 0) {
+               free(outfilename);
+               return -1;
+       }
+
+       /* If we have variables in the arguments, let's expand them */
+       expanded_arguments = expand_variables(o, eo->arguments);
+
+       if (eo->std_redirect) {
+               log_info("%s : Saving output of %s %s : stdout=%s stderr=%s\n",
+                        o->name, eo->program, expanded_arguments, outfilename,
+                        errfilename);
+
+               /* Creating the stderr & stdout output files */
+               outfd = open(outfilename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
+               if (!outfd) {
+                       log_err("fio: cannot open output file %s : %s\n",
+                               outfilename, strerror(errno));
+                       free(outfilename);
+                       free(errfilename);
+                       return -1;
+               }
+
+               errfd = open(errfilename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
+               if (!errfd) {
+                       log_err("fio: cannot open output file %s : %s\n",
+                               errfilename, strerror(errno));
+                       free(outfilename);
+                       free(errfilename);
+                       return -1;
+               }
+       } else {
+               log_info("%s : Running %s %s\n",
+                        o->name, eo->program, expanded_arguments);
+       }
+
+       pid = fork();
+
+       /* We are on the control thread (parent side of the fork */
+       if (pid > 0) {
+               eo->pid = pid;
+               if (eo->std_redirect) {
+                       /* The output file is for the client side of the fork */
+                       close(outfd);
+                       close(errfd);
+                       free(outfilename);
+                       free(errfilename);
+               }
+               return 0;
+       }
+
+       /* If the fork failed */
+       if (pid < 0) {
+               log_err("fio: forking failed %s \n", strerror(errno));
+               if (eo->std_redirect) {
+                       close(outfd);
+                       close(errfd);
+                       free(outfilename);
+                       free(errfilename);
+               }
+               return -1;
+       }
+
+       /* We are in the worker (child side of the fork) */
+       if (pid == 0) {
+               if (eo->std_redirect) {
+                       dup2(outfd, 1); // replace stdout by the output file we create
+                       dup2(errfd, 2); // replace stderr by the output file we create
+                       close(outfd);
+                       close(errfd);
+                       free(outfilename);
+                       free(errfilename);
+               }
+
+               /* Let's split the command line into a null terminated array to be passed to the exec'd program
+                  But don't asprintf expanded_arguments if NULL as it would be converted
+                  to a '(null)' argument, while we want no arguments at all. */
+               if (expanded_arguments != NULL) {
+                       if (asprintf(&exec_cmd, "%s %s", eo->program, expanded_arguments) < 0)
+                               return -1;
+               } else {
+                       if (asprintf(&exec_cmd, "%s", eo->program) < 0)
+                               return -1;
+               }
+
+               /* Let's build an argv array to based on the program name and arguments */
+               p = exec_cmd;
+               for (;;) {
+                       p += strspn(p, " ");
+
+                       if (!(q = strcspn(p, " ")))
+                               break;
+
+                       if (q) {
+                               arguments_array =
+                                   realloc(arguments_array,
+                                           (arguments_nb_items +
+                                            1) * sizeof(char *));
+                               arguments_array[arguments_nb_items] =
+                                   malloc(q + 1);
+                               strncpy(arguments_array[arguments_nb_items], p,
+                                       q);
+                               arguments_array[arguments_nb_items][q] = 0;
+                               arguments_nb_items++;
+                               p += q;
+                       }
+               }
+
+               /* Adding a null-terminated item to close the list */
+               arguments_array =
+                   realloc(arguments_array,
+                           (arguments_nb_items + 1) * sizeof(char *));
+               arguments_array[arguments_nb_items] = NULL;
+
+               /* Replace the fio program from the child fork by the target program */
+               execvp(arguments_array[0], arguments_array);
+       }
+       // We never reach this place
+       return 0;
+}
+
+static enum fio_q_status
+fio_exec_queue(struct thread_data *td, struct io_u fio_unused * io_u)
+{
+       struct thread_options *o = &td->o;
+       struct exec_options *eo = td->eo;
+
+       /* Let's execute the program the first time we get queued */
+       if (eo->pid == -1) {
+               exec_background(o, eo);
+       } else {
+               /* The program is running in background, let's check on a regular basis
+                  if the time is over and if we need to stop the tool */
+               usleep(o->thinktime);
+               if (utime_since_now(&td->start) > o->timeout) {
+                       /* Let's stop the child */
+                       kill(eo->pid, SIGTERM);
+                       /* Let's give grace_time (1 sec by default) to the 3rd party tool to stop */
+                       sleep(eo->grace_time);
+               }
+       }
+
+       return FIO_Q_COMPLETED;
+}
+
+static int fio_exec_init(struct thread_data *td)
+{
+       struct thread_options *o = &td->o;
+       struct exec_options *eo = td->eo;
+       int td_previous_state;
+
+       eo->pid = -1;
+
+       if (!eo->program) {
+               td_vmsg(td, EINVAL,
+                       "no program is defined, it is mandatory to define one",
+                       "exec");
+               return 1;
+       }
+
+       log_info("%s : program=%s, arguments=%s\n",
+                td->o.name, eo->program, eo->arguments);
+
+       /* Saving the current thread state */
+       td_previous_state = td->runstate;
+
+       /* Reporting that we are preparing the engine
+        * This is useful as the qsort() calibration takes time
+        * This prevents the job from starting before init is completed
+        */
+       td_set_runstate(td, TD_SETTING_UP);
+
+       /*
+        * set thinktime_sleep and thinktime_spin appropriately
+        */
+       o->thinktime_blocks = 1;
+       o->thinktime_blocks_type = THINKTIME_BLOCKS_TYPE_COMPLETE;
+       o->thinktime_spin = 0;
+       o->thinktime = 50000;   /* 50ms pause when waiting for the program to complete */
+
+       o->nr_files = o->open_files = 1;
+
+       /* Let's restore the previous state. */
+       td_set_runstate(td, td_previous_state);
+       return 0;
+}
+
+static void fio_exec_cleanup(struct thread_data *td)
+{
+       struct exec_options *eo = td->eo;
+       /* Send a sigkill to ensure the job is well terminated */
+       if (eo->pid > 0)
+               kill(eo->pid, SIGKILL);
+}
+
+static int
+fio_exec_open(struct thread_data fio_unused * td,
+             struct fio_file fio_unused * f)
+{
+       return 0;
+}
+
+static struct ioengine_ops ioengine = {
+       .name = "exec",
+       .version = FIO_IOOPS_VERSION,
+       .queue = fio_exec_queue,
+       .init = fio_exec_init,
+       .cleanup = fio_exec_cleanup,
+       .open_file = fio_exec_open,
+       .flags = FIO_SYNCIO | FIO_DISKLESSIO | FIO_NOIO,
+       .options = options,
+       .option_struct_size = sizeof(struct exec_options),
+};
+
+static void fio_init fio_exec_register(void)
+{
+       register_ioengine(&ioengine);
+}
+
+static void fio_exit fio_exec_unregister(void)
+{
+       unregister_ioengine(&ioengine);
+}