blob: c88f7595f37c748b431c03da450f26a691b84bae [file] [log] [blame]
/*
* speechd.c - Speech Dispatcher server program
*
* Copyright (C) 2001, 2002, 2003, 2006, 2007 Brailcom, o.p.s.
*
* 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/>.
*
* $Id: speechd.c,v 1.81 2008-07-10 15:36:49 hanke Exp $
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <glib.h>
#include <gmodule.h>
#include <glib/gstdio.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdint.h>
#ifdef USE_LIBSYSTEMD
#include <systemd/sd-daemon.h>
#endif
#ifdef HAVE_SYS_FILIO_H
#include <sys/filio.h> /* Needed for FIONREAD on Solaris */
#endif
#include "speechd.h"
/* Declare dotconf functions and data structures*/
#include "configuration.h"
/* Declare functions to allocate and create important data
* structures */
#include "alloc.h"
#include "sem_functions.h"
#include "speaking.h"
#include "speak_queue.h"
#include "set.h"
#include "options.h"
#include "server.h"
#include <i18n.h>
/* list of output modules */
GList *output_modules;
/* Manipulating pid files */
int create_pid_file();
void destroy_pid_file();
/* Server socket file descriptor */
int server_socket;
GMainLoop *main_loop = NULL;
gint server_timeout_source = -1;
int client_count = 0;
struct SpeechdOptions SpeechdOptions;
struct SpeechdStatus SpeechdStatus;
pthread_t speak_thread;
pthread_mutex_t logging_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t element_free_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t output_layer_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t socket_com_mutex = PTHREAD_MUTEX_INITIALIZER;
GHashTable *fd_settings;
GHashTable *language_default_modules;
GHashTable *fd_uid;
TSpeechDQueue *MessageQueue;
GList *MessagePausedList;
GList *client_specific_settings;
GList *last_p5_block;
TFDSetElement GlobalFDSet;
int speaking_pipe[2];
GHashTable *speechd_sockets_status;
FILE *logfile;
FILE *custom_logfile;
char *custom_log_kind;
FILE *debug_logfile;
TSpeechDMode spd_mode;
static gboolean speechd_client_terminate(gpointer key, gpointer value, gpointer user);
static gboolean speechd_reload_dead_modules(gpointer user_data);
static gboolean speechd_reload_configuration(gpointer user_data);
enum quit_reason {
QUIT_SIGINT,
QUIT_SIGTERM,
QUIT_TIMEOUT,
};
static gboolean speechd_quit(gpointer user_data);
static gboolean server_process_incoming (gint fd,
GIOCondition condition,
gpointer data);
static gboolean client_process_incoming (gint fd,
GIOCondition condition,
gpointer data);
static void speechd_load_configuration(void);
static void speechd_check_modules(void);
static void check_client_count(void);
#ifndef HAVE_DAEMON
/* Added by Willie Walker - daemon is a common, but not universal, extension.
*/
static int daemon(int nochdir, int noclose)
{
int fd, i;
switch (fork()) {
case 0:
break;
case -1:
return -1;
default:
_exit(0);
}
if (!nochdir) {
chdir("/");
}
if (setsid() < 0) {
return -1;
}
if (!noclose) {
if (fd = open("/dev/null", O_RDWR) >= 0) {
for (i = 0; i < 3; i++) {
dup2(fd, i);
}
if (fd > 2) {
close(fd);
}
}
}
return 0;
}
#endif /* HAVE_DAEMON */
/* --- DEBUGGING --- */
/* Just to be able to set breakpoints */
void fatal_error(void)
{
int i = 0;
i++;
}
/* Logging messages, level of verbosity is defined between 1 and 5,
* see documentation */
void MSG2(int level, const char *kind, const char *format, ...)
{
int std_log = level <= SpeechdOptions.log_level;
int custom_log = (kind != NULL && custom_log_kind != NULL &&
!strcmp(kind, custom_log_kind) &&
custom_logfile != NULL);
if (std_log || custom_log) {
va_list args;
int i;
pthread_mutex_lock(&logging_mutex);
{
{
/* Print timestamp */
time_t t;
char tstr[26];
struct timeval tv;
t = time(NULL);
ctime_r(&t, tstr);
gettimeofday(&tv, NULL);
/* Remove the trailing \n */
assert(strlen(tstr) > 1);
tstr[strlen(tstr) - 1] = 0;
if (std_log) {
fprintf(logfile, "[%s : %d] speechd: ",
tstr, (int)tv.tv_usec);
// fprintf(logfile, "[test : %d] speechd: ",
// (int) tv.tv_usec);
}
if (custom_log) {
fprintf(custom_logfile,
"[%s : %d] speechd: ", tstr,
(int)tv.tv_usec);
}
if (SpeechdOptions.debug) {
fprintf(debug_logfile,
"[%s : %d] speechd: ", tstr,
(int)tv.tv_usec);
}
}
for (i = 1; i < level; i++) {
if (std_log) {
fprintf(logfile, " ");
}
if (custom_log) {
fprintf(custom_logfile, " ");
}
}
if (std_log) {
va_start(args, format);
vfprintf(logfile, format, args);
va_end(args);
fprintf(logfile, "\n");
fflush(logfile);
}
if (custom_log) {
va_start(args, format);
vfprintf(custom_logfile, format, args);
va_end(args);
fprintf(custom_logfile, "\n");
fflush(custom_logfile);
}
if (SpeechdOptions.debug) {
va_start(args, format);
vfprintf(debug_logfile, format, args);
va_end(args);
fprintf(debug_logfile, "\n");
fflush(debug_logfile);
}
}
pthread_mutex_unlock(&logging_mutex);
}
}
/* The main logging function for Speech Dispatcher,
level is between -1 and 5. 1 means the most important,
5 less important. Loglevels after 4 can contain private
data. -1 logs also to stderr. See Speech Dispatcher
documentation */
/* TODO: Define this in terms of MSG somehow. I don't
know how to pass '...' arguments to another C function */
void MSG(int level, const char *format, ...)
{
if ((level <= SpeechdOptions.log_level)
|| (SpeechdOptions.debug)) {
va_list args;
int i;
pthread_mutex_lock(&logging_mutex);
{
/* Print timestamp */
{
time_t t;
char tstr[26];
struct timeval tv;
t = time(NULL);
ctime_r(&t, tstr);
gettimeofday(&tv, NULL);
/* Remove the trailing \n */
assert(strlen(tstr) > 1);
assert((level >= -1) && (level <= 5));
tstr[strlen(tstr) - 1] = 0;
/* Write timestamps */
if (level <= SpeechdOptions.log_level)
fprintf(logfile, "[%s : %d] speechd: ",
tstr, (int)tv.tv_usec);
if (SpeechdOptions.debug)
fprintf(debug_logfile,
"[%s : %d] speechd: ", tstr,
(int)tv.tv_usec);
/* fprintf(logfile, "[%s : %d] speechd: ",
tstr, (int) tv.tv_usec); */
}
for (i = 1; i < level; i++) {
fprintf(logfile, " ");
}
/* Log to ordinary logfile */
if (level <= SpeechdOptions.log_level) {
va_start(args, format);
vfprintf(logfile, format, args);
va_end(args);
fprintf(logfile, "\n");
fflush(logfile);
}
/* Log into debug logfile */
if (SpeechdOptions.debug) {
va_start(args, format);
vfprintf(debug_logfile, format, args);
va_end(args);
fprintf(debug_logfile, "\n");
fflush(debug_logfile);
}
/* Log also into stderr for loglevel -1 */
if (level == -1) {
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fprintf(stderr, "\n");
fflush(stderr);
}
}
pthread_mutex_unlock(&logging_mutex);
}
}
/* --- CLIENTS / CONNECTIONS MANAGING --- */
/* Initialize sockets status table */
int speechd_sockets_status_init(void)
{
speechd_sockets_status = g_hash_table_new_full(g_int_hash, g_int_equal,
(GDestroyNotify) g_free,
(GDestroyNotify)
speechd_socket_free);
if (speechd_sockets_status)
return 0;
else
return 1;
}
/* Register a new socket for SSIP connection */
int speechd_socket_register(int fd)
{
int *fd_key;
TSpeechDSock *speechd_socket;
speechd_socket = g_malloc(sizeof(TSpeechDSock));
speechd_socket->o_buf = NULL;
speechd_socket->o_bytes = 0;
speechd_socket->awaiting_data = 0;
speechd_socket->inside_block = 0;
fd_key = g_malloc(sizeof(int));
*fd_key = fd;
g_hash_table_insert(speechd_sockets_status, fd_key, speechd_socket);
return 0;
}
/* Free a TSpeechDSock structure including it's data */
void speechd_socket_free(TSpeechDSock * speechd_socket)
{
if (speechd_socket->o_buf)
g_string_free(speechd_socket->o_buf, 1);
g_free(speechd_socket);
}
/* Unregister a socket for SSIP communication */
int speechd_socket_unregister(int fd)
{
return (!g_hash_table_remove(speechd_sockets_status, &fd));
}
/* Get a pointer to the TSpeechDSock structure for a given file descriptor */
TSpeechDSock *speechd_socket_get_by_fd(int fd)
{
return g_hash_table_lookup(speechd_sockets_status, &fd);
}
/* activity is on server_socket (request for a new connection) */
int speechd_connection_new(int server_socket)
{
TFDSetElement *new_fd_set;
struct sockaddr_in client_address;
unsigned int client_len = sizeof(client_address);
int client_socket;
int *p_client_socket, *p_client_uid, *p_client_uid2;
client_socket =
accept(server_socket, (struct sockaddr *)&client_address,
&client_len);
if (client_socket == -1) {
MSG(2,
"Error: Can't handle connection request of a new client");
return -1;
}
/* We add the associated client_socket to the descriptor set. */
if (client_socket > SpeechdStatus.max_fd)
SpeechdStatus.max_fd = client_socket;
MSG(4, "Adding client on fd %d", client_socket);
speechd_socket_register(client_socket);
/* Create a record in fd_settings */
new_fd_set = (TFDSetElement *) default_fd_set();
if (new_fd_set == NULL) {
MSG(2,
"Error: Failed to create a record in fd_settings for the new client");
if (SpeechdStatus.max_fd == client_socket)
SpeechdStatus.max_fd--;
return -1;
}
new_fd_set->fd = client_socket;
new_fd_set->uid = ++SpeechdStatus.max_uid;
p_client_socket = (int *)g_malloc(sizeof(int));
p_client_uid = (int *)g_malloc(sizeof(int));
p_client_uid2 = (int *)g_malloc(sizeof(int));
*p_client_socket = client_socket;
*p_client_uid = SpeechdStatus.max_uid;
*p_client_uid2 = SpeechdStatus.max_uid;
g_hash_table_insert(fd_settings, p_client_uid, new_fd_set);
g_hash_table_insert(fd_uid, p_client_socket, p_client_uid2);
new_fd_set->fd_source = g_unix_fd_add(client_socket, G_IO_IN, client_process_incoming, NULL);
MSG(4, "Data structures for client on fd %d created", client_socket);
client_count++;
check_client_count();
return 0;
}
int speechd_connection_destroy(int fd)
{
TFDSetElement *fdset_element;
/* Client has gone away and we remove it from the descriptor set. */
MSG(4, "Removing client on fd %d", fd);
MSG(4, "Tagging client as inactive in settings");
fdset_element = get_client_settings_by_fd(fd);
if (fdset_element != NULL) {
fdset_element->fd = -1;
fdset_element->active = 0;
g_source_remove(fdset_element->fd_source);
/* The fdset_element will be freed and removed from the
hash table as soon as the client no longer has any
message in the queues, check out the speak() function */
} else if (SPEECHD_DEBUG) {
DIE("Can't find settings for this client\n");
}
MSG(4, "Removing client from the fd->uid table.");
g_hash_table_remove(fd_uid, &fd);
speechd_socket_unregister(fd);
MSG(4, "Closing clients file descriptor %d", fd);
if (close(fd) != 0)
if (SPEECHD_DEBUG)
DIE("Can't close file descriptor associated to this client");
if (fd == SpeechdStatus.max_fd)
SpeechdStatus.max_fd--;
MSG(4, "Connection closed");
client_count--;
check_client_count();
return 0;
}
static gboolean speechd_client_terminate(gpointer key, gpointer value, gpointer user)
{
TFDSetElement *set;
set = (TFDSetElement *) value;
if (set == NULL) {
MSG(2, "Error: Empty connection, internal error");
if (SPEECHD_DEBUG)
FATAL("Internal error");
return TRUE;
}
if (set->fd > 0) {
MSG(4, "Closing connection on fd %d\n", set->fd);
speechd_connection_destroy(set->fd);
}
mem_free_fdset(set);
g_free(set);
return TRUE;
}
/* --- OUTPUT MODULES MANAGING --- */
void speechd_modules_terminate(gpointer data, gpointer user_data)
{
OutputModule *module;
module = (OutputModule *) data;
if (module == NULL) {
MSG(2, "Error: Empty module, internal error");
return;
}
unload_output_module(module);
return;
}
void speechd_modules_reload(gpointer data, gpointer user_data)
{
OutputModule *module;
module = (OutputModule *) data;
if (module == NULL) {
MSG(2, "Empty module, internal error");
return;
}
reload_output_module(module);
return;
}
void speechd_module_debug(gpointer data, gpointer user_data)
{
OutputModule *module;
module = (OutputModule *) data;
if (module == NULL) {
MSG(2, "Empty module, internal error");
return;
}
output_module_debug(module);
return;
}
void speechd_module_nodebug(gpointer data, gpointer user_data)
{
OutputModule *module;
module = (OutputModule *) data;
if (module == NULL) {
MSG(2, "Empty module, internal error");
return;
}
output_module_nodebug(module);
return;
}
static gboolean speechd_reload_dead_modules(gpointer user_data)
{
/* Reload dead modules */
g_list_foreach(output_modules, speechd_modules_reload, NULL);
/* Make sure there aren't any more child processes left */
while (waitpid(-1, NULL, WNOHANG) > 0) ;
return TRUE;
}
void speechd_modules_debug(void)
{
/* Redirect output to debug for all modules */
g_list_foreach(output_modules, speechd_module_debug, NULL);
}
void speechd_modules_nodebug(void)
{
/* Redirect output to normal for all modules */
g_list_foreach(output_modules, speechd_module_nodebug, NULL);
}
/* --- SPEECHD START/EXIT FUNCTIONS --- */
void speechd_options_init(void)
{
SpeechdOptions.spawn = FALSE;
SpeechdOptions.log_level_set = 0;
SpeechdOptions.communication_method = NULL;
SpeechdOptions.socket_path = NULL;
SpeechdOptions.port_set = 0;
SpeechdOptions.localhost_access_only_set = 0;
SpeechdOptions.pid_file = NULL;
SpeechdOptions.conf_file = NULL;
SpeechdOptions.conf_dir = NULL;
SpeechdOptions.user_conf_dir = NULL;
SpeechdOptions.runtime_speechd_dir = NULL;
SpeechdOptions.module_dir = MODULEBINDIR;
SpeechdOptions.user_module_dir = NULL;
SpeechdOptions.log_dir = NULL;
SpeechdOptions.log_dir_set = 0;
SpeechdOptions.debug = 0;
SpeechdOptions.debug_destination = NULL;
debug_logfile = NULL;
spd_mode = SPD_MODE_DAEMON;
}
void speechd_init()
{
SpeechdStatus.max_uid = 0;
SpeechdStatus.max_gid = 0;
/* Initialize inter-thread comm pipe */
if (pipe(speaking_pipe)) {
MSG(1, "Speaking pipe creation failed (%s)", strerror(errno));
FATAL("Can't create pipe");
}
/* Initialize Speech Dispatcher priority queue */
MessageQueue = g_malloc0(sizeof(TSpeechDQueue));
if (MessageQueue == NULL)
FATAL("Couldn't allocate memory for MessageQueue.");
/* Initialize lists */
MessagePausedList = NULL;
/* Initialize hash tables */
fd_settings = g_hash_table_new_full(g_int_hash, g_int_equal,
(GDestroyNotify) g_free, NULL);
assert(fd_settings != NULL);
fd_uid = g_hash_table_new_full(g_int_hash, g_int_equal,
(GDestroyNotify) g_free,
(GDestroyNotify) g_free);
assert(fd_uid != NULL);
language_default_modules = g_hash_table_new(g_str_hash, g_str_equal);
assert(language_default_modules != NULL);
speechd_sockets_status_init();
pause_requested = 0;
resume_requested = 0;
/* Perform some functionality tests */
if (g_module_supported() == FALSE)
DIE("Loadable modules not supported by current platform.\n");
if (_POSIX_VERSION < 199506L)
DIE("This system doesn't support POSIX.1c threads\n");
/* Fill GlobalFDSet with default values */
GlobalFDSet.min_delay_progress = 2000;
/* Initialize list of different client specific settings entries */
client_specific_settings = NULL;
if (SpeechdOptions.log_dir == NULL) {
SpeechdOptions.log_dir =
g_strdup_printf("%s/log/",
SpeechdOptions.runtime_speechd_dir);
mkdir(SpeechdOptions.log_dir, S_IRWXU);
if (!SpeechdOptions.debug_destination) {
SpeechdOptions.debug_destination =
g_strdup_printf("%s/log/debug",
SpeechdOptions.runtime_speechd_dir);
mkdir(SpeechdOptions.debug_destination, S_IRWXU);
}
}
/* Load configuration from the config file */
MSG(4, "Reading Speech Dispatcher configuration from %s",
SpeechdOptions.conf_file);
speechd_load_configuration();
logging_init();
MSG(1, "Speech Dispatcher " VERSION " log start");
#ifndef DARWIN_HOST /* On Darwin we have to delay to after daemon+exec */
module_load_requested_modules();
speechd_check_modules();
#endif
last_p5_block = NULL;
}
static gint modules_compare (gconstpointer a, gconstpointer b)
{
const char **params_a = (const char **) a;
const char *name_a = params_a[0];
const char **params_b = (const char **) b;
const char *name_b = params_b[0];
int index_a;
int index_b;
/* This gives the prioritization order of modules, to automatically select the best quality */
static const char *modules_order[] = {
"voxin",
"baratinoo",
"ivona",
"pico",
"pico-generic",
"rhvoice",
"cicero",
"kali",
"ibmtts",
"festival",
"flite",
"multispeech",
"espeak-ng-mbrola",
"espeak-ng-mbrola-generic",
"espeak-ng",
"espeak-mbrola-generic",
"espeak",
"espeak-generic",
};
static const int index_max = sizeof(modules_order) / sizeof(*modules_order);
for (index_a = 0; index_a < index_max; index_a++) {
if (!strcmp(modules_order[index_a], name_a))
break;
}
for (index_b = 0; index_b < index_max; index_b++) {
if (!strcmp(modules_order[index_b], name_b))
break;
}
if (index_a != index_max && index_b == index_max)
/* We prefer a synth in the list */
return -1;
if (index_a == index_max && index_b != index_max)
/* We prefer a synth in the list */
return 1;
if (index_a != index_max && index_b != index_max)
/* We follow the ordering */
return index_a - index_b;
/* We fall back to alphabetical ordering */
return strcmp(name_a, name_b);
}
static void speechd_load_configuration(void)
{
configfile_t *configfile = NULL;
GList *detected_modules = NULL;
/* Clean previous configuration */
if (output_modules != NULL) {
g_list_foreach(output_modules, speechd_modules_terminate, NULL);
g_list_free(output_modules);
output_modules = NULL;
}
/* Make sure there aren't any more child processes left */
while (waitpid(-1, NULL, WNOHANG) > 0) ;
/* Load new configuration */
load_default_global_set_options();
spd_num_options = 0;
spd_options = load_config_options(&spd_num_options);
/* Add the LAST option */
spd_options = add_config_option(spd_options, &spd_num_options, "", 0,
NULL, NULL, 0);
configfile =
dotconf_create(SpeechdOptions.conf_file, spd_options, 0,
CASE_INSENSITIVE);
if (configfile) {
free(configfile->includepath);
configfile->includepath = g_strdup(SpeechdOptions.conf_dir);
MSG(5, "Config file include path is: %s",
configfile->includepath);
if (dotconf_command_loop(configfile) == 0)
DIE("Error reading config file\n");
dotconf_cleanup(configfile);
MSG(2, "Configuration has been read from \"%s\"",
SpeechdOptions.conf_file);
/* We need to load modules here, since this is called both by speechd_init
* and to handle SIGHUP. */
if (module_number_of_requested_modules() < 1) {
detected_modules = detect_output_modules(NULL,
SpeechdOptions.user_module_dir,
SpeechdOptions.user_conf_dir,
SpeechdOptions.conf_dir);
detected_modules = detect_output_modules(detected_modules,
SpeechdOptions.module_dir,
SpeechdOptions.user_conf_dir,
SpeechdOptions.conf_dir);
detected_modules = detect_output_modules(detected_modules,
OLDMODULEBINDIR,
SpeechdOptions.user_conf_dir,
SpeechdOptions.conf_dir);
detected_modules = g_list_sort(detected_modules, modules_compare);
while (detected_modules != NULL) {
char **parameters = detected_modules->data;
module_add_load_request(parameters[0],
parameters[1],
parameters[2],
parameters[3],
parameters[4],
parameters[5]);
g_free(detected_modules->data);
detected_modules->data = NULL;
detected_modules =
g_list_delete_link(detected_modules,
detected_modules);
}
}
/* Try to make sure we have something that can speak (with no user parameter) */
module_add_load_request(
g_strdup("espeak-ng-fallback"),
g_strdup("sd_espeak-ng"),
g_strdup(""),
g_strdup_printf("%s/%s.log",
SpeechdOptions.log_dir,
"espeak-ng-fallback"),
NULL,
NULL);
/* At worse, tell what is happening */
module_add_load_request(
g_strdup("dummy"),
g_strdup("sd_dummy"),
g_strdup(""),
g_strdup_printf("%s/%s.log",
SpeechdOptions.log_dir,
"dummy"),
NULL,
NULL);
} else {
MSG(1, "Can't open %s", SpeechdOptions.conf_file);
}
free_config_options(spd_options, &spd_num_options);
}
static gboolean speechd_reload_configuration(gpointer user_data)
{
speechd_load_configuration();
module_load_requested_modules();
return TRUE;
}
static void speechd_check_modules(void)
{
/* Check for output modules */
if (g_list_length(output_modules) == 0) {
DIE("No speech output modules were loaded - aborting...");
} else {
MSG(3, "Speech Dispatcher started with %d output module%s",
g_list_length(output_modules),
g_list_length(output_modules) > 1 ? "s" : "");
}
}
static gboolean speechd_quit(gpointer user_data)
{
switch ((enum quit_reason)(uintptr_t) user_data) {
case QUIT_SIGINT:
MSG(4, "Got SIGINT");
break;
case QUIT_SIGTERM:
MSG(4, "Got SIGTERM");
break;
case QUIT_TIMEOUT:
MSG(4, "Timed out");
break;
default:
MSG(4, "Unknown quit reason");
break;
}
MSG(4, "Quitting main loop");
g_main_loop_quit(main_loop);
return FALSE;
}
/* --- PID FILES --- */
int create_pid_file()
{
FILE *pid_file;
int pid_fd;
struct flock lock;
int ret;
/* If the file exists, examine it's lock */
pid_file = fopen(SpeechdOptions.pid_file, "r");
if (pid_file != NULL) {
pid_fd = fileno(pid_file);
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 1;
lock.l_len = 3;
/* If there is a lock, exit, otherwise remove the old file */
ret = fcntl(pid_fd, F_GETLK, &lock);
if (ret == -1) {
MSG(-1,
"Can't check lock status of an existing pid file.\n");
return -1;
}
fclose(pid_file);
if (lock.l_type != F_UNLCK) {
MSG(-1, "Speech Dispatcher already running.\n");
return -1;
}
unlink(SpeechdOptions.pid_file);
}
/* Create a new pid file and lock it */
pid_file = fopen(SpeechdOptions.pid_file, "w");
if (pid_file == NULL) {
MSG(-1, "Can't create pid file in %s, wrong permissions?\n",
SpeechdOptions.pid_file);
return -1;
}
fprintf(pid_file, "%d\n", getpid());
fflush(pid_file);
pid_fd = fileno(pid_file);
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 1;
lock.l_len = 3;
ret = fcntl(pid_fd, F_SETLK, &lock);
if (ret == -1) {
MSG(-1, "Can't set lock on pid file.\n");
return -1;
}
return 0;
}
void destroy_pid_file(void)
{
unlink(SpeechdOptions.pid_file);
}
void logging_init(void)
{
char *file_name =
g_strdup_printf("%s/speech-dispatcher.log", SpeechdOptions.log_dir);
assert(file_name != NULL);
if (!strncmp(file_name, "stdout", 6)) {
logfile = stdout;
} else if (!strncmp(file_name, "stderr", 6)) {
logfile = stderr;
} else {
logfile = fopen(file_name, "a");
if (logfile == NULL) {
fprintf(stderr,
"Error: can't open logging file %s! Using stdout.\n",
file_name);
logfile = stdout;
} else {
MSG(3, "Speech Dispatcher "PACKAGE_VERSION" Logging to file %s at level %d",
file_name, SpeechdOptions.log_level);
}
}
if (!debug_logfile)
debug_logfile = stdout;
g_free(file_name);
return;
}
/* --- Sockets --- */
int make_local_socket(const char *filename)
{
struct sockaddr_un name;
int sock;
size_t size;
/* Create the socket. */
sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock < 0) {
FATAL("Can't create local socket");
}
/* Bind a name to the socket. */
name.sun_family = AF_UNIX;
strncpy(name.sun_path, filename, sizeof(name.sun_path));
name.sun_path[sizeof(name.sun_path) - 1] = '\0';
size = SUN_LEN(&name);
if (bind(sock, (struct sockaddr *)&name, size) < 0) {
FATAL("Can't bind local socket");
}
if (listen(sock, 50) == -1) {
MSG(2, "listen failed: ERRNO:%s", strerror(errno));
FATAL("listen() failed for local socket");
}
return sock;
}
int make_inet_socket(const int port)
{
struct sockaddr_in server_address;
int server_socket;
/* Create an inet socket */
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
FATAL("Can't create inet socket");
}
/* Set REUSEADDR flag */
const int flag = 1;
if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &flag,
sizeof(int)))
MSG(2, "Error: Setting socket option failed!");
server_address.sin_family = AF_INET;
/* Enable access only to localhost or for any address
based on LocalhostAccessOnly configuration option. */
if (SpeechdOptions.localhost_access_only)
server_address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
else
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(port);
MSG(4, "Opening inet socket connection");
if (bind(server_socket, (struct sockaddr *)&server_address,
sizeof(server_address)) == -1) {
MSG(-1, "bind() failed: %s", strerror(errno));
FATAL("Couldn't open inet socket, try a few minutes later.");
}
if (listen(server_socket, 50) == -1) {
MSG(2, "ERRNO:%s", strerror(errno));
FATAL
("listen() failed for inet socket, another Speech Dispatcher running?");
}
return server_socket;
}
gboolean server_process_incoming (gint fd,
GIOCondition condition,
gpointer data)
{
int ret;
ret = speechd_connection_new(fd);
if (ret != 0) {
MSG(2, "Error: Failed to add new client!");
if (SPEECHD_DEBUG) {
FATAL("Failed to add new client");
}
}
return TRUE;
}
gboolean client_process_incoming (gint fd,
GIOCondition condition,
gpointer data)
{
int ret;
int nread;
ioctl(fd, FIONREAD, &nread);
if (nread == 0) {
/* client has gone */
ret = speechd_connection_destroy(fd);
if (ret != 0) {
MSG(2, "Error: Failed to close the client!");
}
return FALSE;
}
/* client sends some commands or data */
if (serve(fd) == -1) {
MSG(2, "Error: Failed to serve client on fd %d!", fd);
}
return TRUE;
}
static void check_client_count(void)
{
if (client_count <= 0
&& SpeechdOptions.server_timeout > 0) {
MSG(4, "Currently no clients connected, enabling shutdown timer.");
server_timeout_source = g_timeout_add_seconds(
SpeechdOptions.server_timeout,
speechd_quit, (void*) (uintptr_t) QUIT_TIMEOUT);
} else {
if (server_timeout_source >= 0) {
MSG(4, "Clients connected, disabling shutdown timer.");
g_source_remove(server_timeout_source);
server_timeout_source = -1;
}
}
}
/* --- MAIN --- */
int main(int argc, char *argv[])
{
int ret;
/* Autospawn helper variables */
char *spawn_communication_method = NULL;
int spawn_port = 0;
char *spawn_socket_path = NULL;
/* Strip all permissions for 'others' from the files created */
umask(007);
/* Initialize logging */
logfile = stdout;
SpeechdOptions.log_level = 1;
custom_logfile = NULL;
custom_log_kind = NULL;
/* initialize i18n support */
i18n_init();
speechd_options_init();
options_parse(argc, argv);
if (SpeechdOptions.spawn) {
/* In case of --spawn, copy the host port and socket_path
parameters into temporary spawn_ variables for later comparison
with the config file and unset them */
if (SpeechdOptions.communication_method_set) {
spawn_communication_method =
g_strdup(SpeechdOptions.communication_method);
g_free(SpeechdOptions.communication_method);
SpeechdOptions.communication_method_set = 0;
}
if (SpeechdOptions.port_set) {
spawn_port = SpeechdOptions.port;
SpeechdOptions.port_set = 0;
}
if (SpeechdOptions.socket_path_set) {
spawn_socket_path =
g_strdup(SpeechdOptions.socket_path);
g_free(SpeechdOptions.socket_path);
SpeechdOptions.socket_path_set = 0;
}
}
MSG(1, "Speech Dispatcher " VERSION " starting");
/* By default, search for configuration options in $XDG_CONFIG_HOME/speech-dispatcher
and sockets and pid files in $XDG_RUNTIME_DIR/speech-dispatcher */
{
const char *user_runtime_dir;
const char *user_config_dir;
const char *user_data_dir;
user_runtime_dir = g_get_user_runtime_dir();
user_config_dir = g_get_user_config_dir();
user_data_dir = g_get_user_data_dir();
/* Setup a speechd-dispatcher directory or create a new one */
SpeechdOptions.runtime_speechd_dir =
g_strdup_printf("%s/speech-dispatcher", user_runtime_dir);
MSG(4, "Trying to find %s", SpeechdOptions.runtime_speechd_dir);
g_mkdir_with_parents(SpeechdOptions.runtime_speechd_dir,
S_IRWXU);
MSG(4, "Using directory: %s for pidfile and logging",
SpeechdOptions.runtime_speechd_dir);
/* Pidfile */
if (SpeechdOptions.pid_file == NULL) {
/* If no pidfile path specified on command line, use default local dir */
SpeechdOptions.pid_file =
g_strdup_printf("%s/pid/speech-dispatcher.pid",
SpeechdOptions.runtime_speechd_dir);
gchar *dirname = g_path_get_dirname(SpeechdOptions.pid_file);
g_mkdir(dirname, S_IRWXU);
g_free(dirname);
}
/* Config file */
if (SpeechdOptions.conf_dir) {
SpeechdOptions.conf_file =
g_strdup_printf("%s/speechd.conf", SpeechdOptions.conf_dir);
} else {
/* If no conf_dir was specified on command line, try default local config dir */
if (strcmp(SYS_CONF, ""))
SpeechdOptions.conf_dir =
g_strdup(SYS_CONF);
else
SpeechdOptions.conf_dir =
g_strdup("/etc/speech-dispatcher/");
SpeechdOptions.user_conf_dir =
g_build_filename(user_config_dir,
"speech-dispatcher", NULL);
MSG(4, "Looking for configuration in %s", SpeechdOptions.user_conf_dir);
SpeechdOptions.conf_file =
g_strdup_printf("%s/speechd.conf", SpeechdOptions.user_conf_dir);
if (!g_file_test
(SpeechdOptions.conf_file, G_FILE_TEST_IS_REGULAR)) {
/* If the local configuration file doesn't exist, read the global configuration */
g_free(SpeechdOptions.conf_file);
SpeechdOptions.conf_file =
g_strdup_printf("%s/speechd.conf", SpeechdOptions.conf_dir);
}
}
SpeechdOptions.user_module_dir =
g_strdup_printf("%s/../libexec/speech-dispatcher-modules", user_data_dir);
MSG(4, "User module dir is %s", SpeechdOptions.user_module_dir);
}
/* Check for PID file or create a new one or exit if Speech Dispatcher
is already running */
if (create_pid_file() != 0)
exit(1);
/* Handle --spawn request */
if (SpeechdOptions.spawn) {
/* Check whether spawning is not disabled */
gchar *config_contents;
int err;
GRegex *regexp;
int result;
err =
g_file_get_contents(SpeechdOptions.conf_file,
&config_contents, NULL, NULL);
if (err == FALSE) {
MSG(-1, "Error opening %s", SpeechdOptions.conf_file);
FATAL("Can't open conf file");
}
regexp =
g_regex_new("^[ ]*DisableAutoSpawn", G_REGEX_MULTILINE, 0,
NULL);
result = g_regex_match(regexp, config_contents, 0, NULL);
if (result) {
MSG(-1,
"Autospawn requested but disabled in configuration");
exit(1);
}
g_free(config_contents);
g_regex_unref(regexp);
MSG(2, "Starting Speech Dispatcher due to auto-spawn");
}
speechd_init();
/* Handle socket_path 'default' */
// TODO: This is a hack, we should do that at appropriate places...
if (!strcmp(SpeechdOptions.socket_path, "default")) {
/* This code cannot be moved above next to conf_dir and pidpath resolution because
we need to also consider the DotConf configuration, which is read in speechd_init() */
GString *socket_filename;
socket_filename = g_string_new("");
if (SpeechdOptions.runtime_speechd_dir) {
g_string_printf(socket_filename, "%s/speechd.sock",
SpeechdOptions.runtime_speechd_dir);
} else {
FATAL
("Socket name file not set and user has no runtime directory");
}
g_free(SpeechdOptions.socket_path);
SpeechdOptions.socket_path = g_strdup(socket_filename->str);
g_string_free(socket_filename, 1);
}
/* Check if the communication method corresponds to the spawn request */
/* TODO: This should preferably be done much sooner, but the current
configuration mechanism doesn't allow it */
if (SpeechdOptions.spawn) {
if (spawn_communication_method) {
if (strcmp
(spawn_communication_method,
SpeechdOptions.communication_method)) {
MSG(-1,
"Autospawn failed: Mismatch in communication methods. Client "
"requests %s, most probably due to its configuration or the value of "
"the SPEECHD_ADDRESS environment variable, but the server is configured "
"to provide the %s method.",
spawn_communication_method,
SpeechdOptions.communication_method);
exit(1);
} else {
if (!strcmp
(SpeechdOptions.communication_method,
"inet_socket")) {
/* Check port */
if (spawn_port != 0)
if (spawn_port !=
SpeechdOptions.port) {
MSG(-1,
"Autospawn failed: Mismatch in port numbers. Server "
"is configured to use the inet_socket method on the port %d "
"while the client requests port %d, most probably due to its "
"configuration or the value of the SPEECHD_ADDRESS environment "
"variable.",
SpeechdOptions.port,
spawn_port);
exit(1);
}
} else if (!strcmp
(SpeechdOptions.communication_method,
"unix_socket")) {
/* Check socket name */
if (spawn_socket_path)
{
char *spawn_socket_path_norm = realpath(spawn_socket_path, NULL);
char *opts_socket_path_norm = realpath(SpeechdOptions.socket_path, NULL);
if (spawn_socket_path_norm && opts_socket_path_norm
&& strcmp(spawn_socket_path_norm, opts_socket_path_norm))
{
MSG(-1,
"Autospawn failed: Mismatch in socket names. The server "
"is configured to provide a socket interface in %s, but the "
"client requests a different path: %s. This is most probably "
"due to the client application configuration or the value of "
"the SPEECHD_ADDRESS environment variable.",
SpeechdOptions.socket_path,
spawn_socket_path);
exit(1);
}
free(spawn_socket_path_norm);
free(opts_socket_path_norm);
}
} else
assert(0);
}
}
g_free(spawn_communication_method);
g_free(spawn_socket_path);
}
char *sock_fd = getenv("SPEECHD_SOCK_FD");
if (sock_fd) {
server_socket = atoi(sock_fd);
}
#ifdef USE_LIBSYSTEMD
else if (sd_listen_fds(0) >= 1) {
MSG(4, "Daemon launched via Systemd socket activation");
server_socket = SD_LISTEN_FDS_START;
}
#endif
else if (!strcmp(SpeechdOptions.communication_method, "inet_socket")) {
MSG(4, "Speech Dispatcher will use inet port %d",
SpeechdOptions.port);
/* Connect and start listening on inet socket */
server_socket = make_inet_socket(SpeechdOptions.port);
} else if (!strcmp(SpeechdOptions.communication_method, "unix_socket")) {
/* Determine appropariate socket file name */
MSG(4, "Speech Dispatcher will use local unix socket: %s",
SpeechdOptions.socket_path);
/* Delete an old socket file if it exists */
if (g_file_test(SpeechdOptions.socket_path, G_FILE_TEST_EXISTS))
{
if (g_unlink(SpeechdOptions.socket_path) == -1)
FATAL
("Local socket file exists but impossible to delete. Wrong permissions?");
else
MSG(4, "Deleted old socket\n");
}
/* Connect and start listening on local unix socket */
server_socket = make_local_socket(SpeechdOptions.socket_path);
} else {
FATAL("Unknown communication method");
}
/* Fork, set uid, chdir, etc. */
if (spd_mode == SPD_MODE_DAEMON) {
MSG(4, "Daemon mode, forking\n");
if (daemon(0, 0)) {
FATAL("Can't fork child process");
}
/* Re-create the pid file under this process */
unlink(SpeechdOptions.pid_file);
if (create_pid_file() == -1)
return -1;
#ifdef DARWIN_HOST
/* On Darwin, libao sound will break if just forking, we have to re-exec, see
* https://developer.apple.com/library/archive/technotes/tn2083/_index.html#//apple_ref/doc/uid/DTS10003794-CH1-SUBSUBSECTION66
*/
char *args[argc+2];
memcpy(args, argv, argc * sizeof *argv);
args[argc] = "-s";
args[argc+1] = NULL;
char sock[64];
sprintf(sock, "SPEECHD_SOCK_FD=%d", server_socket);
putenv(sock);
if (execv(BINDIR "/speech-dispatcher", args))
FATAL("Can't exec child process");
#endif
}
#ifdef DARWIN_HOST
module_load_requested_modules();
speechd_check_modules();
#endif
/* Set up the main loop and register signals */
main_loop = g_main_loop_new(g_main_context_default(), FALSE);
g_unix_signal_add(SIGINT, speechd_quit, (void*) (uintptr_t) QUIT_SIGINT);
g_unix_signal_add(SIGTERM, speechd_quit, (void*) (uintptr_t) QUIT_SIGTERM);
g_unix_signal_add(SIGHUP, speechd_reload_configuration, NULL);
g_unix_signal_add(SIGUSR1, speechd_reload_dead_modules, NULL);
(void)signal(SIGPIPE, SIG_IGN);
MSG(4, "Creating new thread for speak()");
ret = pthread_create(&speak_thread, NULL, speak, NULL);
if (ret != 0)
FATAL("Speak thread failed!\n");
char *status;
ret = module_speak_queue_init(SpeechdOptions.max_queue_size, &status);
if (ret != 0)
FATAL("Speak queue thread failed: %s!\n", status);
SpeechdStatus.max_fd = server_socket;
g_unix_fd_add(server_socket, G_IO_IN,
server_process_incoming, NULL);
/* Now wait for clients and requests. */
MSG(1, "Speech Dispatcher started and waiting for clients ...");
check_client_count();
g_main_loop_run(main_loop);
MSG(1, "Terminating...");
MSG(2, "Closing open connections...");
/* We will browse through all the connections and close them. */
g_hash_table_foreach_remove(fd_settings, speechd_client_terminate,
NULL);
g_hash_table_destroy(fd_settings);
MSG(4, "Closing speak() thread...");
ret = pthread_cancel(speak_thread);
if (ret != 0)
FATAL("Speak thread failed to cancel!\n");
ret = pthread_join(speak_thread, NULL);
if (ret != 0)
FATAL("Speak thread failed to join!\n");
MSG(4, "Closing play() thread...");
module_speak_queue_terminate();
MSG(2, "Closing open output modules...");
/* Call the close() function of each registered output module. */
g_list_foreach(output_modules, speechd_modules_terminate, NULL);
g_list_free(output_modules);
MSG(2, "Closing server connection...");
if (close(server_socket) == -1)
MSG(2, "close() failed: %s", strerror(errno));
MSG(4, "Removing pid file");
destroy_pid_file();
fflush(NULL);
g_main_loop_unref(main_loop);
main_loop = NULL;
MSG(2, "Speech Dispatcher terminated correctly");
exit(0);
}
void check_locked(pthread_mutex_t * lock)
{
if (pthread_mutex_trylock(lock) == 0) {
MSG(1,
"CRITICAL ERROR: Not locked but accessing structure data!");
fprintf(stderr, "WARNING! WARNING! MUTEX CHECK FAILED!\n");
fflush(stderr);
exit(0);
}
}