You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdelibs/kdesu/process.cpp

627 lines
14 KiB

/* vi: ts=8 sts=4 sw=4
*
* $Id$
*
* This file is part of the KDE project, module tdesu.
* Copyright (C) 1999,2000 Geert Jansen <jansen@kde.org>
*
* This file contains code from TEShell.C of the KDE konsole.
* Copyright (c) 1997,1998 by Lars Doelle <lars.doelle@on-line.de>
*
* This is free software; you can use this library under the GNU Library
* General Public License, version 2. See the file "COPYING.LIB" for the
* exact licensing terms.
*
* process.cpp: Functionality to build a front end to password asking
* terminal programs.
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <termios.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/ioctl.h>
#if defined(__SVR4) && defined(sun)
#include <stropts.h>
#include <sys/stream.h>
#endif
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h> // Needed on some systems.
#endif
#include <tqglobal.h>
#include <tqcstring.h>
#include <tqfile.h>
#include <kconfig.h>
#include <kdebug.h>
#include <kstandarddirs.h>
#include "process.h"
#include "tdesu_pty.h"
#include "kcookie.h"
int PtyProcess::waitMS(int fd,int ms)
{
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 1000*ms;
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd,&fds);
return select(fd+1, &fds, 0L, 0L, &tv);
}
/*
** Basic check for the existence of @p pid.
** Returns true iff @p pid is an extant process.
*/
bool PtyProcess::checkPid(pid_t pid)
{
KConfig* config = KGlobal::config();
config->setGroup("super-user-command");
TQString superUserCommand = config->readEntry("super-user-command", DEFAULT_SUPER_USER_COMMAND);
//sudo does not accept signals from user so we except it
if (superUserCommand == "sudo") {
return true;
} else {
return kill(pid,0) == 0;
}
}
/*
** Check process exit status for process @p pid.
** On error (no child, no exit), return Error (-1).
** If child @p pid has exited, return its exit status,
** (which may be zero).
** If child @p has not exited, return NotExited (-2).
*/
int PtyProcess::checkPidExited(pid_t pid)
{
int state, ret;
ret = waitpid(pid, &state, WNOHANG);
if (ret < 0)
{
kdError(900) << k_lineinfo << "waitpid(): " << perror << "\n";
return Error;
}
if (ret == pid)
{
if (WIFEXITED(state))
return WEXITSTATUS(state);
return Killed;
}
return NotExited;
}
class PtyProcess::PtyProcessPrivate
{
public:
QCStringList env;
};
PtyProcess::PtyProcess()
{
m_bTerminal = false;
m_bErase = false;
m_pPTY = 0L;
d = new PtyProcessPrivate;
}
int PtyProcess::init()
{
delete m_pPTY;
m_pPTY = new PTY();
m_Fd = m_pPTY->getpt();
if (m_Fd < 0)
return -1;
if ((m_pPTY->grantpt() < 0) || (m_pPTY->unlockpt() < 0))
{
kdError(900) << k_lineinfo << "Master setup failed.\n";
m_Fd = -1;
return -1;
}
m_TTY = m_pPTY->ptsname();
m_Inbuf.resize(0);
return 0;
}
PtyProcess::~PtyProcess()
{
delete m_pPTY;
delete d;
}
/** Set additinal environment variables. */
void PtyProcess::setEnvironment( const QCStringList &env )
{
d->env = env;
}
const QCStringList& PtyProcess::environment() const
{
return d->env;
}
/*
* Read one line of input. The terminal is in canonical mode, so you always
* read a line at at time, but it's possible to receive multiple lines in
* one time.
*/
TQCString PtyProcess::readLine(bool block)
{
int pos;
TQCString ret;
if (!m_Inbuf.isEmpty())
{
pos = m_Inbuf.find('\n');
if (pos == -1)
{
ret = m_Inbuf;
m_Inbuf.resize(0);
} else
{
ret = m_Inbuf.left(pos);
m_Inbuf = m_Inbuf.mid(pos+1);
}
return ret;
}
int flags = fcntl(m_Fd, F_GETFL);
if (flags < 0)
{
kdError(900) << k_lineinfo << "fcntl(F_GETFL): " << perror << "\n";
return ret;
}
int oflags = flags;
if (block)
flags &= ~O_NONBLOCK;
else
flags |= O_NONBLOCK;
if ((flags != oflags) && (fcntl(m_Fd, F_SETFL, flags) < 0))
{
// We get an error here when the child process has closed
// the file descriptor already.
return ret;
}
int nbytes;
char buf[256];
while (1)
{
nbytes = read(m_Fd, buf, 255);
if (nbytes == -1)
{
if (errno == EINTR)
continue;
else break;
}
if (nbytes == 0)
break; // eof
buf[nbytes] = '\000';
m_Inbuf += buf;
pos = m_Inbuf.find('\n');
if (pos == -1)
{
ret = m_Inbuf;
m_Inbuf.resize(0);
} else
{
ret = m_Inbuf.left(pos);
m_Inbuf = m_Inbuf.mid(pos+1);
}
break;
}
return ret;
}
TQCString PtyProcess::readAll(bool block)
{
TQCString ret;
if (!m_Inbuf.isEmpty())
{
// if there is still something in the buffer, we need not block.
// we should still try to read any further output, from the fd, though.
block = false;
ret = m_Inbuf;
m_Inbuf.resize(0);
}
int flags = fcntl(m_Fd, F_GETFL);
if (flags < 0)
{
kdError(900) << k_lineinfo << "fcntl(F_GETFL): " << perror << "\n";
return ret;
}
int oflags = flags;
if (block)
flags &= ~O_NONBLOCK;
else
flags |= O_NONBLOCK;
if ((flags != oflags) && (fcntl(m_Fd, F_SETFL, flags) < 0))
{
// We get an error here when the child process has closed
// the file descriptor already.
return ret;
}
int nbytes;
char buf[256];
while (1)
{
nbytes = read(m_Fd, buf, 255);
if (nbytes == -1)
{
if (errno == EINTR)
continue;
else break;
}
if (nbytes == 0)
break; // eof
buf[nbytes] = '\000';
ret += buf;
break;
}
return ret;
}
void PtyProcess::writeLine(const TQCString &line, bool addnl)
{
if (!line.isEmpty())
write(m_Fd, line, line.length());
if (addnl)
write(m_Fd, "\n", 1);
}
void PtyProcess::unreadLine(const TQCString &line, bool addnl)
{
TQCString tmp = line;
if (addnl)
tmp += '\n';
if (!tmp.isEmpty())
m_Inbuf.prepend(tmp);
}
/*
* Fork and execute the command. This returns in the parent.
*/
int PtyProcess::exec(const TQCString &command, const QCStringList &args)
{
kdDebug(900) << k_lineinfo << "Running `" << command << "'\n";
if (init() < 0)
return -1;
// Open the pty slave before forking. See SetupTTY()
int slave = open(m_TTY, O_RDWR);
if (slave < 0)
{
kdError(900) << k_lineinfo << "Could not open slave pty.\n";
return -1;
}
if ((m_Pid = fork()) == -1)
{
kdError(900) << k_lineinfo << "fork(): " << perror << "\n";
return -1;
}
// Parent
if (m_Pid)
{
close(slave);
return 0;
}
// Child
if (SetupTTY(slave) < 0)
_exit(1);
for(QCStringList::ConstIterator it = d->env.begin();
it != d->env.end(); it++)
{
putenv(const_cast<TQCString&>(*it).data());
}
unsetenv("TDE_FULL_SESSION");
// set temporarily LC_ALL to C, for su (to be able to parse "Password:")
const char* old_lc_all = getenv( "LC_ALL" );
if( old_lc_all != NULL )
setenv( "KDESU_LC_ALL", old_lc_all, 1 );
else
unsetenv( "KDESU_LC_ALL" );
setenv("LC_ALL", "C", 1);
// From now on, terminal output goes through the tty.
TQCString path;
if (command.contains('/'))
path = command;
else
{
TQString file = KStandardDirs::findExe(command);
if (file.isEmpty())
{
kdError(900) << k_lineinfo << command << " not found\n";
_exit(1);
}
path = TQFile::encodeName(file);
}
const char **argp = (const char **)malloc((args.count()+2)*sizeof(char *));
int i = 0;
argp[i++] = path;
for (QCStringList::ConstIterator it=args.begin(); it!=args.end(); ++it)
argp[i++] = *it;
argp[i] = 0L;
execv(path, (char * const *)argp);
kdError(900) << k_lineinfo << "execv(\"" << path << "\"): " << perror << "\n";
_exit(1);
return -1; // Shut up compiler. Never reached.
}
/*
* Wait until the terminal is set into no echo mode. At least one su
* (RH6 w/ Linux-PAM patches) sets noecho mode AFTER writing the Password:
* prompt, using TCSAFLUSH. This flushes the terminal I/O queues, possibly
* taking the password with it. So we wait until no echo mode is set
* before writing the password.
* Note that this is done on the slave fd. While Linux allows tcgetattr() on
* the master side, Solaris doesn't.
*/
int PtyProcess::WaitSlave()
{
int slave = open(m_TTY, O_RDWR);
if (slave < 0)
{
kdError(900) << k_lineinfo << "Could not open slave tty.\n";
return -1;
}
kdDebug(900) << k_lineinfo << "Child pid " << m_Pid << endl;
struct termios tio;
while (1)
{
if (!checkPid(m_Pid))
{
close(slave);
return -1;
}
if (tcgetattr(slave, &tio) < 0)
{
kdError(900) << k_lineinfo << "tcgetattr(): " << perror << "\n";
close(slave);
return -1;
}
if (tio.c_lflag & ECHO)
{
kdDebug(900) << k_lineinfo << "Echo mode still on.\n";
waitMS(slave,100);
continue;
}
break;
}
close(slave);
return 0;
}
int PtyProcess::enableLocalEcho(bool enable)
{
int slave = open(m_TTY, O_RDWR);
if (slave < 0)
{
kdError(900) << k_lineinfo << "Could not open slave tty.\n";
return -1;
}
struct termios tio;
if (tcgetattr(slave, &tio) < 0)
{
kdError(900) << k_lineinfo << "tcgetattr(): " << perror << "\n";
close(slave); return -1;
}
if (enable)
tio.c_lflag |= ECHO;
else
tio.c_lflag &= ~ECHO;
if (tcsetattr(slave, TCSANOW, &tio) < 0)
{
kdError(900) << k_lineinfo << "tcsetattr(): " << perror << "\n";
close(slave); return -1;
}
close(slave);
return 0;
}
/*
* Copy output to stdout until the child process exists, or a line of output
* matches `m_Exit'.
* We have to use waitpid() to test for exit. Merely waiting for EOF on the
* pty does not work, because the target process may have children still
* attached to the terminal.
*/
int PtyProcess::waitForChild()
{
int retval = 1;
fd_set fds;
FD_ZERO(&fds);
while (1)
{
FD_SET(m_Fd, &fds);
int ret = select(m_Fd+1, &fds, 0L, 0L, 0L);
if (ret == -1)
{
if (errno != EINTR)
{
kdError(900) << k_lineinfo << "select(): " << perror << "\n";
return -1;
}
ret = 0;
}
if (ret)
{
TQCString output = readAll(false);
bool lineStart = true;
while (!output.isNull())
{
if (!m_Exit.isEmpty())
{
// match exit string only at line starts
int pos = output.find(m_Exit.data());
if ((pos >= 0) && ((pos == 0 && lineStart) || (output.at (pos - 1) == '\n')))
{
kill(m_Pid, SIGTERM);
}
}
if (m_bTerminal)
{
fputs(output, stdout);
fflush(stdout);
}
lineStart = output.tqat( output.length() - 1 ) == '\n';
output = readAll(false);
}
}
ret = checkPidExited(m_Pid);
if (ret == Error)
{
if (errno == ECHILD) retval = 0;
else retval = 1;
break;
}
else if (ret == Killed)
{
retval = 0;
break;
}
else if (ret == NotExited)
{
// keep checking
}
else
{
retval = ret;
break;
}
}
return retval;
}
/*
* SetupTTY: Creates a new session. The filedescriptor "fd" should be
* connected to the tty. It is closed after the tty is reopened to make it
* our controlling terminal. This way the tty is always opened at least once
* so we'll never get EIO when reading from it.
*/
int PtyProcess::SetupTTY(int fd)
{
// Reset signal handlers
for (int sig = 1; sig < NSIG; sig++)
signal(sig, SIG_DFL);
signal(SIGHUP, SIG_IGN);
// Close all file handles
struct rlimit rlp;
getrlimit(RLIMIT_NOFILE, &rlp);
for (int i = 0; i < (int)rlp.rlim_cur; i++)
if (i != fd) close(i);
// Create a new session.
setsid();
// Open slave. This will make it our controlling terminal
int slave = open(m_TTY, O_RDWR);
if (slave < 0)
{
kdError(900) << k_lineinfo << "Could not open slave side: " << perror << "\n";
return -1;
}
close(fd);
#if defined(__SVR4) && defined(sun)
// Solaris STREAMS environment.
// Push these modules to make the stream look like a terminal.
ioctl(slave, I_PUSH, "ptem");
ioctl(slave, I_PUSH, "ldterm");
#endif
#ifdef TIOCSCTTY
ioctl(slave, TIOCSCTTY, NULL);
#endif
// Connect stdin, stdout and stderr
dup2(slave, 0); dup2(slave, 1); dup2(slave, 2);
if (slave > 2)
close(slave);
// Disable OPOST processing. Otherwise, '\n' are (on Linux at least)
// translated to '\r\n'.
struct termios tio;
if (tcgetattr(0, &tio) < 0)
{
kdError(900) << k_lineinfo << "tcgetattr(): " << perror << "\n";
return -1;
}
tio.c_oflag &= ~OPOST;
if (tcsetattr(0, TCSANOW, &tio) < 0)
{
kdError(900) << k_lineinfo << "tcsetattr(): " << perror << "\n";
return -1;
}
return 0;
}
void PtyProcess::virtual_hook( int, void* )
{ /*BASE::virtual_hook( id, data );*/ }