Add client system info message

This commit is contained in:
Marcus Weseloh 2024-07-05 14:30:42 +02:00
parent 142afabad4
commit c6a8f5d888
9 changed files with 210 additions and 2 deletions

View file

@ -24,6 +24,7 @@
// standard headers
#include <string>
#include <chrono>
@ -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;
};

View file

@ -64,20 +64,25 @@
#include "common/snap_exception.hpp"
#include "time_provider.hpp"
#include <boost/process.hpp>
// standard headers
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <chrono>
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<MetadataAdapter> 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<msg::ClientSystemInfo>();
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();
}

View file

@ -59,9 +59,12 @@ private:
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> clientConnection_;

View file

@ -179,6 +179,10 @@ int main(int argc, char** argv)
#endif
mixer_mode = op.add<Value<string>>("", "mixer", mixers + "|none|?[:<options>]", "software");
// system info
auto sysInfo = op.add<Value<string>>("", "sysinfo", "source for JSON-formatted system infomation [file:<filename>,script:<executable>]", "none");
op.add<Value<int>>("", "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);

View file

@ -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 <http://www.gnu.org/licenses/>.
***/
#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

View file

@ -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<BaseMessage> createMessage(const BaseMessage& base_messag
return createMessage<PcmChunk>(base_message, buffer);
case message_type::kClientInfo:
return createMessage<ClientInfo>(base_message, buffer);
case message_type::kClientSystemInfo:
return createMessage<ClientSystemInfo>(base_message, buffer);
default:
return nullptr;
}

View file

@ -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";
}

View file

@ -235,6 +235,10 @@ struct ClientInfo
lastSeen.tv_sec = jGet<int32_t>(j["lastSeen"], "sec", 0);
lastSeen.tv_usec = jGet<int32_t>(j["lastSeen"], "usec", 0);
connected = jGet<bool>(j, "connected", true);
if (j.contains("systemInfo"))
{
systemInfo = j["systemInfo"].template get<json>();
}
}
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;
};

View file

@ -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<jsonrpcpp::Notification>(
"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;