diff options
Diffstat (limited to 'libnetdata')
39 files changed, 6194 insertions, 1438 deletions
diff --git a/libnetdata/Makefile.am b/libnetdata/Makefile.am index f01a1d34f0..0a854cd443 100644 --- a/libnetdata/Makefile.am +++ b/libnetdata/Makefile.am @@ -8,9 +8,11 @@ SUBDIRS = \ aral \ avl \ buffer \ + buffered_reader \ clocks \ completion \ config \ + datetime \ dictionary \ ebpf \ eval \ @@ -19,6 +21,7 @@ SUBDIRS = \ json \ july \ health \ + line_splitter \ locks \ log \ onewayalloc \ @@ -31,6 +34,7 @@ SUBDIRS = \ string \ threads \ url \ + uuid \ worker_utilization \ tests \ $(NULL) diff --git a/libnetdata/buffer/buffer.c b/libnetdata/buffer/buffer.c index 488e7dd24f..64f9cce47c 100644 --- a/libnetdata/buffer/buffer.c +++ b/libnetdata/buffer/buffer.c @@ -81,6 +81,7 @@ void buffer_snprintf(BUFFER *wb, size_t len, const char *fmt, ...) va_list args; va_start(args, fmt); + // vsnprintfz() returns the number of bytes actually written - after possible truncation wb->len += vsnprintfz(&wb->buffer[wb->len], len, fmt, args); va_end(args); @@ -89,53 +90,39 @@ void buffer_snprintf(BUFFER *wb, size_t len, const char *fmt, ...) // the buffer is \0 terminated by vsnprintfz } -void buffer_vsprintf(BUFFER *wb, const char *fmt, va_list args) -{ +inline void buffer_vsprintf(BUFFER *wb, const char *fmt, va_list args) { if(unlikely(!fmt || !*fmt)) return; - size_t wrote = 0, need = 2, space_remaining = 0; + size_t full_size_bytes = 0, need = 2, space_remaining = 0; do { - need += space_remaining * 2; + need += full_size_bytes + 2; - netdata_log_debug(D_WEB_BUFFER, "web_buffer_sprintf(): increasing web_buffer at position %zu, size = %zu, by %zu bytes (wrote = %zu)\n", wb->len, wb->size, need, wrote); buffer_need_bytes(wb, need); space_remaining = wb->size - wb->len - 1; - wrote = (size_t) vsnprintfz(&wb->buffer[wb->len], space_remaining, fmt, args); + // Use the copy of va_list for vsnprintf + va_list args_copy; + va_copy(args_copy, args); + // vsnprintf() returns the number of bytes required, even if bigger than the buffer provided + full_size_bytes = (size_t) vsnprintf(&wb->buffer[wb->len], space_remaining, fmt, args_copy); + va_end(args_copy); - } while(wrote >= space_remaining); + } while(full_size_bytes >= space_remaining); - wb->len += wrote; + wb->len += full_size_bytes; - // the buffer is \0 terminated by vsnprintf + wb->buffer[wb->len] = '\0'; + buffer_overflow_check(wb); } void buffer_sprintf(BUFFER *wb, const char *fmt, ...) { - if(unlikely(!fmt || !*fmt)) return; - va_list args; - size_t wrote = 0, need = 2, space_remaining = 0; - - do { - need += space_remaining * 2; - - netdata_log_debug(D_WEB_BUFFER, "web_buffer_sprintf(): increasing web_buffer at position %zu, size = %zu, by %zu bytes (wrote = %zu)\n", wb->len, wb->size, need, wrote); - buffer_need_bytes(wb, need); - - space_remaining = wb->size - wb->len - 1; - - va_start(args, fmt); - wrote = (size_t) vsnprintfz(&wb->buffer[wb->len], space_remaining, fmt, args); - va_end(args); - - } while(wrote >= space_remaining); - - wb->len += wrote; - - // the buffer is \0 terminated by vsnprintf + va_start(args, fmt); + buffer_vsprintf(wb, fmt, args); + va_end(args); } // generate a javascript date, the fastest possible way... diff --git a/libnetdata/buffer/buffer.h b/libnetdata/buffer/buffer.h index e5dd9992d1..88d3f0282a 100644 --- a/libnetdata/buffer/buffer.h +++ b/libnetdata/buffer/buffer.h @@ -94,6 +94,8 @@ typedef struct web_buffer { } json; } BUFFER; +#define CLEAN_BUFFER _cleanup_(buffer_freep) BUFFER + #define buffer_cacheable(wb) do { (wb)->options |= WB_CONTENT_CACHEABLE; if((wb)->options & WB_CONTENT_NO_CACHEABLE) (wb)->options &= ~WB_CONTENT_NO_CACHEABLE; } while(0) #define buffer_no_cacheable(wb) do { (wb)->options |= WB_CONTENT_NO_CACHEABLE; if((wb)->options & WB_CONTENT_CACHEABLE) (wb)->options &= ~WB_CONTENT_CACHEABLE; (wb)->expires = 0; } while(0) @@ -135,6 +137,10 @@ BUFFER *buffer_create(size_t size, size_t *statistics); void buffer_free(BUFFER *b); void buffer_increase(BUFFER *b, size_t free_size_required); +static inline void buffer_freep(BUFFER **bp) { + if(bp) buffer_free(*bp); +} + void buffer_snprintf(BUFFER *wb, size_t len, const char *fmt, ...) PRINTFLIKE(3, 4); void buffer_vsprintf(BUFFER *wb, const char *fmt, va_list args); void buffer_sprintf(BUFFER *wb, const char *fmt, ...) PRINTFLIKE(2,3); @@ -210,6 +216,13 @@ static inline void buffer_fast_rawcat(BUFFER *wb, const char *txt, size_t len) { buffer_overflow_check(wb); } +static inline void buffer_putc(BUFFER *wb, char c) { + buffer_need_bytes(wb, 2); + wb->buffer[wb->len++] = c; + wb->buffer[wb->len] = '\0'; + buffer_overflow_check(wb); +} + static inline void buffer_fast_strcat(BUFFER *wb, const char *txt, size_t len) { if(unlikely(!txt || !*txt || !len)) return; @@ -283,6 +296,19 @@ static inline void buffer_strncat(BUFFER *wb, const char *txt, size_t len) { buffer_overflow_check(wb); } +static inline void buffer_memcat(BUFFER *wb, const void *mem, size_t bytes) { + if(unlikely(!mem)) return; + + buffer_need_bytes(wb, bytes + 1); + + memcpy(&wb->buffer[wb->len], mem, bytes); + + wb->len += bytes; + wb->buffer[wb->len] = '\0'; + + buffer_overflow_check(wb); +} + static inline void buffer_json_strcat(BUFFER *wb, const char *txt) { if(unlikely(!txt || !*txt)) return; diff --git a/libnetdata/buffered_reader/Makefile.am b/libnetdata/buffered_reader/Makefile.am new file mode 100644 index 0000000000..161784b8f6 --- /dev/null +++ b/libnetdata/buffered_reader/Makefile.am @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +AUTOMAKE_OPTIONS = subdir-objects +MAINTAINERCLEANFILES = $(srcdir)/Makefile.in + +dist_noinst_DATA = \ + README.md \ + $(NULL) diff --git a/libnetdata/buffered_reader/README.md b/libnetdata/buffered_reader/README.md new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/libnetdata/buffered_reader/README.md diff --git a/libnetdata/buffered_reader/buffered_reader.c b/libnetdata/buffered_reader/buffered_reader.c new file mode 100644 index 0000000000..7cd17abfe7 --- /dev/null +++ b/libnetdata/buffered_reader/buffered_reader.c @@ -0,0 +1,3 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../libnetdata.h" diff --git a/libnetdata/buffered_reader/buffered_reader.h b/libnetdata/buffered_reader/buffered_reader.h new file mode 100644 index 0000000000..4db57cd29e --- /dev/null +++ b/libnetdata/buffered_reader/buffered_reader.h @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../libnetdata.h" + +#ifndef NETDATA_BUFFERED_READER_H +#define NETDATA_BUFFERED_READER_H + +struct buffered_reader { + ssize_t read_len; + ssize_t pos; + char read_buffer[PLUGINSD_LINE_MAX + 1]; +}; + +static inline void buffered_reader_init(struct buffered_reader *reader) { + reader->read_buffer[0] = '\0'; + reader->read_len = 0; + reader->pos = 0; +} + +typedef enum { + BUFFERED_READER_READ_OK = 0, + BUFFERED_READER_READ_FAILED = -1, + BUFFERED_READER_READ_BUFFER_FULL = -2, + BUFFERED_READER_READ_POLLERR = -3, + BUFFERED_READER_READ_POLLHUP = -4, + BUFFERED_READER_READ_POLLNVAL = -5, + BUFFERED_READER_READ_POLL_UNKNOWN = -6, + BUFFERED_READER_READ_POLL_TIMEOUT = -7, + BUFFERED_READER_READ_POLL_FAILED = -8, +} buffered_reader_ret_t; + + +static inline buffered_reader_ret_t buffered_reader_read(struct buffered_reader *reader, int fd) { +#ifdef NETDATA_INTERNAL_CHECKS + if(reader->read_buffer[reader->read_len] != '\0') + fatal("read_buffer does not start with zero"); +#endif + + char *read_at = reader->read_buffer + reader->read_len; + ssize_t remaining = sizeof(reader->read_buffer) - reader->read_len - 1; + + if(unlikely(remaining <= 0)) + return BUFFERED_READER_READ_BUFFER_FULL; + + ssize_t bytes_read = read(fd, read_at, remaining); + if(unlikely(bytes_read <= 0)) + return BUFFERED_READER_READ_FAILED; + + reader->read_len += bytes_read; + reader->read_buffer[reader->read_len] = '\0'; + + return BUFFERED_READER_READ_OK; +} + +static inline buffered_reader_ret_t buffered_reader_read_timeout(struct buffered_reader *reader, int fd, int timeout_ms, bool log_error) { + errno = 0; + struct pollfd fds[1]; + + fds[0].fd = fd; + fds[0].events = POLLIN; + + int ret = poll(fds, 1, timeout_ms); + + if (ret > 0) { + /* There is data to read */ + if (fds[0].revents & POLLIN) + return buffered_reader_read(reader, fd); + + else if(fds[0].revents & POLLERR) { + if(log_error) + netdata_log_error("PARSER: read failed: POLLERR."); + return BUFFERED_READER_READ_POLLERR; + } + else if(fds[0].revents & POLLHUP) { + if(log_error) + netdata_log_error("PARSER: read failed: POLLHUP."); + return BUFFERED_READER_READ_POLLHUP; + } + else if(fds[0].revents & POLLNVAL) { + if(log_error) + netdata_log_error("PARSER: read failed: POLLNVAL."); + return BUFFERED_READER_READ_POLLNVAL; + } + + if(log_error) + netdata_log_error("PARSER: poll() returned positive number, but POLLIN|POLLERR|POLLHUP|POLLNVAL are not set."); + return BUFFERED_READER_READ_POLL_UNKNOWN; + } + else if (ret == 0) { + if(log_error) + netdata_log_error("PARSER: timeout while waiting for data."); + return BUFFERED_READER_READ_POLL_TIMEOUT; + } + + if(log_error) + netdata_log_error("PARSER: poll() failed with code %d.", ret); + return BUFFERED_READER_READ_POLL_FAILED; +} + +/* Produce a full line if one exists, statefully return where we start next time. + * When we hit the end of the buffer with a partial line move it to the beginning for the next fill. + */ +static inline bool buffered_reader_next_line(struct buffered_reader *reader, BUFFER *dst) { + buffer_need_bytes(dst, reader->read_len - reader->pos + 2); + + size_t start = reader->pos; + + char *ss = &reader->read_buffer[start]; + char *se = &reader->read_buffer[reader->read_len]; + char *ds = &dst->buffer[dst->len]; + char *de = &ds[dst->size - dst->len - 2]; + + if(ss >= se) { + *ds = '\0'; + reader->pos = 0; + reader->read_len = 0; + reader->read_buffer[reader->read_len] = '\0'; + return false; + } + + // copy all bytes to buffer + while(ss < se && ds < de && *ss != '\n') { + *ds++ = *ss++; + dst->len++; + } + + // if we have a newline, return the buffer + if(ss < se && ds < de && *ss == '\n') { + // newline found in the r->read_buffer + + *ds++ = *ss++; // copy the newline too + dst->len++; + + *ds = '\0'; + + reader->pos = ss - reader->read_buffer; + return true; + } + + reader->pos = 0; + reader->read_len = 0; + reader->read_buffer[reader->read_len] = '\0'; + return false; +} + +#endif //NETDATA_BUFFERED_READER_H diff --git a/libnetdata/clocks/clocks.c b/libnetdata/clocks/clocks.c index 489e968553..c77fbb2387 100644 --- a/libnetdata/clocks/clocks.c +++ b/libnetdata/clocks/clocks.c @@ -298,8 +298,10 @@ usec_t heartbeat_next(heartbeat_t *hb, usec_t tick) { // TODO: The heartbeat tick should be specified at the heartbeat_init() function usec_t tmp = (now_realtime_usec() * clock_realtime_resolution) % (tick / 2); - error_limit_static_global_var(erl, 10, 0); - error_limit(&erl, "heartbeat randomness of %"PRIu64" is too big for a tick of %"PRIu64" - setting it to %"PRIu64"", hb->randomness, tick, tmp); + nd_log_limit_static_global_var(erl, 10, 0); + nd_log_limit(&erl, NDLS_DAEMON, NDLP_NOTICE, + "heartbeat randomness of %"PRIu64" is too big for a tick of %"PRIu64" - setting it to %"PRIu64"", + hb->randomness, tick, tmp); hb->randomness = tmp; } @@ -325,13 +327,19 @@ usec_t heartbeat_next(heartbeat_t *hb, usec_t tick) { if(unlikely(now < next)) { errno = 0; - error_limit_static_global_var(erl, 10, 0); - error_limit(&erl, "heartbeat clock: woke up %"PRIu64" microseconds earlier than expected (can be due to the CLOCK_REALTIME set to the past).", next - now); + nd_log_limit_static_global_var(erl, 10, 0); + nd_log_limit(&erl, NDLS_DAEMON, NDLP_NOTICE, + "heartbeat clock: woke up %"PRIu64" microseconds earlier than expected " + "(can be due to the CLOCK_REALTIME set to the past).", + next - now); } else if(unlikely(now - next > tick / 2)) { errno = 0; - error_limit_static_global_var(erl, 10, 0); - error_limit(&erl, "heartbeat clock: woke up %"PRIu64" microseconds later than expected (can be due to system load or the CLOCK_REALTIME set to the future).", now - next); + nd_log_limit_static_global_var(erl, 10, 0); + nd_log_limit(&erl, NDLS_DAEMON, NDLP_NOTICE, + "heartbeat clock: woke up %"PRIu64" microseconds later than expected " + "(can be due to system load or the CLOCK_REALTIME set to the future).", + now - next); } if(unlikely(!hb->realtime)) { diff --git a/libnetdata/datetime/Makefile.am b/libnetdata/datetime/Makefile.am new file mode 100644 index 0000000000..161784b8f6 --- /dev/null +++ b/libnetdata/datetime/Makefile.am @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +AUTOMAKE_OPTIONS = subdir-objects +MAINTAINERCLEANFILES = $(srcdir)/Makefile.in + +dist_noinst_DATA = \ + README.md \ + $(NULL) diff --git a/libnetdata/datetime/README.md b/libnetdata/datetime/README.md new file mode 100644 index 0000000000..ba23663162 --- /dev/null +++ b/libnetdata/datetime/README.md @@ -0,0 +1,11 @@ +<!-- +title: "Datetime" +custom_edit_url: https://github.com/netdata/netdata/edit/master/libnetdata/datetime/README.md +sidebar_label: "Datetime" +learn_topic_type: "Tasks" +learn_rel_path: "Developers/libnetdata" +--> + +# Datetime + +Formatting dates and timestamps. diff --git a/libnetdata/datetime/iso8601.c b/libnetdata/datetime/iso8601.c new file mode 100644 index 0000000000..8e3f4e0276 --- /dev/null +++ b/libnetdata/datetime/iso8601.c @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../libnetdata.h" + +size_t iso8601_datetime_ut(char *buffer, size_t len, usec_t now_ut, ISO8601_OPTIONS options) { + if(unlikely(!buffer || len == 0)) + return 0; + + time_t t = (time_t)(now_ut / USEC_PER_SEC); + struct tm *tmp, tmbuf; + + if(options & ISO8601_UTC) + // Use gmtime_r for UTC time conversion. + tmp = gmtime_r(&t, &tmbuf); + else + // Use localtime_r for local time conversion. + tmp = localtime_r(&t, &tmbuf); + + if (unlikely(!tmp)) { + buffer[0] = '\0'; + return 0; + } + + // Format the date and time according to the ISO 8601 format. + size_t used_length = strftime(buffer, len, "%Y-%m-%dT%H:%M:%S", tmp); + if (unlikely(used_length == 0)) { + buffer[0] = '\0'; + return 0; + } + + if(options & ISO8601_MILLISECONDS) { + // Calculate the remaining microseconds + int milliseconds = (int) ((now_ut % USEC_PER_SEC) / USEC_PER_MS); + if(milliseconds && len - used_length > 4) + used_length += snprintfz(buffer + used_length, len - used_length, ".%03d", milliseconds); + } + else if(options & ISO8601_MICROSECONDS) { + // Calculate the remaining microseconds + int microseconds = (int) (now_ut % USEC_PER_SEC); + if(microseconds && len - used_length > 7) + used_length += snprintfz(buffer + used_length, len - used_length, ".%06d", microseconds); + } + + if(options & ISO8601_UTC) { + if(used_length + 1 < len) { + buffer[used_length++] = 'Z'; + buffer[used_length] = '\0'; // null-terminate the string. + } + } + else { + // Calculate the timezone offset in hours and minutes from UTC. + long offset = tmbuf.tm_gmtoff; + int hours = (int) (offset / 3600); // Convert offset seconds to hours. + int minutes = (int) ((offset % 3600) / 60); // Convert remainder to minutes (keep the sign for minutes). + + // Check if timezone is UTC. + if(hours == 0 && minutes == 0) { + // For UTC, append 'Z' to the timestamp. + if(used_length + 1 < len) { + buffer[used_length++] = 'Z'; + buffer[used_length] = '\0'; // null-terminate the string. + } + } + else { + // For non-UTC, format the timezone offset. Omit minutes if they are zero. + if(minutes == 0) { + // Check enough space is available for the timezone offset string. + if(used_length + 3 < len) // "+hh\0" + used_length += snprintfz(buffer + used_length, len - used_length, "%+03d", hours); + } + else { + // Check enough space is available for the timezone offset string. + if(used_length + 6 < len) // "+hh:mm\0" + used_length += snprintfz(buffer + used_length, len - used_length, + "%+03d:%02d", hours, abs(minutes)); + } + } + } + + return used_length; +} diff --git a/libnetdata/datetime/iso8601.h b/libnetdata/datetime/iso8601.h new file mode 100644 index 0000000000..ce4800963a --- /dev/null +++ b/libnetdata/datetime/iso8601.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../libnetdata.h" + +#ifndef NETDATA_ISO8601_H +#define NETDATA_ISO8601_H + +typedef enum __attribute__((__packed__)) { + ISO8601_UTC = (1 << 0), + ISO8601_LOCAL_TIMEZONE = (1 << 1), + ISO8601_MILLISECONDS = (1 << 2), + ISO8601_MICROSECONDS = (1 << 3), +} ISO8601_OPTIONS; + +#define ISO8601_MAX_LENGTH 64 +size_t iso8601_datetime_ut(char *buffer, size_t len, usec_t now_ut, ISO8601_OPTIONS options); + +#endif //NETDATA_ISO8601_H diff --git a/libnetdata/datetime/rfc7231.c b/libnetdata/datetime/rfc7231.c new file mode 100644 index 0000000000..4925ed2c95 --- /dev/null +++ b/libnetdata/datetime/rfc7231.c @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../libnetdata.h" + +inline size_t rfc7231_datetime(char *buffer, size_t len, time_t now_t) { + if (unlikely(!buffer || !len)) + return 0; + + struct tm *tmp, tmbuf; + + // Use gmtime_r for UTC time conversion. + tmp = gmtime_r(&now_t, &tmbuf); + + if (unlikely(!tmp)) { + buffer[0] = '\0'; + return 0; + } + + // Format the date and time according to the RFC 7231 format. + size_t ret = strftime(buffer, len, "%a, %d %b %Y %H:%M:%S GMT", tmp); + if (unlikely(ret == 0)) + buffer[0] = '\0'; + + return ret; +} + +size_t rfc7231_datetime_ut(char *buffer, size_t len, usec_t now_ut) { + return rfc7231_datetime(buffer, len, (time_t) (now_ut / USEC_PER_SEC)); +} diff --git a/libnetdata/datetime/rfc7231.h b/libnetdata/datetime/rfc7231.h new file mode 100644 index 0000000000..5ba93053fb --- /dev/null +++ b/libnetdata/datetime/rfc7231.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../libnetdata.h" + +#ifndef NETDATA_RFC7231_H +#define NETDATA_RFC7231_H + +#define RFC7231_MAX_LENGTH 30 +size_t rfc7231_datetime(char *buffer, size_t len, time_t now_t); +size_t rfc7231_datetime_ut(char *buffer, size_t len, usec_t now_ut); + +#endif //NETDATA_RFC7231_H diff --git a/libnetdata/functions_evloop/functions_evloop.c b/libnetdata/functions_evloop/functions_evloop.c index 3fcd70aa1f..44f080a9d9 100644 --- a/libnetdata/functions_evloop/functions_evloop.c +++ b/libnetdata/functions_evloop/functions_evloop.c @@ -64,6 +64,12 @@ static void *rrd_functions_worker_globals_worker_main(void *arg) { pthread_mutex_unlock(&wg->worker_mutex); if(acquired) { + ND_LOG_STACK lgs[] = { + ND_LOG_FIELD_TXT(NDF_REQUEST, j->cmd), + ND_LOG_FIELD_END(), + }; + ND_LOG_STACK_PUSH(lgs); + last_acquired = true; j = dictionary_acquired_item_value(acquired); j->cb(j->transaction, j->cmd, j->timeout, &j->cancelled); diff --git a/libnetdata/inlined.h b/libnetdata/inlined.h index 9c07d97b6d..535b791e3f 100644 --- a/libnetdata/inlined.h +++ b/libnetdata/inlined.h @@ -426,13 +426,24 @@ static inline void sanitize_json_string(char *dst, const char *src, size_t dst_s } static inline bool sanitize_command_argument_string(char *dst, const char *src, size_t dst_size) { + if(dst_size) + *dst = '\0'; + // skip leading dashes - while (src[0] == '-') + while (*src == '-') src++; - // escape single quotes - while (src[0] != '\0') { - if (src[0] == '\'') { + while (*src != '\0') { + if (dst_size < 1) + return false; + + if (iscntrl(*src) || *src == '$') { + // remove control characters and characters that are expanded by bash + *dst++ = '_'; + dst_size--; + } + else if (*src == '\'' || *src == '`') { + // escape single quotes if (dst_size < 4) return false; @@ -440,14 +451,10 @@ static inline bool sanitize_command_argument_string(char *dst, const char *src, dst += 4; dst_size -= 4; - } else { - if (dst_size < 1) - return false; - - dst[0] = src[0]; - - dst += 1; - dst_size -= 1; + } + else { + *dst++ = *src; + dst_size--; } src++; @@ -456,6 +463,7 @@ static inline bool sanitize_command_argument_string(char *dst, const char *src, // make sure we have space to terminate the string if (dst_size == 0) return false; + *dst = '\0'; return true; @@ -531,10 +539,6 @@ static inline int read_single_base64_or_hex_number_file(const char *filename, un } } -static inline int uuid_memcmp(const uuid_t *uu1, const uuid_t *uu2) { - return memcmp(uu1, uu2, sizeof(uuid_t)); -} - static inline char *strsep_skip_consecutive_separators(char **ptr, |