From 294024afa675d5b8e31ac4a75e9297b488113bea Mon Sep 17 00:00:00 2001
From: Yurii Rashkovskii <yrashk@gmail.com>
Date: Sun, 7 May 2023 10:01:53 +0200
Subject: [PATCH] Allow listening port to be 0

This makes it possible to start postgres on an unused port instead of
trying to reliably pick it in cases where this is desirable, such as
test benches or colocated database instances.

This is done by retrieving the assigned port number using `getsockname`.

Those using this feature can retrieve the assigned port through
postmaster.pid
---
 doc/src/sgml/config.sgml            |  4 ++
 src/backend/libpq/pqcomm.c          | 76 +++++++++++++++++++++++++++--
 src/backend/postmaster/postmaster.c | 21 ++++++--
 src/backend/utils/misc/guc_tables.c |  2 +-
 src/include/libpq/libpq.h           |  3 ++
 5 files changed, 97 insertions(+), 9 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index bda0da2dc8..84be4e33c7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -682,6 +682,10 @@ include_dir 'conf.d'
         same port number is used for all IP addresses the server listens on.
         This parameter can only be set at server start.
        </para>
+       <para>
+        The port can be set to 0 to make Postgres pick an unused port number.
+        The assigned port number can be then retrieved from <entry><filename>postmaster.pid</filename></entry>.
+       </para>
       </listitem>
      </varlistentry>
 
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index da5bb5fc5d..d805490179 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -302,11 +302,12 @@ socket_close(int code, Datum arg)
  *		Stream functions are used for vanilla TCP connection protocol.
  */
 
-
 /*
- * StreamServerPort -- open a "listening" port to accept connections.
+ * StreamServerPortReturn -- open a "listening" port to accept connections.
+ *
+ * family should be AF_UNIX or AF_UNSPEC; portNumberPtr is a non-NULL pointer
+ * to a port number.
  *
- * family should be AF_UNIX or AF_UNSPEC; portNumber is the port number.
  * For AF_UNIX ports, hostName should be NULL and unixSocketDir must be
  * specified.  For TCP ports, hostName is either NULL for all interfaces or
  * the interface to listen on, and unixSocketDir is ignored (can be NULL).
@@ -314,13 +315,21 @@ socket_close(int code, Datum arg)
  * Successfully opened sockets are added to the ListenSocket[] array (of
  * length MaxListen), at the first position that isn't PGINVALID_SOCKET.
  *
+ * This function allows retrieving assigned port number through portNumberPtr
+ * (if the original value of *portNumberPtr is 0, signifying an unused port
+ * selection)
+ *
+ * If the retrieval of the assigned port number is not necessary, StreamServerPort
+ * can be used instead.
+ *
  * RETURNS: STATUS_OK or STATUS_ERROR
  */
 
 int
-StreamServerPort(int family, const char *hostName, unsigned short portNumber,
+StreamServerPortReturn(int family, const char *hostName, unsigned short *portNumberPtr,
 				 const char *unixSocketDir,
 				 pgsocket ListenSocket[], int MaxListen)
+
 {
 	pgsocket	fd;
 	int			err;
@@ -341,6 +350,11 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
 #if !defined(WIN32) || defined(IPV6_V6ONLY)
 	int			one = 1;
 #endif
+	unsigned short portNumber;
+
+	/* portNumberPtr must contain a value */
+	Assert(portNumberPtr != NULL);
+	portNumber = *portNumberPtr;
 
 	/* Initialize hint structure */
 	MemSet(&hint, 0, sizeof(hint));
@@ -539,6 +553,39 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
 				closesocket(fd);
 				break;
 			}
+		} else if (portNumber == 0)
+		{
+			/* Return actual, effective portNumber through portNumberPtr.
+			 *
+			 * The reason it works uniformly across different socket types is because
+			 * port number is updated in a sequence and on the following iterations, it will
+			 * be no longer set to 0. Therefore, all sockets will share the same IP address.
+			 *
+			 * If it is impossible to listen on a socket on the port that was unused in one family,
+			 * it will result in an error.
+			 */
+			struct sockaddr_in sockaddr;
+			socklen_t socksize = sizeof(sockaddr);
+			StaticAssertStmt(offsetof(struct sockaddr_in, sin_port) == offsetof(struct sockaddr_in6, sin6_port),
+					"sockaddr_in and sockaddr_in6 must have port at the same offset");
+
+			if (getsockname(fd, (struct sockaddr *)&sockaddr, &socksize) == -1)
+			{
+				int	saved_errno = errno;
+				ereport(LOG, errmsg("getsockname failed with: %s", strerror(saved_errno)));
+				closesocket(fd);
+				break;
+			}
+
+			if (addr->ai_family == AF_INET)
+			{
+				*portNumberPtr = ntohs(sockaddr.sin_port);
+			} else if (addr->ai_family == AF_INET6)
+			{
+				*portNumberPtr = ntohs(((struct sockaddr_in6 *)&sockaddr)->sin6_port);
+			}
+
+			portNumber = *portNumberPtr;
 		}
 
 		/*
@@ -582,6 +629,27 @@ StreamServerPort(int family, const char *hostName, unsigned short portNumber,
 	return STATUS_OK;
 }
 
+/*
+ * StreamServerPort -- open a "listening" port to accept connections.
+ *
+ * family should be AF_UNIX or AF_UNSPEC; portNumber is the port number.
+ * For AF_UNIX ports, hostName should be NULL and unixSocketDir must be
+ * specified.  For TCP ports, hostName is either NULL for all interfaces or
+ * the interface to listen on, and unixSocketDir is ignored (can be NULL).
+ *
+ * Successfully opened sockets are added to the ListenSocket[] array (of
+ * length MaxListen), at the first position that isn't PGINVALID_SOCKET.
+ *
+ * RETURNS: STATUS_OK or STATUS_ERROR
+ */
+
+int
+StreamServerPort(int family, const char *hostName, unsigned short portNumber,
+				 const char *unixSocketDir,
+				 pgsocket ListenSocket[], int MaxListen)
+{
+	return StreamServerPortReturn(family, hostName, &portNumber, unixSocketDir, ListenSocket, MaxListen);
+}
 
 /*
  * Lock_AF_UNIX -- configure unix socket file path
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 4c49393fc5..c60afdc29c 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -1209,18 +1209,31 @@ PostmasterMain(int argc, char *argv[])
 		foreach(l, elemlist)
 		{
 			char	   *curhost = (char *) lfirst(l);
+			bool       wildcard_port = PostPortNumber == 0;
 
 			if (strcmp(curhost, "*") == 0)
-				status = StreamServerPort(AF_UNSPEC, NULL,
-										  (unsigned short) PostPortNumber,
+				status = StreamServerPortReturn(AF_UNSPEC, NULL,
+										  (unsigned short *) &PostPortNumber,
 										  NULL,
 										  ListenSocket, MAXLISTEN);
 			else
-				status = StreamServerPort(AF_UNSPEC, curhost,
-										  (unsigned short) PostPortNumber,
+				status = StreamServerPortReturn(AF_UNSPEC, curhost,
+										  (unsigned short *) &PostPortNumber,
 										  NULL,
 										  ListenSocket, MAXLISTEN);
 
+			if (wildcard_port) {
+				char curport[10];
+				/* Ensure picked port is saved to the lock file
+				 *
+				 * Initial port selection is written to the lock file in
+				 * CreateDataDirLockFile but if the supplied port was zero,
+				 * we need to update it.
+				 */
+				curport[pg_ultoa_n(PostPortNumber, curport)] = 0;
+				AddToDataDirLockFile(LOCK_FILE_LINE_PORT, curport);
+			}
+
 			if (status == STATUS_OK)
 			{
 				success++;
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 8062589efd..11b7c3d46f 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2263,7 +2263,7 @@ struct config_int ConfigureNamesInt[] =
 			NULL
 		},
 		&PostPortNumber,
-		DEF_PGPORT, 1, 65535,
+		DEF_PGPORT, 0, 65535,
 		NULL, NULL, NULL
 	},
 
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 50fc781f47..c875da55e2 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -67,6 +67,9 @@ extern PGDLLIMPORT WaitEventSet *FeBeWaitSet;
 extern int	StreamServerPort(int family, const char *hostName,
 							 unsigned short portNumber, const char *unixSocketDir,
 							 pgsocket ListenSocket[], int MaxListen);
+extern int	StreamServerPortReturn(int family, const char *hostName,
+							 unsigned short *portNumberPtr, const char *unixSocketDir,
+							 pgsocket ListenSocket[], int MaxListen);
 extern int	StreamConnection(pgsocket server_fd, Port *port);
 extern void StreamClose(pgsocket sock);
 extern void TouchSocketFiles(void);
-- 
2.39.2 (Apple Git-143)

