HOST: Reimplement and add support for TIMELIMIT subcommand.
[pspp] / src / language / utilities / host.c
1 /* pspp - a program for statistical analysis.
2    Copyright (C) 1997-9, 2000, 2009, 2010, 2011 Free Software Foundation, Inc.
3
4    This program is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13
14    You should have received a copy of the GNU General Public License
15    along with this program.  If not, see <http://www.gnu.org/licenses/>. */
16
17 #include <config.h>
18
19 #include <ctype.h>
20 #include <errno.h>
21 #include <fcntl.h>
22 #include <signal.h>
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <sys/time.h>
26 #include <unistd.h>
27 #if HAVE_SYS_WAIT_H
28 #include <sys/wait.h>
29 #endif
30
31 #include "data/settings.h"
32 #include "language/command.h"
33 #include "language/lexer/lexer.h"
34 #include "libpspp/assertion.h"
35 #include "libpspp/compiler.h"
36 #include "libpspp/i18n.h"
37 #include "libpspp/message.h"
38 #include "libpspp/str.h"
39 #include "libpspp/string-array.h"
40 #include "libpspp/temp-file.h"
41 #include "output/text-item.h"
42
43 #include "gl/intprops.h"
44 #include "gl/localcharset.h"
45 #include "gl/read-file.h"
46 #include "gl/timespec.h"
47 #include "gl/xalloc.h"
48 #include "gl/xmalloca.h"
49
50 #include "gettext.h"
51 #define _(msgid) gettext (msgid)
52 #define N_(msgid) msgid
53 \f
54 #if !HAVE_FORK
55 static bool
56 run_commands (const struct string_array *commands, double time_limit)
57 {
58   if (time_limit != DBL_MAX)
59     {
60       msg (SE, _("Time limit not supported on this platform."));
61       return false;
62     }
63
64   for (size_t i = 0; i < commands->n; i++)
65     {
66       /* XXX No way to capture command output */
67       char *s = recode_string (locale_charset (), "UTF-8",
68                                commands->strings[i], -1);
69       int retval = system (s);
70       free (s);
71
72       if (retval)
73         {
74           msg (SE, _("%s: Command exited with status %d."),
75                commands->strings[i], retval);
76           return false;
77         }
78     }
79   return true;
80 }
81 #else
82 static bool
83 run_command (const char *command, struct timespec timeout)
84 {
85   /* Same exit codes used by 'sh'. */
86   enum {
87     EXIT_CANNOT_INVOKE = 126,
88     EXIT_ENOENT = 127,
89   };
90
91   /* Create a temporary file to capture command output. */
92   FILE *output_file = create_temp_file ();
93   if (!output_file)
94     {
95       msg (SE, _("Failed to create temporary file (%s)."), strerror (errno));
96       return false;
97     }
98
99   int dev_null_fd = open ("/dev/null", O_RDONLY);
100   if (dev_null_fd < 0)
101     {
102       msg (SE, _("/dev/null: Failed to open (%s)."), strerror (errno));
103       fclose (output_file);
104       return false;
105     }
106
107   char *locale_command = recode_string (locale_charset (), "UTF-8",
108                                         command, -1);
109
110   pid_t pid = fork ();
111   if (pid < 0)
112     {
113       close (dev_null_fd);
114       fclose (output_file);
115       free (locale_command);
116
117       msg (SE, _("Couldn't fork: %s."), strerror (errno));
118       return false;
119     }
120   else if (!pid)
121     {
122       /* Running in the child. */
123
124       /* Set up timeout. */
125       if (timeout.tv_sec < TYPE_MAXIMUM (time_t))
126         {
127           signal (SIGALRM, SIG_DFL);
128
129           struct timespec left = timespec_sub (timeout, current_timespec ());
130           if (timespec_sign (left) <= 0)
131             raise (SIGALRM);
132
133           struct itimerval it = {
134             .it_value = {
135               .tv_sec = left.tv_sec,
136               .tv_usec = left.tv_nsec / 1000
137             }
138           };
139           setitimer (ITIMER_REAL, &it, NULL);
140         }
141
142       /* Set up file descriptors:
143          - /dev/null for stdin
144          - Temporary file to capture stdout and stderr.
145          - Close everything else.
146       */
147       dup2 (dev_null_fd, 0);
148       dup2 (fileno (output_file), 1);
149       dup2 (fileno (output_file), 2);
150       close (dev_null_fd);
151       for (int fd = 3; fd < 256; fd++)
152         close (fd);
153
154       /* Choose the shell. */
155       const char *shell = getenv ("SHELL");
156       if (shell == NULL)
157         shell = "/bin/sh";
158
159       /* Run subprocess. */
160       execl (shell, shell, "-c", locale_command, NULL);
161
162       /* Failed to start the shell. */
163       _exit (errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE);
164     }
165
166   /* Running in the parent. */
167   close (dev_null_fd);
168   free (locale_command);
169
170   /* Wait for child to exit. */
171   int status = 0;
172   int error = 0;
173   for (;;)
174     {
175       pid_t retval = waitpid (pid, &status, 0);
176       if (retval == pid)
177         break;
178       else if (retval < 0)
179         {
180           if (errno != EINTR)
181             {
182               error = errno;
183               break;
184             }
185         }
186       else
187         NOT_REACHED ();
188     }
189
190   bool ok = true;
191   if (error)
192     {
193       msg (SW, _("While running \"%s\", waiting for child process "
194                  "failed (%s)."),
195            command, strerror (errno));
196       ok = false;
197     }
198
199   if (WIFSIGNALED (status))
200     {
201       int signum = WTERMSIG (status);
202       if (signum == SIGALRM)
203         msg (SW, _("Command \"%s\" timed out."), command);
204       else
205         msg (SW, _("Command \"%s\" terminated by signal %d."), command, signum);
206       ok = false;
207     }
208   else if (WIFEXITED (status) && WEXITSTATUS (status))
209     {
210       int exit_code = WEXITSTATUS (status);
211       const char *detail = (exit_code == EXIT_ENOENT
212                             ? _("Command or shell not found")
213                             : exit_code == EXIT_CANNOT_INVOKE
214                             ? _("Could not invoke command or shell")
215                             : NULL);
216       if (detail)
217         msg (SW, _("Command \"%s\" exited with status %d (%s)."),
218              command, exit_code, detail);
219       else
220         msg (SW, _("Command \"%s\" exited with status %d."),
221              command, exit_code);
222       ok = false;
223     }
224
225   rewind (output_file);
226   size_t length;
227   char *locale_output = fread_file (output_file, 0, &length);
228   if (!locale_output)
229     {
230       msg (SW, _("Command \"%s\" output could not be read (%s)."),
231            command, strerror (errno));
232       ok = false;
233     }
234   else if (length > 0)
235     {
236       char *output = recode_string ("UTF-8", locale_charset (),
237                                     locale_output, -1);
238
239       /* Drop final new-line, if any. */
240       char *end = strchr (output, '\0');
241       if (end > output && end[-1] == '\n')
242         end[-1] = '\0';
243
244       text_item_submit (text_item_create_nocopy (TEXT_ITEM_LOG, output));
245     }
246   free (locale_output);
247
248   return ok;
249 }
250
251 static bool
252 run_commands (const struct string_array *commands, double time_limit)
253 {
254   struct timespec timeout = timespec_add (dtotimespec (time_limit),
255                                           current_timespec ());
256
257   for (size_t i = 0; i < commands->n; i++)
258     {
259       if (!run_command (commands->strings[i], timeout))
260         return false;
261     }
262
263   return true;
264 }
265 #endif
266
267 int
268 cmd_host (struct lexer *lexer, struct dataset *ds UNUSED)
269 {
270   if (settings_get_safer_mode ())
271     {
272       msg (SE, _("This command not allowed when the %s option is set."), "SAFER");
273       return CMD_FAILURE;
274     }
275
276   if (!lex_force_match_id (lexer, "COMMAND")
277       || !lex_force_match (lexer, T_EQUALS)
278       || !lex_force_match (lexer, T_LBRACK)
279       || !lex_force_string (lexer))
280     return CMD_FAILURE;
281
282   struct string_array commands = STRING_ARRAY_INITIALIZER;
283   while (lex_token (lexer) == T_STRING)
284     {
285       string_array_append (&commands, lex_tokcstr (lexer));
286       lex_get (lexer);
287     }
288   if (!lex_force_match (lexer, T_RBRACK))
289     {
290       string_array_destroy (&commands);
291       return CMD_FAILURE;
292     }
293
294   double time_limit = DBL_MAX;
295   if (lex_match_id (lexer, "TIMELIMIT"))
296     {
297       if (!lex_force_match (lexer, T_EQUALS)
298           || !lex_force_num (lexer))
299         {
300           string_array_destroy (&commands);
301           return CMD_FAILURE;
302         }
303
304       double num = lex_number (lexer);
305       lex_get (lexer);
306       time_limit = num < 0.0 ? 0.0 : num;
307     }
308
309   enum cmd_result result = lex_end_of_command (lexer);
310   if (result == CMD_SUCCESS && !run_commands (&commands, time_limit))
311     result = CMD_FAILURE;
312   string_array_destroy (&commands);
313   return result;
314 }