3

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_
4
  • You can reimplement what curses would do, but anything else is likely to lead to problems. If you rule out the most nearly sane answer, you've got problems. You will have to manage the writing to stderr very carefully, coordinated with what is written to stdout very carefully. In fact, you probably need a full screen management package, possibly with sub-windows for different parts of the screen — like curses gives you. Failing that, you're going to have to intercept all the output to stderr somehow (separate thread?) and have that handle it. Could you write the errors to a log file? Commented May 24, 2015 at 4:03
  • I could writes errors anywhere, but I need them to be on the console too :) Commented May 24, 2015 at 4:16
  • In those circumstances, I strongly recommend using curses unless you wish to reimplement it for your own purposes. Otherwise, you have to write code which determines (or knows) where the cursor is, moves the write position (cursor) to the line where you want the errors to go, write the error, and then move the cursor back to where it was before you started writing the error. It can be done; curses provides mechanisms that do it automatically, so you can do what curses does too. But it ain't trivial to do it for yourself. Commented May 24, 2015 at 4:20
  • I think you really need a lib or (less portable) an OS-specific API for this kind of stuff. Otherwise the output is going to be a sequential stream of characters. Commented May 24, 2015 at 4:30

3 Answers 3

3

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:

Sign up to request clarification or add additional context in comments.

Comments

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

1 Comment

I want a solution that doesn't require any actions from the user or particular environment setup. You suggestion could be useful in other cases I guess, but doesn't answers the original question.
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.

3 Comments

Unfortunately I'm quite limited in selection of the libraries for that project. Also while providing better UX, the readline library doesn't address the original issue by itself.
@wonder.mice I think readline is pretty ubiquitous, the bash shell itself uses it for its input. So if the platform has bash then it has readline.
For example, Android NDK doesn't have libreadline. Unfortunately, availability is not a major concern. Libraries also have other characteristics, like memory footprint, code size, license, dependencies, etc. Readline adds ~200Kb to memory footprint, which is too much for that project. And again, it doesn't solve the original problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.