/* vi: ts=8 sts=4 sw=4 * * This file is part of the KDE project, module kdesu. * Copyright (C) 1999,2000 Geert Jansen * * This file contains code from TEShell.C of the KDE konsole. * Copyright (c) 1997,1998 by Lars Doelle * * 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 "process.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Needed on some systems. #include #include #include #include #include #include #include /* ** Wait for @p ms miliseconds ** @param fd file descriptor ** @param ms time to wait in miliseconds ** @return */ 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); } // XXX this function is nonsense: // - for our child, we could use waitpid(). // - the configurability at this place it *complete* braindamage /* ** Basic check for the existence of @p pid. ** Returns true iff @p pid is an extant process. */ bool PtyProcess::checkPid(pid_t pid) { KSharedConfig::Ptr config = KGlobal::config(); KConfigGroup cg(config, "super-user-command"); QString superUserCommand = cg.readEntry("super-user-command", "sudo"); //sudo does not accept signals from user so we except it if (superUserCommand == "sudo") { return true; } 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) { kError(1512) << "waitpid():" << ::strerror(errno); return Error; } if (ret == pid) { if (WIFEXITED(state)) { return WEXITSTATUS(state); } return Killed; } return NotExited; } class PtyProcess::PtyProcessPrivate { public: PtyProcessPrivate() : m_pPTY(0L) { } ~PtyProcessPrivate() { delete m_pPTY; } QList env; KPty *m_pPTY; QByteArray m_Inbuf; }; PtyProcess::PtyProcess() : d(new PtyProcessPrivate()) { m_bTerminal = false; m_bErase = false; } int PtyProcess::init() { delete d->m_pPTY; d->m_pPTY = new KPty(); if (!d->m_pPTY->open()) { kError(1512) << "Failed to open PTY."; return -1; } d->m_Inbuf.resize(0); return 0; } PtyProcess::~PtyProcess() { delete d; } /** Set additional environment variables. */ void PtyProcess::setEnvironment(const QList &env) { d->env = env; } int PtyProcess::fd() const { if (d->m_pPTY) { return d->m_pPTY->masterFd(); } return -1; } int PtyProcess::pid() const { return m_Pid; } /** Returns the additional environment variables set by setEnvironment() */ QList PtyProcess::environment() const { return d->env; } QByteArray PtyProcess::readAll(bool block) { QByteArray ret; if (!d->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 = d->m_Inbuf; d->m_Inbuf.resize(0); } int flags = fcntl(fd(), F_GETFL); if (flags < 0) { kError(1512) << "fcntl(F_GETFL):" << ::strerror(errno); return ret; } int oflags = flags; if (block) { flags &= ~O_NONBLOCK; } else { flags |= O_NONBLOCK; } if ((flags != oflags) && (fcntl(fd(), F_SETFL, flags) < 0)) { // We get an error here when the child process has closed // the file descriptor already. return ret; } while (1) { ret.reserve(ret.size() + 0x8000); int nbytes = read(fd(), ret.data() + ret.size(), 0x8000); if (nbytes == -1) { if (errno == EINTR) { continue; } else { break; } } if (nbytes == 0) { // nothing available / eof break; } ret.resize(ret.size() + nbytes); break; } return ret; } QByteArray PtyProcess::readLine(bool block) { d->m_Inbuf = readAll(block); QByteArray ret; if (!d->m_Inbuf.isEmpty()) { int pos = d->m_Inbuf.indexOf('\n'); if (pos == -1) { // NOTE: this means we return something even if there in no full line! ret = d->m_Inbuf; d->m_Inbuf.resize(0); } else { ret = d->m_Inbuf.left(pos); d->m_Inbuf.remove(0, pos+1); } } return ret; } void PtyProcess::writeLine(const QByteArray &line, bool addnl) { if (!line.isEmpty()) { write(fd(), line, line.length()); } if (addnl) { write(fd(), "\n", 1); } } void PtyProcess::unreadLine(const QByteArray &line, bool addnl) { QByteArray tmp = line; if (addnl) { tmp += '\n'; } if (!tmp.isEmpty()) { d->m_Inbuf.prepend(tmp); } } void PtyProcess::setExitString(const QByteArray &exit) { m_Exit = exit; } /* * Fork and execute the command. This returns in the parent. */ int PtyProcess::exec(const QByteArray &command, const QList &args) { kDebug(1512) << "Running" << command; if (init() < 0) return -1; if ((m_Pid = fork()) == -1) { kError(1512) << "fork():" << ::strerror(errno); return -1; } // Parent if (m_Pid) { d->m_pPTY->closeSlave(); return 0; } // Child if (setupTTY() < 0) { _exit(1); } for (int i = 0; i < d->env.count(); ++i) { putenv(const_cast(d->env.at(i).constData())); } unsetenv("KDE_FULL_SESSION"); // for : Qt: Session management error unsetenv("SESSION_MANAGER"); // QMutex::lock , deadlocks without that. // you cannot connect to the user's session bus from another UID unsetenv("DBUS_SESSION_BUS_ADDRESS"); // set temporarily LC_ALL to C, for su (to be able to parse "Password:") const QByteArray old_lc_all = qgetenv("LC_ALL"); if (!old_lc_all.isEmpty()) { qputenv("KDESU_LC_ALL", old_lc_all); } else { unsetenv("KDESU_LC_ALL"); } qputenv("LC_ALL", "C"); // From now on, terminal output goes through the tty. QByteArray path; if (command.contains('/')) { path = command; } else { QString file = KStandardDirs::findExe(command); if (file.isEmpty()) { kError(1512) << command << "not found."; _exit(1); } path = QFile::encodeName(file); } const char **argp = (const char **)malloc((args.count()+2)*sizeof(char *)); int i = 1; argp[i] = path; foreach (QByteArray it, args) { argp[i] = it; i++; } argp[i] = NULL; execv(path, const_cast(argp)); free(argp); kError(1512) << "execv(" << path << "):" << ::strerror(errno); _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() { kDebug(1512) << "Child pid" << m_Pid; struct termios tio; while (1) { if (!checkPid(m_Pid)) { kError(1512) << "process has exited while waiting for password."; return -1; } if (!d->m_pPTY->tcGetAttr(&tio)) { kError(1512) << "tcgetattr():" << ::strerror(errno); return -1; } if (tio.c_lflag & ECHO) { kDebug(1512) << "Echo mode still on."; usleep(10000); continue; } break; } return 0; } int PtyProcess::enableLocalEcho(bool enable) { return d->m_pPTY->setEcho(enable) ? 0 : -1; } void PtyProcess::setTerminal(bool terminal) { m_bTerminal = terminal; } void PtyProcess::setErase(bool erase) { m_bErase = erase; } /* * Copy output to stdout until the child process exits, 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() { fd_set fds; FD_ZERO(&fds); QByteArray remainder; while (1) { FD_SET(fd(), &fds); // specify timeout to make sure select() does not block, even if the // process is dead / non-responsive. It does not matter if we abort too // early. In that case 0 is returned, and we'll try again in the next // iteration. (As long as we don't consitently time out in each iteration) timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 100000; int ret = select(fd()+1, &fds, 0L, 0L, &timeout); if (ret == -1) { if (errno != EINTR) { kError(1512) << "select():" << ::strerror(errno); return -1; } ret = 0; } if (ret) { forever { QByteArray output = readAll(false); if (output.isEmpty()) { break; } if (m_bTerminal) { fwrite(output.constData(), output.size(), 1, stdout); fflush(stdout); } if (!m_Exit.isEmpty()) { // match exit string only at line starts remainder += output; while (remainder.length() >= m_Exit.length()) { if (remainder.startsWith(m_Exit)) { kill(m_Pid, SIGTERM); remainder.remove(0, m_Exit.length()); } int off = remainder.indexOf('\n'); if (off < 0) { break; } remainder.remove(0, off + 1); } } } } ret = checkPidExited(m_Pid); if (ret == Error) { if (errno == ECHILD) { return 0; } return 1; } else if (ret == Killed) { return 0; } else if (ret == NotExited) { // keep checking } else { return ret; } } } /* * 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() { // Reset signal handlers for (int sig = 1; sig < NSIG; sig++) { KDE_signal(sig, SIG_DFL); } KDE_signal(SIGHUP, SIG_IGN); d->m_pPTY->setCTty(); // Connect stdin, stdout and stderr int slave = d->m_pPTY->slaveFd(); dup2(slave, 0); dup2(slave, 1); dup2(slave, 2); // Close all file handles // XXX this caused problems in KProcess - not sure why anymore. -- ??? // Because it will close the start notification pipe. -- ossi struct rlimit rlp; getrlimit(RLIMIT_NOFILE, &rlp); for (int i = 3; i < (int)rlp.rlim_cur; i++) { close(i); } // Disable OPOST processing. Otherwise, '\n' are (on Linux at least) // translated to '\r\n'. struct ::termios tio; if (tcgetattr(0, &tio) < 0) { kError(1512) << "tcgetattr():" << ::strerror(errno); return -1; } tio.c_oflag &= ~OPOST; if (tcsetattr(0, TCSANOW, &tio) < 0) { kError(1512) << "tcsetattr():" << ::strerror(errno); return -1; } return 0; }