summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2021-11-26 08:19:58 +0100
committerJoel Klinghed <the_jk@spawned.biz>2021-11-26 08:19:58 +0100
commitf70495a48646e54272783b4b709aca0396cb85f8 (patch)
tree5e63b1f57582b3f025f1008034e4b066db0c32a6
parent9c26f52e0942e3ddc8fe90fad5da871324c66f08 (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.build9
-rw-r--r--src/daemon.cc162
-rw-r--r--src/daemon.hh45
-rw-r--r--src/server.cc67
-rw-r--r--test/test_daemon.cc146
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)));
+}