I am trying to create an interactive zsh session over sockets. To support the command of sudo for example I require a pseudoterminal.
How can I only read the stdout / stderr from a pseudo terminal? Currently my code also reads the data sent to stdin because all of the file descriptors are copied to the master fd by the forkpty function.
This is my current code.
#include <stdio.h>
#include <unistd.h>
#include <array>
#include <util.h>
#include <iostream>
// Test.
int main(int argc, char** args) {
// Error.
auto err = [](const char* msg) {
std::cout << msg << "\n";
return 1;
};
// Vars.
int pid, ptyfd, child, nread, pipepid;
int pipe[2];
char buf[512];
int buffsize = 512;
// Driver pipe.
if (::pipe(pipe) < 0) {
return err("Pipe error.");
}
// Fork parent.
if ((pid = ::forkpty(&ptyfd, NULL, NULL, NULL)) > 0) {
// ::close(pipe[0]);
// ::dup2(pipe[1], STDIN_FILENO);
// ::dup2(pipe[1], STDOUT_FILENO);
}
// Fork child.
else if (pid == 0) {
// ::close(pipe[1]);
// ::dup2(pipe[0], STDIN_FILENO);
// ::dup2(pipe[0], STDOUT_FILENO);
execl("/bin/zsh", "-exec", "-c", "zsh -l -i");
exit(0);
}
// Fork error.
else {
return err("Fork error.");
}
// Loop parent.
if ((child = fork()) > 0) {
// ::dup2(pipe[1], STDIN_FILENO);
// ::dup2(pipe[1], STDOUT_FILENO);
while (true) {
if ((nread = ::read(ptyfd, buf, buffsize)) <= 0) {
std::cout << "Parent stop" << "\n";
break; /* signal caught, error, or EOF */
}
if (::write(STDOUT_FILENO, buf, nread) != nread) {
return err("writen error to stdout");
}
}
}
// Loop child.
else if (child == 0) {
while (true) {
if ((nread = ::read(STDIN_FILENO, buf, buffsize)) < 0) {
return err("Child read error.");
} else if (nread == 0) {
std::cout << "Child stop" << "\n";
break;
} else if (::write(ptyfd, buf, nread) != nread) {
return err("writen error to master pty");
}
}
exit(0);
}
// Loop error.
else {
return err("Loop error.");
}
return 0;
}
So when i execute the program everything works and a zsh session is created. But when I send command ls for example to the session. The output is something like ls\n dir1 dir2\n. Therefore this includes the data sent to stdin ls. How can I only read the stdout?
Update:
I have updated my code to use pipes but when a try a command with sudo I get the error message sudo: a terminal is required to read the password; either use the -S option to read from standard input or configure an askpass helper (macos). Why is this code not creating a valid tty session?
Code:
#include <stdio.h>
#include <unistd.h>
#include <array>
#include <util.h>
#include <iostream>
// Test.
int main(int argc, char** args) {
// Error.
auto err = [](const char* msg) {
std::cout << msg << "\n";
return 1;
};
// Vars.
int pid, ptyfd, child, nread, pipepid;
int rpipe[2], wpipe[2];
char buf[512];
int buffsize = 512;
// Driver pipe.
if (::pipe(rpipe) < 0 || ::pipe(wpipe) < 0) {
return err("Pipe error.");
}
// Fork parent.
if ((pid = ::forkpty(&ptyfd, NULL, NULL, NULL)) > 0) {
}
// Fork child.
else if (pid == 0) {
::dup2(rpipe[0], STDIN_FILENO);
::dup2(wpipe[1], STDOUT_FILENO);
::dup2(wpipe[1], STDERR_FILENO);
execl("/bin/zsh", "-exec", "-c", "zsh -l -i");
exit(0);
}
// Fork error.
else {
return err("Fork error.");
}
// Loop parent.
if ((child = fork()) > 0) {
while (true) {
if ((nread = ::read(wpipe[0], buf, buffsize)) <= 0) {
std::cout << "Parent stop" << "\n";
break; /* signal caught, error, or EOF */
}
if (::write(STDOUT_FILENO, buf, nread) != nread) {
return err("writen error to stdout");
}
}
}
// Loop child.
else if (child == 0) {
while (true) {
if ((nread = ::read(STDIN_FILENO, buf, buffsize)) < 0) {
return err("Child read error.");
} else if (nread == 0) {
std::cout << "Child stop" << "\n";
break;
} else if (::write(rpipe[1], buf, nread) != nread) {
return err("writen error to master pty");
}
}
exit(0);
}
// Loop error.
else {
return err("Loop error.");
}
return 0;
}
The shown code only reads
stdout. This is a done deal.No, it doesn't. The child process runs in a pseudo-terminal. Stop and think what happens when you use your own, regular shell, type
lsand hit enter. Where does thelscome from, that you see in your terminal? The keyboard is not hardwired, in some way, to your terminal shell, and thus typing in a command sends it both to the terminal and the shell, that reads it.The shell puts what it knows as its terminal into "raw" mode, turning off echo, and reads keystrokes, one by one, and echoes them back to the terminal.
zsh, and friends, read keyboard input, and immediately echo it to provide visual feedback. That's howzshand other shells work. They handle terminal input themselves.Your forked
zshreads what it thinks is terminal input, "ls\n", obediently echoes it back to what it thinks is terminal output, then runs the command. This is what you see.There are no convenient means to alter this process. If your goal is to run a program and capture only its standard output you do that the old-fashioned way: fork and exec the program, directly, with standard output in the child process attached to a pipe that your parent process reads.