mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-10-02 15:35:09 -04:00
zm::UnixSocket copies the path with a truncating strncpy, so a PATH_SOCKS long enough to overflow sun_path made the server bind one file while chmod, chown and unlink acted on another, and the client connect to a truncated, different path. Both now refuse such a path with an error. Start() also closes the listener on its own failure paths. SendMedia indexed the two-entry sequence array with the stream id; a caller passing StreamId::Monitor would have written past it. Ignore anything that is not video or audio. Tests: server and client refuse a 200 character path; a Monitor stream id is ignored and does not disturb the video sequence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
695 lines
24 KiB
C++
695 lines
24 KiB
C++
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "zm_stream_socket.h"
|
|
|
|
#include "zm_config.h"
|
|
#include "zm_logger.h"
|
|
#include "zm_utils.h"
|
|
|
|
#include <algorithm>
|
|
#include <cerrno>
|
|
#include <cinttypes>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <grp.h>
|
|
#include <poll.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/uio.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
|
|
using namespace zm::stream_socket;
|
|
|
|
StreamSocket::StreamSocket(unsigned int monitor_id, std::string path) :
|
|
StreamSocket(monitor_id, std::move(path), Config()) {
|
|
}
|
|
|
|
StreamSocket::StreamSocket(unsigned int monitor_id, std::string path, Config config) :
|
|
monitor_id_(monitor_id),
|
|
path_(std::move(path)),
|
|
config_(std::move(config)) {
|
|
}
|
|
|
|
StreamSocket::~StreamSocket() {
|
|
Stop();
|
|
}
|
|
|
|
bool StreamSocket::Start() {
|
|
if (thread_.joinable())
|
|
return true;
|
|
|
|
// zm::UnixSocket copies the path with a truncating strncpy, so an over-long
|
|
// path would bind one file while the chmod, chown and unlink below act on
|
|
// another. Refuse it instead.
|
|
if (path_.size() >= sizeof(sockaddr_un::sun_path)) {
|
|
Error("StreamSocket: socket path %s is longer than the %zu bytes a unix socket"
|
|
" address allows", path_.c_str(), sizeof(sockaddr_un::sun_path) - 1);
|
|
return false;
|
|
}
|
|
|
|
::unlink(path_.c_str());
|
|
if (!listener_.bind(path_.c_str())) {
|
|
Error("StreamSocket: failed to bind %s", path_.c_str());
|
|
listener_.close();
|
|
return false;
|
|
}
|
|
ApplyPermissions();
|
|
if (!listener_.listen()) {
|
|
Error("StreamSocket: failed to listen on %s", path_.c_str());
|
|
listener_.close();
|
|
::unlink(path_.c_str());
|
|
return false;
|
|
}
|
|
listener_.setBlocking(false);
|
|
|
|
if (!wake_.open()) {
|
|
Error("StreamSocket: failed to open wake pipe");
|
|
listener_.close();
|
|
::unlink(path_.c_str());
|
|
return false;
|
|
}
|
|
wake_.setBlocking(false);
|
|
|
|
terminate_ = false;
|
|
thread_ = std::thread(&StreamSocket::Run, this);
|
|
Debug(1, "StreamSocket: monitor %u listening on %s", monitor_id_, path_.c_str());
|
|
return true;
|
|
}
|
|
|
|
void StreamSocket::Stop() {
|
|
if (thread_.joinable()) {
|
|
terminate_ = true;
|
|
Wake();
|
|
thread_.join();
|
|
}
|
|
if (listener_.isOpen()) {
|
|
listener_.close();
|
|
::unlink(path_.c_str());
|
|
}
|
|
wake_.close();
|
|
}
|
|
|
|
void StreamSocket::ApplyPermissions() {
|
|
if (chmod(path_.c_str(), 0660) < 0)
|
|
Warning("StreamSocket: chmod 0660 %s failed: %s", path_.c_str(), strerror(errno));
|
|
|
|
if (config_.group.empty())
|
|
return;
|
|
|
|
long size = sysconf(_SC_GETGR_R_SIZE_MAX);
|
|
std::vector<char> buffer(size > 0 ? size : 1024);
|
|
group grp = {};
|
|
group *result = nullptr;
|
|
if (getgrnam_r(config_.group.c_str(), &grp, buffer.data(), buffer.size(), &result) != 0
|
|
or !result) {
|
|
Warning("StreamSocket: group %s not found, leaving default group on %s",
|
|
config_.group.c_str(), path_.c_str());
|
|
return;
|
|
}
|
|
if (chown(path_.c_str(), static_cast<uid_t>(-1), grp.gr_gid) < 0)
|
|
Warning("StreamSocket: chown group %s on %s failed: %s",
|
|
config_.group.c_str(), path_.c_str(), strerror(errno));
|
|
}
|
|
|
|
StreamSocket::Config StreamSocket::ConfigFromStatic() {
|
|
Config config;
|
|
config.group = staticConfig.STREAM_SOCKET_GROUP;
|
|
config.allowed_uids = ParseAllowedUids(staticConfig.STREAM_SOCKET_ALLOWED_UIDS);
|
|
if (staticConfig.STREAM_SOCKET_MAX_CLIENTS > 0)
|
|
config.max_clients = staticConfig.STREAM_SOCKET_MAX_CLIENTS;
|
|
if (staticConfig.STREAM_SOCKET_QUEUE_BYTES > 0)
|
|
config.queue_max_bytes = staticConfig.STREAM_SOCKET_QUEUE_BYTES;
|
|
if (staticConfig.STREAM_SOCKET_QUEUE_MSGS > 0)
|
|
config.queue_max_msgs = staticConfig.STREAM_SOCKET_QUEUE_MSGS;
|
|
if (staticConfig.STREAM_SOCKET_STALL_SECS > 0)
|
|
config.stall_timeout = std::chrono::seconds(staticConfig.STREAM_SOCKET_STALL_SECS);
|
|
return config;
|
|
}
|
|
|
|
std::vector<uid_t> StreamSocket::ParseAllowedUids(const std::string &value) {
|
|
std::vector<uid_t> uids;
|
|
for (const std::string &token : Split(value, ',')) {
|
|
std::string trimmed = Trim(token, " \t");
|
|
if (trimmed.empty())
|
|
continue;
|
|
errno = 0;
|
|
char *end = nullptr;
|
|
unsigned long uid = strtoul(trimmed.c_str(), &end, 10);
|
|
// strtoul wraps a leading '-' and saturates on overflow; reject both, and
|
|
// any value that does not round-trip through uid_t, so a typo cannot
|
|
// silently allow a different uid (e.g. 2^32 truncating to uid 0).
|
|
bool out_of_range = errno == ERANGE
|
|
or uid != static_cast<unsigned long>(static_cast<uid_t>(uid));
|
|
if (trimmed[0] != '-' and end != trimmed.c_str() and *end == '\0' and !out_of_range) {
|
|
uids.push_back(static_cast<uid_t>(uid));
|
|
} else {
|
|
Warning("StreamSocket: ignoring malformed uid '%s' in allowed uids", trimmed.c_str());
|
|
}
|
|
}
|
|
return uids;
|
|
}
|
|
|
|
bool StreamSocket::CheckPeer(int fd, uid_t &uid, pid_t &pid) const {
|
|
// Kernel-verified peer credentials: SO_PEERCRED on Linux, getpeereid on the
|
|
// BSDs and macOS (which report no pid). Without either the uid allow-list
|
|
// cannot be enforced and only the filesystem permissions apply.
|
|
#if defined(SO_PEERCRED)
|
|
ucred cred = {};
|
|
socklen_t len = sizeof(cred);
|
|
if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) != 0) {
|
|
Warning("StreamSocket: SO_PEERCRED failed on %s: %s", path_.c_str(), strerror(errno));
|
|
return config_.allowed_uids.empty();
|
|
}
|
|
uid = cred.uid;
|
|
pid = cred.pid;
|
|
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
|
|
gid_t gid = 0;
|
|
if (getpeereid(fd, &uid, &gid) != 0) {
|
|
Warning("StreamSocket: getpeereid failed on %s: %s", path_.c_str(), strerror(errno));
|
|
return config_.allowed_uids.empty();
|
|
}
|
|
pid = 0;
|
|
#else
|
|
if (!config_.allowed_uids.empty()) {
|
|
Warning("StreamSocket: peer credentials unavailable on this platform,"
|
|
" ZM_STREAM_SOCKET_ALLOWED_UIDS is not enforced on %s", path_.c_str());
|
|
}
|
|
return true;
|
|
#endif
|
|
if (config_.allowed_uids.empty() or uid == geteuid())
|
|
return true;
|
|
return std::find(config_.allowed_uids.begin(), config_.allowed_uids.end(), uid)
|
|
!= config_.allowed_uids.end();
|
|
}
|
|
|
|
namespace {
|
|
// HELLO body for a stream, or empty when the source has no such stream.
|
|
std::vector<uint8_t> HelloPayload(const AVCodecParameters *par, AVRational frame_rate) {
|
|
if (!par or par->codec_id == AV_CODEC_ID_NONE)
|
|
return {};
|
|
return BuildHello(par, frame_rate);
|
|
}
|
|
} // namespace
|
|
|
|
void StreamSocket::SetStreams(const AVCodecParameters *video, AVRational frame_rate,
|
|
const AVCodecParameters *audio) {
|
|
std::vector<uint8_t> video_payload = HelloPayload(video, frame_rate);
|
|
std::vector<uint8_t> audio_payload = HelloPayload(audio, {0, 0});
|
|
bool changed;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
changed = ApplyStreamsLocked(std::move(video_payload), std::move(audio_payload));
|
|
}
|
|
if (changed)
|
|
Wake();
|
|
}
|
|
|
|
void StreamSocket::SetVideoParams(const AVCodecParameters *par, AVRational frame_rate) {
|
|
std::vector<uint8_t> payload = HelloPayload(par, frame_rate);
|
|
bool changed;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
changed = ApplyStreamsLocked(std::move(payload), std::vector<uint8_t>(hello_audio_payload_));
|
|
}
|
|
if (changed)
|
|
Wake();
|
|
}
|
|
|
|
void StreamSocket::SetAudioParams(const AVCodecParameters *par) {
|
|
std::vector<uint8_t> payload = HelloPayload(par, {0, 0});
|
|
bool changed;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
changed = ApplyStreamsLocked(std::vector<uint8_t>(hello_video_payload_), std::move(payload));
|
|
}
|
|
if (changed)
|
|
Wake();
|
|
}
|
|
|
|
void StreamSocket::ClearVideoParams() {
|
|
SetVideoParams(nullptr, {0, 0});
|
|
}
|
|
|
|
void StreamSocket::ClearAudioParams() {
|
|
SetAudioParams(nullptr);
|
|
}
|
|
|
|
bool StreamSocket::ApplyStreamsLocked(std::vector<uint8_t> video, std::vector<uint8_t> audio) {
|
|
bool video_changed = video != hello_video_payload_;
|
|
bool audio_changed = audio != hello_audio_payload_;
|
|
if (!video_changed and !audio_changed)
|
|
return false;
|
|
|
|
// A generation is complete once its video HELLO has gone out; from then on
|
|
// any change - new parameters, a stream appearing, a stream disappearing -
|
|
// starts a new one. Before that (the initial audio-then-video announcement)
|
|
// only a change to an already announced stream does.
|
|
bool bump = !hello_video_payload_.empty()
|
|
or (audio_changed and !hello_audio_payload_.empty());
|
|
if (bump) {
|
|
++generation_;
|
|
sequence_[0] = sequence_[1] = 0;
|
|
keyframe_.reset(); // its header carries the old generation
|
|
Info("StreamSocket: monitor %u stream parameters changed (video %s, audio %s),"
|
|
" generation now %u", monitor_id_,
|
|
video.empty() ? "none" : (video_changed ? "changed" : "same"),
|
|
audio.empty() ? "none" : (audio_changed ? "changed" : "same"),
|
|
generation_);
|
|
}
|
|
|
|
hello_video_payload_ = std::move(video);
|
|
hello_audio_payload_ = std::move(audio);
|
|
|
|
// On a bump every remaining stream is re-announced under the new generation;
|
|
// otherwise only what is new. Audio first: the video HELLO is always the
|
|
// last HELLO of a generation.
|
|
if (hello_audio_payload_.empty()) {
|
|
hello_audio_.reset();
|
|
} else if (bump or audio_changed) {
|
|
hello_audio_ = MakeMessage(MessageType::Hello, StreamId::Audio, 0, 0, 0,
|
|
std::vector<uint8_t>(hello_audio_payload_), true);
|
|
BroadcastLocked(hello_audio_);
|
|
}
|
|
if (hello_video_payload_.empty()) {
|
|
hello_video_.reset();
|
|
} else if (bump or video_changed) {
|
|
hello_video_ = MakeMessage(MessageType::Hello, StreamId::Video, 0, 0, 0,
|
|
std::vector<uint8_t>(hello_video_payload_), true);
|
|
BroadcastLocked(hello_video_);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void StreamSocket::SendMedia(const AVPacket *packet, StreamId stream,
|
|
bool keyframe, int64_t pts_us) {
|
|
if (!packet or packet->size <= 0)
|
|
return;
|
|
|
|
// Only the two media streams have a sequence counter and a HELLO
|
|
if (stream != StreamId::Video and stream != StreamId::Audio)
|
|
return;
|
|
|
|
bool video_keyframe = keyframe and stream == StreamId::Video;
|
|
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
|
|
// A stream is on the wire only once its parameters are announced. Without a
|
|
// HELLO a consumer cannot decode the payload (no codec id or extradata), so
|
|
// dropping here keeps the protocol's "HELLO precedes MEDIA" guarantee for
|
|
// e.g. audio packets a monitor forwards before record_audio announces them.
|
|
const std::vector<uint8_t> &hello =
|
|
stream == StreamId::Audio ? hello_audio_payload_ : hello_video_payload_;
|
|
if (hello.empty())
|
|
return;
|
|
|
|
uint32_t sequence = sequence_[static_cast<size_t>(stream)]++;
|
|
bool have_clients = !clients_.empty();
|
|
|
|
// With no consumers the message only counts against the sequence; skip the
|
|
// packet reference and fan-out entirely. Keyframes are still cached so the
|
|
// first consumer to connect gets an immediate first frame.
|
|
if (!have_clients and !video_keyframe)
|
|
return;
|
|
|
|
Header header = {};
|
|
header.length = kHeaderLengthBytes + packet->size;
|
|
header.version = kProtocolVersion;
|
|
header.type = static_cast<uint8_t>(MessageType::Media);
|
|
header.stream = static_cast<uint8_t>(stream);
|
|
header.flags = video_keyframe ? kFlagKeyframe : 0;
|
|
header.sequence = sequence;
|
|
header.generation = generation_;
|
|
header.pts_us = pts_us; // Header.pts_us is signed
|
|
|
|
if (video_keyframe) {
|
|
// Cache the keyframe for fast-start of late joiners; the cache references
|
|
// the packet's buffer, no copy is made.
|
|
av_packet_ptr cache_clone{av_packet_clone(packet)};
|
|
if (cache_clone) {
|
|
auto cache = std::make_shared<Message>();
|
|
Header cache_header = header;
|
|
cache_header.type = static_cast<uint8_t>(MessageType::Keyframe);
|
|
SerializeHeader(cache_header, cache->header.data());
|
|
cache->av_payload = std::move(cache_clone);
|
|
keyframe_ = std::move(cache);
|
|
}
|
|
}
|
|
|
|
if (!have_clients)
|
|
return;
|
|
|
|
// Reference the payload buffer; no copy of the media data is made.
|
|
av_packet_ptr clone{av_packet_clone(packet)};
|
|
if (!clone) {
|
|
Error("StreamSocket: av_packet_clone failed for monitor %u", monitor_id_);
|
|
return;
|
|
}
|
|
auto message = std::make_shared<Message>();
|
|
SerializeHeader(header, message->header.data());
|
|
message->av_payload = std::move(clone);
|
|
BroadcastLocked(message);
|
|
lock.unlock();
|
|
Wake();
|
|
}
|
|
|
|
void StreamSocket::SendMonitorEvent(std::vector<uint8_t> payload) {
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
uint32_t sequence = event_sequence_++;
|
|
if (clients_.empty())
|
|
return; // sequence still advanced, so a late joiner sees the gap
|
|
MessagePtr message = MakeMessage(MessageType::Event, StreamId::Monitor, 0,
|
|
sequence, 0, std::move(payload), true);
|
|
BroadcastLocked(message);
|
|
lock.unlock();
|
|
Wake();
|
|
}
|
|
|
|
void StreamSocket::SetSnapshotEvent(std::vector<uint8_t> payload) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
// The snapshot is the consumer's authoritative current status on connect. It
|
|
// is not broadcast, and only the body is kept: AcceptClient frames it, so
|
|
// the header carries the generation and the event-sequence baseline that are
|
|
// current when the consumer connects rather than when the status last moved.
|
|
snapshot_payload_ = std::move(payload);
|
|
}
|
|
|
|
void StreamSocket::InvalidateKeyframe() {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
keyframe_.reset();
|
|
}
|
|
|
|
StreamSocket::MessagePtr StreamSocket::MakeMessage(
|
|
MessageType type, StreamId stream, uint8_t flags, uint32_t sequence,
|
|
int64_t pts_us, std::vector<uint8_t> payload, bool control) const {
|
|
auto message = std::make_shared<Message>();
|
|
Header header = {};
|
|
header.length = kHeaderLengthBytes + payload.size();
|
|
header.version = kProtocolVersion;
|
|
header.type = static_cast<uint8_t>(type);
|
|
header.stream = static_cast<uint8_t>(stream);
|
|
header.flags = flags;
|
|
header.sequence = sequence;
|
|
header.generation = generation_;
|
|
header.pts_us = pts_us; // Header.pts_us is signed
|
|
SerializeHeader(header, message->header.data());
|
|
message->blob_payload = std::move(payload);
|
|
message->control = control;
|
|
return message;
|
|
}
|
|
|
|
void StreamSocket::BroadcastLocked(const MessagePtr &message) {
|
|
for (std::unique_ptr<Client> &client : clients_)
|
|
EnqueueLocked(*client, message);
|
|
}
|
|
|
|
void StreamSocket::EnqueueLocked(Client &client, const MessagePtr &message) {
|
|
while (!client.queue.empty()
|
|
and (client.queued_bytes + message->size() > config_.queue_max_bytes
|
|
or client.queue.size() + 1 > config_.queue_max_msgs)) {
|
|
// Drop the oldest non-control message. The front message is unsafe to
|
|
// drop once partially written (removing it would desync the framing) or
|
|
// while the listener is writing it with the lock released.
|
|
bool front_pinned = client.front_offset > 0 or client.front_in_flight;
|
|
auto begin = client.queue.begin() + (front_pinned ? 1 : 0);
|
|
auto victim = std::find_if(begin, client.queue.end(),
|
|
[](const MessagePtr &m) { return !m->control; });
|
|
if (victim == client.queue.end())
|
|
break;
|
|
client.queued_bytes -= (*victim)->size();
|
|
client.queue.erase(victim);
|
|
++client.dropped;
|
|
}
|
|
|
|
if (client.queued_bytes + message->size() > config_.queue_max_bytes
|
|
or client.queue.size() + 1 > config_.queue_max_msgs) {
|
|
if (!message->control) {
|
|
// Nothing droppable left; drop the incoming message instead.
|
|
++client.dropped;
|
|
return;
|
|
}
|
|
// Control messages (HELLO/BYE) always go through, even above the limit.
|
|
}
|
|
|
|
client.queue.push_back(message);
|
|
client.queued_bytes += message->size();
|
|
}
|
|
|
|
bool StreamSocket::DrainClient(Client &client) {
|
|
while (true) {
|
|
// Peek the front message under the lock and pin it: EnqueueLocked may
|
|
// drop queued messages to make room while we are writing, but never the
|
|
// one in flight, so the bookkeeping below always refers to it.
|
|
MessagePtr front;
|
|
size_t offset = 0;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (client.queue.empty())
|
|
return true;
|
|
front = client.queue.front();
|
|
offset = client.front_offset;
|
|
client.front_in_flight = true;
|
|
}
|
|
const Message &message = *front;
|
|
|
|
iovec iov[2];
|
|
int iovcnt = 0;
|
|
if (offset < kHeaderSize) {
|
|
iov[iovcnt].iov_base = const_cast<uint8_t *>(message.header.data()) + offset;
|
|
iov[iovcnt].iov_len = kHeaderSize - offset;
|
|
++iovcnt;
|
|
if (message.payload_size() > 0) {
|
|
iov[iovcnt].iov_base = const_cast<uint8_t *>(message.payload_data());
|
|
iov[iovcnt].iov_len = message.payload_size();
|
|
++iovcnt;
|
|
}
|
|
} else {
|
|
size_t payload_offset = offset - kHeaderSize;
|
|
iov[iovcnt].iov_base = const_cast<uint8_t *>(message.payload_data()) + payload_offset;
|
|
iov[iovcnt].iov_len = message.payload_size() - payload_offset;
|
|
++iovcnt;
|
|
}
|
|
|
|
msghdr msg = {};
|
|
msg.msg_iov = iov;
|
|
msg.msg_iovlen = iovcnt;
|
|
// MSG_NOSIGNAL: a disconnected client must produce EPIPE, not SIGPIPE.
|
|
// This is the kernel copy of the payload; it runs unlocked.
|
|
ssize_t written = ::sendmsg(client.sock->getDesc(), &msg, MSG_NOSIGNAL);
|
|
int error = written < 0 ? errno : 0;
|
|
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
client.front_in_flight = false;
|
|
if (written < 0) {
|
|
if (error == EAGAIN or error == EWOULDBLOCK)
|
|
return true;
|
|
if (error == EINTR)
|
|
continue;
|
|
Debug(1, "StreamSocket: sendmsg to client pid %d failed: %s",
|
|
client.pid, strerror(error));
|
|
return false;
|
|
}
|
|
|
|
client.front_offset += written;
|
|
client.last_progress = std::chrono::steady_clock::now();
|
|
if (client.front_offset >= message.size()) {
|
|
client.queued_bytes -= message.size();
|
|
client.queue.pop_front();
|
|
client.front_offset = 0;
|
|
++client.sent;
|
|
}
|
|
}
|
|
}
|
|
|
|
void StreamSocket::SendStatsLocked(Client &client, TimePoint now) {
|
|
if (now - client.last_stats < config_.stats_interval)
|
|
return;
|
|
client.last_stats = now;
|
|
MessagePtr stats = MakeMessage(MessageType::Stats, StreamId::Video, 0, 0, 0,
|
|
BuildStats(client.sent, client.dropped), false);
|
|
EnqueueLocked(client, stats);
|
|
}
|
|
|
|
void StreamSocket::AcceptClient() {
|
|
while (true) {
|
|
int fd = ::accept(listener_.getDesc(), nullptr, nullptr);
|
|
if (fd < 0) {
|
|
if (errno != EAGAIN and errno != EWOULDBLOCK and errno != EINTR)
|
|
Warning("StreamSocket: accept on %s failed: %s", path_.c_str(), strerror(errno));
|
|
return;
|
|
}
|
|
|
|
auto sock = std::make_unique<zm::TcpUnixSocket>(listener_, fd);
|
|
|
|
uid_t uid = 0;
|
|
pid_t pid = 0;
|
|
if (!CheckPeer(fd, uid, pid)) {
|
|
Warning("StreamSocket: rejecting client uid %u pid %d on %s (not in allowed uids)",
|
|
uid, pid, path_.c_str());
|
|
continue; // sock destructor closes the connection
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (clients_.size() >= config_.max_clients) {
|
|
Warning("StreamSocket: rejecting client uid %u pid %d on %s (%zu clients connected)",
|
|
uid, pid, path_.c_str(), clients_.size());
|
|
continue;
|
|
}
|
|
|
|
sock->setBlocking(false);
|
|
auto client = std::make_unique<Client>();
|
|
client->sock = std::move(sock);
|
|
client->uid = uid;
|
|
client->pid = pid;
|
|
client->last_progress = std::chrono::steady_clock::now();
|
|
client->last_stats = client->last_progress;
|
|
|
|
// New consumers get the stream parameters first (audio HELLO before video
|
|
// HELLO, the video HELLO always completes a generation's set), then the
|
|
// current-status snapshot, then the cached keyframe so they can render
|
|
// immediately instead of waiting for the next GOP.
|
|
if (hello_audio_)
|
|
EnqueueLocked(*client, hello_audio_);
|
|
if (hello_video_)
|
|
EnqueueLocked(*client, hello_video_);
|
|
if (!snapshot_payload_.empty()) {
|
|
EnqueueLocked(*client, MakeMessage(MessageType::Event, StreamId::Monitor, 0,
|
|
event_sequence_, 0,
|
|
std::vector<uint8_t>(snapshot_payload_), true));
|
|
}
|
|
if (keyframe_)
|
|
EnqueueLocked(*client, keyframe_);
|
|
|
|
Info("StreamSocket: monitor %u client connected uid %u pid %d (%zu clients)",
|
|
monitor_id_, uid, pid, clients_.size() + 1);
|
|
clients_.push_back(std::move(client));
|
|
}
|
|
}
|
|
|
|
void StreamSocket::Wake() {
|
|
if (wake_.isOpen()) {
|
|
uint8_t byte = 0;
|
|
if (::write(wake_.getWriteDesc(), &byte, 1) < 0 and errno != EAGAIN) {
|
|
Debug(1, "StreamSocket: wake write failed: %s", strerror(errno));
|
|
}
|
|
}
|
|
}
|
|
|
|
void StreamSocket::Run() {
|
|
std::vector<pollfd> fds;
|
|
|
|
while (!terminate_) {
|
|
fds.clear();
|
|
fds.push_back({listener_.getDesc(), POLLIN, 0});
|
|
fds.push_back({wake_.getReadDesc(), POLLIN, 0});
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
for (std::unique_ptr<Client> &client : clients_) {
|
|
short events = POLLIN;
|
|
if (!client->queue.empty())
|
|
events |= POLLOUT;
|
|
fds.push_back({client->sock->getDesc(), events, 0});
|
|
}
|
|
}
|
|
|
|
// With no clients there is nothing to drain or report; sleep until a
|
|
// connection or a wake. Idle monitors cost no periodic wakeups.
|
|
int timeout = fds.size() > 2 ? 100 : -1;
|
|
int ready = ::poll(fds.data(), fds.size(), timeout);
|
|
if (ready < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
Error("StreamSocket: poll failed on %s: %s", path_.c_str(), strerror(errno));
|
|
break;
|
|
}
|
|
if (terminate_)
|
|
break;
|
|
|
|
if (fds[1].revents & POLLIN) {
|
|
uint8_t buffer[64];
|
|
while (::read(wake_.getReadDesc(), buffer, sizeof(buffer)) > 0) {}
|
|
}
|
|
|
|
if (fds[0].revents & POLLIN)
|
|
AcceptClient();
|
|
|
|
// clients_ and each Client's socket are owned by this thread; only the
|
|
// queues and counters are shared with producers, so the lock is taken
|
|
// just around those, never around the socket writes.
|
|
TimePoint now = std::chrono::steady_clock::now();
|
|
size_t fd_index = 2;
|
|
for (auto it = clients_.begin(); it != clients_.end();) {
|
|
Client &client = **it;
|
|
short revents = 0;
|
|
if (fd_index < fds.size() and fds[fd_index].fd == client.sock->getDesc()) {
|
|
revents = fds[fd_index].revents;
|
|
++fd_index;
|
|
}
|
|
|
|
bool remove = false;
|
|
if (revents & (POLLERR | POLLHUP | POLLNVAL))
|
|
remove = true;
|
|
|
|
if (!remove and (revents & POLLIN)) {
|
|
// Protocol v1 has no client->server messages; discard inbound bytes,
|
|
// a zero read means the peer closed.
|
|
uint8_t buffer[256];
|
|
ssize_t bytes = ::recv(client.sock->getDesc(), buffer, sizeof(buffer), 0);
|
|
if (bytes == 0)
|
|
remove = true;
|
|
}
|
|
|
|
if (!remove)
|
|
remove = !DrainClient(client);
|
|
|
|
if (!remove) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
SendStatsLocked(client, now);
|
|
if (client.queued_bytes > 0 and now - client.last_progress > config_.stall_timeout) {
|
|
Warning("StreamSocket: monitor %u disconnecting stalled client uid %u pid %d"
|
|
" (%zu bytes queued, %" PRIu64 " dropped)",
|
|
monitor_id_, client.uid, client.pid, client.queued_bytes, client.dropped);
|
|
remove = true;
|
|
}
|
|
}
|
|
|
|
if (remove) {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
Info("StreamSocket: monitor %u client uid %u pid %d disconnected"
|
|
" (sent %" PRIu64 ", dropped %" PRIu64 ")",
|
|
monitor_id_, client.uid, client.pid, client.sent, client.dropped);
|
|
it = clients_.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Best-effort BYE so consumers can tell shutdown from failure
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
MessagePtr bye = MakeMessage(MessageType::Bye, StreamId::Video, 0, 0, 0, {}, true);
|
|
for (std::unique_ptr<Client> &client : clients_)
|
|
EnqueueLocked(*client, bye);
|
|
}
|
|
for (std::unique_ptr<Client> &client : clients_)
|
|
DrainClient(*client);
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
clients_.clear();
|
|
}
|