crossterm icon indicating copy to clipboard operation
crossterm copied to clipboard

Expose waker to wake up blocking read.

Open lotabout opened this issue 4 years ago • 6 comments

Is your feature request related to a problem? Please describe. I'm trying to port my CLI util skim to crossterm for windows support. Skim is a fuzzy matching util that accept user query interactively. That means skill will have a thread accepting user inputs.

Skill will try to free up all the resources on exit, meaning trying to notify the input thread to break out of the read loop.

One alternative solution would be using non-blocking read with a timeout and check a break flag repeatedly. That won't be nice:

  • In order to respond quickly, the timeout is the smaller the better.
  • With smaller timeout, the thread is waked up repeatedly even when there is no input all all(e.g. the whole terminal was left background). Wasting CPU cycles.

Describe the solution you'd like It would be better to provide a way to interrupt a blocking read of event.

Describe alternatives you've considered in any Current implementation in skim would use a self-pipe trick: read from /dev/tty and another piped input, and use select to wait for it. Thus we could write something to the pipe to wake up the blocking read.

Not sure how it could be done on Windows though.

lotabout avatar Mar 10 '20 14:03 lotabout

Crossterm supports interrupting the read. Though it can currently only be accessed from the internal code. This waker was implemented for asynchronous behavior. The asynchronous code uses a type called Waker which can wake up a blocking read call.

On the drop of the stream, we awake the read. Closing the underlying system handles. https://github.com/crossterm-rs/crossterm/blob/master/src/event/stream.rs#L125

TimonPost avatar Mar 10 '20 15:03 TimonPost

Crossterm allows you to poll for event readiness with poll. This can be used in combination with read to prevent read from blocking.

Alternatively, you can set up a loop in your input thread. This loop should call poll with a certain duration. Like poll(Duration::from_millis(100) which will poll 100 millis of incoming events. After the 100 millis you can do a check if the thread needs to be cleaned up. Then you loop again.

see: https://github.com/crossterm-rs/crossterm/blob/master/examples/event-poll-read.rs#L28

TimonPost avatar Mar 10 '20 15:03 TimonPost

@TimonPost Thanks for the detailed explanation. Let me confirm my understanding:

This waker was implemented for asynchronous behavior. The asynchronous code uses a type called Waker which can wake up a blocking read call.

EventStream would only work with async functions right? Is it possible to turn it into a blocking call just like read while remain the possibility to wake up?(examples would be great!) Better not let one async code "pollute" the sync spaces.

Crossterm allows you to poll for event readiness with poll. This can be used in combination with read to prevent read from blocking.

That would be the "obvious" solution. While I don't like polling since it would waste CPU cycles event when there is no user inputs at all.

Finally here is what I'd like to have:

let (rx, tx) = channel();
thread::spawn(|| {
    loop {
        if let Ok(event) = read() {
            tx.send(wrap(event));
        } else {
            // interrupted.
            break;
        }
    }
}

thread::spawn(|| {
    while let Ok(event) = rx.recv() {
        match event {
        Event::Quit => {
            // >> interrupt the input handling thread. <<
        }
         _ => {do something}
        }
    }
})

// rx could be used by the user to send other events.
return rx;

lotabout avatar Mar 11 '20 09:03 lotabout

Is it possible to turn it into a blocking call just like read while remaining the possibility to wake up?

I don't think so, it is based on futures stream and needs a reactor to work. The poll was made for sync environments. You can reduce CPU-cycles by just adding a delay of 5 seconds. Then every 5 seconds your program gets the change to close the input reading thread.

An alternative way would be to expose the waker code and allow a user to get an instance to this awaker.

TimonPost avatar Mar 11 '20 10:03 TimonPost

You can reduce CPU-cycles by just adding a delay of 5 seconds

This solution is actually trade off and won't fit in my scenario.

  • We need to wait for the blocking thread to quit, the input source(/dev/tty) might be reused by another thread.
  • The longer we wait, the less responsive the application would be. Wait less, and the more CPU cycles are wasted

Anyway. Is there any plan to support interrupting the blocking read method?

lotabout avatar Mar 13 '20 01:03 lotabout

No direct plans no. The waker can be exposed. But it is a static global which I would like to keep private. However, I do follow your line of reasoning. I will put it on the feature list.

TimonPost avatar Apr 09 '20 17:04 TimonPost