/* vi: ts=8 sts=4 sw=4 * * $Id$ * * This file is part of the KDE project, module tdesu. * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__SVR4) && defined(sun) #include #include #endif #ifdef HAVE_SYS_SELECT_H #include // Needed on some systems. #endif #include #include #include #include #include #include #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) { TDEConfig* config = TDEGlobal::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(*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( "TDESU_LC_ALL", old_lc_all, 1 ); else unsetenv( "TDESU_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 = TDEStandardDirs::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) { int ret = 0; if (m_Fd != -1) { FD_SET(m_Fd, &fds); 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.at( 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 );*/ }