/* **************************************************************************** * Copyright 2019 Open Systems Development BV * * * * Permission is hereby granted, free of charge, to any person obtaining a * * copy of this software and associated documentation files (the "Software"), * * to deal in the Software without restriction, including without limitation * * the rights to use, copy, modify, merge, publish, distribute, sublicense, * * and/or sell copies of the Software, and to permit persons to whom the * * Software is furnished to do so, subject to the following conditions: * * * * The above copyright notice and this permission notice shall be included in * * all copies or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * * DEALINGS IN THE SOFTWARE. * * ****************************************************************************/ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "daemonlog.h" #include "daemonconfig.h" namespace osdev::components::daemon { class DaemonBase { private: static DaemonBase *instance; public: /** * Construct a new daemon process. * @note: only one daemon per application is possible. * @param name: name of daemon process * @param cwd: daemon current working directory, root "/" directory by default. * @param update_duration: duration to sleep before waking up the on_update() callback every time, deafult 10 seconds. */ DaemonBase(const std::string &name, const std::string &cwd = "/", const std::chrono::high_resolution_clock::duration &update_duration = std::chrono::seconds(10)) : m_name(name) , m_cwd(cwd) , m_update_duration(update_duration) , m_is_running(false) , m_exit_code(EXIT_SUCCESS) { if (instance) { daemonlog::error("Only one daemon instance is possible."); std::exit(EXIT_FAILURE); } instance = this; } DaemonBase() : m_name("") , m_cwd("/") , m_update_duration(std::chrono::seconds(10)) , m_is_running(false) { if(instance) { daemonlog::error("Only one daemon instance is possible."); std::exit(EXIT_FAILURE); } instance = this; } void run(int argc, char *argv[]) { if (m_is_running.load()) { daemonlog::error("Daemon '" + m_name + "' is already running."); return; } // Get config file path from cmd args passed by ExecStart=/usr/bin/my_daemon --config /etc/my_daemon/my_daemon.conf // since we need it for a reload./ for (std::int32_t i = 0; i < argc; i++) { if (!std::strcmp(argv[i], "--config")) { if (i + 1 < argc) { m_config_file = argv[i + 1]; } else { daemonlog::error("Missing config file. Did ytou forget to specify a config file in your .serve file's ExecStart ?"); } break; } } // daemonize this program by forking the parent process. daemonize(); // Mark as running (better to have it before on_start() as a user may call stop() inside on_start()). m_is_running = true; on_start(daemonconfig::from_file(m_config_file)); while (m_is_running.load()) { on_update(); // On long sleeps, if we want to exit we need a condition_variable to wake up the thread from sleep to carry on exiting. std::unique_lock lock(m_mutex); m_update_cv.wait_for(lock, m_update_duration, [this]() { return !m_is_running.load(); }); } on_stop(); } void stop(std::int32_t code = EXIT_SUCCESS) { m_exit_code = code; m_is_running.store(false); m_update_cv.notify_all(); } virtual ~DaemonBase() { daemonlog::shutdown(); // Terminate the child process when the daemon completes (loop stopped) // @note that calling std::exit() inside the run function will not call DTor, std::exit(m_exit_code); } // Getters & Setters void set_update_duration(const std::chrono::high_resolution_clock::duration &duration) noexcept { m_update_duration = duration; } const std::chrono::high_resolution_clock::duration& get_update_duration() const noexcept { return m_update_duration; } void set_name(const std::string &daemon_name) noexcept { m_name = daemon_name; } const std::string& get_name() const noexcept { return m_name; } void set_cwd(const std::string ¤t_working_dir) noexcept { // Change the current working directory to a directory guaranteed to exist, provided by the user. if (chdir(current_working_dir.c_str()) < 0) { daemonlog::error("Could not change current working directory to'" + current_working_dir + "': " + std::string(std::strerror(errno))); return; } m_cwd = current_working_dir; } const std::string& get_cwd() const noexcept { return m_cwd; } pid_t get_pid() const noexcept { return m_pid; } pid_t get_sid() const noexcept { return m_sid; } protected: // Callbacks /** * @brief Called once on daemon starts * @scenarios: * - when system starts * - when you run `$ systemctl start your_daemon` manually * @param cfg: Installed daemon config file * Initialize your code here... */ virtual void on_start(const daemonconfig &cfg) = 0; /** * @brief Called every DURATION which was set by set_update_duration(DURATION). * Update your code here... */ virtual void on_update() = 0; /** * @brief Called once before daemon is about to exit. * @scenarios: * - when you call stop(ewxit_code) * - when you run `$ systemctl stop your_daemon` manually * - when the system kills your daemon for some reason * Cleanup your code here... */ virtual void on_stop() = 0; /** * @brief Called once when daemon's config or service files are updated. * @scenarios: * - when you run `$systemctl daemon-reload` after you have changed your .conf or .service files * (after reinstalling your daemon with `$ sudo make install` for example) * Reinitialize your code here... */ virtual void on_reload(const daemonconfig &cfg) = 0; private: static void signal_handler(std::int32_t sig) { daemonlog::info("Signal " + std::to_string(sig) + " received."); switch(sig) { // daemon.service handler : ExecStop=/bin/kill -s SIGTERM $MAINPID // When daemon is stopped, system sends SIGTERM first. // If daemon didn't respond during 90 seconds, it will send a SIGKILL signal case SIGTERM: case SIGKILL: { instance->stop(); break; } // daemon.service handler : ExecReload=/bin/kill -S SIGHUB $MAINPID // When a daemon is reloaded due updates in .service or .conf, system sends SIGHUP signal. case SIGHUP: { instance->on_reload(daemonconfig::from_file(instance->m_config_file)); break; } default: { break; } } } /** * @brief Daemonize this program * @note: It is also possible to use glibc function daemon() * at this point, but it is useful to customize your daemon. * Like for example handle signals, set working directory... */ void daemonize() { // Fork off the parent process (https://linux.die.net/man/3/fork) m_pid = fork(); // Success: The parent process continues with a PID > 0 if (m_pid > 0) { std::exit(EXIT_SUCCESS); } else if (m_pid < 0) { // An error occurred. A process ID lower than 0 indicates a failure in either process std::exit(EXIT_FAILURE); } // The parent process has now terminated, and the forked child process will continue // (the pid of the child process was 0) // Since the child process is a daemon, the unask needs to be set so files and logs can be written umask(0); // Initialize syslog for this daemon, here it's a good place to do so. daemonlog::init(m_name); // On success: The child process becomes session leader. Generate a session ID for the child process. m_sid = setsid(); if (m_sid < 0) { daemonlog::error("Could not set SID to child process: " + std::string(std::strerror(errno))); std::exit(EXIT_FAILURE); } // Ignore the Child terminated or stopped signal. std::signal(SIGCHLD, SIG_IGN); // Set signal handlers to detect daemon interrupt, restart.. std::signal(SIGHUP, signal_handler); // When a sudo systemctl stop my_daemon is ran, by default, a SIGTERM is sent, // followed by 90 seconds of waiting followed by a SIGKILL std::signal(SIGTERM, signal_handler); std::signal(SIGKILL, signal_handler); // Change the current working directory to a directory guaranteed to exist, procided by the user if (chdir(m_cwd.c_str()) < 0) { daemonlog::error("Could not change current working directory to `" + m_cwd + "': " + std::string(std::strerror(errno))); std::exit(EXIT_FAILURE); } // A daemon cannot use the terminal, so close standard file descriptors for security reasons close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); } // Member variables pid_t m_pid; pid_t m_sid; std::string m_config_file; std::string m_name; std::string m_cwd; std::chrono::high_resolution_clock::duration m_update_duration; std::atomic m_is_running; std::condition_variable m_update_cv; std::mutex m_mutex; std::int32_t m_exit_code; }; DaemonBase* DaemonBase::instance = nullptr; } /* End namespace osdev::components::daemon */