Combine interactive input from stdin with async output to stdout Combine interactive input from stdin with async output to stdout unix unix

Combine interactive input from stdin with async output to stdout


Below is the final solution that I came up with. It's actually a working example that spawns N threads and emits logs from each of them. Meanwhile interactive user is allowed to enter commands. The only supported command is "exit", though. Other commands are silently ignored. It has two minor (in my case) flaws.

First one is that command prompt has to be on a separate line. Like that:

Command:reboo_

The reason for that is VREPRINT control character that also emits a new line. So I didn't find a way how to reprint the current input buffer without that new line.

Second is some occasional flickering when symbol is entered in the same time when log line is printed. But despite that flickering the end result is consistent and no lines overlap is observed. Maybe I will figure out how to avoid it later to make it smooth and clean, but it's already good enough.

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <pthread.h>#include <sys/termios.h>#include <sys/ioctl.h>static const char *const c_prompt = "Command: ";static pthread_mutex_t g_stgout_lock = PTHREAD_MUTEX_INITIALIZER;void log(const char *const msg){    pthread_mutex_lock(&g_stgout_lock);    // \033[1A - move cursor one line up    // \r      - move cursor to the start of the line    // \033[K  - erase from cursor to the end of the line    const char preface[] = "\033[1A\r\033[K";    write(STDOUT_FILENO, preface, sizeof(preface) - 1);    fprintf(stderr, "%s\n", msg);    fflush(stdout);    const char epilogue[] = "\033[K";    write(STDOUT_FILENO, epilogue, sizeof(epilogue) - 1);    fprintf(stdout, "%s", c_prompt);    fflush(stdout);    struct termios tc;    tcgetattr(STDOUT_FILENO, &tc);    const tcflag_t lflag = tc.c_lflag;    // disable echo of control characters    tc.c_lflag &= ~ECHOCTL;    tcsetattr(STDOUT_FILENO, TCSANOW, &tc);    // reprint input buffer    ioctl(STDOUT_FILENO, TIOCSTI, &tc.c_cc[VREPRINT]);    tc.c_lflag = lflag;    tcsetattr(STDOUT_FILENO, TCSANOW, &tc);    pthread_mutex_unlock(&g_stgout_lock);}void *thread_proc(void *const arg){    const size_t i = (size_t)arg;    char ts[16];    char msg[64];    for (;;)    {        const useconds_t delay = (1.0 + rand() / (double)RAND_MAX) * 1000000;        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, 0);        usleep(delay);        pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, 0);        time_t t;        time(&t);        ts[strftime(ts, sizeof(ts), "%T", localtime(&t))] = 0;        snprintf(msg, sizeof(msg), "%s - message from #%zu after %lluns",                 ts, i, (unsigned long long)delay);        log(msg);    }}int main(){    const size_t N = 4;    pthread_t threads[N];    for (size_t i = N; 0 < i--;)    {        pthread_create(threads + i, 0, thread_proc, (void *)i);    }    char *line;    size_t line_len;    for (;;)    {        pthread_mutex_lock(&g_stgout_lock);        fprintf(stdout, "%s\n", c_prompt);        fflush(stdout);        pthread_mutex_unlock(&g_stgout_lock);        line = fgetln(stdin, &line_len);        if (0 == line)        {            break;        }        if (0 == line_len)        {            continue;        }        line[line_len - 1] = 0;        line[strcspn(line, "\n\r")] = 0;        if (0 == strcmp("exit", line))        {            break;        }    }    for (size_t i = N; 0 < i--;)    {        pthread_cancel(threads[i]);        pthread_join(threads[i], 0);    }    return 0;}

Links on the relevant documentation that was used:


Here is something I do. Open 3 consoles:

Console #1: (run the program, input std::cin)

> ./program > output.txt 2> errors.txt

Console #2: (view std::cout)

> tail -f output.txt

Console #3: (view std::cerr)

> tail -f errors.txt

Any program input is typed into Console: #1.

You can get some consoles like Terminator that allow you to split the screen into separate sections:

enter image description here


Following from the update to the question you may want to look at using the readline library:

It partitions off the bottom line for user input and outputs everything to the line above it. It also provides a configurable prompt and even has functions to record a typing history for the input.

Here is an example that you may be able to draw inspiration from for your log() function:

#include <cstdlib>#include <memory>#include <iostream>#include <algorithm>#include <readline/readline.h>#include <readline/history.h>struct malloc_deleter{    template <class T>    void operator()(T* p) { std::free(p); }};using cstring_uptr = std::unique_ptr<char, malloc_deleter>;std::string& trim(std::string& s, const char* t = " \t"){    s.erase(s.find_last_not_of(t) + 1);    s.erase(0, s.find_first_not_of(t));    return s;}int main(){    using_history();    read_history(".history");    std::string shell_prompt = "> ";    cstring_uptr input;    std::string line, prev;    input.reset(readline(shell_prompt.c_str()));    while(input && trim(line = input.get()) != "exit")    {        if(!line.empty())        {            if(line != prev)            {                add_history(line.c_str());                write_history(".history");                prev = line;            }            std::reverse(line.begin(), line.end());            std::cout << line << '\n';        }        input.reset(readline(shell_prompt.c_str()));    }}

This simple example just reverses everything you type at the console.