#include "common.hh" #include "io.hh" #include "timer_state.hh" #include "unique_fd.hh" #include #include #include #include #include #include #include #include #include #include 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 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, 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 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 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( 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::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 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 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(); object->registerSignal("stopped").onInterface(kInterfaceName) .withParameters(); 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(total_.count()); if (active_) { reply << 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 (!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; }