/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ │ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi │ ╞══════════════════════════════════════════════════════════════════════════════╡ │ Copyright 2020 Justine Alexandra Roberts Tunney │ │ │ │ Permission to use, copy, modify, and/or distribute this software for │ │ any purpose with or without fee is hereby granted, provided that the │ │ above copyright notice and this permission notice appear in all copies. │ │ │ │ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ │ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ │ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ │ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ │ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ │ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ │ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/assert.h" #include "libc/calls/createfileflags.internal.h" #include "libc/calls/internal.h" #include "libc/calls/sig.internal.h" #include "libc/calls/state.internal.h" #include "libc/calls/struct/iovec.h" #include "libc/calls/struct/sigset.internal.h" #include "libc/calls/struct/timespec.h" #include "libc/calls/struct/timespec.internal.h" #include "libc/calls/syscall_support-nt.internal.h" #include "libc/cosmo.h" #include "libc/ctype.h" #include "libc/errno.h" #include "libc/fmt/itoa.h" #include "libc/intrin/describeflags.h" #include "libc/intrin/dll.h" #include "libc/intrin/fds.h" #include "libc/intrin/kprintf.h" #include "libc/intrin/nomultics.h" #include "libc/intrin/strace.h" #include "libc/intrin/weaken.h" #include "libc/macros.h" #include "libc/nt/console.h" #include "libc/nt/createfile.h" #include "libc/nt/enum/accessmask.h" #include "libc/nt/enum/consolemodeflags.h" #include "libc/nt/enum/creationdisposition.h" #include "libc/nt/enum/filesharemode.h" #include "libc/nt/enum/vk.h" #include "libc/nt/enum/wait.h" #include "libc/nt/errors.h" #include "libc/nt/runtime.h" #include "libc/nt/struct/inputrecord.h" #include "libc/nt/synchronization.h" #include "libc/str/str.h" #include "libc/str/utf16.h" #include "libc/sysv/consts/limits.h" #include "libc/sysv/consts/o.h" #include "libc/sysv/consts/sicode.h" #include "libc/sysv/consts/sig.h" #include "libc/sysv/errfuns.h" #include "libc/thread/posixthread.internal.h" #include "libc/thread/thread.h" #include "libc/thread/tls.h" #ifdef __x86_64__ /** * @fileoverview Cosmopolitan Standard Input * * This file implements pollable terminal i/o for Windows consoles. On * Windows 10 the "virtual terminal processing" feature works great on * output but their solution for input processing isn't good enough to * support running Linux programs like Emacs. This polyfill fixes that * and it most importantly ensures we can poll() standard input, which * would otherwise have been impossible. We aren't using threads. What * we do instead is have termios behaviors e.g. canonical mode editing * happen on demand as a side effect of read/poll/ioctl activity. */ struct VirtualKey { int vk; int normal_str; int shift_str; int ctrl_str; int shift_ctrl_str; }; #define S(s) W(s "\0\0") #define W(s) (s[3] << 24 | s[2] << 16 | s[1] << 8 | s[0]) static struct VirtualKey kVirtualKey[] = { {kNtVkUp, S("A"), S("1;2A"), S("1;5A"), S("1;6A")}, // order matters {kNtVkDown, S("B"), S("1;2B"), S("1;5B"), S("1;6B")}, // order matters {kNtVkRight, S("C"), S("1;2C"), S("1;5C"), S("1;6C")}, // order matters {kNtVkLeft, S("D"), S("1;2D"), S("1;5D"), S("1;6D")}, // order matters {kNtVkEnd, S("F"), S("1;2F"), S("1;5F"), S("1;6F")}, // order matters {kNtVkHome, S("H"), S("1;2H"), S("1;5H"), S("1;6H")}, // order matters {kNtVkInsert, S("2~"), S("2;2~"), S("2;5~"), S("2;6~")}, {kNtVkDelete, S("3~"), S("3;2~"), S("3;5~"), S("3;6~")}, {kNtVkPrior, S("5~"), S("5;2~"), S("5;5~"), S("5;6~")}, {kNtVkNext, S("6~"), S("6;2~"), S("6;5~"), S("6;6~")}, {kNtVkF1, -S("OP"), S("1;2P"), S("11^"), S("1;6P")}, {kNtVkF2, -S("OQ"), S("1;2Q"), S("12^"), S("1;6Q")}, {kNtVkF3, -S("OR"), S("1;2R"), S("13^"), S("1;6R")}, {kNtVkF4, -S("OS"), S("1;2S"), S("14^"), S("1;6S")}, {kNtVkF5, S("15~"), S("28~"), S("15^"), S("28^")}, {kNtVkF6, S("17~"), S("29~"), S("17^"), S("29^")}, {kNtVkF7, S("18~"), S("31~"), S("18^"), S("31^")}, {kNtVkF8, S("19~"), S("32~"), S("19^"), S("32^")}, {kNtVkF9, S("20~"), S("33~"), S("20^"), S("33^")}, {kNtVkF10, S("21~"), S("34~"), S("21^"), S("34^")}, {kNtVkF11, S("23~"), S("23$"), S("23^"), S("23@")}, {kNtVkF12, S("24~"), S("24$"), S("24^"), S("24@")}, {0}, }; #define KEYSTROKE_CONTAINER(e) DLL_CONTAINER(struct Keystroke, elem, e) struct Keystroke { char buf[23]; unsigned char buflen; struct Dll elem; }; struct Keystrokes { atomic_uint once; bool end_of_file; bool ohno_decckm; bool bypass_mode; uint16_t utf16hs; int16_t freekeys; int64_t cin, cot; struct Dll *list; struct Dll *line; struct Dll *free; pthread_mutex_t lock; struct Keystroke pool[512]; }; static struct Keystrokes __keystroke; textwindows void WipeKeystrokes(void) { bzero(&__keystroke, sizeof(__keystroke)); } textwindows static void FreeKeystrokeImpl(struct Dll *key) { dll_make_first(&__keystroke.free, key); ++__keystroke.freekeys; } textwindows static struct Keystroke *NewKeystroke(void) { struct Dll *e = dll_first(__keystroke.free); if (!e) // See MIN(freekeys) before ReadConsoleInput() __builtin_trap(); struct Keystroke *k = KEYSTROKE_CONTAINER(e); dll_remove(&__keystroke.free, &k->elem); --__keystroke.freekeys; k->buflen = 0; return k; } textwindows static void FreeKeystroke(struct Dll **list, struct Dll *key) { dll_remove(list, key); FreeKeystrokeImpl(key); } textwindows static void FreeKeystrokes(struct Dll **list) { struct Dll *key; while ((key = dll_first(*list))) FreeKeystroke(list, key); } textwindows static void OpenConsole(void) { __keystroke.cin = CreateFile(u"CONIN$", kNtGenericRead | kNtGenericWrite, kNtFileShareRead, 0, kNtOpenExisting, 0, 0); __keystroke.cot = CreateFile(u"CONOUT$", kNtGenericRead | kNtGenericWrite, kNtFileShareWrite, 0, kNtOpenExisting, 0, 0); for (int i = 0; i < ARRAYLEN(__keystroke.pool); ++i) { dll_init(&__keystroke.pool[i].elem); FreeKeystrokeImpl(&__keystroke.pool[i].elem); } } textwindows static int AddSignal(int sig) { atomic_fetch_or_explicit(&__get_tls()->tib_sigpending, 1ull << (sig - 1), memory_order_relaxed); return 0; } textwindows static void InitConsole(void) { cosmo_once(&__keystroke.once, OpenConsole); } textwindows static void LockKeystrokes(void) { pthread_mutex_lock(&__keystroke.lock); } textwindows static void UnlockKeystrokes(void) { pthread_mutex_unlock(&__keystroke.lock); } textwindows int64_t GetConsoleInputHandle(void) { InitConsole(); return __keystroke.cin; } textwindows int64_t GetConsoleOutputHandle(void) { InitConsole(); return __keystroke.cot; } textwindows static bool IsMouseModeCommand(int x) { return x == 1000 || // SET_VT200_MOUSE x == 1002 || // SET_BTN_EVENT_MOUSE x == 1006 || // SET_SGR_EXT_MODE_MOUSE x == 1015; // SET_URXVT_EXT_MODE_MOUSE } textwindows static int GetVirtualKey(uint16_t vk, bool shift, bool ctrl) { for (int i = 0; kVirtualKey[i].vk; ++i) { if (kVirtualKey[i].vk == vk) { if (shift && ctrl) { return kVirtualKey[i].shift_ctrl_str; } else if (shift) { return kVirtualKey[i].shift_str; } else if (ctrl) { return kVirtualKey[i].ctrl_str; } else { return kVirtualKey[i].normal_str; } } } return 0; } textwindows static int ProcessKeyEvent(const struct NtInputRecord *r, char *p) { uint32_t c = r->Event.KeyEvent.uChar.UnicodeChar; uint16_t vk = r->Event.KeyEvent.wVirtualKeyCode; uint16_t cks = r->Event.KeyEvent.dwControlKeyState; // ignore keyup events if (!r->Event.KeyEvent.bKeyDown && (!c || vk != kNtVkMenu)) return 0; #if 0 // this code is useful for troubleshooting why keys don't work kprintf("bKeyDown=%hhhd wVirtualKeyCode=%s wVirtualScanCode=%s " "UnicodeChar=%#x[%#lc] dwControlKeyState=%s\n", r->Event.KeyEvent.bKeyDown, DescribeVirtualKeyCode(r->Event.KeyEvent.wVirtualKeyCode), DescribeVirtualKeyCode(r->Event.KeyEvent.wVirtualScanCode), r->Event.KeyEvent.uChar.UnicodeChar, r->Event.KeyEvent.uChar.UnicodeChar, DescribeControlKeyState(r->Event.KeyEvent.dwControlKeyState)); #endif // turn arrow/function keys into vt100/ansi/xterm byte sequences int n = 0; int v = GetVirtualKey(vk, !!(cks & kNtShiftPressed), !!(cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed))); if (v) { p[n++] = 033; if (cks & (kNtLeftAltPressed | kNtRightAltPressed)) { p[n++] = 033; } if (v > 0) { p[n++] = '['; } else { v = -v; } do p[n++] = v; while ((v >>= 8)); return n; } // ^/ (crtl+slash) maps to ^_ (ctrl-hyphen) on linux if (vk == kNtVkOem_2 && (cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed))) { p[n++] = 037; return n; } // handle cases where win32 doesn't provide character if (!c) { if (vk == '2' && (cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed))) { c = 0; // ctrl-2 → "\000" } else if (isascii(vk) && isdigit(vk) && (cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed))) { c = 030 + (vk - '0'); // e.g. ctrl-3 → "\033" } else if (isascii(vk) && isgraph(vk) && (cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed))) { c = vk ^ 0100; // e.g. ctrl-alt-b → "\033\002" } else { return 0; } } // convert utf-16 to utf-32 if (IsHighSurrogate(c)) { __keystroke.utf16hs = c; return 0; } if (IsLowSurrogate(c)) c = MergeUtf16(__keystroke.utf16hs, c); // enter sends \r with raw terminals // make it a multics newline instead if (c == '\r' && !(__ttyconf.magic & kTtyNoCr2Nl)) c = '\n'; // ctrl-space (^@) is literally zero if (c == ' ' && (cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed))) c = '\0'; // make backspace (^?) distinguishable from ctrl-h (^H) if (c == kNtVkBack && !(cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed))) c = 0177; // handle ctrl-\ and ctrl-c // note we define _POSIX_VDISABLE as zero // tcsetattr() lets anyone reconfigure these keybindings if (c && !(__ttyconf.magic & kTtyNoIsigs) && !__keystroke.bypass_mode) { if (c == __ttyconf.vintr) { return AddSignal(SIGINT); } else if (c == __ttyconf.vquit) { return AddSignal(SIGQUIT); } } // handle ctrl-d which generates end-of-file, unless pending line data // is present, in which case we flush that without the newline instead if (c && c == __ttyconf.veof && // !__keystroke.bypass_mode && // !(__ttyconf.magic & kTtyUncanon)) { if (dll_is_empty(__keystroke.line)) { __keystroke.end_of_file = true; } else { dll_make_last(&__keystroke.list, __keystroke.line); __keystroke.line = 0; } return 0; } // insert esc prefix when alt is held // for example "h" becomes "\033h" (alt-h) // if up arrow is "\033[A" then alt-up is "\033\033[A" if ((cks & (kNtLeftAltPressed | kNtRightAltPressed)) && r->Event.KeyEvent.bKeyDown) { p[n++] = 033; } // finally apply thompson-pike varint encoding uint64_t w = tpenc(c); do p[n++] = w; while ((w >>= 8)); return n; } // To use the tty mouse events feature: // - write(1, "\e[?1000;1002;1015;1006h") to enable // - write(1, "\e[?1000;1002;1015;1006l") to disable // See o//examples/ttyinfo and o//tool/viz/life textwindows static int ProcessMouseEvent(const struct NtInputRecord *r, char *b) { char *p = b; unsigned char e = 0; uint32_t currentbs = __ttyconf.mousebs; uint32_t ev = r->Event.MouseEvent.dwEventFlags; uint32_t bs = r->Event.MouseEvent.dwButtonState; ev &= kNtMouseMoved | kNtMouseWheeled; bs &= kNtFromLeft1stButtonPressed | kNtRightmostButtonPressed; if (ev & kNtMouseWheeled) { // scroll wheel (unnatural mode) bool isup = ((int)r->Event.MouseEvent.dwButtonState >> 16) > 0; if (__ttyconf.magic & kTtyXtMouse) { if (r->Event.MouseEvent.dwControlKeyState & (kNtLeftCtrlPressed | kNtRightCtrlPressed)) { e = isup ? 80 : 81; } else { e = isup ? 64 : 65; } goto OutputXtermMouseEvent; } else if (!(r->Event.MouseEvent.dwControlKeyState & (kNtShiftPressed | kNtLeftCtrlPressed | kNtRightCtrlPressed | kNtLeftAltPressed | kNtRightAltPressed))) { // we disable mouse highlighting when the tty is put in raw mode // to mouse wheel events with widely understood vt100 arrow keys for (int i = 0; i < 3; ++i) { *p++ = 033; *p++ = !__keystroke.ohno_decckm ? '[' : 'O'; if (isup) { *p++ = 'A'; } else { *p++ = 'B'; } } } } else if ((bs || currentbs) && (__ttyconf.magic & kTtyXtMouse)) { if (bs && (ev & kNtMouseMoved) && currentbs) e |= 32; // dragging if ((bs | currentbs) & kNtRightmostButtonPressed) e |= 2; // right OutputXtermMouseEvent: *p++ = 033; *p++ = '['; *p++ = '<'; p = FormatInt32(p, e); *p++ = ';'; p = FormatInt32(p, (r->Event.MouseEvent.dwMousePosition.X + 1) & 0x7fff); *p++ = ';'; p = FormatInt32(p, (r->Event.MouseEvent.dwMousePosition.Y + 1) & 0x7fff); if (!bs && currentbs) { *p++ = 'm'; // up } else { *p++ = 'M'; // down } __ttyconf.mousebs = bs; } return p - b; } textwindows static int ConvertConsoleInputToAnsi(const struct NtInputRecord *r, char p[hasatleast 23]) { switch (r->EventType) { case kNtKeyEvent: return ProcessKeyEvent(r, p); case kNtMouseEvent: return ProcessMouseEvent(r, p); case kNtWindowBufferSizeEvent: return AddSignal(SIGWINCH); default: return 0; } } textwindows static void WriteTty(const char *p, size_t n) { WriteFile(__keystroke.cot, p, n, 0, 0); } textwindows static bool IsCtl(int c, bool escape_harder) { return isascii(c) && iscntrl(c) && (escape_harder || (c != '\n' && c != '\t')); } textwindows static void WriteCtl(const char *p, size_t n, bool escape_harder) { size_t i; for (i = 0; i < n; ++i) { if (IsCtl(p[i], escape_harder)) { char ctl[2]; ctl[0] = '^'; ctl[1] = p[i] ^ 0100; WriteTty(ctl, 2); } else { WriteTty(p + i, 1); } } } textwindows static void EchoTty(const char *p, size_t n, bool escape_harder) { if (!(__ttyconf.magic & kTtySilence)) { if (__ttyconf.magic & kTtyEchoRaw) { WriteTty(p, n); } else { WriteCtl(p, n, escape_harder); } } } textwindows static void EraseCharacter(bool should_echo) { if (should_echo) WriteTty("\b \b", 3); } textwindows static bool EraseKeystroke(bool should_echo) { struct Dll *e; if ((e = dll_last(__keystroke.line))) { struct Keystroke *k = KEYSTROKE_CONTAINER(e); FreeKeystroke(&__keystroke.line, e); for (int i = k->buflen; i--;) { if ((k->buf[i] & 0300) == 0200) continue; // utf-8 cont EraseCharacter(should_echo); if (!(__ttyconf.magic & kTtyEchoRaw) && IsCtl(k->buf[i], true)) EraseCharacter(should_echo); } return true; } else { return false; } } textwindows static int IsLookingAtSpace(void) { struct Dll *e; if ((e = dll_last(__keystroke.line))) { struct Keystroke *k = KEYSTROKE_CONTAINER(e); return k->buflen == 1 && isascii(k->buf[0]) && isspace(k->buf[0]); } else { return -1; } } textwindows static void IngestConsoleInputRecord(struct NtInputRecord *r) { // convert win32 console event into ansi int len; char buf[23]; if (!(len = ConvertConsoleInputToAnsi(r, buf))) return; // handle ctrl-v in canonical mode // the next keystroke will bypass input processing if (!(__ttyconf.magic & kTtyUncanon) && // ICANON !(__ttyconf.magic & kTtyNoIexten)) { // IEXTEN if (__keystroke.bypass_mode) { struct Keystroke *k = NewKeystroke(); memcpy(k->buf, buf, sizeof(k->buf)); k->buflen = len; dll_make_last(&__keystroke.line, &k->elem); EchoTty(buf, len, true); if (!__keystroke.freekeys) { dll_make_last(&__keystroke.list, __keystroke.line); __keystroke.line = 0; } __keystroke.bypass_mode = false; return; } else if (len == 1 && buf[0] && // (buf[0] & 255) == __ttyconf.vlnext) { __keystroke.bypass_mode = true; if (!(__ttyconf.magic & kTtySilence) && // ECHO !(__ttyconf.magic & kTtyEchoRaw)) // ECHOCTL WriteTty("^\b", 2); return; } } // handle backspace in canonical mode if (len == 1 && buf[0] && // (buf[0] & 255) == __ttyconf.verase && // !(__ttyconf.magic & kTtyUncanon) && // !(__ttyconf.magic & kTtyNoIexten)) { bool should_visually_erase = // !(__ttyconf.magic & kTtySilence) && // ECHO !(__ttyconf.magic & kTtyNoEchoe); // ECHOE EraseKeystroke(should_visually_erase); if (!(__ttyconf.magic & kTtySilence) && // ECHO (__ttyconf.magic & kTtyNoEchoe) && // !ECHOE !(__ttyconf.magic & kTtyEchoRaw)) // ECHOCTL WriteCtl(buf, len, true); return; } // handle ctrl-w in canonical mode // this lets you erase the last word if (len == 1 && buf[0] && // (buf[0] & 255) == __ttyconf.vwerase && // !(__ttyconf.magic & kTtyUncanon) && // !(__ttyconf.magic & kTtyNoIexten)) { bool should_visually_erase = // !(__ttyconf.magic & kTtySilence) && // ECHO !(__ttyconf.magic & kTtyNoEchoe); // ECHOE while (IsLookingAtSpace() == 1) EraseKeystroke(should_visually_erase); while (IsLookingAtSpace() == 0) EraseKeystroke(should_visually_erase); if (!(__ttyconf.magic & kTtySilence) && // ECHO (__ttyconf.magic & kTtyNoEchoe) && // !ECHOE !(__ttyconf.magic & kTtyEchoRaw)) // ECHOCTL WriteCtl(buf, len, true); return; } // handle kill in canonical mode // this clears the line you're editing if (len == 1 && buf[0] && // (buf[0] & 255) == __ttyconf.vkill && // !(__ttyconf.magic & kTtyUncanon) && // !(__ttyconf.magic & kTtyNoIexten)) { bool should_visually_kill = // !(__ttyconf.magic & kTtySilence) && // ECHO !(__ttyconf.magic & kTtyNoEchok) && // ECHOK !(__ttyconf.magic & kTtyNoEchoke); // ECHOKE while (EraseKeystroke(should_visually_kill)) { } if (!(__ttyconf.magic & kTtySilence) && // ECHO !(__ttyconf.magic & kTtyNoEchok) && // ECHOK (__ttyconf.magic & kTtyNoEchoke) && // !ECHOKE !(__ttyconf.magic & kTtyEchoRaw)) // ECHOCTL WriteCtl(buf, len, true); return; } // handle ctrl-r in canonical mode // this reprints the line you're editing if (len == 1 && buf[0] && // (buf[0] & 255) == __ttyconf.vreprint && // !(__ttyconf.magic & kTtyUncanon) && // ICANON !(__ttyconf.magic & kTtyNoIexten) && // IEXTEN !(__ttyconf.magic & kTtySilence)) { // ECHO struct Dll *e; if (!(__ttyconf.magic & kTtyEchoRaw)) WriteCtl(buf, len, true); WriteTty("\r\n", 2); for (e = dll_first(__keystroke.line); e; e = dll_next(__keystroke.line, e)) { struct Keystroke *k = KEYSTROKE_CONTAINER(e); WriteCtl(k->buf, k->buflen, true); } return; } // allocate object to hold keystroke struct Keystroke *k = NewKeystroke(); memcpy(k->buf, buf, sizeof(k->buf)); k->buflen = len; // echo input if it was successfully recorded // assuming the win32 console isn't doing it already EchoTty(buf, len, false); // save keystroke to appropriate list if (__ttyconf.magic & kTtyUncanon) { dll_make_last(&__keystroke.list, &k->elem); } else { dll_make_last(&__keystroke.line, &k->elem); // flush canonical mode line if oom or enter if (!__keystroke.freekeys || (len == 1 && buf[0] && ((buf[0] & 255) == '\n' || // (buf[0] & 255) == __ttyconf.veol || // ((buf[0] & 255) == __ttyconf.veol2 && !(__ttyconf.magic & kTtyNoIexten))))) { dll_make_last(&__keystroke.list, __keystroke.line); __keystroke.line = 0; } } } textwindows static void IngestConsoleInput(void) { uint32_t i, n; struct NtInputRecord records[16]; for (;;) { if (!__keystroke.freekeys) return; if (__keystroke.end_of_file) return; if (!GetNumberOfConsoleInputEvents(__keystroke.cin, &n)) goto UnexpectedEof; if (!n || !__keystroke.freekeys) return; n = MIN(__keystroke.freekeys, MIN(ARRAYLEN(records), n)); if (!ReadConsoleInput(__keystroke.cin, records, n, &n)) goto UnexpectedEof; for (i = 0; i < n && !__keystroke.end_of_file; ++i) IngestConsoleInputRecord(records + i); } UnexpectedEof: STRACE("console read error %d", GetLastError()); __keystroke.end_of_file = true; } // Discards all unread stdin bytes. textwindows int FlushConsoleInputBytes(void) { BLOCK_SIGNALS; InitConsole(); LockKeystrokes(); FlushConsoleInputBuffer(__keystroke.cin); FreeKeystrokes(&__keystroke.list); FreeKeystrokes(&__keystroke.line); UnlockKeystrokes(); ALLOW_SIGNALS; return 0; } // Returns number of stdin bytes that may be read without blocking. textwindows int CountConsoleInputBytes(void) { struct Dll *e; int count = 0; BLOCK_SIGNALS; InitConsole(); LockKeystrokes(); IngestConsoleInput(); for (e = dll_first(__keystroke.list); e; e = dll_next(__keystroke.list, e)) count += KEYSTROKE_CONTAINER(e)->buflen; if (!count && __keystroke.end_of_file) count = -1; UnlockKeystrokes(); ALLOW_SIGNALS; return count; } // Intercept ANSI TTY commands that enable features. textwindows void InterceptTerminalCommands(const char *data, size_t size) { int i; unsigned x; bool ismouse; uint32_t cm, cm2; enum { ASC, ESC, CSI, CMD } t; GetConsoleMode(GetConsoleInputHandle(), &cm), cm2 = cm; for (ismouse = false, x = i = t = 0; i < size; ++i) { switch (t) { case ASC: if (data[i] == 033) { t = ESC; } break; case ESC: if (data[i] == '[') { t = CSI; } else { t = ASC; } break; case CSI: if (data[i] == '?') { t = CMD; x = 0; } else { t = ASC; } break; case CMD: if ('0' <= data[i] && data[i] <= '9') { x *= 10; x += data[i] - '0'; } else if (data[i] == ';') { ismouse |= IsMouseModeCommand(x); x = 0; } else if (data[i] == 'h') { if (x == 1) { // \e[?1h decckm on __keystroke.ohno_decckm = true; kVirtualKey[0].normal_str = -S("OA"); // kNtVkUp kVirtualKey[1].normal_str = -S("OB"); // kNtVkDown kVirtualKey[2].normal_str = -S("OC"); // kNtVkRight kVirtualKey[3].normal_str = -S("OD"); // kNtVkLeft kVirtualKey[4].normal_str = -S("OF"); // kNtVkEnd kVirtualKey[5].normal_str = -S("OH"); // kNtVkHome } else if ((ismouse |= IsMouseModeCommand(x))) { __ttyconf.magic |= kTtyXtMouse; cm2 |= kNtEnableMouseInput; cm2 &= ~kNtEnableQuickEditMode; // take mouse } t = ASC; } else if (data[i] == 'l') { if (x == 1) { // \e[?1l decckm off __keystroke.ohno_decckm = false; kVirtualKey[0].normal_str = S("A"); // kNtVkUp kVirtualKey[1].normal_str = S("B"); // kNtVkDown kVirtualKey[2].normal_str = S("C"); // kNtVkRight kVirtualKey[3].normal_str = S("D"); // kNtVkLeft kVirtualKey[4].normal_str = S("F"); // kNtVkEnd kVirtualKey[5].normal_str = S("H"); // kNtVkHome } else if ((ismouse |= IsMouseModeCommand(x))) { __ttyconf.magic &= ~kTtyXtMouse; cm2 |= kNtEnableQuickEditMode; // release mouse } t = ASC; } break; default: __builtin_unreachable(); } } if (cm2 != cm) SetConsoleMode(GetConsoleInputHandle(), cm2); } textwindows static bool DigestConsoleInput(char *data, size_t size, int *rc) { // handle eof once available input is consumed if (dll_is_empty(__keystroke.list) && __keystroke.end_of_file) { *rc = 0; return true; } // copy keystroke(s) into user buffer int toto = 0; struct Dll *e; while (size && (e = dll_first(__keystroke.list))) { struct Keystroke *k = KEYSTROKE_CONTAINER(e); uint32_t got = MIN(size, k->buflen); uint32_t remain = k->buflen - got; if (got) { memcpy(data, k->buf, got); data += got; size -= got; toto += got; } if (remain) { memmove(k->buf, k->buf + got, remain); k->buflen = remain; } else { FreeKeystroke(&__keystroke.list, e); } if ((__ttyconf.magic & kTtyUncanon) && toto >= __ttyconf.vmin) break; } // return result if (toto) { *rc = toto; return true; } else { return false; } } textwindows static uint32_t DisableProcessedInput(void) { // the time has come to ensure that ctrl-v ctrl-c works in icanon mode // we're perfectly capable of generating a SIGINT or SIGQUIT ourselves // while the cosmo termios driver is in control; so we disable windows // console input processing for now; we'll turn it back on when we are // done, since it's useful for ensuring asynchronous signal deliveries uint32_t inmode = 0; if (GetConsoleMode(__keystroke.cin, &inmode)) if (inmode & kNtEnableProcessedInput) SetConsoleMode(__keystroke.cin, inmode & ~kNtEnableProcessedInput); return inmode; } textwindows static void RestoreProcessedInput(uint32_t inmode) { // re-enable win32 console input processing, if it was enabled when we // started, and no signal handler callbacks changed things in-between. if (inmode & kNtEnableProcessedInput) { uint32_t inmode2; if (GetConsoleMode(__keystroke.cin, &inmode2)) if (inmode2 == (inmode & ~kNtEnableProcessedInput)) SetConsoleMode(__keystroke.cin, inmode); } } textwindows static int CountConsoleInputBytesBlockingImpl(uint32_t ms, sigset_t waitmask, bool restartable) { int sig; int64_t sem; uint32_t wi; struct timespec now, deadline; InitConsole(); deadline = timespec_add(sys_clock_gettime_monotonic_nt(), timespec_frommillis(ms)); RestartOperation: if (_check_cancel() == -1) return -1; if (_weaken(__sig_get) && (sig = _weaken(__sig_get)(waitmask))) goto DeliverSignal; struct PosixThread *pt = _pthread_self(); pt->pt_blkmask = waitmask; pt->pt_semaphore = sem = CreateSemaphore(0, 0, 1, 0); atomic_store_explicit(&pt->pt_blocker, PT_BLOCKER_SEM, memory_order_release); wi = WaitForMultipleObjects(2, (int64_t[2]){__keystroke.cin, sem}, 0, ms); atomic_store_explicit(&pt->pt_blocker, 0, memory_order_release); CloseHandle(sem); // check for wait timeout if (wi == kNtWaitTimeout) return etimedout(); // handle event on console handle. this means we can now read from the // conosle without blocking. so the first thing we do is slurp up your // keystroke data. some of those keystrokes might cause a signal to be // raised. so we need to check for pending signals again and handle it if (wi == 0) { int got = CountConsoleInputBytes(); if (_weaken(__sig_get) && (sig = _weaken(__sig_get)(waitmask))) goto DeliverSignal; if (got == -1) // this is a bona fide eof and console errors are logged to strace return 0; if (got == 0) { // this can happen for multiple reasons. first our driver controls // user interactions in canonical mode. secondly we could lose the // race with another thread that's reading input. now = sys_clock_gettime_monotonic_nt(); if (timespec_cmp(now, deadline) >= 0) return etimedout(); ms = timespec_tomillis(timespec_sub(deadline, now)); ms = ms > -1u ? -1u : ms; goto RestartOperation; } return got; } // handle wait itself failing if (wi != 1) return __winerr(); // handle event on throwaway semaphore, it is poked by signal delivery if (_weaken(__sig_get)) { if (!(sig = _weaken(__sig_get)(waitmask))) return eintr(); DeliverSignal: int handler_was_called = _weaken(__sig_relay)(sig, SI_KERNEL, waitmask); if (_check_cancel() == -1) return -1; if (handler_was_called & SIG_HANDLED_NO_RESTART) return eintr(); if (handler_was_called & SIG_HANDLED_SA_RESTART) if (!restartable) return eintr(); } goto RestartOperation; } textwindows static int CountConsoleInputBytesBlocking(uint32_t ms, sigset_t waitmask) { int got = CountConsoleInputBytes(); if (got == -1) return 0; if (got > 0) return got; uint32_t inmode = DisableProcessedInput(); int rc = CountConsoleInputBytesBlockingImpl(ms, waitmask, false); RestoreProcessedInput(inmode); return rc; } textwindows static int WaitToReadFromConsole(struct Fd *f, sigset_t waitmask) { uint32_t ms = -1; if (!__ttyconf.vmin) { if (!__ttyconf.vtime) { return 0; // non-blocking w/o raising eagain } else { ms = __ttyconf.vtime * 100; } } if (f->flags & _O_NONBLOCK) return eagain(); int olderr = errno; int rc = CountConsoleInputBytesBlockingImpl(ms, waitmask, true); if (rc == -1 && errno == ETIMEDOUT) { // read() never raises ETIMEDOUT so if vtime elapses we raise an EOF errno = olderr; rc = 0; } return rc; } textwindows static ssize_t ReadFromConsole(struct Fd *f, void *data, size_t size, sigset_t waitmask) { int rc; InitConsole(); uint32_t inmode = DisableProcessedInput(); do { LockKeystrokes(); IngestConsoleInput(); bool done = DigestConsoleInput(data, size, &rc); UnlockKeystrokes(); if (done) break; } while ((rc = WaitToReadFromConsole(f, waitmask)) > 0); RestoreProcessedInput(inmode); return rc; } textwindows ssize_t ReadBuffer(int fd, void *data, size_t size, int64_t offset, sigset_t waitmask) { // switch to terminal polyfill if reading from win32 console struct Fd *f = g_fds.p + fd; if (f->kind == kFdDevNull) return 0; if (f->kind == kFdDevRandom) return ProcessPrng(data, size) ? size : __winerr(); if (f->kind == kFdConsole) return ReadFromConsole(f, data, size, waitmask); // perform heavy lifting ssize_t rc; rc = sys_readwrite_nt(fd, data, size, offset, f->handle, waitmask, ReadFile); if (rc != -2) return rc; // mops up win32 errors switch (GetLastError()) { case kNtErrorBrokenPipe: // broken pipe case kNtErrorNoData: // closing named pipe case kNtErrorHandleEof: // pread read past EOF return 0; // case kNtErrorAccessDenied: // read doesn't return EACCESS return ebadf(); // default: return __winerr(); } } textwindows static ssize_t ReadIovecs(int fd, const struct iovec *iov, size_t iovlen, int64_t opt_offset, sigset_t waitmask) { ssize_t rc; size_t i, total; if (opt_offset < -1) return einval(); while (iovlen && !iov[0].iov_len) iov++, iovlen--; if (iovlen) { for (total = i = 0; i < iovlen; ++i) { if (!iov[i].iov_len) continue; rc = ReadBuffer(fd, iov[i].iov_base, iov[i].iov_len, opt_offset, waitmask); if (rc == -1) { if (total && errno != ECANCELED) { return total; } else { return -1; } } total += rc; if (opt_offset != -1) opt_offset += rc; if (rc < iov[i].iov_len) break; } return total; } else { return ReadBuffer(fd, NULL, 0, opt_offset, waitmask); } } textwindows ssize_t sys_read_nt(int fd, const struct iovec *iov, size_t iovlen, int64_t opt_offset) { ssize_t rc; sigset_t m = __sig_block(); rc = ReadIovecs(fd, iov, iovlen, opt_offset, m); __sig_unblock(m); return rc; } #endif /* __x86_64__ */