blob: 2c7988620a582674a8c3a31ed0e8a6c13d6ec0ed [file] [log] [blame]
/*
*
* BlueZ - Bluetooth protocol stack for Linux
*
* Copyright (C) 2012 Intel Corporation. All rights reserved.
*
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <glib.h>
#include "lib/bluetooth.h"
#include "lib/mgmt.h"
#include "lib/hci.h"
#include "src/shared/util.h"
#include "src/shared/mgmt.h"
struct mgmt {
int ref_count;
int fd;
bool close_on_unref;
GIOChannel *io;
guint read_watch;
guint write_watch;
GQueue *request_queue;
GQueue *reply_queue;
GList *pending_list;
GList *notify_list;
GList *notify_destroyed;
unsigned int next_request_id;
unsigned int next_notify_id;
bool in_notify;
bool destroyed;
void *buf;
uint16_t len;
mgmt_debug_func_t debug_callback;
mgmt_destroy_func_t debug_destroy;
void *debug_data;
};
struct mgmt_request {
unsigned int id;
uint16_t opcode;
uint16_t index;
void *buf;
uint16_t len;
mgmt_request_func_t callback;
mgmt_destroy_func_t destroy;
void *user_data;
};
struct mgmt_notify {
unsigned int id;
uint16_t event;
uint16_t index;
bool destroyed;
mgmt_notify_func_t callback;
mgmt_destroy_func_t destroy;
void *user_data;
};
static void destroy_request(gpointer data, gpointer user_data)
{
struct mgmt_request *request = data;
if (request->destroy)
request->destroy(request->user_data);
g_free(request->buf);
g_free(request);
}
static int compare_request_id(gconstpointer a, gconstpointer b)
{
const struct mgmt_request *request = a;
unsigned int id = GPOINTER_TO_UINT(b);
return request->id - id;
}
static void destroy_notify(gpointer data, gpointer user_data)
{
struct mgmt_notify *notify = data;
if (notify->destroy)
notify->destroy(notify->user_data);
g_free(notify);
}
static int compare_notify_id(gconstpointer a, gconstpointer b)
{
const struct mgmt_notify *notify = a;
unsigned int id = GPOINTER_TO_UINT(b);
return notify->id - id;
}
static void write_watch_destroy(gpointer user_data)
{
struct mgmt *mgmt = user_data;
mgmt->write_watch = 0;
}
static gboolean can_write_data(GIOChannel *channel, GIOCondition cond,
gpointer user_data)
{
struct mgmt *mgmt = user_data;
struct mgmt_request *request;
ssize_t bytes_written;
if (cond & (G_IO_HUP | G_IO_ERR | G_IO_NVAL))
return FALSE;
request = g_queue_pop_head(mgmt->reply_queue);
if (!request) {
/* only reply commands can jump the queue */
if (mgmt->pending_list)
return FALSE;
request = g_queue_pop_head(mgmt->request_queue);
if (!request)
return FALSE;
}
bytes_written = write(mgmt->fd, request->buf, request->len);
if (bytes_written < 0) {
util_debug(mgmt->debug_callback, mgmt->debug_data,
"write failed: %s", strerror(errno));
if (request->callback)
request->callback(MGMT_STATUS_FAILED, 0, NULL,
request->user_data);
destroy_request(request, NULL);
return TRUE;
}
util_debug(mgmt->debug_callback, mgmt->debug_data,
"[0x%04x] command 0x%04x",
request->index, request->opcode);
util_hexdump('<', request->buf, bytes_written,
mgmt->debug_callback, mgmt->debug_data);
mgmt->pending_list = g_list_append(mgmt->pending_list, request);
return FALSE;
}
static void wakeup_writer(struct mgmt *mgmt)
{
if (mgmt->pending_list) {
/* only queued reply commands trigger wakeup */
if (g_queue_get_length(mgmt->reply_queue) == 0)
return;
}
if (mgmt->write_watch > 0)
return;
mgmt->write_watch = g_io_add_watch_full(mgmt->io, G_PRIORITY_HIGH,
G_IO_OUT | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
can_write_data, mgmt, write_watch_destroy);
}
static GList *lookup_pending(struct mgmt *mgmt, uint16_t opcode, uint16_t index)
{
GList *list;
for (list = g_list_first(mgmt->pending_list); list;
list = g_list_next(list)) {
struct mgmt_request *request = list->data;
if (request->opcode == opcode && request->index == index)
return list;
}
return NULL;
}
static void request_complete(struct mgmt *mgmt, uint8_t status,
uint16_t opcode, uint16_t index,
uint16_t length, const void *param)
{
struct mgmt_request *request;
GList *list;
list = lookup_pending(mgmt, opcode, index);
if (!list)
return;
request = list->data;
mgmt->pending_list = g_list_delete_link(mgmt->pending_list, list);
if (request->callback)
request->callback(status, length, param, request->user_data);
destroy_request(request, NULL);
if (mgmt->destroyed)
return;
wakeup_writer(mgmt);
}
static void process_notify(struct mgmt *mgmt, uint16_t event, uint16_t index,
uint16_t length, const void *param)
{
GList *list;
mgmt->in_notify = true;
for (list = g_list_first(mgmt->notify_list); list;
list = g_list_next(list)) {
struct mgmt_notify *notify = list->data;
if (notify->destroyed)
continue;
if (notify->event != event)
continue;
if (notify->index != index && notify->index != MGMT_INDEX_NONE)
continue;
if (notify->callback)
notify->callback(index, length, param,
notify->user_data);
if (mgmt->destroyed)
break;
}
mgmt->in_notify = false;
g_list_foreach(mgmt->notify_destroyed, destroy_notify, NULL);
g_list_free(mgmt->notify_destroyed);
mgmt->notify_destroyed = NULL;
}
static void read_watch_destroy(gpointer user_data)
{
struct mgmt *mgmt = user_data;
if (mgmt->destroyed) {
g_free(mgmt);
return;
}
mgmt->read_watch = 0;
}
static gboolean received_data(GIOChannel *channel, GIOCondition cond,
gpointer user_data)
{
struct mgmt *mgmt = user_data;
struct mgmt_hdr *hdr;
struct mgmt_ev_cmd_complete *cc;
struct mgmt_ev_cmd_status *cs;
ssize_t bytes_read;
uint16_t opcode, event, index, length;
if (cond & (G_IO_HUP | G_IO_ERR | G_IO_NVAL))
return FALSE;
bytes_read = read(mgmt->fd, mgmt->buf, mgmt->len);
if (bytes_read < 0)
return TRUE;
util_hexdump('>', mgmt->buf, bytes_read,
mgmt->debug_callback, mgmt->debug_data);
if (bytes_read < MGMT_HDR_SIZE)
return TRUE;
hdr = mgmt->buf;
event = btohs(hdr->opcode);
index = btohs(hdr->index);
length = btohs(hdr->len);
if (bytes_read < length + MGMT_HDR_SIZE)
return TRUE;
switch (event) {
case MGMT_EV_CMD_COMPLETE:
cc = mgmt->buf + MGMT_HDR_SIZE;
opcode = btohs(cc->opcode);
util_debug(mgmt->debug_callback, mgmt->debug_data,
"[0x%04x] command 0x%04x complete: 0x%02x",
index, opcode, cc->status);
request_complete(mgmt, cc->status, opcode, index, length - 3,
mgmt->buf + MGMT_HDR_SIZE + 3);
break;
case MGMT_EV_CMD_STATUS:
cs = mgmt->buf + MGMT_HDR_SIZE;
opcode = btohs(cs->opcode);
util_debug(mgmt->debug_callback, mgmt->debug_data,
"[0x%04x] command 0x%02x status: 0x%02x",
index, opcode, cs->status);
request_complete(mgmt, cs->status, opcode, index, 0, NULL);
break;
default:
util_debug(mgmt->debug_callback, mgmt->debug_data,
"[0x%04x] event 0x%04x", index, event);
process_notify(mgmt, event, index, length,
mgmt->buf + MGMT_HDR_SIZE);
break;
}
if (mgmt->destroyed)
return FALSE;
return TRUE;
}
struct mgmt *mgmt_new(int fd)
{
struct mgmt *mgmt;
if (fd < 0)
return NULL;
mgmt = g_try_new0(struct mgmt, 1);
if (!mgmt)
return NULL;
mgmt->fd = fd;
mgmt->close_on_unref = false;
mgmt->len = 512;
mgmt->buf = g_try_malloc(mgmt->len);
if (!mgmt->buf) {
g_free(mgmt);
return NULL;
}
mgmt->io = g_io_channel_unix_new(mgmt->fd);
g_io_channel_set_encoding(mgmt->io, NULL, NULL);
g_io_channel_set_buffered(mgmt->io, FALSE);
mgmt->request_queue = g_queue_new();
mgmt->reply_queue = g_queue_new();
mgmt->read_watch = g_io_add_watch_full(mgmt->io, G_PRIORITY_DEFAULT,
G_IO_IN | G_IO_HUP | G_IO_ERR | G_IO_NVAL,
received_data, mgmt, read_watch_destroy);
return mgmt_ref(mgmt);
}
struct mgmt *mgmt_new_default(void)
{
struct mgmt *mgmt;
struct sockaddr_hci addr;
int fd;
fd = socket(PF_BLUETOOTH, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK,
BTPROTO_HCI);
if (fd < 0)
return NULL;
memset(&addr, 0, sizeof(addr));
addr.hci_family = AF_BLUETOOTH;
addr.hci_dev = HCI_DEV_NONE;
addr.hci_channel = HCI_CHANNEL_CONTROL;
if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
close(fd);
return NULL;
}
mgmt = mgmt_new(fd);
if (!mgmt) {
close(fd);
return NULL;
}
mgmt->close_on_unref = true;
return mgmt;
}
struct mgmt *mgmt_ref(struct mgmt *mgmt)
{
if (!mgmt)
return NULL;
__sync_fetch_and_add(&mgmt->ref_count, 1);
return mgmt;
}
void mgmt_unref(struct mgmt *mgmt)
{
if (!mgmt)
return;
if (__sync_sub_and_fetch(&mgmt->ref_count, 1))
return;
mgmt_unregister_all(mgmt);
mgmt_cancel_all(mgmt);
g_queue_free(mgmt->reply_queue);
g_queue_free(mgmt->request_queue);
if (mgmt->write_watch > 0)
g_source_remove(mgmt->write_watch);
if (mgmt->read_watch > 0)
g_source_remove(mgmt->read_watch);
g_io_channel_unref(mgmt->io);
mgmt->io = NULL;
if (mgmt->close_on_unref)
close(mgmt->fd);
if (mgmt->debug_destroy)
mgmt->debug_destroy(mgmt->debug_data);
g_free(mgmt->buf);
mgmt->buf = NULL;
if (!mgmt->in_notify) {
g_free(mgmt);
return;
}
mgmt->destroyed = true;
}
bool mgmt_set_debug(struct mgmt *mgmt, mgmt_debug_func_t callback,
void *user_data, mgmt_destroy_func_t destroy)
{
if (!mgmt)
return false;
if (mgmt->debug_destroy)
mgmt->debug_destroy(mgmt->debug_data);
mgmt->debug_callback = callback;
mgmt->debug_destroy = destroy;
mgmt->debug_data = user_data;
return true;
}
bool mgmt_set_close_on_unref(struct mgmt *mgmt, bool do_close)
{
if (!mgmt)
return false;
mgmt->close_on_unref = do_close;
return true;
}
static struct mgmt_request *create_request(uint16_t opcode, uint16_t index,
uint16_t length, const void *param,
mgmt_request_func_t callback,
void *user_data, mgmt_destroy_func_t destroy)
{
struct mgmt_request *request;
struct mgmt_hdr *hdr;
if (!opcode)
return NULL;
if (length > 0 && !param)
return NULL;
request = g_try_new0(struct mgmt_request, 1);
if (!request)
return NULL;
request->len = length + MGMT_HDR_SIZE;
request->buf = g_try_malloc(request->len);
if (!request->buf) {
g_free(request);
return NULL;
}
if (length > 0)
memcpy(request->buf + MGMT_HDR_SIZE, param, length);
hdr = request->buf;
hdr->opcode = htobs(opcode);
hdr->index = htobs(index);
hdr->len = htobs(length);
request->opcode = opcode;
request->index = index;
request->callback = callback;
request->destroy = destroy;
request->user_data = user_data;
return request;
}
unsigned int mgmt_send(struct mgmt *mgmt, uint16_t opcode, uint16_t index,
uint16_t length, const void *param,
mgmt_request_func_t callback,
void *user_data, mgmt_destroy_func_t destroy)
{
struct mgmt_request *request;
if (!mgmt)
return 0;
request = create_request(opcode, index, length, param,
callback, user_data, destroy);
if (!request)
return 0;
if (mgmt->next_request_id < 1)
mgmt->next_request_id = 1;
request->id = mgmt->next_request_id++;
g_queue_push_tail(mgmt->request_queue, request);
wakeup_writer(mgmt);
return request->id;
}
unsigned int mgmt_reply(struct mgmt *mgmt, uint16_t opcode, uint16_t index,
uint16_t length, const void *param,
mgmt_request_func_t callback,
void *user_data, mgmt_destroy_func_t destroy)
{
struct mgmt_request *request;
if (!mgmt)
return 0;
request = create_request(opcode, index, length, param,
callback, user_data, destroy);
if (!request)
return 0;
if (mgmt->next_request_id < 1)
mgmt->next_request_id = 1;
request->id = mgmt->next_request_id++;
g_queue_push_tail(mgmt->reply_queue, request);
wakeup_writer(mgmt);
return request->id;
}
bool mgmt_cancel(struct mgmt *mgmt, unsigned int id)
{
struct mgmt_request *request;
GList *list;
if (!mgmt || !id)
return false;
list = g_queue_find_custom(mgmt->request_queue, GUINT_TO_POINTER(id),
compare_request_id);
if (list) {
request = list->data;
g_queue_delete_link(mgmt->request_queue, list);
goto done;
}
list = g_queue_find_custom(mgmt->reply_queue, GUINT_TO_POINTER(id),
compare_request_id);
if (list) {
request = list->data;
g_queue_delete_link(mgmt->reply_queue, list);
goto done;
}
list = g_list_find_custom(mgmt->pending_list, GUINT_TO_POINTER(id),
compare_request_id);
if (!list)
return false;
request = list->data;
mgmt->pending_list = g_list_delete_link(mgmt->pending_list, list);
done:
destroy_request(request, NULL);
wakeup_writer(mgmt);
return true;
}
bool mgmt_cancel_index(struct mgmt *mgmt, uint16_t index)
{
GList *list, *next;
if (!mgmt)
return false;
for (list = g_queue_peek_head_link(mgmt->request_queue); list;
list = next) {
struct mgmt_request *request = list->data;
next = g_list_next(list);
if (request->index != index)
continue;
g_queue_delete_link(mgmt->request_queue, list);
destroy_request(request, NULL);
}
for (list = g_queue_peek_head_link(mgmt->reply_queue); list;
list = next) {
struct mgmt_request *request = list->data;
next = g_list_next(list);
if (request->index != index)
continue;
g_queue_delete_link(mgmt->reply_queue, list);
destroy_request(request, NULL);
}
for (list = g_list_first(mgmt->pending_list); list; list = next) {
struct mgmt_request *request = list->data;
next = g_list_next(list);
if (request->index != index)
continue;
mgmt->pending_list = g_list_delete_link(mgmt->pending_list,
list);
destroy_request(request, NULL);
}
return true;
}
bool mgmt_cancel_all(struct mgmt *mgmt)
{
if (!mgmt)
return false;
g_list_foreach(mgmt->pending_list, destroy_request, NULL);
g_list_free(mgmt->pending_list);
mgmt->pending_list = NULL;
g_queue_foreach(mgmt->reply_queue, destroy_request, NULL);
g_queue_clear(mgmt->reply_queue);
g_queue_foreach(mgmt->request_queue, destroy_request, NULL);
g_queue_clear(mgmt->request_queue);
return true;
}
unsigned int mgmt_register(struct mgmt *mgmt, uint16_t event, uint16_t index,
mgmt_notify_func_t callback,
void *user_data, mgmt_destroy_func_t destroy)
{
struct mgmt_notify *notify;
if (!mgmt || !event)
return 0;
notify = g_try_new0(struct mgmt_notify, 1);
if (!notify)
return 0;
notify->event = event;
notify->index = index;
notify->callback = callback;
notify->destroy = destroy;
notify->user_data = user_data;
if (mgmt->next_notify_id < 1)
mgmt->next_notify_id = 1;
notify->id = mgmt->next_notify_id++;
mgmt->notify_list = g_list_append(mgmt->notify_list, notify);
return notify->id;
}
bool mgmt_unregister(struct mgmt *mgmt, unsigned int id)
{
struct mgmt_notify *notify;
GList *list;
if (!mgmt || !id)
return false;
list = g_list_find_custom(mgmt->notify_list,
GUINT_TO_POINTER(id), compare_notify_id);
if (!list)
return false;
notify = list->data;
mgmt->notify_list = g_list_remove_link(mgmt->notify_list, list);
if (!mgmt->in_notify) {
g_list_free_1(list);
destroy_notify(notify, NULL);
return true;
}
notify->destroyed = true;
mgmt->notify_destroyed = g_list_concat(mgmt->notify_destroyed, list);
return true;
}
bool mgmt_unregister_index(struct mgmt *mgmt, uint16_t index)
{
GList *list, *next;
if (!mgmt)
return false;
for (list = g_list_first(mgmt->notify_list); list; list = next) {
struct mgmt_notify *notify = list->data;
next = g_list_next(list);
if (notify->index != index)
continue;
mgmt->notify_list = g_list_remove_link(mgmt->notify_list, list);
if (!mgmt->in_notify) {
g_list_free_1(list);
destroy_notify(notify, NULL);
continue;
}
notify->destroyed = true;
mgmt->notify_destroyed = g_list_concat(mgmt->notify_destroyed,
list);
}
return true;
}
static void mark_notify(gpointer data, gpointer user_data)
{
struct mgmt_notify *notify = data;
notify->destroyed = true;
}
bool mgmt_unregister_all(struct mgmt *mgmt)
{
if (!mgmt)
return false;
if (!mgmt->in_notify) {
g_list_foreach(mgmt->notify_list, destroy_notify, NULL);
g_list_free(mgmt->notify_list);
} else {
g_list_foreach(mgmt->notify_list, mark_notify, NULL);
mgmt->notify_destroyed = g_list_concat(mgmt->notify_destroyed,
mgmt->notify_list);
}
mgmt->notify_list = NULL;
return true;
}