/*
|
* BlueALSA - ctl.c
|
* Copyright (c) 2016-2018 Arkadiusz Bokowy
|
*
|
* This file is a part of bluez-alsa.
|
*
|
* This project is licensed under the terms of the MIT license.
|
*
|
*/
|
|
#define _GNU_SOURCE
|
#include "ctl.h"
|
|
#include <errno.h>
|
#include <poll.h>
|
#include <pthread.h>
|
#include <stdio.h>
|
#include <stdlib.h>
|
#include <string.h>
|
#include <unistd.h>
|
#include <sys/socket.h>
|
#include <sys/stat.h>
|
#include <sys/types.h>
|
#include <sys/un.h>
|
|
#include <glib.h>
|
|
#include "a2dp-codecs.h"
|
#include "bluealsa.h"
|
#include "bluez.h"
|
#include "hfp.h"
|
#include "transport.h"
|
#include "utils.h"
|
#include "shared/defs.h"
|
#include "shared/log.h"
|
|
|
/**
|
* Looks up a transport matching BT address and profile.
|
*
|
* This function is not thread-safe. It returns references to objects managed
|
* by the devices hash-table. If the devices hash-table is modified in some
|
* other thread, it may result in an undefined behavior.
|
*
|
* @param devices Address of the hash-table with connected devices.
|
* @param addr Address to the structure with the looked up BT address.
|
* @param type Looked up PCM type.
|
* @param stream Looked up PCM stream direction.
|
* @param t Address, where the transport structure pointer should be stored.
|
* @return If the lookup succeeded, this function returns 0. Otherwise, -1 or
|
* -2 is returned respectively for not found device and not found stream.
|
* Upon error value of the transport pointer is undefined. */
|
static int _transport_lookup(GHashTable *devices, const bdaddr_t *addr,
|
enum ba_pcm_type type, enum ba_pcm_stream stream, struct ba_transport **t) {
|
|
bool device_found = false;
|
GHashTableIter iter_d, iter_t;
|
struct ba_device *d;
|
|
for (g_hash_table_iter_init(&iter_d, devices);
|
g_hash_table_iter_next(&iter_d, NULL, (gpointer)&d); ) {
|
|
if (bacmp(&d->addr, addr) != 0)
|
continue;
|
|
device_found = true;
|
|
for (g_hash_table_iter_init(&iter_t, d->transports);
|
g_hash_table_iter_next(&iter_t, NULL, (gpointer)t); ) {
|
|
switch (type) {
|
case BA_PCM_TYPE_NULL:
|
continue;
|
case BA_PCM_TYPE_A2DP:
|
if ((*t)->type != TRANSPORT_TYPE_A2DP)
|
continue;
|
switch (stream) {
|
case BA_PCM_STREAM_PLAYBACK:
|
if ((*t)->profile != BLUETOOTH_PROFILE_A2DP_SOURCE)
|
continue;
|
break;
|
case BA_PCM_STREAM_CAPTURE:
|
if ((*t)->profile != BLUETOOTH_PROFILE_A2DP_SINK)
|
continue;
|
break;
|
case BA_PCM_STREAM_DUPLEX:
|
continue;
|
}
|
break;
|
case BA_PCM_TYPE_SCO:
|
if ((*t)->type != TRANSPORT_TYPE_SCO)
|
continue;
|
/* ignore SCO transport if codec is not selected yet */
|
if ((*t)->codec == HFP_CODEC_UNDEFINED)
|
continue;
|
break;
|
}
|
|
return 0;
|
}
|
|
}
|
|
return device_found ? -2 : -1;
|
}
|
|
static int _transport_lookup_rfcomm(GHashTable *devices, const bdaddr_t *addr,
|
struct ba_transport **t) {
|
|
GHashTableIter iter_d, iter_t;
|
struct ba_device *d;
|
|
for (g_hash_table_iter_init(&iter_d, devices);
|
g_hash_table_iter_next(&iter_d, NULL, (gpointer)&d); ) {
|
|
if (bacmp(&d->addr, addr) != 0)
|
continue;
|
|
for (g_hash_table_iter_init(&iter_t, d->transports);
|
g_hash_table_iter_next(&iter_t, NULL, (gpointer)t); ) {
|
|
if ((*t)->type != TRANSPORT_TYPE_RFCOMM)
|
continue;
|
|
return 0;
|
}
|
}
|
|
return -1;
|
}
|
|
/**
|
* Get transport PCM structure.
|
*
|
* @param t Pointer to the transport structure.
|
* @param stream Stream type.
|
* @return On success address of the PCM structure is returned. If the PCM
|
* structure can not be determined, NULL is returned. */
|
static struct ba_pcm *_transport_get_pcm(struct ba_transport *t, enum ba_pcm_stream stream) {
|
switch (t->type) {
|
case TRANSPORT_TYPE_A2DP:
|
return &t->a2dp.pcm;
|
case TRANSPORT_TYPE_RFCOMM:
|
debug("Trying to get PCM for RFCOMM transport... that's nuts");
|
break;
|
case TRANSPORT_TYPE_SCO:
|
switch (stream) {
|
case BA_PCM_STREAM_PLAYBACK:
|
return &t->sco.spk_pcm;
|
case BA_PCM_STREAM_CAPTURE:
|
return &t->sco.mic_pcm;
|
case BA_PCM_STREAM_DUPLEX:
|
break;
|
}
|
}
|
return NULL;
|
}
|
|
/**
|
* Release transport resources acquired by the controller module. */
|
static void _transport_release_pcm(struct ba_transport *t, int client) {
|
switch (t->type) {
|
case TRANSPORT_TYPE_A2DP:
|
transport_release_pcm(&t->a2dp.pcm);
|
t->a2dp.pcm.client = -1;
|
break;
|
case TRANSPORT_TYPE_RFCOMM:
|
break;
|
case TRANSPORT_TYPE_SCO:
|
if (t->sco.spk_pcm.client == client) {
|
transport_release_pcm(&t->sco.spk_pcm);
|
t->sco.spk_pcm.client = -1;
|
}
|
if (t->sco.mic_pcm.client == client) {
|
transport_release_pcm(&t->sco.mic_pcm);
|
t->sco.mic_pcm.client = -1;
|
}
|
}
|
}
|
|
static void _ctl_transport(const struct ba_transport *t, struct ba_msg_transport *transport) {
|
|
bacpy(&transport->addr, &t->device->addr);
|
|
switch (t->type) {
|
case TRANSPORT_TYPE_A2DP:
|
transport->type = BA_PCM_TYPE_A2DP;
|
transport->stream = t->profile == BLUETOOTH_PROFILE_A2DP_SOURCE ?
|
BA_PCM_STREAM_PLAYBACK : BA_PCM_STREAM_CAPTURE;
|
transport->ch1_muted = t->a2dp.ch1_muted;
|
transport->ch1_volume = t->a2dp.ch1_volume;
|
transport->ch2_muted = t->a2dp.ch2_muted;
|
transport->ch2_volume = t->a2dp.ch2_volume;
|
transport->delay = t->a2dp.delay;
|
break;
|
case TRANSPORT_TYPE_RFCOMM:
|
transport->type = BA_PCM_TYPE_NULL;
|
break;
|
case TRANSPORT_TYPE_SCO:
|
transport->type = BA_PCM_TYPE_SCO;
|
transport->stream = BA_PCM_STREAM_DUPLEX;
|
transport->ch1_muted = t->sco.spk_muted;
|
transport->ch1_volume = t->sco.spk_gain;
|
transport->ch2_muted = t->sco.mic_muted;
|
transport->ch2_volume = t->sco.mic_gain;
|
transport->delay = 10;
|
break;
|
}
|
|
transport->codec = t->codec;
|
transport->channels = transport_get_channels(t);
|
transport->sampling = transport_get_sampling(t);
|
transport->delay += t->delay;
|
|
}
|
|
static void ctl_thread_cmd_ping(const struct ba_request *req, int fd) {
|
(void)req;
|
static const struct ba_msg_status status = { BA_STATUS_CODE_PONG };
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void ctl_thread_cmd_subscribe(const struct ba_request *req, int fd) {
|
|
static const struct ba_msg_status status = { BA_STATUS_CODE_SUCCESS };
|
size_t i;
|
|
for (i = __CTL_IDX_MAX; i < __CTL_IDX_MAX + BLUEALSA_MAX_CLIENTS; i++)
|
if (config.ctl.pfds[i].fd == fd)
|
config.ctl.subs[i - __CTL_IDX_MAX] = req->events;
|
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void ctl_thread_cmd_list_devices(const struct ba_request *req, int fd) {
|
(void)req;
|
|
static const struct ba_msg_status status = { BA_STATUS_CODE_SUCCESS };
|
struct ba_msg_device device;
|
GHashTableIter iter_d;
|
struct ba_device *d;
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
for (g_hash_table_iter_init(&iter_d, config.devices);
|
g_hash_table_iter_next(&iter_d, NULL, (gpointer)&d); ) {
|
|
bacpy(&device.addr, &d->addr);
|
strncpy(device.name, d->name, sizeof(device.name) - 1);
|
device.name[sizeof(device.name) - 1] = '\0';
|
|
device.battery = d->battery.enabled;
|
device.battery_level = d->battery.level;
|
|
send(fd, &device, sizeof(device), MSG_NOSIGNAL);
|
}
|
|
pthread_mutex_unlock(&config.devices_mutex);
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void ctl_thread_cmd_list_transports(const struct ba_request *req, int fd) {
|
(void)req;
|
|
static const struct ba_msg_status status = { BA_STATUS_CODE_SUCCESS };
|
struct ba_msg_transport transport;
|
GHashTableIter iter_d, iter_t;
|
struct ba_device *d;
|
struct ba_transport *t;
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
for (g_hash_table_iter_init(&iter_d, config.devices);
|
g_hash_table_iter_next(&iter_d, NULL, (gpointer)&d); )
|
for (g_hash_table_iter_init(&iter_t, d->transports);
|
g_hash_table_iter_next(&iter_t, NULL, (gpointer)&t); ) {
|
/* ignore SCO transport if codec is not selected yet */
|
if (t->type == TRANSPORT_TYPE_SCO && t->codec == HFP_CODEC_UNDEFINED)
|
continue;
|
_ctl_transport(t, &transport);
|
send(fd, &transport, sizeof(transport), MSG_NOSIGNAL);
|
}
|
|
pthread_mutex_unlock(&config.devices_mutex);
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void ctl_thread_cmd_transport_get(const struct ba_request *req, int fd) {
|
|
struct ba_msg_status status = { BA_STATUS_CODE_SUCCESS };
|
struct ba_msg_transport transport;
|
struct ba_transport *t;
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
switch (_transport_lookup(config.devices, &req->addr, req->type, req->stream, &t)) {
|
case -1:
|
status.code = BA_STATUS_CODE_DEVICE_NOT_FOUND;
|
goto fail;
|
case -2:
|
status.code = BA_STATUS_CODE_STREAM_NOT_FOUND;
|
goto fail;
|
}
|
|
_ctl_transport(t, &transport);
|
send(fd, &transport, sizeof(transport), MSG_NOSIGNAL);
|
|
fail:
|
pthread_mutex_unlock(&config.devices_mutex);
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void ctl_thread_cmd_transport_set_volume(const struct ba_request *req, int fd) {
|
|
struct ba_msg_status status = { BA_STATUS_CODE_SUCCESS };
|
struct ba_transport *t;
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
switch (_transport_lookup(config.devices, &req->addr, req->type, req->stream, &t)) {
|
case -1:
|
status.code = BA_STATUS_CODE_DEVICE_NOT_FOUND;
|
goto fail;
|
case -2:
|
status.code = BA_STATUS_CODE_STREAM_NOT_FOUND;
|
goto fail;
|
}
|
|
transport_set_volume(t, req->ch1_muted, req->ch2_muted, req->ch1_volume, req->ch2_volume);
|
|
fail:
|
pthread_mutex_unlock(&config.devices_mutex);
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void ctl_thread_cmd_pcm_open(const struct ba_request *req, int fd) {
|
|
struct ba_msg_status status = { BA_STATUS_CODE_SUCCESS };
|
struct ba_transport *t;
|
struct ba_pcm *t_pcm;
|
int pipefd[2];
|
|
debug("PCM requested for %s type %d stream %d", batostr_(&req->addr), req->type, req->stream);
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
switch (_transport_lookup(config.devices, &req->addr, req->type, req->stream, &t)) {
|
case -1:
|
status.code = BA_STATUS_CODE_DEVICE_NOT_FOUND;
|
goto fail_lookup;
|
case -2:
|
status.code = BA_STATUS_CODE_STREAM_NOT_FOUND;
|
goto fail_lookup;
|
}
|
|
pthread_mutex_lock(&t->mutex);
|
|
if ((t_pcm = _transport_get_pcm(t, req->stream)) == NULL) {
|
status.code = BA_STATUS_CODE_ERROR_UNKNOWN;
|
goto final;
|
}
|
|
if (t_pcm->fd != -1) {
|
debug("PCM already requested: %d", t_pcm->fd);
|
status.code = BA_STATUS_CODE_DEVICE_BUSY;
|
goto final;
|
}
|
|
if (pipe(pipefd) == -1) {
|
error("Couldn't create FIFO: %s", strerror(errno));
|
status.code = BA_STATUS_CODE_ERROR_UNKNOWN;
|
goto final;
|
}
|
|
union {
|
char buf[CMSG_SPACE(sizeof(int))];
|
struct cmsghdr _align;
|
} control_un;
|
struct iovec io = { .iov_base = "", .iov_len = 1 };
|
struct msghdr msg = {
|
.msg_iov = &io,
|
.msg_iovlen = 1,
|
.msg_control = control_un.buf,
|
.msg_controllen = sizeof(control_un.buf),
|
};
|
|
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
cmsg->cmsg_level = SOL_SOCKET;
|
cmsg->cmsg_type = SCM_RIGHTS;
|
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
|
int *fdptr = (int *)CMSG_DATA(cmsg);
|
|
switch (req->stream) {
|
case BA_PCM_STREAM_PLAYBACK:
|
t_pcm->fd = pipefd[0];
|
*fdptr = pipefd[1];
|
break;
|
case BA_PCM_STREAM_CAPTURE:
|
t_pcm->fd = pipefd[1];
|
*fdptr = pipefd[0];
|
break;
|
case BA_PCM_STREAM_DUPLEX:
|
debug("Invalid PCM stream type: %d", req->stream);
|
status.code = BA_STATUS_CODE_ERROR_UNKNOWN;
|
goto fail;
|
}
|
|
/* Notify our IO thread, that the FIFO has just been created - it may be
|
* used for poll() right away. */
|
transport_send_signal(t, TRANSPORT_PCM_OPEN);
|
|
/* A2DP source profile should be initialized (acquired) only if the audio
|
* is about to be transfered. It is most likely, that BT headset will not
|
* run voltage converter (power-on its circuit board) until the transport
|
* is acquired - in order to extend battery life. */
|
if (t->profile == BLUETOOTH_PROFILE_A2DP_SOURCE)
|
if (transport_acquire_bt_a2dp(t) == -1) {
|
status.code = BA_STATUS_CODE_ERROR_UNKNOWN;
|
goto fail;
|
}
|
|
if (sendmsg(fd, &msg, 0) == -1)
|
goto fail;
|
|
t_pcm->client = fd;
|
close(*fdptr);
|
goto final;
|
|
fail:
|
close(pipefd[0]);
|
close(pipefd[1]);
|
t_pcm->fd = -1;
|
|
final:
|
pthread_mutex_unlock(&t->mutex);
|
fail_lookup:
|
pthread_mutex_unlock(&config.devices_mutex);
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void ctl_thread_cmd_pcm_close(const struct ba_request *req, int fd) {
|
|
struct ba_msg_status status = { BA_STATUS_CODE_SUCCESS };
|
struct ba_transport *t;
|
struct ba_pcm *t_pcm;
|
|
debug("PCM close for %s type %d stream %d", batostr_(&req->addr), req->type, req->stream);
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
switch (_transport_lookup(config.devices, &req->addr, req->type, req->stream, &t)) {
|
case -1:
|
status.code = BA_STATUS_CODE_DEVICE_NOT_FOUND;
|
goto fail_lookup;
|
case -2:
|
status.code = BA_STATUS_CODE_STREAM_NOT_FOUND;
|
goto fail_lookup;
|
}
|
|
pthread_mutex_lock(&t->mutex);
|
|
if ((t_pcm = _transport_get_pcm(t, req->stream)) == NULL) {
|
status.code = BA_STATUS_CODE_ERROR_UNKNOWN;
|
goto fail;
|
}
|
|
if (t_pcm->client != fd) {
|
status.code = BA_STATUS_CODE_FORBIDDEN;
|
goto fail;
|
}
|
|
_transport_release_pcm(t, fd);
|
transport_send_signal(t, TRANSPORT_PCM_CLOSE);
|
|
fail:
|
pthread_mutex_unlock(&t->mutex);
|
fail_lookup:
|
pthread_mutex_unlock(&config.devices_mutex);
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void ctl_thread_cmd_pcm_control(const struct ba_request *req, int fd) {
|
|
struct ba_msg_status status = { BA_STATUS_CODE_SUCCESS };
|
struct ba_transport *t;
|
struct ba_pcm *t_pcm;
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
switch (_transport_lookup(config.devices, &req->addr, req->type, req->stream, &t)) {
|
case -1:
|
status.code = BA_STATUS_CODE_DEVICE_NOT_FOUND;
|
goto fail;
|
case -2:
|
status.code = BA_STATUS_CODE_STREAM_NOT_FOUND;
|
goto fail;
|
}
|
if ((t_pcm = _transport_get_pcm(t, req->stream)) == NULL) {
|
status.code = BA_STATUS_CODE_ERROR_UNKNOWN;
|
goto fail;
|
}
|
if (t_pcm->fd == -1 || t_pcm->client == -1) {
|
status.code = BA_STATUS_CODE_ERROR_UNKNOWN;
|
goto fail;
|
}
|
if (t_pcm->client != fd) {
|
status.code = BA_STATUS_CODE_FORBIDDEN;
|
goto fail;
|
}
|
|
switch (req->command) {
|
case BA_COMMAND_PCM_PAUSE:
|
transport_set_state(t, TRANSPORT_PAUSED);
|
transport_send_signal(t, TRANSPORT_PCM_PAUSE);
|
break;
|
case BA_COMMAND_PCM_RESUME:
|
transport_set_state(t, TRANSPORT_ACTIVE);
|
transport_send_signal(t, TRANSPORT_PCM_RESUME);
|
break;
|
case BA_COMMAND_PCM_DRAIN:
|
transport_drain_pcm(t);
|
break;
|
default:
|
warn("Invalid PCM control command: %d", req->command);
|
}
|
|
fail:
|
pthread_mutex_unlock(&config.devices_mutex);
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void ctl_thread_cmd_rfcomm_send(const struct ba_request *req, int fd) {
|
|
struct ba_msg_status status = { BA_STATUS_CODE_SUCCESS };
|
struct ba_transport *t;
|
|
pthread_mutex_lock(&config.devices_mutex);
|
|
if (_transport_lookup_rfcomm(config.devices, &req->addr, &t) != 0) {
|
status.code = BA_STATUS_CODE_DEVICE_NOT_FOUND;
|
goto fail;
|
}
|
|
transport_send_rfcomm(t, req->rfcomm_command);
|
|
fail:
|
pthread_mutex_unlock(&config.devices_mutex);
|
send(fd, &status, sizeof(status), MSG_NOSIGNAL);
|
}
|
|
static void *ctl_thread(void *arg) {
|
(void)arg;
|
|
static void (*commands[__BA_COMMAND_MAX])(const struct ba_request *, int) = {
|
[BA_COMMAND_PING] = ctl_thread_cmd_ping,
|
[BA_COMMAND_SUBSCRIBE] = ctl_thread_cmd_subscribe,
|
[BA_COMMAND_LIST_DEVICES] = ctl_thread_cmd_list_devices,
|
[BA_COMMAND_LIST_TRANSPORTS] = ctl_thread_cmd_list_transports,
|
[BA_COMMAND_TRANSPORT_GET] = ctl_thread_cmd_transport_get,
|
[BA_COMMAND_TRANSPORT_SET_VOLUME] = ctl_thread_cmd_transport_set_volume,
|
[BA_COMMAND_PCM_OPEN] = ctl_thread_cmd_pcm_open,
|
[BA_COMMAND_PCM_CLOSE] = ctl_thread_cmd_pcm_close,
|
[BA_COMMAND_PCM_PAUSE] = ctl_thread_cmd_pcm_control,
|
[BA_COMMAND_PCM_RESUME] = ctl_thread_cmd_pcm_control,
|
[BA_COMMAND_PCM_DRAIN] = ctl_thread_cmd_pcm_control,
|
[BA_COMMAND_RFCOMM_SEND] = ctl_thread_cmd_rfcomm_send,
|
};
|
|
debug("Starting controller loop");
|
while (config.ctl.thread_created) {
|
|
if (poll(config.ctl.pfds, ARRAYSIZE(config.ctl.pfds), -1) == -1) {
|
if (errno == EINTR)
|
continue;
|
error("Controller poll error: %s", strerror(errno));
|
break;
|
}
|
|
/* Clients handling loop will update this variable to point to the
|
* first available client structure, which might be later used by
|
* the connection handling loop. */
|
struct pollfd *pfd = NULL;
|
size_t i;
|
|
/* handle data transmission with connected clients */
|
for (i = __CTL_IDX_MAX; i < __CTL_IDX_MAX + BLUEALSA_MAX_CLIENTS; i++) {
|
const int fd = config.ctl.pfds[i].fd;
|
|
if (fd == -1) {
|
pfd = &config.ctl.pfds[i];
|
continue;
|
}
|
|
if (config.ctl.pfds[i].revents & POLLIN) {
|
|
struct ba_request request;
|
ssize_t len;
|
|
if ((len = recv(fd, &request, sizeof(request), MSG_DONTWAIT)) != sizeof(request)) {
|
/* if the request cannot be retrieved, release resources */
|
|
if (len == 0)
|
debug("Client closed connection: %d", fd);
|
else
|
debug("Invalid request length: %zd != %zd", len, sizeof(request));
|
|
struct ba_transport *t;
|
if ((t = transport_lookup_pcm_client(config.devices, fd)) != NULL) {
|
_transport_release_pcm(t, fd);
|
transport_send_signal(t, TRANSPORT_PCM_CLOSE);
|
}
|
|
config.ctl.pfds[i].fd = -1;
|
config.ctl.subs[i - __CTL_IDX_MAX] = 0;
|
close(fd);
|
continue;
|
}
|
|
/* validate and execute requested command */
|
if (request.command < __BA_COMMAND_MAX && commands[request.command] != NULL)
|
commands[request.command](&request, fd);
|
else
|
warn("Invalid command: %u", request.command);
|
|
}
|
|
}
|
|
/* process new connections to our controller */
|
if (config.ctl.pfds[CTL_IDX_SRV].revents & POLLIN && pfd != NULL) {
|
|
struct pollfd fd = { -1, POLLIN, 0 };
|
uint16_t ver = 0;
|
|
fd.fd = accept(config.ctl.pfds[CTL_IDX_SRV].fd, NULL, NULL);
|
debug("Received new connection: %d", fd.fd);
|
|
errno = ETIMEDOUT;
|
if (poll(&fd, 1, 500) <= 0 ||
|
recv(fd.fd, &ver, sizeof(ver), MSG_DONTWAIT) != sizeof(ver)) {
|
warn("Couldn't receive protocol version: %s", strerror(errno));
|
close(fd.fd);
|
}
|
else if (ver != BLUEALSA_CRL_PROTO_VERSION) {
|
warn("Invalid protocol version: %#06x != %#06x", ver, BLUEALSA_CRL_PROTO_VERSION);
|
close(fd.fd);
|
}
|
else {
|
debug("New client accepted: %d", fd.fd);
|
pfd->fd = fd.fd;
|
}
|
|
}
|
|
/* generate notifications for subscribed clients */
|
if (config.ctl.pfds[CTL_IDX_EVT].revents & POLLIN) {
|
|
struct ba_msg_event event = { 0 };
|
size_t i;
|
|
if (read(config.ctl.pfds[CTL_IDX_EVT].fd, &event.mask, sizeof(event.mask)) == -1)
|
warn("Couldn't read controller event: %s", strerror(errno));
|
|
for (i = 0; i < BLUEALSA_MAX_CLIENTS; i++)
|
if (config.ctl.subs[i] & event.mask) {
|
const int client = config.ctl.pfds[i + __CTL_IDX_MAX].fd;
|
debug("Emitting notification: %B => %d", event.mask, client);
|
send(client, &event, sizeof(event), MSG_NOSIGNAL);
|
}
|
|
}
|
|
debug("+-+-");
|
}
|
|
debug("Exiting controller thread");
|
return NULL;
|
}
|
|
int bluealsa_ctl_thread_init(void) {
|
|
if (config.ctl.thread_created) {
|
/* thread is already created */
|
errno = EISCONN;
|
return -1;
|
}
|
|
{ /* initialize (mark as closed) all sockets */
|
size_t i;
|
for (i = 0; i < ARRAYSIZE(config.ctl.pfds); i++) {
|
config.ctl.pfds[i].events = POLLIN;
|
config.ctl.pfds[i].fd = -1;
|
}
|
}
|
|
struct sockaddr_un saddr = { .sun_family = AF_UNIX };
|
snprintf(saddr.sun_path, sizeof(saddr.sun_path) - 1,
|
BLUEALSA_RUN_STATE_DIR "/%s", config.hci_dev.name);
|
|
if (mkdir(BLUEALSA_RUN_STATE_DIR, 0755) == -1 && errno != EEXIST) {
|
error("Couldn't create run-state directory: %s", strerror(errno));
|
goto fail;
|
}
|
if ((config.ctl.pfds[CTL_IDX_SRV].fd = socket(PF_UNIX, SOCK_SEQPACKET, 0)) == -1) {
|
error("Couldn't create controller socket: %s", strerror(errno));
|
goto fail;
|
}
|
if (bind(config.ctl.pfds[CTL_IDX_SRV].fd, (struct sockaddr *)(&saddr), sizeof(saddr)) == -1) {
|
error("Couldn't bind controller socket: %s", strerror(errno));
|
goto fail;
|
}
|
config.ctl.socket_created = true;
|
if (chmod(saddr.sun_path, 0660) == -1 ||
|
chown(saddr.sun_path, -1, config.gid_audio) == -1) {
|
error("Couldn't set permission for controller socket: %s", strerror(errno));
|
goto fail;
|
}
|
if (listen(config.ctl.pfds[CTL_IDX_SRV].fd, 2) == -1) {
|
error("Couldn't listen on controller socket: %s", strerror(errno));
|
goto fail;
|
}
|
|
if (pipe(config.ctl.evt) == -1) {
|
error("Couldn't create controller event PIPE: %s", strerror(errno));
|
goto fail;
|
}
|
config.ctl.pfds[CTL_IDX_EVT].fd = config.ctl.evt[0];
|
|
config.ctl.thread_created = true;
|
if ((errno = pthread_create(&config.ctl.thread, NULL, ctl_thread, NULL)) != 0) {
|
error("Couldn't create controller thread: %s", strerror(errno));
|
config.ctl.thread_created = false;
|
goto fail;
|
}
|
|
/* name controller thread - for aesthetic purposes only */
|
pthread_setname_np(config.ctl.thread, "bactl");
|
|
return 0;
|
|
fail:
|
bluealsa_ctl_free();
|
return -1;
|
}
|
|
void bluealsa_ctl_free(void) {
|
|
int created = config.ctl.thread_created;
|
size_t i;
|
|
config.ctl.thread_created = false;
|
|
if (config.ctl.evt[0] != -1)
|
close(config.ctl.evt[0]);
|
if (config.ctl.evt[1] != -1)
|
close(config.ctl.evt[1]);
|
|
config.ctl.pfds[CTL_IDX_EVT].fd = -1;
|
for (i = 0; i < ARRAYSIZE(config.ctl.pfds); i++)
|
if (config.ctl.pfds[i].fd != -1)
|
close(config.ctl.pfds[i].fd);
|
|
if (created) {
|
pthread_cancel(config.ctl.thread);
|
if ((errno = pthread_join(config.ctl.thread, NULL)) != 0)
|
error("Couldn't join controller thread: %s", strerror(errno));
|
}
|
|
if (config.ctl.socket_created) {
|
char tmp[256] = BLUEALSA_RUN_STATE_DIR "/";
|
unlink(strcat(tmp, config.hci_dev.name));
|
config.ctl.socket_created = false;
|
}
|
|
}
|
|
int bluealsa_ctl_event(enum ba_event event) {
|
return write(config.ctl.evt[1], &event, sizeof(event));
|
}
|