blob: ad1b258fac4eba86f16a17d2b532a0bf4d4cbe8b [file] [log] [blame]
/*
* module.c - Output modules for Speech Dispatcher
*
* 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: module.c,v 1.40 2008-07-07 14:30:51 hanke Exp $
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <dirent.h>
#include <glib.h>
#include <dotconf.h>
#include "speechd.h"
#include "output.h"
#include "module.h"
static char *spd_get_path(const char *filename, const char *startdir)
{
char *ret;
if (filename == NULL)
return NULL;
if (filename[0] != '/') {
if (startdir == NULL)
ret = g_strdup(filename);
else
ret = g_strdup_printf("%s/%s", startdir, filename);
} else {
ret = g_strdup(filename);
}
return ret;
}
void destroy_module(OutputModule * module)
{
close(module->pipe_speak[0]);
close(module->pipe_speak[1]);
if (module->stderr_redirect >= 0)
close(module->stderr_redirect);
g_free(module->name);
g_free(module->filename);
g_free(module->configfilename);
g_free(module->debugfilename);
g_free(module->progdir);
g_free(module->configdir);
g_free(module);
}
/*
* Check that we can execute the configured command
*/
DOTCONF_CB(GenericCmdDependency_cb)
{
unsigned *missing_dep = ctx;
char s[5 + strlen(cmd->data.str) + 17 + 1];
if (!cmd->data.str[0])
return NULL;
snprintf(s, sizeof(s), "type %s > /dev/null 2>&1", cmd->data.str);
if (system(s) != 0)
{
MSG(5, "Did not find command %s", cmd->data.str);
(*missing_dep)++;
}
return NULL;
}
/*
* Check that we can connect to the configured local port
*/
DOTCONF_CB(GenericPortDependency_cb)
{
unsigned *missing_dep = ctx;
int s = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in sin;
if (s < 0)
{
MSG(5, "Could not establish IPv4 socket: %s", strerror(errno));
(*missing_dep)++;
return NULL;
}
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
sin.sin_port = htons(cmd->data.value);
if (connect(s, (struct sockaddr *) &sin, sizeof(sin)) < 0)
{
MSG(5, "Could not connect to IPv4 socket: %s", strerror(errno));
(*missing_dep)++;
}
close(s);
return NULL;
}
FUNC_ERRORHANDLER(ignore_errors)
{
return 0;
}
#define FNAME_PREFIX "sd_"
static GList *detect_generic_modules(GList *modules, const char *dirname, const char *config_dirname)
{
struct dirent *entry;
char **module_parameters;
char *full_path;
struct stat fileinfo;
int sys_ret;
/* Special-case the generic module: autoload for the various
* configurations */
full_path = spd_get_path("modules", config_dirname);
MSG(5, "Looking for generic variants configurations in %s",
full_path);
DIR *config_dir = opendir(full_path);
if (config_dir == NULL)
{
MSG(4, "couldn't open directory %s because of error %s\n",
full_path, strerror(errno));
g_free(full_path);
return modules;
}
while (NULL != (entry = readdir(config_dir))) {
size_t len;
char *file_path;
static const configoption_t options[] = {
{
.name = "GenericCmdDependency",
.type = ARG_STR,
.callback = GenericCmdDependency_cb,
.info = NULL,
.context = 0,
},
{
.name = "GenericPortDependency",
.type = ARG_INT,
.callback = GenericPortDependency_cb,
.info = NULL,
.context = 0,
},
{
.name = "",
.type = 0,
.callback = NULL,
.info = NULL,
.context = 0,
}
};
configfile_t *configfile;
unsigned missing_dep = 0;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
file_path = spd_get_path(entry->d_name, full_path);
sys_ret = stat(file_path, &fileinfo);
if (sys_ret != 0) {
MSG(4, "stat failed on file %s in %s", entry->d_name,
full_path);
g_free(file_path);
continue;
}
/* Note: stat(2) dereferences symlinks. */
if (!S_ISREG(fileinfo.st_mode)) {
MSG(4, "Ignoring %s in %s; not a regular file.",
entry->d_name, full_path);
g_free(file_path);
continue;
}
len = strlen(entry->d_name);
if (strcmp(entry->d_name + len - strlen("-generic.conf"),
"-generic.conf")) {
MSG(5, "Ignoring %s: not named something-generic.conf",
entry->d_name);
g_free(file_path);
continue;
}
/* Check for actual binaries and ports given by GenericCmdDependency and GenericPortDependency */
configfile = dotconf_create(file_path, options,
&missing_dep, CASE_INSENSITIVE);
if (!configfile) {
MSG(5, "Ignoring %s: Can not parse config file", file_path);
g_free(file_path);
continue;
}
configfile->errorhandler = (dotconf_errorhandler_t) ignore_errors;
if (dotconf_command_loop(configfile) == 0) {
MSG(5, "Ignoring %s: Can not parse config file", file_path);
g_free(file_path);
dotconf_cleanup(configfile);
continue;
}
dotconf_cleanup(configfile);
if (missing_dep != 0) {
MSG(5, "Ignoring %s: did not find %d dependency",
file_path, missing_dep);
g_free(file_path);
continue;
}
g_free(file_path);
module_parameters = g_malloc(6 * sizeof(char *));
module_parameters[1] = g_strdup(FNAME_PREFIX "generic");
module_parameters[2] = g_strdup(entry->d_name);
entry->d_name[len - strlen(".conf")] = '\0';
module_parameters[0] = g_strdup(entry->d_name);
module_parameters[3] =
g_strdup_printf("%s/%s.log", SpeechdOptions.log_dir,
entry->d_name);
module_parameters[4] = g_strdup(dirname);
module_parameters[5] = g_strdup(full_path);
modules = g_list_append(modules, module_parameters);
MSG(5,
"Module name=%s being inserted into detected_modules list",
entry->d_name);
}
g_free(full_path);
closedir(config_dir);
return modules;
}
/*
* detect_output_modules: automatically discover all available modules.
* Parameters:
* dirname: name of the directory containing module binaries.
* Returns: a list of detected modules.
* For each file in the directory containing module binaries,
* add an entry to a list of discovered modules.
*/
GList *detect_output_modules(GList *modules, const char *dirname, const char *user_config_dirname, const char *config_dirname)
{
static const int FNAME_PREFIX_LENGTH = strlen(FNAME_PREFIX);
DIR *module_dir = opendir(dirname);
struct dirent *entry;
char *module_name;
char **module_parameters;
char *full_path;
struct stat fileinfo;
int sys_ret;
MSG(5, "Detecting modules from %s\n", dirname);
if (module_dir == NULL) {
MSG(3, "couldn't open directory %s because of error %s\n",
dirname, strerror(errno));
return modules;
}
while (NULL != (entry = readdir(module_dir))) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
full_path = spd_get_path(entry->d_name, dirname);
sys_ret = stat(full_path, &fileinfo);
g_free(full_path);
if (sys_ret != 0) {
MSG(4, "stat failed on file %s in %s", entry->d_name,
dirname);
continue;
}
/* Note: stat(2) dereferences symlinks. */
if (!S_ISREG(fileinfo.st_mode)) {
MSG(4, "Ignoring %s in %s; not a regular file.",
entry->d_name, dirname);
continue;
}
if (strncmp(entry->d_name, FNAME_PREFIX, FNAME_PREFIX_LENGTH)
|| (entry->d_name[FNAME_PREFIX_LENGTH] == '\0')) {
MSG(1,
"Module discovery ignoring %s: malformed filename.",
entry->d_name);
continue;
}
module_name = entry->d_name + FNAME_PREFIX_LENGTH;
if (strcmp(module_name, "generic") != 0) {
module_parameters = g_malloc(6 * sizeof(char *));
module_parameters[0] = g_strdup(module_name);
module_parameters[1] = g_strdup(entry->d_name);
module_parameters[2] =
g_strdup_printf("%s.conf", module_parameters[0]);
module_parameters[3] =
g_strdup_printf("%s/%s.log", SpeechdOptions.log_dir,
module_parameters[0]);
module_parameters[4] = g_strdup(dirname);
module_parameters[5] = NULL;
modules = g_list_append(modules, module_parameters);
MSG(5,
"Module name=%s being inserted into detected_modules list",
module_parameters[0]);
} else {
modules = detect_generic_modules(modules, dirname, user_config_dirname);
modules = detect_generic_modules(modules, dirname, config_dirname);
}
}
closedir(module_dir);
return modules;
}
OutputModule *load_output_module(const char *mod_name, const char *mod_prog,
const char *mod_cfgfile, const char *mod_dbgfile,
const char *mod_prog_dir, const char *mod_cfg_dir)
{
OutputModule *module;
int fr;
char *argv[3] = { 0, 0, 0 };
int ret;
char *module_conf_dir;
char *rep_line = NULL;
FILE *f;
size_t n = 0;
char s;
GString *reply;
struct stat fileinfo;
int i;
if (mod_name == NULL)
return NULL;
module = (OutputModule *) g_malloc(sizeof(OutputModule));
if (!module)
return NULL;
if (pipe(module->pipe_speak) != 0) {
g_free(module);
return NULL;
}
module->name = (char *)g_strdup(mod_name);
module->progdir = g_strdup(mod_prog_dir);
module->configdir = g_strdup(mod_cfg_dir);
module->stderr_redirect = -1;
pthread_mutex_init(&module->read_mutex, NULL);
pthread_cond_init(&module->reply_cond, NULL);
pthread_cond_init(&module->event_cond, NULL);
module->reply = NULL;
module->event = NULL;
module->reading_message = FALSE;
module->reading_events = FALSE;
module->waiting_for_reply = FALSE;
if (module->progdir) {
module->filename = (char *)spd_get_path(mod_prog, module->progdir);
} else {
module->filename = (char *)spd_get_path(mod_prog, SpeechdOptions.user_module_dir);
if (stat(module->filename, &fileinfo) != 0) {
g_free(module->filename);
module->filename = (char *)spd_get_path(mod_prog, SpeechdOptions.module_dir);
}
}
if (mod_cfg_dir) {
module->configfilename =
(char *)spd_get_path(mod_cfgfile, mod_cfg_dir);
MSG(4, "Used mod_cfg_dir to build configfilename %s", module->configfilename);
} else {
module_conf_dir = g_strdup_printf("%s/modules",
SpeechdOptions.user_conf_dir);
module->configfilename =
(char *)spd_get_path(mod_cfgfile, module_conf_dir);
g_free(module_conf_dir);
if (stat(module->configfilename, &fileinfo) != 0) {
module_conf_dir = g_strdup_printf("%s/modules",
SpeechdOptions.conf_dir);
MSG(4, "%s does not exist, looking in %s", module->configfilename, module_conf_dir);
g_free(module->configfilename);
module->configfilename =
(char *)spd_get_path(mod_cfgfile, module_conf_dir);
g_free(module_conf_dir);
}
MSG(4, "Built configfilename %s", module->configfilename);
}
if (mod_dbgfile != NULL)
module->debugfilename = g_strdup(mod_dbgfile);
else
module->debugfilename = NULL;
if (!strcmp(mod_name, "testing")) {
module->pipe_in[1] = 1; /* redirect to stdin */
module->pipe_out[0] = 0; /* redirect to stdout */
return module;
}
if ((pipe(module->pipe_in) != 0)
|| (pipe(module->pipe_out) != 0)) {
MSG(3, "Can't open pipe! Module not loaded.");
destroy_module(module);
return NULL;
}
argv[0] = module->filename;
if (mod_cfgfile) {
argv[1] = module->configfilename;
}
/* Open the file for child stderr (logging) redirection */
if (module->debugfilename != NULL) {
module->stderr_redirect = open(module->debugfilename,
O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR);
if (module->stderr_redirect == -1)
MSG(1,
"ERROR: Opening debug file for %s failed: (error=%d) %s",
module->name, module->stderr_redirect,
strerror(errno));
} else {
module->stderr_redirect = -1;
}
MSG(2,
"Initializing output module %s with binary %s and configuration %s",
module->name, module->filename, module->configfilename);
if (module->stderr_redirect >= 0)
MSG(3, "Output module is logging to file %s",
module->debugfilename);
else
MSG(3,
"Output module is logging to standard error output (stderr)");
fr = fork();
if (fr == -1) {
printf("Can't fork, error! Module not loaded.");
destroy_module(module);
return NULL;
}
if (fr == 0) {
ret = dup2(module->pipe_in[0], 0);
close(module->pipe_in[0]);
close(module->pipe_in[1]);
ret = dup2(module->pipe_out[1], 1);
close(module->pipe_out[1]);
close(module->pipe_out[0]);
/* Redirrect stderr to the appropriate logfile */
if (module->stderr_redirect >= 0) {
ret = dup2(module->stderr_redirect, 2);
}
execvp(argv[0], argv);
MSG(1,
"Exec of module \"%s\" with config \"%s\" failed with error %d: %s",
argv[0], argv[1] ? argv[1] : "<none>", errno,
strerror(errno));
exit(1);
}
module->pid = fr;
close(module->pipe_in[0]);
close(module->pipe_out[1]);
usleep(100); /* So that the other child has at least time to fail
with the execlp */
ret = waitpid(module->pid, NULL, WNOHANG);
if (ret != 0) {
MSG(2,
"ERROR: Can't load output module %s with binary %s. Bad filename in configuration?",
module->name, module->filename);
destroy_module(module);
return NULL;
}
module->working = 1;
MSG(2, "Module %s loaded.", module->name);
/* Create a stream from the socket */
module->stream_out = fdopen(module->pipe_out[0], "r");
if (!module->stream_out)
FATAL("Can't create a stream for socket, fdopen() failed.");
/* Switch to line buffering mode */
ret = setvbuf(module->stream_out, NULL, _IONBF, 4096);
if (ret)
FATAL("Can't set line buffering, setvbuf failed.");
MSG(4, "Trying to initialize %s.", module->name);
if (output_send_data("INIT\n", module, 0) != 0) {
MSG(1, "ERROR: Something wrong with %s, can't initialize",
module->name);
output_close(module);
destroy_module(module);
return NULL;
}
reply = g_string_new("\n---------------\n");
f = fdopen(dup(module->pipe_out[0]), "r");
while (1) {
ret = getline(&rep_line, &n, f);
if (ret <= 0) {
MSG(1, "ERROR: Bad syntax from output module %s 1",
module->name);
g_string_free(reply, TRUE);
free(rep_line);
fclose(f);
destroy_module(module);
return NULL;
}
assert(rep_line != NULL);
MSG(5, "Reply from output module: %ld %s", (long) n, rep_line);
if (ret <= 4) {
MSG(1, "ERROR: Bad syntax from output module %s 2",
module->name);
g_string_free(reply, TRUE);
free(rep_line);
fclose(f);
destroy_module(module);
return NULL;
}
if (rep_line[3] != '-') {
s = rep_line[0];
free(rep_line);
break;
}
g_string_append(reply, rep_line + 4);
}
fclose(f);
g_string_append_printf(reply, "---------------\n");
if (s == '3') {
MSG(1, "ERROR: Module %s failed to initialize. Reason: %s",
module->name, reply->str);
module->working = 0;
kill(module->pid, 9);
waitpid(module->pid, NULL, WNOHANG);
destroy_module(module);
g_string_free(reply, TRUE);
return NULL;
}
if (s == '2')
MSG(2, "Module %s started successfully with message: %s",
module->name, reply->str);
g_string_free(reply, 1);
if (SpeechdOptions.debug) {
MSG(4, "Switching debugging on for output module %s",
module->name);
output_module_debug(module);
}
/* Initialize audio settings */
ret = output_send_audio_settings(module);
if (ret != 0) {
MSG(1,
"ERROR: Can't initialize audio in output module, see reason above.");
module->working = 0;
kill(module->pid, 9);
waitpid(module->pid, NULL, WNOHANG);
destroy_module(module);
return NULL;
}
/* Send log level configuration setting */
ret = output_send_loglevel_setting(module);
if (ret != 0) {
MSG(1,
"ERROR: Can't set the log level inin the output module.");
module->working = 0;
kill(module->pid, 9);
waitpid(module->pid, NULL, WNOHANG);
destroy_module(module);
return NULL;
}
/* Try to get the list of voices */
SPDVoice **voices = output_get_voices(module, NULL, NULL);
if (!voices) {
/* No list of voices, that would surprise clients, let's give up
* on this module */
MSG(1,
"ERROR: Can't get a list of voices from the output module.");
module->working = 0;
kill(module->pid, 9);
waitpid(module->pid, NULL, WNOHANG);
destroy_module(module);
return NULL;
}
for (i = 0; voices[i]; i++) {
g_free(voices[i]->name);
g_free(voices[i]->language);
g_free(voices[i]->variant);
g_free(voices[i]);
}
g_free(voices);
return module;
}
int unload_output_module(OutputModule * module)
{
assert(module != NULL);
MSG(3, "Unloading module name=%s", module->name);
output_close(module);
close(module->pipe_in[1]);
close(module->pipe_out[0]);
destroy_module(module);
return 0;
}
int reload_output_module(OutputModule * old_module)
{
OutputModule *new_module;
int pos;
assert(old_module != NULL);
assert(old_module->name != NULL);
if (old_module->working)
return 0;
MSG(3, "Reloading output module %s", old_module->name);
output_close(old_module);
close(old_module->pipe_in[1]);
close(old_module->pipe_out[0]);
new_module = load_output_module(old_module->name, old_module->filename,
old_module->configfilename,
old_module->debugfilename,
old_module->progdir,
old_module->configdir);
if (new_module == NULL) {
MSG(3, "Can't load module %s while reloading modules.",
old_module->name);
return -1;
}
pos = g_list_index(output_modules, old_module);
output_modules = g_list_remove(output_modules, old_module);
output_modules = g_list_insert(output_modules, new_module, pos);
destroy_module(old_module);
return 0;
}
int output_module_debug(OutputModule * module)
{
char *new_log_path;
assert(module != NULL);
assert(module->name != NULL);
if (!module->working)
return -1;
MSG(4, "Output module debug logging for %s into %s", module->name,
SpeechdOptions.debug_destination);
new_log_path = g_strdup_printf("%s/%s.log",
SpeechdOptions.debug_destination,
module->name);
output_send_debug(module, 1, new_log_path);
g_free(new_log_path);
return 0;
}
int output_module_nodebug(OutputModule * module)
{
assert(module != NULL);
assert(module->name != NULL);
if (!module->working)
return -1;
MSG(4, "Output module debug logging off for %s", module->name);
output_send_debug(module, 0, NULL);
return 0;
}
static GList *requested_modules = NULL;
/*
* module_already_requested: determine whether we have already received
* a load request for a given module.
* Parameters:
* module_name: the name of the module.
* module_cmd: name of the binary associated with this module.
* module_cfgfile: the name of the module's configuration file.
*
* Returns:
* TRUE if the module was previously requested, FALSE otherwise.
*
* A module is previously requested if:
* 1. module_name is the same as some other module in the list of load
* requests. We select a given output module using its name, so names
* must be unique.
* 2. The name of the executable and the name of the configuration file
* are exactly the same as those of another module.
* It is acceptable to load the same module binary multiple times, where
* each instance uses a different configuration file. In fact, this is
* how sd_generic works. The instances are differentiated by module_name.
* However, it is not reasonable to load multiple instances, all of which
* use the same (binary, config file) pair.
* These two rules should cover all cases adequately.
*
*/
gboolean
module_already_requested(char *module_name, char *module_cmd,
char *module_cfgfile)
{
GList *lp;
for (lp = requested_modules; lp != NULL; lp = lp->next) {
char **params = lp->data;
if (strcmp(module_name, params[0]) == 0 ||
(strcmp(module_cmd, params[1]) == 0 &&
strcmp(module_cfgfile, params[2]) == 0))
return TRUE;
}
return FALSE;
}
/*
* module_add_load_request - request that a module be loaded.
* In other words, add it to the list of modules which will be loaded
* by module_load_requested_modules.
* Returns: nothing.
* Parameters:
* module_name: the name of the module.
* module_cmd: name of the binary associated with this module.
* module_cfgfile: the name of the module's configuration file.
* module_dbgfile: name of the file to which the module writes logging
* and debugging information.
* Note that all parameters are dynamically-allocated strings (char *),
* and the caller relinquishes ownership of them when calling this function.
*/
void
module_add_load_request(char *module_name, char *module_cmd,
char *module_cfgfile, char *module_dbgfile,
char *module_cmd_dir, char *module_cfg_dir)
{
char **module_params = NULL;
if (module_already_requested(module_name, module_cmd, module_cfgfile)) {
MSG(1,
"Load request for module %s with command %s and configuration file %s was previously received; ignoring the second one.",
module_name, module_cmd, module_cfgfile);
g_free(module_name);
g_free(module_cmd);
g_free(module_cfgfile);
g_free(module_dbgfile);
g_free(module_cmd_dir);
g_free(module_cfg_dir);
return;
}
module_params = g_malloc(6 * sizeof(char *));
module_params[0] = module_name;
module_params[1] = module_cmd;
module_params[2] = module_cfgfile;
module_params[3] = module_dbgfile;
module_params[4] = module_cmd_dir;
module_params[5] = module_cfg_dir;
requested_modules = g_list_append(requested_modules, module_params);
MSG(5, "Module name=%s being inserted into requested_modules list, with '%s' '%s' '%s' '%s' '%s'",
module_params[0],
module_params[1],
module_params[2],
module_params[3],
module_params[4],
module_params[5]);
}
/*
* module_load_requested_modules: load all modules requested by calls
* to module_add_load_request.
* Returns: nothing.
* Parameters: none.
*/
void module_load_requested_modules(void)
{
while (NULL != requested_modules) {
OutputModule *new_module;
char **module_params = requested_modules->data;
new_module =
load_output_module(module_params[0], module_params[1],
module_params[2], module_params[3],
module_params[4], module_params[5]);
if (new_module != NULL)
output_modules =
g_list_append(output_modules, new_module);
g_free(module_params[0]);
g_free(module_params[1]);
g_free(module_params[2]);
g_free(module_params[3]);
g_free(module_params[4]);
g_free(module_params[5]);
g_free(module_params);
requested_modules =
g_list_delete_link(requested_modules, requested_modules);
}
if (output_modules && !GlobalFDSet.output_module) {
OutputModule *first_module = output_modules->data;
GlobalFDSet.output_module = first_module->name;
}
}
/*
* module_number_of_requested_modules: return the current number of requested modules.
* Returns: number of modules.
* Parameters: none.
*/
guint module_number_of_requested_modules(void)
{
if (requested_modules == NULL) {
return 0;
}
return g_list_length(requested_modules);
}