/* * 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 #include #include #include #include #include #include #include #include #include #include #include #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)); }