/*
 * Copyright (c) 1990, 1991 by the Massachusetts Institute of Technology
 * For copying and distribution information, please see the file
 * <mit-copyright.h>.
 *
 * This is the top-level of the display manager and console control
 * for Athena's xlogin.
 */
/*
 *  modified by svalente
 *  based on version dm.c 1.33 93/08/04 13:03:31
 *
 */

#include <mit-copyright.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <time.h>
#include <ctype.h>
#include <string.h>
#include <errno.h>
#include <termios.h>
#include <utmp.h>
#include <pwd.h>
#include <syslog.h>
#include <sys/ioctl.h>
#include <AL/AL.h>

#ifdef __linux__
int rename(); /* prototype left out of unistd.h */
#endif

#ifndef __STDC__
#define volatile
#endif

/* Process states */
#define NONEXISTANT	0
#define RUNNING		1
#define STARTUP		2
#define CONSOLELOGIN	3

#ifndef FALSE
#define FALSE		0
#define TRUE		1
#endif

#ifdef DEBUG
#define debug(str) message(str)
#else
#define debug(str) ;
#endif
#ifndef TRACE
#define trace(str) ;
#endif

/* flags used by signal handlers */
volatile int alarm_running = NONEXISTANT;
volatile int xpid, x_running = NONEXISTANT;
volatile int consolepid, console_running = NONEXISTANT;
volatile int console_tty = 0, console_failed = FALSE;
volatile int loginpid, login_running = NONEXISTANT;
volatile int clflag;
char *logintty;

char *login_prog = "/bin/login";

/* Files */
#ifndef _PATH_UTMP
#define _PATH_UTMP "/etc/utmp"
#endif
#ifndef _PATH_WTMP
#define _PATH_WTMP "/usr/adm/wtmp"
#endif
#ifdef __NetBSD__
#define _PATH_XPID "/var/run/X0.pid"
#define _PATH_CONSOLEPID "/var/run/console.pid"
#define _PATH_DMPID "/var/run/dm.pid"
#define _PATH_CONSLOG "/var/tmp/console.log"
#else
#define _PATH_XPID "/usr/tmp/X0.pid"
#define _PATH_CONSOLEPID "/etc/athena/console.pid"
#define _PATH_DMPID "/etc/athena/dm.pid"
#define _PATH_CONSLOG "/usr/tmp/console.log"
#endif

char utmpf[] =	_PATH_UTMP;
char wtmpf[] =	_PATH_WTMP;
char xpidf[] = 	_PATH_XPID;
char consolepidf[] = _PATH_CONSOLEPID;
char dmpidf[] =	_PATH_DMPID;
char consolelog[] = _PATH_CONSLOG;

/* the console process will run as daemon */
static int DAEMON = 1;

#define X_START_WAIT	30	/* wait up to 30 seconds for X to be ready */
#define X_STOP_WAIT	3	/* (seconds / 2) wait for graceful shutdown */
#define LOGIN_START_WAIT 60	/* wait up to 1 minute for Xlogin */
#ifndef BUFSIZ
#define BUFSIZ		1024
#endif

/* prototypes */
void console_login (), start_console (), cleanup (), removepwent ();
void message (), get_daemon ();
void csignal (), sig_block (), sig_unblock_all (), sig_pause ();

/* Setup signals, start X, start console, start login, wait */

int main(argc, argv)
int argc;
char **argv;
{
    void die(), child(), catchalarm(), xready(), setclflag(), shutdown();
    void loginready(), clean_groups();
    char *consoletty, *conf, *p, *number(), *getconf();
    char **xargv, **consoleargv = NULL, **loginargv, **parseargs();
    char line[16];
    int file, tries, console = TRUE, flags, on;

    get_daemon();

#ifdef DEBUG
    openlog("dm", LOG_ODELAY, LOG_DAEMON);
#endif
    if (argc != 4 &&
	(argc != 5 || strcmp(argv[3], "-noconsole"))) {
	message("usage: ");
	message(argv[0]);
	message(" configfile logintty [-noconsole] consoletty\n");
	console_login(NULL);
    }
    if (argc == 5) console = FALSE;

    /* parse argument lists */
    conf = argv[1];
    logintty = argv[2];
    consoletty = argv[argc-1];
    p = getconf(conf, "X");
    if (p == NULL)
      console_login("\ndm: Can't find X command line\n");
    xargv = parseargs(p, NULL, NULL, NULL);
    if (console) {
	p = getconf(conf, "console");
	if (p == NULL)
	  console_login("\ndm: Can't find console command line\n");
          consoleargv = parseargs(p, NULL, NULL, NULL);
    }
    p = getconf(conf, "login");
    if (p == NULL)
      console_login("\ndm: Can't find login command line\n");
    loginargv = parseargs(p, logintty, "-tty", logintty);

    /* Signal Setup */
    csignal(SIGTSTP, SIG_IGN);
    csignal(SIGTTIN, SIG_IGN);
    csignal(SIGTTOU, SIG_IGN);
    csignal(SIGPIPE, SIG_IGN);	/* so that X pipe errors don't nuke us */
    csignal(SIGFPE, shutdown);
    csignal(SIGHUP, die);
    csignal(SIGINT, die);
    csignal(SIGTERM, die);
    csignal(SIGCHLD, child);
    csignal(SIGALRM, catchalarm);
    csignal(SIGUSR2, setclflag);

    /* setup ttys */
    close(0);
    close(1);
    close(2);
    setsid();
    strcpy(line, "/dev/");
    strcat(line, consoletty);
    open(line, O_RDWR, 0622);
    dup2(0, 1);
    dup2(1, 2);

    /* save our pid file */
    if ((file = open(dmpidf, O_WRONLY|O_TRUNC|O_CREAT, 0644)) >= 0) {
	write(file, number(getpid()), strlen(number(getpid())));
	close(file);
    }

    /* Fire up X */
    xpid = 0;
    for (tries = 0; tries < 3; tries++) {
        debug("Starting X\n");
	x_running = STARTUP;
	xpid = fork();
	switch (xpid) {
	case 0:
	    if(((flags = fcntl(2, F_GETFL, 1)) == -1) ||
	       (fcntl(2, F_SETFD, flags|FD_CLOEXEC) == -1))
	      close(2);
	    sig_unblock_all();
	    /* ignoring SIGUSR1 will cause the server to send us a SIGUSR1
	     * when it is ready to accept connections
	     */
	    csignal(SIGUSR1, SIG_IGN);
	    p = *xargv;
	    *xargv = "X";
	    execv(p, xargv);
	    message("dm: X server failed exec\n");
	    _exit(1);
	case -1:
	    message("dm: Unable to fork to start X server\n");
	    break;
	default:
	    csignal(SIGUSR1, xready);
	    if ((file = open(xpidf, O_WRONLY|O_TRUNC|O_CREAT, 0644)) >= 0) {
		write(file, number(xpid), strlen(number(xpid)));
		close(file);
	    }
	    if (x_running == NONEXISTANT) break;
	    alarm(X_START_WAIT);
	    alarm_running = RUNNING;
	    debug("waiting for X\n");
	    sig_pause();
	    if (x_running != RUNNING) {
		if (alarm_running == NONEXISTANT)
		  message("dm: Unable to start X\n");
		else
		  message("dm: X failed to become ready\n");
	    }
	    csignal(SIGUSR1, SIG_IGN);
	}
	if (x_running == RUNNING) break;
    }
    alarm(0);
    if (x_running != RUNNING) {
	console_login("\nUnable to start X, doing console login instead.\n");
    }

    /* start up console */
    strcpy(line, "/dev/");
    strcat(line, logintty);
    if (console) start_console(line, consoleargv);
    /* had to use a different tty, make sure xlogin uses it too */
    if (strcmp(logintty, &line[5]))
      strcpy(logintty, &line[5]);

    /* Fire up the X login */
    for (tries = 0; tries < 3; tries++) {
	debug("Starting X Login\n");
	clflag = FALSE;
	login_running = STARTUP;
	csignal(SIGUSR1, loginready);
	loginpid = fork();
	switch (loginpid) {
	case 0:
	    for (file = 0; file < sysconf(_SC_OPEN_MAX); file++)
	      close(file);
	    /* setup new tty */
	    strcpy(line, "/dev/");
	    strcat(line, logintty);
	    open("/dev/null", O_RDONLY, 0);
	    setsid();
	    open(line, O_RDWR, 0);
	    dup2(1, 2);
#ifdef TIOCCONS
	    /* redirect output from /dev/console to /dev/logintty */
	    on = 1;
	    ioctl(1, TIOCCONS, &on);
#endif
	    sig_unblock_all();
	    /* ignoring SIGUSR1 will cause xlogin to send us a SIGUSR1
	     * when it is ready
	     */
	    csignal(SIGUSR1, SIG_IGN);
	    /* dm ignores sigpipe; because of this, all of the children (ie, */
	    /* the entire session) inherit this unless we fix it now */
	    csignal(SIGPIPE, SIG_DFL);
	    execv(loginargv[0], loginargv);
	    message("dm: X login failed exec\n");
	    _exit(1);
	case -1:
	    message("dm: Unable to fork to start X login\n");
	    break;
	default:
	    alarm(LOGIN_START_WAIT);
	    alarm_running = RUNNING;
	    while (login_running == STARTUP && alarm_running == RUNNING)
	      sig_pause();
	    if (login_running != RUNNING) {
		kill(loginpid, SIGKILL);
		if (alarm_running != NONEXISTANT)
		  message("dm: Unable to start Xlogin\n");
		else
		  message("dm: Xlogin failed to become ready\n");
	    }
	}
	if (login_running == RUNNING) break;
    }
    csignal(SIGUSR1, SIG_IGN);
    alarm(0);
    if (login_running != RUNNING)
      console_login("\nUnable to start xlogin, doing console login instead.\n");

    /* main loop.  Wait for SIGCHLD, waking up every minute anyway. */
    sig_block(SIGCHLD);
    while (1) {
	debug("waiting...\n");

	/* Wait for something to hapen */
	alarm(60);
	sig_pause();

	if (login_running == STARTUP) {
	    sig_unblock_all();
	    console_login("\nConsole login requested.\n");
	}
	if (console && console_running == NONEXISTANT)
	  start_console(line, consoleargv);
	if (login_running == NONEXISTANT || x_running == NONEXISTANT) {
	    sig_unblock_all();
	    cleanup(logintty);
	    _exit(0);
	}
    }
}


/* Start a login on the raw console */

void console_login(msg)
char *msg;
{
    debug("console login is not supported; exiting\n");
    if (login_running == RUNNING)
      kill(loginpid, SIGHUP);
    if (console_running == RUNNING)
      kill(consolepid, SIGHUP);
    if (x_running == RUNNING)
      kill(xpid, SIGTERM);
    _exit(1);
}


/* start the console program.  It will have stdin set to the controling
 * side of the console pty, and stdout set to /dev/console inherited 
 * from the display manager.
 */

void start_console(line, argv)
char *line;
char **argv;
{
    static time_t last_try = (time_t) 0;
    time_t now;
    int file, i;
    char *number(), c;
    struct termios tc;

    debug("Starting Console\n");

    if (console_tty == 0) {
	/* Open master side of pty */
        line[5] = 'p';
	console_tty = open(line, O_RDONLY, 0);
	if (console_tty < 0) {
	    /* failed to open this tty, find another one */
	    for (c = 'p'; c <= 's'; c++) {
		line[8] = c;
		for (i = 0; i < 16; i++) {
		    line[9] = "0123456789abcdef"[i];
		    console_tty = open(line, O_RDONLY, 0);
		    if (console_tty >= 0) break;
		}
		if (console_tty >= 0) break;
	    }
	}
	/* out of ptys, use stdout (/dev/console) */
	if (console_tty < 0) console_tty = 1;
	/* Create console log file owned by daemon */
	if (access(consolelog, F_OK) != 0) {
	    file = open(consolelog, O_CREAT, 0644);
	    close(file);
	}
	chown(consolelog, DAEMON, 0);
    }
    line[5] = 't';
    
    time (&now);
    if (now <= last_try + 3) {
	/* give up on console */
	debug("Giving up on console\n");
	/* Set the console characteristics so we don't lose later */
	console_failed = TRUE;
	return;
    }
    last_try = now;


    console_running = RUNNING; 
    consolepid = fork();
    switch (consolepid) {
    case 0:
	/* Close all file descriptors except stdout/stderr */
	close(0);
	for (file = 3; file < sysconf(_SC_OPEN_MAX); file++)
	  if (file != console_tty)
	    close(file);
	dup2(console_tty, 0);
	close(console_tty);
	/* lose the controlling tty */
	setsid();
	/* Since we are the session leader, we must initialize the tty */
	tcgetattr(0, &tc);
	/* it is unclear if we actually want to change anything here */
	tcsetattr(0, TCSANOW, &tc);

#ifdef DEBUG
	close(1);
	close(2);
	open("/tmp/console.err", O_CREAT|O_APPEND|O_WRONLY, 0644);
	dup2(1, 2);
#endif
	setgid(DAEMON);
	setuid(DAEMON);
	sig_unblock_all();
	execv(argv[0], argv);
	message("dm: Failed to exec console\n");
	_exit(1);
    case -1:
	message("dm: Unable to fork to start console\n");
	_exit(1);
    default:
	if ((file = open(consolepidf, O_WRONLY|O_TRUNC|O_CREAT, 0644)) >= 0) {
	    write(file, number(consolepid), strlen(number(consolepid)));
	    close(file);
	}
    }
}


/* Kill children and hang around forever */

void shutdown()
{
    char buf[BUFSIZ];
    int i;

    if (login_running == RUNNING)
      kill(loginpid, SIGHUP);
    if (console_running == RUNNING)
      kill(consolepid, SIGHUP);
    if (x_running == RUNNING)
      kill(xpid, SIGTERM);

    sig_unblock_all();

    while (1) {
	i = read(console_tty, buf, sizeof(buf));
	write(1, buf, i);
    }
}


/* Kill children, remove password entry */

void cleanup(tty)
char *tty;
{
    int file, found;
    struct utmp utmp;    
    char login[9];

    if (login_running == RUNNING)
      kill(loginpid, SIGHUP);
    if (console_running == RUNNING)
      kill(consolepid, SIGHUP);
    if (x_running == RUNNING)
      kill(xpid, SIGTERM);

    found = 0;
    if ((file = open(utmpf, O_RDWR, 0)) >= 0) {
	while (read(file, (char *) &utmp, sizeof(utmp)) > 0) {
	    if (!strncmp(utmp.ut_line, tty, sizeof(utmp.ut_line))
#ifdef USER_PROCESS
		&& (utmp.ut_type == USER_PROCESS)
#endif
		) {
		strncpy(login, utmp.ut_name, 8);
		login[8] = '\0';
		if (utmp.ut_name[0] != '\0') {
		    strncpy(utmp.ut_name, "", sizeof(utmp.ut_name));
#ifdef LOGIN_PROCESS
		    utmp.ut_type = LOGIN_PROCESS;
#endif
		    lseek(file, -(off_t)sizeof(utmp), SEEK_CUR);
		    write(file, (char *) &utmp, sizeof(utmp));
		    found = 1;
		}
		break;
	    }
	}
	close(file);
    }
    /*
     *  make a logout notice in the wtmp (last) log.
     *  this is a bsd-style logout notice (appended at the end of the file.)
     *  sysv edits the utmp entry's logout time in place.
     *  make sure your "last" program can understand this format.
     */
    if (found) {
	if ((file = open(wtmpf, O_WRONLY|O_APPEND, 0644)) >= 0) {
	    strncpy(utmp.ut_line, tty, sizeof(utmp.ut_line));
	    strncpy(utmp.ut_name, "", sizeof(utmp.ut_name));
            strncpy(utmp.ut_host, "", sizeof(utmp.ut_host));
	    time(&utmp.ut_time);
	    write(file, (char *) &utmp, sizeof(utmp));
	    close(file);
	}
    }
    if (clflag) {
	/* Clean up password file */
	removepwent(login);
    }

    file = 0;
    tcflush(0, TCIOFLUSH);
}


/* When we get sigchild, figure out which child it was and set
 * appropriate flags
 */

void child()
{
    int pid;
    int status;
    char *number();

    /* we may have more than one child to handle, so keep going until
       we're done */
    while (1) {
	pid = waitpid(-1, &status, WNOHANG);
	if (pid == 0 || pid == -1) return;

	debug("Child exited ");
	debug(number(pid));
	if (pid == xpid) {
	    debug("X Server exited\n");
	    trace("X Server exited status ");
	    trace(number(WEXITSTATUS(status)));
	    x_running = NONEXISTANT;
	} else if (pid == consolepid) {
	    debug("Console exited\n");
	    trace("Console exited status ");
	    trace(number(WEXITSTATUS(status)));
	    console_running = NONEXISTANT;
	} else if (pid == loginpid) {
	    debug("X Login exited\n");
	    trace("X Login exited status ");
	    trace(number(WEXITSTATUS(status)));
	    if (WEXITSTATUS(status) == CONSOLELOGIN)
		login_running = STARTUP;
	    else
		login_running = NONEXISTANT;
	} else {
	    message("dm: Unexpected SIGCHLD from pid ");
	    message(number(pid));
	}
    }
}


void xready()
{
    debug("X Server ready\n");
    x_running = RUNNING;
}

void loginready()
{
    debug("X Login ready\n");
    login_running = RUNNING;
}

void setclflag()
{
    debug("Received Clear Login Flag signal\n");
    clflag = TRUE;
}


/* When an alarm happens, just note it and return */

void catchalarm()
{
    debug("Alarm!\n");
    alarm_running = NONEXISTANT;
}


/* kill children and go away */

void die()
{
    debug("Killing children and exiting\n");
    cleanup(logintty);
    _exit(0);
}


/* Remove a password entry.  Scans the password file for the specified
 * entry, and if found removes it.
 */

void removepwent(login)
char *login;
{
    struct passwd *pwd;

    pwd = getpwnam(login);
    if (pwd)
	PWremovePasswdEntry(pwd);
}

/* Takes a command line, returns an argv-style  NULL terminated array 
 * of strings.  The array is in malloc'ed memory.
 */

char **parseargs(line, extra, extra1, extra2)
char *line;
char *extra;
char *extra1;
char *extra2;
{
    int i = 0;
    char *p = line;
    char **ret;
    char *malloc();

    while (*p) {
	while (*p && isspace(*p)) p++;
	while (*p && !isspace(*p)) p++;
	i++;
    }
    ret = (char **) malloc(sizeof(char *) * (i + 4));

    p = line;
    i = 0;
    while (*p) {
	while (*p && isspace(*p)) p++;
	if (*p == 0) break;
	ret[i++] = p;
	while (*p && !isspace(*p)) p++;
	if (*p == 0) break;
	*p++ = 0;
    }
    if (extra)
      ret[i++] = extra;
    if (extra1)
      ret[i++] = extra1;
    if (extra2)
      ret[i++] = extra2;

    ret[i] = NULL;
    return(ret);
}


/* Used for logging errors and other messages so we don't need to link
 * against the stdio library.
 */

void message(s)
char *s;
{
    int i = strlen(s);
    write(2, s, i);
}


/* Convert an int to a string, and return a pointer to this string in a
 * static buffer.  The string will be newline and null terminated.
 */

char *number(x)
int x;
{
#define NDIGITS 16
    static char buffer[NDIGITS];
    char *p = &buffer[NDIGITS-1];

    *p-- = 0;
    *p-- = '\n';
    while (x) {
	*p-- = x % 10 + '0';
	x = x / 10;
    }
    if (p == &buffer[NDIGITS-3])
      *p-- = '0';
    return(p+1);
}


/* Find a named field in the config file.  Config file contains 
 * comment lines starting with #, and lines with a field name,
 * whitespace, then field value.  This routine returns the field
 * value, or NULL on error.
 */

char *getconf(file, name)
char *file;
char *name;
{
    static char buf[2048];
    static int inited = 0;
    char *p, *ret, *malloc();
    int i;

    if (!inited) {
	int fd;

	fd = open(file, O_RDONLY, 0644);
	if (fd < 0) return(NULL);
	i = read(fd, buf, sizeof(buf));
	if (i >= sizeof(buf) - 1)
	  message("dm: warning - config file is to long to parse\n");
	buf[i] = 0;
	close(fd);
	inited = 1;
    }

    for (p = &buf[0]; p && *p; p = strchr(p, '\n')) {
	if (*p == '\n') p++;
	if (p == NULL || *p == 0) return(NULL);
	if (*p == '#') continue;
	if (strncmp(p, name, strlen(name))) continue;
	p += strlen(name);
	if (*p && !isspace(*p)) continue;
	while (*p && isspace(*p)) p++;
	if (*p == 0) return(NULL);
	ret = strchr(p, '\n');
	if (ret)
	  i = ret - p;
	else
	  i = strlen(p);
	ret = malloc(i+1);
	memcpy(ret, p, i+1);
	ret[i] = 0;
	return(ret);
    }
    return(NULL);
}


#ifdef TRACE
trace(msg)
char *msg;
{
    int f;

    f = open("/tmp/dm.log", O_WRONLY|O_CREAT|O_APPEND, 0644);
    if (!f) return;
    write(f, msg, strlen(msg));
    close(f);
}
#endif

void get_daemon ()
{
    struct passwd *pwd;

    pwd = getpwnam ("daemon");
    if (pwd != NULL)
	DAEMON = pwd->pw_uid;
}

void csignal (sig, handler)
     int sig;
     void (*handler)();
{
  struct sigaction act, oact;
  sigset_t empty;
  sigemptyset (&empty);
  act.sa_handler = handler;
  act.sa_mask = empty;
  act.sa_flags = 0;
  sigaction (sig, &act, &oact);
}

void sig_block (sig)
     int sig;
{
  sigset_t block;
  sigemptyset (&block);
  sigaddset (&block, sig);
  sigprocmask (SIG_BLOCK, &block, NULL);
}

void sig_unblock_all ()
{
  sigset_t empty;
  sigemptyset (&empty);
  sigprocmask (SIG_SETMASK, &empty, NULL);
}

void sig_pause ()
{
  sigset_t empty;
  sigemptyset (&empty);
  sigsuspend (&empty);
}
