| /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
| /* |
| * libqmi-glib -- GLib/GIO based library to control QMI devices |
| * |
| * 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 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 Street, Fifth Floor, |
| * Boston, MA 02110-1301 USA. |
| * |
| * Copyright (C) 2019-2020 Eric Caruso <ejcaruso@chromium.org> |
| * Copyright (C) 2020 Aleksander Morgado <aleksander@aleksander.es> |
| */ |
| |
| #include <errno.h> |
| #include <linux/qrtr.h> |
| #include <string.h> |
| #include <sys/socket.h> |
| #include <sys/types.h> |
| |
| #include <gmodule.h> |
| #include <gio/gio.h> |
| |
| #include <libqrtr-glib.h> |
| |
| #include "qmi-endpoint-qrtr.h" |
| #include "qmi-errors.h" |
| #include "qmi-enum-types.h" |
| #include "qmi-flag-types.h" |
| #include "qmi-error-types.h" |
| #include "qmi-message.h" |
| |
| #define QMI_MESSAGE_OUTPUT_TLV_RESULT 0x02 |
| |
| #define QMI_MESSAGE_TLV_ALLOCATION_INFO 0x01 |
| #define QMI_MESSAGE_INPUT_TLV_SERVICE 0x01 |
| |
| #define QMI_MESSAGE_CTL_GET_VERSION_INFO 0x0021 |
| #define QMI_MESSAGE_CTL_SYNC 0x0027 |
| |
| G_DEFINE_TYPE (QmiEndpointQrtr, qmi_endpoint_qrtr, QMI_TYPE_ENDPOINT) |
| |
| struct _QmiEndpointQrtrPrivate { |
| QrtrNode *node; |
| guint node_removed_id; |
| gboolean node_removed; |
| |
| gboolean endpoint_open; |
| GList *clients; |
| }; |
| |
| /*****************************************************************************/ |
| |
| static void |
| add_qmi_message_to_buffer (QmiEndpointQrtr *self, |
| QmiMessage *message) |
| { |
| g_autoptr(GError) error = NULL; |
| const guint8 *raw_message; |
| gsize raw_message_len; |
| |
| raw_message = qmi_message_get_raw (message, &raw_message_len, &error); |
| if (!raw_message) |
| g_warning ("[%s] Got malformed QMI message: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| else |
| qmi_endpoint_add_message (QMI_ENDPOINT (self), raw_message, raw_message_len); |
| qmi_message_unref (message); |
| } |
| |
| /*****************************************************************************/ |
| |
| #define QRTR_CLIENT_DATA_SERVICE "service" |
| #define QRTR_CLIENT_DATA_CID "cid" |
| |
| typedef struct { |
| QmiService service; |
| guint cid; |
| QrtrClient *client; |
| guint client_message_id; |
| } ClientInfo; |
| |
| static void |
| client_info_free (ClientInfo *client_info) |
| { |
| g_signal_handler_disconnect (client_info->client, client_info->client_message_id); |
| g_object_unref (client_info->client); |
| g_slice_free (ClientInfo, client_info); |
| } |
| |
| static void |
| client_info_list_free (GList *list) |
| { |
| g_list_free_full (list, (GDestroyNotify) client_info_free); |
| } |
| |
| static ClientInfo * |
| client_info_lookup (QmiEndpointQrtr *self, |
| QmiService service, |
| guint cid) |
| { |
| GList *l; |
| |
| for (l = self->priv->clients; l; l = g_list_next (l)) { |
| ClientInfo *client_info = l->data; |
| |
| if ((service == client_info->service) && |
| (cid == client_info->cid)) |
| return client_info; |
| } |
| return NULL; |
| } |
| |
| static gint |
| client_info_cmp (const ClientInfo *a, |
| const ClientInfo *b) |
| { |
| if (a->service != b->service) |
| return a->service - b->service; |
| return a->cid - b->cid; |
| } |
| |
| static void |
| client_message_cb (QrtrClient *qrtr_client, |
| GByteArray *qrtr_message, |
| QmiEndpointQrtr *self) |
| { |
| QmiMessage *message; |
| QmiService service; |
| guint cid; |
| g_autoptr(GError) error = NULL; |
| |
| service = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (qrtr_client), QRTR_CLIENT_DATA_SERVICE)); |
| cid = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (qrtr_client), QRTR_CLIENT_DATA_CID)); |
| |
| /* Create a fake QMUX/QRTR header and add this message to the buffer */ |
| message = qmi_message_new_from_data (service, cid, qrtr_message, &error); |
| if (!message) |
| g_warning ("[%s] Got malformed QMI message: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| else |
| add_qmi_message_to_buffer (self, message); |
| } |
| |
| static ClientInfo * |
| client_info_new (QmiEndpointQrtr *self, |
| QmiService service, |
| GError **error) |
| { |
| ClientInfo *client_info; |
| QrtrClient *qrtr_client; |
| GList *l; |
| guint max_cid = 0; |
| guint min_available_cid = 1; |
| guint cid = 0; |
| gint32 port; |
| |
| for (l = self->priv->clients; l; l = g_list_next (l)) { |
| client_info = l->data; |
| |
| if (service != client_info->service) |
| continue; |
| |
| max_cid = client_info->cid; |
| if (min_available_cid == client_info->cid) |
| min_available_cid++; |
| } |
| |
| cid = max_cid + 1; |
| if (cid > G_MAXUINT8) { |
| cid = min_available_cid; |
| if (min_available_cid > G_MAXUINT8) { |
| g_set_error (error, QMI_PROTOCOL_ERROR, QMI_PROTOCOL_ERROR_CLIENT_IDS_EXHAUSTED, |
| "Client IDs have been exhausted"); |
| return NULL; |
| } |
| } |
| |
| port = qrtr_node_lookup_port (self->priv->node, service); |
| if (port < 0) { |
| g_set_error (error, QMI_CORE_ERROR, QMI_CORE_ERROR_UNSUPPORTED, |
| "Service not supported"); |
| return NULL; |
| } |
| |
| qrtr_client = qrtr_client_new (self->priv->node, (guint)port, NULL, error); |
| if (!qrtr_client) { |
| g_prefix_error (error, "Couldn't create QRTR client: "); |
| return NULL; |
| } |
| |
| /* store service and cid info in the client itself for quicker access */ |
| g_object_set_data (G_OBJECT (qrtr_client), QRTR_CLIENT_DATA_SERVICE, GUINT_TO_POINTER (service)); |
| g_object_set_data (G_OBJECT (qrtr_client), QRTR_CLIENT_DATA_CID, GUINT_TO_POINTER (cid)); |
| |
| client_info = g_slice_new0 (ClientInfo); |
| client_info->service = service; |
| client_info->cid = cid; |
| client_info->client = qrtr_client; |
| client_info->client_message_id = g_signal_connect (qrtr_client, |
| QRTR_CLIENT_SIGNAL_MESSAGE, |
| G_CALLBACK (client_message_cb), |
| self); |
| |
| self->priv->clients = g_list_insert_sorted (self->priv->clients, |
| client_info, |
| (GCompareFunc)client_info_cmp); |
| |
| return client_info; |
| } |
| |
| /*****************************************************************************/ |
| /* Client info operations */ |
| |
| static guint |
| allocate_client (QmiEndpointQrtr *self, |
| QmiService service, |
| GError **error) |
| { |
| ClientInfo *client_info; |
| |
| if (!self->priv->endpoint_open) { |
| g_set_error (error, QMI_CORE_ERROR, QMI_CORE_ERROR_WRONG_STATE, |
| "Endpoint is not open"); |
| return 0; |
| } |
| |
| client_info = client_info_new (self, service, error); |
| if (!client_info) |
| return 0; |
| |
| return client_info->cid; |
| } |
| |
| static void |
| release_client (QmiEndpointQrtr *self, |
| QmiService service, |
| guint cid) |
| { |
| ClientInfo *client_info; |
| |
| client_info = client_info_lookup (self, service, cid); |
| if (!client_info) |
| return; |
| |
| self->priv->clients = g_list_remove (self->priv->clients, client_info); |
| client_info_free (client_info); |
| } |
| |
| /*****************************************************************************/ |
| |
| static gboolean |
| construct_alloc_tlv (QmiMessage *message, |
| QmiService service, |
| guint8 client) |
| { |
| gsize init_offset; |
| |
| init_offset = qmi_message_tlv_write_init (message, |
| QMI_MESSAGE_TLV_ALLOCATION_INFO, |
| NULL); |
| |
| if (qmi_message_get_message_id (message) == QMI_MESSAGE_CTL_ALLOCATE_CID || |
| qmi_message_get_message_id (message) == QMI_MESSAGE_CTL_RELEASE_CID) { |
| g_assert (service <= G_MAXUINT8); |
| return init_offset && |
| qmi_message_tlv_write_guint8 (message, service, NULL) && |
| qmi_message_tlv_write_guint8 (message, client, NULL) && |
| qmi_message_tlv_write_complete (message, init_offset, NULL); |
| } |
| |
| if (qmi_message_get_message_id (message) == QMI_MESSAGE_CTL_INTERNAL_ALLOCATE_CID_QRTR || |
| qmi_message_get_message_id (message) == QMI_MESSAGE_CTL_INTERNAL_RELEASE_CID_QRTR) { |
| g_assert (service <= G_MAXUINT16); |
| return init_offset && |
| qmi_message_tlv_write_guint16 (message, QMI_ENDIAN_LITTLE, service, NULL) && |
| qmi_message_tlv_write_guint8 (message, client, NULL) && |
| qmi_message_tlv_write_complete (message, init_offset, NULL); |
| } |
| |
| g_assert_not_reached (); |
| } |
| |
| static void |
| reply_protocol_error (QmiEndpointQrtr *self, |
| QmiMessage *message, |
| QmiProtocolError error) |
| { |
| QmiMessage *response = NULL; |
| |
| response = qmi_message_response_new (message, error); |
| if (response) |
| add_qmi_message_to_buffer (self, response); |
| } |
| |
| static void |
| handle_alloc_cid (QmiEndpointQrtr *self, |
| QmiMessage *message) |
| { |
| gsize offset = 0; |
| gsize init_offset; |
| QmiService service = QMI_SERVICE_UNKNOWN; |
| guint cid; |
| g_autoptr(QmiMessage) response = NULL; |
| g_autoptr(GError) error = NULL; |
| |
| if ((init_offset = qmi_message_tlv_read_init (message, QMI_MESSAGE_TLV_ALLOCATION_INFO, NULL, &error)) == 0) { |
| g_debug ("[%s] error allocating CID: could not parse message: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_MALFORMED_MESSAGE); |
| return; |
| } |
| |
| if (qmi_message_get_message_id (message) == QMI_MESSAGE_CTL_ALLOCATE_CID) { |
| guint8 service_tmp; |
| |
| if (!qmi_message_tlv_read_guint8 (message, init_offset, &offset, &service_tmp, &error)) { |
| g_debug ("[%s] error allocating CID: failed to read service: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_MALFORMED_MESSAGE); |
| return; |
| } |
| service = (QmiService)service_tmp; |
| } else if (qmi_message_get_message_id (message) == QMI_MESSAGE_CTL_INTERNAL_ALLOCATE_CID_QRTR) { |
| guint16 service_tmp; |
| |
| if (!qmi_message_tlv_read_guint16 (message, init_offset, &offset, QMI_ENDIAN_LITTLE, &service_tmp, &error)) { |
| g_debug ("[%s] error allocating CID (QRTR): failed to read service: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_MALFORMED_MESSAGE); |
| return; |
| } |
| service = (QmiService)service_tmp; |
| } else |
| g_assert_not_reached (); |
| |
| cid = allocate_client (self, service, &error); |
| if (!cid) { |
| g_debug ("[%s] error allocating CID: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_INTERNAL); |
| return; |
| } |
| |
| response = qmi_message_response_new (message, QMI_PROTOCOL_ERROR_NONE); |
| if (!response) |
| return; |
| |
| if (!construct_alloc_tlv (response, service, cid)) |
| return; |
| |
| add_qmi_message_to_buffer (self, g_steal_pointer (&response)); |
| } |
| |
| static void |
| handle_release_cid (QmiEndpointQrtr *self, |
| QmiMessage *message) |
| { |
| gsize offset = 0; |
| gsize init_offset; |
| QmiService service; |
| guint8 cid; |
| g_autoptr(QmiMessage) response = NULL; |
| g_autoptr(GError) error = NULL; |
| |
| if ((init_offset = qmi_message_tlv_read_init (message, QMI_MESSAGE_TLV_ALLOCATION_INFO, NULL, &error)) == 0) { |
| g_debug ("[%s] error releasing CID: could not parse message: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_MALFORMED_MESSAGE); |
| return; |
| } |
| |
| if (qmi_message_get_message_id (message) == QMI_MESSAGE_CTL_RELEASE_CID) { |
| guint8 service_tmp; |
| |
| if (!qmi_message_tlv_read_guint8 (message, init_offset, &offset, &service_tmp, &error)) { |
| g_debug ("[%s] error releasing CID: could not read service: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_MALFORMED_MESSAGE); |
| return; |
| } |
| service = (QmiService)service_tmp; |
| } else if (qmi_message_get_message_id (message) == QMI_MESSAGE_CTL_INTERNAL_RELEASE_CID_QRTR) { |
| guint16 service_tmp; |
| |
| if (!qmi_message_tlv_read_guint16 (message, init_offset, &offset, QMI_ENDIAN_LITTLE, &service_tmp, &error)) { |
| g_debug ("[%s] error releasing CID (QRTR): could not read service: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_MALFORMED_MESSAGE); |
| return; |
| } |
| service = (QmiService)service_tmp; |
| } else |
| g_assert_not_reached (); |
| |
| if (!qmi_message_tlv_read_guint8 (message, init_offset, &offset, &cid, &error)) { |
| g_debug ("[%s] error releasing CID: could not client id: %s", |
| qmi_endpoint_get_name (QMI_ENDPOINT (self)), error->message); |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_MALFORMED_MESSAGE); |
| return; |
| } |
| |
| release_client (self, service, cid); |
| |
| response = qmi_message_response_new (message, QMI_PROTOCOL_ERROR_NONE); |
| if (!response) |
| return; |
| |
| if (!construct_alloc_tlv (response, service, cid)) |
| return; |
| |
| add_qmi_message_to_buffer (self, g_steal_pointer (&response)); |
| } |
| |
| static void |
| handle_sync (QmiEndpointQrtr *self, |
| QmiMessage *message) |
| { |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_NONE); |
| } |
| |
| static void |
| unhandled_message (QmiEndpointQrtr *self, |
| QmiMessage *message) |
| { |
| reply_protocol_error (self, message, QMI_PROTOCOL_ERROR_NOT_SUPPORTED); |
| } |
| |
| static void |
| handle_ctl_message (QmiEndpointQrtr *self, |
| QmiMessage *message) |
| { |
| switch (qmi_message_get_message_id (message)) { |
| case QMI_MESSAGE_CTL_ALLOCATE_CID: |
| case QMI_MESSAGE_CTL_INTERNAL_ALLOCATE_CID_QRTR: |
| handle_alloc_cid (self, message); |
| break; |
| case QMI_MESSAGE_CTL_RELEASE_CID: |
| case QMI_MESSAGE_CTL_INTERNAL_RELEASE_CID_QRTR: |
| handle_release_cid (self, message); |
| break; |
| case QMI_MESSAGE_CTL_SYNC: |
| handle_sync (self, message); |
| break; |
| case QMI_MESSAGE_CTL_GET_VERSION_INFO: |
| default: |
| unhandled_message (self, message); |
| break; |
| } |
| } |
| |
| /*****************************************************************************/ |
| |
| static gboolean |
| endpoint_open_finish (QmiEndpoint *self, |
| GAsyncResult *res, |
| GError **error) |
| { |
| return g_task_propagate_boolean (G_TASK (res), error); |
| } |
| |
| static void |
| endpoint_open (QmiEndpoint *endpoint, |
| gboolean use_proxy, |
| guint timeout, |
| GCancellable *cancellable, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| QmiEndpointQrtr *self = QMI_ENDPOINT_QRTR (endpoint); |
| GTask *task; |
| |
| g_assert (!use_proxy); |
| |
| task = g_task_new (self, cancellable, callback, user_data); |
| |
| if (self->priv->node_removed) { |
| g_task_return_new_error (task, |
| QMI_CORE_ERROR, |
| QMI_CORE_ERROR_FAILED, |
| "Node is not present on bus"); |
| g_object_unref (task); |
| return; |
| } |
| |
| if (self->priv->endpoint_open) { |
| g_task_return_new_error (task, |
| QMI_CORE_ERROR, |
| QMI_CORE_ERROR_WRONG_STATE, |
| "Already open"); |
| g_object_unref (task); |
| return; |
| } |
| |
| g_assert (self->priv->clients == NULL); |
| self->priv->endpoint_open = TRUE; |
| |
| g_task_return_boolean (task, TRUE); |
| g_object_unref (task); |
| } |
| |
| static gboolean |
| endpoint_is_open (QmiEndpoint *self) |
| { |
| return QMI_ENDPOINT_QRTR (self)->priv->endpoint_open; |
| } |
| |
| static gboolean |
| endpoint_send (QmiEndpoint *endpoint, |
| QmiMessage *message, |
| guint timeout, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| QmiEndpointQrtr *self = QMI_ENDPOINT_QRTR (endpoint); |
| ClientInfo *client_info; |
| QmiService service; |
| guint cid; |
| gconstpointer raw_message; |
| gsize raw_message_len; |
| g_autoptr(GByteArray) qrtr_message = NULL; |
| |
| /* We implement the CTL service here, so divert those messages */ |
| service = qmi_message_get_service (message); |
| if (service == QMI_SERVICE_CTL) { |
| handle_ctl_message (self, message); |
| return TRUE; |
| } |
| |
| cid = qmi_message_get_client_id (message); |
| client_info = client_info_lookup (self, service, cid); |
| if (!client_info) { |
| g_set_error (error, QMI_CORE_ERROR, QMI_CORE_ERROR_WRONG_STATE, |
| "Unknown client %u for service %s", cid, qmi_service_get_string (service)); |
| return FALSE; |
| } |
| |
| /* Build raw QRTR message without QMUX/QRTR header */ |
| raw_message = qmi_message_get_data (message, &raw_message_len, error); |
| if (!raw_message) { |
| g_prefix_error (error, "Invalid QMI message: "); |
| return FALSE; |
| } |
| qrtr_message = g_byte_array_sized_new (raw_message_len); |
| g_byte_array_append (qrtr_message, raw_message, raw_message_len); |
| |
| return qrtr_client_send (client_info->client, |
| qrtr_message, |
| cancellable, |
| error); |
| } |
| |
| /*****************************************************************************/ |
| |
| static void |
| internal_close (QmiEndpointQrtr *self) |
| { |
| g_clear_pointer (&self->priv->clients, client_info_list_free); |
| self->priv->endpoint_open = FALSE; |
| } |
| |
| static gboolean |
| endpoint_close_finish (QmiEndpoint *self, |
| GAsyncResult *res, |
| GError **error) |
| { |
| return g_task_propagate_boolean (G_TASK (res), error); |
| } |
| |
| static void |
| endpoint_close (QmiEndpoint *endpoint, |
| guint timeout, |
| GCancellable *cancellable, |
| GAsyncReadyCallback callback, |
| gpointer user_data) |
| { |
| QmiEndpointQrtr *self = QMI_ENDPOINT_QRTR (endpoint); |
| GTask *task; |
| |
| task = g_task_new (self, cancellable, callback, user_data); |
| |
| internal_close (self); |
| |
| g_task_return_boolean (task, TRUE); |
| g_object_unref (task); |
| } |
| |
| /*****************************************************************************/ |
| |
| static void |
| node_removed_cb (QrtrNode *node, |
| QmiEndpointQrtr *self) |
| { |
| self->priv->node_removed = TRUE; |
| g_signal_emit_by_name (QMI_ENDPOINT (self), QMI_ENDPOINT_SIGNAL_HANGUP); |
| } |
| |
| QmiEndpointQrtr * |
| qmi_endpoint_qrtr_new (QrtrNode *node) |
| { |
| QmiEndpointQrtr *self; |
| g_autofree gchar *uri = NULL; |
| g_autoptr(GFile) gfile = NULL; |
| g_autoptr(QmiFile) file = NULL; |
| |
| if (!node) |
| return NULL; |
| |
| uri = qrtr_get_uri_for_node (qrtr_node_get_id (node)); |
| gfile = g_file_new_for_uri (uri); |
| file = qmi_file_new (gfile); |
| |
| self = g_object_new (QMI_TYPE_ENDPOINT_QRTR, |
| QMI_ENDPOINT_FILE, file, |
| NULL); |
| |
| self->priv->node = g_object_ref (node); |
| self->priv->node_removed_id = g_signal_connect (node, |
| QRTR_NODE_SIGNAL_REMOVED, |
| G_CALLBACK (node_removed_cb), |
| self); |
| return self; |
| } |
| |
| /*****************************************************************************/ |
| |
| static void |
| qmi_endpoint_qrtr_init (QmiEndpointQrtr *self) |
| { |
| self->priv = G_TYPE_INSTANCE_GET_PRIVATE ((self), |
| QMI_TYPE_ENDPOINT_QRTR, |
| QmiEndpointQrtrPrivate); |
| } |
| |
| static void |
| dispose (GObject *object) |
| { |
| QmiEndpointQrtr *self = QMI_ENDPOINT_QRTR (object); |
| |
| internal_close (self); |
| |
| if (self->priv->node) { |
| if (self->priv->node_removed_id) { |
| g_signal_handler_disconnect (self->priv->node, self->priv->node_removed_id); |
| self->priv->node_removed_id = 0; |
| } |
| g_clear_object (&self->priv->node); |
| } |
| |
| G_OBJECT_CLASS (qmi_endpoint_qrtr_parent_class)->dispose (object); |
| } |
| |
| static void |
| qmi_endpoint_qrtr_class_init (QmiEndpointQrtrClass *klass) |
| { |
| GObjectClass *object_class = G_OBJECT_CLASS (klass); |
| QmiEndpointClass *endpoint_class = QMI_ENDPOINT_CLASS (klass); |
| |
| g_type_class_add_private (object_class, sizeof (QmiEndpointQrtrPrivate)); |
| |
| object_class->dispose = dispose; |
| |
| endpoint_class->open = endpoint_open; |
| endpoint_class->open_finish = endpoint_open_finish; |
| endpoint_class->is_open = endpoint_is_open; |
| endpoint_class->send = endpoint_send; |
| endpoint_class->close = endpoint_close; |
| endpoint_class->close_finish = endpoint_close_finish; |
| } |