engines: Adding exec engine
[fio.git] / engines / exec.c
1 /*
2  * Exec engine
3  *
4  * Doesn't transfer any data, merely run 3rd party tools
5  *
6  */
7 #include "../fio.h"
8 #include "../optgroup.h"
9 #include <signal.h>
10
11 struct exec_options {
12         void *pad;
13         char *program;
14         char *arguments;
15         int grace_time;
16         unsigned int std_redirect;
17         pid_t pid;
18 };
19
20 static struct fio_option options[] = {
21         {
22          .name = "program",
23          .lname = "Program",
24          .type = FIO_OPT_STR_STORE,
25          .off1 = offsetof(struct exec_options, program),
26          .help = "Program to execute",
27          .category = FIO_OPT_C_ENGINE,
28          .group = FIO_OPT_G_INVALID,
29          },
30         {
31          .name = "arguments",
32          .lname = "Arguments",
33          .type = FIO_OPT_STR_STORE,
34          .off1 = offsetof(struct exec_options, arguments),
35          .help = "Arguments to pass",
36          .category = FIO_OPT_C_ENGINE,
37          .group = FIO_OPT_G_INVALID,
38          },
39         {
40          .name = "grace_time",
41          .lname = "Grace time",
42          .type = FIO_OPT_INT,
43          .minval = 0,
44          .def = "1",
45          .off1 = offsetof(struct exec_options, grace_time),
46          .help = "Grace time before sending a SIGKILL",
47          .category = FIO_OPT_C_ENGINE,
48          .group = FIO_OPT_G_INVALID,
49          },
50         {
51          .name = "std_redirect",
52          .lname = "Std redirect",
53          .type = FIO_OPT_BOOL,
54          .def = "1",
55          .off1 = offsetof(struct exec_options, std_redirect),
56          .help = "Redirect stdout & stderr to files",
57          .category = FIO_OPT_C_ENGINE,
58          .group = FIO_OPT_G_INVALID,
59          },
60         {
61          .name = NULL,
62          },
63 };
64
65 char *str_replace(char *orig, const char *rep, const char *with)
66 {
67         /*
68            Replace a substring by another.
69
70            Returns the new string if occurences were found
71            Returns orig if no occurence is found
72          */
73         char *result, *insert, *tmp;
74         int len_rep, len_with, len_front, count;
75
76         // sanity checks and initialization
77         if (!orig || !rep)
78                 return orig;
79
80         len_rep = strlen(rep);
81         if (len_rep == 0)
82                 return orig;
83
84         if (!with)
85                 with = "";
86         len_with = strlen(with);
87
88         insert = orig;
89         for (count = 0; (tmp = strstr(insert, rep)); ++count) {
90                 insert = tmp + len_rep;
91         }
92
93         tmp = result = malloc(strlen(orig) + (len_with - len_rep) * count + 1);
94
95         if (!result)
96                 return orig;
97
98         while (count--) {
99                 insert = strstr(orig, rep);
100                 len_front = insert - orig;
101                 tmp = strncpy(tmp, orig, len_front) + len_front;
102                 tmp = strcpy(tmp, with) + len_with;
103                 orig += len_front + len_rep;
104         }
105         strcpy(tmp, orig);
106         return result;
107 }
108
109 char *expand_variables(struct thread_options *o, char *arguments)
110 {
111         char str[16];
112         char *expanded_runtime, *expanded_name;
113         snprintf(str, sizeof(str), "%lld", o->timeout / 1000000);
114
115         /* %r is replaced by the runtime in seconds */
116         expanded_runtime = str_replace(arguments, "%r", str);
117
118         /* %n is replaced by the name of the running job */
119         expanded_name = str_replace(expanded_runtime, "%n", o->name);
120
121         return expanded_name;
122 }
123
124 static int exec_background(struct thread_options *o, struct exec_options *eo)
125 {
126         char *outfilename = NULL, *errfilename = NULL;
127         int outfd = 0, errfd = 0;
128         pid_t pid;
129         char *expanded_arguments = NULL;
130         /* For the arguments splitting */
131         char **arguments_array = NULL;
132         char *p;
133         char *exec_cmd = NULL;
134         size_t arguments_nb_items = 0, q;
135
136         if (asprintf(&outfilename, "%s.stdout", o->name) < 0)
137                 return -1;
138
139         if (asprintf(&errfilename, "%s.stderr", o->name) < 0) {
140                 free(outfilename);
141                 return -1;
142         }
143
144         /* If we have variables in the arguments, let's expand them */
145         expanded_arguments = expand_variables(o, eo->arguments);
146
147         if (eo->std_redirect) {
148                 log_info("%s : Saving output of %s %s : stdout=%s stderr=%s\n",
149                          o->name, eo->program, expanded_arguments, outfilename,
150                          errfilename);
151
152                 /* Creating the stderr & stdout output files */
153                 outfd = open(outfilename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
154                 if (!outfd) {
155                         log_err("fio: cannot open output file %s : %s\n",
156                                 outfilename, strerror(errno));
157                         free(outfilename);
158                         free(errfilename);
159                         return -1;
160                 }
161
162                 errfd = open(errfilename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
163                 if (!errfd) {
164                         log_err("fio: cannot open output file %s : %s\n",
165                                 errfilename, strerror(errno));
166                         free(outfilename);
167                         free(errfilename);
168                         return -1;
169                 }
170         } else {
171                 log_info("%s : Running %s %s\n",
172                          o->name, eo->program, expanded_arguments);
173         }
174
175         pid = fork();
176
177         /* We are on the control thread (parent side of the fork */
178         if (pid > 0) {
179                 eo->pid = pid;
180                 if (eo->std_redirect) {
181                         /* The output file is for the client side of the fork */
182                         close(outfd);
183                         close(errfd);
184                         free(outfilename);
185                         free(errfilename);
186                 }
187                 return 0;
188         }
189
190         /* If the fork failed */
191         if (pid < 0) {
192                 log_err("fio: forking failed %s \n", strerror(errno));
193                 if (eo->std_redirect) {
194                         close(outfd);
195                         close(errfd);
196                         free(outfilename);
197                         free(errfilename);
198                 }
199                 return -1;
200         }
201
202         /* We are in the worker (child side of the fork) */
203         if (pid == 0) {
204                 if (eo->std_redirect) {
205                         dup2(outfd, 1); // replace stdout by the output file we create
206                         dup2(errfd, 2); // replace stderr by the output file we create
207                         close(outfd);
208                         close(errfd);
209                         free(outfilename);
210                         free(errfilename);
211                 }
212
213                 /* Let's split the command line into a null terminated array to be passed to the exec'd program
214                    But don't asprintf expanded_arguments if NULL as it would be converted
215                    to a '(null)' argument, while we want no arguments at all. */
216                 if (expanded_arguments != NULL) {
217                         if (asprintf(&exec_cmd, "%s %s", eo->program, expanded_arguments) < 0)
218                                 return -1;
219                 } else {
220                         if (asprintf(&exec_cmd, "%s", eo->program) < 0)
221                                 return -1;
222                 }
223
224                 /* Let's build an argv array to based on the program name and arguments */
225                 p = exec_cmd;
226                 for (;;) {
227                         p += strspn(p, " ");
228
229                         if (!(q = strcspn(p, " ")))
230                                 break;
231
232                         if (q) {
233                                 arguments_array =
234                                     realloc(arguments_array,
235                                             (arguments_nb_items +
236                                              1) * sizeof(char *));
237                                 arguments_array[arguments_nb_items] =
238                                     malloc(q + 1);
239                                 strncpy(arguments_array[arguments_nb_items], p,
240                                         q);
241                                 arguments_array[arguments_nb_items][q] = 0;
242                                 arguments_nb_items++;
243                                 p += q;
244                         }
245                 }
246
247                 /* Adding a null-terminated item to close the list */
248                 arguments_array =
249                     realloc(arguments_array,
250                             (arguments_nb_items + 1) * sizeof(char *));
251                 arguments_array[arguments_nb_items] = NULL;
252
253                 /* Replace the fio program from the child fork by the target program */
254                 execvp(arguments_array[0], arguments_array);
255         }
256         // We never reach this place
257         return 0;
258 }
259
260 static enum fio_q_status
261 fio_exec_queue(struct thread_data *td, struct io_u fio_unused * io_u)
262 {
263         struct thread_options *o = &td->o;
264         struct exec_options *eo = td->eo;
265
266         /* Let's execute the program the first time we get queued */
267         if (eo->pid == -1) {
268                 exec_background(o, eo);
269         } else {
270                 /* The program is running in background, let's check on a regular basis
271                    if the time is over and if we need to stop the tool */
272                 usleep(o->thinktime);
273                 if (utime_since_now(&td->start) > o->timeout) {
274                         /* Let's stop the child */
275                         kill(eo->pid, SIGTERM);
276                         /* Let's give grace_time (1 sec by default) to the 3rd party tool to stop */
277                         sleep(eo->grace_time);
278                 }
279         }
280
281         return FIO_Q_COMPLETED;
282 }
283
284 static int fio_exec_init(struct thread_data *td)
285 {
286         struct thread_options *o = &td->o;
287         struct exec_options *eo = td->eo;
288         int td_previous_state;
289
290         eo->pid = -1;
291
292         if (!eo->program) {
293                 td_vmsg(td, EINVAL,
294                         "no program is defined, it is mandatory to define one",
295                         "exec");
296                 return 1;
297         }
298
299         log_info("%s : program=%s, arguments=%s\n",
300                  td->o.name, eo->program, eo->arguments);
301
302         /* Saving the current thread state */
303         td_previous_state = td->runstate;
304
305         /* Reporting that we are preparing the engine
306          * This is useful as the qsort() calibration takes time
307          * This prevents the job from starting before init is completed
308          */
309         td_set_runstate(td, TD_SETTING_UP);
310
311         /*
312          * set thinktime_sleep and thinktime_spin appropriately
313          */
314         o->thinktime_blocks = 1;
315         o->thinktime_blocks_type = THINKTIME_BLOCKS_TYPE_COMPLETE;
316         o->thinktime_spin = 0;
317         o->thinktime = 50000;   /* 50ms pause when waiting for the program to complete */
318
319         o->nr_files = o->open_files = 1;
320
321         /* Let's restore the previous state. */
322         td_set_runstate(td, td_previous_state);
323         return 0;
324 }
325
326 static void fio_exec_cleanup(struct thread_data *td)
327 {
328         struct exec_options *eo = td->eo;
329         /* Send a sigkill to ensure the job is well terminated */
330         if (eo->pid > 0)
331                 kill(eo->pid, SIGKILL);
332 }
333
334 static int
335 fio_exec_open(struct thread_data fio_unused * td,
336               struct fio_file fio_unused * f)
337 {
338         return 0;
339 }
340
341 static struct ioengine_ops ioengine = {
342         .name = "exec",
343         .version = FIO_IOOPS_VERSION,
344         .queue = fio_exec_queue,
345         .init = fio_exec_init,
346         .cleanup = fio_exec_cleanup,
347         .open_file = fio_exec_open,
348         .flags = FIO_SYNCIO | FIO_DISKLESSIO | FIO_NOIO,
349         .options = options,
350         .option_struct_size = sizeof(struct exec_options),
351 };
352
353 static void fio_init fio_exec_register(void)
354 {
355         register_ioengine(&ioengine);
356 }
357
358 static void fio_exit fio_exec_unregister(void)
359 {
360         unregister_ioengine(&ioengine);
361 }