/*
 * watch.c - login/logout watching
 *
 * This file is part of zsh, the Z shell.
 *
 * Copyright (c) 1992-1997 Paul Falstad
 * All rights reserved.
 *
 * Permission is hereby granted, without written agreement and without
 * license or royalty fees, to use, copy, modify, and distribute this
 * software and to distribute modified versions of this software for any
 * purpose, provided that the above copyright notice and the following
 * two paragraphs appear in all copies of this software.
 *
 * In no event shall Paul Falstad or the Zsh Development Group be liable
 * to any party for direct, indirect, special, incidental, or consequential
 * damages arising out of the use of this software and its documentation,
 * even if Paul Falstad and the Zsh Development Group have been advised of
 * the possibility of such damage.
 *
 * Paul Falstad and the Zsh Development Group specifically disclaim any
 * warranties, including, but not limited to, the implied warranties of
 * merchantability and fitness for a particular purpose.  The software
 * provided hereunder is on an "as is" basis, and Paul Falstad and the
 * Zsh Development Group have no obligation to provide maintenance,
 * support, updates, enhancements, or modifications.
 *
 */

#include "watch.mdh"

/* Headers for utmp/utmpx structures */
#ifdef HAVE_UTMP_H
# include <utmp.h>
#endif
#ifdef HAVE_UTMPX_H
# include <utmpx.h>
#endif

/* Find utmp file */
#if !defined(REAL_UTMP_FILE) && defined(UTMP_FILE)
# define REAL_UTMP_FILE UTMP_FILE
#endif
#if !defined(REAL_UTMP_FILE) && defined(_PATH_UTMP)
# define REAL_UTMP_FILE _PATH_UTMP
#endif
#if !defined(REAL_UTMP_FILE) && defined(PATH_UTMP_FILE)
# define REAL_UTMP_FILE PATH_UTMP_FILE
#endif

/* Find wtmp file */
#if !defined(REAL_WTMP_FILE) && defined(WTMP_FILE)
# define REAL_WTMP_FILE WTMP_FILE
#endif
#if !defined(REAL_WTMP_FILE) && defined(_PATH_WTMP)
# define REAL_WTMP_FILE _PATH_WTMP
#endif
#if !defined(REAL_WTMP_FILE) && defined(PATH_WTMP_FILE)
# define REAL_WTMP_FILE PATH_WTMP_FILE
#endif

/* Find utmpx file */
#if !defined(REAL_UTMPX_FILE) && defined(UTMPX_FILE)
# define REAL_UTMPX_FILE UTMPX_FILE
#endif
#if !defined(REAL_UTMPX_FILE) && defined(_PATH_UTMPX)
# define REAL_UTMPX_FILE _PATH_UTMPX
#endif
#if !defined(REAL_UTMPX_FILE) && defined(PATH_UTMPX_FILE)
# define REAL_UTMPX_FILE PATH_UTMPX_FILE
#endif

/* Find wtmpx file */
#if !defined(REAL_WTMPX_FILE) && defined(WTMPX_FILE)
# define REAL_WTMPX_FILE WTMPX_FILE
#endif
#if !defined(REAL_WTMPX_FILE) && defined(_PATH_WTMPX)
# define REAL_WTMPX_FILE _PATH_WTMPX
#endif
#if !defined(REAL_WTMPX_FILE) && defined(PATH_WTMPX_FILE)
# define REAL_WTMPX_FILE PATH_WTMPX_FILE
#endif

/* Decide which structure to use.  We use a structure that exists in *
 * the headers, and require that its corresponding utmp file exist.  *
 * (wtmp is less important.)                                         */

#if !defined(WATCH_STRUCT_UTMP) && defined(HAVE_STRUCT_UTMPX) && defined(REAL_UTMPX_FILE)
# define WATCH_STRUCT_UTMP struct utmpx
# if defined(HAVE_SETUTXENT) && defined(HAVE_GETUTXENT) && defined(HAVE_ENDUTXENT)
#  define setutent setutxent
#  define getutent getutxent
#  define endutent endutxent
#  ifndef HAVE_GETUTENT
#   define HAVE_GETUTENT 1
#  endif
# endif

/*
 * In utmpx, the ut_name field is replaced by ut_user.
 * However, on some systems ut_name may already be defined this
 * way for the purposes of utmp.
 */
# ifndef ut_name
#  define ut_name ut_user
# endif
# ifdef HAVE_STRUCT_UTMPX_UT_XTIME
#  undef ut_time
#  define ut_time ut_xtime
# else /* !HAVE_STRUCT_UTMPX_UT_XTIME */
#  ifdef HAVE_STRUCT_UTMPX_UT_TV
#   undef ut_time
#   define ut_time ut_tv.tv_sec
#  endif /* HAVE_STRUCT_UTMPX_UT_TV */
# endif /* !HAVE_STRUCT_UTMPX_UT_XTIME */
# define WATCH_UTMP_FILE REAL_UTMPX_FILE
# ifdef REAL_WTMPX_FILE
#  define WATCH_WTMP_FILE REAL_WTMPX_FILE
# endif
# ifdef HAVE_STRUCT_UTMPX_UT_HOST
#  define WATCH_UTMP_UT_HOST 1
# endif
#endif

#if !defined(WATCH_STRUCT_UTMP) && defined(HAVE_STRUCT_UTMP) && defined(REAL_UTMP_FILE)
# define WATCH_STRUCT_UTMP struct utmp
# define WATCH_UTMP_FILE REAL_UTMP_FILE
# ifdef REAL_WTMP_FILE
#  define WATCH_WTMP_FILE REAL_WTMP_FILE
# endif
# ifdef HAVE_STRUCT_UTMP_UT_HOST
#  define WATCH_UTMP_UT_HOST 1
# endif
#endif

#ifdef WATCH_UTMP_UT_HOST
# define DEFAULT_WATCHFMT "%n has %a %l from %m."
#else /* !WATCH_UTMP_UT_HOST */
# define DEFAULT_WATCHFMT "%n has %a %l."
#endif /* !WATCH_UTMP_UT_HOST */

#ifdef WATCH_STRUCT_UTMP

# include "watch.pro"

# ifndef WATCH_WTMP_FILE
#  define WATCH_WTMP_FILE "/dev/null"
# endif

static int wtabsz = 0;
static WATCH_STRUCT_UTMP *wtab = NULL;

/* the last time we checked the people in the WATCH variable */
static time_t lastwatch;

static time_t lastutmpcheck = 0;

/* get the time of login/logout for WATCH */

static time_t
getlogtime(WATCH_STRUCT_UTMP *u, int inout)
{
    FILE *in;
    WATCH_STRUCT_UTMP uu;
    int first = 1;
    int srchlimit = 50;		/* max number of wtmp records to search */

    if (inout)
	return u->ut_time;
    if (!(in = fopen(WATCH_WTMP_FILE, "r")))
	return time(NULL);
    fseek(in, 0, SEEK_END);
    do {
	if (fseek(in, ((first) ? -1 : -2) * sizeof(WATCH_STRUCT_UTMP), SEEK_CUR)) {
	    fclose(in);
	    return time(NULL);
	}
	first = 0;
	if (!fread(&uu, sizeof(WATCH_STRUCT_UTMP), 1, in)) {
	    fclose(in);
	    return time(NULL);
	}
	if (uu.ut_time < lastwatch || !srchlimit--) {
	    fclose(in);
	    return time(NULL);
	}
    }
    while (memcmp(&uu, u, sizeof(uu)));

    do
	if (!fread(&uu, sizeof(WATCH_STRUCT_UTMP), 1, in)) {
	    fclose(in);
	    return time(NULL);
	}
    while (strncmp(uu.ut_line, u->ut_line, sizeof(u->ut_line)));
    fclose(in);
    return uu.ut_time;
}

/* Mutually recursive call to handle ternaries in $WATCHFMT */

# define BEGIN3 '('
# define END3 ')'

static char *
watch3ary(int inout, WATCH_STRUCT_UTMP *u, char *fmt, int prnt)
{
    int truth = 1, sep;

    switch (*fmt++) {
    case 'n':
	truth = (u->ut_name[0] != 0);
	break;
    case 'a':
	truth = inout;
	break;
    case 'l':
	if (!strncmp(u->ut_line, "tty", 3))
	    truth = (u->ut_line[3] != 0);
	else
	    truth = (u->ut_line[0] != 0);
	break;
# ifdef WATCH_UTMP_UT_HOST
    case 'm':
    case 'M':
	truth = (u->ut_host[0] != 0);
	break;
# endif /* WATCH_UTMP_UT_HOST */
    default:
	prnt = 0;		/* Skip unknown conditionals entirely */
	break;
    }
    sep = *fmt++;
    fmt = watchlog2(inout, u, fmt, (truth && prnt), sep);
    return watchlog2(inout, u, fmt, (!truth && prnt), END3);
}

/* print a login/logout event */

/**/
static char *
watchlog2(int inout, WATCH_STRUCT_UTMP *u, char *fmt, int prnt, int fini)
{
    char buf[40], buf2[80];
    time_t timet;
    struct tm *tm;
    char *fm2;
    int len;
    zattr atr;
# ifdef WATCH_UTMP_UT_HOST
    char *p;
    int i;
# endif /* WATCH_UTMP_UT_HOST */

    while (*fmt)
	if (*fmt == '\\') {
	    if (*++fmt) {
		if (prnt) {
		    applytextattributes(TSC_RAW);
		    putchar(*fmt);
		}
		++fmt;
	    } else if (fini)
		return fmt;
	    else
		break;
	}
	else if (*fmt == fini)
	    return ++fmt;
	else if (*fmt != '%') {
	    if (prnt) {
		applytextattributes(TSC_RAW);
		putchar(*fmt);
	    }
	    ++fmt;
	} else {
	    if (*++fmt == BEGIN3)
		fmt = watch3ary(inout, u, ++fmt, prnt);
	    else if (!prnt)
		++fmt;
	    else
		switch (*(fm2 = fmt++)) {
		case 'n':
		    applytextattributes(TSC_RAW);
		    printf("%.*s", (int)sizeof(u->ut_name), u->ut_name);
		    break;
		case 'a':
		    applytextattributes(TSC_RAW);
		    printf("%s", (!inout) ? "logged off" : "logged on");
		    break;
		case 'l':
		    applytextattributes(TSC_RAW);
		    if (!strncmp(u->ut_line, "tty", 3))
			printf("%.*s", (int)sizeof(u->ut_line) - 3, u->ut_line + 3);
		    else
			printf("%.*s", (int)sizeof(u->ut_line), u->ut_line);
		    break;
# ifdef WATCH_UTMP_UT_HOST
		case 'm':
		    applytextattributes(TSC_RAW);
		    for (p = u->ut_host, i = sizeof(u->ut_host); i && *p; i--, p++) {
			if (*p == '.' && !idigit(p[1]))
			    break;
			putchar(*p);
		    }
		    break;
		case 'M':
		    applytextattributes(TSC_RAW);
		    printf("%.*s", (int)sizeof(u->ut_host), u->ut_host);
		    break;
# endif /* WATCH_UTMP_UT_HOST */
		case 'T':
		case 't':
		case '@':
		case 'W':
		case 'w':
		case 'D':
		    switch (*fm2) {
		    case '@':
		    case 't':
			fm2 = "%l:%M%p";
			break;
		    case 'T':
			fm2 = "%K:%M";
			break;
		    case 'w':
			fm2 = "%a %f";
			break;
		    case 'W':
			fm2 = "%m/%d/%y";
			break;
		    case 'D':
			if (fm2[1] == '{') {
			    char *dd, *ss;
			    int n = 79;

			    for (ss = fm2 + 2, dd = buf2;
				 n-- && *ss && *ss != '}'; ++ss, ++dd)
				*dd = *((*ss == '\\' && ss[1]) ? ++ss : ss);
			    if (*ss == '}') {
				*dd = '\0';
				fmt = ss + 1;
				fm2 = buf2;
			    }
			    else fm2 = "%y-%m-%d";
			}
			else fm2 = "%y-%m-%d";
			break;
		    }
		    timet = getlogtime(u, inout);
		    tm = localtime(&timet);
		    len = ztrftime(buf, 40, fm2, tm, 0L);
		    if (len > 0)
			metafy(buf, len, META_NOALLOC);
		    applytextattributes(TSC_RAW);
		    printf("%s", (*buf == ' ') ? buf + 1 : buf);
		    break;
		case '%':
		    applytextattributes(TSC_RAW);
		    putchar('%');
		    break;
		case 'F':
		    if (*fmt == '{') {
			fmt++;
			atr = match_colour((const char**)&fmt, 1, 0);
			if (*fmt == '}')
			    fmt++;
			if (atr && atr != TXT_ERROR) {
			    tsetattrs(atr);
			    break;
			}
		    } /* fall-through */
		case 'f':
		    tunsetattrs(TXTFGCOLOUR);
		    break;
		case 'K':
		    if (*fmt == '{') {
			fmt++;
			atr = match_colour((const char**)&fmt, 0, 0);
			if (*fmt == '}')
			    fmt++;
			if (atr && atr != TXT_ERROR) {
			    tsetattrs(atr);
			    break;
			}
		    } /* fall-through */
		case 'k':
		    tunsetattrs(TXTBGCOLOUR);
		    break;
		case 'S':
		    tsetattrs(TXTSTANDOUT);
		    break;
		case 's':
		    tunsetattrs(TXTSTANDOUT);
		    break;
		case 'B':
		    tsetattrs(TXTBOLDFACE);
		    break;
		case 'b':
		    tunsetattrs(TXTBOLDFACE);
		    break;
		case 'U':
		    tsetattrs(TXTUNDERLINE);
		    break;
		case 'u':
		    tunsetattrs(TXTUNDERLINE);
		    break;
		default:
		    applytextattributes(TSC_RAW);
		    putchar('%');
		    putchar(*fm2);
		    break;
		}
	}
    if (prnt) {
	applytextattributes(TSC_RAW);
	putchar('\n');
    }

    return fmt;
}

/* See if the watch entry matches */

static int
watchlog_match(char *teststr, char *actual, size_t buflen)
{
    int ret = 0;
    Patprog pprog;
    char *str = dupstring(teststr);
    int len = strnlen(actual, buflen);
    char *user = metafy(actual, len,
	    len == buflen ? META_HEAPDUP : META_USEHEAP);

    tokenize(str);

    if ((pprog = patcompile(str, PAT_STATIC, 0))) {
	queue_signals();
	if (pattry(pprog, user))
	    ret = 1;
	unqueue_signals();
    } else if (!strcmp(user, teststr))
	ret = 1;
    return ret;
}

/* check the List for login/logouts */

static void
watchlog(int inout, WATCH_STRUCT_UTMP *u, char **w, char *fmt)
{
    char *v, *vv, sav;
    int bad;

    if (!*u->ut_name)
	return;

    if (*w && !strcmp(*w, "all")) {
	(void)watchlog2(inout, u, fmt, 1, 0);
	return;
    }
    if (*w && !strcmp(*w, "notme")) {
	int len = strnlen(u->ut_name, sizeof(u->ut_name));
	char *username = metafy(u->ut_name, len,
				(len == sizeof(u->ut_name) ?
				 META_HEAPDUP /* allow for nul terminator */ :
				 META_USEHEAP));
	if (strcmp(username, get_username())) {
	    (void)watchlog2(inout, u, fmt, 1, 0);
	    return;
	}
	w++;
    }
    for (; *w; w++) {
	bad = 0;
	v = *w;
	if (*v != '@' && *v != '%') {
	    for (vv = v; *vv && *vv != '@' && *vv != '%'; vv++);
	    sav = *vv;
	    *vv = '\0';
	    if (!watchlog_match(v, u->ut_name, sizeof(u->ut_name)))
		bad = 1;
	    *vv = sav;
	    v = vv;
	}
	for (;;)
	    if (*v == '%') {
		for (vv = ++v; *vv && *vv != '@'; vv++);
		sav = *vv;
		*vv = '\0';
		if (!watchlog_match(v, u->ut_line, sizeof(u->ut_line)))
		    bad = 1;
		*vv = sav;
		v = vv;
	    }
# ifdef WATCH_UTMP_UT_HOST
	    else if (*v == '@') {
		for (vv = ++v; *vv && *vv != '%'; vv++);
		sav = *vv;
		*vv = '\0';
		if (!watchlog_match(v, u->ut_host, sizeof(u->ut_host)))
		    bad = 1;
		*vv = sav;
		v = vv;
	    }
# endif /* WATCH_UTMP_UT_HOST */
	    else
		break;
	if (!bad) {
	    (void)watchlog2(inout, u, fmt, 1, 0);
	    return;
	}
    }
}

/* compare 2 utmp entries */

static int
ucmp(WATCH_STRUCT_UTMP *u, WATCH_STRUCT_UTMP *v)
{
    if (u->ut_time == v->ut_time)
	return strncmp(u->ut_line, v->ut_line, sizeof(u->ut_line));
    return u->ut_time - v->ut_time;
}

/* initialize the user List */

static int
readwtab(WATCH_STRUCT_UTMP **head, int initial_sz)
{
    WATCH_STRUCT_UTMP *uptr;
    int wtabmax = initial_sz < 2 ? 32 : initial_sz;
    int sz = 0;
# ifdef HAVE_GETUTENT
    WATCH_STRUCT_UTMP *tmp;
# else
    FILE *in;
# endif

    uptr = *head = (WATCH_STRUCT_UTMP *)
	zalloc(wtabmax * sizeof(WATCH_STRUCT_UTMP));
# ifdef HAVE_GETUTENT
    setutent();
    while ((tmp = getutent()) != NULL) {
	memcpy(uptr, tmp, sizeof (WATCH_STRUCT_UTMP));
# else
    if (!(in = fopen(WATCH_UTMP_FILE, "r")))
	return 0;
    while (fread(uptr, sizeof(WATCH_STRUCT_UTMP), 1, in)) {
# endif
# ifdef USER_PROCESS
	if (uptr->ut_type == USER_PROCESS)
# else /* !USER_PROCESS */
	if (uptr->ut_name[0])
# endif /* !USER_PROCESS */
	{
	    uptr++;
	    if (++sz == wtabmax) {
		uptr = (WATCH_STRUCT_UTMP *)
		    realloc(*head, (wtabmax *= 2) * sizeof(WATCH_STRUCT_UTMP));
		if (uptr == NULL) {
		    /* memory pressure - so stop consuming and use, what we have
		     * Other option is to exit() here, as zmalloc does on error */
		    sz--;
		    break;
		}
		*head = uptr;
		uptr += sz;
	    }
	}
    }
# ifdef HAVE_GETUTENT
    endutent();
# else
    fclose(in);
# endif

    if (sz)
	qsort((void *) *head, sz, sizeof(WATCH_STRUCT_UTMP),
	           (int (*) _((const void *, const void *)))ucmp);
    return sz;
}

/* Check for login/logout events; executed before *
 * each prompt if WATCH is set                    */

/**/
void
dowatch(void)
{
    WATCH_STRUCT_UTMP *utab, *uptr, *wptr;
    struct stat st;
    char **s;
    char *fmt;
    int utabsz, uct, wct;

    s = watch;

    holdintr();
    if (!wtab)
	wtabsz = readwtab(&wtab, 32);
    if ((stat(WATCH_UTMP_FILE, &st) == -1) || (st.st_mtime <= lastutmpcheck)) {
	noholdintr();
	return;
    }
    lastutmpcheck = st.st_mtime;
    utabsz = readwtab(&utab, wtabsz + 4);
    noholdintr();
    if (errflag) {
	free(utab);
	return;
    }

    wct = wtabsz;
    uct = utabsz;
    uptr = utab;
    wptr = wtab;
    if (errflag) {
	free(utab);
	return;
    }
    queue_signals();
    if (!(fmt = getsparam_u("WATCHFMT")))
	fmt = DEFAULT_WATCHFMT;
    while ((uct || wct) && !errflag) {
	if (!uct || (wct && ucmp(uptr, wptr) > 0))
	    wct--, watchlog(0, wptr++, s, fmt);
	else if (!wct || (uct && ucmp(uptr, wptr) < 0))
	    uct--, watchlog(1, uptr++, s, fmt);
	else
	    uptr++, wptr++, wct--, uct--;
    }
    unqueue_signals();
    free(wtab);
    wtab = utab;
    wtabsz = utabsz;
    fflush(stdout);
    lastwatch = time(NULL);
}

static void
checksched(void)
{
    /* Do nothing if WATCH is not set, or LOGCHECK has not elapsed */
    if (watch && (int) difftime(time(NULL), lastwatch) > getiparam("LOGCHECK"))
	dowatch();
}

/**/
static int
bin_log(UNUSED(char *nam), UNUSED(char **argv), UNUSED(Options ops), UNUSED(int func))
{
    if (!watch)
	return 1;
    if (wtab)
	free(wtab);
    wtab = (WATCH_STRUCT_UTMP *)zalloc(1);
    wtabsz = 0;
    lastutmpcheck = 0;
    dowatch();
    return 0;
}

#else /* !WATCH_STRUCT_UTMP */

static void
checksched(void)
{
}

/**/
static int
bin_log(char *nam, char **argv, Options ops, int func)
{
    return bin_notavail(nam, argv, ops, func);
}

#endif /* !WATCH_STRUCT_UTMP */

/**/
static char **watch; /* $watch */

/* module setup */

static struct builtin bintab[] = {
    BUILTIN("log", 0, bin_log, 0, 0, 0, NULL, NULL),
};

static struct paramdef partab[] = {
    PARAMDEF("WATCH", PM_SCALAR|PM_SPECIAL, &watch, NULL),
    PARAMDEF("watch", PM_ARRAY|PM_SPECIAL, &watch, NULL),
};

static struct features module_features = {
    bintab, sizeof(bintab)/sizeof(*bintab),
    NULL, 0,
    NULL, 0,
    partab, sizeof(partab)/sizeof(*partab),
    0
};

/**/
int
setup_(UNUSED(Module m))
{
    /* On Cygwin, colonarr_gsu exists in libzsh.dll and we can't
     * use &colonarr_gsu in the initialization of partab[] above */
    partab[0].gsu = (void *)&colonarr_gsu;
    partab[1].gsu = (void *)&vararray_gsu;
    return 0;
}

/**/
int
features_(Module m, char ***features)
{
    *features = featuresarray(m, &module_features);
    return 0;
}

/**/
int
enables_(Module m, int **enables)
{
    return handlefeatures(m, &module_features, enables);
}

/**/
int
boot_(UNUSED(Module m))
{
    static char const * const default_watchfmt = DEFAULT_WATCHFMT;

    Param pma = (Param) paramtab->getnode(paramtab, "watch");
    Param pms = (Param) paramtab->getnode(paramtab, "WATCH");
    if (pma && pms && pma->u.arr == watch && pms->u.arr == watch) {
	/* only tie the two parameters if both were added */
	pma->ename = "WATCH";
	pms->ename = "watch";
	pma->node.flags |= PM_TIED;
	pms->node.flags |= PM_TIED;
    }
    watch = mkarray(NULL);

    /* These two parameters are only set to defaults if not set.
     * So setting them in .zshrc will not be enough to load the
     * module. It's useless until the watch array is set anyway. */
    if (!paramtab->getnode(paramtab, "WATCHFMT"))
	setsparam("WATCHFMT", ztrdup_metafy(default_watchfmt));
    if (!paramtab->getnode(paramtab, "LOGCHECK"))
	setiparam("LOGCHECK", 60);

    addprepromptfn(&checksched);

    return 0;
}

/**/
int
cleanup_(Module m)
{
    delprepromptfn(&checksched);
    return setfeatureenables(m, &module_features, NULL);
}

/**/
int
finish_(UNUSED(Module m))
{
    return 0;
}
