取消`read_line`使用`^C`

huangapple go评论108阅读模式
英文:

Canceling `read_line` with `^C`

问题

以下是翻译好的部分:

我正在编写一个Shell解释器作为一种爱好,以学习Rust。

以下是关于这段代码的一些事实:

  • 为了捕获用户输入^C并取消他们的输入,我使用了ctrlc库,因为它似乎很受欢迎。
  • 据我所知,loop_exec()需要是一个future。这是我的Shell等待用户输入的地方,使用io::stdin().read_line(&mut buf)。据我所知,它无法被取消。
  • 因此,我希望运行loop_exec()的future,在用户按下^C时终止它,并使用loop {}创建另一个。
  • 为了做到这一点,我需要注册^C处理程序以使用future(execution)的JoinHandle<()>
  • 很明显,我不能在两个地方使用它(我必须用它来注册处理程序并等待它),根据Rust的工作方式(借用检查器)。但是,我也不能克隆execution: JoinHandle<()>,也不能使用as_bytes()。我假设这是为了防止开发人员自己搞砸,或者做一些本不应该发生的事情。

因此,我在这里有点困惑。你是否预料到这一点并采取了完全不同的方法,或者我是否只是缺少一些信息?也许我在这个问题上缺少了很多知识。

在这种情况下,你会如何处理事情,或者更广泛地说,你会如何处理?

#[tokio::main]
async fn main() {
    let mut kill_execution = false;

    ctrlc::set_handler(move || {
        kill_execution = true;
    })
    .expect("应该能够设置Ctrl-C处理程序");

    loop {
        let execution = tokio::spawn(loop_exec());

        let killer = tokio::spawn(async move {
            while !kill_execution {}
            kill_execution = false;
            execution.abort();
        });

        match execution.await {
            Ok(_) => {}
            Err(error) => {
                println!("执行主线程时出错:{:?}", error);
            }
        }
    }
}

编辑

根据下面的综合答案,我简化了代码如下:

#[tokio::main]
async fn main() {
    loop {
        tokio::select! {
            error = tokio::signal::ctrl_c() => {
                println!("收到Ctrl-C,退出:{:?}", error);
                break;
            },
            res = loop_exec() => match res {
                Ok(_) => {
                    println!("成功")
                },
                Err(e) => {
                    println!("loop_exec中出现错误:{:?}", e);
                    break;
                }
            }
        }
    }
}

async fn loop_exec() -> Result<(), Error> {
    let stdin = io::stdin();
    let mut input = String::new();

    stdin.read_line(&mut input).expect("读取行失败");

    // 进行一些操作

    Ok(())
}

问题仍然部分未知:^C不会触发tokio::signal::ctrl_c()一旦监听程序已初始化

英文:

I'm writing a shell interpreter as a hobby in order to learn Rust.

Here are a few facts about this code:

  • In order to catch when users enter ^C and cancel their input, I use the ctrlc crate, as it seems popular.
  • To my knowledge, the loop_exec() need to be a future. This is where my shell awaits for user input with io::stdin().read_line(&amp;mut buf). Still to my knowledge, it can't be canceled.
  • I hence want to run the loop_exec() future, kill it when users hit ^C, and make another with loop {}.
  • In order to do that, I need to register the ^C handler to use the JoinHandle&lt;()&gt; of the future (execution).
  • I obviously can't use it at two places (I have to use it to register the handler AND to await it) as per how Rust works (borrow-checker). However, I can't clone execution: JoinHandle&lt;()&gt; either or use as_bytes(). I'm assuming this is in order to protect devs from shooting themselves in the foot, or do things that are never supposed to happen.

Hence, I'm kind of stuck here. Would you have seen that coming and taken a whole different approach, or do I miss just a bit of information ? Heck I am probably missing a whole lot of knowledge in order to arrive at that point.

How would you do things in this situation or more globally ?

#[tokio::main]
async fn main() {
    let mut kill_execution = false;

    ctrlc::set_handler(move || {
        kill_execution = true;
    })
    .expect(&quot;should be able to set Ctrl-C handler&quot;);

    loop {
        let execution = tokio::spawn(loop_exec());

        let killer = tokio::spawn(async move {
            while !kill_execution {}
            kill_execution = false;
            execution.abort();
        });

        match execution.await {
            Ok(_) =&gt; {}
            Err(error) =&gt; {
                println!(&quot;Error while executing the main thread: {:?}&quot;, error);
            }
        }
    }
}

Edit

With the combined answers below, I simplified the code to the following:

#[tokio::main]
async fn main() {
    loop {
        tokio::select! {
            error = tokio::signal::ctrl_c() =&gt; {
                println!(&quot;Ctrl-C received, exiting: {:?}&quot;, error);
                break;
            },
            res = loop_exec() =&gt; match res {
                Ok(_) =&gt; {
                    println!(&quot;Success&quot;)
                },
                Err(e) =&gt; {
                    println!(&quot;Error in loop_exec: {:?}&quot;, e);
                    break;
                }
            }
        }
    }
}

async fn loop_exec() -&gt; Result&lt;(), Error&gt; {
    let stdin = io::stdin();
    let mut input = String::new();

    stdin.read_line(&amp;mut input).expect(&quot;Failed to read line&quot;);

    // do stuff

    Ok(())
}

The problem is still partially to be identified: ^C won't trigger tokio::signal::ctrl_c() once the listener has been initialized.

答案1

得分: 1

有多种可能的解决方案来解决这个问题(例如,将JoinHandle包装在Mutex&lt;Option&gt;中,或者使用select来中止它),但你似乎误解了JoinHandle::abort():它将在下一个.await点处取消任务。所以如果你的函数执行计算密集型任务(比如解释器),它将不会执行任何操作。

相反,你需要设置一个AtomicBool,并在每个操作之后检查它(例如,在每个字节码操作处)。你还应该使用tokio自己的信号处理,而不是ctrlc库,所以代码看起来像这样:

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let kill_execution = Arc::new(AtomicBool::new(false));

    tokio::spawn({
        let kill_execution = Arc::clone(&kill_execution);
        async move {
            loop {
                tokio::signal::ctrl_c()
                    .await
                    .expect("failed to listen for Ctrl+C");
                kill_execution.store(true, Ordering::Relaxed);
            }
        }
    });

    loop {
        let execution = tokio::spawn(loop_exec(Arc::clone(&kill_execution)));

        match execution.await {
            Ok(_) => {}
            Err(error) => {
                println!("Error while executing the main thread: {:?}", error);
            }
        }
    }
}

async fn loop_exec(kill_execution: Arc<AtomicBool>) -> Result<(), String> {
    // 从标准输入获取输入。

    kill_execution.store(false, Ordering::Relaxed);
    
    // 并在每个操作之后:
    if kill_execution.load(Ordering::Relaxed) {
        return Err("Ctrl+C pressed".to_owned())
    }
}

希望这对你有所帮助。如果你有任何其他问题,请随时提出。

英文:

There are multiple possible solutions to this problem (for example, wrapping the JoinHandle in Mutex&lt;Option&gt;, or using a select to abort it) but you seem to misunderstand JoinHandle::abort(): it will cancel the task at the next .await point. So if your function does a computationally-intensive task (such as an interpreter), it won't do anything.

Instead, you need to set an AtomicBool, and check it every few operations (for example, at each bytecode operation). You should also use tokio's own signal handling and not the ctrlc crate, so this will look like this:

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let kill_execution = Arc::new(AtomicBool::new(false));

    tokio::spawn({
        let kill_execution = Arc::clone(&amp;kill_execution);
        async move {
            loop {
                tokio::signal::ctrl_c()
                    .await
                    .expect(&quot;failed to listen for Ctrl+C&quot;);
                kill_execution.store(true, Ordering::Relaxed);
            }
        }
    });

    loop {
        let execution = tokio::spawn(loop_exec(Arc::clone(&amp;kill_execution)));

        match execution.await {
            Ok(_) =&gt; {}
            Err(error) =&gt; {
                println!(&quot;Error while executing the main thread: {:?}&quot;, error);
            }
        }
    }
}

async fn loop_exec(kill_execution: Arc&lt;AtomicBool&gt;) -&gt; Result&lt;(), String&gt; {
    // Take input from stdin.

    kill_execution.store(false, Ordering::Relaxed);
    
    // And every some operations:
    if kill_execution.load(Ordering::Relaxed) {
        return Err(&quot;Ctrl+C pressed&quot;.to_owned())
    }
}

答案2

得分: 1

Tokio 提供了一个在 Ctrl+C 时解析的 future,tokio::signal::ctrl_c()。这与 tokio::select! 结合使用,使得编写所需的内容非常简单:

#[tokio::main]
async fn main() {
    let mut ctrlc = tokio::spawn(tokio::signal::ctrl_c());

    loop {
        tokio::select! {
            _ = &mut ctrlc => break,
            res = loop_exec() => match res {
                Ok(_) => {},
                Err(e) => {
                    println!("loop_exec 中出现错误: {:?}", e);
                    break;
                }
            } 
        }
    }
}
英文:

Tokio provides a future which resolves on Ctrl+C, tokio::signal::ctrl_c(). This, combined with tokio::select!, makes it super straightforward to write what you want:

#[tokio::main]
async fn main() {
    let mut ctrlc = tokio::spawn(tokio::signal::ctrl_c());

    loop {
        tokio::select! {
            _ = &amp;mut ctrlc =&gt; break,
            res = loop_exec() =&gt; match res {
                Ok(_) =&gt; {},
                Err(e) =&gt; {
                    println!(&quot;Error in loop_exec: {:?}&quot;, e);
                    break;
                }
            } 
        }
    }
}

答案3

得分: 0

以下是已翻译的部分:

# 最终编辑

根据以下多个答案,这是我采用的最终代码片段:

```rust
use std::io::{stdout, Stdout, Write};
use std::time::Duration;

use crossterm::event::KeyEventKind::Release;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crossterm::queue;
use crossterm::style::Print;
use crossterm::terminal::{self, ClearType};

fn setup_term() -> Stdout {
    let stdout = stdout();
    terminal::enable_raw_mode().unwrap();
    stdout
}

fn reset_term(stdout: &mut Stdout) {
    terminal::disable_raw_mode().unwrap();
    queue!(stdout, terminal::Clear(ClearType::CurrentLine)).unwrap();
    print!("\r");
    stdout.flush().unwrap();
}

fn print_prompt(stdout: &mut Stdout, prompt: String, buff: String) {
    reset_term(stdout);
    queue!(
        stdout,
        Print(format!("{} ({}) {}", prompt, buff.len(), buff))
    )
    .unwrap();
    stdout.flush().unwrap();
}

fn main() {
    term();
}

fn term() {
    let mut buff = String::new();
    let mut stdout = setup_term();

    let prompt = format!(
        "{}@{}:{} ",
        // 获取用户名,
        // 获取主机名,
        // 获取当前工作目录
    );

    print_prompt(&mut stdout, prompt.clone(), buff.clone());

    loop {
        if event::poll(Duration::from_millis(10)).unwrap() {
            match event::read().unwrap() {
                Event::Key(KeyEvent { kind: Release, .. }) => {
                    continue;
                }
                Event::Key(KeyEvent {
                    code: KeyCode::Enter,
                    ..
                }) => {
                    // 发送缓冲并运行命令
                }
                Event::Key(KeyEvent {
                    code: KeyCode::Char('c'),
                    modifiers: KeyModifiers::CONTROL,
                    kind: KeyEventKind::Press,
                    ..
                }) => {
                    reset_term(&mut stdout);
                    std::process::exit(0);
                }
                Event::Key(KeyEvent {
                    code: KeyCode::Char(char),
                    ..
                }) => {
                    buff.push(char);
                    print_prompt(&mut stdout, prompt.clone(), buff.clone());
                }
                _ => {
                    continue;
                }
            }
        };
    }
}

值得注意的是:

  • 在这种情况下使用 futures 没有用处
  • 使用 crossterm 或类似的库允许我自由控制 TUI 的反应
  • ...并且使用 crossterm 的键盘事件允许我监听退格、箭头和 ^C,非常完美。

因此,最终,取消 read_lines 不是一个选项。相反,我正在实现自己的键盘读取机制。

如前所述,coreutils 的 more 是我用例的完美示例。


<details>
<summary>英文:</summary>
# Final edit
Following the multiple answers below, this is the final snippet I&#39;m going with:
```rust
use std::io::{stdout, Stdout, Write};
use std::time::Duration;
use crossterm::event::KeyEventKind::Release;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use crossterm::queue;
use crossterm::style::Print;
use crossterm::terminal::{self, ClearType};
fn setup_term() -&gt; Stdout {
let stdout = stdout();
terminal::enable_raw_mode().unwrap();
stdout
}
fn reset_term(stdout: &amp;mut Stdout) {
terminal::disable_raw_mode().unwrap();
queue!(stdout, terminal::Clear(ClearType::CurrentLine)).unwrap();
print!(&quot;\r&quot;);
stdout.flush().unwrap();
}
fn print_prompt(stdout: &amp;mut Stdout, prompt: String, buff: String) {
reset_term(stdout);
queue!(
stdout,
Print(format!(&quot;{} ({}) {}&quot;, prompt, buff.len(), buff))
)
.unwrap();
stdout.flush().unwrap();
}
fn main() {
term();
}
fn term() {
let mut buff = String::new();
let mut stdout = setup_term();
let prompt = format!(
&quot;{}@{}:{} &quot;,
// get username,
// get hotsname,
// get CWD
);
print_prompt(&amp;mut stdout, prompt.clone(), buff.clone());
loop {
if event::poll(Duration::from_millis(10)).unwrap() {
match event::read().unwrap() {
Event::Key(KeyEvent { kind: Release, .. }) =&gt; {
continue;
}
Event::Key(KeyEvent {
code: KeyCode::Enter,
..
}) =&gt; {
// send buffer and run command
}
Event::Key(KeyEvent {
code: KeyCode::Char(&#39;c&#39;),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
}) =&gt; {
reset_term(&amp;mut stdout);
std::process::exit(0);
}
Event::Key(KeyEvent {
code: KeyCode::Char(char),
..
}) =&gt; {
buff.push(char);
print_prompt(&amp;mut stdout, prompt.clone(), buff.clone());
}
_ =&gt; {
continue;
}
}
};
}
}

Notable points:

  • Using futures was useless in this context
  • Using crossterm or a similar crate allows me to have free control over how the tui reacts
  • ...and using crossterm's key events allow me to listen to backspaces, arrows, and ^C, which is perfect.

So, in the end, canceling read_lines is not an option. Rather, I'm implementing my own key reading mechanism.

As mentioned before, coreutils' more was a perfect example for y use case.

huangapple
  • 本文由 发表于 2023年7月7日 04:26:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/76632333.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定