blob: 8c92939db68fc0092bea803c56b3b9c4c3c92f44 [file] [log] [blame]
/* upstart
*
* conf.c - configuration management
*
* Copyright © 2009 Canonical Ltd.
* Author: Scott James Remnant <scott@netsplit.com>.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2, as
* published by the Free Software Foundation.
*
* This program 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, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <libgen.h>
#include <string.h>
#include <unistd.h>
#include <nih/macros.h>
#include <nih/alloc.h>
#include <nih/list.h>
#include <nih/hash.h>
#include <nih/string.h>
#include <nih/io.h>
#include <nih/file.h>
#include <nih/watch.h>
#include <nih/logging.h>
#include <nih/error.h>
#include <nih/errors.h>
#include "parse_job.h"
#include "parse_conf.h"
#include "conf.h"
#include "errors.h"
/* Prototypes for static functions */
static int conf_source_reload_file (ConfSource *source)
__attribute__ ((warn_unused_result));
static int conf_source_reload_dir (ConfSource *source)
__attribute__ ((warn_unused_result));
static int conf_file_filter (ConfSource *source, const char *path,
int is_dir);
static int conf_dir_filter (ConfSource *source, const char *path,
int is_dir);
static void conf_create_modify_handler (ConfSource *source, NihWatch *watch,
const char *path,
struct stat *statbuf);
static void conf_delete_handler (ConfSource *source, NihWatch *watch,
const char *path);
static int conf_file_visitor (ConfSource *source,
const char *dirname, const char *path,
struct stat *statbuf)
__attribute__ ((warn_unused_result));
static int conf_reload_path (ConfSource *source, const char *path)
__attribute__ ((warn_unused_result));
/**
* conf_sources:
*
* This list holds the list of known sources of configuration; each item
* is a ConfSource structure. The order of this list dictates the priority
* of the sources, with the first one having the highest priority.
**/
NihList *conf_sources = NULL;
/**
* conf_init:
*
* Initialise the conf_sources list.
**/
void
conf_init (void)
{
if (! conf_sources)
conf_sources = NIH_MUST (nih_list_new (NULL));
}
/**
* conf_source_new:
* @parent: parent of new block,
* @path: path to source,
* @type: type of source.
*
* Allocates and returns a new ConfSource structure for the given @path;
* @type indicates whether this @path is a file or directory and what type
* of files are within the directory.
*
* The returned structure is automatically added to the conf_sources list.
*
* Configuration is not parsed immediately, instead you must call
* conf_source_reload() on this source to set up any watches and load the
* current configuration. Normally you would set up all of the sources and
* then call conf_reload() which will load them all.
*
* If @parent is not NULL, it should be a pointer to another allocated
* block which will be used as the parent for this block. When @parent
* is freed, the returned block will be freed too.
*
* Returns: newly allocated ConfSource structure or NULL if
* insufficient memory.
**/
ConfSource *
conf_source_new (const void *parent,
const char *path,
ConfSourceType type)
{
ConfSource *source;
nih_assert (path != NULL);
conf_init ();
source = nih_new (parent, ConfSource);
if (! source)
return NULL;
nih_list_init (&source->entry);
source->path = nih_strdup (source, path);
if (! source->path) {
nih_free (source);
return NULL;
}
source->type = type;
source->watch = NULL;
source->flag = FALSE;
source->files = nih_hash_string_new (source, 0);
if (! source->files) {
nih_free (source);
return NULL;
}
nih_alloc_set_destructor (source, nih_list_destroy);
nih_list_add (conf_sources, &source->entry);
return source;
}
/**
* conf_file_new:
* @source: configuration source,
* @path: path to file.
*
* Allocates and returns a new ConfFile structure for the given @source,
* with @path indicating which file it is.
*
* The returned structure is automatically placed in the @source's files hash
* and the flag of the returned ConfFile will be set to that of the @source.
*
* Returns: newly allocated ConfFile structure or NULL if insufficient memory.
**/
ConfFile *
conf_file_new (ConfSource *source,
const char *path)
{
ConfFile *file;
nih_assert (source != NULL);
nih_assert (path != NULL);
file = nih_new (source, ConfFile);
if (! file)
return NULL;
nih_list_init (&file->entry);
file->path = nih_strdup (file, path);
if (! file->path) {
nih_free (file);
return NULL;
}
file->source = source;
file->flag = source->flag;
file->data = NULL;
nih_alloc_set_destructor (file, conf_file_destroy);
nih_hash_add (source->files, &file->entry);
return file;
}
/**
* conf_reload:
*
* Reloads all configuration sources.
*
* Watches on new configuration sources are established so that future
* changes will be automatically detected with inotify. Then for both
* new and existing sources, the current state is parsed.
*
* Any errors are logged through the usual mechanism, and not returned,
* since some configuration may have been parsed; and it's possible to
* parse no configuration without error.
**/
void
conf_reload (void)
{
conf_init ();
NIH_LIST_FOREACH (conf_sources, iter) {
ConfSource *source = (ConfSource *)iter;
if (conf_source_reload (source) < 0) {
NihError *err;
err = nih_error_get ();
if (err->number != ENOENT)
nih_error ("%s: %s: %s", source->path,
_("Unable to load configuration"),
err->message);
nih_free (err);
}
}
}
/**
* conf_source_reload:
* @source: configuration source to reload.
*
* Reloads the given configuration @source.
*
* If not already established, an inotify watch is created so that future
* changes to this source are automatically detected and parsed. For files,
* this watch is actually on the parent directory, since we need to watch
* out for editors that rename over the top, etc.
*
* We then parse the current state of the source. The flag member is
* toggled first, and this is propogated to all new and modified files and
* items that we find as a result of parsing. Once done, we scan for
* anything with the wrong flag, and delete them.
*
* Returns: zero on success, negative value on raised error.
**/
int
conf_source_reload (ConfSource *source)
{
NihList deleted;
int ret;
nih_assert (source != NULL);
nih_info (_("Loading configuration from %s"), source->path);
/* Toggle the flag so we can detect deleted files and items. */
source->flag = (! source->flag);
/* Reload the source itself. */
switch (source->type) {
case CONF_FILE:
ret = conf_source_reload_file (source);
break;
case CONF_DIR:
case CONF_JOB_DIR:
ret = conf_source_reload_dir (source);
break;
default:
nih_assert_not_reached ();
}
/* Scan for files that have been deleted since the last time we
* reloaded; these are simple to detect, as they will have the wrong
* flag.
*
* We take them out of the files list and then we can delete the
* attached jobs and free the file. We can't just do this from
* the one loop because to delete the jobs, we need to be able
* to iterate the sources and files.
*/
nih_list_init (&deleted);
NIH_HASH_FOREACH_SAFE (source->files, iter) {
ConfFile *file = (ConfFile *)iter;
if (file->flag != source->flag)
nih_list_add (&deleted, &file->entry);
}
NIH_LIST_FOREACH_SAFE (&deleted, iter) {
ConfFile *file = (ConfFile *)iter;
nih_info (_("Handling deletion of %s"), file->path);
nih_unref (file, source);
}
return ret;
}
/**
* conf_source_reload_file:
* @source: configuration source to reload.
*
* Reloads the configuration file specified by @source.
*
* If not already established, an inotify watch is created on the parent
* directory so that future changes to the file are automatically detected
* and parsed. It is the parent directory because we need to watch out for
* editors that rename over the top, etc.
*
* Returns: zero on success, negative value on raised error.
**/
static int
conf_source_reload_file (ConfSource *source)
{
NihError *err = NULL;
nih_assert (source != NULL);
nih_assert (source->type == CONF_FILE);
if (! source->watch) {
nih_local char *dpath = NULL;
char *dname;
dpath = NIH_MUST (nih_strdup (NULL, source->path));
dname = dirname (dpath);
source->watch = nih_watch_new (source, dname, FALSE, FALSE,
(NihFileFilter)conf_file_filter,
(NihCreateHandler)conf_create_modify_handler,
(NihModifyHandler)conf_create_modify_handler,
(NihDeleteHandler)conf_delete_handler,
source);
/* If successful mark the file descriptor close-on-exec,
* otherwise stash the error for comparison with a later
* failure to parse the file.
*/
if (source->watch) {
nih_io_set_cloexec (source->watch->fd);
} else {
err = nih_error_steal ();
}
}
/* Parse the file itself. If this fails, then we can discard the
* inotify error, since this one will be better.
*/
if (conf_reload_path (source, source->path) < 0) {
if (err)
nih_free (err);
return -1;
}
/* We were able to parse the file, but were not able to set up an
* inotify watch. This isn't critical, so we just warn about it,
* unless this is simply that inotify isn't supported, in which case
* we do nothing.
*/
if (err) {
if (err->number != ENOSYS)
nih_warn ("%s: %s: %s", source->path,
_("Unable to watch configuration file"),
err->message);
nih_free (err);
}
return 0;
}
/**
* conf_source_reload_dir:
* @source: configuration source to reload.
*
* Reloads the configuration directory specified by @source.
*
* If not already established, an inotify watch is created on the directory
* so that future changes to the structure or files within it are
* automatically parsed. This has the side-effect of parsing the current
* tree.
*
* Otherwise we walk the tree ourselves and parse all files that we find,
* propogating the value of the flag member to all files so that deletion
* can be detected by the calling function.
*
* Returns: zero on success, negative value on raised error.
**/
static int
conf_source_reload_dir (ConfSource *source)
{
NihError *err = NULL;
nih_assert (source != NULL);
nih_assert (source->type != CONF_FILE);
if (! source->watch) {
source->watch = nih_watch_new (source, source->path,
TRUE, TRUE,
(NihFileFilter)conf_dir_filter,
(NihCreateHandler)conf_create_modify_handler,
(NihModifyHandler)conf_create_modify_handler,
(NihDeleteHandler)conf_delete_handler,
source);
/* If successful, the directory tree will have been walked
* already; so just mark the file descriptor close-on-exec
* and return; otherwise we'll try and walk ourselves, so
* stash the error for comparison.
*/
if (source->watch) {
nih_io_set_cloexec (source->watch->fd);
return 0;
} else {
err = nih_error_steal ();
}
}
/* We're either performing a mandatory reload, or we failed to set
* up an inotify watch; walk the directory tree the old fashioned
* way. If this fails too, then we can discard the inotify error
* since this one will be better.
*/
if (nih_dir_walk (source->path, (NihFileFilter)conf_dir_filter,
(NihFileVisitor)conf_file_visitor, NULL,
source) < 0) {
if (err)
nih_free (err);
return -1;
}
/* We were able to walk the directory, but were not able to set up
* an inotify watch. This isn't critical, so we just warn about it,
* unless this is simply that inotify isn't supported, in which case
* we do nothing.
*/
if (err) {
if (err->number != ENOSYS)
nih_warn ("%s: %s: %s", source->path,
_("Unable to watch configuration directory"),
err->message);
nih_free (err);
}
return 0;
}
/**
* conf_file_filter:
* @source: configuration source,
* @path: path to check,
* @is_dir: TRUE if @path is a directory.
*
* When we watch the parent directory of a file for changes, we receive
* notification about all changes to that directory. We only care about
* those that affect the path in @source, and the path that we're watching,
* so we use this function to filter out all others.
*
* Returns: FALSE if @path matches @source, TRUE otherwise.
**/
static int
conf_file_filter (ConfSource *source,
const char *path,
int is_dir)
{
nih_assert (source != NULL);
nih_assert (path != NULL);
if (! strcmp (source->path, path))
return FALSE;
if (! strcmp (source->watch->path, path))
return FALSE;
return TRUE;
}
/**
* conf_dir_filter:
* @source: configuration source,
* @path: path to check,
* @is_dir: TRUE of @path is a directory.
*
* This is the file filter used for the jobs directory, we only care
* about paths with the ".conf" extension. Directories that
* match the nih_file_ignore() function are also ignored.
*
* Returns: FALSE if @path ends in ".conf", or is the original source,
* TRUE otherwise.
**/
static int
conf_dir_filter (ConfSource *source,
const char *path,
int is_dir)
{
char *ptr;
nih_assert (source != NULL);
nih_assert (path != NULL);
if (! strcmp (source->path, path))
return FALSE;
if (is_dir)
return nih_file_ignore (NULL, path);
ptr = strrchr (path, '.');
if (ptr && (! strcmp (ptr, ".conf")))
return FALSE;
return TRUE;
}
/**
* conf_create_modify_handler:
* @source: configuration source,
* @watch: NihWatch for source,
* @path: full path to modified file,
* @statbuf: stat of @path.
*
* This function will be called whenever a file is created in a directory
* that we're watching, moved into the directory we're watching, or is
* modified. This works for both directory and file sources, since the
* watch for the latter is on the parent and filtered to only return the
* path that we're interested in.
*
* After checking that it was a regular file that was changed, we reload it;
* we expect this to fail sometimes since the file may be only partially
* written.
**/
static void
conf_create_modify_handler (ConfSource *source,
NihWatch *watch,
const char *path,
struct stat *statbuf)
{
nih_assert (source != NULL);
nih_assert (watch != NULL);
nih_assert (path != NULL);
nih_assert (statbuf != NULL);
if (! S_ISREG (statbuf->st_mode))
return;
if (conf_reload_path (source, path) < 0) {
NihError *err;
err = nih_error_get ();
nih_error ("%s: %s: %s", path,
_("Error while loading configuration file"),
err->message);
nih_free (err);
}
}
/**
* conf_delete_handler:
* @source: configuration source,
* @watch: NihWatch for source,
* @path: full path to deleted file.
*
* This function will be called whenever a file is removed or moved out
* of a directory that we're watching. This works for both directory and
* file sources, since the watch for the latter is on the parent and
* filtered to only return the path that we're interested in.
*
* We lookup the file in our hash table, and if we can find it, perform
* the usual deletion of it.
**/
static void
conf_delete_handler (ConfSource *source,
NihWatch *watch,
const char *path)
{
ConfFile *file;
nih_assert (source != NULL);
nih_assert (watch != NULL);
nih_assert (path != NULL);
/* Lookup the file in the source. If we haven't parsed it, this
* could actually mean that it was the top-level directory itself
* that was deleted, in which case we free the watch, otherwise
* it's probably a directory or something, so just ignore it.
*/
file = (ConfFile *)nih_hash_lookup (source->files, path);
if (! file) {
if (! strcmp (watch->path, path)) {
nih_warn ("%s: %s", source->path,
_("Configuration directory deleted"));
nih_unref (source->watch, source);
source->watch = NULL;
}
return;
}
nih_unref (file, source);
}
/**
* conf_file_visitor:
* @source: configuration source,
* @dirname: top-level directory being walked,
* @path: path found in directory,
* @statbuf: stat of @path.
*
* This function is called when walking a directory tree for each file
* found within it.
*
* After checking that it's a regular file, we reload it.
*
* Returns: always zero.
**/
static int
conf_file_visitor (ConfSource *source,
const char *dirname,
const char *path,
struct stat *statbuf)
{
nih_assert (source != NULL);
nih_assert (dirname != NULL);
nih_assert (path != NULL);
nih_assert (statbuf != NULL);
if (! S_ISREG (statbuf->st_mode))
return 0;
if (conf_reload_path (source, path) < 0) {
NihError *err;
err = nih_error_get ();
nih_error ("%s: %s: %s", path,
_("Error while loading configuration file"),
err->message);
nih_free (err);
}
return 0;
}
/**
* conf_reload_path:
* @source: configuration source,
* @path: path of file to be reloaded.
*
* This function is used to parse the file at @path in the context of the
* given configuration @source. Necessary ConfFile structures are allocated
* and attached to @source as appropriate. CONF_FILE sources always have
* a single ConfFile when the file exists.
*
* If the file has been parsed before, then the existing item is deleted and
* freed if the file fails to load, or after the new item has been parsed.
* Items are not reused between reloads.
*
* Physical errors are returned, parse errors are not.
*
* Returns: zero on success, negative value on raised error.
**/
static int
conf_reload_path (ConfSource *source,
const char *path)
{
ConfFile *file;
nih_local char *buf = NULL;
const char *start, *end;
nih_local char *name = NULL;
size_t len, pos, lineno;
NihError *err = NULL;
nih_assert (source != NULL);
nih_assert (path != NULL);
/* Look up the old file in memory, and then free it. In cases
* of failure, we discard it anyway, so there's no particular reason
* to keep it around anymore.
*/
file = (ConfFile *)nih_hash_lookup (source->files, path);
if (file)
nih_unref (file, source);
/* Read the file into memory for parsing, if this fails we don't
* bother creating a new ConfFile structure for it and bail out
* now.
*/
buf = nih_file_read (NULL, path, &len);
if (! buf)
return -1;
/* Parse the file, storing the item in a new ConfFile structure. */
file = NIH_MUST (conf_file_new (source, path));
pos = 0;
lineno = 1;
switch (source->type) {
case CONF_FILE:
case CONF_DIR:
/* Simple file of options; usually no item attached to it. */
nih_debug ("Loading configuration from %s", path);
if (parse_conf (file, buf, len, &pos, &lineno) < 0)
err = nih_error_get ();
break;
case CONF_JOB_DIR:
/* Construct the job name by taking the path and removing
* the directory name from the front and the extension
* from the end.
*/
start = path;
if (! strncmp (start, source->path, strlen (source->path)))
start += strlen (source->path);
while (*start == '/')
start++;
end = strrchr (start, '.');
if (end && (! strcmp (end, ".conf"))) {
name = NIH_MUST (nih_strndup (NULL, start, end - start));
} else {
name = NIH_MUST (nih_strdup (NULL, start));
}
/* Create a new job item and parse the buffer to produce
* the job definition.
*/
nih_debug ("Loading %s from %s", name, path);
file->job = parse_job (NULL, name, buf, len, &pos, &lineno);
if (file->job) {
job_class_consider (file->job);
} else {
err = nih_error_get ();
}
break;
default:
nih_assert_not_reached ();
}
/* Deal with any parsing errors that occurred; we don't consider
* these to be hard failures, which means we can warn about them
* here and give the path and line number along with the warning.
*/
if (err) {
switch (err->number) {
case NIH_CONFIG_EXPECTED_TOKEN:
case NIH_CONFIG_UNEXPECTED_TOKEN:
case NIH_CONFIG_TRAILING_SLASH:
case NIH_CONFIG_UNTERMINATED_QUOTE:
case NIH_CONFIG_UNTERMINATED_BLOCK:
case NIH_CONFIG_UNKNOWN_STANZA:
case PARSE_ILLEGAL_INTERVAL:
case PARSE_ILLEGAL_EXIT:
case PARSE_ILLEGAL_UMASK:
case PARSE_ILLEGAL_NICE:
case PARSE_ILLEGAL_OOM:
case PARSE_ILLEGAL_LIMIT:
case PARSE_EXPECTED_EVENT:
case PARSE_EXPECTED_OPERATOR:
case PARSE_EXPECTED_VARIABLE:
case PARSE_MISMATCHED_PARENS:
nih_error ("%s:%zi: %s", path, lineno, err->message);
nih_free (err);
err = NULL;
break;
}
}
/* If we had any unknown error from parsing the file, raise it again
* and return an error condition.
*/
if (err)
return -1;
return 0;
}
/**
* conf_file_destroy:
* @file: configuration file to be destroyed.
*
* Handles the replacement and deletion of a configuration file, ensuring
* that @file is removed from the containing linked list and that the item
* attached to it is destroyed if not currently in use.
*
* Normally used or called from an nih_alloc() destructor so that the list
* item is automatically removed from its containing list when freed.
*
* Returns: zero.
**/
int
conf_file_destroy (ConfFile *file)
{
nih_assert (file != NULL);
nih_list_destroy (&file->entry);
switch (file->source->type) {
case CONF_FILE:
case CONF_DIR:
break;
case CONF_JOB_DIR:
if (! file->job)
break;
/* Mark the job to be deleted when it stops, in case
* it cannot be deleted here.
*/
file->job->deleted = TRUE;
/* Check whether the job is the current one with that name;
* if it is, try and replace it. If it wasn't the current
* job, or isn't after replacement, we can free it now.
*/
if (job_class_reconsider (file->job)) {
nih_debug ("Destroyed unused job %s", file->job->name);
nih_free (file->job);
}
break;
default:
nih_assert_not_reached ();
}
return 0;
}
/**
* conf_select_job:
* @name: name of job class to locate.
*
* Select the best available class of a job named @name from the registered
* configuration sources.
*
* Returns: Best available job class or NULL if none available.
**/
JobClass *
conf_select_job (const char *name)
{
nih_assert (name != NULL);
conf_init ();
NIH_LIST_FOREACH (conf_sources, iter) {
ConfSource *source = (ConfSource *)iter;
if (source->type != CONF_JOB_DIR)
continue;
NIH_HASH_FOREACH (source->files, file_iter) {
ConfFile *file = (ConfFile *)file_iter;
if (! file->job)
continue;
if (! strcmp (file->job->name, name))
return file->job;
}
}
return NULL;
}