From 13bc7e5d95ddbcfcce29080eb2ca2b6eb17d7433 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Tue, 1 Sep 2020 18:54:00 -0700 Subject: [PATCH] HOST: Reimplement and add support for TIMELIMIT subcommand. Fixes bug #25816. Thanks to John Darrington for reporting this bug. --- NEWS | 4 +- Smake | 5 + doc/utilities.texi | 50 ++++-- src/language/utilities/host.c | 325 ++++++++++++++++++++++++---------- tests/atlocal.in | 6 +- tests/automake.mk | 1 + tests/data/file.at | 2 +- 7 files changed, 287 insertions(+), 106 deletions(-) diff --git a/NEWS b/NEWS index 8e562cdd84..0d969504fc 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,8 @@ Changes from 1.4.0 to 1.5.0: * HTML output is now HTML5 instead of HTML4.0 Transitional. + * The HOST command has been reimplemented. It now supports TIMELIMIT. + Changes from 1.2.0 to 1.4.0: * The file pspp-mode.el (the pspp-mode for Emacs) is no longer distributed @@ -16,7 +18,7 @@ Changes from 1.2.0 to 1.4.0: * The Find dialog box, when searching for numeric values, will match only to the precision of the variable's print format. This avoids behaviour - which is suprising to some users. + which is surprising to some users. * PSPP now supports the SPSS viewer (.spv) format that SPSS 16 and later use to save the contents of its output editor: diff --git a/Smake b/Smake index caae02fd97..b38e98040d 100644 --- a/Smake +++ b/Smake @@ -43,6 +43,7 @@ GNULIB_MODULES = \ crypto/rijndael \ dirname \ dtoastr \ + dtotimespec \ environ \ fatal-signal \ fcntl \ @@ -56,6 +57,7 @@ GNULIB_MODULES = \ getline \ getpass \ gettext-h \ + gettime \ gettimeofday \ getopt-gnu \ gitlog-to-changelog \ @@ -105,6 +107,9 @@ GNULIB_MODULES = \ sys_stat \ tempname \ termios \ + timespec \ + timespec-add \ + timespec-sub \ trunc \ unicase/u8-casecmp \ unicase/u8-casefold \ diff --git a/doc/utilities.texi b/doc/utilities.texi index 8aeb4a0b0d..6549e94f2c 100644 --- a/doc/utilities.texi +++ b/doc/utilities.texi @@ -236,21 +236,51 @@ control to the operating system. @section HOST @vindex HOST +In the syntax below, the square brackets must be included in the +command syntax and do not indicate that that their contents are +optional. + @display -HOST. -HOST COMMAND=['@var{command}'...]. +HOST COMMAND=['@var{command}'...] + TIMELIMIT=@var{secs}. @end display -@cmd{HOST} suspends the current @pspp{} session and temporarily returns control -to the operating system. -This command cannot be used if the SAFER (@pxref{SET}) setting is active. +@cmd{HOST} executes one or more commands, each provided as a string in +the required @subcmd{COMMAND} subcommand, in the shell of the +underlying operating system. PSPP runs each command in a separate +shell process and waits for it to finish before running the next one. +If a command fails (with a nonzero exit status, or because it is +killed by a signal), then PSPP does not run any remaining commands. + +PSPP provides @file{/dev/null} as the shell's standard input. If a +process needs to read from stdin, redirect from a file or device, or +use a pipe. + +PSPP displays the shell's standard output and standard error as PSPP +output. Redirect to a file or @code{/dev/null} or another device if +this is not desired. + +The following example runs @code{rsync} to copy a file from a remote +server to the local file @file{data.txt}, writing @code{rsync}'s own +output to @file{rsync-log.txt}. PSPP displays the command's error +output, if any. If @code{rsync} needs to prompt the user (e.g.@: to +obtain a password), the command fails. Only if the @code{rsync} +succeeds, PSPP then runs the @code{sha512sum} command. + +@example +HOST COMMAND=['rsync remote:data.txt data.txt > rsync-log.txt' + 'sha512sum -c data.txt.sha512sum]. +@end example + +By default, PSPP waits as long as necessary for the series of commands +to complete. Use the optional @subcmd{TIMELIMIT} subcommand to limit +the execution time to the specified number of seconds. -If the @subcmd{COMMAND} subcommand is specified, as a sequence of shell -commands as quoted strings within square brackets, then @pspp{} executes -them together in a single subshell. +PSPP built for mingw does not support all the features of +@subcmd{HOST}. -If no subcommands are specified, then @pspp{} invokes an interactive -subshell. +PSPP rejects this command if the SAFER (@pxref{SET}) setting is +active. @node INCLUDE @section INCLUDE diff --git a/src/language/utilities/host.c b/src/language/utilities/host.c index b01e621bf1..4a32d46397 100644 --- a/src/language/utilities/host.c +++ b/src/language/utilities/host.c @@ -1,4 +1,4 @@ -/* PSPP - a program for statistical analysis. +/* pspp - a program for statistical analysis. Copyright (C) 1997-9, 2000, 2009, 2010, 2011 Free Software Foundation, Inc. This program is free software: you can redistribute it and/or modify @@ -16,10 +16,13 @@ #include -#include -#include #include #include +#include +#include +#include +#include +#include #include #if HAVE_SYS_WAIT_H #include @@ -33,8 +36,14 @@ #include "libpspp/i18n.h" #include "libpspp/message.h" #include "libpspp/str.h" +#include "libpspp/string-array.h" +#include "libpspp/temp-file.h" +#include "output/text-item.h" +#include "gl/intprops.h" #include "gl/localcharset.h" +#include "gl/read-file.h" +#include "gl/timespec.h" #include "gl/xalloc.h" #include "gl/xmalloca.h" @@ -42,85 +51,218 @@ #define _(msgid) gettext (msgid) #define N_(msgid) msgid -#if HAVE_FORK && HAVE_EXECL -/* Spawn an interactive shell process. */ +#if !HAVE_FORK static bool -shell (void) +run_commands (const struct string_array *commands, double time_limit) { - int pid; - - pid = fork (); - switch (pid) + if (time_limit != DBL_MAX) { - case 0: - { - const char *shell_fn; - char *shell_process; + msg (SE, _("Time limit not supported on this platform.")); + return false; + } - { - int i; + for (size_t i = 0; i < commands->n; i++) + { + /* XXX No way to capture command output */ + char *s = recode_string (locale_charset (), "UTF-8", + commands->strings[i], -1); + int retval = system (s); + free (s); - for (i = 3; i < 20; i++) - close (i); - } + if (retval) + { + msg (SE, _("%s: Command exited with status %d."), + commands->strings[i], retval); + return false; + } + } + return true; +} +#else +static bool +run_command (const char *command, struct timespec timeout) +{ + /* Same exit codes used by 'sh'. */ + enum { + EXIT_CANNOT_INVOKE = 126, + EXIT_ENOENT = 127, + }; - shell_fn = getenv ("SHELL"); - if (shell_fn == NULL) - shell_fn = "/bin/sh"; + /* Create a temporary file to capture command output. */ + FILE *output_file = create_temp_file (); + if (!output_file) + { + msg (SE, _("Failed to create temporary file (%s)."), strerror (errno)); + return false; + } - { - const char *cp = strrchr (shell_fn, '/'); - cp = cp ? &cp[1] : shell_fn; - shell_process = xmalloca (strlen (cp) + 8); - strcpy (shell_process, "-"); - strcat (shell_process, cp); - if (strcmp (cp, "sh")) - shell_process[0] = '+'; - } + int dev_null_fd = open ("/dev/null", O_RDONLY); + if (dev_null_fd < 0) + { + msg (SE, _("/dev/null: Failed to open (%s)."), strerror (errno)); + fclose (output_file); + return false; + } - execl (shell_fn, shell_process, NULL); + char *locale_command = recode_string (locale_charset (), "UTF-8", + command, -1); - _exit (1); - } + pid_t pid = fork (); + if (pid < 0) + { + close (dev_null_fd); + fclose (output_file); + free (locale_command); - case -1: msg (SE, _("Couldn't fork: %s."), strerror (errno)); return false; + } + else if (!pid) + { + /* Running in the child. */ + + /* Set up timeout. */ + if (timeout.tv_sec < TYPE_MAXIMUM (time_t)) + { + signal (SIGALRM, SIG_DFL); + + struct timespec left = timespec_sub (timeout, current_timespec ()); + if (timespec_sign (left) <= 0) + raise (SIGALRM); + + struct itimerval it = { + .it_value = { + .tv_sec = left.tv_sec, + .tv_usec = left.tv_nsec / 1000 + } + }; + setitimer (ITIMER_REAL, &it, NULL); + } - default: - assert (pid > 0); - while (wait (NULL) != pid) - ; - return true; + /* Set up file descriptors: + - /dev/null for stdin + - Temporary file to capture stdout and stderr. + - Close everything else. + */ + dup2 (dev_null_fd, 0); + dup2 (fileno (output_file), 1); + dup2 (fileno (output_file), 2); + close (dev_null_fd); + for (int fd = 3; fd < 256; fd++) + close (fd); + + /* Choose the shell. */ + const char *shell = getenv ("SHELL"); + if (shell == NULL) + shell = "/bin/sh"; + + /* Run subprocess. */ + execl (shell, shell, "-c", locale_command, NULL); + + /* Failed to start the shell. */ + _exit (errno == ENOENT ? EXIT_ENOENT : EXIT_CANNOT_INVOKE); } + + /* Running in the parent. */ + close (dev_null_fd); + free (locale_command); + + /* Wait for child to exit. */ + int status = 0; + int error = 0; + for (;;) + { + pid_t retval = waitpid (pid, &status, 0); + if (retval == pid) + break; + else if (retval < 0) + { + if (errno != EINTR) + { + error = errno; + break; + } + } + else + NOT_REACHED (); + } + + bool ok = true; + if (error) + { + msg (SW, _("While running \"%s\", waiting for child process " + "failed (%s)."), + command, strerror (errno)); + ok = false; + } + + if (WIFSIGNALED (status)) + { + int signum = WTERMSIG (status); + if (signum == SIGALRM) + msg (SW, _("Command \"%s\" timed out."), command); + else + msg (SW, _("Command \"%s\" terminated by signal %d."), command, signum); + ok = false; + } + else if (WIFEXITED (status) && WEXITSTATUS (status)) + { + int exit_code = WEXITSTATUS (status); + const char *detail = (exit_code == EXIT_ENOENT + ? _("Command or shell not found") + : exit_code == EXIT_CANNOT_INVOKE + ? _("Could not invoke command or shell") + : NULL); + if (detail) + msg (SW, _("Command \"%s\" exited with status %d (%s)."), + command, exit_code, detail); + else + msg (SW, _("Command \"%s\" exited with status %d."), + command, exit_code); + ok = false; + } + + rewind (output_file); + size_t length; + char *locale_output = fread_file (output_file, 0, &length); + if (!locale_output) + { + msg (SW, _("Command \"%s\" output could not be read (%s)."), + command, strerror (errno)); + ok = false; + } + else if (length > 0) + { + char *output = recode_string ("UTF-8", locale_charset (), + locale_output, -1); + + /* Drop final new-line, if any. */ + char *end = strchr (output, '\0'); + if (end > output && end[-1] == '\n') + end[-1] = '\0'; + + text_item_submit (text_item_create_nocopy (TEXT_ITEM_LOG, output)); + } + free (locale_output); + + return ok; } -#else /* !(HAVE_FORK && HAVE_EXECL) */ -/* Don't know how to spawn an interactive shell. */ -static bool -shell (void) -{ - msg (SE, _("Interactive shell not supported on this platform.")); - return false; -} -#endif -/* Executes the specified COMMAND in a subshell. Returns true if - successful, false otherwise. */ static bool -run_command (const char *command) +run_commands (const struct string_array *commands, double time_limit) { - if (system (NULL) == 0) + struct timespec timeout = timespec_add (dtotimespec (time_limit), + current_timespec ()); + + for (size_t i = 0; i < commands->n; i++) { - msg (SE, _("Command shell not supported on this platform.")); - return false; + if (!run_command (commands->strings[i], timeout)) + return false; } - /* Execute the command. */ - if (system (command) == -1) - msg (SE, _("Error executing command: %s."), strerror (errno)); - return true; } +#endif int cmd_host (struct lexer *lexer, struct dataset *ds UNUSED) @@ -131,45 +273,42 @@ cmd_host (struct lexer *lexer, struct dataset *ds UNUSED) return CMD_FAILURE; } - if (lex_token (lexer) == T_ENDCMD) - return shell () ? CMD_SUCCESS : CMD_FAILURE; - else if (lex_match_id (lexer, "COMMAND")) - { - struct string command; - char *locale_command; - bool ok; + if (!lex_force_match_id (lexer, "COMMAND") + || !lex_force_match (lexer, T_EQUALS) + || !lex_force_match (lexer, T_LBRACK) + || !lex_force_string (lexer)) + return CMD_FAILURE; - lex_match (lexer, T_EQUALS); - if (!lex_force_match (lexer, T_LBRACK)) - return CMD_FAILURE; + struct string_array commands = STRING_ARRAY_INITIALIZER; + while (lex_token (lexer) == T_STRING) + { + string_array_append (&commands, lex_tokcstr (lexer)); + lex_get (lexer); + } + if (!lex_force_match (lexer, T_RBRACK)) + { + string_array_destroy (&commands); + return CMD_FAILURE; + } - ds_init_empty (&command); - while (lex_is_string (lexer)) - { - if (!ds_is_empty (&command)) - ds_put_byte (&command, '\n'); - ds_put_substring (&command, lex_tokss (lexer)); - lex_get (lexer); - } - if (!lex_force_match (lexer, T_RBRACK)) + double time_limit = DBL_MAX; + if (lex_match_id (lexer, "TIMELIMIT")) + { + if (!lex_force_match (lexer, T_EQUALS) + || !lex_force_num (lexer)) { - ds_destroy (&command); + string_array_destroy (&commands); return CMD_FAILURE; } - locale_command = recode_string (locale_charset (), "UTF-8", - ds_cstr (&command), - ds_length (&command)); - ds_destroy (&command); - - ok = run_command (locale_command); - free (locale_command); - - return ok ? CMD_SUCCESS : CMD_FAILURE; - } - else - { - lex_error (lexer, NULL); - return CMD_FAILURE; + double num = lex_number (lexer); + lex_get (lexer); + time_limit = num < 0.0 ? 0.0 : num; } + + enum cmd_result result = lex_end_of_command (lexer); + if (result == CMD_SUCCESS && !run_commands (&commands, time_limit)) + result = CMD_FAILURE; + string_array_destroy (&commands); + return result; } diff --git a/tests/atlocal.in b/tests/atlocal.in index 35e763ed66..df10bef95c 100644 --- a/tests/atlocal.in +++ b/tests/atlocal.in @@ -63,7 +63,6 @@ export GZIP WINEPREFIX=$HOME/.wine # Work around the following kludge to keep wine happy export WINEPREFIX - HOME=/nonexistent # Kluge to make PSPP ignore $HOME/.pspprc. export HOME @@ -77,6 +76,11 @@ if test X"$RUNNER" = Xwine; then } fi +case $host in + *-*-mingw*) MINGW=: ;; + *) MINGW=false ;; +esac + # Enable leak suppressions for Address Sanitizer/Leak Sanitizer. LSAN_OPTIONS="suppressions=$abs_top_srcdir/tests/lsan.supp print_suppressions=0" export LSAN_OPTIONS diff --git a/tests/automake.mk b/tests/automake.mk index c5f01b16bd..66e17bb2a7 100644 --- a/tests/automake.mk +++ b/tests/automake.mk @@ -380,6 +380,7 @@ TESTSUITE_AT = \ tests/language/utilities/cache.at \ tests/language/utilities/cd.at \ tests/language/utilities/date.at \ + tests/language/utilities/host.at \ tests/language/utilities/insert.at \ tests/language/utilities/permissions.at \ tests/language/utilities/set.at \ diff --git a/tests/data/file.at b/tests/data/file.at index d3013d7501..e48fe05a65 100644 --- a/tests/data/file.at +++ b/tests/data/file.at @@ -84,7 +84,7 @@ AT_CLEANUP AT_SETUP([Write fifo]) dnl The Fifo feature is not available in w32 builds -AT_SKIP_IF([case $host in *-*-mingw*) true ;; *) false ;; esac]) +AT_SKIP_IF([$MINGW]) AT_DATA([file.sps], [dnl DATA LIST NOTABLE/x 1. -- 2.30.2