#include "common.hh" #include "io.hh" #include "timer_state.hh" #include "unique_fd.hh" #include #include #include #include #include #include #include #include #include #include #include namespace { const sdbus::ServiceName kServiceName{"org.the_jk.timer"}; const sdbus::ObjectPath kObjectPath{"/org/the_jk/timer/state"}; const sdbus::InterfaceName 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 conn, TimerState::Delegate* delegate) : conn_(std::move(conn)), delegate_(delegate) { } std::shared_ptr conn_; TimerState::Delegate* const delegate_; }; class TimerStateClient : public TimerStateImpl { public: TimerStateClient(std::shared_ptr 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, int64_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(); }); dbus_proxy_ = sdbus::createProxy(*conn_.get(), sdbus::ServiceName{"org.freedesktop.DBus"}, sdbus::ObjectPath{"/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(); } }); 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 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, sdbus::MethodName{"get_state"}); auto reply = proxy_->callMethod(std::move(method)); bool active; uint32_t total; int64_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 proxy_; std::unique_ptr dbus_proxy_; }; class TimerStateServer : public TimerStateImpl { public: TimerStateServer(std::shared_ptr 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( static_cast(total_.count()), static_cast(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::system_clock::now() - start_); write_state(); try { object_->emitSignal("stopped").onInterface(kInterfaceName).withArguments( static_cast(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); auto start = [this]() { this->start(); }; auto stop = [this]() { this->stop(); }; auto reset = [this]() { this->reset(); }; auto get_state = std::bind(&TimerStateServer::get_state, this, std::placeholders::_1); object->addVTable( sdbus::registerMethod("start").implementedAs(std::move(start)).withNoReply(), sdbus::registerMethod("stop").implementedAs(std::move(stop)).withNoReply(), sdbus::registerMethod("reset").implementedAs(std::move(reset)).withNoReply(), sdbus::MethodVTableItem{ sdbus::MethodName{"get_state"}, sdbus::Signature{""}, {}, sdbus::Signature{"bux"}, {}, get_state, {} }, sdbus::registerSignal("started").withParameters(), sdbus::registerSignal("stopped").withParameters(), sdbus::registerSignal("reset") ).forInterface(kInterfaceName); 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(total_.count()); if (active_) { reply << static_cast( std::chrono::system_clock::to_time_t(start_)); } else { reply << static_cast(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 (data.empty()) { // Newly created file. active_ = false; total_ = std::chrono::minutes::zero(); } else { 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(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 start_; std::unique_ptr 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 conn( sdbus::createSessionBusConnection()); try { conn->requestName(kServiceName); auto server = std::make_unique(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(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 impl_; std::thread thread_; }; } // namespace std::unique_ptr TimerState::create(std::filesystem::path state_file, Delegate* delegate) { auto state = std::make_unique(delegate); return state->init(std::move(state_file)) ? std::move(state) : nullptr; }