Rewrite Windows console input handling

This change removes our use of ENABLE_VIRTUAL_TERMINAL_INPUT (which
isn't very good) in favor of having read() translate Windows Console
input events to ANSI/XTERM sequences by hand. This makes it possible to
capture important keystrokes (e.g. ctrl-space) that weren't possible
before. Most importantly this change also removes the stdin/sigwinch
worker threads, which never really worked that well. Interactive TTY
sessions will now work reliably when a Cosmo process spawns or forks
another Cosmo process, e.g. unbourne.com launching emacs.com.
This commit is contained in:
Justine Tunney 2023-09-19 11:42:38 -07:00
parent ececec4c94
commit d6c2830850
No known key found for this signature in database
GPG key ID: BE714B4575D6E328
27 changed files with 635 additions and 464 deletions

View file

@ -17,32 +17,39 @@
PERFORMANCE OF THIS SOFTWARE.
*/
#include "libc/calls/calls.h"
#include "libc/calls/console.internal.h"
#include "libc/calls/internal.h"
#include "libc/calls/sig.internal.h"
#include "libc/calls/state.internal.h"
#include "libc/calls/struct/fd.internal.h"
#include "libc/calls/struct/iovec.h"
#include "libc/calls/struct/iovec.internal.h"
#include "libc/calls/struct/timespec.h"
#include "libc/calls/syscall_support-nt.internal.h"
#include "libc/calls/wincrash.internal.h"
#include "libc/errno.h"
#include "libc/intrin/kprintf.h"
#include "libc/fmt/itoa.h"
#include "libc/intrin/nomultics.internal.h"
#include "libc/intrin/weaken.h"
#include "libc/macros.internal.h"
#include "libc/nt/console.h"
#include "libc/nt/enum/filetype.h"
#include "libc/nt/enum/vk.h"
#include "libc/nt/enum/wait.h"
#include "libc/nt/errors.h"
#include "libc/nt/events.h"
#include "libc/nt/files.h"
#include "libc/nt/ipc.h"
#include "libc/nt/runtime.h"
#include "libc/nt/struct/inputrecord.h"
#include "libc/nt/struct/overlapped.h"
#include "libc/nt/synchronization.h"
#include "libc/nt/thread.h"
#include "libc/nt/thunk/msabi.h"
#include "libc/str/str.h"
#include "libc/str/utf16.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/sa.h"
#include "libc/sysv/consts/sicode.h"
#include "libc/sysv/consts/sig.h"
#include "libc/sysv/consts/termios.h"
@ -50,33 +57,385 @@
#include "libc/thread/posixthread.internal.h"
#include "libc/thread/tls.h"
__static_yoink("WinMainStdin");
#ifdef __x86_64__
__msabi extern typeof(CloseHandle) *const __imp_CloseHandle;
static const struct {
uint16_t vk;
uint32_t normal_str;
uint32_t shift_str;
uint32_t ctrl_str;
uint32_t shift_ctrl_str;
} kVirtualKey[] = {
#define SW(s) W4(s "\0\0")
#define W4(s) (s[3] + 0u) << 24 | s[2] << 16 | s[1] << 8 | s[0]
#define VK(vk, normal_str, shift_str, ctrl_str, shift_ctrl_str) \
{ vk, SW(normal_str), SW(shift_str), SW(ctrl_str), SW(shift_ctrl_str) }
VK(kNtVkInsert, "2~", "2;2~", "2;5~", "2;6~"),
VK(kNtVkEnd, "4~", "4;2~", "4;5~", "4;6~"),
VK(kNtVkDown, "B", "1;2B", "1;5B", "1;6B"),
VK(kNtVkNext, "6~", "6;2~", "6;5~", "6;6~"),
VK(kNtVkLeft, "D", "1;2D", "1;5D", "1;6D"),
VK(kNtVkClear, "G", "1;2G", "1;5G", "1;6G"),
VK(kNtVkRight, "C", "1;2C", "1;5C", "1;6C"),
VK(kNtVkUp, "A", "1;2A", "1;5A", "1;6A"),
VK(kNtVkHome, "1~", "1;2~", "1;5~", "1;6~"),
VK(kNtVkPrior, "5~", "5;2~", "5;5~", "5;6~"),
VK(kNtVkDelete, "3~", "3;2~", "3;5~", "3;6~"),
VK(kNtVkNumpad0, "2~", "2;2~", "2;5~", "2;6~"),
VK(kNtVkNumpad1, "4~", "4;2~", "4;5~", "4;6~"),
VK(kNtVkNumpad2, "B", "1;2B", "1;5B", "1;6B"),
VK(kNtVkNumpad3, "6~", "6;2~", "6;5~", "6;6~"),
VK(kNtVkNumpad4, "D", "1;2D", "1;5D", "1;6D"),
VK(kNtVkNumpad5, "G", "1;2G", "1;5G", "1;6G"),
VK(kNtVkNumpad6, "C", "1;2C", "1;5C", "1;6C"),
VK(kNtVkNumpad7, "A", "1;2A", "1;5A", "1;6A"),
VK(kNtVkNumpad8, "1~", "1;2~", "1;5~", "1;6~"),
VK(kNtVkNumpad9, "5~", "5;2~", "5;5~", "5;6~"),
VK(kNtVkDecimal, "3~", "3;2~", "3;5~", "3;6~"),
VK(kNtVkF1, "[A", "23~", "11^", "23^"),
VK(kNtVkF2, "[B", "24~", "12^", "24^"),
VK(kNtVkF3, "[C", "25~", "13^", "25^"),
VK(kNtVkF4, "[D", "26~", "14^", "26^"),
VK(kNtVkF5, "[E", "28~", "15^", "28^"),
VK(kNtVkF6, "17~", "29~", "17^", "29^"),
VK(kNtVkF7, "18~", "31~", "18^", "31^"),
VK(kNtVkF8, "19~", "32~", "19^", "32^"),
VK(kNtVkF9, "20~", "33~", "20^", "33^"),
VK(kNtVkF10, "21~", "34~", "21^", "34^"),
VK(kNtVkF11, "23~", "23$", "23^", "23@"),
VK(kNtVkF12, "24~", "24$", "24^", "24@"),
#undef VK
#undef W4
#undef SW
};
static textwindows int ProcessSignal(int sig, struct Fd *f) {
if (f) {
if (_weaken(__sig_raise)) {
pthread_mutex_unlock(&f->lock);
_weaken(__sig_raise)(sig, SI_KERNEL);
pthread_mutex_lock(&f->lock);
if (!(__sighandflags[sig] & SA_RESTART)) {
return eintr();
}
} else if (sig != SIGWINCH) {
TerminateThisProcess(sig);
}
}
return 0;
}
static textwindows uint32_t GetVirtualKey(uint16_t vk, bool shift, bool ctrl) {
for (int i = 0; i < ARRAYLEN(kVirtualKey); ++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;
}
// Manual CMD.EXE echoing for when !ICANON && ECHO is the case.
static textwindows void EchoTerminalInput(struct Fd *f, char *p, size_t n) {
int64_t hOutput;
if (f->kind == kFdConsole) {
hOutput = f->extra;
} else {
hOutput = g_fds.p[1].handle;
}
if (__ttymagic & kFdTtyEchoRaw) {
WriteFile(hOutput, p, n, 0, 0);
} else {
size_t i;
for (i = 0; i < n; ++i) {
if (isascii(p[i]) && iscntrl(p[i]) && p[i] != '\n' && p[i] != '\t') {
char ctl[2];
ctl[0] = '^';
ctl[1] = p[i] ^ 0100;
WriteFile(hOutput, ctl, 2, 0, 0);
} else {
WriteFile(hOutput, p + i, 1, 0, 0);
}
}
}
}
static textwindows int ProcessKeyEvent(const struct NtInputRecord *r, char *p,
uint16_t *utf16hs, struct Fd *f) {
uint16_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;
}
// ignore numpad being used to compose a character
if ((cks & kNtLeftAltPressed) && !(cks & kNtEnhancedKey) &&
(vk == kNtVkInsert || vk == kNtVkEnd || vk == kNtVkDown ||
vk == kNtVkNext || vk == kNtVkLeft || vk == kNtVkClear ||
vk == kNtVkRight || vk == kNtVkHome || vk == kNtVkUp ||
vk == kNtVkPrior || vk == kNtVkNumpad0 || vk == kNtVkNumpad1 ||
vk == kNtVkNumpad2 || vk == kNtVkNumpad3 || vk == kNtVkNumpad4 ||
vk == kNtVkNumpad5 || vk == kNtVkNumpad6 || vk == kNtVkNumpad7 ||
vk == kNtVkNumpad8 || vk == kNtVkNumpad9)) {
return 0;
}
int n = 0;
// process virtual keys
if (!c) {
uint32_t w;
w = GetVirtualKey(vk, !!(cks & kNtShiftPressed),
!!(cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed)));
if (!w) return 0;
p[n++] = 033;
if (cks & (kNtLeftAltPressed | kNtRightAltPressed)) {
p[n++] = 033;
}
p[n++] = '[';
do p[n++] = w;
while ((w >>= 8));
return n;
}
// translate utf-16 into utf-32
if (IsHighSurrogate(c)) {
*utf16hs = c;
return 0;
}
if (IsLowSurrogate(c)) {
c = MergeUtf16(*utf16hs, c);
}
// enter sends \r in a raw terminals
// make it a multics newline instead
if (c == '\r' && !(__ttymagic & kFdTtyNoCr2Nl)) {
c = '\n';
}
// microsoft doesn't encode ctrl-space (^@) as nul
// detecting it is also impossible w/ kNtEnableVirtualTerminalInput
if (c == ' ' && (cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed))) {
c = '\0';
}
// make it possible to distinguish ctrl-h (^H) from backspace (^?)
if (c == kNtVkBack) {
c = 0177;
}
// handle ctrl-c and ctrl-\, which tcsetattr() is able to remap
if (!(__ttymagic & kFdTtyNoIsigs)) {
if (c == __vintr && __vintr != _POSIX_VDISABLE) {
return ProcessSignal(SIGINT, f);
} else if (c == __vquit && __vquit != _POSIX_VDISABLE) {
return ProcessSignal(SIGQUIT, f);
}
}
// handle ctrl-d the end of file keystroke
if (c == __veof && __veof != _POSIX_VDISABLE) {
return enodata();
}
// insert esc prefix when alt is held
if ((cks & (kNtLeftAltPressed | kNtRightAltPressed)) &&
!(cks & (kNtLeftCtrlPressed | kNtRightCtrlPressed)) &&
r->Event.KeyEvent.bKeyDown) {
p[n++] = 033;
}
// convert utf-32 to utf-8
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.com and o//tool/viz/life.com
static textwindows int ProcessMouseEvent(const struct NtInputRecord *r, char *b,
struct Fd *f) {
int e = 0;
char *p = b;
uint32_t currentbs = f ? f->mousebuttons : 0;
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)
if (!(r->Event.MouseEvent.dwControlKeyState &
(kNtShiftPressed | kNtLeftCtrlPressed | kNtRightCtrlPressed |
kNtLeftAltPressed | kNtRightAltPressed))) {
bool isup = ((int)r->Event.MouseEvent.dwButtonState >> 16) > 0;
if (__ttymagic & kFdTtyXtMouse) {
e = isup ? 80 : 81;
goto OutputXtermMouseEvent;
} else {
// we disable mouse highlighting when the tty is put in raw mode
// to mouse wheel events with widely understood vt100 arrow keys
*p++ = 033;
*p++ = '[';
if (isup) {
*p++ = 'A'; // \e[A up
} else {
*p++ = 'B'; // \e[B down
}
}
}
} else if ((bs || currentbs) && (__ttymagic & kFdTtyXtMouse)) {
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);
*p++ = ';';
p = FormatInt32(p, r->Event.MouseEvent.dwMousePosition.Y + 1);
if (!bs && currentbs) {
*p++ = 'm'; // up
} else {
*p++ = 'M'; // down
}
if (f) {
f->mousebuttons = bs;
}
}
return p - b;
}
textwindows int ConvertConsoleInputToAnsi(const struct NtInputRecord *r,
char p[hasatleast 32],
uint16_t *utf16hs, struct Fd *f) {
switch (r->EventType) {
case kNtKeyEvent:
return ProcessKeyEvent(r, p, utf16hs, f);
case kNtMouseEvent:
return ProcessMouseEvent(r, p, f);
case kNtWindowBufferSizeEvent:
return ProcessSignal(SIGWINCH, f);
default:
return 0;
}
}
static textwindows ssize_t ReadFromWindowsConsole(struct Fd *f, void *data,
size_t size) {
ssize_t rc;
int e = errno;
uint16_t utf16hs = 0;
pthread_mutex_lock(&f->lock);
for (;;) {
if (f->eoftty) {
rc = 0;
break;
}
uint32_t got = MIN(size, f->buflen);
uint32_t remain = f->buflen - got;
if (got) memcpy(data, f->buf, got);
if (remain) memmove(f->buf, f->buf + got, remain);
f->buflen = remain;
if (got) {
rc = got;
break;
}
uint32_t events_available;
if (!GetNumberOfConsoleInputEvents(f->handle, &events_available)) {
rc = __winerr();
break;
}
if (events_available) {
uint32_t n;
struct NtInputRecord r;
if (!ReadConsoleInput(f->handle, &r, 1, &n)) {
rc = __winerr();
break;
}
rc = ConvertConsoleInputToAnsi(&r, f->buf, &utf16hs, f);
if (rc == -1) {
if (errno == ENODATA) {
f->eoftty = true;
errno = e;
rc = 0;
}
break;
}
f->buflen = rc;
} else {
if (f->flags & O_NONBLOCK) {
rc = 0;
break;
}
uint32_t ms = __SIG_POLL_INTERVAL_MS;
if (__ttymagic & kFdTtyNoBlock) {
if (!__vtime) {
rc = 0;
break;
} else {
ms = __vtime * 100;
}
}
if ((rc = _check_interrupts(kSigOpRestartable))) {
break;
}
struct PosixThread *pt = _pthread_self();
pt->pt_flags |= PT_INSEMAPHORE;
WaitForSingleObject(pt->semaphore, ms);
pt->pt_flags &= ~PT_INSEMAPHORE;
if (pt->abort_errno == ECANCELED) {
rc = ecanceled();
break;
}
}
}
pthread_mutex_unlock(&f->lock);
if (rc > 0 && (__ttymagic & kFdTtyEchoing)) {
EchoTerminalInput(f, data, size);
}
return rc;
}
textwindows ssize_t sys_read_nt_impl(int fd, void *data, size_t size,
int64_t offset) {
// perform the read i/o operation
bool32 ok;
struct Fd *f;
uint32_t got;
int64_t handle;
uint32_t remain;
char *targetdata;
uint32_t targetsize;
struct PosixThread *pt;
f = g_fds.p + fd;
handle = __resolve_stdin_handle(f->handle);
handle = f->handle;
pt = _pthread_self();
pt->abort_errno = EAGAIN;
size = MIN(size, 0x7ffff000);
bool pwriting = offset != -1;
bool seekable = f->kind == kFdFile && GetFileType(handle) == kNtFileTypeDisk;
bool nonblock = !!(f->flags & O_NONBLOCK);
pt->abort_errno = EAGAIN;
if (pwriting && !seekable) {
return espipe();
@ -85,33 +444,9 @@ textwindows ssize_t sys_read_nt_impl(int fd, void *data, size_t size,
offset = 0;
}
uint32_t dwConsoleMode;
bool is_console_input =
g_fds.stdin.handle
? f->handle == g_fds.stdin.handle
: !seekable && (f->kind == kFdConsole ||
GetConsoleMode(handle, &dwConsoleMode));
StartOver:
size = MIN(size, 0x7ffff000);
// the caller might be reading a single byte at a time. but we need to
// be able to munge three bytes into just 1 e.g. "\342\220\200" → "\0"
if (size && f->buflen) {
ReturnDataFromBuffer:
got = MIN(size, f->buflen);
remain = f->buflen - got;
if (got) memcpy(data, f->buf, got);
if (remain) memmove(f->buf, f->buf + got, remain);
f->buflen = remain;
return got;
}
if (is_console_input && size && size < 3 && (__ttymagic & kFdTtyMunging)) {
targetdata = f->buf;
targetsize = sizeof(f->buf);
} else {
targetdata = data;
targetsize = size;
uint32_t cm;
if (!seekable && (f->kind == kFdConsole || GetConsoleMode(handle, &cm))) {
return ReadFromWindowsConsole(f, data, size);
}
if (!pwriting && seekable) {
@ -123,7 +458,7 @@ StartOver:
.Pointer = offset};
// the win32 manual says it's important to *not* put &got here
// since for overlapped i/o, we always use GetOverlappedResult
ok = ReadFile(handle, targetdata, targetsize, 0, &overlap);
ok = ReadFile(handle, data, size, 0, &overlap);
if (!ok && GetLastError() == kNtErrorIoPending) {
BlockingOperation:
if (!nonblock) {
@ -172,27 +507,6 @@ StartOver:
}
if (ok) {
if (is_console_input) {
if (__ttymagic & kFdTtyMunging) {
switch (_weaken(__munge_terminal_input)(targetdata, &got)) {
case DO_NOTHING:
break;
case DO_RESTART:
goto StartOver;
case DO_EINTR:
return eintr();
default:
__builtin_unreachable();
}
}
if (__ttymagic & kFdTtyEchoing) {
_weaken(__echo_terminal_input)(f, targetdata, got);
}
}
if (targetdata != data) {
f->buflen = got;
goto ReturnDataFromBuffer;
}
return got;
}