// Implementation file for the low level input library. #include "config.h" #include #include // IWYU pragma: keep #include #include #include #ifdef HAVE_SYS_SELECT_H #include #endif #include #include #include #include #include #include #include #include "common.h" #include "env.h" #include "env_universal_common.h" #include "fallback.h" // IWYU pragma: keep #include "fd_readable_set.rs.h" #include "fds.h" #include "flog.h" #include "input_common.h" #include "iothread.h" #include "wutil.h" /// Time in milliseconds to wait for another byte to be available for reading /// after \x1B is read before assuming that escape key was pressed, and not an /// escape sequence. #define WAIT_ON_ESCAPE_DEFAULT 30 static int wait_on_escape_ms = WAIT_ON_ESCAPE_DEFAULT; input_event_queue_t::input_event_queue_t(int in) : in_(in) {} /// Internal function used by readch to read one byte. /// This calls select() on three fds: input (e.g. stdin), the ioport notifier fd (for main thread /// requests), and the uvar notifier. This returns either the byte which was read, or one of the /// special values below. enum { // The in fd has been closed. readb_eof = -1, // select() was interrupted by a signal. readb_interrupted = -2, // Our uvar notifier reported a change (either through poll() or its fd). readb_uvar_notified = -3, // Our ioport reported a change, so service main thread requests. readb_ioport_notified = -4, }; using readb_result_t = int; static readb_result_t readb(int in_fd) { assert(in_fd >= 0 && "Invalid in fd"); universal_notifier_t& notifier = universal_notifier_t::default_notifier(); auto fdset_box = new_fd_readable_set(); fd_readable_set_t& fdset = *fdset_box; for (;;) { fdset.clear(); fdset.add(in_fd); // Add the completion ioport. int ioport_fd = iothread_port(); fdset.add(ioport_fd); // Get the uvar notifier fd (possibly none). int notifier_fd = notifier.notification_fd(); fdset.add(notifier_fd); // Get its suggested delay (possibly none). // Note a 0 here means do not poll. uint64_t timeout = kNoTimeout; if (uint64_t usecs_delay = notifier.usec_delay_between_polls()) { timeout = usecs_delay; } // Here's where we call select(). int select_res = fdset.check_readable(timeout); if (select_res < 0) { if (errno == EINTR || errno == EAGAIN) { // A signal. return readb_interrupted; } else { // Some fd was invalid, so probably the tty has been closed. return readb_eof; } } // select() did not return an error, so we may have a readable fd. // The priority order is: uvars, stdin, ioport. // Check to see if we want a universal variable barrier. // This may come about through readability, or through a call to poll(). if ((fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd)) || notifier.poll()) { return readb_uvar_notified; } // Check stdin. if (fdset.test(in_fd)) { unsigned char arr[1]; if (read_blocked(in_fd, arr, 1) != 1) { // The terminal has been closed. return readb_eof; } // The common path is to return a (non-negative) char. return static_cast(arr[0]); } // Check for iothread completions only if there is no data to be read from the stdin. // This gives priority to the foreground. if (fdset.test(ioport_fd)) { return readb_ioport_notified; } } } // Update the wait_on_escape_ms value in response to the fish_escape_delay_ms user variable being // set. void update_wait_on_escape_ms(const environment_t& vars) { auto escape_time_ms = vars.get_unless_empty(L"fish_escape_delay_ms"); if (!escape_time_ms) { wait_on_escape_ms = WAIT_ON_ESCAPE_DEFAULT; return; } long tmp = fish_wcstol(escape_time_ms->as_string().c_str()); if (errno || tmp < 10 || tmp >= 5000) { std::fwprintf(stderr, L"ignoring fish_escape_delay_ms: value '%ls' " L"is not an integer or is < 10 or >= 5000 ms\n", escape_time_ms->as_string().c_str()); } else { wait_on_escape_ms = static_cast(tmp); } } maybe_t input_event_queue_t::try_pop() { if (queue_.empty()) { return none(); } auto result = std::move(queue_.front()); queue_.pop_front(); return result; } char_event_t input_event_queue_t::readch() { wchar_t res{}; mbstate_t state = {}; for (;;) { // Do we have something enqueued already? // Note this may be initially true, or it may become true through calls to // iothread_service_main() or env_universal_barrier() below. if (auto mevt = try_pop()) { return mevt.acquire(); } // We are going to block; but first allow any override to inject events. this->prepare_to_select(); if (auto mevt = try_pop()) { return mevt.acquire(); } readb_result_t rr = readb(in_); switch (rr) { case readb_eof: return char_event_type_t::eof; case readb_interrupted: // FIXME: here signals may break multibyte sequences. this->select_interrupted(); break; case readb_uvar_notified: this->uvar_change_notified(); break; case readb_ioport_notified: iothread_service_main(); break; default: { assert(rr >= 0 && rr <= UCHAR_MAX && "Read byte out of bounds - missing error case?"); char read_byte = static_cast(static_cast(rr)); if (MB_CUR_MAX == 1) { // single-byte locale, all values are legal res = read_byte; return res; } size_t sz = std::mbrtowc(&res, &read_byte, 1, &state); switch (sz) { case static_cast(-1): std::memset(&state, '\0', sizeof(state)); FLOG(reader, L"Illegal input"); return char_event_type_t::check_exit; case static_cast(-2): // Sequence not yet complete. break; case 0: // Actual nul char. return 0; default: // Sequence complete. return res; } break; } } } } maybe_t input_event_queue_t::readch_timed() { if (auto evt = try_pop()) { return evt; } // We are not prepared to handle a signal immediately; we only want to know if we get input on // our fd before the timeout. Use pselect to block all signals; we will handle signals // before the next call to readch(). sigset_t sigs; sigfillset(&sigs); // pselect expects timeouts in nanoseconds. const uint64_t nsec_per_msec = 1000 * 1000; const uint64_t nsec_per_sec = nsec_per_msec * 1000; const uint64_t wait_nsec = wait_on_escape_ms * nsec_per_msec; struct timespec timeout; timeout.tv_sec = (wait_nsec) / nsec_per_sec; timeout.tv_nsec = (wait_nsec) % nsec_per_sec; // We have one fd of interest. fd_set fdset; FD_ZERO(&fdset); FD_SET(in_, &fdset); int res = pselect(in_ + 1, &fdset, nullptr, nullptr, &timeout, &sigs); // Prevent signal starvation on WSL causing the `torn_escapes.py` test to fail if (is_windows_subsystem_for_linux()) { // Merely querying the current thread's sigmask is sufficient to deliver a pending signal pthread_sigmask(0, nullptr, &sigs); } if (res > 0) { return readch(); } return none(); } void input_event_queue_t::push_back(const char_event_t& ch) { queue_.push_back(ch); } void input_event_queue_t::push_front(const char_event_t& ch) { queue_.push_front(ch); } void input_event_queue_t::promote_interruptions_to_front() { // Find the first sequence of non-char events. // EOF is considered a char: we don't want to pull EOF in front of real chars. auto is_char = [](const char_event_t& ch) { return ch.is_char() || ch.is_eof(); }; auto first = std::find_if_not(queue_.begin(), queue_.end(), is_char); auto last = std::find_if(first, queue_.end(), is_char); std::rotate(queue_.begin(), first, last); } void input_event_queue_t::prepare_to_select() {} void input_event_queue_t::select_interrupted() {} void input_event_queue_t::uvar_change_notified() {} input_event_queue_t::~input_event_queue_t() = default;