diff --git a/include/components/config.hpp b/include/components/config.hpp index 296c33ae..e4181920 100644 --- a/include/components/config.hpp +++ b/include/components/config.hpp @@ -9,10 +9,11 @@ #include "utils/file.hpp" #include "utils/string.hpp" -#define GET_CONFIG_VALUE(section, var, name) var = m_conf.get(section, name, var) - LEMONBUDDY_NS +#define GET_CONFIG_VALUE(section, var, name) var = m_conf.get(section, name, var) +#define REQ_CONFIG_VALUE(section, var, name) var = m_conf.get(section, name) + using ptree = boost::property_tree::ptree; DEFINE_ERROR(value_error); diff --git a/include/modules/script.hpp b/include/modules/script.hpp index 9bf7cc3f..5ed202cf 100644 --- a/include/modules/script.hpp +++ b/include/modules/script.hpp @@ -6,189 +6,164 @@ LEMONBUDDY_NS #define SHELL_CMD "/usr/bin/env\nsh\n-c\n" +#define OUTPUT_ACTION(BUTTON) \ + if (!m_actions[BUTTON].empty()) \ + m_builder->cmd(BUTTON, string_util::replace_all(m_actions[BUTTON], "%counter%", counter_str)) namespace modules { - class script_module : public timer_module { + class script_module : public event_module { public: - using timer_module::timer_module; + using event_module::event_module; void setup() { - // Load configuration values + m_formatter->add(DEFAULT_FORMAT, TAG_OUTPUT, {TAG_OUTPUT}); - m_exec = m_conf.get(name(), "exec"); - m_tail = m_conf.get(name(), "tail", m_tail); - - m_maxlen = m_conf.get(name(), "maxlen", 0); - m_ellipsis = m_conf.get(name(), "ellipsis", m_ellipsis); - - if (m_tail) - m_interval = 0s; - else - m_interval = chrono::duration(m_conf.get(name(), "interval", 1)); + // Load configuration values {{{ + REQ_CONFIG_VALUE(name(), m_exec, "exec"); + GET_CONFIG_VALUE(name(), m_tail, "tail"); + GET_CONFIG_VALUE(name(), m_maxlen, "maxlen"); + GET_CONFIG_VALUE(name(), m_ellipsis, "ellipsis"); m_actions[mousebtn::LEFT] = m_conf.get(name(), "click-left", ""); m_actions[mousebtn::MIDDLE] = m_conf.get(name(), "click-middle", ""); m_actions[mousebtn::RIGHT] = m_conf.get(name(), "click-right", ""); - m_actions[mousebtn::SCROLL_UP] = m_conf.get(name(), "scroll-up", ""); m_actions[mousebtn::SCROLL_DOWN] = m_conf.get(name(), "scroll-down", ""); - // Add formats and elements + if (!m_tail) { + m_interval = interval_t{m_conf.get(name(), "interval", m_interval.count())}; + } + // }}} + // Execute the tail command {{{ + if (m_tail) { + try { + auto exec = string_util::replace_all(m_exec, "%counter%", to_string(++m_counter)); + m_log.trace("%s: Executing '%s'", name(), exec); - m_formatter->add(DEFAULT_FORMAT, TAG_OUTPUT, {TAG_OUTPUT}); + m_command = command_util::make_command(SHELL_CMD + exec); + m_command->exec(false); + } catch (const std::exception& err) { + m_log.err("%s: Failed to execute tail command, stopping module..", name()); + m_log.err("%s: %s", name(), err.what()); + stop(); + } + } + // }}} + } - // Start a subthread tailing the script + void stop() { + // Put the module in stopped state {{{ + event_module::stop(); + // }}} + // Terminate running command {{{ + try { + if (m_command) + m_command.reset(); + } catch (const std::exception& err) { + m_log.err("%s: %s", name(), err.what()); + } + // }}} + } - if (m_tail) - dispatch_tailscript_runner(); + bool has_event() { + // Handle non-tailing command {{{ + if (!m_tail) { + sleep(m_interval); + return enabled(); + } + // }}} + // Handle tailing command {{{ + if (!m_command || !m_command->is_running()) { + m_log.warn("%s: Tail command finished, stopping module...", name()); + stop(); + return false; + } else if ((m_output = m_command->readline()) != m_prev) { + m_prev = m_output; + return true; + } else { + return false; + } + // }}} } bool update() { - auto previous_output = m_output; + // Handle tailing command {{{ + if (m_tail) + return true; + // }}} + // Handle non-tailing command {{{ + try { + auto exec = string_util::replace_all(m_exec, "%counter%", to_string(++m_counter)); + auto cmd = command_util::make_command(SHELL_CMD + exec); - m_output.clear(); + m_log.trace("%s: Executing '%s'", name(), exec); - if (m_tail) { - m_output = tail_command(); - } else { - m_output = read_output(); + cmd->exec(); + cmd->tail([this](string contents) { m_output = contents; }); + } catch (const std::exception& err) { + m_log.err("%s: Failed to execute command, stopping module..", name()); + m_log.err("%s: %s", name(), err.what()); + stop(); + return false; } - if (m_output != previous_output) - m_broadcasted = false; - - if (m_maxlen > 0 && m_output.length() > m_maxlen) { - m_output.erase(m_maxlen); - if (m_ellipsis) - m_output += "..."; + if (m_output != m_prev) { + m_prev = m_output; + return true; } - if (!enabled()) - return false; - else if (m_output.empty() && !m_broadcasted) - return true; - else if (m_output.empty()) - return false; - else - return true; + return false; + // }}} } string get_output() { - if (m_output.empty()) - return ""; + auto output = string_util::replace_all(m_output, "\n", ""); + if (output.empty()) + return " "; + // Truncate output to the defined max length {{{ + if (m_maxlen > 0 && output.length() > m_maxlen) { + output.erase(m_maxlen); + output += m_ellipsis ? "..." : ""; + } + // }}} + // Add mousebtn command handlers {{{ auto counter_str = to_string(m_counter); - if (!m_actions[mousebtn::LEFT].empty()) - m_builder->cmd(mousebtn::LEFT, - string_util::replace_all(m_actions[mousebtn::LEFT], "%counter%", counter_str)); - if (!m_actions[mousebtn::MIDDLE].empty()) - m_builder->cmd(mousebtn::MIDDLE, - string_util::replace_all(m_actions[mousebtn::MIDDLE], "%counter%", counter_str)); - if (!m_actions[mousebtn::RIGHT].empty()) - m_builder->cmd(mousebtn::RIGHT, - string_util::replace_all(m_actions[mousebtn::RIGHT], "%counter%", counter_str)); + OUTPUT_ACTION(mousebtn::LEFT); + OUTPUT_ACTION(mousebtn::MIDDLE); + OUTPUT_ACTION(mousebtn::RIGHT); + OUTPUT_ACTION(mousebtn::SCROLL_UP); + OUTPUT_ACTION(mousebtn::SCROLL_DOWN); + // }}} - if (!m_actions[mousebtn::SCROLL_UP].empty()) - m_builder->cmd(mousebtn::SCROLL_UP, - string_util::replace_all(m_actions[mousebtn::SCROLL_UP], "%counter%", counter_str)); - if (!m_actions[mousebtn::SCROLL_DOWN].empty()) - m_builder->cmd(mousebtn::SCROLL_DOWN, - string_util::replace_all(m_actions[mousebtn::SCROLL_DOWN], "%counter%", counter_str)); - - m_builder->node(module::get_output()); + m_builder->node(output); return m_builder->flush(); } bool build(builder* builder, string tag) { - if (tag == TAG_OUTPUT) - builder->node(string_util::replace_all(m_output, "\n", "")); - else + if (tag != TAG_OUTPUT) return false; + builder->node(m_output); return true; } protected: - /** - * Read line from tailscript - */ - string tail_command() { - int bytes_read = 0; - - if (!m_command) - return ""; - if (io_util::poll_read(m_command->get_stdout(PIPE_READ), 100)) - return io_util::readline(m_command->get_stdout(PIPE_READ), bytes_read); - return ""; - } - - /** - * Execute command and read its output - */ - string read_output() { - string output; - - try { - m_log.trace("%s: Executing command '%s'", name(), m_exec); - - auto cmd = command_util::make_command( - SHELL_CMD + string_util::replace_all(m_exec, "%counter%", to_string(++m_counter))); - - cmd->exec(false); - - while (true) { - int bytes_read = 0; - string contents = io_util::readline(cmd->get_stdout(PIPE_READ), bytes_read); - if (bytes_read <= 0) - break; - output += contents; - output += "\n"; - } - - cmd->wait(); - } catch (const system_error& err) { - m_log.err(err.what()); - return ""; - } - - return output; - } - - /** - * Run tail script in separate thread - */ - void dispatch_tailscript_runner() { - m_threads.emplace_back([this] { - try { - while (enabled() && (!m_command || !m_command->is_running())) { - m_log.trace("%s: Executing command '%s'", name(), m_exec); - m_command = command_util::make_command( - SHELL_CMD + string_util::replace_all(m_exec, "%counter%", to_string(++m_counter))); - m_command->exec(true); - } - } catch (const system_error& err) { - m_log.err("Failed to create command (what: %s)", err.what()); - } - }); - } - - private: static constexpr auto TAG_OUTPUT = ""; - unique_ptr m_command; - - map m_actions; + command_util::command_t m_command; string m_exec; bool m_tail = false; - string m_output; - int m_counter{0}; - + interval_t m_interval = 1s; size_t m_maxlen = 0; bool m_ellipsis = true; + map m_actions; - stateflag m_broadcasted{false}; + string m_output; + string m_prev; + int m_counter{0}; }; } diff --git a/include/utils/command.hpp b/include/utils/command.hpp index a59f6a2d..b67bdd97 100644 --- a/include/utils/command.hpp +++ b/include/utils/command.hpp @@ -14,59 +14,70 @@ LEMONBUDDY_NS +DEFINE_ERROR(command_error); +DEFINE_CHILD_ERROR(command_strerror, command_error); + namespace command_util { /** + * Wrapper used to execute command in a subprocess. + * In-/output streams are opened to enable ipc. + * * Example usage: + * * @code cpp - * auto cmd = make_unique("cat /etc/rc.local"); + * auto cmd = command_util::make_command("cat /etc/rc.local"); * cmd->exec(); - * cmd->tail(callback); //---> the contents of /etc/rc.local is sent to callback() + * cmd->tail([](string s) { std::cout << s << std::endl; }); + * @endcode * - * auto cmd = make_unique( + * @code cpp + * auto cmd = command_util::make_command( * "/bin/sh\n-c\n while read -r line; do echo data from parent process: $line; done"); - * cmd->exec(); + * cmd->exec(false); * cmd->writeline("Test"); - * cout << cmd->readline(); //---> data from parent process: Test + * cout << cmd->readline(); + * cmd->wait(); + * @endcode * - * auto cmd = make_unique("/bin/sh\n-c\nfor i in 1 2 3; do echo $i; done"); - * cout << cmd->readline(); //---> 1 - * cout << cmd->readline() << cmd->readline(); //---> 23 - * @encode + * @code cpp + * vector exec{{"/bin/sh"}, {"-c"}, {"for i in 1 2 3; do echo $i; done"}}; + * auto cmd = command_util::make_command(exec); + * cmd->exec(); + * cout << cmd->readline(); // 1 + * cout << cmd->readline() << cmd->readline(); // 23 + * @endcode */ class command { public: - explicit command(const logger& logger, string cmd, int out[2] = nullptr, int in[2] = nullptr) - : m_log(logger), m_cmd(cmd) { - if (in != nullptr) { - m_stdin[PIPE_READ] = in[PIPE_READ]; - m_stdin[PIPE_WRITE] = in[PIPE_WRITE]; - } else if (pipe(m_stdin) != 0) { - throw system_error("Failed to allocate pipe"); - } + explicit command(const logger& logger, vector cmd) + : command(logger, string_util::join(cmd, "\n")) {} - if (out != nullptr) { - m_stdout[PIPE_READ] = out[PIPE_READ]; - m_stdout[PIPE_WRITE] = out[PIPE_WRITE]; - } else if (pipe(m_stdout) != 0) { - close(m_stdin[PIPE_READ]); - close(m_stdin[PIPE_WRITE]); - throw system_error("Failed to allocate pipe"); - } + explicit command(const logger& logger, string cmd) : m_log(logger), m_cmd(cmd) { + if (pipe(m_stdin) != 0) + throw command_strerror("Failed to allocate input stream"); + if (pipe(m_stdout) != 0) + throw command_strerror("Failed to allocate output stream"); } ~command() { if (is_running()) { - m_log.warn("command: Sending SIGKILL to forked process (%d)", m_forkpid); - kill(m_forkpid, SIGKILL); + try { + m_log.trace("command: Sending SIGTERM to running child process (%d)", m_forkpid); + killpg(m_forkpid, SIGTERM); + wait(); + } catch (const std::exception& err) { + m_log.err("command: %s", err.what()); + } } - if (m_stdin[PIPE_READ] > 0 && (close(m_stdin[PIPE_READ]) == -1)) - m_log.err("command: Failed to close fd: %s (%d)", strerror(errno), errno); - if (m_stdin[PIPE_WRITE] > 0 && (close(m_stdin[PIPE_WRITE]) == -1)) - m_log.err("command: Failed to close fd: %s (%d)", strerror(errno), errno); - if (m_stdout[PIPE_READ] > 0 && (close(m_stdout[PIPE_READ]) == -1)) - m_log.err("command: Failed to close fd: %s (%d)", strerror(errno), errno); - if (m_stdout[PIPE_WRITE] > 0 && (close(m_stdout[PIPE_WRITE]) == -1)) - m_log.err("command: Failed to close fd: %s (%d)", strerror(errno), errno); + + if (m_stdin[PIPE_READ] > 0) + close(m_stdin[PIPE_READ]); + if (m_stdin[PIPE_WRITE] > 0) + close(m_stdin[PIPE_WRITE]); + if (m_stdout[PIPE_READ] > 0) + close(m_stdout[PIPE_READ]); + if (m_stdout[PIPE_WRITE] > 0) + close(m_stdout[PIPE_WRITE]); } /** @@ -78,31 +89,35 @@ namespace command_util { if (process_util::in_forked_process(m_forkpid)) { if (dup2(m_stdin[PIPE_READ], STDIN_FILENO) == -1) - throw system_error("Failed to redirect stdin in child process"); + throw command_strerror("Failed to redirect stdin in child process"); if (dup2(m_stdout[PIPE_WRITE], STDOUT_FILENO) == -1) - throw system_error("Failed to redirect stdout in child process"); + throw command_strerror("Failed to redirect stdout in child process"); if (dup2(m_stdout[PIPE_WRITE], STDERR_FILENO) == -1) - throw system_error("Failed to redirect stderr in child process"); + throw command_strerror("Failed to redirect stderr in child process"); - // Close file descriptors that won't be used by forked process + // Close file descriptors that won't be used by the child if ((m_stdin[PIPE_READ] = close(m_stdin[PIPE_READ])) == -1) - throw system_error("Failed to close fd"); + throw command_strerror("Failed to close fd"); if ((m_stdin[PIPE_WRITE] = close(m_stdin[PIPE_WRITE])) == -1) - throw system_error("Failed to close fd"); + throw command_strerror("Failed to close fd"); if ((m_stdout[PIPE_READ] = close(m_stdout[PIPE_READ])) == -1) - throw system_error("Failed to close fd"); + throw command_strerror("Failed to close fd"); if ((m_stdout[PIPE_WRITE] = close(m_stdout[PIPE_WRITE])) == -1) - throw system_error("Failed to close fd"); + throw command_strerror("Failed to close fd"); - // Replace the forked process with the given command + // Make sure SIGTERM is raised + process_util::unblock_signal(SIGTERM); + + setpgid(m_forkpid, 0); process_util::exec(m_cmd); - std::exit(0); + + throw command_error("Exec failed"); } else { - // Close file descriptors that won't be used by parent process + // Close file descriptors that won't be used by the parent if ((m_stdin[PIPE_READ] = close(m_stdin[PIPE_READ])) == -1) - throw system_error("Failed to close fd"); + throw command_strerror("Failed to close fd"); if ((m_stdout[PIPE_WRITE] = close(m_stdout[PIPE_WRITE])) == -1) - throw system_error("Failed to close fd"); + throw command_strerror("Failed to close fd"); if (wait_for_completion) return wait(); @@ -115,23 +130,18 @@ namespace command_util { * Wait for the child processs to finish */ int wait() { - do { - pid_t pid; + auto waitflags = WCONTINUED | WUNTRACED; - if ((pid = process_util::wait_for_completion( - m_forkpid, &m_forkstatus, WCONTINUED | WUNTRACED)) == -1) { - throw system_error( - "Process did not finish successfully (" + to_string(m_forkstatus) + ")"); - } + do { + if (process_util::wait_for_completion(m_forkpid, &m_forkstatus, waitflags) == -1) + throw command_error("Process did not finish successfully"); if (WIFEXITED(m_forkstatus)) m_log.trace("command: Exited with status %d", WEXITSTATUS(m_forkstatus)); else if (WIFSIGNALED(m_forkstatus)) - m_log.trace("command: Got killed by signal %d (%s)", WTERMSIG(m_forkstatus), - strerror(WTERMSIG(m_forkstatus))); + m_log.trace("command: killed by signal %d", WTERMSIG(m_forkstatus)); else if (WIFSTOPPED(m_forkstatus)) - m_log.trace("command: Stopped by signal %d (%s)", WSTOPSIG(m_forkstatus), - strerror(WSTOPSIG(m_forkstatus))); + m_log.trace("command: Stopped by signal %d", WSTOPSIG(m_forkstatus)); else if (WIFCONTINUED(m_forkstatus) == true) m_log.trace("command: Continued"); } while (!WIFEXITED(m_forkstatus) && !WIFSIGNALED(m_forkstatus)); @@ -154,6 +164,14 @@ namespace command_util { return io_util::writeline(m_stdin[PIPE_WRITE], data); } + /** + * Read a line from the commands output stream + */ + string readline() { + std::lock_guard lck(m_pipelock); + return io_util::readline(m_stdout[PIPE_READ]); + } + /** * Get command output channel */ @@ -203,8 +221,10 @@ namespace command_util { threading_util::spin_lock m_pipelock; }; + using command_t = unique_ptr; + template - auto make_command(Args&&... args) { + command_t make_command(Args&&... args) { return make_unique( logger::configure().create(), forward(args)...); } diff --git a/include/utils/process.hpp b/include/utils/process.hpp index 55968bb0..e7672b7e 100644 --- a/include/utils/process.hpp +++ b/include/utils/process.hpp @@ -103,6 +103,14 @@ namespace process_util { inline auto notify_childprocess() { return wait_for_completion_nohang() > 0; } + + inline auto unblock_signal(int sig) { + sigset_t sigmask; + sigemptyset(&sigmask); + sigaddset(&sigmask, sig); + if (pthread_sigmask(SIG_UNBLOCK, &sigmask, nullptr) == -1) + throw system_error("Failed to change pthread_sigmask"); + } } LEMONBUDDY_NS_END