758 lines
21 KiB
C
758 lines
21 KiB
C
/*
|
|
* object_monitor.c
|
|
*
|
|
* $Id$
|
|
*
|
|
* functions and data structures for cooperating code to monitor objects.
|
|
*
|
|
* WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING!
|
|
* WARNING! WARNING!
|
|
* WARNING! WARNING!
|
|
* WARNING! This code is under active development WARNING!
|
|
* WARNING! and is subject to change at any time. WARNING!
|
|
* WARNING! WARNING!
|
|
* WARNING! WARNING!
|
|
* WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING!
|
|
*/
|
|
|
|
#include <net-snmp/net-snmp-config.h>
|
|
#include <net-snmp/net-snmp-includes.h>
|
|
#include <net-snmp/agent/net-snmp-agent-includes.h>
|
|
#include <net-snmp/library/container.h>
|
|
#include <net-snmp/library/snmp_assert.h>
|
|
|
|
#include "net-snmp/agent/object_monitor.h"
|
|
|
|
#if ! defined TRUE
|
|
# define TRUE 1
|
|
#elsif TRUE != 1
|
|
error "TRUE != 1"
|
|
#endif
|
|
/**************************************************************************
|
|
*
|
|
* Private data structures
|
|
*
|
|
**************************************************************************/
|
|
/*
|
|
* individual callback info for an object
|
|
*/
|
|
typedef struct monitor_info_s {
|
|
|
|
/** priority for this callback */
|
|
int priority;
|
|
|
|
/** handler that registred to watch this object */
|
|
netsnmp_mib_handler *watcher;
|
|
|
|
/** events that the watcher cares about */
|
|
unsigned int events;
|
|
|
|
/** callback function */
|
|
netsnmp_object_monitor_callback *cb;
|
|
|
|
/** pointer to data from the watcher */
|
|
void *watcher_data;
|
|
|
|
struct monitor_info_s *next;
|
|
|
|
} monitor_info;
|
|
|
|
/*
|
|
* list of watchers for a given object
|
|
*/
|
|
typedef struct watcher_list_s {
|
|
|
|
/** netsnmp_index must be first! */
|
|
netsnmp_index monitored_object;
|
|
|
|
monitor_info *head;
|
|
|
|
} watcher_list;
|
|
|
|
/*
|
|
* temp holder for ordered list of callbacks
|
|
*/
|
|
typedef struct callback_placeholder_s {
|
|
|
|
monitor_info *mi;
|
|
netsnmp_monitor_callback_header *cbh;
|
|
|
|
struct callback_placeholder_s *next;
|
|
|
|
} callback_placeholder;
|
|
|
|
|
|
/**************************************************************************
|
|
*
|
|
*
|
|
*
|
|
**************************************************************************/
|
|
|
|
/*
|
|
* local statics
|
|
*/
|
|
static char need_init = 1;
|
|
static netsnmp_container *monitored_objects = NULL;
|
|
static netsnmp_monitor_callback_header *callback_pending_list;
|
|
static callback_placeholder *callback_ready_list;
|
|
|
|
/*
|
|
* local prototypes
|
|
*/
|
|
static watcher_list *find_watchers(oid * object, size_t oid_len);
|
|
static int insert_watcher(oid *, size_t, monitor_info *);
|
|
static int check_registered(unsigned int event, oid * o, int o_l,
|
|
watcher_list ** pWl, monitor_info ** pMi);
|
|
static void move_pending_to_ready(void);
|
|
|
|
|
|
/**************************************************************************
|
|
*
|
|
* Public functions
|
|
*
|
|
**************************************************************************/
|
|
|
|
/*
|
|
*
|
|
*/
|
|
void
|
|
netsnmp_monitor_init(void)
|
|
{
|
|
if (!need_init)
|
|
return;
|
|
|
|
callback_pending_list = NULL;
|
|
callback_ready_list = NULL;
|
|
|
|
monitored_objects = netsnmp_container_get("object_monitor:binary_array");
|
|
if (NULL != monitored_objects)
|
|
need_init = 0;
|
|
monitored_objects->compare = netsnmp_compare_netsnmp_index;
|
|
monitored_objects->ncompare = netsnmp_ncompare_netsnmp_index;
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
/**************************************************************************
|
|
*
|
|
* Registration functions
|
|
*
|
|
**************************************************************************/
|
|
|
|
/**
|
|
* Register a callback for the specified object.
|
|
*
|
|
* @param object pointer to the OID of the object to monitor.
|
|
* @param oid_len length of the OID pointed to by object.
|
|
* @param priority the priority to associate with this callback. A
|
|
* higher number indicates higher priority. This
|
|
* allows multiple callbacks for the same object to
|
|
* coordinate the order in which they are called. If
|
|
* two callbacks register with the same priority, the
|
|
* order is undefined.
|
|
* @param events the events which the callback is interested in.
|
|
* @param watcher_data pointer to data that will be supplied to the
|
|
* callback method when an event occurs.
|
|
* @param cb pointer to the function to be called when an event occurs.
|
|
*
|
|
* NOTE: the combination of the object, priority and watcher_data must
|
|
* be unique, as they are the parameters for unregistering a
|
|
* callback.
|
|
*
|
|
* @return SNMPERR_NOERR registration was successful
|
|
* @return SNMPERR_MALLOC memory allocation failed
|
|
* @return SNMPERR_VALUE the combination of the object, priority and
|
|
* watcher_data is not unique.
|
|
*/
|
|
int
|
|
netsnmp_monitor_register(oid * object, size_t oid_len, int priority,
|
|
unsigned int events, void *watcher_data,
|
|
netsnmp_object_monitor_callback * cb)
|
|
{
|
|
monitor_info *mi;
|
|
int rc;
|
|
|
|
netsnmp_assert(need_init == 0);
|
|
|
|
mi = calloc(1, sizeof(monitor_info));
|
|
if (NULL == mi)
|
|
return SNMPERR_MALLOC;
|
|
|
|
mi->priority = priority;
|
|
mi->events = events;
|
|
mi->watcher_data = watcher_data;
|
|
mi->cb = cb;
|
|
|
|
rc = insert_watcher(object, oid_len, mi);
|
|
if (rc != SNMPERR_SUCCESS)
|
|
free(mi);
|
|
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* Unregister a callback for the specified object.
|
|
*
|
|
* @param object pointer to the OID of the object to monitor.
|
|
* @param oid_len length of the OID pointed to by object.
|
|
* @param priority the priority to associate with this callback.
|
|
* @param wd pointer to data that was supplied when the
|
|
* callback was registered.
|
|
* @param cb pointer to the function to be called when an event occurs.
|
|
*/
|
|
int
|
|
netsnmp_monitor_unregister(oid * object, size_t oid_len, int priority,
|
|
void *wd, netsnmp_object_monitor_callback * cb)
|
|
{
|
|
monitor_info *mi, *last;
|
|
|
|
watcher_list *wl = find_watchers(object, oid_len);
|
|
if (NULL == wl)
|
|
return SNMPERR_GENERR;
|
|
|
|
last = NULL;
|
|
mi = wl->head;
|
|
while (mi) {
|
|
if ((mi->cb == cb) && (mi->priority == priority) &&
|
|
(mi->watcher_data == wd))
|
|
break;
|
|
last = mi;
|
|
mi = mi->next;
|
|
}
|
|
|
|
if (NULL == mi)
|
|
return SNMPERR_GENERR;
|
|
|
|
if (NULL == last)
|
|
wl->head = mi->next;
|
|
else
|
|
last->next = mi->next;
|
|
|
|
if (NULL == wl->head) {
|
|
CONTAINER_REMOVE(monitored_objects, wl);
|
|
free(wl->monitored_object.oids);
|
|
free(wl);
|
|
}
|
|
|
|
free(mi);
|
|
|
|
return SNMPERR_SUCCESS;
|
|
}
|
|
|
|
/**************************************************************************
|
|
*
|
|
* object monitor functions
|
|
*
|
|
**************************************************************************/
|
|
|
|
/**
|
|
* Notifies the object monitor of an event.
|
|
*
|
|
* The object monitor funtions will save the callback information
|
|
* until all varbinds in the current PDU have been processed and
|
|
* a response has been sent. At that time, the object monitor will
|
|
* determine if there are any watchers monitoring for the event.
|
|
*
|
|
* NOTE: the actual type of the callback structure may vary. The
|
|
* object monitor functions require only that the start of
|
|
* the structure match the netsnmp_monitor_callback_header
|
|
* structure. It is up to the watcher and monitored objects
|
|
* to agree on the format of other data.
|
|
*
|
|
* @param cbh pointer to a callback header.
|
|
*/
|
|
void
|
|
netsnmp_notify_monitor(netsnmp_monitor_callback_header * cbh)
|
|
{
|
|
|
|
netsnmp_assert(need_init == 0);
|
|
|
|
/*
|
|
* put processing of until response has been sent
|
|
*/
|
|
cbh->private = callback_pending_list;
|
|
callback_pending_list = cbh;
|
|
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* check to see if a registration exists for an object/event combination
|
|
*
|
|
* @param event the event type to check for
|
|
* @param o the oid to check for
|
|
* @param o_l the length of the oid
|
|
*
|
|
* @returns TRUE(1) if a callback is registerd
|
|
* @returns FALSE(0) if no callback is registered
|
|
*/
|
|
int
|
|
netsnmp_monitor_check_registered(int event, oid * o, int o_l)
|
|
{
|
|
return check_registered(event, o, o_l, NULL, NULL);
|
|
}
|
|
|
|
/**
|
|
* Process all pending callbacks
|
|
*
|
|
* NOTE: this method is not in the public header, as it should only be
|
|
* called in one place, in the agent.
|
|
*/
|
|
void
|
|
netsnmp_monitor_process_callbacks(void)
|
|
{
|
|
netsnmp_assert(need_init == 0);
|
|
netsnmp_assert(NULL == callback_ready_list);
|
|
|
|
if (NULL == callback_pending_list) {
|
|
DEBUGMSGT(("object_monitor", "No callbacks to process"));
|
|
return;
|
|
}
|
|
|
|
DEBUGMSG(("object_monitor", "Checking for registered " "callbacks."));
|
|
|
|
/*
|
|
* move an pending notification which has a registered watcher to the
|
|
* ready list. Free any other notifications.
|
|
*/
|
|
move_pending_to_ready();
|
|
|
|
/*
|
|
* call callbacks
|
|
*/
|
|
while (callback_ready_list) {
|
|
|
|
/*
|
|
* pop off the first item
|
|
*/
|
|
callback_placeholder *current_cbr;
|
|
current_cbr = callback_ready_list;
|
|
callback_ready_list = current_cbr->next;
|
|
|
|
/*
|
|
* setup, then call callback
|
|
*/
|
|
current_cbr->cbh->watcher_data = current_cbr->mi->watcher_data;
|
|
current_cbr->cbh->priority = current_cbr->mi->priority;
|
|
(*current_cbr->mi->cb) (current_cbr->cbh);
|
|
|
|
/*
|
|
* release memory (don't free current_cbr->mi)
|
|
*/
|
|
if (--(current_cbr->cbh->refs) == 0) {
|
|
free(current_cbr->cbh->monitored_object.oids);
|
|
free(current_cbr->cbh);
|
|
}
|
|
free(current_cbr);
|
|
|
|
/*
|
|
* check for any new pending notifications
|
|
*/
|
|
move_pending_to_ready();
|
|
|
|
}
|
|
|
|
netsnmp_assert(callback_ready_list == NULL);
|
|
netsnmp_assert(callback_pending_list = NULL);
|
|
|
|
return;
|
|
}
|
|
|
|
/**************************************************************************
|
|
*
|
|
* COOPERATIVE helpers
|
|
*
|
|
**************************************************************************/
|
|
/**
|
|
* Notifies the object monitor of a cooperative event.
|
|
*
|
|
* This convenience function will build a
|
|
* ::netsnmp_monitor_callback_header and call
|
|
* netsnmp_notify_monitor().
|
|
*
|
|
* @param event the event type
|
|
* @param o pointer to the oid of the object sending the event
|
|
* @param o_len the lenght of the oid
|
|
* @param o_steal set to true if the function may keep the pointer
|
|
* to the memory (and free it later). set to false
|
|
* to make the function allocate and copy the oid.
|
|
* @param object_info pointer to data supplied by the object for
|
|
* the callback. This pointer must remain valid,
|
|
* will be provided to each callback registered
|
|
* for the object (i.e. it will not be copied or
|
|
* freed).
|
|
*/
|
|
void
|
|
netsnmp_notify_cooperative(int event, oid * o, size_t o_len, char o_steal,
|
|
void *object_info)
|
|
{
|
|
netsnmp_monitor_callback_cooperative *cbh;
|
|
|
|
netsnmp_assert(need_init == 0);
|
|
|
|
cbh = SNMP_MALLOC_TYPEDEF(netsnmp_monitor_callback_cooperative);
|
|
if (NULL == cbh) {
|
|
snmp_log(LOG_ERR, "could not allocate memory for "
|
|
"cooperative callback");
|
|
return;
|
|
}
|
|
|
|
cbh->hdr.event = event;
|
|
cbh->hdr.object_info = object_info;
|
|
cbh->hdr.monitored_object.len = o_len;
|
|
|
|
if (o_steal) {
|
|
cbh->hdr.monitored_object.oids = o;
|
|
} else {
|
|
cbh->hdr.monitored_object.oids = snmp_duplicate_objid(o, o_len);
|
|
}
|
|
|
|
netsnmp_notify_monitor((netsnmp_monitor_callback_header *) cbh);
|
|
}
|
|
|
|
/** @cond */
|
|
/*************************************************************************
|
|
*************************************************************************
|
|
*************************************************************************
|
|
* WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING!
|
|
* WARNING! WARNING!
|
|
* WARNING! WARNING!
|
|
* WARNING! This code is under active development WARNING!
|
|
* WARNING! and is subject to change at any time. WARNING!
|
|
* WARNING! WARNING!
|
|
* WARNING! WARNING!
|
|
* WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING! WARNING!
|
|
*************************************************************************
|
|
*************************************************************************
|
|
*************************************************************************
|
|
*/
|
|
static watcher_list *
|
|
find_watchers(oid * object, size_t oid_len)
|
|
{
|
|
netsnmp_index oah;
|
|
|
|
oah.oids = object;
|
|
oah.len = oid_len;
|
|
|
|
return (watcher_list *)CONTAINER_FIND(monitored_objects, &oah);
|
|
}
|
|
|
|
static int
|
|
insert_watcher(oid * object, size_t oid_len, monitor_info * mi)
|
|
{
|
|
watcher_list *wl = find_watchers(object, oid_len);
|
|
int rc = SNMPERR_SUCCESS;
|
|
|
|
if (NULL != wl) {
|
|
|
|
monitor_info *last, *current;
|
|
|
|
netsnmp_assert(wl->head != NULL);
|
|
|
|
last = NULL;
|
|
current = wl->head;
|
|
while (current) {
|
|
if (mi->priority == current->priority) {
|
|
/*
|
|
* check for duplicate
|
|
*/
|
|
if (mi->watcher_data == current->watcher_data)
|
|
return SNMPERR_VALUE; /** duplicate! */
|
|
} else if (mi->priority > current->priority) {
|
|
break;
|
|
}
|
|
last = current;
|
|
current = current->next;
|
|
}
|
|
if (NULL == last) {
|
|
mi->next = wl->head;
|
|
wl->head = mi;
|
|
} else {
|
|
mi->next = last->next;
|
|
last->next = mi;
|
|
}
|
|
} else {
|
|
|
|
/*
|
|
* first watcher for this oid; set up list
|
|
*/
|
|
wl = SNMP_MALLOC_TYPEDEF(watcher_list);
|
|
if (NULL == wl)
|
|
return SNMPERR_MALLOC;
|
|
|
|
/*
|
|
* copy index oid
|
|
*/
|
|
wl->monitored_object.len = oid_len;
|
|
wl->monitored_object.oids = malloc(sizeof(oid) * oid_len);
|
|
if (NULL == wl->monitored_object.oids) {
|
|
free(wl);
|
|
return SNMPERR_MALLOC;
|
|
}
|
|
memcpy(wl->monitored_object.oids, object, sizeof(oid) * oid_len);
|
|
|
|
/*
|
|
* add watcher, and insert into array
|
|
*/
|
|
wl->head = mi;
|
|
mi->next = NULL;
|
|
rc = CONTAINER_INSERT(monitored_objects, wl);
|
|
if (rc) {
|
|
free(wl->monitored_object.oids);
|
|
free(wl);
|
|
return rc;
|
|
}
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* check to see if a registration exists for an object/event combination
|
|
*
|
|
* @param event the event type to check for
|
|
* @param o the oid to check for
|
|
* @param o_l the length of the oid
|
|
* @param pWl if a pointer to a watcher_list pointer is supplied,
|
|
* upon return the watcher list pointer will be set to
|
|
* the watcher list for the object, or NULL if there are
|
|
* no watchers for the object.
|
|
* @param pMi if a pointer to a monitor_info pointer is supplied,
|
|
* upon return the pointer will be set to the first
|
|
* monitor_info object for the specified event.
|
|
*
|
|
* @returns TRUE(1) if a callback is registerd
|
|
* @returns FALSE(0) if no callback is registered
|
|
*/
|
|
static int
|
|
check_registered(unsigned int event, oid * o, int o_l,
|
|
watcher_list ** pWl, monitor_info ** pMi)
|
|
{
|
|
watcher_list *wl;
|
|
monitor_info *mi;
|
|
|
|
netsnmp_assert(need_init == 0);
|
|
|
|
/*
|
|
* check to see if anyone has registered for callbacks
|
|
* for the object.
|
|
*/
|
|
wl = find_watchers(o, o_l);
|
|
if (pWl)
|
|
*pWl = wl;
|
|
if (NULL == wl)
|
|
return 0;
|
|
|
|
/*
|
|
* check if any watchers are watching for this specific event
|
|
*/
|
|
for (mi = wl->head; mi; mi = mi->next) {
|
|
|
|
if (mi->events & event) {
|
|
if (pMi)
|
|
*pMi = mi;
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
*@internal
|
|
*/
|
|
inline void
|
|
insert_ready(callback_placeholder * new_cbr)
|
|
{
|
|
callback_placeholder *current_cbr, *last_cbr;
|
|
|
|
/*
|
|
* insert in callback ready list
|
|
*/
|
|
last_cbr = NULL;
|
|
current_cbr = callback_ready_list;
|
|
while (current_cbr) {
|
|
|
|
if (new_cbr->mi->priority > current_cbr->mi->priority)
|
|
break;
|
|
|
|
last_cbr = current_cbr;
|
|
current_cbr = current_cbr->next;
|
|
}
|
|
if (NULL == last_cbr) {
|
|
new_cbr->next = callback_ready_list;
|
|
callback_ready_list = new_cbr;
|
|
} else {
|
|
new_cbr->next = last_cbr->next;
|
|
last_cbr->next = new_cbr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*@internal
|
|
*
|
|
* move an pending notification which has a registered watcher to the
|
|
* ready list. Free any other notifications.
|
|
*/
|
|
static void
|
|
move_pending_to_ready(void)
|
|
{
|
|
/*
|
|
* check to see if anyone has registered for callbacks
|
|
* for each object.
|
|
*/
|
|
while (callback_pending_list) {
|
|
|
|
watcher_list *wl;
|
|
monitor_info *mi;
|
|
netsnmp_monitor_callback_header *cbp;
|
|
|
|
/*
|
|
* pop off first item
|
|
*/
|
|
cbp = callback_pending_list;
|
|
callback_pending_list = cbp->private; /** next */
|
|
|
|
if (0 == check_registered(cbp->event, cbp->monitored_object.oids,
|
|
cbp->monitored_object.len, &wl,
|
|
&mi)) {
|
|
|
|
/*
|
|
* nobody watching, free memory
|
|
*/
|
|
free(cbp);
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* Found at least one; check the rest of the list and
|
|
* save callback for processing
|
|
*/
|
|
for (; mi; mi = mi->next) {
|
|
|
|
callback_placeholder *new_cbr;
|
|
|
|
if (0 == (mi->events & cbp->event))
|
|
continue;
|
|
|
|
/*
|
|
* create temprory placeholder.
|
|
*
|
|
* I hate to allocate memory here, as I'd like this code to
|
|
* be fast and lean. But I don't have time to think of another
|
|
* solution os this will have to do for now.
|
|
*
|
|
* I need a list of monitor_info (mi) objects for each
|
|
* callback which has registered for the event, and want
|
|
* that list sorted by the priority required by the watcher.
|
|
*/
|
|
new_cbr = SNMP_MALLOC_TYPEDEF(callback_placeholder);
|
|
if (NULL == new_cbr) {
|
|
snmp_log(LOG_ERR, "malloc failed, callback dropped.");
|
|
continue;
|
|
}
|
|
new_cbr->cbh = cbp;
|
|
new_cbr->mi = mi;
|
|
++cbp->refs;
|
|
|
|
/*
|
|
* insert in callback ready list
|
|
*/
|
|
insert_ready(new_cbr);
|
|
|
|
} /** end mi loop */
|
|
} /** end cbp loop */
|
|
|
|
netsnmp_assert(callback_pending_list == NULL);
|
|
}
|
|
|
|
|
|
#if defined TESTING_OBJECT_MONITOR
|
|
/**************************************************************************
|
|
*
|
|
* (untested) TEST CODE
|
|
*
|
|
*/
|
|
void
|
|
dummy_callback(netsnmp_monitor_callback_header * cbh)
|
|
{
|
|
printf("Callback received.\n");
|
|
}
|
|
|
|
void
|
|
dump_watchers(netsnmp_index *oah, void *)
|
|
{
|
|
watcher_list *wl = (watcher_list *) oah;
|
|
netsnmp_monitor_callback_header *cbh = wl->head;
|
|
|
|
printf("Watcher List for OID ");
|
|
print_objid(wl->hdr->oids, wl->hdr->len);
|
|
printf("\n");
|
|
|
|
while (cbh) {
|
|
|
|
printf("Priority = %d;, Events = %d; Watcher Data = 0x%x\n",
|
|
cbh->priority, cbh->events, cbh->watcher_data);
|
|
|
|
cbh = cbh->private;
|
|
}
|
|
}
|
|
|
|
void
|
|
main(int argc, char **argv)
|
|
{
|
|
|
|
oid object[3] = { 1, 3, 6 };
|
|
int object_len = 3;
|
|
int rc;
|
|
|
|
/*
|
|
* init
|
|
*/
|
|
netsnmp_monitor_init();
|
|
|
|
/*
|
|
* insert an object
|
|
*/
|
|
rc = netsnmp_monitor_register(object, object_len, 0,
|
|
EVENT_ROW_ADD, (void *) 0xdeadbeef,
|
|
dummy_callback);
|
|
printf("insert an object: %d\n", rc);
|
|
|
|
/*
|
|
* insert same object, new priority
|
|
*/
|
|
netsnmp_monitor_register(object, object_len, 10,
|
|
EVENT_ROW_ADD, (void *) 0xdeadbeef,
|
|
dummy_callback);
|
|
printf("insert same object, new priority: %d\n", rc);
|
|
|
|
/*
|
|
* insert same object, same priority, new data
|
|
*/
|
|
netsnmp_monitor_register(object, object_len, 10,
|
|
EVENT_ROW_ADD, (void *) 0xbeefdead,
|
|
dummy_callback);
|
|
printf("insert same object, same priority, new data: %d\n", rc);
|
|
|
|
/*
|
|
* insert same object, same priority, same data
|
|
*/
|
|
netsnmp_monitor_register(object, object_len, 10,
|
|
EVENT_ROW_ADD, (void *) 0xbeefdead,
|
|
dummy_callback);
|
|
printf("insert same object, same priority, new data: %d\n", rc);
|
|
|
|
|
|
/*
|
|
* dump table
|
|
*/
|
|
CONTAINER_FOR_EACH(monitored_objects, dump_watchers, NULL);
|
|
}
|
|
#endif /** defined TESTING_OBJECT_MONITOR */
|
|
|
|
/** @endcond */
|
|
|
|
|