@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
-/* 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)
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;
}