/*
 * virtualconsole.c: A plugin for the Video Disk Recorder
 *
 * See the README file for copyright information and how to reach the author.
 *
 * $Id$
 */


#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stropts.h>
#include <errno.h>

#include <vdr/config.h>
#include <vdr/remote.h>

#include "virtualconsole.h"
#include "i18n.h"






cConsVirtualConsole::cConsVirtualConsole(const char* title, const char* command, char* const argv[])
: _title (NULL)
{

  _master = -1;
  _childPid = 0;
  _isOpen = false;

  _title = strdup(title);

  Open(command, argv);
}



cConsVirtualConsole::~cConsVirtualConsole() {

  Close();

  free(_title);
}



bool cConsVirtualConsole::IsOpen() {

  return _isOpen;
}



// closeall() -- close all FDs >= a specified value

void closeall(int fd) {

    int fdlimit = sysconf(_SC_OPEN_MAX);

    while (fd < fdlimit)
      close(fd++);
}



bool cConsVirtualConsole::Open(const char* command, char* const argv[]) {

  // Let's watch if the slave is already running
  if (! _isOpen) {

    CONSOLE_DEBUG(fprintf(stderr, "opening master\n"));

    if ((_master = open("/dev/ptmx", O_RDWR | O_NONBLOCK)) < 0) {  // open a master
      esyslog("console: could not open master pty for command %s: %s", command, strerror(errno));
      return false;
    }

    CONSOLE_DEBUG(fprintf(stderr, "master opened %i\n", _master));
    if (grantpt(_master) < 0) {                 // change permission of slave
      esyslog("console: could not change permission of slave: %s\n", strerror(errno));
      return false;
    }
    if (unlockpt(_master) < 0) {                // unlock slave
      esyslog("console: could not unlock slave: %s", strerror(errno));
      return false;
    }
    char* slavename = ptsname(_master);         // get name of slave
    if (! slavename) {
      esyslog("console: could not get a slave name");
      return false;
    }

    CONSOLE_DEBUG(fprintf(stderr, "name of slave device is %s\n", slavename));


    int pid = fork();
    if (pid < 0) {
      close(_master);
      _master = 0;
      esyslog("console: fork failed");
      return false;
    }


    if (pid == 0) {

      CONSOLE_DEBUG(fprintf(stderr, "slave: reached\n"));

      // We are in the child process
      closeall(0);



      // We need to make this process a session group leader, because
      // it is on a new PTY, and things like job control simply will
      // not work correctly unless there is a session group leader
      // and process group leader (which a session group leader
      // automatically is). This also disassociates us from our old
      // controlling tty.

      if (setsid() < 0) {
        esyslog("console: could not set session leader for %s", slavename);
      }

      CONSOLE_DEBUG(fprintf(stderr, "slave: setsid ok\n"));


      int slave = open(slavename, O_RDWR);       // open slave
      if (slave < 0) {
        esyslog("console: could not open slave pty %s: %s", slavename, strerror(errno));
        exit(1);
      }

      CONSOLE_DEBUG(fprintf(stderr, "slave: pts id is %i\n", slave));

      ioctl(slave, I_PUSH, "ptem");          // push ptem
      ioctl(slave, I_PUSH, "ldterm");        // push ldterm


      // Tie us to our new controlling tty.
      if (ioctl(slave, TIOCSCTTY, NULL)) {
        esyslog("console: could not set new controlling tty for %s", slavename);
      }

      CONSOLE_DEBUG(fprintf(stderr, "slave: ioctl ok\n"));

      // make slave pty be standard in, out, and error
      dup2(slave, STDIN_FILENO);
      dup2(slave, STDOUT_FILENO);
      dup2(slave, STDERR_FILENO);

      // at this point the slave pty should be standard input
      if (slave > 2) {
        close(slave);
      }

      // Tell the executing program which protocol we are using.
      putenv("TERM=linux");

      //printf("Message from slave :-)\n"); // Should be displayed on virtual console

      // now start the login
      if (strcmp(command, "/bin/login") == 0) {

        // Replace the standard login program with our own login routine.
        char userName[60];
        for (int loginCount = 3; loginCount >= 0; --loginCount) {

          // Empty the input buffer
          cPoller poller(STDIN_FILENO, false);
          while (poller.Poll())
            read(STDIN_FILENO, userName, 60);

          // Ask for login name
          printf("Login: ");
          *userName = 0;
          fgets(userName, 60, stdin);

          // Prevent su from reacting on invalid input!
          if (*userName) {
            for (int i = 0;; ++i) {
              if (!(userName[i] >= '0' && userName[i] <= '9' ||
                    userName[i] >= 'a' && userName[i] <= 'z' ||
                    userName[i] >= 'A' && userName[i] <= 'Z' ||
                    userName[i] == '_')) {

                userName[i] = 0;
                break;
              }
            }

            char cmd[70];
            sprintf(cmd, "/bin/su \"%s\"", userName);
            if (system(cmd) == 0) break;
          }
        }

        // Terminated 
        _exit(0);
      }
      execv(command, argv);

      // exec has failed
      _exit(1);
    }

    // save the child id
    _childPid = pid;
    _isOpen = true;


    // With this, the terminal emulation can respond to requests from the console.
    _screen.setOutputFileDescriptor(_master);

    CONSOLE_DEBUG(fprintf(stderr, "master: reached\n"));
    isyslog("console: new child started (%s, pid=%d, pts=%d)", _title, pid, _master);

    // We are in the master process
  }

  return true;
}



#define CONSOLE_USE_TIME_RESOLUTION 100

bool cConsVirtualConsole::ConsoleWaitPid(volatile int& pid, int timeoutMs) {

  for (int i = timeoutMs / CONSOLE_USE_TIME_RESOLUTION; i > 0; --i) {

    if (waitpid(pid, NULL, WNOHANG) == pid)
      return true;

    HandleOutput();

    usleep(CONSOLE_USE_TIME_RESOLUTION * 1000);
  }

  // timeout
  return (pid < 0);
}



bool cConsVirtualConsole::Close() {

  // give the process the ability to quit
  if (_isOpen) {

    //isyslog("console: sending SIGTERM to child (pid=%d)", _childPid);
    kill(_childPid, SIGTERM);

    if (! ConsoleWaitPid(_childPid, 500)) {

      isyslog("console: killing not interuptable child (pid=%d)", _childPid);
      kill(_childPid, SIGKILL);

      if (! ConsoleWaitPid(_childPid, 500))
        return false;
    }

    return true;
  }
  return false;
}



void cConsVirtualConsole::HasClosed(bool force) {

  _isOpen = false;

  if (force) {

    // Ok, the child has terminated.
    // Try to read all pending output.
    if (_master >= 0)
      HandleOutput();

    if (_childPid >= 0) {

      // avoid zombies
      waitpid(_childPid, NULL, WNOHANG);

      CONSOLE_DEBUG(fprintf(stderr, "child terminated pid=%i\n", _childPid));
      _childPid = -1;
    }


    if (_master >= 0) {
      if (close(_master) < 0)
        esyslog("console: could not close pts (pid=%d, pts=%d)", _childPid, _master);
      _master = -1;
    }
  }
}



void cConsVirtualConsole::setTerminalSize(int charW, int charH, int pixelW, int pixelH) {

  _screen.setSize(charW, charH);

  if (_isOpen) {

    winsize ws;
    ws.ws_col = charW;
    ws.ws_row = charH;
    ws.ws_xpixel = pixelW;
    ws.ws_ypixel = pixelH;

    // Try to set window size; failure isn't critical
    if (ioctl(_master, TIOCSWINSZ, &ws) < 0)
      esyslog("console: could not set window size to width=%d, height=%d (pid=%d, pts=%d)", charW, charH, _childPid, _master);

//fprintf(stderr, "Terminal Resized: %i, %i\n", charW, charH);
  }
}



void cConsVirtualConsole::Write(const unsigned char* data, int len) {

  if (_isOpen) {

    int i = write(_master, (char*)data, len);
    if (i < 0 && errno != EINTR)
      HasClosed();
  }
}



bool cConsVirtualConsole::HandleOutput() {

  if (_master >= 0) {

    int i = 0;

    for (;;) {

      i = read(_master, _buf, INPUT_BUFSIZE);
      if (i > 0) {

        _buf[i] = 0;                     // Terminate string
        _screen.Write(_buf);             // Print on screen

      } else if (i == 0 ||
                 (i < 0 && errno != EAGAIN && errno != EINTR)) {

        // Slave has terminated, so free the pty...
        HasClosed();

        // ... and signal the user interface (with an empty string)
        _screen.Write((unsigned char*)"");

        // Brings the consoles to remove me from its polling list
        return false;
      }

      if (i < INPUT_BUFSIZE)
        break;


      // If the buffer is full then do a new cycle to ensure
      // that all available data were read.
    }
  }

  return true;
}


