refactor(script): Make the module behave as intended

Tail script now block execution until there's data
available on the script's output stream.

Running commands are also being terminated properly.
This commit is contained in:
Michael Carlberg 2016-10-15 13:15:56 +02:00
parent ce93a63cb8
commit dc82d0ed57
4 changed files with 199 additions and 195 deletions

View File

@ -9,10 +9,11 @@
#include "utils/file.hpp"
#include "utils/string.hpp"
#define GET_CONFIG_VALUE(section, var, name) var = m_conf.get<decltype(var)>(section, name, var)
LEMONBUDDY_NS
#define GET_CONFIG_VALUE(section, var, name) var = m_conf.get<decltype(var)>(section, name, var)
#define REQ_CONFIG_VALUE(section, var, name) var = m_conf.get<decltype(var)>(section, name)
using ptree = boost::property_tree::ptree;
DEFINE_ERROR(value_error);

View File

@ -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<script_module> {
class script_module : public event_module<script_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<string>(name(), "exec");
m_tail = m_conf.get<bool>(name(), "tail", m_tail);
m_maxlen = m_conf.get<size_t>(name(), "maxlen", 0);
m_ellipsis = m_conf.get<bool>(name(), "ellipsis", m_ellipsis);
if (m_tail)
m_interval = 0s;
else
m_interval = chrono::duration<double>(m_conf.get<float>(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<string>(name(), "click-left", "");
m_actions[mousebtn::MIDDLE] = m_conf.get<string>(name(), "click-middle", "");
m_actions[mousebtn::RIGHT] = m_conf.get<string>(name(), "click-right", "");
m_actions[mousebtn::SCROLL_UP] = m_conf.get<string>(name(), "scroll-up", "");
m_actions[mousebtn::SCROLL_DOWN] = m_conf.get<string>(name(), "scroll-down", "");
// Add formats and elements
if (!m_tail) {
m_interval = interval_t{m_conf.get<float>(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 = "<output>";
unique_ptr<command_util::command> m_command;
map<mousebtn, string> 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<mousebtn, string> m_actions;
stateflag m_broadcasted{false};
string m_output;
string m_prev;
int m_counter{0};
};
}

View File

@ -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<Command>("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<Command>(
* @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<Command>("/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<string> 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<string> 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<threading_util::spin_lock> 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<command>;
template <typename... Args>
auto make_command(Args&&... args) {
command_t make_command(Args&&... args) {
return make_unique<command>(
logger::configure().create<const logger&>(), forward<Args>(args)...);
}

View File

@ -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