Robert Haas <robertmh...@gmail.com> writes:
> On Thu, Aug 25, 2022 at 10:48 AM Tom Lane <t...@sss.pgh.pa.us> wrote:
>> If we were executing a program that the user needs to have some control
>> over, sure, but what we have here is an implementation detail that I
>> doubt anyone cares about.  The fact that we're using a shell at all is
>> only because nobody has cared to manually implement I/O redirection logic
>> in these places; otherwise we'd be execl()'ing the server or psql directly.
>> Maybe the best answer would be to do that, and get out of the business
>> of knowing where the shell is?

> Well that also would not be crazy.

I experimented with this, and it seems like it might not be as awful as
we've always assumed it would be.  Attached is an incomplete POC that
converts pg_regress proper to doing things this way.  (isolationtester
and pg_regress_ecpg are outright broken by this patch, because they rely
on pg_regress' spawn_process and I didn't fix them yet.  But you can run
the core regression tests to see it works.)

The Windows side of this is completely untested and may be broken; also,
perhaps Windows has something more nearly equivalent to execvp() that we
could use instead of reconstructing a command line?  It's annoying that
the patch removes all shell-quoting hazards on the Unix side but they
are still there on the Windows side.

                        regards, tom lane

diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c
index a803355f8e..e1e0d5f7a2 100644
--- a/src/test/regress/pg_regress.c
+++ b/src/test/regress/pg_regress.c
@@ -19,6 +19,7 @@
 #include "postgres_fe.h"
 
 #include <ctype.h>
+#include <fcntl.h>
 #include <sys/resource.h>
 #include <sys/stat.h>
 #include <sys/time.h>
@@ -51,10 +52,6 @@ typedef struct _resultmap
  */
 char	   *host_platform = HOST_TUPLE;
 
-#ifndef WIN32					/* not used in WIN32 case */
-static char *shellprog = SHELLPROG;
-#endif
-
 /*
  * On Windows we use -w in diff switches to avoid problems with inconsistent
  * newline representation.  The actual result files will generally have
@@ -73,7 +70,7 @@ _stringlist *dblist = NULL;
 bool		debug = false;
 char	   *inputdir = ".";
 char	   *outputdir = ".";
-char       *expecteddir = ".";
+char	   *expecteddir = ".";
 char	   *bindir = PGBINDIR;
 char	   *launcher = NULL;
 static _stringlist *loadextension = NULL;
@@ -1052,12 +1049,71 @@ psql_end_command(StringInfo buf, const char *database)
 	} while (0)
 
 /*
- * Spawn a process to execute the given shell command; don't wait for it
+ * Redirect f to the file specified by fpath, opened with given flags and mode
+ * Does not return upon failure
+ */
+static void
+redirect_to(FILE *f, const char *fpath, int flags, int mode)
+{
+	int			fh;
+
+	/* Let's just be sure the FILE is idle */
+	fflush(f);
+
+	fh = open(fpath, flags, mode);
+	if (fh < 0)
+	{
+		fprintf(stderr, "could not open file \"%s\": %m\n", fpath);
+		_exit(1);
+	}
+	if (dup2(fh, fileno(f)) < 0)
+	{
+		fprintf(stderr, "could not duplicate descriptor for file \"%s\": %m\n",
+				fpath);
+		_exit(1);
+	}
+	(void) close(fh);
+}
+
+/*
+ * Redirect f to the same file used by fref
+ * Does not return upon failure
+ */
+static void
+redirect_link(FILE *f, FILE *fref)
+{
+	/* Let's just be sure the FILE is idle */
+	fflush(f);
+
+	if (dup2(fileno(fref), fileno(f)) < 0)
+	{
+		fprintf(stderr, "could not duplicate file descriptor: %m\n");
+		_exit(1);
+	}
+}
+
+/*
+ * Spawn a process to execute a sub-command; don't wait for it
  *
  * Returns the process ID (or HANDLE) so we can wait for it later
+ * Does not return upon failure
+ *
+ * Arguments:
+ * file: program to be executed (may be a full path, or just program name)
+ * argv: NULL-terminated array of argument strings, as for execvp(2);
+ *       argv[0] should normally be the same as file
+ * proc_stdin: filename to make child's stdin read from, or NULL
+ *       for no redirection
+ * proc_stdout: filename to make child's stdout write to, or NULL
+ *       for no redirection
+ * proc_stderr: filename to make child's stderr write to, or NULL
+ *       for no redirection, or "&1" to link it to child's stdout
  */
 PID_TYPE
-spawn_process(const char *cmdline)
+spawn_process(const char *file, char *argv[],
+			  const char *proc_stdin,
+			  const char *proc_stdout,
+			  const char *proc_stderr)
 {
 #ifndef WIN32
 	pid_t		pid;
@@ -1085,35 +1141,65 @@ spawn_process(const char *cmdline)
 	if (pid == 0)
 	{
 		/*
-		 * In child
-		 *
-		 * Instead of using system(), exec the shell directly, and tell it to
-		 * "exec" the command too.  This saves two useless processes per
-		 * parallel test case.
+		 * In child: redirect stdio as requested, then execvp()
 		 */
-		char	   *cmdline2;
-
-		cmdline2 = psprintf("exec %s", cmdline);
-		execl(shellprog, shellprog, "-c", cmdline2, (char *) NULL);
+		if (proc_stdin)
+			redirect_to(stdin, proc_stdin, O_RDONLY, 0);
+		if (proc_stdout)
+			redirect_to(stdout, proc_stdout, O_WRONLY | O_CREAT | O_TRUNC,
+						S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
+		if (proc_stderr)
+		{
+			if (strcmp(proc_stderr, "&1") == 0)
+				redirect_link(stderr, stdout);
+			else
+				redirect_to(stderr, proc_stderr, O_WRONLY | O_CREAT | O_TRUNC,
+							S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
+		}
+		execvp(file, argv);
+		/* must have failed */
 		fprintf(stderr, _("%s: could not exec \"%s\": %s\n"),
-				progname, shellprog, strerror(errno));
+				progname, file, strerror(errno));
 		_exit(1);				/* not exit() here... */
 	}
 	/* in parent */
 	return pid;
 #else
 	PROCESS_INFORMATION pi;
+	StringInfoData cmdline;
 	char	   *cmdline2;
 	HANDLE		restrictedToken;
 	const char *comspec;
 
+	/*
+	 * Construct the equivalent command string.  We assume simplistic double
+	 * quoting of the arguments is sufficient.
+	 */
+	initStringInfo(&cmdline);
+	appendStringInfo(&cmdline, "\"%s\"", path);
+	for (int i = 1; argv[i] != NULL; i++)
+		appendStringInfo(&cmdline, " \"%s\"", argv[i]);
+	if (proc_stdin)
+		appendStringInfo(&cmdline, " <\"%s\"", proc_stdin);
+	if (proc_stdout)
+		appendStringInfo(&cmdline, " >\"%s\"", proc_stdout);
+	if (proc_stderr)
+	{
+		if (strcmp(proc_stderr, "&1") == 0)
+			appendStringInfo(&cmdline, " 2>&1");
+		else
+			appendStringInfo(&cmdline, " 2>\"%s\"", proc_stderr);
+	}
+
 	/* Find CMD.EXE location using COMSPEC, if it's set */
 	comspec = getenv("COMSPEC");
 	if (comspec == NULL)
 		comspec = "CMD";
 
+	/* Prepare command for CMD.EXE */
+	cmdline2 = psprintf("\"%s\" /c \"%s\"", comspec, cmdline.data);
+
 	memset(&pi, 0, sizeof(pi));
-	cmdline2 = psprintf("\"%s\" /c \"%s\"", comspec, cmdline);
 
 	if ((restrictedToken =
 		 CreateRestrictedProcess(cmdline2, &pi)) == 0)
@@ -2228,6 +2314,8 @@ regression_main(int argc, char *argv[],
 		FILE	   *pg_conf;
 		const char *env_wait;
 		int			wait_seconds;
+		char	   *pmargv[20];
+		char	   *outputname;
 
 		/*
 		 * Prepare the temp instance
@@ -2361,16 +2449,30 @@ regression_main(int argc, char *argv[],
 		 * Start the temp postmaster
 		 */
 		header(_("starting postmaster"));
-		snprintf(buf, sizeof(buf),
-				 "\"%s%spostgres\" -D \"%s/data\" -F%s "
-				 "-c \"listen_addresses=%s\" -k \"%s\" "
-				 "> \"%s/log/postmaster.log\" 2>&1",
-				 bindir ? bindir : "",
-				 bindir ? "/" : "",
-				 temp_instance, debug ? " -d 5" : "",
-				 hostname ? hostname : "", sockdir ? sockdir : "",
-				 outputdir);
-		postmaster_pid = spawn_process(buf);
+		i = 0;
+		pmargv[i++] = psprintf("%s%spostgres",
+							   bindir ? bindir : "",
+							   bindir ? "/" : "");
+		pmargv[i++] = "-D";
+		pmargv[i++] = psprintf("%s/data", temp_instance);
+		pmargv[i++] = "-F";
+		if (debug)
+		{
+			pmargv[i++] = "-d";
+			pmargv[i++] = "5";
+		}
+		pmargv[i++] = "-c";
+		pmargv[i++] = psprintf("listen_addresses=%s",
+							   hostname ? hostname : "");
+		pmargv[i++] = "-k";
+		pmargv[i++] = psprintf("%s", sockdir ? sockdir : "");
+		pmargv[i++] = NULL;
+		Assert(i <= lengthof(pmargv));
+		outputname = psprintf("%s/log/postmaster.log", outputdir);
+		postmaster_pid = spawn_process(pmargv[0], pmargv,
+									   NULL,
+									   outputname,
+									   "&1");
 		if (postmaster_pid == INVALID_PID)
 		{
 			fprintf(stderr, _("\n%s: could not spawn postmaster: %s\n"),
diff --git a/src/test/regress/pg_regress.h b/src/test/regress/pg_regress.h
index d8772fec8e..a9eb1a3ba1 100644
--- a/src/test/regress/pg_regress.h
+++ b/src/test/regress/pg_regress.h
@@ -65,5 +65,8 @@ int			regression_main(int argc, char *argv[],
 							postprocess_result_function postfunc);
 
 void		add_stringlist_item(_stringlist **listhead, const char *str);
-PID_TYPE	spawn_process(const char *cmdline);
+PID_TYPE	spawn_process(const char *file, char *argv[],
+						  const char *proc_stdin,
+						  const char *proc_stdout,
+						  const char *proc_stderr);
 bool		file_exists(const char *file);
diff --git a/src/test/regress/pg_regress_main.c b/src/test/regress/pg_regress_main.c
index a4b354c9e6..0b5ac124e9 100644
--- a/src/test/regress/pg_regress_main.c
+++ b/src/test/regress/pg_regress_main.c
@@ -34,8 +34,8 @@ psql_start_test(const char *testname,
 	char		infile[MAXPGPATH];
 	char		outfile[MAXPGPATH];
 	char		expectfile[MAXPGPATH];
-	char		psql_cmd[MAXPGPATH * 3];
-	size_t		offset = 0;
+	char	   *psqlargv[20];
+	int			i = 0;
 	char	   *appnameenv;
 
 	/*
@@ -63,40 +63,30 @@ psql_start_test(const char *testname,
 	add_stringlist_item(expectfiles, expectfile);
 
 	if (launcher)
-	{
-		offset += snprintf(psql_cmd + offset, sizeof(psql_cmd) - offset,
-						   "%s ", launcher);
-		if (offset >= sizeof(psql_cmd))
-		{
-			fprintf(stderr, _("command too long\n"));
-			exit(2);
-		}
-	}
-
-	/*
-	 * Use HIDE_TABLEAM to hide different AMs to allow to use regression tests
-	 * against different AMs without unnecessary differences.
-	 */
-	offset += snprintf(psql_cmd + offset, sizeof(psql_cmd) - offset,
-					   "\"%s%spsql\" -X -a -q -d \"%s\" %s < \"%s\" > \"%s\" 2>&1",
-					   bindir ? bindir : "",
-					   bindir ? "/" : "",
-					   dblist->str,
-					   "-v HIDE_TABLEAM=on -v HIDE_TOAST_COMPRESSION=on",
-					   infile,
-					   outfile);
-	if (offset >= sizeof(psql_cmd))
-	{
-		fprintf(stderr, _("command too long\n"));
-		exit(2);
-	}
+		psqlargv[i++] = launcher;
+
+	psqlargv[i++] = psprintf("%s%spsql",
+							 bindir ? bindir : "",
+							 bindir ? "/" : "");
+	psqlargv[i++] = "-Xaq";
+	psqlargv[i++] = "-d";
+	psqlargv[i++] = dblist->str;
+	psqlargv[i++] = "-v";
+	/* Hide TABLEAM and compression to allow tests against different AMs */
+	psqlargv[i++] = "HIDE_TABLEAM=on";
+	psqlargv[i++] = "-v";
+	psqlargv[i++] = "HIDE_TOAST_COMPRESSION=on";
+	psqlargv[i++] = NULL;
+	Assert(i <= lengthof(psqlargv));
 
 	appnameenv = psprintf("pg_regress/%s", testname);
 	setenv("PGAPPNAME", appnameenv, 1);
 	free(appnameenv);
 
-	pid = spawn_process(psql_cmd);
-
+	pid = spawn_process(psqlargv[0], psqlargv,
+						infile,
+						outfile,
+						"&1");
 	if (pid == INVALID_PID)
 	{
 		fprintf(stderr, _("could not start process for test %s\n"),

Reply via email to