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.
505 lines
13 KiB
505 lines
13 KiB
/* -------------------------------------------------------------------------- */ |
|
/* */ |
|
/* [TEPty.C] Pseudo Terminal Device */ |
|
/* */ |
|
/* -------------------------------------------------------------------------- */ |
|
/* */ |
|
/* Copyright (c) 1997,1998 by Lars Doelle <lars.doelle@on-line.de> */ |
|
/* */ |
|
/* This file is part of Konsole - an X terminal for KDE */ |
|
/* -------------------------------------------------------------------------- */ |
|
|
|
/*! \file |
|
*/ |
|
|
|
/*! \class TEPty |
|
|
|
\brief Ptys provide a pseudo terminal connection to a program. |
|
|
|
Although closely related to pipes, these pseudo terminal connections have |
|
some ability, that makes it nessesary to uses them. Most importent, they |
|
know about changing screen sizes and UNIX job control. |
|
|
|
Within the terminal emulation framework, this class represents the |
|
host side of the terminal together with the connecting serial line. |
|
|
|
One can create many instances of this class within a program. |
|
As a side effect of using this class, a signal(2) handler is |
|
installed on SIGCHLD. |
|
|
|
\par FIXME |
|
|
|
[NOTE: much of the technical stuff below will be replaced by forkpty.] |
|
|
|
publish the SIGCHLD signal if not related to an instance. |
|
|
|
clearify TEPty::done vs. TEPty::~TEPty semantics. |
|
check if pty is restartable via run after done. |
|
|
|
\par Pseudo terminals |
|
|
|
Pseudo terminals are a unique feature of UNIX, and always come in form of |
|
pairs of devices (/dev/ptyXX and /dev/ttyXX), which are connected to each |
|
other by the operating system. One may think of them as two serial devices |
|
linked by a null-modem cable. Being based on devices the number of |
|
simultanous instances of this class is (globally) limited by the number of |
|
those device pairs, which is 256. |
|
|
|
Another technic are UNIX 98 PTY's. These are supported also, and prefered |
|
over the (obsolete) predecessor. |
|
|
|
There's a sinister ioctl(2), signal(2) and job control stuff |
|
nessesary to make everything work as it should. |
|
|
|
Much of the stuff can be simplified by using openpty from glibc2. |
|
Compatibility issues with obsolete installations and other unixes |
|
my prevent this. |
|
*/ |
|
|
|
#ifdef HAVE_CONFIG_H |
|
#include <config.h> |
|
#endif |
|
|
|
#include <stdlib.h> |
|
#include <stdio.h> |
|
|
|
#include <sys/types.h> |
|
#include <sys/ioctl.h> |
|
#ifdef TIME_WITH_SYS_TIME |
|
#include <sys/time.h> |
|
#endif |
|
#include <sys/resource.h> |
|
#ifdef HAVE_SYS_STROPTS_H |
|
#include <sys/stropts.h> |
|
#define _NEW_TTY_CTRL |
|
#endif |
|
#include <sys/wait.h> |
|
|
|
#ifdef HAVE_UTEMPTER |
|
extern "C" { |
|
#include <utempter.h> |
|
} |
|
#endif |
|
|
|
|
|
#include <assert.h> |
|
#include <fcntl.h> |
|
#include <grp.h> |
|
|
|
#include <signal.h> |
|
#ifdef HAVE_TERMIO_H |
|
/* needed at least on AIX */ |
|
#include <termio.h> |
|
#endif |
|
#ifdef HAVE_TERMIOS_H |
|
#include <termios.h> |
|
#endif |
|
//#include <time.h> |
|
#include <unistd.h> |
|
|
|
#if defined (_HPUX_SOURCE) |
|
#define _TERMIOS_INCLUDED |
|
#include <bsdtty.h> |
|
#endif |
|
|
|
#ifdef __sgi__ |
|
#define SVR4 |
|
#endif |
|
|
|
#include <qintdict.h> |
|
#include <qstring.h> |
|
#include <sys/wait.h> |
|
#include <sys/stat.h> |
|
|
|
#include "TEPty.h" |
|
#include "TEPty.moc" |
|
|
|
|
|
#include <kapp.h> |
|
#include <kglobal.h> |
|
#include <kstddirs.h> |
|
#include <kdebug.h> |
|
#include "iostream.h" |
|
|
|
#ifndef HERE |
|
#define HERE fprintf(stdout,"%s(%d): here\n",__FILE__,__LINE__) |
|
#endif |
|
|
|
template class QIntDict<TEPty>; |
|
|
|
FILE* syslog_file = NULL; //stdout; |
|
|
|
static QIntDict<TEPty> * ptys = 0L; |
|
|
|
#define PTY_FILENO 3 |
|
#define BASE_CHOWN "konsole_grantpty" |
|
|
|
int chownpty(int fd, int grant) |
|
// param fd: the fd of a master pty. |
|
// param grant: 1 to grant, 0 to revoke |
|
// returns 1 on success 0 on fail |
|
{ |
|
void(*tmp)(int) = signal(SIGCHLD,SIG_DFL); |
|
pid_t pid = fork(); |
|
if (pid < 0) |
|
{ |
|
signal(SIGCHLD,tmp); |
|
return 0; |
|
} |
|
if (pid == 0) |
|
{ |
|
/* We pass the master pseudo terminal as file descriptor PTY_FILENO. */ |
|
if (fd != PTY_FILENO && dup2(fd, PTY_FILENO) < 0) exit(1); |
|
QString path = locate("exe", BASE_CHOWN); |
|
execle(path.ascii(), BASE_CHOWN, grant?"--grant":"--revoke", NULL, NULL); |
|
exit(1); // should not be reached |
|
} |
|
if (pid > 0) |
|
{ int w; |
|
retry: |
|
int rc = waitpid (pid, &w, 0); |
|
if (rc != pid) |
|
{ // signal from other child, behave like catchChild. |
|
// guess this gives quite some control chaos... |
|
TEPty* sh = ptys->find(rc); |
|
if (sh) { ptys->remove(rc); sh->donePty(w); } |
|
goto retry; |
|
} |
|
signal(SIGCHLD,tmp); |
|
return (rc != -1 && WIFEXITED(w) && WEXITSTATUS(w) == 0); |
|
} |
|
signal(SIGCHLD,tmp); |
|
return 0; //dummy. |
|
} |
|
|
|
/* -------------------------------------------------------------------------- */ |
|
|
|
/*! |
|
Informs the client program about the |
|
actual size of the window. |
|
*/ |
|
|
|
void TEPty::setSize(int lines, int columns) |
|
{ |
|
wsize.ws_row = (unsigned short)lines; |
|
wsize.ws_col = (unsigned short)columns; |
|
if(fd < 0) return; |
|
ioctl(fd,TIOCSWINSZ,(char *)&wsize); |
|
} |
|
|
|
//! Catch a SIGCHLD signal and propagate that the child died. |
|
void catchChild(int) |
|
{ int status; |
|
pid_t pid = waitpid(-1,&status,WNOHANG); |
|
TEPty* sh = ptys->find(pid); |
|
if (sh) { ptys->remove(pid); sh->donePty(status); } |
|
} |
|
|
|
void TEPty::donePty(int status) |
|
{ |
|
#ifdef HAVE_UTEMPTER |
|
removeLineFromUtmp(ttynam, fd); |
|
#endif |
|
if (needGrantPty) chownpty(fd,FALSE); |
|
emit done(status); |
|
} |
|
|
|
|
|
const char* TEPty::deviceName() |
|
{ |
|
return ttynam; |
|
} |
|
|
|
/*! |
|
start the client program. |
|
*/ |
|
int TEPty::run(const char* pgm, QStrList & args, const char* term, int addutmp) |
|
{ |
|
if (!ptys) |
|
ptys = new QIntDict<TEPty>; |
|
|
|
comm_pid = fork(); |
|
if (comm_pid < 0) { fprintf(stderr,"Can't fork\n"); return -1; } |
|
if (comm_pid == 0) makePty(ttynam,pgm,args,term,addutmp); |
|
if (comm_pid > 0) ptys->insert(comm_pid,this); |
|
return 0; |
|
} |
|
|
|
int TEPty::openPty() |
|
{ int ptyfd = -1; |
|
needGrantPty = TRUE; |
|
|
|
// Find a master pty that we can open //////////////////////////////// |
|
|
|
#ifdef __sgi__ |
|
ptyfd = open("/dev/ptmx",O_RDWR); |
|
if (ptyfd < 0) |
|
{ |
|
perror("Can't open a pseudo teletype"); |
|
return(-1); |
|
} |
|
strncpy(ttynam, ptsname(ptyfd), 50); |
|
grantpt(ptyfd); |
|
unlockpt(ptyfd); |
|
needGrantPty = FALSE; |
|
#endif |
|
|
|
// first we try UNIX PTY's |
|
#ifdef TIOCGPTN |
|
strcpy(ptynam,"/dev/ptmx"); |
|
strcpy(ttynam,"/dev/pts/"); |
|
ptyfd = open(ptynam,O_RDWR); |
|
if (ptyfd >= 0) // got the master pty |
|
{ int ptyno; |
|
if (ioctl(ptyfd, TIOCGPTN, &ptyno) == 0) |
|
{ struct stat sbuf; |
|
sprintf(ttynam,"/dev/pts/%d",ptyno); |
|
if (stat(ttynam,&sbuf) == 0 && S_ISCHR(sbuf.st_mode)) |
|
needGrantPty = FALSE; |
|
else |
|
{ |
|
close(ptyfd); |
|
ptyfd = -1; |
|
} |
|
} |
|
else |
|
{ |
|
close(ptyfd); |
|
ptyfd = -1; |
|
} |
|
} |
|
#endif |
|
|
|
#if defined(_SCO_DS) || defined(__USLC__) /* SCO OSr5 and UnixWare */ |
|
if (ptyfd < 0) |
|
{ for (int idx = 0; idx < 256; idx++) |
|
{ sprintf(ptynam, "/dev/ptyp%d", idx); |
|
sprintf(ttynam, "/dev/ttyp%d", idx); |
|
if (access(ttynam, F_OK) < 0) { idx = 256; break; } |
|
if ((ptyfd = open (ptynam, O_RDWR)) >= 0) |
|
{ if (access (ttynam, R_OK|W_OK) == 0) break; |
|
close(ptyfd); ptyfd = -1; |
|
} |
|
} |
|
} |
|
#endif |
|
if (ptyfd < 0) // Linux, FIXME: Trouble on other systems? |
|
{ for (const char* s3 = "pqrstuvwxyzabcdefghijklmno"; *s3 != 0; s3++) |
|
{ for (const char* s4 = "0123456789abcdefghijklmnopqrstuvwxyz"; *s4 != 0; s4++) |
|
{ sprintf(ptynam,"/dev/pty%c%c",*s3,*s4); |
|
sprintf(ttynam,"/dev/tty%c%c",*s3,*s4); |
|
if ((ptyfd = open(ptynam,O_RDWR)) >= 0) |
|
{ if (geteuid() == 0 || access(ttynam,R_OK|W_OK) == 0) break; |
|
close(ptyfd); ptyfd = -1; |
|
} |
|
} |
|
if (ptyfd >= 0) break; |
|
} |
|
} |
|
|
|
if (ptyfd < 0) |
|
{ |
|
//FIXME: handle more gracefully. |
|
fprintf(stderr,"Can't open a pseudo teletype\n"); exit(1); |
|
} |
|
if (needGrantPty && !chownpty(ptyfd,TRUE)) |
|
{ |
|
fprintf(stderr,"konsole: chownpty failed for device %s::%s.\n",ptynam,ttynam); |
|
fprintf(stderr," : This means the session can be eavesdroped.\n"); |
|
fprintf(stderr," : Make sure konsole_grantpty is installed in\n"); |
|
fprintf(stderr," : %s and setuid root.\n", |
|
KGlobal::dirs()->findResourceDir("exe", "konsole").data()); |
|
} |
|
fcntl(ptyfd,F_SETFL,O_NDELAY); |
|
|
|
return ptyfd; |
|
} |
|
|
|
//! only used internally. See `run' for interface |
|
void TEPty::makePty(const char* dev, const char* pgm, QStrList & args, const char* term, int addutmp) |
|
{ int sig; |
|
// kdDebug() << "Making new terminal " << pgm << "\n"; |
|
if (fd < 0) // no master pty could be opened |
|
{ |
|
//FIXME: |
|
fprintf(stderr,"opening master pty failed.\n"); |
|
exit(1); |
|
} |
|
|
|
#ifdef TIOCSPTLCK |
|
int flag = 0; ioctl(fd,TIOCSPTLCK,&flag); // unlock pty |
|
#endif |
|
// open and set all standard files to slave tty |
|
int tt = open(dev, O_RDWR); |
|
|
|
if (tt < 0) // the slave pty could be opened |
|
{ |
|
//FIXME: |
|
//fprintf(stderr,"opening slave pty (%s) failed.\n",dev); |
|
//exit(1); |
|
} |
|
|
|
#if (defined(SVR4) || defined(__SVR4)) && (defined(i386) || defined(__i386__) || defined(__sgi__)) |
|
// Solaris x86 |
|
ioctl(tt, I_PUSH, "ptem"); |
|
ioctl(tt, I_PUSH, "ldterm"); |
|
#endif |
|
|
|
// Stamp utmp/wtmp if we have and want them |
|
#ifdef HAVE_UTEMPTER |
|
if (addutmp) addToUtmp(dev, "", fd); |
|
#endif |
|
|
|
//reset signal handlers for child process |
|
for (sig = 1; sig < NSIG; sig++) signal(sig,SIG_DFL); |
|
|
|
// Don't know why, but his is vital for SIGHUP to find the child. |
|
// Could be, we get rid of the controling terminal by this. |
|
// getrlimit is a getdtablesize() equivalent, more portable (David Faure) |
|
struct rlimit rlp; |
|
getrlimit(RLIMIT_NOFILE, &rlp); |
|
for (int i = 0; i < (int)rlp.rlim_cur; i++) |
|
if (i != tt && i != fd) close(i); //FIXME: (result of merge) Check if not closing fd is OK) |
|
|
|
dup2(tt,fileno(stdin)); |
|
dup2(tt,fileno(stdout)); |
|
dup2(tt,fileno(stderr)); |
|
|
|
if (tt > 2) close(tt); |
|
|
|
// Setup job control ////////////////////////////////// |
|
|
|
// This is pretty obscure stuff which makes the session |
|
// to be the controlling terminal of a process group. |
|
|
|
if (setsid() < 0) perror("failed to set process group"); // (vital for bash) |
|
|
|
#if defined(TIOCSCTTY) |
|
ioctl(0, TIOCSCTTY, 0); |
|
#endif |
|
|
|
int pgrp = getpid(); // This sequence is necessary for |
|
ioctl(0, TIOCSPGRP, (char *)&pgrp); // event propagation. Omitting this |
|
setpgid(0,0); // is not noticeable with all |
|
close(open(dev, O_WRONLY, 0)); // clients (bash,vi). Because bash |
|
setpgid(0,0); // heals this, use '-e' to test it. |
|
|
|
static struct termios ttmode; |
|
#undef CTRL |
|
#define CTRL(c) ((c) - '@') |
|
|
|
// #ifdef SVR4 |
|
// #define CINTR 0177 |
|
// #define CQUIT CTRL('U') |
|
// #define CERASE CTRL('H') |
|
// // #else |
|
// #define CINTR CTRL('C') |
|
// #define CQUIT CTRL('\\') |
|
// #define CERASE 0177 |
|
// #endif |
|
|
|
#if defined (__FreeBSD__) || (__NetBSD__) || defined(__bsdi__) |
|
ioctl(0,TIOCGETA,(char *)&ttmode); |
|
#else |
|
# if defined (_HPUX_SOURCE) || defined(__Lynx__) |
|
tcgetattr(0, &ttmode); |
|
# else |
|
ioctl(0,TCGETS,(char *)&ttmode); |
|
# endif |
|
#endif |
|
ttmode.c_cc[VINTR] = CTRL('C'); |
|
ttmode.c_cc[VQUIT] = CTRL('\\'); |
|
ttmode.c_cc[VERASE] = 0177; |
|
#if defined (__FreeBSD__) || (__NetBSD__) || defined(__bsdi__) |
|
ioctl(0,TIOCSETA,(char *)&ttmode); |
|
#else |
|
# ifdef _HPUX_SOURCE |
|
tcsetattr(0, TCSANOW, &ttmode); |
|
# else |
|
ioctl(0,TCSETS,(char *)&ttmode); |
|
# endif |
|
#endif |
|
|
|
close(fd); |
|
|
|
// drop privileges |
|
setuid(getuid()); setgid(getgid()); |
|
|
|
// propagate emulation |
|
if (term && term[0]) setenv("TERM",term,1); |
|
|
|
// convert QStrList into char*[] |
|
unsigned int i; |
|
char **argv = (char**)malloc(sizeof(char*)*(args.count()+1)); |
|
for (i = 0; i<args.count(); i++) argv[i] = strdup(args.at(i)); |
|
argv[i] = 0L; |
|
|
|
ioctl(0,TIOCSWINSZ,(char *)&wsize); // set screen size |
|
|
|
// finally, pass to the new program |
|
// kdDebug() << "We are ready to run the program " << pgm << "\n"; |
|
execvp(pgm, argv); |
|
perror("exec failed"); |
|
exit(1); // control should never come here. |
|
} |
|
|
|
/*! |
|
Create an instance. |
|
*/ |
|
TEPty::TEPty() |
|
{ |
|
fd = openPty(); |
|
|
|
signal(SIGCHLD,catchChild); |
|
|
|
mn = new QSocketNotifier(fd, QSocketNotifier::Read); |
|
connect( mn, SIGNAL(activated(int)), this, SLOT(DataReceived(int)) ); |
|
} |
|
|
|
/*! |
|
Destructor. |
|
Note that the related client program is not killed |
|
(yet) when a instance is deleted. |
|
*/ |
|
TEPty::~TEPty() |
|
{ |
|
delete mn; |
|
close(fd); |
|
} |
|
|
|
/*! send signal to child */ |
|
void TEPty::kill(int signal) |
|
{ |
|
::kill(comm_pid,signal); |
|
} |
|
|
|
/*! sends a character through the line */ |
|
void TEPty::send_byte(char c) |
|
{ |
|
write(fd,&c,1); |
|
} |
|
|
|
/*! sends a 0 terminated string through the line */ |
|
void TEPty::send_string(const char* s) |
|
{ |
|
write(fd,s,strlen(s)); |
|
} |
|
|
|
/*! sends len bytes through the line */ |
|
void TEPty::send_bytes(const char* s, int len) |
|
{ |
|
write(fd,s,len); |
|
} |
|
|
|
/*! indicates that a block of data is received */ |
|
void TEPty::DataReceived(int) |
|
{ char buf[4096]; |
|
int n = read(fd, buf, 4096); |
|
emit block_in(buf,n); |
|
if (syslog_file) // if (debugging) ... |
|
{ |
|
int i; |
|
for (i = 0; i < n; i++) fputc(buf[i],syslog_file); |
|
fflush(syslog_file); |
|
} |
|
}
|
|
|