From 516f9a66259e5a89d327ca970548e0b983a35c38 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Mon, 6 Jul 2026 16:44:53 +1200
Subject: [PATCH v1 12/14] pg_signal_processor: Infrastructure for cleanup.

Provide a way to register signal handlers than run serially in a special
thread.  This is intended to allow frontend programs to use more
complicated shutdown code when you hit ^C, without having to worry
about async-signal-safety.

XXX experimental

Discussion:
Reviewed-by:
---
 src/include/port/pg_signal_processor.h |  11 ++
 src/port/Makefile                      |   1 +
 src/port/meson.build                   |   1 +
 src/port/pg_signal_processor.c         | 192 +++++++++++++++++++++++++
 src/tools/pgindent/typedefs.list       |   1 +
 5 files changed, 206 insertions(+)
 create mode 100644 src/include/port/pg_signal_processor.h
 create mode 100644 src/port/pg_signal_processor.c

diff --git a/src/include/port/pg_signal_processor.h b/src/include/port/pg_signal_processor.h
new file mode 100644
index 00000000000..aa482e8ec65
--- /dev/null
+++ b/src/include/port/pg_signal_processor.h
@@ -0,0 +1,11 @@
+#ifndef PG_SIGNAL_PROCESSOR
+#define PG_SIGNAL_PROCESSOR
+
+typedef void (*pg_signal_processor_handler) (int signo);
+
+extern void pg_signal_processor_set_serial_handler(int signo,
+												   pg_signal_processor_handler handler);
+extern int	pg_signal_processor_start(void);
+extern void pg_signal_processor_stop(void);
+
+#endif
diff --git a/src/port/Makefile b/src/port/Makefile
index 29734bb0d35..d5563dd4eab 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -50,6 +50,7 @@ OBJS = \
 	pg_numa.o \
 	pg_popcount_aarch64.o \
 	pg_popcount_x86.o \
+	pg_signal_processor.o \
 	pg_strong_random.o \
 	pg_threads.o \
 	pgcheckdir.o \
diff --git a/src/port/meson.build b/src/port/meson.build
index d8ca8af072d..4a965fcb7cb 100644
--- a/src/port/meson.build
+++ b/src/port/meson.build
@@ -13,6 +13,7 @@ pgport_sources = [
   'pg_numa.c',
   'pg_popcount_aarch64.c',
   'pg_popcount_x86.c',
+  'pg_signal_processor.c',
   'pg_strong_random.c',
   'pg_threads.c',
   'pgcheckdir.c',
diff --git a/src/port/pg_signal_processor.c b/src/port/pg_signal_processor.c
new file mode 100644
index 00000000000..651cf49b1cb
--- /dev/null
+++ b/src/port/pg_signal_processor.c
@@ -0,0 +1,192 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_signal_processor.c
+ *    Mechanism for handling OS signals serially in a thread.
+ *
+ * This is primarily intended to handle SIGINT (^C) and SIGQUIT in frontend
+ * programs, to perform cleanup work before exiting.  Handlers must be
+ * thread-safe, but do not need to be reentrant or async-signal-safe.
+ *
+ * On Windows, only SIGINT and SIGBREAK are supported, and they are mapped to
+ * CTRL_C_EVENT and CTRL_BREAK_EVENT console events.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *    src/port/pg_signal_processor.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "c.h"
+
+#include <signal.h>
+#include <stdio.h>
+
+#include "port/pg_signal_processor.h"
+#include "port/pg_threads.h"
+
+#ifndef WIN32
+/* Steal one signal to use for stopping the helper thread. */
+#define PG_SIGNAL_PROCESSOR_EOF_SIGNAL SIGXCPU
+#endif
+
+#ifndef NSIG
+#define NSIG 64
+#endif
+
+static pg_signal_processor_handler serial_handlers[NSIG];
+
+#ifdef WIN32
+static pg_mtx_t serial_handler_mutex = PG_MTX_INIT;
+#else
+static pg_thrd_t serial_handler_thread;
+#endif
+
+#ifdef WIN32
+/* Windows: serialized execution using Windows' event thread pool. */
+static BOOL WINAPI
+pg_signal_processor_console_ctrl_handler(DWORD control_type)
+{
+	pg_signal_processor_handler handler;
+	int			signo = -1;
+
+	/* CTRL_XXX_EVENT -> SIGXXX mapping matches Windows' own signal(). */
+	if (control_type == CTRL_C_EVENT)
+		signo = SIGINT;
+	else if (control_type == CTRL_BREAK_EVENT)
+		signo = SIGBREAK;
+
+	/* If we have a handler, serialize execution. */
+	if (signo != -1 && (handler = serial_handlers[signo]))
+	{
+		pg_mtx_lock(&serial_handler_mutex);
+		handler(signo);
+		pg_mtx_unlock(&serial_handler_mutex);
+
+		/*
+		 * If we made it out of the handler (ie it didn't choose to call
+		 * _exit()), then don't allow further processing of this event, to
+		 * avoid reaching Windows' default process-exit behavior.
+		 */
+		return true;
+	}
+
+	return false;
+}
+#endif
+
+#ifndef WIN32
+/* Unix: consume signals synchronously in a dispatch loop. */
+static int
+pg_signal_processor_loop(void *data)
+{
+	sigset_t	wait_signals;
+
+	sigemptyset(&wait_signals);
+	sigaddset(&wait_signals, PG_SIGNAL_PROCESSOR_EOF_SIGNAL);
+	for (int i = 0; i < lengthof(serial_handlers); ++i)
+		if (serial_handlers[i])
+			sigaddset(&wait_signals, i);
+
+	for (;;)
+	{
+		int			signo;
+
+		errno = sigwait(&wait_signals, &signo);
+		if (errno != 0)
+		{
+			fprintf(stderr,
+					"pg_signal_processor_loop: sigwait() failed: %s\n",
+					strerror(errno));
+			break;
+		}
+
+		if (signo == PG_SIGNAL_PROCESSOR_EOF_SIGNAL)
+			break;
+
+		serial_handlers[signo] (signo);
+	}
+
+	return 0;
+}
+
+static void
+dummy_handler(int signo)
+{
+	pg_unreachable();
+}
+#endif
+
+/*
+ * Install handler for a given signal.  This must be called to set up the
+ * handler table *before* the program becomes multithreaded on Unix, at must
+ * be called at least once before pg_signal_processor_start().
+ */
+void
+pg_signal_processor_set_serial_handler(int signo,
+									   pg_signal_processor_handler handler)
+{
+#ifndef WIN32
+	sigset_t	block_mask;
+
+	/* Can't use our reserved EOF signal number. */
+	Assert(signo != PG_SIGNAL_PROCESSOR_EOF_SIGNAL);
+
+	/* Block from normal delivery, and the same for our EOF signal. */
+	sigemptyset(&block_mask);
+	sigaddset(&block_mask, PG_SIGNAL_PROCESSOR_EOF_SIGNAL);
+	sigaddset(&block_mask, signo);
+	sigprocmask(SIG_BLOCK, &block_mask, NULL);
+
+	/*
+	 * Install a dummy handler.  This prevents the OS from discarding SIG_IGN
+	 * signals that we want to read with sigwait().
+	 */
+	signal(signo, dummy_handler);
+#endif
+
+	Assert(signo >= 0 && signo < NSIG);
+	Assert(handler);
+
+	serial_handlers[signo] = handler;
+}
+
+/* Start serial signal processing. */
+int
+pg_signal_processor_start(void)
+{
+#ifdef WIN32
+	/* Windows: install control handler to be called by Windows' threads. */
+	if (!SetConsoleCtrlHandler(pg_signal_processor_console_ctrl_handler, true))
+	{
+		_dosmaperr(GetLastError());
+		return -1;
+	}
+#else
+	/* Unix: start serial execution thread. */
+	if (pg_thrd_create(&serial_handler_thread,
+					   pg_signal_processor_loop,
+					   NULL) != pg_thrd_success)
+		return -1;
+#endif
+
+	return 0;
+}
+
+/* Stop serial signal processing. */
+void
+pg_signal_processor_stop(void)
+{
+#ifdef WIN32
+	/* Windows: uninstall control handler function. */
+	SetConsoleCtrlHandler(pg_signal_processor_console_ctrl_handler, false);
+#else
+	/* Unix: tell helper thread to exit, and wait for it to return. */
+	raise(PG_SIGNAL_PROCESSOR_EOF_SIGNAL);
+	if (pg_thrd_join(serial_handler_thread, NULL) != pg_thrd_success)
+		fprintf(stderr, "pg_signal_processor_stop: could not join thread\n");
+#endif
+}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 338e5578979..e830092ee62 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4075,6 +4075,7 @@ pg_sha256_ctx
 pg_sha384_ctx
 pg_sha512_ctx
 pg_signal_info
+pg_signal_processor
 pg_snapshot
 pg_special_case
 pg_stack_base_t
-- 
2.47.3

