LinuxQuestions.org
Share your knowledge at the LQ Wiki.
Home Forums Tutorials Articles Register
Go Back   LinuxQuestions.org > Forums > Non-*NIX Forums > Programming
User Name
Password
Programming This forum is for all programming questions.
The question does not have to be directly related to Linux and any language is fair game.

Notices


Reply
  Search this Thread
Old 09-01-2011, 06:21 PM   #1
damix
LQ Newbie
 
Registered: Sep 2011
Posts: 4

Rep: Reputation: 1
Post Theory of Terminal Emulators


Hello everybody, I'm not a very experienced *NIX programmer and just yesterday I decided to explore a little more in detail the relationship between shells and terminal emulators. I had some difficulties in finding good theoretical information on this topic, so I decided to try to figure out how an application (the TE) could interact bidirectionally with another application (the shell). I downloaded the code of rxvt and I skimmed very fast through what I thought would be the relevant sections (I was mainly interested in the communication part, not in the details of different terminal standards like VT100 and VT220). After studying the code and also reading some other stuff about UNIX system calls, I came up with this simple program that seems to be working (C++, I'm a C++ guy). It is basically a console application that hosts /bin/bash, and acts as a filter between it and the user. To me it looks like a terminal emulation window without window. I used pipe(), fork(), execlp(), kill(), select(), read(), write(), dup2(). Now, I'm wondering, is the inter-process communication handled like this in the famous TEs like xterm, GNOME Terminal and Konsole, or they are based on something radically different? I wrote this code because I was not able to find a good didactical example. I hope it is someway useful. If you find it useful you can let me know :-) I would be quite happy about that.

Bye, have a nice day and happy coding.

Dario

Code:
// C++ includes
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <csignal>
#include <cassert>
#include <vector>

// UNIX includes
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>

int main(int argc, char *argv[])
{
	// This pipe is written by the TE and read by
	// the executed shell as STDIN.
	int pipeTEtoSH[2];
	
	// This pipe is written by the executed shell
	// as STDOUT and read by the TE.
	int pipeSHtoTE[2];
	
	// This pipe is written by the executed shell
	// as STDERR and read by the TE.
	int pipeSHtoTEerr[2];
	
	// Instantiation of the 3 pipes.
	int r1 = pipe(pipeTEtoSH);
	int r2 = pipe(pipeSHtoTE);
	int r3 = pipe(pipeSHtoTEerr);
	
	// Check that all the pipes were successfully
	// instantiated.
	if (r1 == -1 || r2 == -1 || r3 == -1)
	{
		std::cout << "Can not pipe." << std::endl;
		return -1;
	}
	
	// The shell will be run as a separate process.
	int pid = fork();
	
	// Check that a child process was created.
	if (pid == -1)
	{
		std::cout << "Can not fork." << std::endl;
		return -1;
	}
	
	// From now on the same source code will be
	// shared by two distinct processes; in the
	// parent process fork() returns the PID of
	// the child, while in the child process
	// it returns 0.
	
	if (pid != 0)
	{
		// The code in this if will be executed by the parent.
		// This is the TE logic.
		
		// TE receives data from the shell one char at a time.
		char c;
		
		// TE sends entire commands, possibly containing
		// whitespaces, to the shell.
		std::string cmd;
		
		do
		{
			// Terminal emulator prompt.
			std::cout << "§ " << std::flush;
			std::getline(std::cin, cmd);
			
			if (cmd == "exit")
			{
				// We don't want to execute exit on the shell,
				// because this would terminate the child
				// process while the parent process, the TE,
				// would keep running. Instead, we display an
				// error message.
				
				std::cout << "Emulated terminal - use 'stop' instead." << std::endl;
			}
			else if (cmd == "info")
			{
				// This is a command implemented by the TE; it
				// simply prints an informative message about
				// the terminal emulator.
				
				std::cout << "Damix's Terminal Emulator, 2011." << std::endl;
			}
			else if (cmd != "stop")
			{
				// Any other command different from stop is
				// sent to the shell using the appropriate
				// pipe. The command is terminated by a newline.
				write(pipeTEtoSH[1], cmd.c_str(), cmd.size());
				write(pipeTEtoSH[1], "\n", 1);
				
				// Data structures needed for select() and
				// non-blocking I/O.
				fd_set rfd;
				fd_set wfd;
				fd_set efd;
				timeval tv;

				do {
					// Wait for data available on either the
					// STDOUT pipe or the STDERR pipe.
					FD_ZERO(&rfd);
					FD_ZERO(&wfd);
					FD_ZERO(&efd);
					FD_SET(pipeSHtoTE[0], &rfd);
					FD_SET(pipeSHtoTEerr[0], &rfd);
					tv.tv_sec = 0;
					tv.tv_usec = 100000;
					select(100, &rfd, &wfd, &efd, &tv);
					
					// Check for data on the STDOUT pipe; the
					// executed console will write most of the
					// output on this pipe.
					if (FD_ISSET(pipeSHtoTE[0], &rfd))
					{
						read(pipeSHtoTE[0], &c, 1);
						std::cout << c << std::flush;
					}
					
					// Check for data on the STDERR pipe; the
					// executed console will write error messages
					// on this pipe (for instance the error message
					// that results from typing an unexisting command.
					// Just for display purposes, the STDERR messages
					// will be shown in upper case.
					if (FD_ISSET(pipeSHtoTEerr[0], &rfd))
					{
						read(pipeSHtoTEerr[0], &c, 1);
						std::cout << (char)toupper(c) << std::flush;
					}
					
					// If no data is detected for more than 1/10 of a
					// second, reading is aborted and the TE prompts
					// the user again. The timeout is specified by
					// the tv.tv_sec and tv.tv_usec.
				} while (FD_ISSET(pipeSHtoTE[0], &rfd) || FD_ISSET(pipeSHtoTEerr[0], &rfd));
			}
			
			// Command stop is interpreted by the TE and
			// results in the TE stopping prompting the
			// user and proceed to kill the shell and
			// itself.
		} while (cmd != "stop");

		// Parent TE process kills the child shell.
		kill(pid, SIGKILL);
	}
	else
	{
		// The code in this else will be executed by the child.
		// This is the shell logic.
		
		// Redirect STDIN.
		assert(dup2(pipeTEtoSH[0], 0) == 0);
		
		// Redirect STDOUT.
		assert(dup2(pipeSHtoTE[1], 1) == 1);
		
		// Redirect STDERR.
		assert(dup2(pipeSHtoTEerr[1], 2) == 2);
		
		// The executed shell is bash.
		execlp("bash", "/bin/bash", NULL);
	}
	
	// Everything went fine.
	return 0;
}

Last edited by damix; 09-02-2011 at 02:22 PM.
 
Old 09-01-2011, 06:52 PM   #2
ta0kira
Senior Member
 
Registered: Sep 2004
Distribution: FreeBSD 9.1, Kubuntu 12.10
Posts: 3,078

Rep: Reputation: Disabled
Sessions, process groups, and the controlling process group are very important to Unix terminals. Sending signals from the terminal is also important (e.g. Ctrl+C -> SIGINT), which relates to the process group controlling the terminal.
Kevin Barry

Last edited by ta0kira; 09-01-2011 at 06:56 PM.
 
Old 09-02-2011, 07:58 PM   #3
damix
LQ Newbie
 
Registered: Sep 2011
Posts: 4

Original Poster
Rep: Reputation: 1
ta0kira

Hello ta0kira, thanks for your reply :-)

I did some more research and it looks like the standard way to achieve this bidirectional communication is with forkpty() or equivalently with a combination of openpty(), fork() and login_tty(). There are still a few things I don't understand, for instance, who is responsible for printing the prompt? Is it the shell or the terminal emulator? I also downloaded the sources of ROTE from http://rote.sourceforge.net/ and it looks like a nice start for those interested in this topic. However, a more theoretical description of these concepts would be great.
 
Old 09-02-2011, 08:38 PM   #4
ta0kira
Senior Member
 
Registered: Sep 2004
Distribution: FreeBSD 9.1, Kubuntu 12.10
Posts: 3,078

Rep: Reputation: Disabled
For the most part the shell is just an interactive line-based program like you might write in C. Normally it's the session leader, which essentially means it's the "supreme owner" of the terminal and it gets to determine which process group can read from the terminal and change the terminal's settings. When the shell needs keyboard input it prints the prompt to the pty (after making sure input is canonical and echoing is on) and reads from the pty, which the terminal program writes to based on input from the UI. When a process writes to the pty the terminal program reads it and updates the display as necessary. I'm not exactly sure how the terminal program knows when the termios settings change since I've never written a terminal emulator, but I'm assuming it's either sent a signal or it reads the settings when they're relevant.
Kevin Barry
 
Old 09-05-2011, 06:04 AM   #5
damix
LQ Newbie
 
Registered: Sep 2011
Posts: 4

Original Poster
Rep: Reputation: 1
Post An example with forkpty

Hi everybody, I wrote another little example program. This time I used forkpty(). You can compile it with the following command

g++ -lutil forkptyte.cc

In this approach the shell itself is printing the prompt; the terminal emulator uses a single master file descriptor, returned by forkpty(), for both writing and reading to/from the shell. This apparently means that when it writes to the master at line 90:

90: write(master, &b, 1);

the characters of the command are made available for the following read, at line 80.

80: if (read(master, &c, 1) != -1)
...

All the data received from the master file descriptor is sent to STDOUT in the terminal emulator; this data includes the output of the shell, if any was produced, as well as these characters which were written at line 90. To avoid duplicated echo of the commands input by the user I disable STDIN echo at the very beginning of main(), and I re-enable it at the end. Finally, it looks like if the shell is terminated by command 'exit', the read at line 80 will return -1, so in this case I set run to false and the terminal emulator terminates also.

Why the shell is printing the prompt while in the previous example (first post) this was not the case? Also, I was wondering whether I handled the duplicated echo issue in the correct way. Is it ok that the terminal emulator, when it reads back the data from the master file descriptor, also retrieves the input command which was responsible for that output? This is what apparently is happening at line 90, when the terminal emulator reads back from the master file descriptor also the characters which were written at line 80.

Thank you for your help, have a nice day :-)

Dario

Code:
// C++ includes
#include <cstring>

// UNIX includes
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <pty.h>
#include <termios.h>

int main(int argc, char *argv[])
{
	// Disable echo on STDIN; data sent to
	// the shell will be read back together
	// with shell output. Also, data is read
	// immediately, without waiting for a
	// delimiter.
	termios oldt, newt;
	tcgetattr(STDIN_FILENO, &oldt);
	newt = oldt;
	newt.c_lflag &= ~(ECHO | ECHONL | ICANON);
	tcsetattr(STDIN_FILENO, TCSAFLUSH, &newt);
	
	// File descriptor master is one of the
	// endpoints of the communication channel
	// between the TE (master) and the shell
	// (slave).
	int master;
	
	// The shell will be run as a separate process.
	int pid = forkpty(&master, NULL, NULL, NULL);
	
	// Check that a child process was created.
	if (pid == -1)
	{
		write(STDOUT_FILENO, "Can not forkpty.\n", strlen("Can not forkpty.\n"));
		return -1;
	}
	
	// From now on the same source code will be
	// shared by two distinct processes; in the
	// parent process forkpty() returns the PID of
	// the child, while in the child process
	// it returns 0.
	
	if (pid != 0)
	{
		// The code in this if will be executed by the parent.
		// This is the TE logic.
		
		// Whether the TE should keep running.
		bool run = true;
		
		// TE sends/receives data to/from the shell one char at a time.
		char b, c;
		
		while (run)
		{
			// Data structures needed for select() and
			// non-blocking I/O.
			fd_set rfd;
			fd_set wfd;
			fd_set efd;
			timeval tv;

			FD_ZERO(&rfd);
			FD_ZERO(&wfd);
			FD_ZERO(&efd);
			FD_SET(master, &rfd);
			FD_SET(STDIN_FILENO, &rfd);
			tv.tv_sec = 0;
			tv.tv_usec = 100000;
			select(master + 1, &rfd, &wfd, &efd, &tv);
			
			// Check for data to receive; the received
			// data includes also the data previously sent
			// on the same master descriptor (line 90).
			if (FD_ISSET(master, &rfd))
			{
				if (read(master, &c, 1) != -1)
					write(STDOUT_FILENO, &c, 1);
				else
					run = false;
			}
			
			// Check for data to send.
			if (FD_ISSET(STDIN_FILENO, &rfd))
			{
				read(STDIN_FILENO, &b, 1);
				write(master, &b, 1);
			}
		}
	}
	else
	{
		// The code in this else will be executed by the child.
		// This is the shell logic.
		
		// The executed shell is bash.
		execlp("bash", "/bin/bash", NULL);
	}
	
	// Restore initial properties of STDIN.
	oldt.c_lflag |= ECHO | ECHONL | ICANON;
	tcsetattr(STDIN_FILENO, TCSAFLUSH, &oldt);
	
	// Everything went fine.
	return 0;
}

Last edited by damix; 09-05-2011 at 06:19 AM.
 
1 members found this post helpful.
Old 09-05-2011, 10:58 AM   #6
ta0kira
Senior Member
 
Registered: Sep 2004
Distribution: FreeBSD 9.1, Kubuntu 12.10
Posts: 3,078

Rep: Reputation: Disabled
Quote:
Originally Posted by damix View Post
Why the shell is printing the prompt while in the previous example (first post) this was not the case? Also, I was wondering whether I handled the duplicated echo issue in the correct way. Is it ok that the terminal emulator, when it reads back the data from the master file descriptor, also retrieves the input command which was responsible for that output? This is what apparently is happening at line 90, when the terminal emulator reads back from the master file descriptor also the characters which were written at line 80.
In your first example you left a lot of pipes open that should have been closed. Specifically, after fork you need to close the end of the pipe not being used and after dup2 you need to close the old descriptor (but only if it wasn't already equal to what you dup2ed it to!) You should also use SIGTERM instead of SIGKILL since the latter eliminates the process rather than signaling it. You should also check read for a return of 0 since that means EOF. I'm not getting the duplication you're reporting; it works fine for me.
Kevin Barry
 
Old 12-01-2012, 06:38 PM   #7
cristinaparas
LQ Newbie
 
Registered: Dec 2012
Posts: 1

Rep: Reputation: Disabled
Permission

Damix: may I use parts of your code for a project crediting you (assuming I have attribution info)?
 
  


Reply

Tags
terminal



Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is Off
HTML code is Off



Similar Threads
Thread Thread Starter Forum Replies Last Post
Problem with terminal emulators: schema JosephS Slackware 12 01-29-2009 07:12 AM
LXer: 13 Terminal Emulators for Linux LXer Syndicated Linux News 0 09-22-2008 10:00 AM
LXer: Terminal Emulators Review LXer Syndicated Linux News 0 09-16-2007 10:50 PM
Terminal emulators don't work in X pherthyl Linux - Software 1 03-12-2004 12:58 AM
Terminal Emulators Question ximiansavior Linux - Software 4 11-17-2003 06:07 AM

LinuxQuestions.org > Forums > Non-*NIX Forums > Programming

All times are GMT -5. The time now is 02:55 AM.

Main Menu
Advertisement
My LQ
Write for LQ
LinuxQuestions.org is looking for people interested in writing Editorials, Articles, Reviews, and more. If you'd like to contribute content, let us know.
Main Menu
Syndicate
RSS1  Latest Threads
RSS1  LQ News
Twitter: @linuxquestions
Open Source Consulting | Domain Registration