Combine interactive input from stdin with async output to stdout

Refresh

February 2019

Views

850 time

1

My test application writes logs in stderr and uses stdin to receive interactive commands from the user. Needless to say, that any stderr output spoils user input (and command prompt) in terminal. For example, this command line (_ is a cursor position):

Command: reboo_

will become:

Command: reboo04-23 20:26:12.799 52422  2563 D [email protected]:27 started
_

after log() call.

To fix that, I want to have something like old Quake console in terminal, where logs go one line above the current input line. In other words, I want to get that instead:

04-23 20:26:12.799 52422  2563 D [email protected]:27 started
Command: reboo_

I can modify both logging code and code that reads user input. Want that to work for Linux and OS X. log() function could be invoked from different thread. The log() function is the only writer to stderr.

Other suggestion to fix that problem (spoiled input line) are welcome. I'm looking for a solution that could be implemented without additional libraries (like Curses). I tried to google that up, but realized that I need a sort of idiomatic kickoff to understand what exactly I want.

Upate

Thanks to Jonathan Leffler comment I realized that I also should mention that separating stderr and stdout is no that important. Since I control the log() function it's not a problem to make it write to stdout instead of stderr. No sure whether it makes the task easier or not, though.

Update

Crafted something that seems to work good enough:

void set_echoctl(const int fd, const int enable)
{
    struct termios tc; 
    tcgetattr(fd, &tc);
    tc.c_lflag &= ~ECHOCTL;
    if (enable)
    {   
        tc.c_lflag |= ECHOCTL;
    }   
    tcsetattr(fd, TCSANOW, &tc);
}

void log(const char *const msg)
{
        // Go to line start
        write(1, "\r", 1);
        // Erases from the current cursor position to the end of the current line
        write(1, "\033[K", strlen("\033[K"));

        fprintf(stderr, "%s\n", msg);

        // Move cursor one line up
        write(1, "\033[1A", strlen("\033[1A"));
        // Disable echo control characters
        set_echoctl(1, 0);
        // Ask to reprint input buffer
        termios tc;
        tcgetattr(1, &tc);
        ioctl(1, TIOCSTI, &tc.c_cc[VREPRINT]);
        // Enable echo control characters back
        set_echoctl(1, 1);
}

However, that doesn't support command prompt ("Command: " at the start of the input line). But probably I can have two lines for that - one for the command prompt and another for the input itself, like:

Command: 
reboo_

3 answers

2

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:

2

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

0

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.