]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
config, lib-master: Send configuration as a seekable file descriptor
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Tue, 29 Nov 2022 23:58:06 +0000 (01:58 +0200)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Fri, 27 Jan 2023 13:01:47 +0000 (13:01 +0000)
This allows lib-master to re-read the configuration using the same open fd.

The binary configuration is written to an unlinked file. The file's fd is
sent to config clients, so the binary config stays allocated until all
config clients have closed the fd.

src/config/config-connection.c
src/config/config-dump-full.c
src/config/config-dump-full.h
src/config/config-request.c
src/config/config-request.h
src/config/doveconf.c
src/lib-master/master-service-settings.c
src/lib-master/master-service-settings.h
src/master/main.c

index 9a7fa68ee38f1848f5b0e212a88bafa0d98d60cf..e2225b6b0dc2b8a27ead071333964cbcf002cce4 100644 (file)
@@ -5,6 +5,7 @@
 #include "llist.h"
 #include "istream.h"
 #include "ostream.h"
+#include "ostream-unix.h"
 #include "strescape.h"
 #include "settings-parser.h"
 #include "master-service.h"
@@ -34,6 +35,7 @@ struct config_connection {
 };
 
 static struct config_connection *config_connections = NULL;
+static int global_config_fd = -1;
 
 static const char *const *
 config_connection_next_line(struct config_connection *conn)
@@ -52,10 +54,31 @@ static int config_connection_request(struct config_connection *conn,
 {
        const char *import_environment;
 
-       if (config_dump_full(conn->output, &import_environment) < 0) {
-               config_connection_destroy(conn);
-               return -1;
+       if (null_strcmp(*args, "reload") == 0) {
+               const char *path, *error;
+
+               path = master_service_get_config_path(master_service);
+               if (config_parse_file(path, TRUE, NULL, &error) <= 0) {
+                       o_stream_nsend_str(conn->output,
+                               t_strconcat("-", error, "\n", NULL));
+                       return 0;
+               }
+               i_close_fd(&global_config_fd);
+       }
+
+       if (global_config_fd == -1) {
+               int fd = config_dump_full(CONFIG_DUMP_FULL_DEST_RUNDIR,
+                                         &import_environment);
+               if (fd == -1) {
+                       o_stream_nsend_str(conn->output, "-Failed\n");
+                       return 0;
+               }
+               global_config_fd = fd;
        }
+       if (!o_stream_unix_write_fd(conn->output, global_config_fd))
+               i_unreached();
+
+       o_stream_nsend_str(conn->output, "+\n");
        return 0;
 }
 
@@ -105,7 +128,7 @@ struct config_connection *config_connection_create(int fd)
        conn = i_new(struct config_connection, 1);
        conn->fd = fd;
        conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE);
-       conn->output = o_stream_create_fd(fd, SIZE_MAX);
+       conn->output = o_stream_create_unix(fd, SIZE_MAX);
        o_stream_set_no_error_handling(conn->output, TRUE);
        conn->io = io_add(fd, IO_READ, config_connection_input, conn);
        DLLIST_PREPEND(&config_connections, conn);
@@ -130,4 +153,5 @@ void config_connections_destroy_all(void)
 {
        while (config_connections != NULL)
                config_connection_destroy(config_connections);
+       i_close_fd(&global_config_fd);
 }
index af305c80505a6987d0a8f7fa4d1d53fd9bb2fad5..aef944b8c6ecaefa8f516c16dc3fb4f51fb99d6a 100644 (file)
@@ -3,6 +3,7 @@
 #include "lib.h"
 #include "str.h"
 #include "strescape.h"
+#include "safe-mkstemp.h"
 #include "ostream.h"
 #include "config-parser.h"
 #include "config-request.h"
@@ -70,7 +71,7 @@ config_dump_full_write_filter(struct ostream *output,
        o_stream_nsend(output, str_data(str), str_len(str));
 }
 
-static int
+static bool
 config_dump_full_sections(struct ostream *output, unsigned int section_idx)
 {
        struct config_filter_parser *const *filters;
@@ -98,22 +99,20 @@ config_dump_full_sections(struct ostream *output, unsigned int section_idx)
                config_export_parsers(export_ctx, (*filters)->parsers);
                ret = config_export_finish(&export_ctx, &section_idx);
        } T_END;
-       return 0;
+       return ret == 0;
 }
 
-int config_dump_full(struct ostream *output, const char **import_environment_r)
+int config_dump_full(enum config_dump_full_dest dest,
+                    const char **import_environment_r)
 {
        struct config_export_context *export_ctx;
        struct config_filter empty_filter;
        enum config_dump_flags flags;
        unsigned int section_idx = 0;
-       int ret;
-
-       i_zero(&empty_filter);
-       o_stream_cork(output);
+       int fd = -1;
+       bool failed = FALSE;
 
        struct dump_context dump_ctx = {
-               .output = output,
                .delayed_output = str_new(default_pool, 256),
        };
 
@@ -123,12 +122,51 @@ int config_dump_full(struct ostream *output, const char **import_environment_r)
        i_zero(&empty_filter);
        config_export_by_filter(export_ctx, &empty_filter);
 
+       string_t *path = t_str_new(128);
+       const char *final_path = NULL;
+       switch (dest) {
+       case CONFIG_DUMP_FULL_DEST_RUNDIR: {
+               const char *base_dir = config_export_get_base_dir(export_ctx);
+               final_path = t_strdup_printf("%s/dovecot.conf.binary", base_dir);
+               str_append(path, final_path);
+               str_append_c(path, '.');
+               break;
+       }
+       case CONFIG_DUMP_FULL_DEST_TEMPDIR:
+               /* create an unlinked file to /tmp */
+               str_append(path, "/tmp/doveconf.");
+               break;
+       case CONFIG_DUMP_FULL_DEST_STDOUT:
+               dump_ctx.output = o_stream_create_fd(STDOUT_FILENO, IO_BLOCK_SIZE);
+               o_stream_set_name(dump_ctx.output, "<stdout>");
+               fd = 0;
+               break;
+       }
+
+       if (dump_ctx.output == NULL) {
+               fd = safe_mkstemp(path, 0700, (uid_t)-1, (gid_t)-1);
+               if (fd == -1) {
+                       i_error("safe_mkstemp(%s) failed: %m", str_c(path));
+                       (void)config_export_finish(&export_ctx, &section_idx);
+                       str_free(&dump_ctx.delayed_output);
+                       return -1;
+               }
+               if (dest == CONFIG_DUMP_FULL_DEST_TEMPDIR)
+                       i_unlink(str_c(path));
+               dump_ctx.output = o_stream_create_fd(fd, IO_BLOCK_SIZE);
+               o_stream_set_name(dump_ctx.output, str_c(path));
+       }
+       struct ostream *output = dump_ctx.output;
+
+       i_zero(&empty_filter);
+       o_stream_cork(output);
+
        *import_environment_r =
                t_strdup(config_export_get_import_environment(export_ctx));
        if (config_export_finish(&export_ctx, &section_idx) < 0)
-               ret = -1;
+               failed = TRUE;
        else
-               ret = config_dump_full_sections(output, section_idx);
+               failed = !config_dump_full_sections(output, section_idx);
 
        if (dump_ctx.delayed_output != NULL &&
            str_len(dump_ctx.delayed_output) > 0) {
@@ -139,6 +177,30 @@ int config_dump_full(struct ostream *output, const char **import_environment_r)
        str_free(&dump_ctx.delayed_output);
 
        o_stream_nsend_str(output, "\n");
-       o_stream_uncork(output);
-       return ret;
+       if (o_stream_finish(output) < 0) {
+               i_error("write(%s) failed: %s",
+                       o_stream_get_name(output), o_stream_get_error(output));
+               failed = TRUE;
+       }
+
+       if (final_path != NULL && !failed) {
+               if (rename(str_c(path), final_path) < 0) {
+                       i_error("rename(%s, %s) failed: %m",
+                               str_c(path), final_path);
+                       /* the fd is still readable, so don't return failure */
+               }
+       }
+       if (!failed && dest != CONFIG_DUMP_FULL_DEST_STDOUT &&
+           lseek(fd, 0, SEEK_SET) < 0) {
+               i_error("lseek(%s, 0) failed: %m", o_stream_get_name(output));
+               failed = TRUE;
+       }
+       if (failed) {
+               if (dest == CONFIG_DUMP_FULL_DEST_STDOUT)
+                       fd = -1;
+               else
+                       i_close_fd(&fd);
+       }
+       o_stream_destroy(&output);
+       return fd;
 }
index b10ef8bdeb310cb850d66e92aa3d5a1faee6e2a8..56a364a3e37f78cc227d3cfdcbabdc8c896478a3 100644 (file)
@@ -1,6 +1,13 @@
 #ifndef CONFIG_DUMP_FULL
 #define CONFIG_DUMP_FULL
 
-int config_dump_full(struct ostream *output, const char **import_environment_r);
+enum config_dump_full_dest {
+       CONFIG_DUMP_FULL_DEST_RUNDIR,
+       CONFIG_DUMP_FULL_DEST_TEMPDIR,
+       CONFIG_DUMP_FULL_DEST_STDOUT,
+};
+
+int config_dump_full(enum config_dump_full_dest dest,
+                    const char **import_environment_r);
 
 #endif
index 9e1019dd183e1b03f58938f8211e58b0b639944d..4aef57014f928b14b7ba71823794555786be65bf 100644 (file)
@@ -442,6 +442,23 @@ config_export_get_import_environment(struct config_export_context *ctx)
        i_unreached();
 }
 
+const char *config_export_get_base_dir(struct config_export_context *ctx)
+{
+       enum setting_type stype;
+       unsigned int i;
+
+       for (i = 0; ctx->parsers[i].root != NULL; i++) {
+               if (ctx->parsers[i].root == &master_service_setting_parser_info) {
+                       const char *const *value =
+                               settings_parse_get_value(ctx->parsers[i].parser,
+                                       "base_dir", &stype);
+                       i_assert(value != NULL);
+                       return *value;
+               }
+       }
+       i_unreached();
+}
+
 static void config_export_free(struct config_export_context *ctx)
 {
        if (ctx->dup_parsers != NULL)
index cda6fd90735c29c836779e4ead9ac0952fb5f3b9..a7fac72daebd4501d6f73d4d93782dd98b2cfe88 100644 (file)
@@ -52,6 +52,7 @@ void config_export_get_output(struct config_export_context *ctx,
                              struct master_service_settings_output *output_r);
 const char *
 config_export_get_import_environment(struct config_export_context *ctx);
+const char *config_export_get_base_dir(struct config_export_context *ctx);
 int config_export_finish(struct config_export_context **ctx,
                         unsigned int *section_idx);
 
index 3a3d3be596cbf76f4dc4f8087c11d417c90141b9..dd75b8f2518f0b470e699d746b855baf4f1913fa 100644 (file)
@@ -11,7 +11,6 @@
 #include "ostream.h"
 #include "str.h"
 #include "strescape.h"
-#include "safe-mkstemp.h"
 #include "settings-parser.h"
 #include "master-interface.h"
 #include "master-service.h"
@@ -24,7 +23,6 @@
 #include "config-request.h"
 #include "dovecot-version.h"
 
-#include <stdio.h>
 #include <ctype.h>
 #include <unistd.h>
 #include <sysexits.h>
@@ -979,28 +977,11 @@ int main(int argc, char *argv[])
                i_fatal("%s", error);
 
        if (dump_full && exec_args == NULL) {
-               struct ostream *output = o_stream_create_fd(STDOUT_FILENO, 0);
-               o_stream_set_no_error_handling(output, TRUE);
-               ret2 = config_dump_full(output, &import_environment);
-               o_stream_destroy(&output);
+               ret2 = config_dump_full(CONFIG_DUMP_FULL_DEST_STDOUT,
+                                       &import_environment);
        } else if (dump_full) {
-               /* create an unlinked file to /tmp */
-               string_t *path = t_str_new(128);
-               str_append(path, "/tmp/doveconf.");
-               int temp_fd = safe_mkstemp(path, 0700, (uid_t)-1, (gid_t)-1);
-               if (temp_fd == -1)
-                       i_fatal("safe_mkstemp(%s) failed: %m", str_c(path));
-               i_unlink(str_c(path));
-               struct ostream *output =
-                       o_stream_create_fd(temp_fd, IO_BLOCK_SIZE);
-               if (config_dump_full(output, &import_environment) < 0)
-                       i_fatal("Invalid configuration");
-               if (o_stream_finish(output) < 0) {
-                       i_fatal("write(%s) failed: %s",
-                               str_c(path), o_stream_get_error(output));
-               }
-               o_stream_destroy(&output);
-
+               int temp_fd = config_dump_full(CONFIG_DUMP_FULL_DEST_TEMPDIR,
+                                              &import_environment);
                if (getenv(DOVECOT_PRESERVE_ENVS_ENV) != NULL) {
                        /* Standalone binary is getting its configuration via
                           doveconf. Clean the environment before calling it.
@@ -1010,9 +991,12 @@ int main(int argc, char *argv[])
                        master_service_import_environment(import_environment);
                        master_service_env_clean();
                }
-               env_put(DOVECOT_CONFIG_FD_ENV, dec2str(temp_fd));
-               execvp(exec_args[0], exec_args);
-               i_fatal("execvp(%s) failed: %m", exec_args[0]);
+               if (temp_fd != -1) {
+                       env_put(DOVECOT_CONFIG_FD_ENV, dec2str(temp_fd));
+                       execvp(exec_args[0], exec_args);
+                       i_fatal("execvp(%s) failed: %m", exec_args[0]);
+               }
+               ret2 = -1;
        } else if (simple_output) {
                struct config_export_context *ctx;
                unsigned int section_idx = 0;
index b7357ab480279fa8217031f6a3d67fef85d005d7..42775fe29c3630e4fe942ca720fe32561024d81f 100644 (file)
@@ -5,6 +5,7 @@
 #include "event-filter.h"
 #include "path-util.h"
 #include "istream.h"
+#include "fdpass.h"
 #include "write-full.h"
 #include "str.h"
 #include "strescape.h"
@@ -275,7 +276,7 @@ master_service_open_config(struct master_service *service,
 {
        struct stat st;
        const char *path;
-       int fd;
+       int fd = -1;
 
        *path_r = path = input->config_path != NULL ? input->config_path :
                master_service_get_config_path(service);
@@ -297,52 +298,75 @@ master_service_open_config(struct master_service *service,
                   this fails because the socket is 0600. it's useful for
                   developers though. :) */
                fd = net_connect_unix(DOVECOT_CONFIG_SOCKET_PATH);
-               if (fd >= 0) {
+               if (fd >= 0)
                        *path_r = DOVECOT_CONFIG_SOCKET_PATH;
-                       net_set_nonblock(fd, FALSE);
-                       return fd;
+               else {
+                       /* fallback to executing doveconf */
                }
-               /* fallback to executing doveconf */
        }
 
-       if (stat(path, &st) < 0) {
-               *error_r = errno == EACCES ? eacces_error_get("stat", path) :
-                       t_strdup_printf("stat(%s) failed: %m", path);
-               config_error_update_path_source(service, input, error_r);
-               return -1;
-       }
+       if (fd == -1) {
+               if (stat(path, &st) < 0) {
+                       *error_r = errno == EACCES ?
+                               eacces_error_get("stat", path) :
+                               t_strdup_printf("stat(%s) failed: %m", path);
+                       config_error_update_path_source(service, input, error_r);
+                       return -1;
+               }
 
-       if (!S_ISSOCK(st.st_mode) && !S_ISFIFO(st.st_mode)) {
-               /* it's not an UNIX socket, don't even try to connect */
-               fd = -1;
-               errno = ENOTSOCK;
-       } else {
-               fd = net_connect_unix_with_retries(path, 1000);
-       }
-       if (fd < 0) {
-               *error_r = t_strdup_printf("net_connect_unix(%s) failed: %m",
-                                          path);
-               config_exec_fallback(service, input, error_r);
-               return -1;
+               if (!S_ISSOCK(st.st_mode) && !S_ISFIFO(st.st_mode)) {
+                       /* it's not an UNIX socket, don't even try to connect */
+                       fd = -1;
+                       errno = ENOTSOCK;
+               } else {
+                       fd = net_connect_unix_with_retries(path, 1000);
+               }
+               if (fd < 0) {
+                       *error_r = t_strdup_printf(
+                               "net_connect_unix(%s) failed: %m", path);
+                       config_exec_fallback(service, input, error_r);
+                       return -1;
+               }
        }
        net_set_nonblock(fd, FALSE);
-       return fd;
-}
+       const char *str = !input->reload_config ?
+               CONFIG_HANDSHAKE"REQ\n" :
+               CONFIG_HANDSHAKE"REQ\treload\n";
+       alarm(CONFIG_READ_TIMEOUT_SECS);
+       int ret = write_full(fd, str, strlen(str));
+       if (ret < 0)
+               *error_r = t_strdup_printf("write_full(%s) failed: %m", path);
 
-static int
-config_send_request(int fd, const char *path, const char **error_r)
-{
-       int ret;
+       int config_fd = -1;
+       if (ret == 0) {
+               /* read the config fd as reply */
+               char buf[1024];
+               ret = fd_read(fd, buf, sizeof(buf)-1, &config_fd);
+               if (ret < 0)
+                       *error_r = t_strdup_printf("fd_read() failed: %m");
+               else if (ret > 0 && buf[0] == '+' && buf[1] == '\n')
+                       ; /* success */
+               else if (ret > 0 && buf[0] == '-') {
+                       buf[ret] = '\0';
+                       *error_r = t_strdup_printf("Failed to read config: %s",
+                                                  t_strcut(buf+1, '\n'));
+                       i_close_fd(&config_fd);
+               } else {
+                       buf[ret] = '\0';
+                       *error_r = t_strdup_printf(
+                               "Failed to read config: Unexpected reply '%s'",
+                               t_strcut(buf, '\n'));
+                       i_close_fd(&config_fd);
+               }
+       }
+       alarm(0);
+       i_close_fd(&fd);
 
-       T_BEGIN {
-               const char *str = CONFIG_HANDSHAKE"REQ\n";
-               ret = write_full(fd, str, strlen(str));
-       } T_END;
-       if (ret < 0) {
-               *error_r = t_strdup_printf("write_full(%s) failed: %m", path);
+       if (config_fd == -1) {
+               config_exec_fallback(service, input, error_r);
                return -1;
        }
-       return 0;
+       return config_fd;
 }
 
 static int
@@ -387,8 +411,10 @@ void master_service_config_socket_try_open(struct master_service *service)
        i_zero(&input);
        input.never_exec = TRUE;
        fd = master_service_open_config(service, &input, &path, &error);
-       if (fd != -1)
+       if (fd != -1) {
                service->config_fd = fd;
+               fd_close_on_exec(service->config_fd, TRUE);
+       }
 }
 
 static void
@@ -434,10 +460,6 @@ master_service_settings_read_stream(struct setting_parser_context *parser,
                        return 0;
                }
                linenum++;
-               if (linenum == 1 && str_begins(line, "ERROR ", &line)) {
-                       *error_r = t_strdup(line);
-                       return -1;
-               }
                if (str_begins(line, ":FILTER ", &filter_string)) {
                        struct event_filter *filter = event_filter_create();
                        filter_string_parse_protocol(filter_string, &protocols);
@@ -478,11 +500,11 @@ master_service_settings_read_stream(struct setting_parser_context *parser,
                                           i_stream_get_error(input));
        } else if (input->v_offset == 0) {
                *error_r = t_strdup_printf(
-                       "read(%s) disconnected before receiving any data",
+                       "read(%s) returned EOF before receiving any data",
                        i_stream_get_name(input));
        } else {
                *error_r = t_strdup_printf(
-                       "read(%s) disconnected before receiving end-of-settings line",
+                       "read(%s) returned EOF before receiving end-of-settings line",
                        i_stream_get_name(input));
        }
        return -1;
@@ -500,41 +522,33 @@ int master_service_settings_read(struct master_service *service,
        const char *path = NULL, *value, *error;
        unsigned int i;
        int ret, fd = -1;
-       bool use_environment = FALSE, retry;
+       bool use_environment = FALSE;
 
        i_zero(output_r);
 
-       value = getenv(DOVECOT_CONFIG_FD_ENV);
-       if (value != NULL) {
+       if (service->config_fd != -1 && !input->reload_config) {
+               /* config was already read once */
+               fd = service->config_fd;
+               if (lseek(fd, 0, SEEK_SET) < 0)
+                       i_fatal("lseek(config fd) failed: %m");
+               path = "<config fd>";
+       } else if ((value = getenv(DOVECOT_CONFIG_FD_ENV)) != NULL) {
                /* doveconf -F parameter already executed us back.
                   The configuration is in DOVECOT_CONFIG_FD. */
                if (str_to_int(value, &fd) < 0 || fd < 0)
                        i_fatal("Invalid "DOVECOT_CONFIG_FD_ENV": %s", value);
+               path = t_strdup_printf("<"DOVECOT_CONFIG_FD_ENV" %d>", fd);
        } else if (getenv("DOVECONF_ENV") != NULL) {
                use_environment = (service->flags &
                                   MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS) == 0;
        } else if ((service->flags & MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS) == 0) {
                /* Open config via socket if possible. If it doesn't work,
                   execute doveconf -F. */
-               retry = service->config_fd != -1;
-               for (;;) {
-                       fd = master_service_open_config(service, input,
-                                                       &path, error_r);
-                       if (fd == -1) {
-                               if (errno == EACCES)
-                                       output_r->permission_denied = TRUE;
-                               return -1;
-                       }
-
-                       if (config_send_request(fd, path, error_r) == 0)
-                               break;
-                       i_close_fd(&fd);
-                       if (!retry) {
-                               config_exec_fallback(service, input, error_r);
-                               return -1;
-                       }
-                       /* config process died, retry connecting */
-                       retry = FALSE;
+               fd = master_service_open_config(service, input, &path, error_r);
+               if (fd == -1) {
+                       if (errno == EACCES)
+                               output_r->permission_denied = TRUE;
+                       return -1;
                }
        }
 
@@ -575,10 +589,9 @@ int master_service_settings_read(struct master_service *service,
 
        if (fd != -1) {
                istream = i_stream_create_fd(fd, SIZE_MAX);
-               alarm(CONFIG_READ_TIMEOUT_SECS);
+               i_stream_set_name(istream, path);
                ret = master_service_settings_read_stream(parser, event,
                                istream, output_r, error_r);
-               alarm(0);
                i_stream_unref(&istream);
 
                if (ret < 0) {
@@ -593,12 +606,14 @@ int master_service_settings_read(struct master_service *service,
                        return -1;
                }
 
-               if ((service->flags & MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN) != 0 &&
-                   service->config_fd == -1 && input->config_path == NULL &&
-                   getenv(DOVECOT_CONFIG_FD_ENV) == NULL)
+               if (input->config_path == NULL) {
+                       i_assert(service->config_fd == -1 ||
+                                service->config_fd == fd);
                        service->config_fd = fd;
-               else
+                       fd_close_on_exec(service->config_fd, TRUE);
+               } else {
                        i_close_fd(&fd);
+               }
                env_remove(DOVECOT_CONFIG_FD_ENV);
        }
        event_unref(&event);
index 846085d3304649c3b46d41f7a90d5a65ce0664db..bfde3ab9c779607a6040f3e26e3283cc1db8e5a7 100644 (file)
@@ -38,6 +38,7 @@ struct master_service_settings_input {
        bool preserve_environment;
        bool preserve_user;
        bool preserve_home;
+       bool reload_config;
        bool never_exec;
        bool always_exec;
        bool use_sysexits;
index 116bce0dc2a17b88707c7b95724d7e64b97f3209..afc0b000faac9157b64ce19182514988d7796835 100644 (file)
@@ -419,6 +419,7 @@ sig_settings_reload(const siginfo_t *si ATTR_UNUSED,
        input.roots = set_roots;
        input.config_path = services_get_config_socket_path(services);
        input.never_exec = TRUE;
+       input.reload_config = TRUE;
        if (master_service_settings_read(master_service, &input,
                                         &output, &error) < 0) {
                i_error("Error reading configuration: %s", error);