简体   繁体   English

将标准输入的交互式输入与异步输出组合到标准输出

[英]Combine interactive input from stdin with async output to stdout

My test application writes logs in stderr and uses stdin to receive interactive commands from the user. 我的测试应用程序在stderr写入日志,并使用stdin接收来自用户的交互式命令。 Needless to say, that any stderr output spoils user input (and command prompt) in terminal. 不用说,任何stderr输出都会破坏终端中的用户输入(和命令提示符)。 For example, this command line ( _ is a cursor position): 例如,以下命令行( _是光标位置):

Command: reboo_

will become: 会变成:

Command: reboo04-23 20:26:12.799 52422  2563 D run@main.cpp:27 started
_

after log() call. log()调用之后。

To fix that, I want to have something like old Quake console in terminal, where logs go one line above the current input line. 为了解决这个问题,我想在终端中使用旧的Quake控制台,日志在当前输入行的上方一行。 In other words, I want to get that instead: 换句话说,我想得到的是:

04-23 20:26:12.799 52422  2563 D run@main.cpp: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. 希望该功能适用​​于Linux和OSX。可以从不同的线程调用log()函数。 The log() function is the only writer to stderr . log()函数是stderr的唯一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). 我正在寻找一种无需其他库(例如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. 感谢Jonathan Leffler的评论,我意识到我还应该提到分离 stderrstdout并不那么重要。 Since I control the log() function it's not a problem to make it write to stdout instead of stderr . 由于我控制log()函数,因此将其写入stdout而不是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). 但是,这不支持命令提示符(输入行开头的“ Command:”)。 But probably I can have two lines for that - one for the command prompt and another for the input itself, like: 但是可能我可以有两行-一行用于命令提示符,另一行用于输入本身,例如:

Command: 
reboo_

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. 这实际上是一个工作示例,它产生N个线程并从每个线程中发出日志。 Meanwhile interactive user is allowed to enter commands. 同时,允许交互式用户输入命令。 The only supported command is "exit", though. 但是,唯一受支持的命令是“ exit”。 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. 原因是VREPRINT控制字符也发出新行。 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: 打开3个控制台:

Console #1: (run the program, input std::cin) 控制台1 :(运行程序,输入std :: cin)

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

Console #2: (view std::cout) 控制台2 :(查看std :: cout)

> tail -f output.txt

Console #3: (view std::cerr) 控制台3 :(查看std :: cerr)

> tail -f errors.txt

Any program input is typed into Console: #1 . 任何程序输入都将输入到Console:#1中

You can get some consoles like Terminator that allow you to split the screen into separate sections: 您可以使用诸如Terminator控制台,该控制台允许您将屏幕拆分为单独的部分:

在此处输入图片说明

Following from the update to the question you may want to look at using the readline library: 在更新问题之后,您可能需要使用readline库进行查看:

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: 这是一个示例,您可以从中为log()函数汲取灵感:

#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. 这个简单的示例只是反转您在控制台上键入的所有内容。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM