diff options
Diffstat (limited to 'collectors/plugins.d/pluginsd_parser.c')
-rw-r--r-- | collectors/plugins.d/pluginsd_parser.c | 738 |
1 files changed, 738 insertions, 0 deletions
diff --git a/collectors/plugins.d/pluginsd_parser.c b/collectors/plugins.d/pluginsd_parser.c new file mode 100644 index 0000000..4a97c55 --- /dev/null +++ b/collectors/plugins.d/pluginsd_parser.c @@ -0,0 +1,738 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "pluginsd_parser.h" + +/* + * This is the action defined for the FLUSH command + */ +PARSER_RC pluginsd_set_action(void *user, RRDSET *st, RRDDIM *rd, long long int value) +{ + UNUSED(user); + + rrddim_set_by_pointer(st, rd, value); + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_flush_action(void *user, RRDSET *st) +{ + UNUSED(user); + UNUSED(st); + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_begin_action(void *user, RRDSET *st, usec_t microseconds, int trust_durations) +{ + UNUSED(user); + if (likely(st->counter_done)) { + if (likely(microseconds)) { + if (trust_durations) + rrdset_next_usec_unfiltered(st, microseconds); + else + rrdset_next_usec(st, microseconds); + } else + rrdset_next(st); + } + return PARSER_RC_OK; +} + + +PARSER_RC pluginsd_end_action(void *user, RRDSET *st) +{ + UNUSED(user); + + rrdset_done(st); + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_chart_action(void *user, char *type, char *id, char *name, char *family, char *context, char *title, char *units, char *plugin, + char *module, int priority, int update_every, RRDSET_TYPE chart_type, char *options) +{ + RRDSET *st = NULL; + RRDHOST *host = ((PARSER_USER_OBJECT *) user)->host; + + st = rrdset_create( + host, type, id, name, family, context, title, units, + plugin, module, priority, update_every, + chart_type); + + if (options && *options) { + if (strstr(options, "obsolete")) + rrdset_is_obsolete(st); + else + rrdset_isnot_obsolete(st); + + if (strstr(options, "detail")) + rrdset_flag_set(st, RRDSET_FLAG_DETAIL); + else + rrdset_flag_clear(st, RRDSET_FLAG_DETAIL); + + if (strstr(options, "hidden")) + rrdset_flag_set(st, RRDSET_FLAG_HIDDEN); + else + rrdset_flag_clear(st, RRDSET_FLAG_HIDDEN); + + if (strstr(options, "store_first")) + rrdset_flag_set(st, RRDSET_FLAG_STORE_FIRST); + else + rrdset_flag_clear(st, RRDSET_FLAG_STORE_FIRST); + } else { + rrdset_isnot_obsolete(st); + rrdset_flag_clear(st, RRDSET_FLAG_DETAIL); + rrdset_flag_clear(st, RRDSET_FLAG_STORE_FIRST); + } + ((PARSER_USER_OBJECT *)user)->st = st; + + return PARSER_RC_OK; +} + + +PARSER_RC pluginsd_disable_action(void *user) +{ + UNUSED(user); + + info("called DISABLE. Disabling it."); + ((PARSER_USER_OBJECT *) user)->enabled = 0; + return PARSER_RC_ERROR; +} + + +PARSER_RC pluginsd_variable_action(void *user, RRDHOST *host, RRDSET *st, char *name, int global, calculated_number value) +{ + UNUSED(user); + + if (global) { + RRDVAR *rv = rrdvar_custom_host_variable_create(host, name); + if (rv) + rrdvar_custom_host_variable_set(host, rv, value); + else + error("cannot find/create HOST VARIABLE '%s' on host '%s'", name, host->hostname); + } else { + RRDSETVAR *rs = rrdsetvar_custom_chart_variable_create(st, name); + if (rs) + rrdsetvar_custom_chart_variable_set(rs, value); + else + error("cannot find/create CHART VARIABLE '%s' on host '%s', chart '%s'", name, host->hostname, st->id); + } + return PARSER_RC_OK; +} + + + +PARSER_RC pluginsd_dimension_action(void *user, RRDSET *st, char *id, char *name, char *algorithm, long multiplier, long divisor, char *options, + RRD_ALGORITHM algorithm_type) +{ + UNUSED(user); + UNUSED(algorithm); + + RRDDIM *rd = rrddim_add(st, id, name, multiplier, divisor, algorithm_type); + rrddim_flag_clear(rd, RRDDIM_FLAG_HIDDEN); + rrddim_flag_clear(rd, RRDDIM_FLAG_DONT_DETECT_RESETS_OR_OVERFLOWS); + if (options && *options) { + if (strstr(options, "obsolete") != NULL) + rrddim_is_obsolete(st, rd); + else + rrddim_isnot_obsolete(st, rd); + if (strstr(options, "hidden") != NULL) + rrddim_flag_set(rd, RRDDIM_FLAG_HIDDEN); + if (strstr(options, "noreset") != NULL) + rrddim_flag_set(rd, RRDDIM_FLAG_DONT_DETECT_RESETS_OR_OVERFLOWS); + if (strstr(options, "nooverflow") != NULL) + rrddim_flag_set(rd, RRDDIM_FLAG_DONT_DETECT_RESETS_OR_OVERFLOWS); + } else { + rrddim_isnot_obsolete(st, rd); + } + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_label_action(void *user, char *key, char *value, LABEL_SOURCE source) +{ + + ((PARSER_USER_OBJECT *) user)->new_labels = add_label_to_list(((PARSER_USER_OBJECT *) user)->new_labels, key, value, source); + + return PARSER_RC_OK; +} + + +PARSER_RC pluginsd_overwrite_action(void *user, RRDHOST *host, struct label *new_labels) +{ + UNUSED(user); + + if (!host->labels.head) { + host->labels.head = new_labels; + } else { + rrdhost_rdlock(host); + replace_label_list(&host->labels, new_labels); + rrdhost_unlock(host); + } + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_set(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + char *dimension = words[1]; + char *value = words[2]; + + RRDSET *st = ((PARSER_USER_OBJECT *) user)->st; + RRDHOST *host = ((PARSER_USER_OBJECT *) user)->host; + + if (unlikely(!dimension || !*dimension)) { + error("requested a SET on chart '%s' of host '%s', without a dimension. Disabling it.", st->id, host->hostname); + goto disable; + } + + if (unlikely(!value || !*value)) + value = NULL; + + if (unlikely(!st)) { + error( + "requested a SET on dimension %s with value %s on host '%s', without a BEGIN. Disabling it.", dimension, + value ? value : "<nothing>", host->hostname); + goto disable; + } + + if (unlikely(rrdset_flag_check(st, RRDSET_FLAG_DEBUG))) + debug(D_PLUGINSD, "is setting dimension %s/%s to %s", st->id, dimension, value ? value : "<nothing>"); + + if (value) { + RRDDIM *rd = rrddim_find(st, dimension); + if (unlikely(!rd)) { + error( + "requested a SET to dimension with id '%s' on stats '%s' (%s) on host '%s', which does not exist. Disabling it.", + dimension, st->name, st->id, st->rrdhost->hostname); + goto disable; + } else { + if (plugins_action->set_action) { + return plugins_action->set_action( + user, st, rd, strtoll(value, NULL, 0)); + } + } + } + return PARSER_RC_OK; + +disable: + ((PARSER_USER_OBJECT *) user)->enabled = 0; + return PARSER_RC_ERROR; +} + +PARSER_RC pluginsd_begin(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + char *id = words[1]; + char *microseconds_txt = words[2]; + + RRDSET *st = NULL; + RRDHOST *host = ((PARSER_USER_OBJECT *)user)->host; + + if (unlikely(!id)) { + error("requested a BEGIN without a chart id for host '%s'. Disabling it.", host->hostname); + goto disable; + } + + st = rrdset_find(host, id); + if (unlikely(!st)) { + error("requested a BEGIN on chart '%s', which does not exist on host '%s'. Disabling it.", id, host->hostname); + goto disable; + } + ((PARSER_USER_OBJECT *)user)->st = st; + + usec_t microseconds = 0; + if (microseconds_txt && *microseconds_txt) + microseconds = str2ull(microseconds_txt); + + if (plugins_action->begin_action) { + return plugins_action->begin_action(user, st, microseconds, + ((PARSER_USER_OBJECT *)user)->trust_durations); + } + return PARSER_RC_OK; +disable: + ((PARSER_USER_OBJECT *)user)->enabled = 0; + return PARSER_RC_ERROR; +} + +PARSER_RC pluginsd_end(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + UNUSED(words); + RRDSET *st = ((PARSER_USER_OBJECT *) user)->st; + RRDHOST *host = ((PARSER_USER_OBJECT *) user)->host; + + if (unlikely(!st)) { + error("requested an END, without a BEGIN on host '%s'. Disabling it.", host->hostname); + ((PARSER_USER_OBJECT *) user)->enabled = 0; + return PARSER_RC_ERROR; + } + + if (unlikely(rrdset_flag_check(st, RRDSET_FLAG_DEBUG))) + debug(D_PLUGINSD, "requested an END on chart %s", st->id); + + ((PARSER_USER_OBJECT *) user)->st = NULL; + ((PARSER_USER_OBJECT *) user)->count++; + if (plugins_action->end_action) { + return plugins_action->end_action(user, st); + } + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_chart(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + RRDHOST *host = ((PARSER_USER_OBJECT *) user)->host; + if (unlikely(!host && !((PARSER_USER_OBJECT *) user)->host_exists)) { + debug(D_PLUGINSD, "Ignoring chart belonging to missing or ignored host."); + return PARSER_RC_OK; + } + + char *type = words[1]; + char *name = words[2]; + char *title = words[3]; + char *units = words[4]; + char *family = words[5]; + char *context = words[6]; + char *chart = words[7]; + char *priority_s = words[8]; + char *update_every_s = words[9]; + char *options = words[10]; + char *plugin = words[11]; + char *module = words[12]; + + int have_action = ((plugins_action->chart_action) != NULL); + + // parse the id from type + char *id = NULL; + if (likely(type && (id = strchr(type, '.')))) { + *id = '\0'; + id++; + } + + // make sure we have the required variables + if (unlikely((!type || !*type || !id || !*id))) { + if (likely(host)) + error("requested a CHART, without a type.id, on host '%s'. Disabling it.", host->hostname); + else + error("requested a CHART, without a type.id. Disabling it."); + ((PARSER_USER_OBJECT *) user)->enabled = 0; + return PARSER_RC_ERROR; + } + + // parse the name, and make sure it does not include 'type.' + if (unlikely(name && *name)) { + // when data are streamed from child nodes + // name will be type.name + // so we have to remove 'type.' from name too + size_t len = strlen(type); + if (strncmp(type, name, len) == 0 && name[len] == '.') + name = &name[len + 1]; + + // if the name is the same with the id, + // or is just 'NULL', clear it. + if (unlikely(strcmp(name, id) == 0 || strcasecmp(name, "NULL") == 0 || strcasecmp(name, "(NULL)") == 0)) + name = NULL; + } + + int priority = 1000; + if (likely(priority_s && *priority_s)) + priority = str2i(priority_s); + + int update_every = ((PARSER_USER_OBJECT *) user)->cd->update_every; + if (likely(update_every_s && *update_every_s)) + update_every = str2i(update_every_s); + if (unlikely(!update_every)) + update_every = ((PARSER_USER_OBJECT *) user)->cd->update_every; + + RRDSET_TYPE chart_type = RRDSET_TYPE_LINE; + if (unlikely(chart)) + chart_type = rrdset_type_id(chart); + + if (unlikely(name && !*name)) + name = NULL; + if (unlikely(family && !*family)) + family = NULL; + if (unlikely(context && !*context)) + context = NULL; + if (unlikely(!title)) + title = ""; + if (unlikely(!units)) + units = "unknown"; + + debug( + D_PLUGINSD, + "creating chart type='%s', id='%s', name='%s', family='%s', context='%s', chart='%s', priority=%d, update_every=%d", + type, id, name ? name : "", family ? family : "", context ? context : "", rrdset_type_name(chart_type), + priority, update_every); + + if (have_action) { + return plugins_action->chart_action( + user, type, id, name, family, context, title, units, + (plugin && *plugin) ? plugin : ((PARSER_USER_OBJECT *)user)->cd->filename, module, priority, update_every, + chart_type, options); + } + + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_dimension(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + char *id = words[1]; + char *name = words[2]; + char *algorithm = words[3]; + char *multiplier_s = words[4]; + char *divisor_s = words[5]; + char *options = words[6]; + + RRDSET *st = ((PARSER_USER_OBJECT *) user)->st; + RRDHOST *host = ((PARSER_USER_OBJECT *) user)->host; + if (unlikely(!host && !((PARSER_USER_OBJECT *) user)->host_exists)) { + debug(D_PLUGINSD, "Ignoring dimension belonging to missing or ignored host."); + return PARSER_RC_OK; + } + + if (unlikely(!id)) { + error( + "requested a DIMENSION, without an id, host '%s' and chart '%s'. Disabling it.", host->hostname, + st ? st->id : "UNSET"); + goto disable; + } + + if (unlikely(!st && !((PARSER_USER_OBJECT *) user)->st_exists)) { + error("requested a DIMENSION, without a CHART, on host '%s'. Disabling it.", host->hostname); + goto disable; + } + + long multiplier = 1; + if (multiplier_s && *multiplier_s) { + multiplier = strtol(multiplier_s, NULL, 0); + if (unlikely(!multiplier)) + multiplier = 1; + } + + long divisor = 1; + if (likely(divisor_s && *divisor_s)) { + divisor = strtol(divisor_s, NULL, 0); + if (unlikely(!divisor)) + divisor = 1; + } + + if (unlikely(!algorithm || !*algorithm)) + algorithm = "absolute"; + + if (unlikely(st && rrdset_flag_check(st, RRDSET_FLAG_DEBUG))) + debug( + D_PLUGINSD, + "creating dimension in chart %s, id='%s', name='%s', algorithm='%s', multiplier=%ld, divisor=%ld, hidden='%s'", + st->id, id, name ? name : "", rrd_algorithm_name(rrd_algorithm_id(algorithm)), multiplier, divisor, + options ? options : ""); + + if (plugins_action->dimension_action) { + return plugins_action->dimension_action( + user, st, id, name, algorithm, + multiplier, divisor, (options && *options)?options:NULL, rrd_algorithm_id(algorithm)); + } + + return PARSER_RC_OK; +disable: + ((PARSER_USER_OBJECT *)user)->enabled = 0; + return PARSER_RC_ERROR; +} + +PARSER_RC pluginsd_variable(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + char *name = words[1]; + char *value = words[2]; + calculated_number v; + + RRDSET *st = ((PARSER_USER_OBJECT *) user)->st; + RRDHOST *host = ((PARSER_USER_OBJECT *) user)->host; + + int global = (st) ? 0 : 1; + + if (name && *name) { + if ((strcmp(name, "GLOBAL") == 0 || strcmp(name, "HOST") == 0)) { + global = 1; + name = words[2]; + value = words[3]; + } else if ((strcmp(name, "LOCAL") == 0 || strcmp(name, "CHART") == 0)) { + global = 0; + name = words[2]; + value = words[3]; + } + } + + if (unlikely(!name || !*name)) { + error("requested a VARIABLE on host '%s', without a variable name. Disabling it.", host->hostname); + ((PARSER_USER_OBJECT *)user)->enabled = 0; + return PARSER_RC_ERROR; + } + + if (unlikely(!value || !*value)) + value = NULL; + + if (unlikely(!value)) { + error("cannot set %s VARIABLE '%s' on host '%s' to an empty value", (global) ? "HOST" : "CHART", name, + host->hostname); + return PARSER_RC_OK; + } + + if (!global && !st) { + error("cannot find/create CHART VARIABLE '%s' on host '%s' without a chart", name, host->hostname); + return PARSER_RC_OK; + } + + char *endptr = NULL; + v = (calculated_number)str2ld(value, &endptr); + if (unlikely(endptr && *endptr)) { + if (endptr == value) + error( + "the value '%s' of VARIABLE '%s' on host '%s' cannot be parsed as a number", value, name, + host->hostname); + else + error( + "the value '%s' of VARIABLE '%s' on host '%s' has leftovers: '%s'", value, name, host->hostname, + endptr); + } + + if (plugins_action->variable_action) { + return plugins_action->variable_action(user, host, st, name, global, v); + } + + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_flush(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + UNUSED(words); + debug(D_PLUGINSD, "requested a FLUSH"); + RRDSET *st = ((PARSER_USER_OBJECT *) user)->st; + ((PARSER_USER_OBJECT *) user)->st = NULL; + if (plugins_action->flush_action) { + return plugins_action->flush_action(user, st); + } + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_disable(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + UNUSED(user); + UNUSED(words); + + if (plugins_action->disable_action) { + return plugins_action->disable_action(user); + } + return PARSER_RC_ERROR; +} + +PARSER_RC pluginsd_label(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + char *store; + + if (!words[1] || !words[2] || !words[3]) { + error("Ignoring malformed or empty LABEL command."); + return PARSER_RC_OK; + } + if (!words[4]) + store = words[3]; + else { + store = callocz(PLUGINSD_LINE_MAX + 1, sizeof(char)); + size_t remaining = PLUGINSD_LINE_MAX; + char *move = store; + int i = 3; + while (i < PLUGINSD_MAX_WORDS) { + size_t length = strlen(words[i]); + if ((length + 1) >= remaining) + break; + + remaining -= (length + 1); + memcpy(move, words[i], length); + move += length; + *move++ = ' '; + + i++; + if (!words[i]) + break; + } + } + + if (plugins_action->label_action) { + PARSER_RC rc = plugins_action->label_action(user, words[1], store, strtol(words[2], NULL, 10)); + if (store != words[3]) + freez(store); + return rc; + } + + if (store != words[3]) + freez(store); + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_overwrite(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + UNUSED(words); + + RRDHOST *host = ((PARSER_USER_OBJECT *) user)->host; + debug(D_PLUGINSD, "requested a OVERWITE a variable"); + + struct label *new_labels = ((PARSER_USER_OBJECT *)user)->new_labels; + ((PARSER_USER_OBJECT *)user)->new_labels = NULL; + + if (plugins_action->overwrite_action) { + return plugins_action->overwrite_action(user, host, new_labels); + } + + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_guid(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + char *uuid_str = words[1]; + uuid_t uuid; + + if (unlikely(!uuid_str)) { + error("requested a GUID, without a uuid."); + return PARSER_RC_ERROR; + } + if (unlikely(strlen(uuid_str) != GUID_LEN || uuid_parse(uuid_str, uuid) == -1)) { + error("requested a GUID, without a valid uuid string."); + return PARSER_RC_ERROR; + } + + debug(D_PLUGINSD, "Parsed uuid=%s", uuid_str); + if (plugins_action->guid_action) { + return plugins_action->guid_action(user, &uuid); + } + + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_context(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + char *uuid_str = words[1]; + uuid_t uuid; + + if (unlikely(!uuid_str)) { + error("requested a CONTEXT, without a uuid."); + return PARSER_RC_ERROR; + } + if (unlikely(strlen(uuid_str) != GUID_LEN || uuid_parse(uuid_str, uuid) == -1)) { + error("requested a CONTEXT, without a valid uuid string."); + return PARSER_RC_ERROR; + } + + debug(D_PLUGINSD, "Parsed uuid=%s", uuid_str); + if (plugins_action->context_action) { + return plugins_action->context_action(user, &uuid); + } + + return PARSER_RC_OK; +} + +PARSER_RC pluginsd_tombstone(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + char *uuid_str = words[1]; + uuid_t uuid; + + if (unlikely(!uuid_str)) { + error("requested a TOMBSTONE, without a uuid."); + return PARSER_RC_ERROR; + } + if (unlikely(strlen(uuid_str) != GUID_LEN || uuid_parse(uuid_str, uuid) == -1)) { + error("requested a TOMBSTONE, without a valid uuid string."); + return PARSER_RC_ERROR; + } + + debug(D_PLUGINSD, "Parsed uuid=%s", uuid_str); + if (plugins_action->tombstone_action) { + return plugins_action->tombstone_action(user, &uuid); + } + + return PARSER_RC_OK; +} + +PARSER_RC metalog_pluginsd_host(char **words, void *user, PLUGINSD_ACTION *plugins_action) +{ + char *machine_guid = words[1]; + char *hostname = words[2]; + char *registry_hostname = words[3]; + char *update_every_s = words[4]; + char *os = words[5]; + char *timezone = words[6]; + char *tags = words[7]; + + int update_every = 1; + if (likely(update_every_s && *update_every_s)) + update_every = str2i(update_every_s); + if (unlikely(!update_every)) + update_every = 1; + + debug(D_PLUGINSD, "HOST PARSED: guid=%s, hostname=%s, reg_host=%s, update=%d, os=%s, timezone=%s, tags=%s", + machine_guid, hostname, registry_hostname, update_every, os, timezone, tags); + + if (plugins_action->host_action) { + return plugins_action->host_action( + user, machine_guid, hostname, registry_hostname, update_every, os, timezone, tags); + } + + return PARSER_RC_OK; +} + +// New plugins.d parser + +inline size_t pluginsd_process(RRDHOST *host, struct plugind *cd, FILE *fp, int trust_durations) +{ + int enabled = cd->enabled; + + if (!fp || !enabled) { + cd->enabled = 0; + return 0; + } + + if (unlikely(fileno(fp) == -1)) { + error("file descriptor given is not a valid stream"); + cd->serial_failures++; + return 0; + } + clearerr(fp); + + PARSER_USER_OBJECT *user = callocz(1, sizeof(*user)); + ((PARSER_USER_OBJECT *) user)->enabled = cd->enabled; + ((PARSER_USER_OBJECT *) user)->host = host; + ((PARSER_USER_OBJECT *) user)->cd = cd; + ((PARSER_USER_OBJECT *) user)->trust_durations = trust_durations; + + PARSER *parser = parser_init(host, user, fp, PARSER_INPUT_SPLIT); + + if (unlikely(!parser)) { + error("Failed to initialize parser"); + cd->serial_failures++; + return 0; + } + + parser->plugins_action->begin_action = &pluginsd_begin_action; + parser->plugins_action->flush_action = &pluginsd_flush_action; + parser->plugins_action->end_action = &pluginsd_end_action; + parser->plugins_action->disable_action = &pluginsd_disable_action; + parser->plugins_action->variable_action = &pluginsd_variable_action; + parser->plugins_action->dimension_action = &pluginsd_dimension_action; + parser->plugins_action->label_action = &pluginsd_label_action; + parser->plugins_action->overwrite_action = &pluginsd_overwrite_action; + parser->plugins_action->chart_action = &pluginsd_chart_action; + parser->plugins_action->set_action = &pluginsd_set_action; + + user->parser = parser; + + while (likely(!parser_next(parser))) { + if (unlikely(netdata_exit || parser_action(parser, NULL))) + break; + } + info("PARSER ended"); + + parser_destroy(parser); + + cd->enabled = ((PARSER_USER_OBJECT *) user)->enabled; + size_t count = ((PARSER_USER_OBJECT *) user)->count; + + freez(user); + + if (likely(count)) { + cd->successful_collections += count; + cd->serial_failures = 0; + } else + cd->serial_failures++; + + return count; +} |