Cannot mimic the action of `echo` command in C code - c

I have written a couple of bash script files that communicate via two serial ports. One script can be thought of as a receiver and the other as a transmitter. The receiving script reads and displays data line by line until it reads the character sequence <break>. It then stops listening, and sends a character sequence of either one or two back to the transmitter. The script is thus:
#!/bin/bash
# Read and reply bash script.
exec 3<> /dev/ttyS4
while true; do
#cat -v /dev/ttyS4 | while read -r input; do
while read -r -u 3 input; do
if [ "$input" = "<break>" ]
then
echo "break command received."
break
else
echo -e "${input}"
fi
done
echo "Sending selection"
if [ "$selection" = "one" ]
then
selection="two"
else
selection="one"
fi
echo "$selection" >&3
done
The transmitter script transmits some character data and then waits for the reply of one or two from the receiver script above:
exec 4<> /dev/ttyS0
selection="one"
while true; do
echo "************************************" > /dev/ttyS0
echo " Selection: ${selection}" > /dev/ttyS0
echo "************************************" > /dev/ttyS0
echo "<break>" > /dev/ttyS0
read -r -t 3 -u 4 input
if [ -z "$input" ]
then
echo "Response from remote timed out."
elif [ "$input" = "one" ]
then
selection=$input
elif [ "$input" = "two" ]
then
selection=$input
colour=$RED
else
echo "Unknown selection: $input"
fi
sleep 1
done
The above two scripts work fine and the receiver script correctly identifies the <break> character sequence.
I wish to replace the 'transmitter' script with a small C program however I find that when I send the character sequence <break> the receiver script this time does NOT identify is as the 'end-of-transmission', is simple echos <break> to stdout, and NOT break command received. I have tried several things such as adding escape characters but there is obviously something different about the way Bash echo works and how I send the data in my C code:
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#define TRUE 1
#define FALSE 0
int main(int argc, char *argv[]) {
int selected_tool = DEFAULT_TOOL_SELECTION;
FILE *ser2_fd_write, *ser2_fdrw;
struct termios tios, tios_w; // ser 2 termios structure
int ser2_fd, ser2_fdw;
char read_buffer[BUFFER_SIZE];
struct timespec tdelay;
bzero(&tdelay, sizeof(tdelay));
tdelay.tv_sec = 1;
tdelay.tv_nsec = 5000;
if ((ser2_fd = open("/dev/ttyS0", O_RDWR)) == -1){
printf("Unable to open ttyS0 as read-write only.\n");
return EXIT_FAILURE;
}
bzero(&tios, sizeof(tios));
cfsetispeed(&tios, B38400);
cfsetospeed(&tios, B38400);
tios.c_cflag = B38400 | CS8 | CLOCAL | CREAD | CRTSCTS;
tios.c_iflag = IGNPAR;
tios.c_oflag = 0;
tios.c_lflag = ICANON; //0
tios.c_cc[VTIME] = 0;
tios.c_cc[VMIN] = 10;
tcflush(ser2_fd, TCIFLUSH);
if (tcsetattr(ser2_fd, TCSANOW, &tios) == -1){
printf("Could not set ser 2 attributes.\n");
return -1;
}
if ((ser2_fdrw = fdopen(ser2_fd, "awr")) == NULL){
printf("Unable to open file descriptor.\n");
return EXIT_FAILURE;
}
while(1){
push_to_ser2(ser2_fdrw, selected_tool);
/*
* This is where sending <break> differs from echo
*/
//fputs("break", ser2_fdrw);
fprintf(ser2_fdrw, "break\r\n");
//fprintf(ser2_fdrw, "\r\n");
//write(ser2_fd,"break\r\n", 9);
fflush(ser2_fdrw);
int c = 0;
nanosleep(&tdelay, NULL);
tcflush(ser2_fd, TCIOFLUSH);
tcdrain(ser2_fd);
fcntl(ser2_fd, F_SETFL, 0);
if ( (c = read(ser2_fd, read_buffer, BUFFER_SIZE)) > 0){
read_buffer[c] = 0;
if (strcmp(read_buffer, "one\r\n") == 0){
selected_tool = 1;
} else if (strcmp(read_buffer, "two\r\n") == 0){
selected_tool = 2;
}
}else{
}
}
return EXIT_SUCCESS;
}
/*
* Convenience function to push data to ser 2
* *c_data pointer to the card data
* *t_data pointer to the tool data
*/
void push_to_ser2(FILE * fd, int tool){
fprintf(fd, "**********************************************************\n");
fprintf(fd, "* *\n");
fprintf(fd, "* Tool %d Data *\n", tool);
fprintf(fd, "* *\n");
fprintf(fd, "**********************************************************\n");
fprintf(fd,"\r\n");
fflush(fd);
}
I've tried various alterations to the termios struct too but it makes no difference. Any ideas?

There are several issues with your code that should be corrected:
Instead of bzero(&tios, sizeof(tios)) the code should be calling tcgetattr() to properly initialize the structure. This can be a serious issue for canonical input, as the existing code will have zeroed out all of the control code specifications instead of having proper definitions.
Instead of direct assignments, the code should be performing bit-wise operations (in order to preserve existing settings). See Setting Terminal Modes Properly.
You're specifying non-canonical output with canonical input, which is an atypical combination. Seems like you want canonical mode for both input and output.
VMIN and VTIME are only meaningful for non-canonical input, and should not be specified for canonical input (as it clobbers the VEOF and VEOL character specifications).
The read(ser2_fd,...) is silently ignoring errors.
Instead of using fdopen() and fprintf(), just use write(ser2_fd, ...) to simplify the code and overhead. Downsides are that you'll have to specify the byte counts and use sprintf() to perform integer to string conversion.
See Serial Programming Guide for POSIX Operating Systems.

It looks as if the bash script is sending the literal string <break>, angle brackets included. Meanwhile, the C program is only sending break. Add the angle brackets to the literal in the C program. (Those brackets don't have any special meaning to either echo or fprintf, so they just get sent as-is.)

Related

How to remove characters(^\), ctrl + \(SIGQUIT), from STDIN in C [duplicate]

Good day,
I'm writing my own shell in C for my school which has to resemble bash as closely as possible.
I have to handle signals such as Ctrl-\ and Ctrl-C as bash does; for this reason I'm allowed to use signal function. It works fine, but the thing is whenever a Ctrl-C signal is caught (starting from the second catch), a ^C is printed.
On the net, I've found a workaround suggesting printing "\b \b\b \b\nminishell$ " whenever a Ctrl-C is caught, which will devour the two symbols. The thing is, since at the very first time ^C is not printed, the print devours two symbols of my prompting, making it just minishell instead of minishell$ , with the cursor incorrectly displayed.
Now I've come up with another workaround for this workaround which is to declare a static boolean to not print the baskspaces at the very first call. This doesn't help in case of Ctrl-\ though; Ctrl-\ proceeds to move my cursor to right when I attempt to write the two whitespaces that must replace the ^\.
I don't like these workarounds and would like to know whether there is a way to instruct the terminal not to output this stuff? I'm allowed to use tgetent, tgetflag, tgetnum, tgetstr, tgoto, tputs, tcsetattr, tcgetattr, have read their man pages but nothing seems to be helpful.
When you type a key on a terminal, two things happen
the character is echoed (displayed) on this terminal
the character is sent (over the line) to the attached program
Both these actions can be controlled via termios/tcsetattr(): a different character(s) can be sent or echoed, some can be suppressed, etc. (some/most of these actions take place in the terminal-driver , but this is not relevant here)
Demonstration: using tcsetattr() to control the echoing of the terminal:
#include <stdio.h>
#include <stdlib.h>
#define _SVID_SOURCE 1
#include <termios.h>
#include <unistd.h>
#include <signal.h>
struct termios termios_save;
void reset_the_terminal(void)
{
tcsetattr(0, 0, &termios_save );
}
sig_atomic_t the_flag = 0;
void handle_the_stuff(int num)
{
char buff[4];
buff[0] = '[';
buff[2] = '0' + num%10;
num /= 10;
buff[1] = '0' + num%10;
buff[3] = ']';
write(0, buff, sizeof buff);
the_flag = 1;
}
int main (void)
{
int rc;
int ch;
struct termios termios_new;
rc = tcgetattr(0, &termios_save );
if (rc) {perror("tcgetattr"); exit(1); }
rc = atexit(reset_the_terminal);
if (rc) {perror("atexit"); exit(1); }
termios_new = termios_save;
termios_new.c_lflag &= ~ECHOCTL;
rc = tcsetattr(0, 0, &termios_new );
if (rc) {perror("tcsetattr"); exit(1); }
signal(SIGINT, handle_the_stuff);
printf("(pseudoshell)Start typing:\n" );
while(1) {
ch = getc(stdin);
if (the_flag) {
printf("Saw the signal, last character was %02x\n", (unsigned) ch);
break;
}
}
exit (0);
}
The way to set the console such a SW may intercept all typed chars is to set the terminal in RAW MODE. The problems this way may present are that all keys that aren't in the ASCII 0-255 space, such as è, ì, à will be received from the console as a bytes sequence and all the function and control keys included cursors and backspace will not accomplish any action, some code such as CR, LF and some ANSI sequence may accomplish actions when are read from the input channel and rewritten on the output channel.
To set the terminal in raw mode you have to use the function cfmakeraw followed by the function tcsetattr.
The code below implements a simple but not very good implemented terminal, anyway I think this code is a good point to start. In any case, the code flow and the error control must be at least better arranged.
The code writes all sequence of ASCII char that enter into the console when a key is typed. All chars that have value smaller then 32 or greater then 126 will be written as [HEX-CODE]
I.E. hitting Esc on the console will be written [1B], the code of Ctrl+C will be written as [03], F1 will be [1B]OP, F11 will be [1B][23~, Enter will be [0D].
If you will hit Ctrl+X [18] will be written and the program stops, but this behaviour is under SW control as you can see in the code.
Here the code:
#include <stdio.h> // Standard input/output definitions
#include <string.h> // String function definitions
#include <unistd.h> // UNIX standard function definitions
#include <fcntl.h> // File control definitions
#include <errno.h> // Error number definitions
#include <termios.h> // POSIX terminal control definitions (struct termios)
#include <sys/ioctl.h> // Used for TCGETS2, which is required for custom baud rates
#include <sys/select.h> // might be used to manage select
int setAttr(int ch, int resetToOld);
#define IN 0
#define OUT 1
typedef struct TermCap
{
int fd;
struct termios oldTermios;
struct termios newTermios;
// fd_set fds; // might be used to manage select
} TermCap;
TermCap m_termCap[2];
int main()
{
int i,ex=0;
char msg;
char buff[20];
m_termCap[IN].fd=STDIN_FILENO;
m_termCap[OUT].fd=STDOUT_FILENO;
// Gets STDIN config and set raw config
setAttr(IN,0);
// Gets STDOUT config and set raw config
setAttr(OUT,0);
// Console loop ... the console terminates when ^X is intercepted.
do {
do {
i=read(m_termCap[IN].fd,&msg,1);
if (i>0){
if (msg<32 || msg>126) {
sprintf(buff,"[%02X]",(unsigned char)msg);
write(m_termCap[OUT].fd,buff,4);
if (msg==24)
ex=1;
}else{
write(m_termCap[OUT].fd,&msg,i);
}
}
usleep(10000); // a minimal delay of 10 millisec
} while(i>0 && !ex);
} while(!ex);
// Reset console to initial state.
setAttr(IN,1);
setAttr(OUT,1);
printf("\r\n\nThe end!");
return 0;
}
int setAttr(int ch, int resetToOld)
{
int retVal=0;
int i;
if (!resetToOld) {
// Read old term config
i=tcgetattr(m_termCap[ch].fd, &m_termCap[ch].oldTermios);
if (i==-1) {
return 1;
}
}
m_termCap[ch].newTermios = m_termCap[ch].oldTermios;
if (!resetToOld) {
// Terminal in raw mode
cfmakeraw(&m_termCap[ch].newTermios);
}
i=tcsetattr(m_termCap[ch].fd, TCSANOW, &m_termCap[ch].newTermios);
if (i==-1) {
retVal = 2;
}
return retVal;
}
Wouldn't this work?
void signalHandler(int signo){
if(signo==SIGINT){
printf("\b\b \b\b");
fflush(NULL);
printf("\nHello World\n");
}
}
In my shell it seems to work fine. The first printf and fflush is what you have to implement in your handler. The printf after that is just a way for me to show you that you can, then, do whatever you want after the ^C not appearing.
Why does this make it not appear? In the first printf I erase the characters by using backspaces and spaces. As stdout is buffered by default and I didn't want to use a newline character, I flushed the buffer manually.

Forcing a terminal not to print Ctrl hotkeys when signals are caught

Good day,
I'm writing my own shell in C for my school which has to resemble bash as closely as possible.
I have to handle signals such as Ctrl-\ and Ctrl-C as bash does; for this reason I'm allowed to use signal function. It works fine, but the thing is whenever a Ctrl-C signal is caught (starting from the second catch), a ^C is printed.
On the net, I've found a workaround suggesting printing "\b \b\b \b\nminishell$ " whenever a Ctrl-C is caught, which will devour the two symbols. The thing is, since at the very first time ^C is not printed, the print devours two symbols of my prompting, making it just minishell instead of minishell$ , with the cursor incorrectly displayed.
Now I've come up with another workaround for this workaround which is to declare a static boolean to not print the baskspaces at the very first call. This doesn't help in case of Ctrl-\ though; Ctrl-\ proceeds to move my cursor to right when I attempt to write the two whitespaces that must replace the ^\.
I don't like these workarounds and would like to know whether there is a way to instruct the terminal not to output this stuff? I'm allowed to use tgetent, tgetflag, tgetnum, tgetstr, tgoto, tputs, tcsetattr, tcgetattr, have read their man pages but nothing seems to be helpful.
When you type a key on a terminal, two things happen
the character is echoed (displayed) on this terminal
the character is sent (over the line) to the attached program
Both these actions can be controlled via termios/tcsetattr(): a different character(s) can be sent or echoed, some can be suppressed, etc. (some/most of these actions take place in the terminal-driver , but this is not relevant here)
Demonstration: using tcsetattr() to control the echoing of the terminal:
#include <stdio.h>
#include <stdlib.h>
#define _SVID_SOURCE 1
#include <termios.h>
#include <unistd.h>
#include <signal.h>
struct termios termios_save;
void reset_the_terminal(void)
{
tcsetattr(0, 0, &termios_save );
}
sig_atomic_t the_flag = 0;
void handle_the_stuff(int num)
{
char buff[4];
buff[0] = '[';
buff[2] = '0' + num%10;
num /= 10;
buff[1] = '0' + num%10;
buff[3] = ']';
write(0, buff, sizeof buff);
the_flag = 1;
}
int main (void)
{
int rc;
int ch;
struct termios termios_new;
rc = tcgetattr(0, &termios_save );
if (rc) {perror("tcgetattr"); exit(1); }
rc = atexit(reset_the_terminal);
if (rc) {perror("atexit"); exit(1); }
termios_new = termios_save;
termios_new.c_lflag &= ~ECHOCTL;
rc = tcsetattr(0, 0, &termios_new );
if (rc) {perror("tcsetattr"); exit(1); }
signal(SIGINT, handle_the_stuff);
printf("(pseudoshell)Start typing:\n" );
while(1) {
ch = getc(stdin);
if (the_flag) {
printf("Saw the signal, last character was %02x\n", (unsigned) ch);
break;
}
}
exit (0);
}
The way to set the console such a SW may intercept all typed chars is to set the terminal in RAW MODE. The problems this way may present are that all keys that aren't in the ASCII 0-255 space, such as è, ì, à will be received from the console as a bytes sequence and all the function and control keys included cursors and backspace will not accomplish any action, some code such as CR, LF and some ANSI sequence may accomplish actions when are read from the input channel and rewritten on the output channel.
To set the terminal in raw mode you have to use the function cfmakeraw followed by the function tcsetattr.
The code below implements a simple but not very good implemented terminal, anyway I think this code is a good point to start. In any case, the code flow and the error control must be at least better arranged.
The code writes all sequence of ASCII char that enter into the console when a key is typed. All chars that have value smaller then 32 or greater then 126 will be written as [HEX-CODE]
I.E. hitting Esc on the console will be written [1B], the code of Ctrl+C will be written as [03], F1 will be [1B]OP, F11 will be [1B][23~, Enter will be [0D].
If you will hit Ctrl+X [18] will be written and the program stops, but this behaviour is under SW control as you can see in the code.
Here the code:
#include <stdio.h> // Standard input/output definitions
#include <string.h> // String function definitions
#include <unistd.h> // UNIX standard function definitions
#include <fcntl.h> // File control definitions
#include <errno.h> // Error number definitions
#include <termios.h> // POSIX terminal control definitions (struct termios)
#include <sys/ioctl.h> // Used for TCGETS2, which is required for custom baud rates
#include <sys/select.h> // might be used to manage select
int setAttr(int ch, int resetToOld);
#define IN 0
#define OUT 1
typedef struct TermCap
{
int fd;
struct termios oldTermios;
struct termios newTermios;
// fd_set fds; // might be used to manage select
} TermCap;
TermCap m_termCap[2];
int main()
{
int i,ex=0;
char msg;
char buff[20];
m_termCap[IN].fd=STDIN_FILENO;
m_termCap[OUT].fd=STDOUT_FILENO;
// Gets STDIN config and set raw config
setAttr(IN,0);
// Gets STDOUT config and set raw config
setAttr(OUT,0);
// Console loop ... the console terminates when ^X is intercepted.
do {
do {
i=read(m_termCap[IN].fd,&msg,1);
if (i>0){
if (msg<32 || msg>126) {
sprintf(buff,"[%02X]",(unsigned char)msg);
write(m_termCap[OUT].fd,buff,4);
if (msg==24)
ex=1;
}else{
write(m_termCap[OUT].fd,&msg,i);
}
}
usleep(10000); // a minimal delay of 10 millisec
} while(i>0 && !ex);
} while(!ex);
// Reset console to initial state.
setAttr(IN,1);
setAttr(OUT,1);
printf("\r\n\nThe end!");
return 0;
}
int setAttr(int ch, int resetToOld)
{
int retVal=0;
int i;
if (!resetToOld) {
// Read old term config
i=tcgetattr(m_termCap[ch].fd, &m_termCap[ch].oldTermios);
if (i==-1) {
return 1;
}
}
m_termCap[ch].newTermios = m_termCap[ch].oldTermios;
if (!resetToOld) {
// Terminal in raw mode
cfmakeraw(&m_termCap[ch].newTermios);
}
i=tcsetattr(m_termCap[ch].fd, TCSANOW, &m_termCap[ch].newTermios);
if (i==-1) {
retVal = 2;
}
return retVal;
}
Wouldn't this work?
void signalHandler(int signo){
if(signo==SIGINT){
printf("\b\b \b\b");
fflush(NULL);
printf("\nHello World\n");
}
}
In my shell it seems to work fine. The first printf and fflush is what you have to implement in your handler. The printf after that is just a way for me to show you that you can, then, do whatever you want after the ^C not appearing.
Why does this make it not appear? In the first printf I erase the characters by using backspaces and spaces. As stdout is buffered by default and I didn't want to use a newline character, I flushed the buffer manually.

Non-canonical mode don't work on backtick

When I launch my program using backticks like:
cat `./my_program`
I expect my read() to read character by character, it still reads line by line. The non-canonical mode of my terminal doesn't work. I don't really understand why.
Here is my code:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <term.h>
#include <strings.h>
int main()
{
struct termios term_setting;
char *term;
char buff[10];
term = getenv("TERM");
tgetent(NULL, term);
tcgetattr(STDOUT_FILENO, &term_setting); //
term_setting.c_lflag &= ~(ICANON | ECHO); //set my term in echo and non-canonique mode
tcsetattr(STDOUT_FILENO, 0, &term_setting); //
for (;;){
bzero(buff, sizeof(char) * 10);
read(STDIN_FILENO, buff, 9);
printf("%s\n", buff);
}
return (0);
}
There seems to be some confusion here: what do you expect
cat `./my_program`
to produce?
my_program copies standard input to standard output, in an infinite loop, even after reaching the end of file, which you do not test, producing unexpected output.
The shell collects this output either in a file, in a pseudo-terminal or possibly via a pipe and, once completed, passes it to cat as command line arguments, but since you must kill my_program in order to complete its output, the shell will abort this operation too.
cat expects options and filenames as command line arguments, probably not what you type.
Regarding the change of settings for the terminal, you could use STDIN_FILENO or just 0 instead of STDOUT_FILENO, since you want to change the behavior of the input handle.
Ry-♦ say in comment:
Because stdout isn’t the terminal anymore. Did you mean to get/set those attributes on STDIN_FILENO?
It's exactly my probleme, thx!

Understanding read + write in c

char buf[1];
if (argc == 1) {
while (read(STDIN_FILENO, buf, 1) > 0) {
write(1, buf, sizeof(buf));
}
}
I have a few things I'd like to clarify about this snippet. We run it, ./exec_file Let's say we just press Enter. We move to the next line and read 1 byte '\n' then write it to stdout bringing us down one more line... simple enough. Now lets say we type h, then Enter. The program spits out h on the next line with an invisible '\n'.
Looking at the code after we type h it reads it into the buffer then writes it to stdout but somehow the program waits to spit it out on the next line till after I've pressed Enter..how?
Lastly, when we first hit the while loop wouldn't read initially return 0 since we haven't typed anything in initially??
stdin behaves a bit different than most other streams.
First, input is line buffered. That means that input isn't available until you press enter. this explains while the h won't appear until you press enter.
Since it is a stream it doesn't really have an end. Instead of failing when there is no data to read, the call will block until some data is available (or until the program receives a signal). A socket works the same way.
The blocking behaviour can be turned off using fcntl :
int fd = STDIN_FILENO;
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
The terminal is by default line buffered, because it is in canonical mode. From Linux manuals tcgetattr(3):
Canonical and noncanonical mode
The setting of the ICANON canon
flag in c_lflag determines whether the terminal is operating in
canonical mode (ICANON set) or noncanonical mode (ICANON unset).
By default, ICANON set.
In canonical mode:
Input is made available line by line. An input line is
available
when one of the line delimiters is typed (NL, EOL, EOL2; or EOF at
the start of line). Except in the case of EOF, the line delimiter
is included in the buffer returned by read(2).
Line editing is enabled (ERASE, KILL; and if the IEXTEN flag is
set:
WERASE, REPRINT, LNEXT). A read(2) returns at most one line of
input; if the read(2) requested fewer bytes than are available in
the current line of input, then only as many bytes as requested are
read, and the remaining characters will be available for a future
read(2).
You can switch off canonical mode on the terminal by calling tcgetattr with proper flags. First of all disable the canonical mode; then set the timeout to 0; set minimum read to 1 for blocking reads or 0 for non-blocking reads. Usually it is customary to also disable local echo, otherwise everything you type would still be automatically visible (and displayed twice in your program):
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
int main() {
struct termios old_settings, new_settings;
int is_terminal;
// check whether the stdin is a terminal to begin with
if (is_terminal = isatty(STDIN_FILENO)) {
// get the old settings
tcgetattr(STDIN_FILENO, &old_settings);
new_settings = old_settings;
// disable canonical mode and echo
new_settings.c_lflag &= (~ICANON & ~ECHO);
// at least one character must be written before read returns
new_settings.c_cc[VMIN] = 1;
// no timeout
new_settings.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &new_settings);
}
while (read(STDIN_FILENO, buf, 1) > 0) {
// add this here so that you can verify that it is character by character,
// and not the local echo from the terminal
write(STDOUT_FILENO, ">", 1);
write(STDOUT_FILENO, buf, sizeof(buf));
}
// finally restore the old settings if it was a terminal
if (is_terminal) {
tcsetattr(STDIN_FILENO, TCSANOW, &old_settings);
}
return 0;
}
If you still want the blocking to happen, but want to read character by character, you can use termios to configure how the input will be given to your program. See the code below.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
int main()
{
char buf[1];
struct termios term, term_orig;
if (tcgetattr(0, &term_orig)) {
printf("tcgetattr failed\n");
exit(-1);
}
term = term_orig;
term.c_lflag &= ~ICANON;
term.c_lflag |= ECHO;
term.c_cc[VMIN] = 1;
term.c_cc[VTIME] = 0;
if (tcsetattr(0, TCSANOW, &term)) {
printf("tcsetattr failed\n");
exit(-1);
}
while (read(0, buf, 1) > 0) {
write(1, buf, sizeof(buf));
}
return 0;
}

Linux C: "Interactive session" with separate read and write named pipes?

I am trying to work with "Introduction to Interprocess Communication Using Named Pipes - Full-Duplex Communication Using Named Pipes", link ; in particular fd_server.c (included below for reference)
Here is my info and compile line:
:~$ cat /etc/issue
Ubuntu 10.04 LTS \n \l
:~$ gcc --version
gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3
:~$ gcc fd_server.c -o fd_server
fd_server.c creates two named pipes, one for reading and one for writing. What one can do, is: in one terminal, run the server and read (through cat) its write pipe:
:~$ ./fd_server & 2>/dev/null
[1] 11354
:~$ cat /tmp/np2
and in another, write (using echo) to server's read pipe:
:~$ echo "heeellloooo" > /tmp/np1
going back to first terminal, one can see:
:~$ cat /tmp/np2
HEEELLLOOOO
0[1]+ Exit 13 ./fd_server 2> /dev/null
What I would like to do, is make sort of a "interactive" (or "shell"-like) session; that is, the server is run as usual, but instead of running cat and echo, I'd like to use something akin to screen. What I mean by that, is that screen can be called like screen /dev/ttyS0 38400, and then it makes a sort of a interactive session, where what is typed in terminal is passed to /dev/ttyS0, and its response is written to terminal. Now, of course, I cannot use screen, because in my case the program has two separate nodes, and as far as I can tell, screen can refer to only one.
How would one go about to achieve this sort of "interactive" session in this context (with two separate read/write pipes)?
Code below:
#include <stdio.h>
#include <errno.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//#include <fullduplex.h> /* For name of the named-pipe */
#define NP1 "/tmp/np1"
#define NP2 "/tmp/np2"
#define MAX_BUF_SIZE 255
#include <stdlib.h> //exit
#include <string.h> //strlen
int main(int argc, char *argv[])
{
int rdfd, wrfd, ret_val, count, numread;
char buf[MAX_BUF_SIZE];
/* Create the first named - pipe */
ret_val = mkfifo(NP1, 0666);
if ((ret_val == -1) && (errno != EEXIST)) {
perror("Error creating the named pipe");
exit (1);
}
ret_val = mkfifo(NP2, 0666);
if ((ret_val == -1) && (errno != EEXIST)) {
perror("Error creating the named pipe");
exit (1);
}
/* Open the first named pipe for reading */
rdfd = open(NP1, O_RDONLY);
/* Open the second named pipe for writing */
wrfd = open(NP2, O_WRONLY);
/* Read from the first pipe */
numread = read(rdfd, buf, MAX_BUF_SIZE);
buf[numread] = '0';
fprintf(stderr, "Full Duplex Server : Read From the pipe : %sn", buf);
/* Convert to the string to upper case */
count = 0;
while (count < numread) {
buf[count] = toupper(buf[count]);
count++;
}
/*
* Write the converted string back to the second
* pipe
*/
write(wrfd, buf, strlen(buf));
}
Edit:
Right, just to clarify - it seems I found a document discussing something very similar, it is - a modification of the script there ("For example, the following script configures the device and starts a background process for copying all received data from the serial device to standard output...") for the above program is below:
# stty raw #
( ./fd_server 2>/dev/null; )&
bgPidS=$!
( cat < /tmp/np2 ; )&
bgPid=$!
# Read commands from user, send them to device
echo $(kill -0 $bgPidS 2>/dev/null ; echo $?)
while [ "$(kill -0 $bgPidS 2>/dev/null ; echo $?)" -eq "0" ] && read cmd; do
# redirect debug msgs to stderr, as here we're redirected to /tmp/np1
echo "$? - $bgPidS - $bgPid" >&2
echo "$cmd"
echo -e "\nproc: $(kill -0 $bgPidS 2>/dev/null ; echo $?)" >&2
done >/tmp/np1
echo OUT
# Terminate background read process - if they still exist
if [ "$(kill -0 $bgPid 2>/dev/null ; echo $?)" -eq "0" ] ;
then
kill $bgPid
fi
if [ "$(kill -0 $bgPidS 2>/dev/null ; echo $?)" -eq "0" ] ;
then
kill $bgPidS
fi
# stty cooked
So, saving the script as say starter.sh and calling it, results with the following session:
$ ./starter.sh
0
i'm typing here and pressing [enter] at end
0 - 13496 - 13497
I'M TYPING HERE AND PRESSING [ENTER] AT END
0~�.N=�(�~� �����}����#������~� [garble]
proc: 0
OUT
which is what I'd call for "interactive session" (ignoring the debug statements) - server waits for me to enter a command; it gives its output after it receives a command (and as in this case it exits after first command, so does the starter script as well). Except that, I'd like to not have buffered input, but sent character by character (meaning the above session should exit after first key press, and print out a single letter only - which is what I expected stty raw would help with, but it doesn't: it just kills reaction to both Enter and Ctrl-C :) )
I was just wandering if there already is an existing command (akin to screen in respect to serial devices, I guess) that would accept two such named pipes as arguments, and establish a "terminal" or "shell" like session through them; or would I have to use scripts as above and/or program own 'client' that will behave as a terminal..
If you just want to be able to receive multiple lines, rather than exiting after one, this is simple. You just need to place a loop around your read/write code, like so (quick and dirty):
while( 1 ) {
numread = read(rdfd, buf, MAX_BUF_SIZE);
fprintf(stderr, "Full Duplex Server : Read From the pipe : %sn", buf);
/* Convert to the string to upper case */
count = 0;
while (count < numread) {
buf[count] = toupper(buf[count]);
count++;
}
/*
* Write the converted string back to the second
* pipe
*/
write(wrfd, buf, strlen(buf));
}
Of course, now you have an application which will never exit, and will start doing nothing as soon as it gets an EOF, etc. So, you can reorganize it to check for errors:
numread = read(rdfd, buf, MAX_BUF_SIZE);
while( numread > 0) {
/* ... etc ... */
numread = read(rdfd,buf, MAX_BUF_SIZE);
}
if( numread == 0 ) {
/* ... handle eof ... */
}
if( numread < 0 ) {
/* ... handle io error ... */
}
From the man page, read returns 0 for EOF and -1 for an error (you have read the man page, right? http://linux.die.net/man/2/read ). So what this does is keeps on grabbing bytes from the read pipe until it reaches EOF or some error, in which case you (probably) print a message and exit. That said, you might just do a reopen when you get an EOF so you can get more input.
Once you've modified your program to read continuously, entering multiple lines interactively is simple. Just execute:
cat - > /tmp/np1
The '-' explicitly tells cat to read from stdin (this is the default, so you don't actually need the dash). So cat will pass everything you enter on to your pipe program. You can insert an EOF using Ctrl+D, which will cause cat to stop reading stdin. What happens to your pipe program depends on how you handle the EOF in your read loop.
Now, if you want another program that does all the io, without cat, (so you end up with a stdio echo program), the pseudocode is going to look sort of like this:
const int stdin_fd = 0; // known unix constant!
int readpipe_fd = open the read pipe, as before
int writepipe_fd = open the write pipe, as before
read stdin into buffer
while( stdin is reading correctly ) {
write data from stdin to read pipe
check write is successful
read write pipe into buffer
check read is successful
write buffer to stdout (fprintf is fine)
read stdin into buffer.
}
You can use the read system call to read stdin if you feel like it, but you can also just use stdio. Reading, writing, and opening your pipes should all be identical to your server program, except read/write is all reversed.

Resources