/*
 * tapestat: report tape statistics
 * (C) 2015 Hewlett-Packard Development Company, L.P.
 *
 * Initial revision by Shane M. SEYMOUR (shane.seymour <at> hpe.com)
 * Modified for sysstat by Sebastien GODARD (sysstat <at> orange.fr)
 *
 ***************************************************************************
 * This program is free software; you can redistribute it and/or modify it *
 * under the terms of the GNU General Public License as published  by  the *
 * Free Software Foundation; either version 2 of the License, or (at  your *
 * option) any later version.                                              *
 *                                                                         *
 * This program is distributed in the hope that it  will  be  useful,  but *
 * WITHOUT ANY WARRANTY; without the implied warranty  of  MERCHANTABILITY *
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License *
 * for more details.                                                       *
 *                                                                         *
 * You should have received a copy of the GNU General Public License along *
 * with this program; if not, write to the Free Software Foundation, Inc., *
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA              *
 ***************************************************************************
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <dirent.h>
#define __DO_NOT_DEFINE_COMPILE
#include <regex.h>
#include <inttypes.h>
#include <stdint.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/utsname.h>

#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#undef HZ /* sys/param.h defines HZ but needed for MAXPATHLEN */
#endif
#ifndef MAXPATHLEN
#define MAXPATHLEN 256
#endif

#include "version.h"
#include "tapestat.h"
#include "rd_stats.h"
#include "count.h"

#ifdef USE_NLS
#include <locale.h>
#include <libintl.h>
#define _(string) gettext(string)
#else
#define _(string) (string)
#endif

#ifdef USE_SCCSID
#define SCCSID "@(#)sysstat-" VERSION ": " __FILE__ " compiled " __DATE__ " " __TIME__
char *sccsid(void) { return (SCCSID); }
#endif

#ifdef TEST
void int_handler(int n) { return; }
#endif

int cpu_nr = 0;		/* Nb of processors on the machine */
int flags = 0;		/* Flag for common options and system state */

long interval = 0;
char timestamp[TIMESTAMP_LEN];

struct sigaction alrm_act;

/* Number of decimal places */
int dplaces_nr = -1;

/*
 * For tape stats - it would be extremely rare for there to be a very large
 * number of tape drives attached to a system. I wouldn't expect to see more
 * than 20-30 in a very large configuration and discontinguous ones should
 * be even more rare. Because of this we keep the old and new data in a
 * simple data structure with the tape index being the number after the tape
 * drive, st0 at index 0, etc.
 */
int max_tape_drives = 0;
struct tape_stats *tape_new_stats = { NULL };
struct tape_stats *tape_old_stats = { NULL };
regex_t tape_reg;

/*
 ***************************************************************************
 * Print usage and exit.
 *
 * IN:
 * @progname	Name of sysstat command.
 ***************************************************************************
 */
void usage(char *progname)
{
	fprintf(stderr, _("Usage: %s [ options ] [ <interval> [ <count> ] ]\n"),
		progname);
	fprintf(stderr, _("Options are:\n"
			  "[ --human ] [ -k | -m ] [ -t ] [ -V ] [ -y ] [ -z ]\n"));
	exit(1);
}

/*
 ***************************************************************************
 * SIGALRM signal handler. No need to reset the handler here.
 *
 * IN:
 * @sig	Signal number.
 ***************************************************************************
 */
void alarm_handler(int sig)
{
	alarm(interval);
}

/*
 ***************************************************************************
 * Initialization.
 ***************************************************************************
 */
void tape_initialise(void)
{
	/* How many processors on this machine? */
	cpu_nr = get_cpu_nr(~0, FALSE);

	/* Compile regular expression for tape names */
        if (regcomp(&tape_reg, "^st[0-9]+$", REG_EXTENDED) != 0) {
		exit(1);
        }
}

/*
 ***************************************************************************
 * Free structures.
 ***************************************************************************
 */
void tape_uninitialise(void)
{
	regfree(&tape_reg);
	if (tape_old_stats != NULL) {
		free(tape_old_stats);
	}
	if (tape_new_stats != NULL) {
		free(tape_new_stats);
	}
}

/*
 ***************************************************************************
 * Get maximum number of tapes in the system.
 *
 * RETURNS:
 * Number of tapes found.
 ***************************************************************************
 */
int get_max_tape_drives(void)
{
	DIR *dir;
	struct dirent *entry;
	int new_max_tape_drives, tmp, num_stats_dir = 0;
	regmatch_t match;
	char stats_dir[MAXPATHLEN + 1];
	struct stat stat_buf;

	new_max_tape_drives = max_tape_drives;

	/* Open sysfs tree */
	dir = opendir(SYSFS_CLASS_TAPE_DIR);
	if (dir == NULL)
		return 0;

	while ((entry = readdir(dir)) != NULL) {
		if (regexec(&tape_reg, &entry->d_name[0], 1, &match, 0) == 0) {
			/* d_name[2] to skip the st at the front */
			tmp = atoi(&entry->d_name[2]) + 1;
			if (tmp > new_max_tape_drives) {
				new_max_tape_drives = tmp;
			}
		}
		snprintf(stats_dir, MAXPATHLEN, "%s/%s/%s",
			SYSFS_CLASS_TAPE_DIR, &entry->d_name[0], "stats");
		if (stat(stats_dir, &stat_buf) == 0) {
			if (S_ISDIR(stat_buf.st_mode)) {
				num_stats_dir++;
			}
		}
	}
	closedir(dir);

	/* If there are no stats directories make the new number of tape drives 0 */
	if (num_stats_dir == 0) {
		new_max_tape_drives = 0;
	}

	return new_max_tape_drives;
}

/*
 ***************************************************************************
 * Check if new tapes have been added and reallocate structures accordingly.
 ***************************************************************************
 */
void tape_check_tapes_and_realloc(void)
{
	int new_max_tape_drives, i;

	/* Count again number of tapes */
	new_max_tape_drives = get_max_tape_drives();

	if (new_max_tape_drives > max_tape_drives && new_max_tape_drives > 0) {
		/* New tapes found: Realloc structures */
		struct tape_stats *tape_old_stats_t = (struct tape_stats *)
			realloc(tape_old_stats,	sizeof(struct tape_stats) * new_max_tape_drives);
		struct tape_stats *tape_new_stats_t = (struct tape_stats *)
			realloc(tape_new_stats,	sizeof(struct tape_stats) * new_max_tape_drives);
		if ((tape_old_stats_t == NULL) || (tape_new_stats_t == NULL)) {
			if (tape_old_stats_t != NULL) {
				free(tape_old_stats_t);
				tape_old_stats_t = NULL;
			} else {
				free(tape_old_stats);
				tape_old_stats = NULL;
			}
			if (tape_new_stats_t != NULL) {
				free(tape_new_stats_t);
				tape_new_stats_t = NULL;
			} else {
				free(tape_new_stats);
				tape_new_stats = NULL;
			}

			perror("realloc");
			exit(4);
		}

		tape_old_stats = tape_old_stats_t;
		tape_new_stats = tape_new_stats_t;

		for (i = max_tape_drives; i < new_max_tape_drives; i++) {
			tape_old_stats[i].valid = TAPE_STATS_INVALID;
			tape_new_stats[i].valid = TAPE_STATS_INVALID;
		}
		max_tape_drives = new_max_tape_drives;
	}
}

/*
 ***************************************************************************
 * Collect initial statistics for all existing tapes in the system.
 * This function should be called only once.
 ***************************************************************************
 */
void tape_gather_initial_stats(void)
{
	int new_max_tape_drives, i;
	FILE *fp;
	char filename[MAXPATHLEN + 1];

	/* Get number of tapes in the system */
	new_max_tape_drives = get_max_tape_drives();

	if (new_max_tape_drives == 0) {
		/* No tapes found */
		fprintf(stderr, _("No tape drives with statistics found\n"));
		exit(1);
	}
	else {
		/* Allocate structures */
		if (tape_old_stats == NULL) {
			tape_old_stats = (struct tape_stats *)
				malloc(sizeof(struct tape_stats) * new_max_tape_drives);
			tape_new_stats = (struct tape_stats *)
				malloc(sizeof(struct tape_stats) * new_max_tape_drives);
			for (i = 0; i < new_max_tape_drives; i++) {
				tape_old_stats[i].valid = TAPE_STATS_INVALID;
				tape_new_stats[i].valid = TAPE_STATS_INVALID;
			}
			max_tape_drives = new_max_tape_drives;
		} else
			/* This should only be called once */
			return;
	}

	/* Read stats for each tape */
	for (i = 0; i < max_tape_drives; i++) {
		/*
		 * Everything starts out valid but failing to open
		 * a file gets the tape drive marked invalid.
		 */
		tape_new_stats[i].valid = TAPE_STATS_VALID;
		tape_old_stats[i].valid = TAPE_STATS_VALID;

		__gettimeofday(&tape_old_stats[i].tv, NULL);

		tape_new_stats[i].tv.tv_sec = tape_old_stats[i].tv.tv_sec;
		tape_new_stats[i].tv.tv_usec = tape_old_stats[i].tv.tv_usec;

		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "read_ns", read_time)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "write_ns", write_time)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "io_ns", other_time)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "read_byte_cnt", read_bytes)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "write_byte_cnt", write_bytes)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "read_cnt", read_count)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "write_cnt", write_count)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "other_cnt", other_count)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "resid_cnt", resid_count)

		tape_old_stats[i].read_time = 0;
		tape_old_stats[i].write_time = 0;
		tape_old_stats[i].other_time = 0;
		tape_old_stats[i].read_bytes = 0;
		tape_old_stats[i].write_bytes = 0;
		tape_old_stats[i].read_count = 0;
		tape_old_stats[i].write_count = 0;
		tape_old_stats[i].other_count = 0;
		tape_old_stats[i].resid_count = 0;
	}
}

/*
 ***************************************************************************
 * Collect a new sample of statistics for all existing tapes in the system.
 ***************************************************************************
 */
void tape_get_updated_stats(void)
{
	int i;
	FILE *fp;
	char filename[MAXPATHLEN + 1] = { 0 };

	/* Check tapes and realloc structures if  needed */
	tape_check_tapes_and_realloc();

	for (i = 0; i < max_tape_drives; i++) {
		/*
		 * Everything starts out valid but failing
		 * to open a file gets the tape drive marked invalid.
		 */
		tape_new_stats[i].valid = TAPE_STATS_VALID;
		__gettimeofday(&tape_new_stats[i].tv, NULL);

		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "read_ns", read_time)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "write_ns", write_time)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "io_ns", other_time)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "read_byte_cnt", read_bytes)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "write_byte_cnt", write_bytes)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "read_cnt", read_count)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "write_cnt", write_count)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "other_cnt", other_count)
		TAPE_STAT_FILE_VAL(TAPE_STAT_PATH "resid_cnt", resid_count)

		if ((tape_new_stats[i].read_time < tape_old_stats[i].read_time) ||
		    (tape_new_stats[i].write_time < tape_old_stats[i].write_time) ||
		    (tape_new_stats[i].other_time < tape_old_stats[i].other_time)) {
			tape_new_stats[i].valid = TAPE_STATS_INVALID;
		}
	}
}

/*
 ***************************************************************************
 * Display tapes statistics headings.
 ***************************************************************************
 */
void tape_write_headings(void)
{
	printf("Tape:     r/s     w/s   ");
	if (DISPLAY_MEGABYTES(flags)) {
		printf("MB_read/s   MB_wrtn/s");
	} else {
		printf("kB_read/s   kB_wrtn/s");
	}
	printf("  %%Rd  %%Wr  %%Oa    Rs/s    Ot/s\n");
}

/*
 ***************************************************************************
 * Calculate statistics for current tape.
 *
 * IN:
 * @i		Index in array for current tape.
 *
 * OUT:
 * @stats	Statistics for current tape.
 ***************************************************************************
 */
void tape_calc_one_stats(struct calc_stats *stats, int i)
{
	uint64_t duration;
	double temp;
	FILE *fp;

	/* Duration in ms done in ms to prevent rounding issues with using seconds */
	duration = (tape_new_stats[i].tv.tv_sec -
		tape_old_stats[i].tv.tv_sec) * 1000;
	duration -= tape_old_stats[i].tv.tv_usec / 1000;
	duration += tape_new_stats[i].tv.tv_usec / 1000;

	/* If duration is zero we need to calculate the ms since boot time */
	if (duration == 0) {
		fp = fopen(UPTIME, "r");

		/*
		 * Get uptime from /proc/uptime and if we can't then just set duration to
		 * be 0 - it will mean that we don't calculate stats.
		 */
		if (fp == NULL) {
			duration = 0;
		} else {
			if (fscanf(fp, "%lf", &temp) != 1) {
				temp = 0;
			}
			duration = (uint64_t) (temp * 1000);
			fclose(fp);
		}
	}

	/* The second value passed into the macro is the thing being calculated */
	CALC_STAT_CNT(read_count, reads_per_second)
	CALC_STAT_CNT(write_count, writes_per_second)
	CALC_STAT_CNT(other_count, other_per_second)
	CALC_STAT_KB(read_bytes, kbytes_read_per_second)
	CALC_STAT_KB(write_bytes, kbytes_written_per_second)
	CALC_STAT_PCT(read_time, read_pct_wait)
	CALC_STAT_PCT(write_time, write_pct_wait)
	CALC_STAT_PCT(other_time, all_pct_wait)
	CALC_STAT_CNT(resid_count, resids_per_second)
}

/*
 ***************************************************************************
 * Display statistics for current tape.
 *
 * IN:
 * @tape	Statistics for current tape.
 * @i		Index in array for current tape.
 ***************************************************************************
 */
void tape_write_stats(struct calc_stats *tape, int i)
{
	char buffer[32];
	uint64_t divisor = 1;

	if (DISPLAY_MEGABYTES(flags))
		divisor = 1024;

	sprintf(buffer, "st%i        ", i);
	buffer[5] = 0;
	cprintf_in(IS_STR, "%s", buffer, 0);
	cprintf_u64(NO_UNIT, 2, 7,
		    tape->reads_per_second,
		    tape->writes_per_second);
	cprintf_u64(DISPLAY_UNIT(flags) ? UNIT_KILOBYTE : NO_UNIT, 2, 11,
		    DISPLAY_UNIT(flags) ? tape->kbytes_read_per_second
					: tape->kbytes_read_per_second / divisor,
		    DISPLAY_UNIT(flags) ? tape->kbytes_written_per_second
					: tape->kbytes_written_per_second / divisor);
	cprintf_pc(DISPLAY_UNIT(flags), 3, 4, 0,
		   (double) tape->read_pct_wait,
		   (double) tape->write_pct_wait,
		   (double) tape->all_pct_wait);
	cprintf_u64(NO_UNIT, 2, 7,
		    tape->resids_per_second,
		    tape->other_per_second);
	printf("\n");
}

/*
 ***************************************************************************
 * Print everything now (stats and uptime).
 *
 * IN:
 * @rectime	Current date and time.
 ***************************************************************************
 */
void write_stats(struct tm *rectime)
{
	int i;
	struct calc_stats tape;
	struct tape_stats *tmp;

	/* Test stdout */
	TEST_STDOUT(STDOUT_FILENO);

	/* Print time stamp */
	if (DISPLAY_TIMESTAMP(flags)) {
		if (DISPLAY_ISO(flags)) {
			strftime(timestamp, sizeof(timestamp), "%FT%T%z", rectime);
		}
		else {
			strftime(timestamp, sizeof(timestamp), "%x %X", rectime);
		}
		printf("%s\n", timestamp);
	}

	/* Print the headings */
	tape_write_headings();

	/*
	 * If either new or old is invalid or the I/Os per second is 0 and
	 * zero omit is true then we print nothing.
	 */
	if (max_tape_drives > 0) {

		for (i = 0; i < max_tape_drives; i++) {
			if ((tape_new_stats[i].valid == TAPE_STATS_VALID) &&
				(tape_old_stats[i].valid == TAPE_STATS_VALID)) {
				tape_calc_one_stats(&tape, i);
				if (!(DISPLAY_ZERO_OMIT(flags)
					&& (tape.other_per_second == 0)
					&& (tape.reads_per_second == 0)
					&& (tape.writes_per_second == 0)
					&& (tape.kbytes_read_per_second == 0)
					&& (tape.kbytes_written_per_second == 0)
					&& (tape.read_pct_wait == 0)
					&& (tape.write_pct_wait == 0)
					&& (tape.all_pct_wait == 0)
					&& (tape.resids_per_second == 0))) {
					tape_write_stats(&tape, i);
				}
			}
		}
		/*
		 * Swap new and old so next time we compare against the new old stats.
		 * If a new tape drive appears it won't appear in the output until after
		 * the second time we gather information about it.
		 */
		tmp = tape_old_stats;
		tape_old_stats = tape_new_stats;
		tape_new_stats = tmp;
	}
	printf("\n");
}

/*
 ***************************************************************************
 * Main loop: Read tape stats from the relevant sources and display them.
 *
 * IN:
 * @count	Number of lines of stats to print.
 * @rectime	Current date and time.
 ***************************************************************************
 */
void rw_tape_stat_loop(long int count, struct tm *rectime)
{
	struct tape_stats *tmp;
	int skip = 0;

	/* Should we skip first report? */
	if (DISPLAY_OMIT_SINCE_BOOT(flags) && interval > 0) {
		skip = 1;
	}

	do {

		if (tape_new_stats == NULL) {
			tape_gather_initial_stats();
		} else {
			tape_get_updated_stats();
		}

		/* Get time */
		get_localtime(rectime, 0);

		/* Check whether we should skip first report */
		if (!skip) {
			/* Print results */
			write_stats(rectime);

			if (count > 0) {
				count--;
			}
		}
		else {
			skip = 0;
			tmp = tape_old_stats;
			tape_old_stats = tape_new_stats;
			tape_new_stats = tmp;
		}

		if (count) {
			__pause();
		}
	}
	while (count);
}

/*
 ***************************************************************************
 * Main entry to the tapestat program.
 ***************************************************************************
 */
int main(int argc, char **argv)
{
	int it = 0;
	int opt = 1;
	int i;
	long count = 1;
	struct utsname header;
	struct tm rectime;

#ifdef USE_NLS
	/* Init National Language Support */
	init_nls();
#endif

	/* Init color strings */
	init_colors();

	/* Process args... */
	while (opt < argc) {

		if (!strcmp(argv[opt], "--human")) {
			flags |= T_D_UNIT;
			opt++;
		}

		else if (!strncmp(argv[opt], "-", 1)) {
			for (i = 1; *(argv[opt] + i); i++) {

				switch (*(argv[opt] + i)) {

				case 'k':
					if (DISPLAY_MEGABYTES(flags)) {
						usage(argv[0]);
					}
					/* Display stats in kB/s */
					flags |= T_D_KILOBYTES;
					break;

				case 'm':
					if (DISPLAY_KILOBYTES(flags)) {
						usage(argv[0]);
					}
					/* Display stats in MB/s */
					flags |= T_D_MEGABYTES;
					break;

				case 't':
					/* Display timestamp */
					flags |= T_D_TIMESTAMP;
					break;

				case 'y':
					/* Don't display stats since system restart */
					flags |= T_D_OMIT_SINCE_BOOT;
					break;

				case 'z':
					/* Omit output for devices with no activity */
					flags |= T_D_ZERO_OMIT;
					break;

				case 'V':
					/* Print version number and exit */
					print_version();
					break;

				default:
					usage(argv[0]);
				}
			}
			opt++;
		}

		else if (!it) {
			interval = atol(argv[opt++]);
			if (interval < 0) {
				usage(argv[0]);
			}
			count = -1;
			it = 1;
		}

		else if (it > 0) {
			count = atol(argv[opt++]);
			if ((count < 1) || !interval) {
				usage(argv[0]);
			}
			it = -1;
		}
		else {
			usage(argv[0]);
		}
	}

	if (!interval) {
		count = 1;
	}

	tape_initialise();

	get_localtime(&rectime, 0);

	/*
	 * Don't buffer data if redirected to a pipe.
	 * Note: With musl-c, the behavior of this function is undefined except
	 * when it is the first operation on the stream.
	 */
	setbuf(stdout, NULL);

	/* Get system name, release number and hostname */
	__uname(&header);
	if (print_gal_header(&rectime, header.sysname, header.release,
			     header.nodename, header.machine, cpu_nr,
			     PLAIN_OUTPUT)) {
		flags |= T_D_ISO;
	}
	printf("\n");

	/* Set a handler for SIGALRM */
	memset(&alrm_act, 0, sizeof(alrm_act));
	alrm_act.sa_handler = alarm_handler;
	sigaction(SIGALRM, &alrm_act, NULL);
	alarm(interval);

	/* Main loop */
	rw_tape_stat_loop(count, &rectime);

	/* Free structures */
	tape_uninitialise();

	return 0;
}
