blob: 6e5cab894a93df096e88413bbe2b2314a4f5b3fb [file] [log] [blame]
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
* Copyright (C) 2019, Google Inc.
*
* ipc_unixsocket.cpp - IPC mechanism based on Unix sockets
*/
#include "ipc_unixsocket.h"
#include <poll.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include "log.h"
/**
* \file ipc_unixsocket.h
* \brief IPC mechanism based on Unix sockets
*/
namespace libcamera {
LOG_DEFINE_CATEGORY(IPCUnixSocket)
/**
* \struct IPCUnixSocket::Payload
* \brief Container for an IPC payload
*
* Holds an array of bytes and an array of file descriptors that can be
* transported across a IPC boundary.
*/
/**
* \var IPCUnixSocket::Payload::data
* \brief Array of bytes to cross IPC boundary
*/
/**
* \var IPCUnixSocket::Payload::fds
* \brief Array of file descriptors to cross IPC boundary
*/
/**
* \class IPCUnixSocket
* \brief IPC mechanism based on Unix sockets
*
* The Unix socket IPC allows bidirectional communication between two processes
* through unnamed Unix sockets. It implements datagram-based communication,
* transporting entire payloads with guaranteed ordering.
*
* The IPC design is asynchronous, a message is queued to a receiver which gets
* notified that a message is ready to be consumed by the \ref readyRead
* signal. The sender of the message gets no notification when a message is
* delivered nor processed. If such interactions are needed a protocol specific
* to the users use-case should be implemented on top of the IPC objects.
*
* Establishment of an IPC channel is asymmetrical. The side that initiates
* communication first instantiates a local side socket and creates the channel
* with create(). The method returns a file descriptor for the remote side of
* the channel, which is passed to the remote process through an out-of-band
* communication method. The remote side then instantiates a socket, and binds
* it to the other side by passing the file descriptor to bind(). At that point
* the channel is operation and communication is bidirectional and symmmetrical.
*
* \context This class is \threadbound.
*/
IPCUnixSocket::IPCUnixSocket()
: fd_(-1), headerReceived_(false), notifier_(nullptr)
{
}
IPCUnixSocket::~IPCUnixSocket()
{
close();
}
/**
* \brief Create an new IPC channel
*
* This method creates a new IPC channel. The socket instance is bound to the
* local side of the channel, and the method returns a file descriptor bound to
* the remote side. The caller is responsible for passing the file descriptor to
* the remote process, where it can be used with IPCUnixSocket::bind() to bind
* the remote side socket.
*
* \return A file descriptor on success, negative error code on failure
*/
int IPCUnixSocket::create()
{
int sockets[2];
int ret;
ret = socketpair(AF_UNIX, SOCK_DGRAM | SOCK_NONBLOCK, 0, sockets);
if (ret) {
ret = -errno;
LOG(IPCUnixSocket, Error)
<< "Failed to create socket pair: " << strerror(-ret);
return ret;
}
ret = bind(sockets[0]);
if (ret)
return ret;
return sockets[1];
}
/**
* \brief Bind to an existing IPC channel
* \param[in] fd File descriptor
*
* This method binds the socket instance to an existing IPC channel identified
* by the file descriptor \a fd. The file descriptor is obtained from the
* IPCUnixSocket::create() method.
*
* \return 0 on success or a negative error code otherwise
*/
int IPCUnixSocket::bind(int fd)
{
if (isBound())
return -EINVAL;
fd_ = fd;
notifier_ = new EventNotifier(fd_, EventNotifier::Read);
notifier_->activated.connect(this, &IPCUnixSocket::dataNotifier);
return 0;
}
/**
* \brief Close the IPC channel
*
* No communication is possible after close() has been called.
*/
void IPCUnixSocket::close()
{
if (!isBound())
return;
delete notifier_;
notifier_ = nullptr;
::close(fd_);
fd_ = -1;
headerReceived_ = false;
}
/**
* \brief Check if the IPC channel is bound
* \return True if the IPC channel is bound, false otherwise
*/
bool IPCUnixSocket::isBound() const
{
return fd_ != -1;
}
/**
* \brief Send a message payload
* \param[in] payload Message payload to send
*
* This method queues the message payload for transmission to the other end of
* the IPC channel. It returns immediately, before the message is delivered to
* the remote side.
*
* \return 0 on success or a negative error code otherwise
*/
int IPCUnixSocket::send(const Payload &payload)
{
int ret;
if (!isBound())
return -ENOTCONN;
Header hdr = {};
hdr.data = payload.data.size();
hdr.fds = payload.fds.size();
if (!hdr.data && !hdr.fds)
return -EINVAL;
ret = ::send(fd_, &hdr, sizeof(hdr), 0);
if (ret < 0) {
ret = -errno;
LOG(IPCUnixSocket, Error)
<< "Failed to send: " << strerror(-ret);
return ret;
}
return sendData(payload.data.data(), hdr.data, payload.fds.data(), hdr.fds);
}
/**
* \brief Receive a message payload
* \param[out] payload Payload where to write the received message
*
* This method receives the message payload from the IPC channel and writes it
* to the \a payload. If no message payload is available, it returns
* immediately with -EAGAIN. The \ref readyRead signal shall be used to receive
* notification of message availability.
*
* \todo Add state machine to make sure we don't block forever and that
* a header is always followed by a payload.
*
* \return 0 on success or a negative error code otherwise
* \retval -EAGAIN No message payload is available
* \retval -ENOTCONN The socket is not connected (neither create() nor bind()
* has been called)
*/
int IPCUnixSocket::receive(Payload *payload)
{
if (!isBound())
return -ENOTCONN;
if (!headerReceived_)
return -EAGAIN;
payload->data.resize(header_.data);
payload->fds.resize(header_.fds);
int ret = recvData(payload->data.data(), header_.data,
payload->fds.data(), header_.fds);
if (ret < 0)
return ret;
headerReceived_ = false;
notifier_->setEnabled(true);
return 0;
}
/**
* \var IPCUnixSocket::readyRead
* \brief A Signal emitted when a message is ready to be read
*/
int IPCUnixSocket::sendData(const void *buffer, size_t length,
const int32_t *fds, unsigned int num)
{
struct iovec iov[1];
iov[0].iov_base = const_cast<void *>(buffer);
iov[0].iov_len = length;
char buf[CMSG_SPACE(num * sizeof(uint32_t))];
memset(buf, 0, sizeof(buf));
struct cmsghdr *cmsg = (struct cmsghdr *)buf;
cmsg->cmsg_len = CMSG_LEN(num * sizeof(uint32_t));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
struct msghdr msg;
msg.msg_name = nullptr;
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg;
msg.msg_controllen = cmsg->cmsg_len;
msg.msg_flags = 0;
memcpy(CMSG_DATA(cmsg), fds, num * sizeof(uint32_t));
if (sendmsg(fd_, &msg, 0) < 0) {
int ret = -errno;
LOG(IPCUnixSocket, Error)
<< "Failed to sendmsg: " << strerror(-ret);
return ret;
}
return 0;
}
int IPCUnixSocket::recvData(void *buffer, size_t length,
int32_t *fds, unsigned int num)
{
struct iovec iov[1];
iov[0].iov_base = buffer;
iov[0].iov_len = length;
char buf[CMSG_SPACE(num * sizeof(uint32_t))];
memset(buf, 0, sizeof(buf));
struct cmsghdr *cmsg = (struct cmsghdr *)buf;
cmsg->cmsg_len = CMSG_LEN(num * sizeof(uint32_t));
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
struct msghdr msg;
msg.msg_name = nullptr;
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsg;
msg.msg_controllen = cmsg->cmsg_len;
msg.msg_flags = 0;
if (recvmsg(fd_, &msg, 0) < 0) {
int ret = -errno;
if (ret != -EAGAIN)
LOG(IPCUnixSocket, Error)
<< "Failed to recvmsg: " << strerror(-ret);
return ret;
}
memcpy(fds, CMSG_DATA(cmsg), num * sizeof(uint32_t));
return 0;
}
void IPCUnixSocket::dataNotifier(EventNotifier *notifier)
{
int ret;
if (!headerReceived_) {
/* Receive the header. */
ret = ::recv(fd_, &header_, sizeof(header_), 0);
if (ret < 0) {
ret = -errno;
LOG(IPCUnixSocket, Error)
<< "Failed to receive header: " << strerror(-ret);
return;
}
headerReceived_ = true;
}
/*
* If the payload has arrived, disable the notifier and emit the
* readyRead signal. The notifier will be reenabled by the receive()
* method.
*/
struct pollfd fds = { fd_, POLLIN, 0 };
ret = poll(&fds, 1, 0);
if (ret < 0)
return;
if (!(fds.revents & POLLIN))
return;
notifier_->setEnabled(false);
readyRead.emit(this);
}
} /* namespace libcamera */