I am trying to talk interactively with a C
executable on Linux (give input on stdin and read output from stdout).
I am using a C
program host.c
and talking to a compiled executable target.c
.
However, the output is not as expected. Especially giving input to target.c
does not work.
Here is my full code:
host.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
struct PStd {
FILE *p_stdin;
FILE *p_stdout;
};
// Like popen(), but returns two FILE*: child's stdin and stdout, respectively.
struct PStd* popen2(const char *executableCommand)
{
// pipes[0]: parent writes, child reads (child's stdin)
// pipes[1]: child writes, parent reads (child's stdout)
int pipes[2][2];
pipe(pipes[0]);
pipe(pipes[1]);
if (fork() > 0)
{
// parent
close(pipes[0][0]);
close(pipes[1][1]);
struct PStd *output = (struct PStd *) malloc(sizeof(struct PStd));
output->p_stdin = fdopen(pipes[0][1], "w");
output->p_stdout = fdopen(pipes[1][0], "r");
return output;
}
else
{
// child
close(pipes[0][1]);
close(pipes[1][0]);
dup2(pipes[0][0], STDIN_FILENO);
dup2(pipes[1][1], STDOUT_FILENO);
execl("/bin/sh", "/bin/sh", "-c", executableCommand, NULL);
// execl(executableCommand, executableCommand, NULL);
exit(1);
}
}
int main()
{
printf("HOST BEGIN\n");
sleep(1);
struct PStd *values = popen2("./target.out");
if (values->p_stdin == NULL || values->p_stdout == NULL)
{
printf("popen2() failed\n");
return 1;
}
printf("popen2() success\n");
setvbuf(values->p_stdin, NULL, _IONBF, 0);
setvbuf(values->p_stdout, NULL, _IONBF, 0);
// const char msg[] = "Hello there!\n";
char buf[320];
// &&&&&&&&&&&&&&&
printf("LOGS 0\n");
while(fgets(buf, sizeof(buf), values->p_stdout)==NULL);
printf("LOGS READ %s", buf);
//////////////////
fputs("data1\n", values->p_stdin);
printf("LOGS 1\n");
while(fgets(buf, sizeof(buf), values->p_stdout)==NULL);
printf("LOGS READ %s", buf);
//////////////////
//////////////////
fputs("data2\n", values->p_stdin);
printf("LOGS 2\n");
while(fgets(buf, sizeof(buf), values->p_stdout)==NULL);
printf("LOGS READ %s", buf);
//////////////////
//////////////////
fputs("data3\n", values->p_stdin);
printf("LOGS 3\n");
while(fgets(buf, sizeof(buf), values->p_stdout)==NULL);
printf("LOGS READ %s", buf);
//////////////////
printf("LOGS 4\n");
while(fgets(buf, sizeof(buf), values->p_stdout)==NULL);
printf("LOGS READ %s", buf);
// &&&&&&&&&&&&&&&
fclose(values->p_stdin);
fclose(values->p_stdout);
free(values);
printf("HOST END\n");
return 0;
}
target.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
printf("TARGET BEGIN\n");
char s[1000];
scanf("%s",s);
printf("TARGET got 0: %s\n",s);
scanf("%s",s);
printf("TARGET got 1: %s\n",s);
scanf("%s",s);
printf("TARGET got 2: %s\n",s);
printf("TARGET DONE\n");
return 0;
}
The current output is stuck at
HOST BEGIN
popen2() success
LOGS 0
Expected output:
HOST BEGIN
popen2() success
LOGS 0
LOGS READ TARGET BEGIN
LOGS 1
LOGS READ TARGET got 0: data1
LOGS 2
LOGS READ TARGET got 1: data2
LOGS 3
LOGS READ TARGET got 2: data3
LOGS 4
LOGS READ TARGET DONE
HOST END
The target.c
executable is named target.out
. Please do not change target.c
. How can I write my host.c
to converse with target.c
?
EDIT:
I changed target.c
to following and everything is working.
Thanks to @Ian Abbott to guiding me on the right path :)
target.c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
printf("TARGET BEGIN\n");
char s[1000] = "dummy";
fflush(stdin);
scanf("%s",s);
printf("TARGET got 0: %s\n",s);
scanf("%s",s);
printf("TARGET got 1: %s\n",s);
scanf("%s",s);
printf("TARGET got 2: %s\n",s);
printf("TARGET DONE\n");
return 0;
}
Output:
HOST BEGIN
popen2() success
LOGS 0
LOGS READ TARGET BEGIN
LOGS 1
LOGS READ TARGET got 0: data1
LOGS 2
LOGS READ TARGET got 1: data2
LOGS 3
LOGS READ TARGET got 2: data3
LOGS 4
LOGS READ TARGET DONE
HOST END
It seems to me there is no way to accomplish this without changing target.c
. This can be known from how in Google Kickstart interactive questions they ask us to make sure we flush the output.
There is nothing to take care of target.c
flushing responsibilities from host.c
.
CodePudding user response:
A likely cause of the problem is that target.out
is running with the stdout
stream set to fully buffered _IOFBF
mode. The C execution environment is allowed to initialize the stdout
and stdin
streams to fully buffered mode if and only if the stream can be determined not to refer to an interactive device (C17 7.21.3/7). The streams do in fact refer to pipe devices and GNU libc on Linux decides that pipes are not interactive devices, so freely sets the streams to fully buffered mode.
The first I/O sequence that target.out
does is:
printf("TARGET BEGIN\n");
scanf("%s",s);
OP is hoping that the output from the printf
call will have been automatically flushed to its output pipe before the scanf
call reads from its input pipe. The GNU libc manual's section on Flushing Buffers has this as one of the circumstances when buffered output on a stream is automatically flushed:
Whenever an input operation on any stream actually reads data from its file.
It is not clear to me what it means by "actually reads" because it would seem more useful if the flush occurred before it read an input stream. (EDIT: I think "actually reads" means it has used up all previously buffered input and now needs to read from the device.) It is also not what GNU libc actually does in practice. The old GLIBC_2.0 interface has code to flush all the line buffered output streams before reading an input stream. The newer GLIBC interface is similar except it only flushes the stdout
stream (and only if it is line buffered, as for the old GLIBC_2.0 interface).
From the above, it can be determined that GNU libc is not automatically flushing stdout
to the output pipe before the scanf
call reads from the input pipe. The output from the printf
call will be stuck in the stdout
stream's buffer.
This means that host.out
and target.out
are both reading from empty pipes at the same time, so no progress can be made.
There are two possible solutions that can be implemented in target.c
:
The
stdout
stream can be set to something other than fully buffered mode. Setting it to line buffered mode will do. This should be done before the first call toprintf
:setvbuf(stdout, NULL, _IOLBF, 0);
Alternatively, the
stdout
stream could be explicitly flushed before each call toscanf
:fflush(stdout); scanf("%s",s);
Either solution should result in the output from the printf
call being flushed to the output pipe before the input pipe is read.