How do *nix pseudo-terminals work ? What's the master/slave channel?
I want to write a simple, dumb, X terminal emulator in C on a Linux system.
At first, I just thought I would have to popen a shell and display its output. I checked xterm and rxvt code, and it looks a bit more complicated.
First, I have to open a pseudo-terminal with openpty. So I look at the man page and see that openpty fills 2 file descriptors, the master and the slave. Both xterm and rxvt code are messy because of the system-dependent-ness of those specials files.
I understand the termios stuff : it's just a bunch of information about the escape code of the terminal. What I really don't get is : what am I suppose to do with the master/slave file descriptor ?
An example program which open a terminal, logs in, executes a "ls" on the shell would be awesome.
(English is not my native language, excuse my eventual mistake)
Edit: Here's the sample code I came up with :
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pty.h>
#include <utmp.h>
#include <ctype.h>
void
safe_print (char* s)
{
while(*s) {
if(*s == '\n')
putchar("\n");
else if(iscntrl(*s))
printf("\\e(%d)", *s);
else
putchar(*s);
s++;
}
}
int
main (int argc, char** argv)
{
char buf[BUFSIZ] = {0};
int master;
int ret = forkpty(&master, NULL, NULL, NULL);
if(ret == -1)
puts("no fork"), exit(0);
if(!ret) {
execl("/bin/sh", "sh", NULL);
exit(0);
}
sleep(1); /* let the shell run */
if(argc >= 2) {
write(master, argv[1], strlen(argv[1]));
write(master, "\n", 1);
} else {
write(master, "date\n", sizeof "date\n");
}
while(1) {
switch(ret = read(master, buf, BUFSIZ)) {
case -1:
puts("error!");
exit(1);
break;
case 0:
puts("nothing.."), sleep(1);
break;
default:
buf[ret] = '\0';
safe_print(buf);
}
}
close(master);
return 0;
}
Solution 1:
With respect to the master/slave part of your question, from the pty(4) man page (which is referenced from the openpty(3) man page on my system):
A pseudo terminal is a pair of character devices, a master device and a slave device. The slave device provides to a process an interface identical to that described in tty(4). However, whereas all other devices which provide the interface described in tty(4) have a hardware device of some sort behind them, the slave device has, instead, another process manipulating it through the master half of the pseudo terminal. That is, anything written on the master device is given to the slave device as input and anything written on the slave device is presented as input on the master device.
Man pages are your friends.
Solution 2:
I just tried the examples found on this tutorial, they work very fine for me and I think they are an interesting starting point for the problem.
EDIT: The tutorial explain briefly the pseudo-terminals function. The explanation is done step by step and is followed by examples.
The following example show how to create a new pseudo-terminal, and fork the process in two parts, one writing on the master side of the pseudo-terminal, the other reading from the slave side of the pseudo-terminal.
#define _XOPEN_SOURCE 600
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#define __USE_BSD
#include <termios.h>
int main(void)
{
int fdm, fds, rc;
char input[150];
fdm = posix_openpt(O_RDWR);
if (fdm < 0)
{
fprintf(stderr, "Error %d on posix_openpt()\n", errno);
return 1;
}
rc = grantpt(fdm);
if (rc != 0)
{
fprintf(stderr, "Error %d on grantpt()\n", errno);
return 1;
}
rc = unlockpt(fdm);
if (rc != 0)
{
fprintf(stderr, "Error %d on unlockpt()\n", errno);
return 1;
}
// Open the slave PTY
fds = open(ptsname(fdm), O_RDWR);
printf("Virtual interface configured\n");
printf("The master side is named : %s\n", ptsname(fdm));
// Creation of a child process
if (fork())
{
// Father
// Close the slave side of the PTY
close(fds);
while (1)
{
// Operator's entry (standard input = terminal)
write(1, "Input : ", sizeof("Input : "));
rc = read(0, input, sizeof(input));
if (rc > 0)
{
// Send the input to the child process through the PTY
write(fdm, input, rc);
// Get the child's answer through the PTY
rc = read(fdm, input, sizeof(input) - 1);
if (rc > 0)
{
// Make the answer NUL terminated to display it as a string
input[rc] = '\0';
fprintf(stderr, "%s", input);
}
else
{
break;
}
}
else
{
break;
}
} // End while
}
else
{
struct termios slave_orig_term_settings; // Saved terminal settings
struct termios new_term_settings; // Current terminal settings
// Child
// Close the master side of the PTY
close(fdm);
// Save the default parameters of the slave side of the PTY
rc = tcgetattr(fds, &slave_orig_term_settings);
// Set raw mode on the slave side of the PTY
new_term_settings = slave_orig_term_settings;
cfmakeraw (&new_term_settings);
tcsetattr (fds, TCSANOW, &new_term_settings);
// The slave side of the PTY becomes the standard input and outputs of the child process
close(0); // Close standard input (current terminal)
close(1); // Close standard output (current terminal)
close(2); // Close standard error (current terminal)
dup(fds); // PTY becomes standard input (0)
dup(fds); // PTY becomes standard output (1)
dup(fds); // PTY becomes standard error (2)
while (1)
{
rc = read(fds, input, sizeof(input) - 1);
if (rc > 0)
{
// Replace the terminating \n by a NUL to display it as a string
input[rc - 1] = '\0';
printf("Child received : '%s'\n", input);
}
else
{
break;
}
} // End while
}
return 0;
} // main