Overhaul input thread

Remove raw/non raw distinction.

Use a pipe for input thread commands and poll stdin/pipe for events
memfd
Manos Pitsidianakis 2020-05-29 15:35:29 +03:00
parent 839c1b1eb5
commit b8261ee36a
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
7 changed files with 107 additions and 164 deletions

View File

@ -199,6 +199,13 @@ impl From<std::ffi::NulError> for MeliError {
}
}
impl From<nix::Error> for MeliError {
#[inline]
fn from(kind: nix::Error) -> MeliError {
MeliError::new(format!("{}", kind)).set_source(Some(Arc::new(kind)))
}
}
impl From<&str> for MeliError {
#[inline]
fn from(kind: &str) -> MeliError {

View File

@ -429,33 +429,28 @@ fn run_app() -> Result<()> {
_ => {debug!(&r);}
}
match r.unwrap() {
ThreadEvent::Input(Key::Ctrl('z')) => {
ThreadEvent::Input((Key::Ctrl('z'), _)) if state.mode != UIMode::Embed => {
state.switch_to_main_screen();
//_thread_handler.join().expect("Couldn't join on the associated thread");
let self_pid = nix::unistd::Pid::this();
nix::sys::signal::kill(self_pid, nix::sys::signal::Signal::SIGSTOP).unwrap();
state.switch_to_alternate_screen();
state.restore_input();
// BUG: thread sends input event after one received key
state.update_size();
state.render();
state.redraw();
},
ThreadEvent::Input(Key::Ctrl('l')) => {
/* Manual screen redraw */
state.update_size();
state.render();
state.redraw();
},
ThreadEvent::InputRaw(raw_input @ (Key::Ctrl('l'), _)) => {
ThreadEvent::Input(raw_input @ (Key::Ctrl('l'), _)) => {
/* Manual screen redraw */
state.update_size();
state.render();
state.redraw();
if state.mode == UIMode::Embed {
state.rcv_event(UIEvent::EmbedInput(raw_input));
state.redraw();
}
},
ThreadEvent::Input(k) => {
ThreadEvent::Input((k, r)) => {
match state.mode {
UIMode::Normal => {
match k {
@ -504,17 +499,15 @@ fn run_app() -> Result<()> {
},
}
},
UIMode::Embed => state.redraw(),
UIMode::Embed => {
state.rcv_event(UIEvent::EmbedInput((k,r)));
state.redraw();
},
UIMode::Fork => {
break 'inner; // `goto` 'reap loop, and wait on child.
},
}
},
ThreadEvent::InputRaw(raw_input) => {
state.rcv_event(UIEvent::EmbedInput(raw_input));
state.redraw();
},
ThreadEvent::RefreshMailbox(event) => {
state.refresh_event(*event);
state.redraw();

View File

@ -176,9 +176,6 @@ impl MailcapEntry {
a => a.to_string(),
})
.collect::<Vec<String>>();
{
context.input_kill();
}
let cmd_string = format!("{} {}", cmd, args.join(" "));
melib::log(
format!("Executing: sh -c \"{}\"", cmd_string.replace("\"", "\\\"")),
@ -236,7 +233,6 @@ impl MailcapEntry {
}
}
context.replies.push_back(UIEvent::Fork(ForkType::Finished));
context.restore_input();
Ok(())
}
}

View File

@ -37,6 +37,7 @@ use smallvec::SmallVec;
use std::collections::HashMap;
use std::env;
use std::io::Write;
use std::os::unix::io::RawFd;
use std::thread;
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
@ -45,6 +46,7 @@ use termion::{clear, cursor};
pub type StateStdout = termion::screen::AlternateScreen<termion::raw::RawTerminal<std::io::Stdout>>;
struct InputHandler {
pipe: (RawFd, RawFd),
rx: Receiver<InputCommand>,
tx: Sender<InputCommand>,
}
@ -54,36 +56,27 @@ impl InputHandler {
/* Clear channel without blocking. switch_to_main_screen() issues a kill when
* returning from a fork and there's no input thread, so the newly created thread will
* receive it and die. */
let _ = self.rx.try_iter().count();
//let _ = self.rx.try_iter().count();
let rx = self.rx.clone();
let pipe = self.pipe.0;
thread::Builder::new()
.name("input-thread".to_string())
.spawn(move || {
get_events(
|k| {
tx.send(ThreadEvent::Input(k)).unwrap();
},
|i| {
tx.send(ThreadEvent::InputRaw(i)).unwrap();
tx.send(ThreadEvent::Input(i)).unwrap();
},
&rx,
None,
pipe,
)
})
.unwrap();
}
fn kill(&self) {
let _ = nix::unistd::write(self.pipe.1, &[1]);
self.tx.send(InputCommand::Kill).unwrap();
}
fn switch_to_raw(&self) {
self.tx.send(InputCommand::Raw).unwrap();
}
fn switch_from_raw(&self) {
self.tx.send(InputCommand::NoRaw).unwrap();
}
}
/// A context container for loaded settings, accounts, UI changes, etc.
@ -117,14 +110,6 @@ impl Context {
self.input.kill();
}
pub fn input_from_raw(&self) {
self.input.switch_from_raw();
}
pub fn input_to_raw(&self) {
self.input.switch_to_raw();
}
pub fn restore_input(&self) {
self.input.restore(self.sender.clone());
}
@ -225,6 +210,8 @@ impl State {
* stdin, see get_events() for details
* */
let input_thread = unbounded();
let input_thread_pipe = nix::unistd::pipe()
.map_err(|err| Box::new(err) as Box<dyn std::error::Error + Send + Sync + 'static>)?;
let mut backends = Backends::new();
let settings = Settings::new()?;
let mut plugin_manager = PluginManager::new();
@ -338,6 +325,7 @@ impl State {
sender,
receiver,
input: InputHandler {
pipe: input_thread_pipe,
rx: input_thread.1,
tx: input_thread.0,
},
@ -423,7 +411,6 @@ impl State {
.unwrap();
self.flush();
self.stdout = None;
self.context.input.kill();
}
pub fn switch_to_alternate_screen(&mut self) {
@ -937,16 +924,10 @@ impl State {
return;
}
UIEvent::ChangeMode(m) => {
if self.mode == UIMode::Embed {
self.context.input_from_raw();
}
self.context
.sender
.send(ThreadEvent::UIEvent(UIEvent::ChangeMode(m)))
.unwrap();
if m == UIMode::Embed {
self.context.input_to_raw();
}
}
UIEvent::Timer(id) if id == self.draw_rate_limit.id() => {
self.draw_rate_limit.reset();

View File

@ -20,8 +20,7 @@
*/
use crate::terminal::position::*;
use melib::log;
use melib::ERROR;
use melib::{log, ERROR, error::*};
use nix::fcntl::{open, OFlag};
use nix::libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
@ -29,8 +28,8 @@ use nix::pty::{grantpt, posix_openpt, ptsname, unlockpt, Winsize};
use nix::sys::stat;
use nix::unistd::{dup2, fork, ForkResult};
use nix::{ioctl_none_bad, ioctl_write_ptr_bad};
use std::ffi::CString;
use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::ffi::{CString, OsStr};
use std::os::unix::{ffi::OsStrExt, io::{AsRawFd, FromRawFd, IntoRawFd}};
mod grid;
@ -56,7 +55,7 @@ pub fn create_pty(
width: usize,
height: usize,
command: String,
) -> nix::Result<Arc<Mutex<EmbedGrid>>> {
) -> Result<Arc<Mutex<EmbedGrid>>> {
// Open a new PTY master
let master_fd = posix_openpt(OFlag::O_RDWR)?;
@ -115,17 +114,31 @@ pub fn create_pty(
std::process::exit(-1);
}
}
if let Err(e) = nix::unistd::execv(
&CString::new("sh").unwrap(),
&[
&CString::new("-c").unwrap(),
&CString::new(command.as_bytes()).unwrap(),
],
) {
log(format!("Could not execute `{}`: {}", command, e,), ERROR);
std::process::exit(-1);
/* Find posix sh location, because POSIX shell is not always at /bin/sh */
let path_var = std::process::Command::new("getconf").args(&["PATH"]).output()?.stdout;
for mut p in std::env::split_paths(&OsStr::from_bytes(&path_var[..])) {
p.push("sh");
if p.exists() {
if let Err(e) = nix::unistd::execv(
&CString::new(p.as_os_str().as_bytes()).unwrap(),
&[
&CString::new("sh").unwrap(),
&CString::new("-c").unwrap(),
&CString::new(command.as_bytes()).unwrap(),
],
) {
log(format!("Could not execute `{}`: {}", command, e,), ERROR);
std::process::exit(-1);
}
}
}
/* This path shouldn't be executed. */
log(
format!(
"Could not execute `{}`: did not find the standard POSIX sh shell in PATH = {}",
command, String::from_utf8_lossy(&path_var),
),
ERROR,
);
std::process::exit(-1);
}
Ok(ForkResult::Parent { child }) => child,

View File

@ -137,8 +137,7 @@ impl PartialEq<Key> for &Key {
/// Keep track of whether we're accepting normal user input or a pasted string.
enum InputMode {
Normal,
Paste,
PasteRaw(Vec<u8>),
Paste(Vec<u8>),
}
#[derive(Debug)]
@ -146,12 +145,10 @@ enum InputMode {
pub enum InputCommand {
/// Exit thread
Kill,
/// Send raw bytes as well
Raw,
/// Ignore raw bytes
NoRaw,
}
use nix::poll::{poll, PollFd, PollFlags};
use std::os::unix::io::{AsRawFd, RawFd};
use termion::input::TermReadEventsAndRaw;
/*
* If we fork (for example start $EDITOR) we want the input-thread to stop reading from stdin. The
@ -163,111 +160,68 @@ use termion::input::TermReadEventsAndRaw;
*/
/// The thread function that listens for user input and forwards it to the main event loop.
pub fn get_events(
mut closure: impl FnMut(Key),
closure_raw: impl FnMut((Key, Vec<u8>)),
rx: &Receiver<InputCommand>,
input: Option<(TermionEvent, Vec<u8>)>,
) -> () {
let stdin = std::io::stdin();
let mut input_mode = InputMode::Normal;
let mut paste_buf = String::with_capacity(256);
for c in input
.map(|v| Ok(v))
.into_iter()
.chain(stdin.events_and_raw())
{
select! {
default => {},
recv(rx) -> cmd => {
match cmd.unwrap() {
InputCommand::Kill => return,
InputCommand::Raw => {
get_events_raw(closure, closure_raw, rx, c.ok());
return;
}
InputCommand::NoRaw => unreachable!(),
}
}
};
match c {
Ok((TermionEvent::Key(k), _)) if input_mode == InputMode::Normal => {
closure(Key::from(k));
}
Ok((TermionEvent::Key(TermionKey::Char(k)), _)) if input_mode == InputMode::Paste => {
paste_buf.push(k);
}
Ok((TermionEvent::Unsupported(ref k), _)) if k.as_slice() == BRACKET_PASTE_START => {
input_mode = InputMode::Paste;
}
Ok((TermionEvent::Unsupported(ref k), _)) if k.as_slice() == BRACKET_PASTE_END => {
input_mode = InputMode::Normal;
let ret = Key::from(&paste_buf);
paste_buf.clear();
closure(ret);
}
_ => {} // Mouse events or errors.
}
}
}
/// Same as `get_events` but also forwards the raw bytes of the input as well
pub fn get_events_raw(
closure_nonraw: impl FnMut(Key),
mut closure: impl FnMut((Key, Vec<u8>)),
rx: &Receiver<InputCommand>,
input: Option<(TermionEvent, Vec<u8>)>,
new_command_fd: RawFd,
) -> () {
let stdin = std::io::stdin();
let stdin_fd = PollFd::new(std::io::stdin().as_raw_fd(), PollFlags::POLLIN);
let new_command_pollfd = nix::poll::PollFd::new(new_command_fd, nix::poll::PollFlags::POLLIN);
let mut input_mode = InputMode::Normal;
let mut paste_buf = String::with_capacity(256);
for c in input
.map(|v| Ok(v))
.into_iter()
.chain(stdin.events_and_raw())
{
let mut stdin_iter = stdin.events_and_raw();
'poll_while: while let Ok(_n_raw) = poll(&mut [new_command_pollfd, stdin_fd], -1) {
//debug!(_n_raw);
select! {
default => {},
default => {
if stdin_fd.revents().is_some() {
if let Some(c) = stdin_iter.next(){
match (c, &mut input_mode) {
(Ok((TermionEvent::Key(k), bytes)), InputMode::Normal) => {
closure((Key::from(k), bytes));
}
(
Ok((TermionEvent::Key(TermionKey::Char(k)), ref mut bytes)), InputMode::Paste(ref mut buf),
) => {
paste_buf.push(k);
let bytes = std::mem::replace(bytes, Vec::new());
buf.extend(bytes.into_iter());
}
(Ok((TermionEvent::Unsupported(ref k), _)), _) if k.as_slice() == BRACKET_PASTE_START => {
input_mode = InputMode::Paste(Vec::new());
}
(Ok((TermionEvent::Unsupported(ref k), _)), InputMode::Paste(ref mut buf))
if k.as_slice() == BRACKET_PASTE_END =>
{
let buf = std::mem::replace(buf, Vec::new());
input_mode = InputMode::Normal;
let ret = Key::from(&paste_buf);
paste_buf.clear();
closure((ret, buf));
}
_ => {} // Mouse events or errors.
}
}
}
},
recv(rx) -> cmd => {
use nix::sys::time::TimeValLike;
let mut buf = [0;2];
//debug!("get_events_raw will nix::unistd::read");
let mut read_fd_set = nix::sys::select::FdSet::new();
read_fd_set.insert(new_command_fd);
let mut error_fd_set = nix::sys::select::FdSet::new();
error_fd_set.insert(new_command_fd);
let timeval: nix::sys::time::TimeSpec = nix::sys::time::TimeSpec::seconds(2);
if nix::sys::select::pselect(None, Some(&mut read_fd_set), None, Some(&mut error_fd_set), Some(&timeval), None).is_err() || error_fd_set.highest() == Some(new_command_fd) || read_fd_set.highest() != Some(new_command_fd) {
continue 'poll_while;
};
let _ = nix::unistd::read(new_command_fd, buf.as_mut());
match cmd.unwrap() {
InputCommand::Kill => return,
InputCommand::NoRaw => {
get_events(closure_nonraw, closure, rx, c.ok());
return;
}
InputCommand::Raw => unreachable!(),
}
}
};
match (c, &mut input_mode) {
(Ok((TermionEvent::Key(k), bytes)), InputMode::Normal) => {
closure((Key::from(k), bytes));
}
(
Ok((TermionEvent::Key(TermionKey::Char(k)), ref mut bytes)),
InputMode::PasteRaw(ref mut buf),
) => {
paste_buf.push(k);
let bytes = std::mem::replace(bytes, Vec::new());
buf.extend(bytes.into_iter());
}
(Ok((TermionEvent::Unsupported(ref k), _)), _)
if k.as_slice() == BRACKET_PASTE_START =>
{
input_mode = InputMode::PasteRaw(Vec::new());
}
(Ok((TermionEvent::Unsupported(ref k), _)), InputMode::PasteRaw(ref mut buf))
if k.as_slice() == BRACKET_PASTE_END =>
{
let buf = std::mem::replace(buf, Vec::new());
input_mode = InputMode::Normal;
let ret = Key::from(&paste_buf);
paste_buf.clear();
closure((ret, buf));
}
_ => {} // Mouse events or errors.
}
}
}

View File

@ -60,9 +60,8 @@ pub enum StatusEvent {
pub enum ThreadEvent {
NewThread(thread::ThreadId, String),
/// User input.
Input(Key),
Input((Key, Vec<u8>)),
/// User input and input as raw bytes.
InputRaw((Key, Vec<u8>)),
/// A watched Mailbox has been refreshed.
RefreshMailbox(Box<RefreshEvent>),
UIEvent(UIEvent),