summaryrefslogtreecommitdiff
path: root/src/timer_state.cc
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2021-01-27 22:06:49 +0100
committerJoel Klinghed <the_jk@spawned.biz>2021-01-27 22:06:49 +0100
commit06950aab233de6a2f47293d59575bb42f6131660 (patch)
tree62f6eed4a6d35414f656d22b9ac7420849018a11 /src/timer_state.cc
parent1ef9c463f1efc1adfb62e42ab3dd17e8c6394373 (diff)
Complete rewrite using C++ and with shared state support
Diffstat (limited to 'src/timer_state.cc')
-rw-r--r--src/timer_state.cc419
1 files changed, 419 insertions, 0 deletions
diff --git a/src/timer_state.cc b/src/timer_state.cc
new file mode 100644
index 0000000..770bbad
--- /dev/null
+++ b/src/timer_state.cc
@@ -0,0 +1,419 @@
+#include "common.hh"
+
+#include "io.hh"
+#include "timer_state.hh"
+#include "unique_fd.hh"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <functional>
+#include <iostream>
+#include <sdbus-c++/sdbus-c++.h>
+#include <string.h>
+#include <sys/file.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <thread>
+
+namespace {
+
+constexpr char kServiceName[] = "org.the_jk.timer";
+constexpr char kObjectPath[] = "/org/the_jk/timer/state";
+constexpr char kInterfaceName[] = "org.the_jk.timer.State";
+
+class TimerStateImpl {
+public:
+ virtual ~TimerStateImpl() = default;
+
+ virtual void start() = 0;
+ virtual void stop() = 0;
+ virtual void reset() = 0;
+
+ virtual void enterLoop() {
+ conn_->enterEventLoop();
+ }
+
+ virtual void leaveLoop() {
+ conn_->leaveEventLoop();
+ }
+
+protected:
+ TimerStateImpl(std::shared_ptr<sdbus::IConnection> conn,
+ TimerState::Delegate* delegate)
+ : conn_(std::move(conn)), delegate_(delegate) {
+ }
+
+ std::shared_ptr<sdbus::IConnection> conn_;
+ TimerState::Delegate* const delegate_;
+};
+
+class TimerStateClient : public TimerStateImpl {
+public:
+ TimerStateClient(std::shared_ptr<sdbus::IConnection> conn,
+ TimerState::Delegate* delegate)
+ : TimerStateImpl(std::move(conn), delegate) {}
+
+ void start() override {
+ try {
+ proxy_->callMethod("start").onInterface(kInterfaceName).dontExpectReply();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to call start: " << err.what() << std::endl;
+ }
+ }
+
+ void stop() override {
+ try {
+ proxy_->callMethod("stop").onInterface(kInterfaceName).dontExpectReply();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to call stop: " << err.what() << std::endl;
+ }
+ }
+
+ void reset() override {
+ try {
+ proxy_->callMethod("reset").onInterface(kInterfaceName).dontExpectReply();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to call reset: " << err.what() << std::endl;
+ }
+ }
+
+ bool init() {
+ try {
+ proxy_ = sdbus::createProxy(*conn_.get(), kServiceName, kObjectPath);
+ proxy_->uponSignal("started").onInterface(kInterfaceName)
+ .call([this](uint32_t total, time_t epoch){
+ signal_started(std::chrono::minutes(total),
+ std::chrono::system_clock::from_time_t(epoch));
+ });
+ proxy_->uponSignal("stopped").onInterface(kInterfaceName)
+ .call([this](uint32_t total){
+ signal_stopped(std::chrono::minutes(total));
+ });
+ proxy_->uponSignal("reset").onInterface(kInterfaceName)
+ .call([this](){ signal_reset(); });
+ proxy_->finishRegistration();
+
+ dbus_proxy_ = sdbus::createProxy(*conn_.get(), "org.freedesktop.DBus",
+ "/org/freedesktop/DBus");
+ dbus_proxy_->uponSignal("NameOwnerChanged")
+ .onInterface("org.freedesktop.DBus")
+ .call([this](const std::string& name,
+ const std::string& /* old_owner */,
+ const std::string& new_owner) {
+ if (name == kServiceName && new_owner.empty()) {
+ signal_restart();
+ }
+ });
+ dbus_proxy_->finishRegistration();
+
+ sync_state();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to init client: " << err.what() << std::endl;
+ return false;
+ }
+
+ return true;
+ }
+
+private:
+ void signal_started(
+ std::chrono::minutes total,
+ std::chrono::time_point<std::chrono::system_clock> epoch) {
+ delegate_->start(total, epoch);
+ }
+
+ void signal_stopped(std::chrono::minutes total) {
+ delegate_->stop(total);
+ }
+
+ void signal_reset() {
+ delegate_->reset();
+ }
+
+ void signal_restart() {
+ delegate_->restart();
+ }
+
+ void sync_state() {
+ auto method = proxy_->createMethodCall(kInterfaceName, "get_state");
+ auto reply = proxy_->callMethod(std::move(method));
+ bool active;
+ uint32_t total;
+ time_t epoch;
+ reply >> active;
+ reply >> total;
+ reply >> epoch;
+ if (active) {
+ delegate_->start(std::chrono::minutes(total),
+ std::chrono::system_clock::from_time_t(epoch));
+ } else {
+ delegate_->stop(std::chrono::minutes(total));
+ }
+ }
+
+ std::unique_ptr<sdbus::IProxy> proxy_;
+ std::unique_ptr<sdbus::IProxy> dbus_proxy_;
+};
+
+class TimerStateServer : public TimerStateImpl {
+public:
+ TimerStateServer(std::shared_ptr<sdbus::IConnection> conn,
+ TimerState::Delegate* delegate)
+ : TimerStateImpl(std::move(conn), delegate) {}
+
+ void start() override {
+ if (active_) return;
+ active_ = true;
+ start_ = std::chrono::system_clock::now();
+ write_state();
+
+ try {
+ object_->emitSignal("started").onInterface(kInterfaceName).withArguments(
+ total_.count(), std::chrono::system_clock::to_time_t(start_));
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to emit started: " << err.what() << std::endl;
+ }
+ delegate_->start(total_, start_);
+ }
+
+ void stop() override {
+ if (!active_) return;
+ active_ = false;
+ total_ +=
+ std::chrono::duration_cast<std::chrono::minutes>(
+ std::chrono::system_clock::now() - start_);
+ write_state();
+
+ try {
+ object_->emitSignal("stopped").onInterface(kInterfaceName).withArguments(
+ total_.count());
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to emit started: " << err.what() << std::endl;
+ }
+ delegate_->stop(total_);
+ }
+
+ void reset() override {
+ if (active_) return;
+ total_ = std::chrono::minutes::zero();
+ write_state();
+
+ try {
+ object_->emitSignal("reset").onInterface(kInterfaceName);
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to emit reset: " << err.what() << std::endl;
+ }
+ delegate_->reset();
+ }
+
+ bool init(std::filesystem::path state_file) {
+ if (!load_state(state_file))
+ return false;
+
+ try {
+ auto object = sdbus::createObject(*conn_.get(), kObjectPath);
+ std::function<void()> fun = std::bind(&TimerStateServer::start, this);
+ object->registerMethod("start").onInterface(kInterfaceName)
+ .implementedAs(fun).withNoReply();
+ fun = std::bind(&TimerStateServer::stop, this);
+ object->registerMethod("stop").onInterface(kInterfaceName)
+ .implementedAs(fun).withNoReply();
+ fun = std::bind(&TimerStateServer::reset, this);
+ object->registerMethod("reset").onInterface(kInterfaceName)
+ .implementedAs(fun).withNoReply();
+ std::function<void(sdbus::MethodCall)> call_fun =
+ std::bind(&TimerStateServer::get_state, this, std::placeholders::_1);
+ object->registerMethod(kInterfaceName, "get_state", "", "bdd", call_fun);
+ object->registerSignal("started").onInterface(kInterfaceName)
+ .withParameters<uint32_t, time_t>();
+ object->registerSignal("stopped").onInterface(kInterfaceName)
+ .withParameters<uint32_t>();
+ object->registerSignal("reset").onInterface(kInterfaceName);
+ object->finishRegistration();
+
+ object_ = std::move(object);
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to init server: " << err.what() << std::endl;
+ return false;
+ }
+
+ if (active_) {
+ delegate_->start(total_, start_);
+ } else {
+ delegate_->stop(total_);
+ }
+
+ return true;
+ }
+
+private:
+ void get_state(sdbus::MethodCall call) {
+ try {
+ auto reply = call.createReply();
+ reply << active_;
+ reply << static_cast<uint32_t>(total_.count());
+ if (active_) {
+ reply << std::chrono::system_clock::to_time_t(start_);
+ } else {
+ reply << static_cast<time_t>(0);
+ }
+ reply.send();
+ } catch (sdbus::Error const& err) {
+ std::cerr << "Failed to reply to get_state: " << err.what() << std::endl;
+ }
+ }
+
+ bool load_state(std::filesystem::path state_file) {
+ fd_.reset(open(state_file.c_str(), O_RDWR | O_CREAT, S_IRWXU));
+ if (!fd_) {
+ std::cerr << "Unable to open or create " << state_file
+ << " for reading and writing." << std::endl;
+ return false;
+ }
+ if (flock(fd_.get(), LOCK_EX | LOCK_NB)) {
+ std::cerr << "Unable to get exclusive lock on " << state_file
+ << ": " << strerror(errno) << std::endl;
+ return false;
+ }
+ std::string data;
+ if (!io::read_all(fd_.get(), &data)) {
+ std::cerr << "Error reading " << state_file
+ << ": " << strerror(errno) << std::endl;
+ return false;
+ }
+ if (!parse_state(std::move(data))) {
+ std::cerr << "Invalid data in state " << state_file << "." << std::endl;
+ return false;
+ }
+ return true;
+ }
+
+ bool parse_state(std::string data) {
+ try {
+ size_t end;
+ auto active = std::stol(data, &end);
+ if (end == data.size() || data[end] != '|')
+ return false;
+ data = data.substr(end + 1);
+ auto total = std::stoul(data, &end);
+ if (end == data.size() || data[end] != '|')
+ return false;
+ struct tm tm;
+ auto* endp = strptime(
+ data.substr(end + 1).c_str(), "%Y-%m-%d %H:%M:%S", &tm);
+ if (!endp || (*endp != '\0' && *endp != '\n'))
+ return false;
+
+ active_ = active == 1;
+ total_ = std::chrono::minutes(total);
+ start_ = std::chrono::system_clock::from_time_t(timegm(&tm));
+
+ return true;
+ } catch (std::exception const& e) {
+ return false;
+ }
+ }
+
+ bool write_state() {
+ std::string data =
+ std::to_string(active_ ? 1L : -1L) + '|' + std::to_string(
+ std::chrono::duration_cast<std::chrono::minutes>(total_).count())
+ + '|';
+ char tmp[50];
+ auto time = std::chrono::system_clock::to_time_t(start_);
+ auto len = strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S", gmtime(&time));
+ if (len == 0 || len == sizeof(tmp)) {
+ std::cerr << "Failed to store state: invalid time." << std::endl;
+ return false;
+ }
+ data.append(tmp, len);
+ data.push_back('\n');
+ if (lseek(fd_.get(), 0, SEEK_SET) ||
+ !io::write_all(fd_.get(), data) ||
+ ftruncate(fd_.get(), data.size())) {
+ std::cerr << "Failed to store state: " << strerror(errno) << std::endl;
+ return false;
+ }
+ return true;
+ }
+
+ bool active_{false};
+ std::chrono::minutes total_{0};
+ std::chrono::time_point<std::chrono::system_clock> start_;
+ std::unique_ptr<sdbus::IObject> object_;
+ unique_fd fd_;
+};
+
+class TimerStateWrapper : public TimerState {
+public:
+ explicit TimerStateWrapper(Delegate* delegate)
+ : delegate_(delegate) {}
+
+ ~TimerStateWrapper() override {
+ if (impl_) impl_->leaveLoop();
+ thread_.join();
+ }
+
+ bool init(std::filesystem::path state_file) {
+ try {
+ std::shared_ptr<sdbus::IConnection> conn(
+ sdbus::createSessionBusConnection());
+
+ try {
+ conn->requestName(kServiceName);
+
+ auto server = std::make_unique<TimerStateServer>(conn, delegate_);
+ if (server->init(std::move(state_file))) {
+ impl_ = std::move(server);
+ return post_init();
+ }
+ // If server fails to init in any way, try client as backup.
+ } catch (sdbus::Error const& e) {
+ // This is here to catch requestName call, if requestName fails there
+ // is a server running.
+ }
+
+ auto client = std::make_unique<TimerStateClient>(conn, delegate_);
+ if (client->init()) {
+ impl_ = std::move(client);
+ return post_init();
+ }
+ } catch (sdbus::Error const& e) {
+ }
+ return false;
+ }
+
+ void start() override {
+ impl_->start();
+ }
+
+ void stop() override {
+ impl_->stop();
+ }
+
+ void reset() override {
+ impl_->reset();
+ }
+
+private:
+ bool post_init() {
+ thread_ = std::thread(&TimerStateWrapper::run_impl, this);
+ return true;
+ }
+
+ void run_impl() {
+ impl_->enterLoop();
+ }
+
+ Delegate* const delegate_;
+ std::unique_ptr<TimerStateImpl> impl_;
+ std::thread thread_;
+};
+
+} // namespace
+
+std::unique_ptr<TimerState> TimerState::create(std::filesystem::path state_file,
+ Delegate* delegate) {
+ auto state = std::make_unique<TimerStateWrapper>(delegate);
+ return state->init(std::move(state_file)) ? std::move(state) : nullptr;
+}