如何跨终端监控交互式C程序的按键输入?现有kbhit方案无效
Hey there! Let's break down why your current observer setup isn't catching keystrokes from another terminal, then walk through a few practical solutions to fix this.
Why Your Current Code Fails
Your existing kbhit() and select() logic is only listening to your observer program's own standard input (stdin, file descriptor 0). Every terminal window is a separate TTY device (think /dev/pts/0, /dev/pts/1, etc.), and their input streams are completely isolated. So your observer can only see keys pressed in its own terminal, not the one running program1.
Solution 1: Use a Named Pipe (FIFO)
This is the easiest approach if you can modify program1. The idea is to have program1 write every keystroke to a shared pipe, and your observer watches that pipe for new data.
Step 1: Create the FIFO
Run this command in your terminal to make a shared pipe:
mkfifo /tmp/key_pipe
Step 2: Update program1 to Write Keystrokes to the Pipe
Modify your interactive program to send each valid digit to the pipe:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { // Open the pipe for writing int pipe_fd = open("/tmp/key_pipe", O_WRONLY); if (pipe_fd == -1) { perror("Failed to open pipe"); return 1; } char ch; printf("Enter digits (0-9):\n"); while ((ch = getchar()) != EOF) { // Only send valid 0-9 digits if (ch >= '0' && ch <= '9') { write(pipe_fd, &ch, 1); } } close(pipe_fd); return 0; }
Step 3: Rewrite the Observer to Watch the Pipe
Replace your observer's logic to listen to the pipe instead of its own stdin:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/select.h> void press(char key) { printf("Detected key press: %c !!!\n", key); } int main() { // Open the pipe in non-blocking read mode int pipe_fd = open("/tmp/key_pipe", O_RDONLY | O_NONBLOCK); if (pipe_fd == -1) { perror("Failed to open pipe"); return 1; } fd_set read_fds; char ch; while (1) { FD_ZERO(&read_fds); FD_SET(pipe_fd, &read_fds); // Wait for data to arrive in the pipe int status = select(pipe_fd + 1, &read_fds, NULL, NULL, NULL); if (status > 0 && FD_ISSET(pipe_fd, &read_fds)) { ssize_t bytes_read = read(pipe_fd, &ch, 1); if (bytes_read > 0) { press(ch); } } } close(pipe_fd); return 0; }
How to Use
- Launch your observer first:
./observer_prog - Open a new terminal and run
program1:./program1 - Type digits into
program1's terminal—your observer will instantly detect and report each keystroke.
Solution 2: Monitor the Target Terminal's TTY Device
If you can't modify program1, you can directly watch the TTY device that program1 is running in.
Step 1: Find program1's TTY Path
In the terminal running program1, run:
tty
You'll get a path like /dev/pts/2—this is the device we'll monitor.
Step 2: Update the Observer to Watch the Target TTY
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <sys/select.h> #include <termios.h> void press(char key) { printf("Detected key press: %c !!!\n", key); } int main() { // Replace this with the TTY path from the step above const char* target_tty = "/dev/pts/2"; // Open the TTY in non-blocking read mode int tty_fd = open(target_tty, O_RDONLY | O_NONBLOCK); if (tty_fd == -1) { perror("Failed to open target TTY"); return 1; } // Adjust TTY settings to read keystrokes in real-time struct termios tty_attr; tcgetattr(tty_fd, &tty_attr); tty_attr.c_lflag &= ~(ICANON | ECHO); // Disable canonical mode and echo tty_attr.c_cc[VMIN] = 1; // Wait for at least 1 character tty_attr.c_cc[VTIME] = 0; // No timeout tcsetattr(tty_fd, TCSANOW, &tty_attr); fd_set read_fds; char ch; while (1) { FD_ZERO(&read_fds); FD_SET(tty_fd, &read_fds); int status = select(tty_fd + 1, &read_fds, NULL, NULL, NULL); if (status > 0 && FD_ISSET(tty_fd, &read_fds)) { ssize_t bytes_read = read(tty_fd, &ch, 1); if (bytes_read > 0) { press(ch); } } } close(tty_fd); return 0; }
Notes
- You might need to run the observer with
sudoto access the target TTY (unless both terminals are owned by the same user with proper permissions). - This will capture all input to the target terminal (like Enter keys), so you'll want to add logic to filter for just 0-9 digits if needed.
Solution 3: Use a UNIX Domain Socket
For more robust communication (especially if you might have multiple observers or need to handle disconnections), a UNIX domain socket is a great choice.
Step 1: Update program1 to Send Keystrokes to the Socket
#include <stdio.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <string.h> #define SOCKET_PATH "/tmp/key_socket" int main() { int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (sock_fd == -1) { perror("Failed to create socket"); return 1; } struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { perror("Failed to connect to observer"); close(sock_fd); return 1; } char ch; printf("Enter digits (0-9):\n"); while ((ch = getchar()) != EOF) { if (ch >= '0' && ch <= '9') { send(sock_fd, &ch, 1, 0); } } close(sock_fd); return 0; }
Step 2: Rewrite the Observer to Listen on the Socket
#include <stdio.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <sys/select.h> #include <string.h> #define SOCKET_PATH "/tmp/key_socket" void press(char key) { printf("Detected key press: %c !!!\n", key); } int main() { // Clean up any existing socket file unlink(SOCKET_PATH); int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (sock_fd == -1) { perror("Failed to create socket"); return 1; } struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (bind(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { perror("Failed to bind socket"); close(sock_fd); return 1; } if (listen(sock_fd, 1) == -1) { perror("Failed to listen for connections"); close(sock_fd); return 1; } // Accept connection from program1 int client_fd = accept(sock_fd, NULL, NULL); if (client_fd == -1) { perror("Failed to accept connection"); close(sock_fd); return 1; } fd_set read_fds; char ch; while (1) { FD_ZERO(&read_fds); FD_SET(client_fd, &read_fds); int status = select(client_fd + 1, &read_fds, NULL, NULL, NULL); if (status > 0 && FD_ISSET(client_fd, &read_fds)) { ssize_t bytes_read = recv(client_fd, &ch, 1, 0); if (bytes_read > 0) { press(ch); } else { // Reconnect if program1 restarts close(client_fd); client_fd = accept(sock_fd, NULL, NULL); if (client_fd == -1) { perror("Failed to accept new connection"); break; } } } } close(client_fd); close(sock_fd); unlink(SOCKET_PATH); return 0; }
Quick Recommendation
- If you can modify
program1, go with the named pipe—it's simple, requires minimal code changes, and works reliably. - If you can't touch
program1, the TTY monitoring approach is your best bet, just be mindful of permissions and input filtering. - For more complex setups, the UNIX domain socket offers better reliability and flexibility.
内容的提问来源于stack exchange,提问作者Pert8S

