text: /* Copyright (c) 2011-2017 Dovecot authors, see the included COPYING file */
  
  #include "lib.h"
  #include "array.h"
  #include "net.h"
  #include "ioloop.h"
  #include "istream.h"
  #include "hash.h"
  #include "str.h"
  #include "strescape.h"
  #include "write-full.h"
  #include "doveadm.h"
  #include "doveadm-print.h"
  
  #include <stdio.h>
  #include <unistd.h>
  #include <sys/ioctl.h>
  #include <termios.h>
  
  struct top_line {
  	char *id;
  	/* [headers_count] */
  	const char **prev_values, **cur_values;
  
  	unsigned int flip:1;
  };
  
  struct top_context {
  	const char *path;
  	int fd;
  	struct istream *input;
  	const char *sort_type;
  
  	char **headers;
  	unsigned int headers_count;
  
  	pool_t prev_pool, cur_pool;
  	/* id => struct top_line. */
  	HASH_TABLE(char *, struct top_line *) sessions;
  	ARRAY(struct top_line *) lines;
  	int (*lines_sort)(struct top_line *const *, struct top_line *const *);
  
  	unsigned int last_update_idx, user_idx;
  	unsigned int sort_idx1, sort_idx2;
  
  	unsigned int flip:1;
  };
  
  static struct top_context *sort_ctx = NULL;
  static const char *disk_input_field = "disk_input";
  static const char *disk_output_field = "disk_output";
  
  static char **
  p_read_next_line(pool_t pool, struct istream *input)
  {
  	const char *line;
  	char **args;
  	unsigned int i;
  
  	line = i_stream_read_next_line(input);
  	if (line == NULL)
  		return NULL;
  
  	args = p_strsplit(pool, line, "\t");
  	for (i = 0; args[i] != NULL; i++)
  		args[i] = str_tabunescape(args[i]);
  	return args;
  }
  
  static const char *const*
  read_next_line(struct istream *input)
  {
  	return (const void *)p_read_next_line(pool_datastack_create(), input);
  }
  
  static void stats_dump(const char *path, const char *cmd)
  {
  	struct istream *input;
  	const char *const *args;
  	unsigned int i;
  	int fd;
  
  	fd = doveadm_connect(path);
  	net_set_nonblock(fd, FALSE);
  	if (write_full(fd, cmd, strlen(cmd)) < 0)
  		i_fatal("write(%s) failed: %m", path);
  
  	input = i_stream_create_fd_autoclose(&fd, (size_t)-1);
  
  	/* read header */
  	args = read_next_line(input);
  	if (args == NULL)
  		i_fatal("read(%s) unexpectedly disconnected", path);
  	if (*args == NULL)
  		i_info("no statistics available");
  	else {
  		for (; *args != NULL; args++)
  			doveadm_print_header_simple(*args);
  
  		/* read lines */
  		do {
  			T_BEGIN {
  				args = read_next_line(input);
  				if (args != NULL && args[0] == NULL)
  					args = NULL;
  				if (args != NULL) {
  					for (i = 0; args[i] != NULL; i++)
  						doveadm_print(args[i]);
  				}
  			} T_END;
  		} while (args != NULL);
  	}
  	if (input->stream_errno != 0)
  		i_fatal("read(%s) failed: %s", path, i_stream_get_error(input));
  	i_stream_destroy(&input);
  }
  
  static void
  doveadm_cmd_stats_dump(struct doveadm_cmd_context* cctx)
  {
  	const char *path, *cmd;
  	const char *args[3] = {0};
  
  	if (!doveadm_cmd_param_str(cctx, "socket-path", &path))
  		path = t_strconcat(doveadm_settings->base_dir, "/stats", NULL);
  
  	if (!doveadm_cmd_param_str(cctx, "type", &args[0])) {
  		i_error("Missing type parameter");
  		doveadm_exit_code = EX_USAGE;
  		return;
  	}
  
  	/* purely optional */
  	if (!doveadm_cmd_param_str(cctx, "filter", &args[1]))
  		args[1] = NULL;
  
  	cmd = t_strdup_printf("EXPORT\t%s\n", t_strarray_join(args, "\t"));
  
  	doveadm_print_init(DOVEADM_PRINT_TYPE_TAB);
  	stats_dump(path, cmd);
  	return;
  }
  
  static void
  stats_line_set_prev_values(struct top_context *ctx,
  			   const struct top_line *old_line,
  			   struct top_line *line)
  {
  	const char **values;
  	unsigned int i;
  
  	if (old_line->prev_values == NULL ||
  	    strcmp(old_line->cur_values[ctx->last_update_idx],
  		   line->cur_values[ctx->last_update_idx]) != 0) {
  		values = old_line->cur_values;
  	} else {
  		values = old_line->prev_values;
  	}
  
  	/* line hasn't been updated, keep old values */
  	line->prev_values =
  		p_new(ctx->cur_pool, const char *, ctx->headers_count);
  	for (i = 0; i < ctx->headers_count; i++)
  		line->prev_values[i] = p_strdup(ctx->cur_pool, values[i]);
  }
  
  static void stats_read(struct top_context *ctx)
  {
  	struct top_line *old_line, *line;
  	unsigned int i;
  	char **args;
  
  	/* read lines */
  	while ((args = p_read_next_line(ctx->cur_pool, ctx->input)) != NULL) {
  		if (args[0] == NULL) {
  			/* end of stats */
  			return;
  		}
  		if (str_array_length((void *)args) != ctx->headers_count)
  			i_fatal("read(%s): invalid stats line", ctx->path);
  
  		line = p_new(ctx->cur_pool, struct top_line, 1);
  		line->id = args[0];
  		line->flip = ctx->flip;
  		line->cur_values = p_new(ctx->cur_pool, const char *, ctx->headers_count);
  		for (i = 0; i < ctx->headers_count; i++)
  			line->cur_values[i] = args[i];
  
  		old_line = hash_table_lookup(ctx->sessions, line->id);
  		if (old_line != NULL) {
  			stats_line_set_prev_values(ctx, old_line, line);
  			array_append(&ctx->lines, &line, 1);
  		}
  		hash_table_insert(ctx->sessions, line->id, line);
  	}
  
  	if (ctx->input->stream_errno != 0) {
  		i_fatal("read(%s) failed: %s", ctx->path,
  			i_stream_get_error(ctx->input));
  	}
  	i_fatal("read(%s): unexpected EOF", ctx->path);
  }
  
  static void stats_drop_stale(struct top_context *ctx)
  {
  	struct hash_iterate_context *iter;
  	char *id;
  	struct top_line *line;
  
  	iter = hash_table_iterate_init(ctx->sessions);
  	while (hash_table_iterate(iter, ctx->sessions, &id, &line)) {
  		if (line->flip != ctx->flip)
  			hash_table_remove(ctx->sessions, id);
  	}
  	hash_table_iterate_deinit(&iter);
  }
  
  static int get_double(const char *str, double *num_r)
  {
  	char *p;
  
  	*num_r = strtod(str, &p);
  	return *p == '\0' ? 0 : -1;
  }
  
  static double sort_cpu_diff(const struct top_line *line)
  {
  	double prev, cur, diff, prev_time, cur_time, time_multiplier;
  
  	if (get_double(line->prev_values[sort_ctx->last_update_idx], &prev_time) < 0 ||
  	    get_double(line->cur_values[sort_ctx->last_update_idx], &cur_time) < 0)
  		i_fatal("sorting: invalid last_update value");
  	time_multiplier = (cur_time - prev_time) * 100;
  
  	if (get_double(line->prev_values[sort_ctx->sort_idx1], &prev) < 0 ||
  	    get_double(line->cur_values[sort_ctx->sort_idx1], &cur) < 0)
  		i_fatal("sorting: not a double");
  
  	diff = cur - prev;
  
  	if (sort_ctx->sort_idx2 != 0) {
  		if (get_double(line->prev_values[sort_ctx->sort_idx2], &prev) < 0 ||
  		    get_double(line->cur_values[sort_ctx->sort_idx2], &cur) < 0)
  			i_fatal("sorting: not a double");
  		diff += cur - prev;
  	}
  	return diff * time_multiplier;
  }
  
  static int sort_cpu(struct top_line *const *l1, struct top_line *const *l2)
  {
  	double d1, d2;
  
  	d1 = sort_cpu_diff(*l1);
  	d2 = sort_cpu_diff(*l2);
  	if (d1 < d2)
  		return -1;
  	if (d1 > d2)
  		return 1;
  	return strcmp((*l1)->cur_values[sort_ctx->user_idx],
  		      (*l2)->cur_values[sort_ctx->user_idx]);
  }
  
  static double sort_num_diff(const struct top_line *line)
  {
  	uint64_t prev, cur, diff;
  
  	if (str_to_uint64(line->prev_values[sort_ctx->sort_idx1], &prev) < 0 ||
  	    str_to_uint64(line->cur_values[sort_ctx->sort_idx1], &cur) < 0)
  		i_fatal("sorting: not a number");
  	diff = cur - prev;
  
  	if (sort_ctx->sort_idx2 != 0) {
  		if (str_to_uint64(line->prev_values[sort_ctx->sort_idx2], &prev) < 0 ||
  		    str_to_uint64(line->cur_values[sort_ctx->sort_idx2], &cur) < 0)
  			i_fatal("sorting: not a number");
  		diff += cur - prev;
  	}
  	return diff;
  }
  
  static int sort_num(struct top_line *const *l1, struct top_line *const *l2)
  {
  	uint64_t n1, n2;
  
  	n1 = sort_num_diff(*l1);
  	n2 = sort_num_diff(*l2);
  	if (n1 < n2)
  		return -1;
  	if (n1 > n2)
  		return 1;
  	return strcmp((*l1)->cur_values[sort_ctx->user_idx],
  		      (*l2)->cur_values[sort_ctx->user_idx]);
  }
  
  static bool
  stats_header_find(struct top_context *ctx, const char *name,
  		  unsigned int *idx_r)
  {
  	unsigned int i;
  
  	for (i = 0; ctx->headers[i] != NULL; i++) {
  		if (strcmp(ctx->headers[i], name) == 0) {
  			*idx_r = i;
  			return TRUE;
  		}
  	}
  	return FALSE;
  }
  
  static void stats_top_get_sorting(struct top_context *ctx)
  {
  	if (stats_header_find(ctx, ctx->sort_type, &ctx->sort_idx1))
  		return;
  
  	if (strcmp(ctx->sort_type, "cpu") == 0) {
  		if (!stats_header_find(ctx, "user_cpu", &ctx->sort_idx1) ||
  		    !stats_header_find(ctx, "sys_cpu", &ctx->sort_idx2))
  			i_fatal("cpu sort type is missing fields");
  		return;
  	}
  	if (strcmp(ctx->sort_type, "disk") == 0) {
  		if (!stats_header_find(ctx, disk_input_field, &ctx->sort_idx1) ||
  		    !stats_header_find(ctx, disk_output_field, &ctx->sort_idx2))
  			i_fatal("disk sort type is missing fields");
  		return;
  	}
  	i_fatal("unknown sort type: %s", ctx->sort_type);
  }
  
  static bool stats_top_round(struct top_context *ctx)
  {
  #define TOP_CMD "EXPORT\tsession\tconnected\n"
  	const char *const *args;
  	pool_t tmp_pool;
  
  	if (write_full(ctx->fd, TOP_CMD, strlen(TOP_CMD)) < 0)
  		i_fatal("write(%s) failed: %m", ctx->path);
  
  	/* read header */
  	if (ctx->headers != NULL) {
  		args = read_next_line(ctx->input);
  		if (args == NULL)
  			i_fatal("read(%s) unexpectedly disconnected", ctx->path);
  		if (*args == NULL)
  			return TRUE;
  		if (str_array_length(args) != ctx->headers_count)
  			i_fatal("headers changed");
  	} else {
  		ctx->headers = p_read_next_line(default_pool, ctx->input);
  		if (ctx->headers == NULL)
  			i_fatal("read(%s) unexpectedly disconnected", ctx->path);
  		if (*ctx->headers == NULL) {
  			i_free_and_null(ctx->headers);
  			return FALSE;
  		}
  		ctx->headers_count = str_array_length((void *)ctx->headers);
  		if (!stats_header_find(ctx, "last_update", &ctx->last_update_idx))
  			i_fatal("last_update header missing");
  		if (!stats_header_find(ctx, "user", &ctx->user_idx))
  			i_fatal("user header missing");
  		stats_top_get_sorting(ctx);
  	}
  
  	array_clear(&ctx->lines);
  	p_clear(ctx->prev_pool);
  	tmp_pool = ctx->prev_pool;
  	ctx->prev_pool = ctx->cur_pool;
  	ctx->cur_pool = tmp_pool;
  
  	ctx->flip = !ctx->flip;
  	stats_read(ctx);
  	stats_drop_stale(ctx);
  
  	sort_ctx = ctx;
  	array_sort(&ctx->lines, *ctx->lines_sort);
  	sort_ctx = NULL;
  	return TRUE;
  }
  
  static void
  stats_top_output_diff(struct top_context *ctx,
  		      const struct top_line *line, unsigned int i)
  {
  	uint64_t prev_num, cur_num;
  	double prev_double, cur_double, prev_time, cur_time;
  	char numstr[MAX_INT_STRLEN];
  
  	if (str_to_uint64(line->prev_values[i], &prev_num) == 0 &&
  	    str_to_uint64(line->cur_values[i], &cur_num) == 0) {
  		if (i_snprintf(numstr, sizeof(numstr), "%llu",
  			       (unsigned long long)(cur_num - prev_num)) < 0)
  			i_unreached();
  		doveadm_print(numstr);
  	} else if (get_double(line->prev_values[i], &prev_double) == 0 &&
  		   get_double(line->cur_values[i], &cur_double) == 0 &&
  		   get_double(line->prev_values[ctx->last_update_idx], &prev_time) == 0 &&
  		   get_double(line->cur_values[ctx->last_update_idx], &cur_time) == 0) {
  		/* %CPU */
  		if (i_snprintf(numstr, sizeof(numstr), "%d",
  			       (int)((cur_double - prev_double) *
  				     (cur_time - prev_time) * 100)) < 0)
  			i_unreached();
  		doveadm_print(numstr);
  	} else {
  		doveadm_print(line->cur_values[i]);
  	}
  }
  
  static void stats_top_output(struct top_context *ctx)
  {
  	static const char *names[] = {
  		"user", "service", "user_cpu", "sys_cpu",
  		"", ""
  	};
  	struct winsize ws;
  	struct top_line *const *lines;
  	unsigned int i, j, row, maxrow, count, indexes[N_ELEMENTS(names)];
  
  	names[4] = disk_input_field;
  	names[5] = disk_output_field;
  
  	/* ANSI clear screen and move cursor to top of screen */
  	printf("\x1b[2J\x1b[1;1H"); fflush(stdout);
  	doveadm_print_deinit();
  	doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
  
  	doveadm_print_header("USER", "USER", DOVEADM_PRINT_HEADER_FLAG_EXPAND);
  	doveadm_print_header_simple("SERVICE");
  	doveadm_print_header_simple("%CPU");
  	doveadm_print_header_simple("%SYS");
  	doveadm_print_header_simple("DISKIN");
  	doveadm_print_header_simple("DISKOUT");
  
  	if (!stats_top_round(ctx)) {
  		/* no connections yet */
  		return;
  	}
  
  	for (i = 0; i < N_ELEMENTS(names); i++) {
  		if (!stats_header_find(ctx, names[i], &indexes[i]))
  			indexes[i] = UINT_MAX;
  	}
  
  	if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0)
  		ws.ws_row = 24;
  	maxrow = ws.ws_row-1;
  
  	lines = array_get(&ctx->lines, &count);
  	for (i = 0, row = 1; row < maxrow && i < count; i++, row++) {
  		for (j = 0; j < N_ELEMENTS(names); j++) {
  			if (indexes[j] == UINT_MAX)
  				doveadm_print("?");
  			else
  				stats_top_output_diff(ctx, lines[i], indexes[j]);
  		}
  	}
  }
  
  static void stats_top_start(struct top_context *ctx)
  {
  	struct timeout *to;
  
  	stats_top_output(ctx);
  	to = timeout_add(1000, stats_top_output, ctx);
  	io_loop_run(current_ioloop);
  	timeout_remove(&to);
  }
  
  static void stats_top(const char *path, const char *sort_type)
  {
  	struct top_context ctx;
  
  	i_zero(&ctx);
  	ctx.path = path;
  	ctx.fd = doveadm_connect(path);
  	ctx.prev_pool = pool_alloconly_create("stats top", 1024*16);
  	ctx.cur_pool = pool_alloconly_create("stats top", 1024*16);
  	i_array_init(&ctx.lines, 128);
  	hash_table_create(&ctx.sessions, default_pool, 0, str_hash, strcmp);
  	net_set_nonblock(ctx.fd, FALSE);
  
  	ctx.input = i_stream_create_fd(ctx.fd, (size_t)-1, FALSE);
  
  	if (strstr(sort_type, "cpu") != NULL)
  		ctx.lines_sort = sort_cpu;
  	else
  		ctx.lines_sort = sort_num;
  	ctx.sort_type = sort_type;
  
  	stats_top_start(&ctx);
  	i_stream_destroy(&ctx.input);
  	hash_table_destroy(&ctx.sessions);
  	array_free(&ctx.lines);
  	pool_unref(&ctx.prev_pool);
  	pool_unref(&ctx.cur_pool);
  	i_close_fd(&ctx.fd);
  }
  
  static void stats_reset(const char *path, const char **items ATTR_UNUSED)
  {
  	const char **ptr ATTR_UNUSED;
  	int fd,ret;
  	string_t *cmd;
  	struct istream *input;
  	const char *line;
  
  	fd = doveadm_connect(path);
  	net_set_nonblock(fd, FALSE);
  	input = i_stream_create_fd(fd, (size_t)-1, FALSE);
  
  	cmd = t_str_new(10);
  	str_append(cmd, "RESET");
  /* XXX: Not supported yet.
  	for(ptr = items; *ptr; ptr++)
  	{
  		str_append_c(cmd, '\t');
  		str_append(cmd, *ptr);
  	}
  */
  	str_append_c(cmd, '\n');
  
  	/* send command */
  	ret = write_full(fd, str_c(cmd), str_len(cmd));
  
  	if (ret < 0) {
  		i_close_fd(&fd);
  		i_error("write(%s) failed: %m", path);
  		return;
  	}
  
  	line = i_stream_read_next_line(input);
  
  	if (line == NULL) {
  		i_error("read(%s) failed: %s", path, i_stream_get_error(input));
  	} else if (strncmp(line, "OK", 2) != 0) {
  		i_error("%s",line);
  	} else {
  		i_info("Stats reset");
  	}
  
  	i_stream_destroy(&input);
  	i_close_fd(&fd);
  }
  
  static void cmd_stats_top(int argc, char *argv[])
  {
  	const char *path, *sort_type;
  	int c;
  
  	path = t_strconcat(doveadm_settings->base_dir, "/stats", NULL);
  
  	while ((c = getopt(argc, argv, "bs:")) > 0) {
  		switch (c) {
  		case 'b':
  			disk_input_field = "read_bytes";
  			disk_output_field = "write_bytes";
  			break;
  		case 's':
  			path = optarg;
  			break;
  		default:
  			help_ver2(&doveadm_cmd_stats_top_ver2);
  		}
  	}
  	argv += optind - 1;
  	if (argv[1] == NULL)
  		sort_type = "disk";
  	else if (argv[2] != NULL)
  		help_ver2(&doveadm_cmd_stats_top_ver2);
  	else
  		sort_type = argv[1];
  
  	doveadm_print_init(DOVEADM_PRINT_TYPE_TABLE);
  	stats_top(path, sort_type);
  }
  
  static void cmd_stats_reset(int argc, char *argv[])
  {
  	const char *path;
  	int c;
  
  	path = t_strconcat(doveadm_settings->base_dir, "/stats", NULL);
  	while((c = getopt(argc, argv, "s:")) > 0) {
  		switch (c) {
  		case 's':
  			path = optarg;
  			break;
  		default:
  			help_ver2(&doveadm_cmd_stats_reset_ver2);
  		}
  	}
  	argv += optind - 1;
  	/* items is now argv */
  /*	if (optind >= argc) {
  		i_fatal("missing item(s) to reset");
  	}
  */
  	stats_reset(path, (const char**)argv);
  }
  
  struct doveadm_cmd_ver2 doveadm_cmd_stats_dump_ver2 = {
  	.cmd = doveadm_cmd_stats_dump,
  	.name = "stats dump",
  	.usage = "[-s <stats socket path>] <type> [<filter>]",
  DOVEADM_CMD_PARAMS_START
  DOVEADM_CMD_PARAM('s', "socket-path", CMD_PARAM_STR, 0)
  DOVEADM_CMD_PARAM('\0', "type", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
  DOVEADM_CMD_PARAM('\0', "filter", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
  DOVEADM_CMD_PARAMS_END
  };
  
  struct doveadm_cmd_ver2 doveadm_cmd_stats_top_ver2 = {
  	.old_cmd = cmd_stats_top,
  	.name = "stats top",
  	.usage = "[-s <stats socket path>] [-b] [<sort field>]",
  DOVEADM_CMD_PARAMS_START
  DOVEADM_CMD_PARAM('s', "socket-path", CMD_PARAM_STR, 0)
  DOVEADM_CMD_PARAM('b', "show-disk-io", CMD_PARAM_BOOL, 0)
  DOVEADM_CMD_PARAM('\0', "field", CMD_PARAM_STR, CMD_PARAM_FLAG_POSITIONAL)
  DOVEADM_CMD_PARAMS_END
  };
  
  
  struct doveadm_cmd_ver2 doveadm_cmd_stats_reset_ver2 = {
  	.old_cmd = cmd_stats_reset,
  	.name = "stats reset",
  	.usage = "[-s <stats socket path>]",
  DOVEADM_CMD_PARAMS_START
  DOVEADM_CMD_PARAM('s', "socket-path", CMD_PARAM_STR, 0)
  DOVEADM_CMD_PARAMS_END
  };
  