/*
* This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information
*
* 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 2 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 .
*/
#include "zm_catch2.h"
#include "zm_stream_socket.h"
#include "zm_stream_socket_client.h"
#include "zm_stream_socket_protocol.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace zm::stream_socket;
namespace {
// Per-process path so concurrent test runs do not share a socket file
const std::string kSockPathStr =
"/tmp/zm.stream_socket_client.unittest." + std::to_string(getpid()) + ".sock";
const char *kSockPath = kSockPathStr.c_str();
struct AVCodecParametersDeleter {
void operator()(AVCodecParameters *par) const { avcodec_parameters_free(&par); }
};
using codec_parameters_ptr = std::unique_ptr;
// Collects callback invocations with waiting helpers
struct Collector {
std::mutex mutex;
std::condition_variable cv;
std::vector> hellos;
std::vector>> media;
std::vector> events;
int byes = 0;
int disconnects = 0;
StreamSocketClient::Callbacks MakeCallbacks() {
StreamSocketClient::Callbacks callbacks;
callbacks.on_hello = [this](StreamId stream, const HelloInfo &info, uint32_t) {
std::lock_guard lock(mutex);
hellos.emplace_back(stream, info);
cv.notify_all();
};
callbacks.on_event = [this](const Header &header, const MonitorEvent &event) {
std::lock_guard lock(mutex);
events.emplace_back(header, event);
cv.notify_all();
};
callbacks.on_media = [this](const Header &header, const uint8_t *data, size_t size) {
std::lock_guard lock(mutex);
media.emplace_back(header, std::vector(data, data + size));
cv.notify_all();
};
callbacks.on_bye = [this]() {
std::lock_guard lock(mutex);
++byes;
cv.notify_all();
};
callbacks.on_disconnect = [this]() {
std::lock_guard lock(mutex);
++disconnects;
cv.notify_all();
};
return callbacks;
}
template
bool WaitFor(Pred pred, std::chrono::milliseconds timeout = std::chrono::seconds(5)) {
std::unique_lock lock(mutex);
return cv.wait_for(lock, timeout, pred);
}
};
av_packet_ptr make_packet(size_t size, uint8_t fill) {
av_packet_ptr packet{av_packet_alloc()};
REQUIRE(av_new_packet(packet.get(), size) == 0);
memset(packet->data, fill, size);
return packet;
}
} // namespace
TEST_CASE("StreamSocketClient receives HELLO and media end to end", "[stream_socket_client]") {
StreamSocket server(7, kSockPath);
REQUIRE(server.Start());
codec_parameters_ptr par{avcodec_parameters_alloc()};
par->codec_type = AVMEDIA_TYPE_VIDEO;
par->codec_id = AV_CODEC_ID_HEVC;
par->width = 2560;
par->height = 1440;
server.SetVideoParams(par.get(), {20, 1});
Collector collector;
StreamSocketClient client(std::string(kSockPath), collector.MakeCallbacks());
REQUIRE(collector.WaitFor([&] { return !collector.hellos.empty(); }));
{
std::lock_guard lock(collector.mutex);
REQUIRE(collector.hellos[0].first == StreamId::Video);
REQUIRE(collector.hellos[0].second.codec_id == AV_CODEC_ID_HEVC);
REQUIRE(collector.hellos[0].second.width == 2560);
REQUIRE(collector.hellos[0].second.fps_num == 20);
}
// wait until the server has the client registered, then send media
std::this_thread::sleep_for(std::chrono::milliseconds(100));
av_packet_ptr packet = make_packet(4096, 0xC3);
server.SendMedia(packet.get(), StreamId::Video, true, 777000);
REQUIRE(collector.WaitFor([&] { return !collector.media.empty(); }));
{
std::lock_guard lock(collector.mutex);
const Header &header = collector.media[0].first;
REQUIRE(header.type == static_cast(MessageType::Media));
REQUIRE(header.flags == kFlagKeyframe);
REQUIRE(header.pts_us == 777000);
REQUIRE(collector.media[0].second.size() == 4096);
REQUIRE(collector.media[0].second[100] == 0xC3);
}
client.Stop();
server.Stop();
}
TEST_CASE("StreamSocketClient gets BYE on server stop and reconnects", "[stream_socket_client]") {
auto server = std::make_unique(7, kSockPath);
REQUIRE(server->Start());
codec_parameters_ptr par{avcodec_parameters_alloc()};
par->codec_type = AVMEDIA_TYPE_VIDEO;
par->codec_id = AV_CODEC_ID_H264;
server->SetVideoParams(par.get(), {0, 0});
Collector collector;
StreamSocketClient client(std::string(kSockPath), collector.MakeCallbacks());
REQUIRE(collector.WaitFor([&] { return collector.hellos.size() >= 1; }));
server->Stop();
REQUIRE(collector.WaitFor([&] { return collector.byes >= 1 && collector.disconnects >= 1; }));
// Producer comes back (zmc restart): client reconnects and gets a new HELLO
server = std::make_unique(7, kSockPath);
REQUIRE(server->Start());
server->SetVideoParams(par.get(), {0, 0});
REQUIRE(collector.WaitFor([&] { return collector.hellos.size() >= 2; },
std::chrono::seconds(10)));
client.Stop();
server->Stop();
}
TEST_CASE("StreamSocketClient handles fragmented delivery", "[stream_socket_client]") {
int fds[2];
REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
Collector collector;
StreamSocketClient client(fds[0], collector.MakeCallbacks());
// Build one MEDIA message and dribble it through in small fragments
std::vector payload(300, 0x5e);
Header header = {};
header.length = kHeaderLengthBytes + payload.size();
header.version = kProtocolVersion;
header.type = static_cast(MessageType::Media);
header.stream = static_cast(StreamId::Audio);
header.sequence = 9;
header.pts_us = 42;
uint8_t wire[kHeaderSize];
SerializeHeader(header, wire);
std::vector message(wire, wire + kHeaderSize);
message.insert(message.end(), payload.begin(), payload.end());
for (size_t pos = 0; pos < message.size(); pos += 7) {
size_t chunk = std::min(7, message.size() - pos);
REQUIRE(::send(fds[1], message.data() + pos, chunk, 0) == static_cast(chunk));
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
REQUIRE(collector.WaitFor([&] { return !collector.media.empty(); }));
{
std::lock_guard lock(collector.mutex);
REQUIRE(collector.media.size() == 1);
REQUIRE(collector.media[0].first.sequence == 9);
REQUIRE(collector.media[0].second == payload);
}
::close(fds[1]);
REQUIRE(collector.WaitFor([&] { return collector.disconnects >= 1; }));
client.Stop();
}
TEST_CASE("StreamSocketClient disconnects on malformed input", "[stream_socket_client]") {
int fds[2];
REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
Collector collector;
StreamSocketClient client(fds[0], collector.MakeCallbacks());
// Garbage: invalid version byte in an otherwise plausible header
std::vector garbage(kHeaderSize, 0xFF);
REQUIRE(::send(fds[1], garbage.data(), garbage.size(), 0)
== static_cast(garbage.size()));
REQUIRE(collector.WaitFor([&] { return collector.disconnects >= 1; }));
{
std::lock_guard lock(collector.mutex);
REQUIRE(collector.media.empty());
REQUIRE(collector.hellos.empty());
}
::close(fds[1]);
client.Stop();
}
TEST_CASE("StreamSocketClient skips unknown message types", "[stream_socket_client]") {
int fds[2];
REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
Collector collector;
StreamSocketClient client(fds[0], collector.MakeCallbacks());
// Unknown type 0x7f with a small payload, then a valid MEDIA
Header header = {};
header.length = kHeaderLengthBytes + 4;
header.version = kProtocolVersion;
header.type = 0x7f;
uint8_t wire[kHeaderSize];
SerializeHeader(header, wire);
std::vector message(wire, wire + kHeaderSize);
message.insert(message.end(), {1, 2, 3, 4});
header.type = static_cast(MessageType::Media);
header.length = kHeaderLengthBytes + 2;
SerializeHeader(header, wire);
message.insert(message.end(), wire, wire + kHeaderSize);
message.insert(message.end(), {0xAA, 0xBB});
REQUIRE(::send(fds[1], message.data(), message.size(), 0)
== static_cast(message.size()));
REQUIRE(collector.WaitFor([&] { return !collector.media.empty(); }));
{
std::lock_guard lock(collector.mutex);
REQUIRE(collector.media.size() == 1);
REQUIRE(collector.media[0].second == std::vector({0xAA, 0xBB}));
}
::close(fds[1]);
client.Stop();
}
TEST_CASE("StreamSocketClient backs off when the producer rejects the connection",
"[stream_socket_client]") {
// A producer that accepts and immediately closes, as StreamSocket does for
// a client failing the uid allow-list or the client limit
::unlink(kSockPath);
int listener = ::socket(AF_UNIX, SOCK_STREAM, 0);
REQUIRE(listener >= 0);
sockaddr_un addr = {};
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, kSockPath, sizeof(addr.sun_path) - 1);
REQUIRE(::bind(listener, reinterpret_cast(&addr), sizeof(addr)) == 0);
REQUIRE(::listen(listener, 8) == 0);
std::atomic accepts{0};
std::atomic stop{false};
std::thread rejecter([&] {
while (!stop) {
timeval tv = {0, 100000};
fd_set fds;
FD_ZERO(&fds);
FD_SET(listener, &fds);
if (::select(listener + 1, &fds, nullptr, nullptr, &tv) > 0) {
int fd = ::accept(listener, nullptr, nullptr);
if (fd >= 0) {
++accepts;
::close(fd);
}
}
}
});
Collector collector;
{
StreamSocketClient client(std::string(kSockPath), collector.MakeCallbacks());
std::this_thread::sleep_for(std::chrono::milliseconds(2500));
client.Stop();
}
stop = true;
rejecter.join();
::close(listener);
::unlink(kSockPath);
// With the 1s backoff after every rejected connection: the initial attempt
// plus two or three retries in 2.5s. Without backoff this is thousands.
REQUIRE(accepts >= 2);
REQUIRE(accepts <= 5);
// A rejection is not a session, so no disconnect was reported
std::lock_guard lock(collector.mutex);
REQUIRE(collector.disconnects == 0);
}
TEST_CASE("StreamSocketClient delivers EVENT frames", "[stream_socket_client]") {
StreamSocket server(7, kSockPath);
REQUIRE(server.Start());
// A cached snapshot is replayed on connect, then a broadcast state change
MonitorEvent snapshot;
snapshot.code = kEventSnapshot;
snapshot.state_id = 0; snapshot.has_state_id = true;
snapshot.state_name = "IDLE";
snapshot.health_code = kEventCaptureFailed; snapshot.has_health_code = true;
snapshot.message = "Capture failed";
server.SetSnapshotEvent(BuildEvent(snapshot));
Collector collector;
StreamSocketClient client(std::string(kSockPath), collector.MakeCallbacks());
REQUIRE(collector.WaitFor([&] { return collector.events.size() >= 1; }));
{
std::lock_guard lock(collector.mutex);
const Header &header = collector.events[0].first;
const MonitorEvent &event = collector.events[0].second;
REQUIRE(header.stream == static_cast(StreamId::Monitor));
REQUIRE(event.code == kEventSnapshot);
REQUIRE(event.state_name == "IDLE");
REQUIRE(event.has_health_code);
REQUIRE(event.health_code == kEventCaptureFailed);
REQUIRE(event.message == "Capture failed");
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
MonitorEvent change;
change.code = kEventStateChanged;
change.state_id = 2; change.has_state_id = true;
change.prev_state_id = 0; change.has_prev_state_id = true;
change.state_name = "ALARM";
change.wall_clock_us = 1718355103501000ULL; change.has_wall_clock = true;
server.SendMonitorEvent(BuildEvent(change));
REQUIRE(collector.WaitFor([&] { return collector.events.size() >= 2; }));
{
std::lock_guard lock(collector.mutex);
const Header &header = collector.events[1].first;
const MonitorEvent &event = collector.events[1].second;
REQUIRE(header.sequence == 0); // first event produced by this monitor
REQUIRE(event.code == kEventStateChanged);
REQUIRE(event.state_id == 2);
REQUIRE(event.prev_state_id == 0);
REQUIRE(event.state_name == "ALARM");
REQUIRE(event.wall_clock_us == 1718355103501000ULL);
}
client.Stop();
server.Stop();
}
TEST_CASE("StreamSocketClient refuses a path too long for a unix socket address", "[stream_socket_client]") {
Collector collector;
StreamSocketClient client("/tmp/" + std::string(200, 'x') + ".sock", collector.MakeCallbacks());
// The reader thread gives up instead of connecting to a truncated path
std::this_thread::sleep_for(std::chrono::milliseconds(200));
REQUIRE_FALSE(client.IsConnected());
client.Stop();
REQUIRE(collector.disconnects == 0);
}