英文:
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 thectrlc
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 withio::stdin().read_line(&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 withloop {}
. - In order to do that, I need to register the
^C
handler to use theJoinHandle<()>
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<()>
either or useas_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("should be able to set Ctrl-C handler");
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 while executing the main thread: {:?}", 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() => {
println!("Ctrl-C received, exiting: {:?}", error);
break;
},
res = loop_exec() => match res {
Ok(_) => {
println!("Success")
},
Err(e) => {
println!("Error in 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("Failed to read line");
// 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<Option>
中,或者使用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<Option>
, 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(&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> {
// Take input from stdin.
kill_execution.store(false, Ordering::Relaxed);
// And every some operations:
if kill_execution.load(Ordering::Relaxed) {
return Err("Ctrl+C pressed".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! {
_ = &mut ctrlc => break,
res = loop_exec() => match res {
Ok(_) => {},
Err(e) => {
println!("Error in loop_exec: {:?}", 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'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() -> 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!(
"{}@{}:{} ",
// get username,
// get hotsname,
// get CWD
);
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,
..
}) => {
// send buffer and run command
}
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;
}
}
};
}
}
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论