Files
polybar/src/components/controller.cpp
Chase Geigle 42fda5b105 feat(plugin): Add initial draft plugin architecture.
Modules and other features that require optional libraries are now
dynamically loaded on the construction of the controller via dlopen().
This allows polybar to be built with support for all of the features on
one machine, but gracefully fall back and disable those features on
machines where the required optional shared libraries are not found.
2018-09-20 15:50:51 -05:00

701 lines
19 KiB
C++

#include <csignal>
#include "components/bar.hpp"
#include "components/config.hpp"
#include "components/controller.hpp"
#include "components/ipc.hpp"
#include "components/logger.hpp"
#include "components/types.hpp"
#include "events/signal.hpp"
#include "events/signal_emitter.hpp"
#include "modules/ipc.hpp"
#include "modules/meta/event_handler.hpp"
#include "modules/meta/factory.hpp"
#include "modules/meta/input_handler.hpp"
#include "utils/command.hpp"
#include "utils/factory.hpp"
#include "utils/inotify.hpp"
#include "utils/plugin.hpp"
#include "utils/string.hpp"
#include "utils/time.hpp"
#include "x11/connection.hpp"
#include "x11/extensions/all.hpp"
#include "x11/types.hpp"
POLYBAR_NS
array<int, 2> g_eventpipe{{-1, -1}};
sig_atomic_t g_reload{0};
sig_atomic_t g_terminate{0};
void interrupt_handler(int signum) {
g_terminate = 1;
g_reload = (signum == SIGUSR1);
if (write(g_eventpipe[PIPE_WRITE], &g_terminate, 1) == -1) {
throw system_error("Failed to write to eventpipe");
}
}
/**
* Build controller instance
*/
controller::make_type controller::make(unique_ptr<ipc>&& ipc, unique_ptr<inotify_watch>&& config_watch) {
return factory_util::unique<controller>(connection::make(), signal_emitter::make(), logger::make(), config::make(),
bar::make(), forward<decltype(ipc)>(ipc), forward<decltype(config_watch)>(config_watch));
}
/**
* Construct controller
*/
controller::controller(connection& conn, signal_emitter& emitter, const logger& logger, const config& config,
unique_ptr<bar>&& bar, unique_ptr<ipc>&& ipc, unique_ptr<inotify_watch>&& confwatch)
: m_connection(conn)
, m_sig(emitter)
, m_log(logger)
, m_conf(config)
, m_bar(forward<decltype(bar)>(bar))
, m_ipc(forward<decltype(ipc)>(ipc))
, m_confwatch(forward<decltype(confwatch)>(confwatch)) {
m_swallow_input = m_conf.get("settings", "throttle-input-for", m_swallow_input);
m_swallow_limit = m_conf.deprecated("settings", "eventqueue-swallow", "throttle-output", m_swallow_limit);
m_swallow_update = m_conf.deprecated("settings", "eventqueue-swallow-time", "throttle-output-for", m_swallow_update);
if (pipe(g_eventpipe.data()) == 0) {
m_queuefd[PIPE_READ] = make_unique<file_descriptor>(g_eventpipe[PIPE_READ]);
m_queuefd[PIPE_WRITE] = make_unique<file_descriptor>(g_eventpipe[PIPE_WRITE]);
} else {
throw system_error("Failed to create event channel pipes");
}
m_log.trace("controller: Install signal handler");
struct sigaction act {};
memset(&act, 0, sizeof(act));
act.sa_handler = &interrupt_handler;
sigaction(SIGINT, &act, nullptr);
sigaction(SIGQUIT, &act, nullptr);
sigaction(SIGTERM, &act, nullptr);
sigaction(SIGUSR1, &act, nullptr);
sigaction(SIGALRM, &act, nullptr);
m_log.trace("controller: Load plugins");
for (const auto name : plugin_names) {
try {
m_plugins.emplace_back(name);
} catch (const application_error& err) {
m_log.warn("Failed to load plugin '%s': %s", name, err.what());
}
}
m_log.trace("controller: Setup user-defined modules");
size_t created_modules{0};
for (int i = 0; i < 3; i++) {
alignment align{static_cast<alignment>(i + 1)};
string configured_modules;
if (align == alignment::LEFT) {
configured_modules = m_conf.get(m_conf.section(), "modules-left", ""s);
} else if (align == alignment::CENTER) {
configured_modules = m_conf.get(m_conf.section(), "modules-center", ""s);
} else if (align == alignment::RIGHT) {
configured_modules = m_conf.get(m_conf.section(), "modules-right", ""s);
}
for (auto& module_name : string_util::split(configured_modules, ' ')) {
if (module_name.empty()) {
continue;
}
try {
auto type = m_conf.get("module/" + module_name, "type");
if (type == "custom/ipc" && !m_ipc) {
throw application_error("Inter-process messaging needs to be enabled");
}
m_modules[align].emplace_back(modules::make_module(move(type), m_bar->settings(), module_name));
created_modules++;
} catch (const runtime_error& err) {
m_log.err("Disabling module \"%s\" (reason: %s)", module_name, err.what());
}
}
}
if (!created_modules) {
throw application_error("No modules created");
}
}
/**
* Deconstruct controller
*/
controller::~controller() {
m_log.trace("controller: Uninstall sighandler");
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGTERM, SIG_DFL);
m_log.trace("controller: Detach signal receiver");
m_sig.detach(this);
m_log.trace("controller: Stop modules");
for (auto&& block : m_modules) {
for (auto&& module : block.second) {
auto module_name = module->name();
auto cleanup_ms = time_util::measure([&module] {
module->stop();
module.reset();
});
m_log.info("Deconstruction of %s took %lu ms.", module_name, cleanup_ms);
}
}
m_log.trace("controller: Joining threads");
for (auto&& t : m_threads) {
if (t.joinable()) {
t.join();
}
}
}
/**
* Run the main loop
*/
bool controller::run(bool writeback, string snapshot_dst) {
m_log.info("Starting application");
m_log.trace("controller: Main thread id = %i", concurrency_util::thread_id(this_thread::get_id()));
assert(!m_connection.connection_has_error());
m_writeback = writeback;
m_snapshot_dst = move(snapshot_dst);
m_sig.attach(this);
size_t started_modules{0};
for (const auto& block : m_modules) {
for (const auto& module : block.second) {
auto inp_handler = dynamic_cast<modules::input_handler*>(&*module);
auto evt_handler = dynamic_cast<modules::event_handler_interface*>(&*module);
if (inp_handler != nullptr) {
m_inputhandlers.emplace_back(inp_handler);
}
if (evt_handler != nullptr) {
evt_handler->connect(m_connection);
}
try {
m_log.info("Starting %s", module->name());
module->start();
started_modules++;
} catch (const application_error& err) {
m_log.err("Failed to start '%s' (reason: %s)", module->name(), err.what());
}
}
}
if (!started_modules) {
throw application_error("No modules started");
}
m_connection.flush();
m_event_thread = thread(&controller::process_eventqueue, this);
read_events();
if (m_event_thread.joinable()) {
enqueue(make_quit_evt(static_cast<bool>(g_reload)));
m_event_thread.join();
}
m_log.warn("Termination signal received, shutting down...");
return !g_reload;
}
/**
* Enqueue event
*/
bool controller::enqueue(event&& evt) {
if (!m_process_events && evt.type != event_type::QUIT) {
return false;
}
if (!m_queue.enqueue(forward<decltype(evt)>(evt))) {
m_log.warn("Failed to enqueue event");
return false;
}
// if (write(g_eventpipe[PIPE_WRITE], " ", 1) == -1) {
// m_log.err("Failed to write to eventpipe (reason: %s)", strerror(errno));
// }
return true;
}
/**
* Enqueue input data
*/
bool controller::enqueue(string&& input_data) {
if (!m_inputdata.empty()) {
m_log.trace("controller: Swallowing input event (pending data)");
} else if (chrono::system_clock::now() - m_swallow_input < m_lastinput) {
m_log.trace("controller: Swallowing input event (throttled)");
} else {
m_inputdata = forward<string>(input_data);
return enqueue(make_input_evt());
}
return false;
}
/**
* Read events from configured file descriptors
*/
void controller::read_events() {
m_log.info("Entering event loop (thread-id=%lu)", this_thread::get_id());
int fd_connection{-1};
int fd_confwatch{-1};
int fd_ipc{-1};
vector<int> fds;
fds.emplace_back(*m_queuefd[PIPE_READ]);
fds.emplace_back((fd_connection = m_connection.get_file_descriptor()));
if (m_confwatch) {
m_log.trace("controller: Attach config watch");
m_confwatch->attach(IN_MODIFY | IN_IGNORED);
fds.emplace_back((fd_confwatch = m_confwatch->get_file_descriptor()));
}
if (m_ipc) {
fds.emplace_back((fd_ipc = m_ipc->get_file_descriptor()));
}
while (!g_terminate) {
fd_set readfds{};
FD_ZERO(&readfds);
int maxfd{0};
for (auto&& fd : fds) {
FD_SET(fd, &readfds);
maxfd = std::max(maxfd, fd);
}
// Wait until event is ready on one of the configured streams
int events = select(maxfd + 1, &readfds, nullptr, nullptr, nullptr);
// Check for errors
if (events == -1) {
/*
* The Interrupt errno is generated when polybar is stopped, so it
* shouldn't generate an error message
*/
if (errno != EINTR) {
m_log.err("select failed in event loop: %s", strerror(errno));
}
break;
}
if (g_terminate || m_connection.connection_has_error()) {
break;
}
// Process event on the internal fd
if (m_queuefd[PIPE_READ] && FD_ISSET(static_cast<int>(*m_queuefd[PIPE_READ]), &readfds)) {
char buffer[BUFSIZ];
if (read(static_cast<int>(*m_queuefd[PIPE_READ]), &buffer, BUFSIZ) == -1) {
m_log.err("Failed to read from eventpipe (err: %s)", strerror(errno));
}
}
// Process event on the config inotify watch fd
unique_ptr<inotify_event> confevent;
if (fd_confwatch > -1 && FD_ISSET(fd_confwatch, &readfds) && (confevent = m_confwatch->await_match())) {
if (confevent->mask & IN_IGNORED) {
// IN_IGNORED: file was deleted or filesystem was unmounted
//
// This happens in some configurations of vim when a file is saved,
// since it is not actually issuing calls to write() but rather
// moves a file into the original's place after moving the original
// file to a different location (and subsequently deleting it).
//
// We need to re-attach the watch to the new file in this case.
fds.erase(std::remove_if(fds.begin(), fds.end(), [fd_confwatch](int fd) { return fd == fd_confwatch; }), fds.end());
m_confwatch = inotify_util::make_watch(m_confwatch->path());
m_confwatch->attach(IN_MODIFY | IN_IGNORED);
fds.emplace_back((fd_confwatch = m_confwatch->get_file_descriptor()));
}
m_log.info("Configuration file changed");
g_terminate = 1;
g_reload = 1;
}
// Process event on the xcb connection fd
if (fd_connection > -1 && FD_ISSET(fd_connection, &readfds)) {
shared_ptr<xcb_generic_event_t> evt{};
while ((evt = shared_ptr<xcb_generic_event_t>(xcb_poll_for_event(m_connection), free)) != nullptr) {
try {
m_connection.dispatch_event(evt);
} catch (xpp::connection_error& err) {
m_log.err("X connection error, terminating... (what: %s)", m_connection.error_str(err.code()));
} catch (const exception& err) {
m_log.err("Error in X event loop: %s", err.what());
}
}
}
// Process event on the ipc fd
if (fd_ipc > -1 && FD_ISSET(fd_ipc, &readfds)) {
m_ipc->receive_message();
fds.erase(std::remove_if(fds.begin(), fds.end(), [fd_ipc](int fd) { return fd == fd_ipc; }), fds.end());
fds.emplace_back((fd_ipc = m_ipc->get_file_descriptor()));
}
}
}
/**
* Eventqueue worker loop
*/
void controller::process_eventqueue() {
m_log.info("Eventqueue worker (thread-id=%lu)", this_thread::get_id());
if (!m_writeback) {
m_sig.emit(signals::eventqueue::start{});
} else {
// bypass the start eventqueue signal
m_sig.emit(signals::ui::ready{});
}
while (!g_terminate) {
event evt{};
m_queue.wait_dequeue(evt);
if (g_terminate) {
break;
} else if (evt.type == event_type::QUIT) {
if (evt.flag) {
on(signals::eventqueue::exit_reload{});
} else {
on(signals::eventqueue::exit_terminate{});
}
} else if (evt.type == event_type::INPUT) {
process_inputdata();
} else if (evt.type == event_type::UPDATE && evt.flag) {
process_update(true);
} else {
event next{};
size_t swallowed{0};
while (swallowed++ < m_swallow_limit && m_queue.wait_dequeue_timed(next, m_swallow_update)) {
if (next.type == event_type::QUIT) {
evt = next;
break;
} else if (next.type == event_type::INPUT) {
evt = next;
break;
} else if (evt.type != next.type) {
enqueue(move(next));
break;
} else {
m_log.trace_x("controller: Swallowing event within timeframe");
evt = next;
}
}
if (evt.type == event_type::UPDATE) {
process_update(evt.flag);
} else if (evt.type == event_type::INPUT) {
process_inputdata();
} else if (evt.type == event_type::QUIT) {
if (evt.flag) {
on(signals::eventqueue::exit_reload{});
} else {
on(signals::eventqueue::exit_terminate{});
}
} else if (evt.type == event_type::CHECK) {
on(signals::eventqueue::check_state{});
} else {
m_log.warn("Unknown event type for enqueued event (%d)", evt.type);
}
}
}
}
/**
* Process stored input data
*/
void controller::process_inputdata() {
if (!m_inputdata.empty()) {
string cmd = m_inputdata;
m_lastinput = chrono::time_point_cast<decltype(m_swallow_input)>(chrono::system_clock::now());
m_inputdata.clear();
for (auto&& handler : m_inputhandlers) {
if (handler->input(string{cmd})) {
return;
}
}
try {
m_log.info("Uncaught input event, forwarding to shell... (input: %s)", cmd);
if (m_command) {
m_log.warn("Terminating previous shell command");
m_command->terminate();
}
m_log.info("Executing shell command: %s", cmd);
m_command = command_util::make_command(move(cmd));
m_command->exec();
m_command.reset();
process_update(true);
} catch (const application_error& err) {
m_log.err("controller: Error while forwarding input to shell -> %s", err.what());
}
}
}
/**
* Process eventqueue update event
*/
bool controller::process_update(bool force) {
const bar_settings& bar{m_bar->settings()};
string contents;
string separator{bar.separator};
string padding_left(bar.padding.left, ' ');
string padding_right(bar.padding.right, ' ');
string margin_left(bar.module_margin.left, ' ');
string margin_right(bar.module_margin.right, ' ');
for (const auto& block : m_modules) {
string block_contents;
bool is_left = false;
bool is_center = false;
bool is_right = false;
bool is_first = true;
if (block.first == alignment::LEFT) {
is_left = true;
} else if (block.first == alignment::CENTER) {
is_center = true;
} else if (block.first == alignment::RIGHT) {
is_right = true;
}
for (const auto& module : block.second) {
if (!module->running()) {
continue;
}
string module_contents;
try {
module_contents = module->contents();
} catch (const exception& err) {
m_log.err("Failed to get contents for \"%s\" (err: %s)", module->name(), err.what());
}
if (module_contents.empty()) {
continue;
}
if (!block_contents.empty() && !margin_right.empty()) {
block_contents += margin_right;
}
if (!block_contents.empty() && !separator.empty()) {
block_contents += separator;
}
if (!block_contents.empty() && !margin_left.empty() && !(is_left && is_first)) {
block_contents += margin_left;
}
block_contents.reserve(module_contents.size());
block_contents += module_contents;
is_first = false;
}
if (block_contents.empty()) {
continue;
} else if (is_left) {
contents += "%{l}";
contents += padding_left;
} else if (is_center) {
contents += "%{c}";
} else if (is_right) {
contents += "%{r}";
block_contents += padding_right;
}
// Strip unnecessary reset tags
block_contents = string_util::replace_all(block_contents, "T-}%{T", "T");
block_contents = string_util::replace_all(block_contents, "B-}%{B#", "B#");
block_contents = string_util::replace_all(block_contents, "F-}%{F#", "F#");
block_contents = string_util::replace_all(block_contents, "U-}%{U#", "U#");
block_contents = string_util::replace_all(block_contents, "u-}%{u#", "u#");
block_contents = string_util::replace_all(block_contents, "o-}%{o#", "o#");
// Join consecutive tags
contents += string_util::replace_all(block_contents, "}%{", " ");
}
try {
if (!m_writeback) {
m_bar->parse(move(contents), force);
} else {
std::cout << contents << std::endl;
}
} catch (const exception& err) {
m_log.err("Failed to update bar contents (reason: %s)", err.what());
}
return true;
}
/**
* Process broadcast events
*/
bool controller::on(const signals::eventqueue::notify_change&) {
return enqueue(make_update_evt(false));
}
/**
* Process forced broadcast events
*/
bool controller::on(const signals::eventqueue::notify_forcechange&) {
return enqueue(make_update_evt(true));
}
/**
* Process eventqueue terminate event
*/
bool controller::on(const signals::eventqueue::exit_terminate&) {
raise(SIGALRM);
return true;
}
/**
* Process eventqueue reload event
*/
bool controller::on(const signals::eventqueue::exit_reload&) {
raise(SIGUSR1);
return true;
}
/**
* Process eventqueue check event
*/
bool controller::on(const signals::eventqueue::check_state&) {
for (const auto& block : m_modules) {
for (const auto& module : block.second) {
if (module->running()) {
return true;
}
}
}
m_log.warn("No running modules...");
on(signals::eventqueue::exit_terminate{});
return true;
}
/**
* Process ui ready event
*/
bool controller::on(const signals::ui::ready&) {
m_process_events = true;
enqueue(make_update_evt(true));
if (!m_snapshot_dst.empty()) {
m_threads.emplace_back(thread([&] {
this_thread::sleep_for(3s);
m_sig.emit(signals::ui::request_snapshot{move(m_snapshot_dst)});
enqueue(make_update_evt(true));
}));
}
// let the event bubble
return false;
}
/**
* Process ui button press event
*/
bool controller::on(const signals::ui::button_press& evt) {
string input{evt.cast()};
if (input.empty()) {
m_log.err("Cannot enqueue empty input");
return false;
}
enqueue(move(input));
return true;
}
/**
* Process ipc action messages
*/
bool controller::on(const signals::ipc::action& evt) {
string action{evt.cast()};
if (action.empty()) {
m_log.err("Cannot enqueue empty ipc action");
return false;
}
m_log.info("Enqueuing ipc action: %s", action);
enqueue(move(action));
return true;
}
/**
* Process ipc command messages
*/
bool controller::on(const signals::ipc::command& evt) {
string command{evt.cast()};
if (command.empty()) {
return false;
}
if (command == "quit") {
enqueue(make_quit_evt(false));
} else if (command == "restart") {
enqueue(make_quit_evt(true));
} else if (command == "hide") {
m_bar->hide();
} else if (command == "show") {
m_bar->show();
} else if (command == "toggle") {
m_bar->toggle();
} else {
m_log.warn("\"%s\" is not a valid ipc command", command);
}
return true;
}
/**
* Process ipc hook messages
*/
bool controller::on(const signals::ipc::hook& evt) {
string hook{evt.cast()};
for (const auto& block : m_modules) {
for (const auto& module : block.second) {
if (!module->running()) {
continue;
}
auto ipc = dynamic_cast<modules::ipc_module*>(module.get());
if (ipc != nullptr) {
ipc->on_message(hook);
}
}
}
return true;
}
POLYBAR_NS_END