feat(ipc): New ipc module

Add a new module that allow users to configure hooks
on received ipc messages. The hook will execute the defined
shell script and the output of the script will be used
as the module content.

Ref #84
This commit is contained in:
Michael Carlberg 2016-11-14 09:21:18 +01:00
parent 04fac96d78
commit e3065d0e6c
7 changed files with 230 additions and 36 deletions

View File

@ -17,13 +17,12 @@ LEMONBUDDY_NS
class controller {
public:
explicit controller(connection& conn, const logger& logger, const config& config, unique_ptr<eventloop> eventloop,
unique_ptr<bar> bar, unique_ptr<ipc> ipc, inotify_util::watch_t& confwatch)
unique_ptr<bar> bar, inotify_util::watch_t& confwatch)
: m_connection(conn)
, m_log(logger)
, m_conf(config)
, m_eventloop(forward<decltype(eventloop)>(eventloop))
, m_bar(forward<decltype(bar)>(bar))
, m_ipc(forward<decltype(ipc)>(ipc))
, m_confwatch(confwatch) {}
~controller();
@ -85,8 +84,7 @@ namespace {
configure_logger(),
configure_config(),
configure_eventloop(),
configure_bar(),
configure_ipc());
configure_bar());
// clang-format on
}
}

View File

@ -5,6 +5,18 @@
LEMONBUDDY_NS
/**
* Message types
*/
struct ipc_command {
static constexpr auto prefix{"cmd:"};
string payload;
};
struct ipc_hook {
static constexpr auto prefix{"hook:"};
string payload;
};
/**
* Component used for inter-process communication.
*
@ -14,27 +26,24 @@ LEMONBUDDY_NS
*/
class ipc {
public:
struct message_internal {
static constexpr auto prefix{"app:"};
};
struct message_command {
static constexpr auto prefix{"cmd:"};
};
struct message_custom {
static constexpr auto prefix{"custom:"};
};
explicit ipc(const logger& logger) : m_log(logger) {}
~ipc();
void attach_callback(callback<const ipc_command&>&& cb);
void attach_callback(callback<const ipc_hook&>&& cb);
void receive_messages();
protected:
void parse(string payload);
void parse(const string& payload) const;
void delegate(const ipc_command& msg) const;
void delegate(const ipc_hook& msg) const;
private:
const logger& m_log;
vector<callback<const ipc_command&>> m_command_callbacks;
vector<callback<const ipc_hook&>> m_hook_callbacks;
stateflag m_running{false};
string m_fifo;

44
include/modules/ipc.hpp Normal file
View File

@ -0,0 +1,44 @@
#pragma once
#include "modules/meta.hpp"
#include "utils/command.hpp"
LEMONBUDDY_NS
struct ipc_hook; // fwd
namespace modules {
/**
* Hook structure that will be fired
* when receiving a message with specified id
*/
struct hook {
string payload;
string command;
};
/**
* Module that allow users to configure hooks on
* received ipc messages. The hook will execute the defined
* shell script and the resulting output will be used
* as the module content.
*/
class ipc_module : public static_module<ipc_module> {
public:
using static_module::static_module;
void setup();
string get_output();
bool build(builder* builder, string tag) const;
void on_message(const ipc_hook& msg);
private:
static constexpr auto TAG_OUTPUT = "<output>";
vector<unique_ptr<hook>> m_hooks;
string m_output;
map<mousebtn, string> m_actions;
};
}
LEMONBUDDY_NS_END

View File

@ -80,4 +80,7 @@ namespace command_util {
}
}
using command = command_util::command;
using command_t = command_util::command_t;
LEMONBUDDY_NS_END

View File

@ -3,12 +3,6 @@
#include "components/controller.hpp"
#include "components/bar.hpp"
#include "components/config.hpp"
#include "components/eventloop.hpp"
#include "components/ipc.hpp"
#include "components/logger.hpp"
#include "components/signals.hpp"
#include "modules/backlight.hpp"
#include "modules/battery.hpp"
#include "modules/bspwm.hpp"
@ -16,6 +10,7 @@
#include "modules/cpu.hpp"
#include "modules/date.hpp"
#include "modules/fs.hpp"
#include "modules/ipc.hpp"
#include "modules/memory.hpp"
#include "modules/menu.hpp"
#include "modules/script.hpp"
@ -23,6 +18,13 @@
#include "modules/text.hpp"
#include "modules/unsupported.hpp"
#include "modules/xbacklight.hpp"
#include "components/bar.hpp"
#include "components/config.hpp"
#include "components/eventloop.hpp"
#include "components/ipc.hpp"
#include "components/logger.hpp"
#include "components/signals.hpp"
#include "utils/process.hpp"
#include "utils/string.hpp"
@ -62,12 +64,10 @@ controller::~controller() {
m_eventloop.reset();
}
#if DEBUG
if (m_ipc) {
m_log.info("Deconstructing ipc");
m_ipc.reset();
}
#endif
if (m_bar) {
m_log.info("Deconstructing bar");
@ -104,6 +104,13 @@ void controller::bootstrap(bool writeback, bool dump_wmname) {
m_log.trace("controller: Query X extension data");
m_connection.query_extensions();
if (m_conf.get<bool>(m_conf.bar_section(), "enable-ipc", false)) {
m_log.trace("controller: Create IPC handler");
m_ipc = configure_ipc().create<decltype(m_ipc)>();
} else {
m_log.warn("Inter-process communication support disabled");
}
// Listen for events on the root window to be able to
// break the blocking wait call when cleaning up
m_log.trace("controller: Listen for events on the root window");
@ -144,10 +151,10 @@ bool controller::run() {
install_sigmask();
install_confwatch();
#if DEBUG
// Start listening for ipc messages
m_threads.emplace_back(thread(&ipc::receive_messages, m_ipc.get()));
#endif
// Start ipc receiver if its enabled
if (m_conf.get<bool>(m_conf.bar_section(), "enable-ipc", false)) {
m_threads.emplace_back(thread(&ipc::receive_messages, m_ipc.get()));
}
// Listen for X events in separate thread
if (!m_writeback) {
@ -337,6 +344,10 @@ void controller::bootstrap_modules() {
}
for (auto& module_name : string_util::split(m_conf.get<string>(bs, confkey, ""), ' ')) {
if (module_name.empty()) {
continue;
}
try {
auto type = m_conf.get<string>("module/" + module_name, "type");
module_t module;
@ -375,7 +386,13 @@ void controller::bootstrap_modules() {
module.reset(new script_module(bar, m_log, m_conf, module_name));
else if (type == "custom/menu")
module.reset(new menu_module(bar, m_log, m_conf, module_name));
else
else if (type == "custom/ipc") {
if (!m_ipc)
throw application_error("Inter-process communication support needs to be enabled");
module.reset(new ipc_module(bar, m_log, m_conf, module_name));
m_ipc->attach_callback(
bind(&ipc_module::on_message, dynamic_cast<ipc_module*>(module.get()), placeholders::_1));
} else
throw application_error("Unknown module: " + module_name);
module->set_update_cb(

View File

@ -27,6 +27,20 @@ ipc::~ipc() {
}
}
/**
* Register listener callback for ipc_command messages
*/
void ipc::attach_callback(callback<const ipc_command&>&& cb) {
m_command_callbacks.emplace_back(cb);
}
/**
* Register listener callback for ipc_hook messages
*/
void ipc::attach_callback(callback<const ipc_hook&>&& cb) {
m_hook_callbacks.emplace_back(cb);
}
/**
* Start listening for event messages
*/
@ -50,18 +64,36 @@ void ipc::receive_messages() {
* Process received message and delegate
* valid events to the target modules
*/
void ipc::parse(string payload) {
void ipc::parse(const string& payload) const {
if (payload.empty()) {
return;
} else if (payload.find(message_internal::prefix) == 0) {
m_log.info("Received internal message: (payload=%s)", payload);
} else if (payload.find(message_command::prefix) == 0) {
m_log.info("Received command message: (payload=%s)", payload);
} else if (payload.find(message_custom::prefix) == 0) {
m_log.info("Received custom message: (payload=%s)", payload);
} else if (payload.find(ipc_command::prefix) == 0) {
delegate(ipc_command{payload});
} else if (payload.find(ipc_hook::prefix) == 0) {
delegate(ipc_hook{payload});
} else {
m_log.info("Received unknown message: (payload=%s)", payload);
m_log.warn("Received unknown ipc message: (payload=%s)", payload);
}
}
/**
* Send ipc message to attached listeners
*/
void ipc::delegate(const ipc_command& message) const {
if (!m_command_callbacks.empty())
for (auto&& callback : m_command_callbacks) callback(message);
else
m_log.warn("Unhandled message (payload=%s)", message.payload);
}
/**
* Send ipc message to attached listeners
*/
void ipc::delegate(const ipc_hook& message) const {
if (!m_hook_callbacks.empty())
for (auto&& callback : m_hook_callbacks) callback(message);
else
m_log.warn("Unhandled message (payload=%s)", message.payload);
}
LEMONBUDDY_NS_END

91
src/modules/ipc.cpp Normal file
View File

@ -0,0 +1,91 @@
#include "modules/ipc.hpp"
#include "components/ipc.hpp"
LEMONBUDDY_NS
namespace modules {
/**
* Load user-defined ipc hooks and
* create formatting tags
*/
void ipc_module::setup() {
size_t index = 0;
for (auto&& command : m_conf.get_list<string>(name(), "hook")) {
m_hooks.emplace_back(new hook{name() + to_string(++index), command});
}
if (m_hooks.empty()) {
throw module_error("No ipc hooks defined");
}
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", "");
m_formatter->add(DEFAULT_FORMAT, TAG_OUTPUT, {TAG_OUTPUT});
}
/**
* Wrap the output with defined mouse actions
*/
string ipc_module::get_output() {
if (!m_actions[mousebtn::LEFT].empty())
m_builder->cmd(mousebtn::LEFT, m_actions[mousebtn::LEFT]);
if (!m_actions[mousebtn::MIDDLE].empty())
m_builder->cmd(mousebtn::MIDDLE, m_actions[mousebtn::MIDDLE]);
if (!m_actions[mousebtn::RIGHT].empty())
m_builder->cmd(mousebtn::RIGHT, m_actions[mousebtn::RIGHT]);
if (!m_actions[mousebtn::SCROLL_UP].empty())
m_builder->cmd(mousebtn::SCROLL_UP, m_actions[mousebtn::SCROLL_UP]);
if (!m_actions[mousebtn::SCROLL_DOWN].empty())
m_builder->cmd(mousebtn::SCROLL_DOWN, m_actions[mousebtn::SCROLL_DOWN]);
m_builder->node(module::get_output());
return m_builder->flush();
}
/**
* Output content retrieved from hook commands
*/
bool ipc_module::build(builder* builder, string tag) const {
if (tag == TAG_OUTPUT)
builder->node(m_output);
else
return false;
return true;
}
/**
* Map received message hook to the ones
* configured from the user config and
* execute its command
*/
void ipc_module::on_message(const ipc_hook& message) {
bool match = false;
for (auto&& hook : m_hooks) {
if (ipc_hook::prefix + hook->payload != message.payload) {
continue;
}
match = true;
m_log.info("%s: Found matching hook (%s)", name(), hook->payload);
m_output.clear();
auto command = command_util::make_command(hook->command);
command->exec(false);
command->tail([this](string line) { m_output = line; });
}
if (match) {
broadcast();
}
}
}
LEMONBUDDY_NS_END