HOST: Reimplement and add support for TIMELIMIT subcommand.
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 2 Sep 2020 01:54:00 +0000 (18:54 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 2 Sep 2020 01:54:16 +0000 (18:54 -0700)
Fixes bug #25816.
Thanks to John Darrington for reporting this bug.

NEWS
Smake
doc/utilities.texi
src/language/utilities/host.c
tests/atlocal.in
tests/automake.mk
tests/data/file.at

diff --git a/NEWS b/NEWS
index 8e562cdd845792af24b4c15bec4c19d961271794..0d969504fcef0331101985913c792c02b51eea8d 100644 (file)
--- 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 caae02fd97718c881cd5bf6ef95f978b707a3fe7..b38e98040d69f1d45f94677aebba8a1a9efe0cb3 100644 (file)
--- 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 \
index 8aeb4a0b0d352bde86a720e4985a3b88e458d66b..6549e94f2cd0c7c5651b35fc283fc3c8055d0388 100644 (file)
@@ -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
index b01e621bf14e9bf7133ec4249f72af5f99c5ca13..4a32d463973862739942190cae7cadccb5aa5739 100644 (file)
@@ -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
 
 #include <config.h>
 
-#include <stdio.h>
-#include <stdlib.h>
 #include <ctype.h>
 #include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/time.h>
 #include <unistd.h>
 #if HAVE_SYS_WAIT_H
 #include <sys/wait.h>
 #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"
 
 #define _(msgid) gettext (msgid)
 #define N_(msgid) msgid
 \f
-#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;
 }
index 35e763ed66de56697a957561d7077cba29cad0e4..df10bef95c6cd1755a6532c3790d2c9fcd30ba78 100644 (file)
@@ -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
index c5f01b16bdc97a4be7b08cfb691160042b88a13a..66e17bb2a7010b4cdbd190f20932ee9fec18faec 100644 (file)
@@ -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 \
index d3013d7501ab2be6efc16698de34b96bc81b0a80..e48fe05a65c95e190c9891a008b8b69f84bfc4b3 100644 (file)
@@ -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.