char buffer[BUF_SIZE];
};
-static TAILQ_HEAD(tailhead, event) head;
-static int eventarray;
+TAILQ_HEAD(tailhead, event);
-/* This has to match the values in the metric table. */
-static pmID pmid_string = PMDA_PMID(0,1); /* event.param_string */
+struct EventFileData {
+ struct LogfileData *logfile;
+ int fd;
+ int numclients;
+ struct tailhead head;
+
+ /* current client list - allocated? */
+};
+static struct EventFileData *file_data_tab = NULL;
+
+static int eventarray;
+static int numlogfiles;
struct ctx_client_data {
- struct event **last; /* addr of last next element */
+ unsigned int active_logfile;
+ struct event **last;
};
-static int monitorfd = 0;
+static void event_cleanup(void);
static void *
ctx_start_callback(int ctx)
struct ctx_client_data *c = ctx_get_user_data();
if (c == NULL) {
- c = malloc(sizeof(struct ctx_client_data));
+ c = calloc(numlogfiles, sizeof(struct ctx_client_data));
if (c == NULL) {
__pmNotifyErr(LOG_ERR, "allocation failure");
return NULL;
}
- c->last = head.tqh_last;
- __pmNotifyErr(LOG_INFO, "Setting last.");
}
return c;
}
}
void
-event_init(pmdaInterface *dispatch, const char *monitor_path)
+event_init(pmdaInterface *dispatch, struct LogfileData *logfiles,
+ int nlogfiles)
{
- /* initialize queue */
- TAILQ_INIT(&head);
- eventarray = pmdaEventNewArray();
+ int i;
- /*
- * fix the domain field in the event parameter PMIDs ...
- * note these PMIDs must match the corresponding metrics in
- * desctab[] and this cannot easily be done automatically
- */
- ((__pmID_int *)&pmid_string)->domain = dispatch->domain;
+ numlogfiles = nlogfiles;
+ if (numlogfiles <= 0 || logfiles == NULL) {
+ __pmNotifyErr(LOG_ERR, "no logfiles");
+ exit(1);
+ }
+
+ eventarray = pmdaEventNewArray();
ctx_register_callbacks(ctx_start_callback, ctx_end_callback);
- /* We can't really select on the logfile. Why? If the logfile is
- * a normal file, select will (continually) return EOF after we've
- * read all the data. Then we tried a custom main that that read
- * data before handling any message we get on the control channel.
- * That didn't work either, since the client context wasn't set up
- * yet (since that is the 1st control message). So, now we read
- * data inside the event fetch routine. */
-
- /* Try to open logfile to monitor */
- monitorfd = open(monitor_path, O_RDONLY|O_NONBLOCK);
- if (monitorfd < 0) {
- __pmNotifyErr(LOG_ERR, "open failure on %s", monitor_path);
- exit(1);
+ /* Allocate our EventFileData table. */
+ file_data_tab = malloc(sizeof(struct EventFileData) * numlogfiles);
+ if (file_data_tab == NULL) {
+ fprintf(stderr, "%s: allocation error: %s\n", __FUNCTION__,
+ strerror(errno));
+ return;
+ }
+
+ /* Fill it the table. */
+ for (i = 0; i < numlogfiles; i++) {
+ file_data_tab[i].logfile = &logfiles[i];
+ file_data_tab[i].numclients = 0;
+ TAILQ_INIT(&file_data_tab[i].head); /* initialize queue */
}
- /* Skip to the end. */
- //(void)lseek(monitorfd, 0, SEEK_END);
+ /* We can't really use select() on the logfiles. Why? If the
+ * logfile is a normal file, select will (continually) return EOF
+ * after we've read all the data. Then we tried a custom main
+ * that that read data before handling any message we get on the
+ * control channel. That didn't work either, since the client
+ * context wasn't set up yet (since that is the 1st control
+ * message). So, now we read data inside the event fetch
+ * routine. */
+
+ /* Try to open all the logfiles to monitor */
+ for (i = 0; i < numlogfiles; i++) {
+ file_data_tab[i].fd = open(logfiles[i].pathname, O_RDONLY|O_NONBLOCK);
+ if (file_data_tab[i].fd < 0) {
+ __pmNotifyErr(LOG_ERR, "open failure on %s", logfiles[i].pathname);
+ exit(1);
+ }
+
+ /* Skip to the end. */
+ //(void)lseek(file_data_tab[i].fd, 0, SEEK_END);
+ }
}
static int
-event_create(int fd)
+event_create(int logfile)
{
ssize_t c;
}
/* Read up to BUF_SIZE bytes at a time. */
- if ((c = read(fd, e->buffer, sizeof(e->buffer) - 1)) < 0) {
- __pmNotifyErr(LOG_ERR, "read failure: %s", strerror(errno));
+ if ((c = read(file_data_tab[logfile].fd, e->buffer,
+ sizeof(e->buffer) - 1)) < 0) {
+ __pmNotifyErr(LOG_ERR, "read failure on %s: %s",
+ file_data_tab[logfile].logfile->pathname,
+ strerror(errno));
free(e);
return -1;
}
}
/* Store event in queue. */
- e->clients = ctx_get_num();
+ e->clients = file_data_tab[logfile].numclients;
e->buffer[c] = '\0';
- TAILQ_INSERT_TAIL(&head, e, events);
+ TAILQ_INSERT_TAIL(&file_data_tab[logfile].head, e, events);
__pmNotifyErr(LOG_INFO, "Inserted item, clients = %d.", e->clients);
return 0;
}
int
-event_fetch(pmValueBlock **vbpp)
+event_get_clients_per_logfile(unsigned int logfile)
+{
+ return file_data_tab[logfile].numclients;
+}
+
+int
+event_fetch(pmValueBlock **vbpp, unsigned int logfile)
{
struct event *e, *next;
struct timeval stamp;
int records = 0;
struct ctx_client_data *c = ctx_get_user_data();
+ /* Make sure the we keep track of which clients are interested in
+ * which logfiles is up to date. */
+ if (c[logfile].active_logfile == 0) {
+ c[logfile].active_logfile = 1;
+ c[logfile].last = file_data_tab[logfile].head.tqh_last;
+ file_data_tab[logfile].numclients++;
+ }
+
/* Update the event queue with new data (if any). */
- if ((rc = event_create(monitorfd)) < 0)
+ if ((rc = event_create(logfile)) < 0)
return rc;
if (vbpp == NULL)
if ((rc = pmdaEventAddRecord(eventarray, &stamp, PM_EVENT_FLAG_POINT)) < 0)
return rc;
- e = *c->last;
+ e = *c[logfile].last;
while (e != NULL) {
/* Add the string parameter. Note that pmdaEventAddParam()
* copies the string, so we can free it soon after. */
atom.cp = e->buffer;
__pmNotifyErr(LOG_INFO, "Adding param: %s", e->buffer);
- if ((rc = pmdaEventAddParam(eventarray, pmid_string, PM_TYPE_STRING,
- &atom)) < 0)
+ rc = pmdaEventAddParam(eventarray,
+ file_data_tab[logfile].logfile->pmid_string,
+ PM_TYPE_STRING, &atom);
+ if (rc < 0)
return rc;
records++;
/* Remove the current one (if its use count is at 0). */
if (--e->clients <= 0) {
- TAILQ_REMOVE(&head, e, events);
+ TAILQ_REMOVE(&file_data_tab[logfile].head, e, events);
free(e);
}
}
/* Update queue pointer. */
- c->last = head.tqh_last;
+ c[logfile].last = file_data_tab[logfile].head.tqh_last;
if (records > 0)
*vbpp = (pmValueBlock *)pmdaEventGetAddr(eventarray);
return 0;
}
-void
+static void
event_cleanup(void)
{
struct event *e, *next;
struct ctx_client_data *c = ctx_get_user_data();
+ int logfile;
/* We've lost a client. Cleanup. */
- e = *c->last;
- while (e != NULL) {
- /* Get the next event. */
- next = e->events.tqe_next;
-
- /* Remove the current one (if its use count is at 0). */
- if (--e->clients <= 0) {
- TAILQ_REMOVE(&head, e, events);
- free(e);
+ for (logfile = 0; logfile < numlogfiles; logfile++) {
+ if (c[logfile].active_logfile == 0)
+ continue;
+
+ file_data_tab[logfile].numclients--;
+ e = *c[logfile].last;
+ while (e != NULL) {
+ /* Get the next event. */
+ next = e->events.tqe_next;
+
+ /* Remove the current one (if its use count is at 0). */
+ if (--e->clients <= 0) {
+ TAILQ_REMOVE(&file_data_tab[logfile].head, e, events);
+ free(e);
+ }
+
+ /* Go on to the next event. */
+ e = next;
}
-
- /* Go on to the next event. */
- e = next;
}
}
#include <pcp/impl.h>
#include <pcp/pmda.h>
#include <ctype.h>
+#include <string.h>
#include "domain.h"
#include "percontext.h"
#include "event.h"
* and could be extended to implement a much more complex PMDA.
*
* Metrics
- * logger.clients - number of attached clients
+ * logger.numclients - number of attached clients
+ * logger.numlogfiles - number of monitored logfiles
+ * logger.param_string - string event data
+ * logger.perfile.{LOGFILE}.numclients - number of attached
+ * clients/logfile
+ * logger.perfile.{LOGFILE}.records - event records/logfile
*/
-struct LogfileData {
- char pathname[MAXPATHLEN];
- int numclients;
- /* current client list - allocated? */
- /* char *pathname; - stored in pmdaInstid */
- /* int fd; */
- /* head of event queue */
- /* perhaps a 'void *' for event.c to store stuff in? */
-};
-
static struct LogfileData *logfiles = NULL;
static int numlogfiles = 0;
+static int nummetrics = 0;
+static __pmnsTree *pmns;
+
+struct dynamic_metric_info {
+ int logfile;
+ int pmid_index;
+};
+static struct dynamic_metric_info *dynamic_metric_infotab = NULL;
/*
* all metrics supported in this PMDA - one table entry for each
*/
-static pmdaMetric metrictab[] = {
+static pmdaMetric dynamic_metrictab[] = {
+/* perfile.{LOGFILE}.numclients */
+ { (void *)0,
+ { 0 /* pmid gets filled in later */, PM_TYPE_U32, PM_INDOM_NULL,
+ PM_SEM_DISCRETE, PMDA_PMUNITS(0,0,1,0,0,PM_COUNT_ONE) }, },
+/* perfile.{LOGFILE}.records */
+ { (void *)1,
+ { 0 /* pmid gets filled in later */, PM_TYPE_EVENT, PM_INDOM_NULL,
+ PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0) }, },
+};
+
+static char *dynamic_nametab[] = {
+/* perfile.numclients */
+ "numclients",
+/* perfile.records */
+ "records",
+};
+
+static pmdaMetric static_metrictab[] = {
+/* numclients */
{ NULL,
-/* clients */
- { PMDA_PMID(0,0), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_INSTANT,
- PMDA_PMUNITS(0,0,0,0,0,0) }, },
-/* event.records */
+ { PMDA_PMID(0,0), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_DISCRETE,
+ PMDA_PMUNITS(0,0,1,0,0,PM_COUNT_ONE) }, },
+/* numlogfiles */
{ NULL,
- { PMDA_PMID(PM_CLUSTER_EVENT,0), PM_TYPE_EVENT, PM_INDOM_NULL,
- PM_SEM_INSTANT, PMDA_PMUNITS(0,0,0,0,0,0) }, },
-/* event.param_string */
+ { PMDA_PMID(0,1), PM_TYPE_U32, PM_INDOM_NULL, PM_SEM_DISCRETE,
+ PMDA_PMUNITS(0,0,1,0,0,PM_COUNT_ONE) }, },
+/* param_string */
{ NULL,
- { PMDA_PMID(0,1), PM_TYPE_STRING, PM_INDOM_NULL, PM_SEM_INSTANT,
+ { PMDA_PMID(0,2), PM_TYPE_STRING, PM_INDOM_NULL, PM_SEM_INSTANT,
PMDA_PMUNITS(0,0,0,0,0,0) }, },
};
+static pmdaMetric *metrictab = NULL;
+
static char mypath[MAXPATHLEN];
static int isDSO = 1; /* ==0 if I am a daemon */
char *configfile = NULL;
int status = PMDA_FETCH_STATIC;
__pmNotifyErr(LOG_INFO, "%s called\n", __FUNCTION__);
- if ((idp->cluster == 0 && (idp->item < 0 || idp->item > 1))
- || (idp->cluster == PM_CLUSTER_EVENT && idp->item != 0)) {
+ if (idp->cluster != 0 || (idp->item < 0 || idp->item > nummetrics)) {
__pmNotifyErr(LOG_ERR, "%s: PM_ERR_PMID (cluster = %d, item = %d)\n",
__FUNCTION__, idp->cluster, idp->item);
return PM_ERR_PMID;
}
- else if (inst != PM_IN_NULL) {
- __pmNotifyErr(LOG_ERR, "%s: PM_ERR_INST (inst = %d)\n",
- __FUNCTION__, inst);
- return PM_ERR_INST;
- }
- if (idp->cluster == 0) {
+ if (idp->item < 3) {
switch(idp->item) {
- case 0:
+ case 0: /* logger.numclients */
atom->ul = ctx_get_num();
break;
- case 1:
+ case 1: /* logger.numlogfiles */
+ atom->ul = numlogfiles;
+ break;
+ case 2: /* logger.param_string */
status = PMDA_FETCH_NOVALUES;
break;
default:
__pmNotifyErr(LOG_ERR,
- "%s: PM_ERR_PMID (cluster = %d, item = %d)\n",
- __FUNCTION__, idp->cluster, idp->item);
+ "%s: PM_ERR_PMID (inst = %d, cluster = %d, item = %d)\n",
+ __FUNCTION__, inst, idp->cluster, idp->item);
return PM_ERR_PMID;
}
}
- else if (idp->cluster == PM_CLUSTER_EVENT) {
- switch(idp->item) {
- case 0:
- if ((rc = event_fetch(&atom->vbp)) != 0)
+ else {
+ struct dynamic_metric_info *pinfo = ((mdesc != NULL) ? mdesc->m_user
+ : NULL);
+ if (pinfo == NULL) {
+ __pmNotifyErr(LOG_ERR,
+ "%s: PM_ERR_PMID - bad pinfo (item = %d)\n",
+ __FUNCTION__, idp->item);
+ return PM_ERR_PMID;
+ }
+
+ switch(pinfo->pmid_index) {
+ case 0: /* logger.perfile.{LOGFILE}.numclients */
+ atom->ul = event_get_clients_per_logfile(pinfo->logfile);
+ break;
+ case 1: /* logger.perfile.{LOGFILE}.records */
+ if ((rc = event_fetch(&atom->vbp, pinfo->logfile)) != 0)
return rc;
if (atom->vbp == NULL)
status = PMDA_FETCH_NOVALUES;
break;
default:
__pmNotifyErr(LOG_ERR,
- "%s: PM_ERR_PMID (cluster = %d, item = %d)\n",
- __FUNCTION__, idp->cluster, idp->item);
+ "%s: PM_ERR_PMID (item = %d)\n", __FUNCTION__,
+ idp->item);
return PM_ERR_PMID;
}
}
-
return status;
}
-/*
- * Initialise the agent (both daemon and DSO).
- */
-void
-logger_init(pmdaInterface *dp)
-{
- if (isDSO) {
- int sep = __pmPathSeparator();
- snprintf(mypath, sizeof(mypath), "%s%c" "logger" "%c" "help",
- pmGetConfig("PCP_PMDAS_DIR"), sep, sep);
- pmdaDSO(dp, PMDA_INTERFACE_5, "logger DSO", mypath);
- }
-
- if (dp->status != 0)
- return;
-
- dp->version.four.profile = logger_profile;
-
- pmdaSetFetchCallBack(dp, logger_fetchCallBack);
- pmdaSetEndContextCallBack(dp, logger_end_contextCallBack);
-
- pmdaInit(dp, NULL, 0,
- metrictab, sizeof(metrictab)/sizeof(metrictab[0]));
-
- /* For now, only handle the 1st logfile. */
- event_init(dp, logfiles[0].pathname);
-}
-
static int
read_config(const char *filename)
{
int rc = 0;
size_t len;
char tmp[MAXPATHLEN];
- char *endptr;
+ char *ptr;
configFile = fopen(filename, "r");
if (configFile == NULL) {
}
tmp[len - 1] = '\0'; /* Remove the '\n'. */
- /* Remove all trailing whitespace. Set endptr to last char of
+ /* Remove all trailing whitespace. Set ptr to last char of
* string. */
- endptr = tmp + strlen(tmp) - 1;
+ ptr = tmp + strlen(tmp) - 1;
/* While trailing whitespace, move back. */
- while (endptr >= tmp && isspace(*endptr)) {
- --endptr;
+ while (ptr >= tmp && isspace(*ptr)) {
+ --ptr;
}
- *(endptr+1) = '\0'; /* Now set '\0' as terminal byte. */
+ *(ptr+1) = '\0'; /* Now set '\0' as terminal byte. */
/* If the string is now empty, just ignore the line. */
len = strlen(tmp);
break;
}
data = &logfiles[numlogfiles - 1];
- data->numclients = 0;
- strcpy(data->pathname, tmp);
+ strncpy(data->pathname, tmp, sizeof(data->pathname));
+ /* data->pmid_string gets filled in after pmdaInit() is called. */
+
+ /* Now we've got to munge the pathname and turn it into a
+ * pmns name. For example, "/var/log/messages" would end up
+ * as "var.log.messages". First, skip past any leading '/'
+ * chars. */
+ ptr = tmp;
+ while (*ptr == '/') {
+ ptr++;
+ if (*ptr == '\0')
+ break;
+ }
+ /* Copy the string, then replace all the '.' characters with
+ * '_', then replace all the '/' characters with '.'. */
- __pmNotifyErr(LOG_INFO, "%s: saw logfile %s\n", __FUNCTION__,
- data->pathname);
+ /* DRS: FIXME - Is '_' a valid char? I also think the 1st
+ * char must be alphabetic. See valid_pmns_name() in
+ * pmdas/linux/cgroups.c.*/
+
+ strncpy(data->pmns_name, ptr, sizeof(data->pmns_name));
+ ptr = data->pmns_name;
+ while ((ptr = strchr(ptr, '.')) != NULL) {
+ *ptr = '_';
+ }
+ ptr = data->pmns_name;
+ while ((ptr = strchr(ptr, '/')) != NULL) {
+ *ptr = '.';
+ }
+
+ __pmNotifyErr(LOG_INFO, "%s: saw logfile %s (%s)\n", __FUNCTION__,
+ data->pathname, data->pmns_name);
}
if (rc != 0) {
free(logfiles);
exit(1);
}
+static int
+logger_pmid(const char *name, pmID *pmid, pmdaExt *pmda)
+{
+ __pmNotifyErr(LOG_INFO, "%s: name %s\n", __FUNCTION__,
+ (name == NULL) ? "NULL" : name);
+ return pmdaTreePMID(pmns, name, pmid);
+}
+
+static int
+logger_name(pmID pmid, char ***nameset, pmdaExt *pmda)
+{
+ __pmNotifyErr(LOG_INFO, "%s: pmid 0x%x\n", __FUNCTION__, pmid);
+ return pmdaTreeName(pmns, pmid, nameset);
+}
+
+static int
+logger_children(const char *name, int traverse, char ***kids, int **sts,
+ pmdaExt *pmda)
+{
+ __pmNotifyErr(LOG_INFO, "%s: name %s\n", __FUNCTION__,
+ (name == NULL) ? "NULL" : name);
+ return pmdaTreeChildren(pmns, name, traverse, kids, sts);
+}
+
+/*
+ * Initialise the agent (both daemon and DSO).
+ */
+void
+logger_init(pmdaInterface *dp)
+{
+ int i, j, rc;
+ int numstatics = sizeof(static_metrictab)/sizeof(static_metrictab[0]);
+ int numdynamics = sizeof(dynamic_metrictab)/sizeof(dynamic_metrictab[0]);
+ pmdaMetric *pmetric;
+ int pmid_num;
+ char name[MAXPATHLEN * 2];
+ struct dynamic_metric_info *pinfo;
+
+ if (isDSO) {
+ int sep = __pmPathSeparator();
+ snprintf(mypath, sizeof(mypath), "%s%c" "logger" "%c" "help",
+ pmGetConfig("PCP_PMDAS_DIR"), sep, sep);
+ pmdaDSO(dp, PMDA_INTERFACE_5, "logger DSO", mypath);
+ }
+
+ /* Read and parse config file. */
+ if (read_config(configfile) != 0) {
+ exit(1);
+ }
+ if (numlogfiles == 0) {
+ usage();
+ }
+
+ /* Create the dynamic metric info table based on the logfile
+ * table. */
+ dynamic_metric_infotab = malloc(sizeof(struct dynamic_metric_info)
+ * numdynamics * numlogfiles);
+ if (dynamic_metric_infotab == NULL) {
+ fprintf(stderr, "%s: allocation error: %s\n", __FUNCTION__,
+ strerror(errno));
+ return;
+ }
+ pinfo = dynamic_metric_infotab;
+ for (i = 0; i < numlogfiles; i++) {
+ for (j = 0; j < numdynamics; j++) {
+ pinfo->logfile = i;
+ pinfo->pmid_index = j;
+ pinfo++;
+ }
+ }
+
+ /* Create the metric table based on the static and dynamic metric
+ * tables. */
+ nummetrics = numstatics + (numlogfiles * numdynamics);
+ metrictab = malloc(sizeof(pmdaMetric) * nummetrics);
+ if (metrictab == NULL) {
+ free(dynamic_metric_infotab);
+ fprintf(stderr, "%s: allocation error: %s\n", __FUNCTION__,
+ strerror(errno));
+ return;
+ }
+ memcpy(metrictab, static_metrictab, sizeof(static_metrictab));
+ pmetric = &metrictab[numstatics];
+ pmid_num = numstatics;
+ pinfo = dynamic_metric_infotab;
+ for (i = 0; i < numlogfiles; i++) {
+ memcpy(pmetric, dynamic_metrictab, sizeof(dynamic_metrictab));
+ for (j = 0; j < numdynamics; j++) {
+ pmetric[j].m_desc.pmid = PMDA_PMID(0, pmid_num);
+ pmetric[j].m_user = pinfo++;
+ pmid_num++;
+ }
+ pmetric += numdynamics;
+ }
+
+ if (dp->status != 0)
+ return;
+ dp->version.four.profile = logger_profile;
+
+ /* Dynamic PMNS handling. */
+ dp->version.four.pmid = logger_pmid;
+ dp->version.four.name = logger_name;
+ dp->version.four.children = logger_children;
+ /* DRS: if we want to generate help text for the dynamic metrics,
+ * we'll have to override 'four.text'. */
+
+ pmdaSetFetchCallBack(dp, logger_fetchCallBack);
+ pmdaSetEndContextCallBack(dp, logger_end_contextCallBack);
+
+ pmdaInit(dp, NULL, 0, metrictab, nummetrics);
+
+ /* Create the dynamic PMNS tree and populate it. */
+ if ((rc = __pmNewPMNS(&pmns)) < 0) {
+ __pmNotifyErr(LOG_ERR, "%s: failed to create new pmns: %s\n",
+ pmProgname, pmErrStr(rc));
+ pmns = NULL;
+ return;
+ }
+ pmetric = &metrictab[numstatics];
+ for (i = 0; i < numlogfiles; i++) {
+ for (j = 0; j < numdynamics; j++) {
+ snprintf(name, sizeof(name), "logger.perfile.%s.%s",
+ logfiles[i].pmns_name, dynamic_nametab[j]);
+ __pmAddPMNSNode(pmns, pmetric[j].m_desc.pmid, name);
+ }
+ pmetric += numdynamics;
+ }
+ pmdaTreeRebuildHash(pmns, (numlogfiles * numdynamics)); /* for reverse (pmid->name) lookups */
+
+ /* Now that the metric table has been fully filled in, update
+ * each LogfileData with the proper string pmid to use. */
+ for (i = 0; i < numlogfiles; i++) {
+ logfiles[i].pmid_string = metrictab[2].m_desc.pmid;
+ }
+
+ event_init(dp, logfiles, numlogfiles);
+}
+
/*
* Set up the agent if running as a daemon.
*/
configfile = argv[optind];
pmdaOpenLog(&desc);
- if (read_config(configfile) != 0) {
- exit(1);
- }
-
- /* For now, only allow 1 logfile. */
- if (numlogfiles == 0) {
- usage();
- }
- if (numlogfiles > 1) {
- __pmNotifyErr(LOG_INFO, "%s: Only handling first logfile\n",
- __FUNCTION__);
- }
-
logger_init(&desc);
pmdaConnect(&desc);