diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2021-11-26 08:19:58 +0100 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2021-11-26 08:19:58 +0100 |
| commit | f70495a48646e54272783b4b709aca0396cb85f8 (patch) | |
| tree | 5e63b1f57582b3f025f1008034e4b066db0c32a6 | |
| parent | 9c26f52e0942e3ddc8fe90fad5da871324c66f08 (diff) | |
Create daemon module and use it from server
Need to run setup() after forking, otherwise each TaskRunner created
in setup() will dead-lock at exit as there are now two copies of
each of them but not of the threads causing the destructors to lock.
This made setup a little bit more complicated as it has to forward
the log and status to parent process but I turned out quite nice.
| -rw-r--r-- | meson.build | 9 | ||||
| -rw-r--r-- | src/daemon.cc | 162 | ||||
| -rw-r--r-- | src/daemon.hh | 45 | ||||
| -rw-r--r-- | src/server.cc | 67 | ||||
| -rw-r--r-- | test/test_daemon.cc | 146 |
5 files changed, 393 insertions, 36 deletions
diff --git a/meson.build b/meson.build index 654a6bf..9f23dfc 100644 --- a/meson.build +++ b/meson.build @@ -69,6 +69,8 @@ common_lib = static_library( 'src/buffer.hh', 'src/common.hh', 'src/config.hh', + 'src/daemon.cc', + 'src/daemon.hh', 'src/date.cc', 'src/date.hh', 'src/hash_method.cc', @@ -334,6 +336,13 @@ test('config', cpp_args: test_cpp_flags, dependencies: [common_dep, config_dep, test_utils_dep])) +test('daemon', + executable( + 'test_daemon', + sources: ['test/test_daemon.cc'], + cpp_args: test_cpp_flags, + dependencies: [common_dep, gmock_dep, gtest_dep])) + test('date', executable( 'test_date', diff --git a/src/daemon.cc b/src/daemon.cc new file mode 100644 index 0000000..2cebf1c --- /dev/null +++ b/src/daemon.cc @@ -0,0 +1,162 @@ +#include "common.hh" + +#include "daemon.hh" +#include "io.hh" +#include "logger.hh" +#include "logger_base.hh" +#include "unique_pipe.hh" + +#include <errno.h> +#include <string.h> +#include <unistd.h> + +namespace { + +enum class Type { + EXIT_GOOD, + EXIT_BAD, + LOG_ERR, + LOG_WARN, + LOG_INFO, + LOG_DBG, +}; + +struct MsgHeader { + Type type_; + size_t len_; + + MsgHeader() = default; + MsgHeader(Type type, size_t len) + : type_(type), len_(len) {} +}; + +class DaemonWriter : public LoggerBase { +public: + explicit DaemonWriter(unique_fd fd) + : fd_(std::move(fd)) {} + + void exit(bool success) { + MsgHeader header(success ? Type::EXIT_GOOD : Type::EXIT_BAD, 0); + if (!io::write_all(fd_.get(), &header, sizeof(header))) { + abort(); + } + } + +protected: + void msg(Level level, std::string_view str) override { + Type type; + switch (level) { + case Level::ERR: + type = Type::LOG_ERR; + break; + case Level::WARN: + type = Type::LOG_WARN; + break; + case Level::INFO: + type = Type::LOG_INFO; + break; + case Level::DBG: + type = Type::LOG_DBG; + break; + } + MsgHeader header(type, str.size()); + if (!io::write_all(fd_.get(), &header, sizeof(header)) || + !io::write_all(fd_.get(), str.data(), str.size())) { + abort(); + } + } + +private: + unique_fd fd_; +}; + +class DaemonReader { +public: + explicit DaemonReader(unique_fd fd) + : fd_(std::move(fd)) {} + + bool wait(Logger* logger) { + MsgHeader header; + std::string msg; + while (true) { + if (!io::read_all(fd_.get(), &header, sizeof(header))) { + logger->err("Error reading pipe: %s", strerror(errno)); + return false; + } + msg.resize(header.len_); + if (!io::read_all(fd_.get(), msg.data(), header.len_)) { + logger->err("Error reading pipe: %s", strerror(errno)); + return false; + } + switch (header.type_) { + case Type::EXIT_GOOD: + return true; + case Type::EXIT_BAD: + return false; + case Type::LOG_ERR: + logger->err("%.*s", static_cast<int>(msg.size()), msg.data()); + break; + case Type::LOG_WARN: + logger->warn("%.*s", static_cast<int>(msg.size()), msg.data()); + break; + case Type::LOG_INFO: + logger->info("%.*s", static_cast<int>(msg.size()), msg.data()); + break; + case Type::LOG_DBG: + logger->dbg("%.*s", static_cast<int>(msg.size()), msg.data()); + break; + } + } + } + +private: + unique_fd fd_; +}; + +} // namespace + +bool Daemon::run_in_foreground(Logger* logger, + std::unique_ptr<Daemon> daemon) { + return daemon->setup(logger) && daemon->run(); +} + +bool Daemon::fork_in_background(Logger* logger, + std::unique_ptr<Daemon> daemon) { + unique_pipe pipe; + if (!pipe) { + logger->err("Unable to create pipe(): %s", strerror(errno)); + return false; + } + auto pid = fork(); + if (pid == -1) { + logger->err("Failed to fork(): %s", strerror(errno)); + return false; + } + if (pid == 0) { + // Daemon process + setpgrp(); + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + { + DaemonWriter writer(pipe.release_writer()); + pipe.reset(); + + if (!daemon->setup(&writer)) { + writer.exit(false); + exit(EXIT_FAILURE); + } + + writer.exit(true); + } + + chdir("/"); + exit(daemon->run() ? EXIT_SUCCESS : EXIT_FAILURE); + } else { + // Calling process + DaemonReader reader(pipe.release_reader()); + pipe.reset(); + return reader.wait(logger); + } +} diff --git a/src/daemon.hh b/src/daemon.hh new file mode 100644 index 0000000..10f0584 --- /dev/null +++ b/src/daemon.hh @@ -0,0 +1,45 @@ +#ifndef DAEMON_HH +#define DAEMON_HH + +#include <memory> + +class Logger; + +class Daemon { +public: + virtual ~Daemon() = default; + + // Method is called after forking; + // if it returns true then parent process will detach and return success and + // run is executed. + // if it returns false then parent process will exit in failure and run is + // never executed. + // Anything logged to logger will write to calling logger regardless of return + // value. + // Note that stdin, stdout and stderr are all closed when this runs, you + // must use logger object if you want to output anything. + // Current directory is not modified. + virtual bool setup(Logger* logger) = 0; + + // Method is called after forking and only after setup returns true. + // Note that stdin, stdout and stderr are all closed when this runs. + // Current directory is changed to root. + virtual bool run() = 0; + + // Forks, runs setup() method before returning true or false. + // If setup() returns true then so does this method and run() is called. + // If setup() returns false then so does this method and run() is never + // called. + static bool fork_in_background(Logger* logger, + std::unique_ptr<Daemon> daemon); + + // Doesn't fork, just runs setup() and if it returns true then + // also run() and waits for it to return. + static bool run_in_foreground(Logger* logger, + std::unique_ptr<Daemon> daemon); + +protected: + Daemon() = default; +}; + +#endif // DAEMON_HH diff --git a/src/server.cc b/src/server.cc index 7200be0..b5a1b91 100644 --- a/src/server.cc +++ b/src/server.cc @@ -2,6 +2,7 @@ #include "args.hh" #include "config.hh" +#include "daemon.hh" #include "inet.hh" #include "logger.hh" #include "looper.hh" @@ -30,24 +31,30 @@ namespace { -class Server { +class Server : public Daemon { public: - ~Server() { + Server(Option const* config_arg, Option const* log_arg, + std::function<std::unique_ptr<Logger>()> default_logger_factory) + : config_name_(config_arg->is_set() ? config_arg->arg() : "travel3.conf"), + log_file_(log_arg->is_set() ? std::optional<std::string>(log_arg->arg()) + : std::nullopt), + default_logger_factory_(default_logger_factory) { + } + + ~Server() override { for (auto& fd : listen_) looper_->remove(fd.get()); } - bool setup(Logger* logger, Option const* config_arg, Option const* log_arg, - std::function<std::unique_ptr<Logger>()> default_logger_factory) { - auto config = Config::create(logger, config_arg->is_set() - ? config_arg->arg() : "travel3.conf"); + bool setup(Logger* logger) override { + auto config = Config::create(logger, config_name_); if (!config) return false; { std::filesystem::path log_file; - if (log_arg->is_set()) { - log_file = log_arg->arg(); + if (log_file_.has_value()) { + log_file = log_file_.value(); } else { log_file = config->get_path("log_file", ""); } @@ -58,7 +65,7 @@ public: if (!logger_) return false; } else { - logger_ = default_logger_factory(); + logger_ = default_logger_factory_(); } } @@ -112,7 +119,7 @@ public: return true; } - bool run() { + bool run() override { assert(logger_); assert(looper_); assert(transport_); @@ -177,6 +184,9 @@ private: assert(false); } + std::string const config_name_; + std::optional<std::string> const log_file_; + std::function<std::unique_ptr<Logger>()> default_logger_factory_; std::shared_ptr<Logger> logger_; std::shared_ptr<Looper> looper_; std::shared_ptr<TaskRunner> runner_; @@ -227,36 +237,21 @@ int main(int argc, char** argv) { return EXIT_SUCCESS; } - Server server; + auto server = std::make_unique<Server>( + config_arg, log_arg, + [daemon_arg] () { + return daemon_arg->is_set() + ? Logger::create_syslog("travel3") + : Logger::create_stdio(); + }); - // Setup errors will always be logged to stdio to make them more visible. auto logger = Logger::create_stdio(); - if (!server.setup(logger.get(), config_arg, log_arg, - [daemon_arg] () { - return daemon_arg->is_set() - ? Logger::create_syslog("travel3") - : Logger::create_stdio(); - })) - return EXIT_FAILURE; + bool ret; if (daemon_arg->is_set()) { - auto pid = fork(); - if (pid == -1) { - logger->err("Failed to fork(): %s", strerror(errno)); - return EXIT_FAILURE; - } - if (pid == 0) { - // Daemon process - chdir("/"); - setpgrp(); - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); - return server.run() ? EXIT_SUCCESS : EXIT_FAILURE; - } else { - return EXIT_SUCCESS; - } + ret = Daemon::fork_in_background(logger.get(), std::move(server)); } else { - return server.run() ? EXIT_SUCCESS : EXIT_FAILURE; + ret = Daemon::run_in_foreground(logger.get(), std::move(server)); } + return ret ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/test/test_daemon.cc b/test/test_daemon.cc new file mode 100644 index 0000000..04ba105 --- /dev/null +++ b/test/test_daemon.cc @@ -0,0 +1,146 @@ +#include "common.hh" + +#include "daemon.hh" +#include "logger.hh" + +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include <memory> + +namespace { + +class MockDaemon : public Daemon { +public: + MOCK_METHOD(bool, setup, (Logger*), (override)); + MOCK_METHOD(bool, run, (), (override)); +}; + +class MockLogger : public Logger { +public: + void err(char const* format, ...) override { + va_list args; + va_start(args, format); + vlog("err", format, args); + va_end(args); + } + + void warn(char const* format, ...) override { + va_list args; + va_start(args, format); + vlog("warn", format, args); + va_end(args); + } + + void info(char const* format, ...) override { + va_list args; + va_start(args, format); + vlog("info", format, args); + va_end(args); + } + + void dbg(char const* format, ...) override { + va_list args; + va_start(args, format); + vlog("dbg", format, args); + va_end(args); + } + + void vlog(std::string level, char const* format, va_list args) { + if (strcmp(format, "%s") == 0) { + log(level, va_arg(args, char const*)); + } else if (strcmp(format, "%.*s") == 0) { + auto len = va_arg(args, int); + auto ptr = va_arg(args, char const*); + log(level, std::string(ptr, len)); + } else { + log(level, format); + } + } + + MOCK_METHOD(void, log, (std::string, std::string)); +}; + +class TestDaemon : public Daemon { +public: + bool setup(Logger*) override { + return false; + } + + bool run() override { + return false; + } +}; + +class FailSetupDaemon : public TestDaemon { +public: + explicit FailSetupDaemon(std::string error) + : error_(std::move(error)) {} + + bool setup(Logger* logger) override { + logger->err("%s", error_.c_str()); + return false; + } + +private: + std::string error_; +}; + +class SuccessSetupDaemon : public TestDaemon { +public: + explicit SuccessSetupDaemon(std::string success) + : success_(std::move(success)) {} + + bool setup(Logger* logger) override { + logger->info("%s", success_.c_str()); + return true; + } + +private: + std::string success_; +}; + +} // namespace + +TEST(daemon, run_in_foreground_setup_fail) { + auto daemon = std::make_unique<MockDaemon>(); + auto logger = Logger::create_null(); + EXPECT_CALL(*daemon, setup(logger.get())) + .WillOnce(testing::Return(false)); + EXPECT_FALSE(Daemon::run_in_foreground(logger.get(), std::move(daemon))); +} + +TEST(daemon, run_in_foreground_run_fail) { + auto daemon = std::make_unique<MockDaemon>(); + auto logger = Logger::create_null(); + EXPECT_CALL(*daemon, setup(logger.get())) + .WillOnce(testing::Return(true)); + EXPECT_CALL(*daemon, run()) + .WillOnce(testing::Return(false)); + EXPECT_FALSE(Daemon::run_in_foreground(logger.get(), std::move(daemon))); +} + +TEST(daemon, run_in_foreground_run_success) { + auto daemon = std::make_unique<MockDaemon>(); + auto logger = Logger::create_null(); + EXPECT_CALL(*daemon, setup(logger.get())) + .WillOnce(testing::Return(true)); + EXPECT_CALL(*daemon, run()) + .WillOnce(testing::Return(true)); + EXPECT_TRUE(Daemon::run_in_foreground(logger.get(), std::move(daemon))); +} + +TEST(daemon, fork_in_background_setup_fail) { + auto daemon = std::make_unique<FailSetupDaemon>("Something failed"); + MockLogger logger; + testing::Mock::AllowLeak(&logger); // Forking copies the mock. + EXPECT_CALL(logger, log("err", "Something failed")); + EXPECT_FALSE(Daemon::fork_in_background(&logger, std::move(daemon))); +} + +TEST(daemon, fork_in_background_run_fail) { + auto daemon = std::make_unique<SuccessSetupDaemon>("All good"); + MockLogger logger; + testing::Mock::AllowLeak(&logger); // Forking copies the mock. + EXPECT_CALL(logger, log("info", "All good")); + EXPECT_TRUE(Daemon::fork_in_background(&logger, std::move(daemon))); +} |
