diff options
| author | Joel Klinghed <the_jk@spawned.biz> | 2021-01-27 22:06:49 +0100 |
|---|---|---|
| committer | Joel Klinghed <the_jk@spawned.biz> | 2021-01-27 22:06:49 +0100 |
| commit | 06950aab233de6a2f47293d59575bb42f6131660 (patch) | |
| tree | 62f6eed4a6d35414f656d22b9ac7420849018a11 /src/timer_state.cc | |
| parent | 1ef9c463f1efc1adfb62e42ab3dd17e8c6394373 (diff) | |
Complete rewrite using C++ and with shared state support
Diffstat (limited to 'src/timer_state.cc')
| -rw-r--r-- | src/timer_state.cc | 419 |
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; +} |
