Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Add method to pass arbitrary client system info #1259

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions client/client_settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

// standard headers
#include <string>
#include <chrono>



Expand Down Expand Up @@ -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;
};
97 changes: 96 additions & 1 deletion client/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
{
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -382,6 +471,7 @@ void Controller::start()
void Controller::reconnect()
{
timer_.cancel();
systemInfoTimer_.cancel();
clientConnection_->disconnect();
player_.reset();
stream_.reset();
Expand Down Expand Up @@ -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();
}
Expand Down
3 changes: 3 additions & 0 deletions client/controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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> clientConnection_;
Expand Down
20 changes: 20 additions & 0 deletions client/snapclient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
45 changes: 45 additions & 0 deletions common/message/client_system_info.hpp
Original file line number Diff line number Diff line change
@@ -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

3 changes: 3 additions & 0 deletions common/message/factory.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#pragma once

// local headers
#include "client_system_info.hpp"
#include "client_info.hpp"
#include "codec_header.hpp"
#include "hello.hpp"
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 5 additions & 1 deletion common/message/message.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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";
}
Expand Down
6 changes: 6 additions & 0 deletions server/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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;
}

Expand All @@ -256,6 +261,7 @@ struct ClientInfo
ClientConfig config;
timeval lastSeen;
bool connected;
json systemInfo;
};


Expand Down
17 changes: 17 additions & 0 deletions server/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down
Loading