I looked into this a bit and came up with the attached patch (written
against 0.10.1-2), but for further improvement I'd need some feedback.

The patch adds a new thread that is on the lookout for Pulseaudio
processes and reconfigures/restarts the output modules accordingly
once it finds one.  This allows speech-dispatcher (with the correct
privileges) to transition between ALSA and Pulseaudio without the need
for a restart (albeit with the loss of the not-yet-spoken messages in
the module and a pause of about 1-2 seconds).  In my experience it
works quite nicely.

I don't doubt that making the output modules do the transition
themselves without restarting them would be better, but that would
require changes far more intrusive than the ones in this patch.

N.B.: For this to fix the original bug during installation of
speechd-up, the default for AudioOutputMethod would have to be changed
to "pulse,alsa".

Regards,
Dennis.
Fix for #859926 <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=859926>

diff --git a/speech-dispatcher-0.10.1/src/server/configuration.c b/speech-dispatcher-0.10.1/src/server/configuration.c
index 3d08fe1..9457e3a 100644
--- a/speech-dispatcher-0.10.1/src/server/configuration.c
+++ b/speech-dispatcher-0.10.1/src/server/configuration.c
@@ -187,6 +187,7 @@ GLOBAL_FDSET_OPTION_CB_STR(DefaultModule, output_module)
     GLOBAL_FDSET_OPTION_CB_STR(AudioPulseServer, audio_pulse_server)
     GLOBAL_FDSET_OPTION_CB_STR(AudioPulseDevice, audio_pulse_device)
     GLOBAL_FDSET_OPTION_CB_INT(AudioPulseMinLength, audio_pulse_min_length, 1, "")
+    GLOBAL_FDSET_OPTION_CB_INT(AudioPulseFallback, audio_pulse_fallback, 1, "")

     GLOBAL_FDSET_OPTION_CB_INT(DefaultRate, msg_settings.rate, (val >= -100)
 			       && (val <= +100), "Rate out of range.")
@@ -458,6 +459,7 @@ configoption_t *load_config_options(int *num_options)
 	ADD_CONFIG_OPTION(AudioPulseServer, ARG_STR);
 	ADD_CONFIG_OPTION(AudioPulseDevice, ARG_STR);
 	ADD_CONFIG_OPTION(AudioPulseMinLength, ARG_INT);
+	ADD_CONFIG_OPTION(AudioPulseFallback, ARG_INT);

 	ADD_CONFIG_OPTION(BeginClient, ARG_STR);
 	ADD_CONFIG_OPTION(EndClient, ARG_NONE);
@@ -494,6 +496,7 @@ void load_default_global_set_options()
 	GlobalFDSet.audio_pulse_server = g_strdup("default");
 	GlobalFDSet.audio_pulse_device = g_strdup("default");
 	GlobalFDSet.audio_pulse_min_length = 10;
+	GlobalFDSet.audio_pulse_fallback = 1;

 	SpeechdOptions.max_history_messages = 10000;

diff --git a/speech-dispatcher-0.10.1/src/modules/Makefile.am b/speech-dispatcher-0.10.1/src/modules/Makefile.am
index 670c04c..3dcf920 100644
--- a/speech-dispatcher-0.10.1/src/modules/Makefile.am
+++ b/speech-dispatcher-0.10.1/src/modules/Makefile.am
@@ -28,9 +28,16 @@ audio_SOURCES = spd_audio.c spd_audio.h
 common_SOURCES = module_main.c module_utils.c module_utils.h
 common_LDADD = $(SNDFILE_LIBS) $(DOTCONF_LIBS) $(GLIB_LIBS) $(GTHREAD_LIBS)

+if pulse_support
+patrack = 1
+else
+patrack = 0
+endif
+
 AM_CFLAGS = $(ERROR_CFLAGS)
 AM_CPPFLAGS = $(inc_local) -DDATADIR=\"$(snddatadir)\" -D_GNU_SOURCE \
 	-DPLUGIN_DIR="\"$(audiodir)\"" \
+	-DPATRACK=\"$(patrack)\" \
 	$(DOTCONF_CFLAGS) $(GLIB_CFLAGS) $(GTHREAD_CFLAGS) \
 	$(ibmtts_include) $(SNDFILE_CFLAGS)

diff --git a/speech-dispatcher-0.10.1/src/server/Makefile.am b/speech-dispatcher-0.10.1/src/server/Makefile.am
index df15268..4f3e657 100644
--- a/speech-dispatcher-0.10.1/src/server/Makefile.am
+++ b/speech-dispatcher-0.10.1/src/server/Makefile.am
@@ -20,18 +20,26 @@
 inc_local = -I$(top_srcdir)/include/
 lib_common = $(top_builddir)/src/common/libcommon.la

+if pulse_support
+patrack = 1
+else
+patrack = 0
+endif
+
 bin_PROGRAMS = speech-dispatcher
 speech_dispatcher_SOURCES = speechd.c speechd.h server.c server.h \
 	history.c history.h module.c module.h configuration.c configuration.h \
 	parse.c parse.h set.c set.h msg.h alloc.c alloc.h \
 	compare.c compare.h speaking.c speaking.h options.c options.h \
 	output.c output.h sem_functions.c sem_functions.h \
+	patrack.c patrack.h \
 	index_marking.c index_marking.h symbols.c symbols.h
 speech_dispatcher_CFLAGS = $(ERROR_CFLAGS)
 speech_dispatcher_CPPFLAGS = $(inc_local) $(DOTCONF_CFLAGS) $(GLIB_CFLAGS) \
 	$(GMODULE_CFLAGS) $(GTHREAD_CFLAGS) -DSYS_CONF=\"$(spdconfdir)\" \
 	-DSND_DATA=\"$(snddatadir)\" -DMODULEBINDIR=\"$(modulebindir)\" \
 	-DLOCALE_DATA=\"$(localedatadir)\" \
+	-DPATRACK=\"$(patrack)\" \
 	-D_GNU_SOURCE -DDEFAULT_AUDIO_METHOD=\"$(default_audio_method)\"
 speech_dispatcher_LDFLAGS = $(RDYNAMIC)
 speech_dispatcher_LDADD = $(lib_common) $(DOTCONF_LIBS) $(GLIB_LIBS) \
diff --git a/speech-dispatcher-0.10.1/src/modules/module_utils.c b/speech-dispatcher-0.10.1/src/modules/module_utils.c
index 4f65dd3..c61db39 100644
--- a/speech-dispatcher-0.10.1/src/modules/module_utils.c
+++ b/speech-dispatcher-0.10.1/src/modules/module_utils.c
@@ -28,6 +28,7 @@
 #include <fdsetconv.h>
 #include <spd_utils.h>
 #include "module_utils.h"
+#include "sigabort.h"

 static char *module_audio_pars[10];

@@ -347,6 +348,8 @@ char *do_audio(void)
 				SET_AUDIO_STR(audio_pulse_min_length, 5)
 				    else
 				/* 6 reserved for speech-dispatcher module name */
+				SET_AUDIO_STR(audio_pulse_fallback, 7)
+				    else
 				err = 2;	/* Unknown parameter */
 		}
 		g_free(line);
@@ -884,9 +887,18 @@ void set_speaking_thread_parameters()

 	ret = sigfillset(&all_signals);
 	if (ret == 0) {
+		if (PATRACK && (module_audio_pars[4/*audio_pulse_server*/]!=NULL) && module_audio_pars[7/*audio_pulse_fallback*/]) {
+			sigdelset(&all_signals, SIGABRT);
+		}
 		ret = pthread_sigmask(SIG_BLOCK, &all_signals, NULL);
-		if (ret != 0)
+		if (ret != 0) {
 			DBG("Can't set signal set, expect problems when terminating!\n");
+		} else {
+			/* install the SIGABRT handler needed to deal with Pulseaudio
+			 * crashes */
+			if (PATRACK && (module_audio_pars[4/*audio_pulse_server*/]!=NULL) && module_audio_pars[7/*audio_pulse_fallback*/])
+				install_ABRT_handler();
+		}
 	} else {
 		DBG("Can't fill signal set, expect problems when terminating!\n");
 	}
diff --git a/speech-dispatcher-0.10.1/src/server/speaking.c b/speech-dispatcher-0.10.1/src/server/speaking.c
index 05c5d1a..8ab3ab8 100644
--- a/speech-dispatcher-0.10.1/src/server/speaking.c
+++ b/speech-dispatcher-0.10.1/src/server/speaking.c
@@ -107,7 +107,7 @@ void *speak(void *data)
 		}
 		if (poll_count > 1) {
 			if ((revents = poll_fds[1].revents)) {
-				if (revents & POLLHUP) {
+				if (!GlobalFDSet.audio_pulse_fallback && revents & POLLHUP) {
 					// FIXME: We should handle this more gracefully
 					FATAL
 					    ("wait_for_poll: output_module disconnected");
diff --git a/speech-dispatcher-0.10.1/config/speechd.conf b/speech-dispatcher-0.10.1/config/speechd.conf
index 49e18ce..07c701f 100644
--- a/speech-dispatcher-0.10.1/config/speechd.conf
+++ b/speech-dispatcher-0.10.1/config/speechd.conf
@@ -216,6 +216,17 @@ SymbolsPreprocFile "orca-chars.dic"

 #AudioPulseMinLength 10

+# If set to 1, speech-dispatcher will try to automatically fall back
+# to other audio backends configured in "AudioOutputMethod" when
+# Pulseaudio disappears/crashes or is not running.  This is useful
+# mostly if you want to use programs speechd-up and espeakup without
+# running Pulseaudio in system mode.  For this to work for non-root
+# users, speech-dispatcher must be installed with elevated
+# privileges/capabilities (see section "2.4.3.1 Pulseaudio
+# interoperability (Linux)").  To disable this, set this value to 0.
+
+#AudioPulseFallback 1
+
 # -- ALSA parameters --

 # Audio device for ALSA output
diff --git a/speech-dispatcher-0.10.1/src/server/speechd.c b/speech-dispatcher-0.10.1/src/server/speechd.c
index 7845757..16e553e 100644
--- a/speech-dispatcher-0.10.1/src/server/speechd.c
+++ b/speech-dispatcher-0.10.1/src/server/speechd.c
@@ -47,6 +47,7 @@
 #include "set.h"
 #include "options.h"
 #include "server.h"
+#include "patrack.h"

 #include <i18n.h>

@@ -69,6 +70,7 @@ struct SpeechdOptions SpeechdOptions;
 struct SpeechdStatus SpeechdStatus;

 pthread_t speak_thread;
+pthread_t patrack_thread;
 pthread_mutex_t logging_mutex;
 pthread_mutex_t element_free_mutex;
 pthread_mutex_t output_layer_mutex;
@@ -725,6 +727,11 @@ static gboolean speechd_load_configuration(gpointer user_data)
 		if (dotconf_command_loop(configfile) == 0)
 			DIE("Error reading config file\n");
 		dotconf_cleanup(configfile);
+
+		 /* must happen after audio_output_method is configured, but before fork() */
+		if (PATRACK && strstr(GlobalFDSet.audio_output_method, "pulse") && GlobalFDSet.audio_pulse_fallback)
+			initial_pulseaudio_discovery();
+
 		MSG(2, "Configuration has been read from \"%s\"",
 		    SpeechdOptions.conf_file);

@@ -748,6 +755,7 @@ static gboolean speechd_load_configuration(gpointer user_data)
 		}

 		module_load_requested_modules();
+		in_changeover = 0;
 	} else {
 		MSG(1, "Can't open %s", SpeechdOptions.conf_file);
 	}
@@ -1259,6 +1268,11 @@ int main(int argc, char *argv[])
 	g_unix_signal_add(SIGUSR1, speechd_reload_dead_modules, NULL);
 	(void)signal(SIGPIPE, SIG_IGN);

+	if (PATRACK && strstr(GlobalFDSet.audio_output_method, "pulse") && GlobalFDSet.audio_pulse_fallback) {
+		MSG(4, "Creating new thread for patrack()");
+		pthread_create(&patrack_thread, NULL, &patrack, NULL);
+	}
+
 	MSG(4, "Creating new thread for speak()");
 	ret = pthread_create(&speak_thread, NULL, speak, NULL);
 	if (ret != 0)
@@ -1293,6 +1307,17 @@ int main(int argc, char *argv[])
 	if (ret != 0)
 		FATAL("Speak thread failed to join!\n");

+	if (PATRACK && strstr(GlobalFDSet.audio_output_method, "pulse") && GlobalFDSet.audio_pulse_fallback) {
+		MSG(4, "Closing patrack() thread...");
+		ret = pthread_cancel(patrack_thread);
+		if (ret != 0)
+			FATAL("patrack() thread failed to cancel!\n");
+
+		ret = pthread_join(patrack_thread, NULL);
+		if (ret != 0)
+			FATAL("patrack() thread failed to join!\n");
+	}
+
 	MSG(2, "Closing open output modules...");
 	/*  Call the close() function of each registered output module. */
 	g_list_foreach(output_modules, speechd_modules_terminate, NULL);
diff --git a/speech-dispatcher-0.10.1/src/server/speechd.h b/speech-dispatcher-0.10.1/src/server/speechd.h
index d84d313..ff72db5 100644
--- a/speech-dispatcher-0.10.1/src/server/speechd.h
+++ b/speech-dispatcher-0.10.1/src/server/speechd.h
@@ -101,6 +101,7 @@ typedef struct {
 	char *audio_pulse_server;
 	char *audio_pulse_device;
 	int audio_pulse_min_length;
+	int audio_pulse_fallback;
 	int log_level;

 	/* TODO: Should be moved out */
@@ -181,6 +182,8 @@ extern struct SpeechdStatus {

 /* speak() thread defined in speaking.c */
 extern pthread_t speak_thread;
+/* stalk_pulseadio() thread defined in patrack.c */
+extern pthread_t patracker;
 extern pthread_mutex_t logging_mutex;
 extern pthread_mutex_t element_free_mutex;
 extern pthread_mutex_t output_layer_mutex;
diff --git a/speech-dispatcher-0.10.1/doc/speech-dispatcher.texi b/speech-dispatcher-0.10.1/doc/speech-dispatcher.texi
old mode 100644
new mode 100755
index 09b0dd3..b62461d
--- a/speech-dispatcher-0.10.1/doc/speech-dispatcher.texi
+++ b/speech-dispatcher-0.10.1/doc/speech-dispatcher.texi
@@ -687,6 +687,75 @@ influenced this way. On the other hand, the fallback dummy output
 module tries to use any available means of audio output to deliver its
 error message.

+@node Pulseaudio interoperability (Linux), Client Specific Configuration, Audio Output Configuration, Audio Output Configuration
+@subsubsection Pulseaudio interoperability (Linux)
+
+After version 0.10.1 a Linux-specific mechanism was introduced to
+automatically switch between Pulseaudio and
+@acronym{ALSA}/@acronym{OSS}/@abbr{etc.} depending on whether a
+Pulseaudio process is currently running.  @samp{AudioOutputMethod}
+must be set to @samp{"pulse,alsa"} or @samp{"pulse,oss"} for this to
+work.  This helps accomodating programs like @command{speechd-up}
+which are usually started before Pulseaudio and then prevent it from
+accessing the sound device.
+
+The downside, however, is that in order to make this work,
+speech-dispatcher needs to run with some elevated privileges.  The
+options you have for this are:
+
+@table @code
+
+@item run as root
+For programs like @command{speechd-up} and @command{espeakup} nothing
+needs to be done as they are usually run as root during system
+startup.
+
+@item setcap
+To allow other non-root users to connect speech-dispatcher to
+Pulseaudio, you can permanently give the speech-dispatcher executable
+file and the executable files of its output modules the capability
+@samp{CAP_DAC_READ_SEARCH}.  speech-dispatcher also needs
+@samp{CAP_DAC_PTRACE}.  For this you need the program @command{setcap}
+(usually shipped in a package called something like ``libcap2-bin'').
+As root run these commands:
+
+@example
+setcap "cap_dac_read_search,cap_sys_ptrace=pie" \
+    /usr/bin/speech-dispatcher
+find /usr/lib/speech-dispatcher-modules/sd_* \
+    | xargs -d '\n' -n 1 setcap cap_dac_read_search=pie
+@end example
+
+(@strong{Warning:} You should only use this approach on a system with
+low security requirements as this turns speech-dispatcher into an
+attractive target for getting read-access to almost any file and
+control over almost any process.  speech-dispatcher was not written to
+be secure enough for this.)
+
+@item setuid root
+To allow other non-root users to connect speech-dispatcher to
+Pulseaudio, you can alternatively set the set-user-ID bit of the
+speech-dispatcher executable file and the executable files of its
+output modules.  You should only resort to this if using the previous
+capability-based approach is for some reason not an option.  As root
+run this command:
+
+@example
+chmod u+s /usr/bin/speech-dispatcher \
+    /usr/lib/speech-dispatcher-modules/sd_*
+@end example
+
+(@strong{Warning:} You should only use this approach on a system with
+very low security requirements as this turns speech-dispatcher into an
+attractive target for getting unauthorized root access.
+speech-dispatcher was not written to be secure enough to be run with
+set-user-ID of root.)
+
+@end table
+
+To disable this mechanism set @env{AudioPulseFallback} to 0 in the
+speechd.conf configuration file.
+
 @node Client Specific Configuration, Output Modules Configuration, Audio Output Configuration, Configuration
 @subsection Client Specific Configuration

diff --git a/speech-dispatcher-0.10.1/src/server/patrack.c b/speech-dispatcher-0.10.1/src/server/patrack.c
new file mode 100644
index 0000000..b94808f
--- /dev/null
+++ b/speech-dispatcher-0.10.1/src/server/patrack.c
@@ -0,0 +1,624 @@
+/* -*- indent-tabs-mode: t -*-
+ * patrack.c - Pulseaudio fallback logic
+ *
+ * Copyright (C) 2020 Dennis Filder
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ ***************************************************************************
+ * Definition of correctness:
+ *
+ * The code in this file behaves correctly if it
+ *
+ * - always detects a Pulseaudio process appearing/disappearing in the
+ *   same process namespace within 100 ms,
+ *
+ * - sets up the environment so that the libpulse-simple code can
+ *   connect to the server which is most likely to produce audio,
+ *
+ * - restarts the speaking modules while avoiding needless restarts,
+ *
+ * - does everything possible to always have a working audio
+ *   configuration.
+ *
+ * - Dropping spoken content is permissible if this is the only way to
+ *   restore audio to workable conditions.
+ */
+#include <dirent.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <sys/utsname.h>
+
+#include <unistd.h>
+#include <pwd.h>
+#include <sys/stat.h>
+#include <signal.h>
+#include <pthread.h>
+#include <glib.h> /* provides MIN() */
+
+#define PATRACKER_SIMPLE
+#include "patrack.h"
+#include "speechd.h"
+
+int in_changeover = 0; /* documented in patrack.h */
+
+/* 1 if current OS is supported, 0 otherwise */
+int os_is_supported()
+{
+	struct utsname un;
+	uname(&un);
+	/* currently only Linux */
+	return (strcmp(un.sysname, "Linux") == 0);
+	/* alternatively, test for presence of /proc/1/fdinfo */
+}
+
+/* Returns 1 on success, 0 on error.
+
+"success" means "information for a running Pulseaudio was copied into
+the buffers" or "No Pulseaudio is running" in which case the buffers
+will be zeroed.
+ */
+int find_pulse_server_info2(DIR *papiddir, char *server, size_t size_serv,
+			   char *cookie, size_t size_cook)
+{
+	char haystack[PATH_MAX], buf[PATH_MAX];
+	char *hs = haystack;	/* needed for getline() to work */
+	FILE *net_unix;
+	int fd, stillgood=1;
+	unsigned long sockino;	/* linux/include/linux/fs.h defines .i_ino ULONG */
+	ssize_t bytes_read, err = 0;
+	struct stat sb;
+	size_t sz = sizeof(haystack);
+	struct dirent *de;
+	DIR *fdsdir;
+
+	if (!server || !cookie || strlen(server) || strlen(cookie)
+	    || !size_serv || !size_cook || !papiddir) {
+		MSG(2,
+		    "Error: find_pulse_server_info() called with wrong arguments.");
+		stillgood=0;
+		goto error;
+	}
+	memset(&server[0], 0, size_serv);
+	memset(&cookie[0], 0, size_cook);
+
+	if ((fd = openat(dirfd(papiddir), "net/unix", O_RDONLY)) == -1) {
+		MSG(2,
+		    "Error: openat(..., \"net/unix\", ...) call failed [%s:%d]: %s",
+		    __FILE__, __LINE__, strerror(errno));
+		stillgood=0;
+		goto error;
+	}
+	net_unix = fdopen(fd, "r");	/* we don't really have to error-check here, do we? */
+
+	/* NOTE: to open "fd" we need CAP_SYS_PTRACE */
+	if ((fd = openat(dirfd(papiddir), "fd", O_RDONLY | O_DIRECTORY)) == -1) {
+		MSG(2,
+		    "Error: openat(..., \"fd\", ...) call failed [%s:%d]: %s",
+		    __FILE__, __LINE__, strerror(errno));
+		stillgood=0;
+		goto net_unix_opened;
+	}
+	fdsdir = fdopendir(fd);
+
+	for (de = readdir(fdsdir); de != NULL; de = readdir(fdsdir)) {
+		if ((strlen(de->d_name) == 1 && strcmp(de->d_name, ".") == 0) ||
+		    (strlen(de->d_name) == 2 && strcmp(de->d_name, "..") == 0))
+			continue; /* skip . and .. */
+
+		err = readlinkat(dirfd(fdsdir), de->d_name, &buf[0], sizeof(buf)-1);
+		if (err < 1 || err > sizeof(buf) - 1) {
+			MSG(2, "Error: readlinkat() call failed [%s:%d]: %s",
+			    __FILE__, __LINE__, strerror(errno));
+			stillgood=0; /* readlinkat failed; not recoverable */
+			goto fdsdir_opened;
+		} else
+			buf[err] = '\0'; /* readlinkat writes no '\0' */
+
+		if (strlen(buf) >= strlen("socket:[") &&
+		    strncmp(buf, "socket:[", strlen("socket:[")) == 0) {
+			if (sscanf(buf, "socket:[%lu]", &sockino) != 1) {
+				MSG(2,
+				    "Error: sscanf() call failed [%s:%d]: %s",
+				    __FILE__, __LINE__, strerror(errno));
+				stillgood=0; /* sscanf failed; not recoverable */
+				goto fdsdir_opened;
+			}
+			snprintf(&buf[0], sizeof(buf), " %lu /", sockino); /* needle */
+		} else {	/* skip non-socket */
+			continue;
+		}
+
+		/* Now find in /proc/[pid]/net/unix the path of the socket */
+		rewind(net_unix);
+		while ((bytes_read = getline(&hs, &sz, net_unix)) >= 0) {
+			if (strstr(hs, buf) && strstr(hs, "/native")) { /* AFAIK Pulseaudio always uses "native" */
+				strncpy(server, strchr(hs, '/'),
+					MIN(size_serv,
+					    strchr(hs, '\n') - strchr(hs, '/')));
+				if (stat(server, &sb) == -1) {
+					MSG(2,
+					    "Error: stat(\"%s\", ...) call failed [%s:%d]: %s",
+					    server, __FILE__, __LINE__,
+					    strerror(errno));
+					stillgood=0;
+					goto fdsdir_opened;
+				}
+				if (fstat(dirfd(papiddir), &sb) == -1) { /* papiddir's owner owns the cookie */
+					MSG(2,
+					    "Error: fstat() call failed [%s:%d]: %s",
+					    __FILE__, __LINE__,
+					    strerror(errno));
+					stillgood=0;
+					goto fdsdir_opened;
+				}
+				/* checking default location is all we can do */
+				snprintf(cookie, size_cook,
+					 "%s/.config/pulse/cookie",
+					 getpwuid(sb.st_uid)->pw_dir);
+				if (stat(cookie, &sb) == -1) {
+					MSG(2,
+					    "Error: stat(\"%s\") call failed [%s:%d]: %s",
+					    cookie, __FILE__, __LINE__,
+					    strerror(errno));
+					stillgood=0;
+					goto fdsdir_opened;
+				}
+				break; /* we're done */
+			}
+		}
+	}
+
+fdsdir_opened:
+	if (closedir(fdsdir) == -1) {
+		MSG(2, "Error: closedir() call failed [%s:%d]: %s",
+		    __FILE__, __LINE__, strerror(errno));
+		stillgood=0;
+	}
+
+net_unix_opened:
+	if (fclose(net_unix) == EOF) {
+		MSG(2, "Error: fclose() call failed [%s:%d]: %s",
+		    __FILE__, __LINE__, strerror(errno));
+		stillgood=0;
+	}
+
+	if (stillgood) {
+/*success:*/
+		return 1;
+	} else {
+error:
+		return 0;
+	}
+}
+
+/* returns !=0 if the Pulseaudio process of papiddir has a primary
+ * audio device opened for writing, 0 otherwise.
+ */
+int hogs_soundcard2(DIR *papiddir)
+{
+	char haystack[PATH_MAX], devpath[PATH_MAX];
+	char *q = haystack;	/* needed for getline() to work */
+	int fd;
+	int stillgood=1;
+	size_t sz = sizeof(haystack);
+	struct dirent *de;
+	DIR *fdsdir;
+
+	/* reading "fd" symlinks requires CAP_SYS_PTRACE */
+	if (!papiddir
+	    || (fd =
+		openat(dirfd(papiddir), "fd", O_RDONLY | O_DIRECTORY)) == -1) {
+		MSG(2, "Error: openat() call failed [%s:%d]: %s", __FILE__,
+		    __LINE__, strerror(errno));
+		stillgood=0;
+		goto error;
+	}
+	fdsdir = fdopendir(fd);
+
+	for (de = readdir(fdsdir); de != NULL; de = readdir(fdsdir)) {
+		ssize_t nbytes;
+		if ((strlen(de->d_name) == 1 && strcmp(de->d_name, ".") == 0) ||
+		    (strlen(de->d_name) == 2 && strcmp(de->d_name, "..") == 0))
+			continue;	/* skip . and .. */
+
+		nbytes =
+		    readlinkat(dirfd(fdsdir), de->d_name, &devpath[0],
+			       sizeof(devpath) - 1);
+		if (nbytes < 1 || nbytes > sizeof(devpath) - 1) {
+			MSG(2, "Error: readlinkat() call failed [%s:%d]: %s",
+			    __FILE__, __LINE__, strerror(errno));
+			stillgood=0; /* readlinkat failed; not recoverable */
+			goto fdsdir_opened;
+		} else
+			devpath[nbytes] = '\0'; /* readlinkat writes no '\0' */
+
+		if (IS_PRIMARY_SOUNDCARD(devpath)) {
+
+			/* now inspect O_* flags in ../fdinfo/[d->d_name] */
+			DIR *fdinfodir;
+			FILE *f;
+			/* reading anything in "fdinfo/" requires CAP_SYS_PTRACE */
+			if ((fd =
+			     openat(dirfd(papiddir), "fdinfo",
+				    O_RDONLY | O_DIRECTORY)) == -1) {
+				MSG(2,
+				    "Error: openat(..., \"fdinfo\", ...) call failed [%s:%d]: %s",
+				    __FILE__, __LINE__,
+				    strerror(errno));
+				stillgood=0;
+				goto fdsdir_opened;
+			}
+			fdinfodir = fdopendir(fd);
+			fd = openat(dirfd(fdinfodir), de->d_name, O_RDONLY);
+			if (fd != -1) {
+				ssize_t e1, e2;
+				f = fdopen(fd, "r");
+				e1 = getline(&q, &sz, f); /* skip line 1: pos (see "fdinfo" in proc(5)) */
+				memset(&haystack[0], 0,
+				       sizeof(haystack));
+				e2 = getline(&q, &sz, f); /* line 2: flags (see "fdinfo" in proc(5)) */
+				fclose(f);
+				closedir(fdinfodir);
+				/* the last digit on the line indicates access mode (2==O_RDWR) */
+				if (e1 != -1 && e2 != -1 && strstr(q, "\n")
+				    && (strstr(q, "\n") - 1)[0] ==
+				    '2') {
+					MSG(5, "Soundcard-holding Pulseaudio process found.");
+					/* You can tell this code works correctly if it eschews even
+					 * the designated Pulseaudio process itself until after it has
+					 * its sound device opened (which takes roughly 500 ms after start-up) */
+					break;
+				}
+			} else {
+				/* on error we just try the next file descriptor */
+				MSG(2,
+				    "Warning: openat(..., \"%s\") call failed [%s:%d]: %s",
+				    de->d_name, __FILE__, __LINE__,
+				    strerror(errno));
+			}
+		}
+	}
+
+fdsdir_opened:
+	if (closedir(fdsdir) == -1) {
+		MSG(2, "Error: closedir() call failed [%s:%d]: %s",
+		    __FILE__, __LINE__, strerror(errno));
+		stillgood=0;
+	}
+
+	if (stillgood) {
+		return 1;
+	} else {
+error:
+		return 0;
+	}
+}
+
+int block_SIGHUP_in_this_thread(void)
+{
+	sigset_t set;
+	sigemptyset(&set);
+	sigaddset(&set, SIGHUP);
+	return pthread_sigmask(SIG_BLOCK, &set, NULL);
+}
+
+int restart_speaking_modules(void)
+{
+	kill(getpid(), SIGHUP);
+	return 0;
+}
+
+/* returns 0 on error, >0 on success */
+unsigned long long int starttime_from_piddir(DIR * piddir)
+{
+	FILE *f;
+	char s[1024];		/* should be plenty */
+	char *t1, *t2;
+	int i;
+	unsigned long long int n;
+
+	if ((i = openat(dirfd(piddir), "stat", O_RDONLY)) == -1) {
+		MSG(2, "Error: openat() call failed [%s:%d]: %s",
+		    __FILE__, __LINE__, strerror(errno));
+		goto error;
+	}
+	f = fdopen(i, "r");
+	memset(&s[0], 0, sizeof(s));
+	i=fread(&s[0], 1, sizeof(s), f);
+	if (fclose(f)) { /* checking fclose() obviates checking ferror() */
+		MSG(2, "Error: fclose() call failed [%s:%d]: %s",
+		    __FILE__, __LINE__, strerror(errno));
+		goto error;
+	}
+
+	for (i = 1, t1 = s; i < 22; i++, t1 = strchr(t1, ' ') + 1)
+	  ; /* 22nd field is starttime acc. to proc(5) */
+	n = strtoull(t1, &t2, 10);
+	if (n == ULLONG_MAX && errno == ERANGE) {
+		MSG(2, "Error: strtoull(\"%s\", ...) failed [%s:%d]: %s",
+		    t1, __FILE__, __LINE__, strerror(errno));
+error:
+		return 0;
+	} else
+		return n;
+}
+
+/* Returns 1 if contender is more likely to produce actual sound than
+ * bestguess, 0 otherwise. */
+int scores_higher(DIR *contender, DIR *bestguess)
+{
+	unsigned long long int st_cont, st_begu;
+
+	if (bestguess == NULL && contender != NULL)
+		return 1;
+
+	/* Originally we wanted to test here whether any Pulseaudio
+	   process has /dev/dsp or /dev/snd/controlC0 open for writing
+	   yet.  However, if an output module already has this device
+	   open, then Pulseaudio will refrain from opening it and
+	   instead monitor it with inotify watches.  This means we
+	   have no universal way of knowing in advance which
+	   Pulseaudio process will win the race for the soundcard
+	   after we restart the output modules.  So there's no real
+	   way to eliminate all uncertainty. */
+	if (hogs_soundcard2(contender) && !hogs_soundcard2(bestguess))
+		return 1;
+
+	/* we assume that a lower starttime means higher likelihood of
+	 * producing sound (even though it's not really true) */
+	st_cont = starttime_from_piddir(contender);
+	st_begu = starttime_from_piddir(bestguess);
+	if (st_cont < st_begu)
+		return 1;
+
+	return 0;
+}
+
+/* Finds the PID and starttime of the Pulseaudio process that is most
+ * likely to produce actual sound (if any are running) and writes them
+ * to the locations pointed to by papid and starttime.  If papiddir is
+ * not NULL, a DIR* to pulseaudio's /proc/[pid] directory is also
+ * written there.
+ *
+ * returns 1 on success, 0 on error
+ *
+ * (NOTE: finding no Pulseaudio process does not count as an error)
+ */
+int find_pulseaudio_process2(pid_t * papid, DIR ** papiddir,
+			     unsigned long long int *starttime)
+{
+	DIR *procdir, *bestguess = NULL;
+	struct dirent *de;
+	char p[PATH_MAX];
+	int stillgood;
+
+	*papid = -1;
+	*starttime = 0;
+	stillgood = 1;
+
+	if ((procdir = opendir("/proc")) == NULL) {
+		MSG(2, "Error: opendir(\"/proc\") failed [%s:%d]: %s",
+		    __FILE__, __LINE__, strerror(errno));
+		stillgood=0;
+		goto return_value;
+	}
+
+	unsetenv("BEST_GUESS");
+
+	while ((de = readdir(procdir))) {
+		char *fn;
+		int err;
+		DIR *piddir = NULL;
+
+		if (!IS_PIDDIR(de->d_name) ||
+		    (err = openat(dirfd(procdir), de->d_name, O_RDONLY)) == -1)
+			continue; /* not a PID dir or insufficient access: skip it */
+		piddir = fdopendir(err);
+
+		memset(&p[0], 0, sizeof(p));
+		/* Only process owner and CAP_SYS_PTRACE-havers can read
+		 * /proc/[pid]/exe. */
+		if ((err =
+		     readlinkat(dirfd(piddir), "exe", &p[0],
+				sizeof(p))) == -1) {
+			MSG(5, "Note: readlinkat() call failed [%s:%d]: %s",
+			    __FILE__, __LINE__, strerror(errno));
+			goto piddir_opened;
+		}
+		fn = strrchr(p, '/') + 1;
+		if (strncmp
+		    (fn, "pulseaudio",
+		     MIN(strlen(fn), strlen("pulseaudio"))) == 0) {
+			if (scores_higher(piddir, bestguess)) {
+				if (bestguess)
+					closedir(bestguess);
+				bestguess = piddir;
+				setenv("BEST_GUESS", de->d_name, 1); /* aids debugging output modules */
+				*papid = atoi(de->d_name);
+				*starttime = starttime_from_piddir(bestguess);
+				if (papiddir) {
+					*papiddir = bestguess;
+				}
+				MSG(5,
+				    "Found new best guess (pid=%s, starttime=%llu)",
+				    de->d_name, *starttime);
+			} else {
+				MSG(5,
+				    "Skipping inferior pulseaudio (pid=%s, starttime=%llu)",
+				    de->d_name, *starttime);
+			}
+		}
+
+piddir_opened:
+		if (papiddir != NULL && piddir == bestguess) {
+			;	/* only keep piddir open if the caller wants it and it's
+				 * currently the best guess */
+		} else {
+			closedir(piddir);
+		}
+	} /* while(de = readdir(procdir)) { */
+
+	closedir(procdir);
+return_value:
+	return stillgood;
+}
+
+void set_pulse_server_info(DIR * papiddir)
+{
+	int err;
+	char server[PATH_MAX], cookie[PATH_MAX];
+
+	memset(&server[0], 0, sizeof(server));
+	memset(&cookie[0], 0, sizeof(cookie));
+	err = find_pulse_server_info2(papiddir,
+				     &server[0], sizeof(server), &cookie[0],
+				     sizeof(cookie));
+	if (err == 1) {
+		MSG(4, "Info: set_pulse_server_info(): setting envvars.");
+		setenv("PULSE_SERVER", server, 1);
+		setenv("PULSE_COOKIE", cookie, 1);
+	} else {
+		MSG(4,
+		    "Info: set_pulse_server_info(): find_pulse_server_info2() found nothing (%d, %s, %s).",
+		    err, server, cookie);
+	}
+}
+
+/* performs the Pulseaudio discovery before the output modules are
+ * forked off */
+void initial_pulseaudio_discovery()
+{
+	int err;
+	pid_t papid;
+	DIR *papiddir;
+	unsigned long long int starttime;
+
+	if (!os_is_supported())
+		return;
+
+	err = find_pulseaudio_process2(&papid, &papiddir, &starttime);
+
+	if (papid == -1) {
+		unsetenv("PULSE_SERVER");
+		unsetenv("PULSE_COOKIE");
+	} else if (err == 1) {
+		set_pulse_server_info(papiddir);
+		/* no killing since we're in initial discovery */
+	}
+}
+
+void unset_pulse_server_info(int *do_kill)
+{
+	if (getenv("PULSE_SERVER")) {
+		unsetenv("PULSE_SERVER");
+		unsetenv("PULSE_COOKIE");
+		*do_kill = 1;
+		MSG(3, "Falling back from Pulseaudio mode");
+	} else {
+		MSG(4, "Staying in non-Pulseaudio mode");
+	}
+}
+
+void patrack_helper2()
+{
+	pid_t papid, papidold;
+	unsigned long long int starttime, starttimeold;	/* %llu acc. to proc(5) */
+	int err;
+
+	/* initialise papidold and starttimeold */
+	err = find_pulseaudio_process2(&papidold, NULL, &starttimeold);
+	if (err == 0) {
+		MSG(1,
+		    "Fatal error: find_pulseaudio_process2() call failed [%s:%d]",
+		    __FILE__, __LINE__);
+		exit(1);
+	}
+	MSG(5, "papidold=%i, starttimeold=%llu", papidold, starttimeold);
+
+	while (1) {		/* NEVER use "continue" in this loop! closedir() below relies on it */
+		DIR *papiddir;
+		int do_restart;
+
+		papiddir = NULL;
+		do_restart = 0;
+
+		usleep(100 * 1000);	/* we sleep before find_pulseaudio_process2() to
+					 * not call it twice in quick succession on the
+					 * first iteration */
+
+		err = find_pulseaudio_process2(&papid, &papiddir, &starttime);
+		if (err == 0) {
+			MSG(2,
+			    "Warning: find_pulseaudio_process2() returned error.  Falling back. [%s:%d]",
+			    __FILE__, __LINE__);
+			unset_pulse_server_info(&do_restart);
+		} else if (papid != papidold || starttime != starttimeold
+			   || (papid > 0 && !getenv("PULSE_SERVER"))) {
+			if (papid > 0) {
+				set_pulse_server_info(papiddir);
+			} else {
+				unset_pulse_server_info(&do_restart);
+			}
+			do_restart = 1;	/* this branch must ALWAYS restart the modules */
+		} else if (papid == papidold && starttime == starttimeold) {
+			if (papid > 0 && getenv("PULSE_SERVER")) {
+				MSG(4, "Staying in Pulseaudio mode");
+			} else if (papid == -1 && !getenv("PULSE_SERVER")) {
+				MSG(4, "Staying in non-Pulseaudio mode");
+			} else {
+				;	/* should never happen. */
+			}
+		} else {
+			;	/* should never happen.  What to do if it does? */
+		}
+
+		if (do_restart) {
+			in_changeover = 1;
+			MSG(3, "patrack thread: restarting speaking modules.");
+			restart_speaking_modules();
+		}
+		MSG(4,
+		    "papid=%6i, starttime=%9llu, PULSE_SERVER=%33s PULSE_COOKIE=%33s",
+		    papid, starttime, getenv("PULSE_SERVER"),
+		    getenv("PULSE_COOKIE"));
+		papidold = papid;
+		starttimeold = starttime;
+
+		if (papiddir)
+			closedir(papiddir);
+	}
+}
+
+/* called (and cancelled/joined) by the main thread */
+void *patrack()
+{
+	const char *tname = "patrack";
+	pthread_setname_np(pthread_self(), tname);
+	block_SIGHUP_in_this_thread();
+
+	if (os_is_supported()) {
+		patrack_helper2();
+	} else {
+		while (1) {
+			usleep(1000 * 1000);
+		}
+	}
+	pthread_exit(0);
+}
diff --git a/speech-dispatcher-0.10.1/src/server/patrack.h b/speech-dispatcher-0.10.1/src/server/patrack.h
new file mode 100644
index 0000000..e3e2daf
--- /dev/null
+++ b/speech-dispatcher-0.10.1/src/server/patrack.h
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: t -*-
+ * patrack.h - Pulseaudio fallback logic (macro and function definitions)
+ *
+ * Copyright (C) 2020 Dennis Filder
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+#ifndef PATRACK_H
+#define PATRACK_H
+
+/* 1 if a changeover from one audio backend to another (or itself) is
+ * currently happening; 0 otherwise.
+ *
+ * speaking.c:speak() will someday need this to tell whether any
+ * currently-speaking module crashed or was merely killed by our
+ * request.  Must be cleared after output modules have finished
+ * loading.
+ */
+extern int in_changeover;
+
+
+/* this assumes process directory names match regex [0-9]{1,5} */
+#define IS_PIDDIR(s) ( \
+         (isdigit((unsigned char)(s)[0]) && !(s)[1]) || \
+         (isdigit((unsigned char)(s)[0]) && isdigit((unsigned char)(s)[1]) && !(s)[2]) ||    \
+         (isdigit((unsigned char)(s)[0]) && isdigit((unsigned char)(s)[1]) && isdigit((unsigned char)(s)[2]) && !(s)[3]) ||     \
+         (isdigit((unsigned char)(s)[0]) && isdigit((unsigned char)(s)[1]) && isdigit((unsigned char)(s)[2]) && isdigit((unsigned char)(s)[3]) && !(s)[4]) ||     \
+         (isdigit((unsigned char)(s)[0]) && isdigit((unsigned char)(s)[1]) && isdigit((unsigned char)(s)[2]) && isdigit((unsigned char)(s)[3]) && isdigit((unsigned char)(s)[4]) && !(s)[5]) \
+         )
+
+/* Currently we only consider /dev/dsp and /dev/snd/controlC0 as
+ * primary soundcards.  This might change in the future if it proves
+ * not to work in practice. */
+#define IS_PRIMARY_SOUNDCARD(rp) (					\
+		(strlen(rp) >= strlen("/dev/snd/controlC0") &&		\
+		 strncmp(rp, "/dev/snd/controlC0",			\
+			 strlen("/dev/snd/controlC0")) == 0)		\
+		|| (strlen(rp) >= strlen("/dev/dsp")			\
+		    && strncmp(rp, "/dev/dsp", strlen("/dev/dsp")) == 0) \
+                )
+
+void *patrack();
+void initial_pulseaudio_discovery();
+int block_SIGHUP_in_this_thread(void);
+
+#ifdef PATRACKER_SIMPLE
+#include "msg.h"
+#endif
+
+#endif /* PATRACK_H */
diff --git a/speech-dispatcher-0.10.1/src/modules/sigabort.h b/speech-dispatcher-0.10.1/src/modules/sigabort.h
new file mode 100644
index 0000000..97602b0
--- /dev/null
+++ b/speech-dispatcher-0.10.1/src/modules/sigabort.h
@@ -0,0 +1,46 @@
+/* -*- indent-tabs-mode: t -*-
+ * sigabort.h - SIGABRT handler for Pulseaudio fallback logic
+ *
+ * Copyright (C) 2020 Dennis Filder
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/* This handler is needed to deal with Pulseaudio server crashes which
+ * make libpulse-simple call abort() which, if not caught, leads to
+ * speech-dispatcher failing fatally with "Fatal error
+ * [speaking.c:102]:wait_for_poll: output_module disconnected".
+ *
+ * This handler gives the patrack thread enough time to detect
+ * Pulseaudio's disappearance and perform the changeover.
+ *
+ * If the changeover fails for some reason speech-dispatcher will
+ * probably fail as if this handler had not been installed.
+ */
+#include <signal.h>
+#include "module_utils.h"
+
+void ABRT_handler(int n)
+{
+	DBG("Entered ABRT handler, sleeping for 5 seconds\n");
+	usleep(5 * 1000 * 1000);
+	DBG("5 seconds are over\n"); /* should never get here */
+}
+
+void install_ABRT_handler()
+{
+	struct sigaction sa_abrt;
+	sa_abrt.sa_handler = &ABRT_handler;
+	sigaction(SIGABRT, &sa_abrt, NULL);
+}

Reply via email to