diff --git a/client/client_settings.hpp b/client/client_settings.hpp index 877216c2..a99275c3 100644 --- a/client/client_settings.hpp +++ b/client/client_settings.hpp @@ -24,6 +24,7 @@ // standard headers #include +#include @@ -73,10 +74,24 @@ struct ClientSettings std::string filter{"*:info"}; }; + struct SystemInfo + { + enum class Mode + { + file, + script, + none + }; + Mode mode{Mode::none}; + std::string path; + int interval_secs; + }; + size_t instance{1}; std::string host_id; Server server; Player player; Logging logging; + SystemInfo systemInfo; }; diff --git a/client/controller.cpp b/client/controller.cpp index 08be149c..4503df6f 100644 --- a/client/controller.cpp +++ b/client/controller.cpp @@ -64,20 +64,25 @@ #include "common/snap_exception.hpp" #include "time_provider.hpp" +#include + // standard headers #include #include #include #include +#include using namespace std; using namespace player; +namespace bp = boost::process; + static constexpr auto LOG_TAG = "Controller"; static constexpr auto TIME_SYNC_INTERVAL = 1s; Controller::Controller(boost::asio::io_context& io_context, const ClientSettings& settings) //, std::unique_ptr meta) - : io_context_(io_context), timer_(io_context), settings_(settings), stream_(nullptr), decoder_(nullptr), player_(nullptr), + : io_context_(io_context), timer_(io_context), systemInfoTimer_(io_context), settings_(settings), stream_(nullptr), decoder_(nullptr), player_(nullptr), serverSettings_(nullptr) // meta_(std::move(meta)), { } @@ -304,6 +309,90 @@ void Controller::sendTimeSyncMessage(int quick_syncs) }); } +void Controller::sendSystemInfoMessage() +{ + auto data = getSystemInfo(); + if (data != nullptr) + { + auto sysInfo = std::make_shared(); + sysInfo->msg = data; + + LOG(TRACE, LOG_TAG) << "Sending system info: " << sysInfo->msg.dump() << "\n"; + clientConnection_->send(sysInfo, + [this](const boost::system::error_code& ec) + { + if (ec) + { + LOG(ERROR, LOG_TAG) << "Failed to send client system info, error: " << ec.message() << "\n"; + reconnect(); + return; + } + }); + } + auto interval = std::chrono::seconds(settings_.systemInfo.interval_secs); + systemInfoTimer_.expires_after(interval); + systemInfoTimer_.async_wait( + [this](const boost::system::error_code& ec) + { + if (!ec) + { + sendSystemInfoMessage(); + } + }); +} + +json Controller::getSystemInfo() +{ + + if (settings_.systemInfo.mode == ClientSettings::SystemInfo::Mode::script) + { + try + { + bp::ipstream istream; + auto ret = bp::system(settings_.systemInfo.path, bp::std_out > istream); + if (ret != 0) + { + LOG(ERROR, LOG_TAG) + << "System info process returned with exit code " + << ret << std::endl; + return nullptr; + } + return json::parse(istream); + } + catch (const std::exception& e) + { + LOG(ERROR, LOG_TAG) + << "Unable to read system info from process (" + << settings_.systemInfo.path + << "): " + << e.what() << std::endl; + return nullptr; + } + } + + if (settings_.systemInfo.mode == ClientSettings::SystemInfo::Mode::file) + { + try + { + ifstream fJson(settings_.systemInfo.path); + stringstream buffer; + buffer << fJson.rdbuf(); + return json::parse(buffer.str()); + } + catch (const std::exception& e) + { + LOG(ERROR, LOG_TAG) + << "Unable to read system info file (" + << settings_.systemInfo.path + << "): " + << e.what() << std::endl; + return nullptr; + } + } + + return nullptr; +} + void Controller::browseMdns(const MdnsHandler& handler) { #if defined(HAS_AVAHI) || defined(HAS_BONJOUR) @@ -382,6 +471,7 @@ void Controller::start() void Controller::reconnect() { timer_.cancel(); + systemInfoTimer_.cancel(); clientConnection_->disconnect(); player_.reset(); stream_.reset(); @@ -431,6 +521,11 @@ void Controller::worker() // Do initial time sync with the server sendTimeSyncMessage(50); + // start system info updates, if configured + if (settings_.systemInfo.mode != ClientSettings::SystemInfo::Mode::none) + { + sendSystemInfoMessage(); + } // Start receiver loop getNextMessage(); } diff --git a/client/controller.hpp b/client/controller.hpp index 27299bf1..a1ece4f5 100644 --- a/client/controller.hpp +++ b/client/controller.hpp @@ -59,9 +59,12 @@ class Controller void getNextMessage(); void sendTimeSyncMessage(int quick_syncs); + void sendSystemInfoMessage(); + json getSystemInfo(); boost::asio::io_context& io_context_; boost::asio::steady_timer timer_; + boost::asio::steady_timer systemInfoTimer_; ClientSettings settings_; SampleFormat sampleFormat_; std::unique_ptr clientConnection_; diff --git a/client/snapclient.cpp b/client/snapclient.cpp index f05b328d..40f16e47 100644 --- a/client/snapclient.cpp +++ b/client/snapclient.cpp @@ -179,6 +179,10 @@ int main(int argc, char** argv) #endif mixer_mode = op.add>("", "mixer", mixers + "|none|?[:]", "software"); + // system info + auto sysInfo = op.add>("", "sysinfo", "source for JSON-formatted system infomation [file:,script:]", "none"); + op.add>("", "sysinfo-interval", "interval in seconds between system info updates", 10, &settings.systemInfo.interval_secs); + // daemon settings #ifdef HAS_DAEMON int processPriority(-3); @@ -427,6 +431,22 @@ int main(int argc, char** argv) else throw SnapException("Mixer mode not supported: " + mode); + string sysInfoMode = utils::string::split_left(sysInfo->value(), ':', settings.systemInfo.path); + if (sysInfoMode == "none") + settings.systemInfo.mode = ClientSettings::SystemInfo::Mode::none; + else if (sysInfoMode == "file") + settings.systemInfo.mode = ClientSettings::SystemInfo::Mode::file; + else if (sysInfoMode == "script") + settings.systemInfo.mode = ClientSettings::SystemInfo::Mode::script; + else + throw SnapException("System info mode not supported: " + sysInfoMode); + + if (settings.systemInfo.interval_secs < 1) + { + LOG(ERROR, LOG_TAG) << "System info interval too low, setting to default value (10 seconds)\n"; + settings.systemInfo.interval_secs = 10; + } + boost::asio::io_context io_context; // Construct a signal set registered for process termination. boost::asio::signal_set signals(io_context, SIGHUP, SIGINT, SIGTERM); diff --git a/common/message/client_system_info.hpp b/common/message/client_system_info.hpp new file mode 100644 index 00000000..992a2fcb --- /dev/null +++ b/common/message/client_system_info.hpp @@ -0,0 +1,45 @@ +/*** + This file is part of snapcast + Copyright (C) 2014-2022 Johannes Pohl + Copyright (C) 2024 Marcus Weseloh + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +***/ + +#ifndef MESSAGE_CLIENT_SYSTEM_INFO_HPP +#define MESSAGE_CLIENT_SYSTEM_INFO_HPP + +// local headers +#include "json_message.hpp" + + +namespace msg +{ + +// Client system information sent from client to server. +// This message can contrain arbitrary JSON data. +class ClientSystemInfo : public JsonMessage +{ +public: + ClientSystemInfo() : JsonMessage(message_type::kClientSystemInfo) + { + } + + ~ClientSystemInfo() override = default; +}; +} // namespace msg + + +#endif + diff --git a/common/message/factory.hpp b/common/message/factory.hpp index ac236ed6..d24612bd 100644 --- a/common/message/factory.hpp +++ b/common/message/factory.hpp @@ -19,6 +19,7 @@ #pragma once // local headers +#include "client_system_info.hpp" #include "client_info.hpp" #include "codec_header.hpp" #include "hello.hpp" @@ -76,6 +77,8 @@ static std::unique_ptr createMessage(const BaseMessage& base_messag return createMessage(base_message, buffer); case message_type::kClientInfo: return createMessage(base_message, buffer); + case message_type::kClientSystemInfo: + return createMessage(base_message, buffer); default: return nullptr; } diff --git a/common/message/message.hpp b/common/message/message.hpp index 4bb52e43..8844a714 100644 --- a/common/message/message.hpp +++ b/common/message/message.hpp @@ -63,9 +63,10 @@ enum class message_type : uint16_t kHello = 5, // kStreamTags = 6, kClientInfo = 7, + kClientSystemInfo = 8, kFirst = kBase, - kLast = kClientInfo + kLast = kClientSystemInfo }; static std::ostream& operator<<(std::ostream& os, const message_type& msg_type) @@ -93,6 +94,9 @@ static std::ostream& operator<<(std::ostream& os, const message_type& msg_type) case message_type::kClientInfo: os << "ClientInfo"; break; + case message_type::kClientSystemInfo: + os << "ClientSystemInfo"; + break; default: os << "Unknown"; } diff --git a/server/config.hpp b/server/config.hpp index 736bc0f4..bbcbe0e0 100644 --- a/server/config.hpp +++ b/server/config.hpp @@ -235,6 +235,10 @@ struct ClientInfo lastSeen.tv_sec = jGet(j["lastSeen"], "sec", 0); lastSeen.tv_usec = jGet(j["lastSeen"], "usec", 0); connected = jGet(j, "connected", true); + if (j.contains("systemInfo")) + { + systemInfo = j["systemInfo"].template get(); + } } json toJson() @@ -247,6 +251,7 @@ struct ClientInfo j["lastSeen"]["sec"] = lastSeen.tv_sec; j["lastSeen"]["usec"] = lastSeen.tv_usec; j["connected"] = connected; + j["systemInfo"] = systemInfo; return j; } @@ -256,6 +261,7 @@ struct ClientInfo ClientConfig config; timeval lastSeen; bool connected; + json systemInfo; }; diff --git a/server/server.cpp b/server/server.cpp index 6a450fdb..fdbd993b 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -22,6 +22,7 @@ // local headers #include "common/aixlog.hpp" #include "common/message/client_info.hpp" +#include "common/message/client_system_info.hpp" #include "common/message/hello.hpp" #include "common/message/server_settings.hpp" #include "common/message/time.hpp" @@ -763,6 +764,22 @@ void Server::onMessageReceived(StreamSession* streamSession, const msg::BaseMess "Client.OnVolumeChanged", jsonrpcpp::Parameter("id", streamSession->clientId, "volume", clientInfo->config.volume.toJson())); controlServer_->send(notification->to_json().dump()); } + else if (baseMessage.type == message_type::kClientSystemInfo) + { + ClientInfoPtr clientInfo = Config::instance().getClientInfo(streamSession->clientId); + if (clientInfo == nullptr) + { + LOG(ERROR, LOG_TAG) << "client not found: " << streamSession->clientId << "\n"; + return; + } + msg::ClientSystemInfo sysInfoMsg; + sysInfoMsg.deserialize(baseMessage, buffer); + + clientInfo->systemInfo = sysInfoMsg.msg; + jsonrpcpp::notification_ptr notification = make_shared( + "Client.OnSystemInfoChanged", jsonrpcpp::Parameter("id", streamSession->clientId, "systemInfo", clientInfo->systemInfo)); + controlServer_->send(notification->to_json().dump()); + } else if (baseMessage.type == message_type::kHello) { msg::Hello helloMsg;