From 293bdab8abeb8f13a96ecb41d575147c5dac1a2d Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:39:52 +0900 Subject: [PATCH 01/10] Closing a directory ends its unfinished enumerations, and a closed directory is refused by kal_fs_list_next The shape openkal-linux#34 measured was here too. Listings not yet read to the end are kept on a list under a slim lock with their directory's handle, the close ends them, a listing another context is reading is released by that context on its way out (clause 6.6), and the iterator is found among the open listings rather than read through. --- src/fs.cpp | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/src/fs.cpp b/src/fs.cpp index 33e07ac..3a0fdf1 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -230,6 +230,10 @@ void fill_absent(kal_node_info* out) { // each other's entries. struct listing { void* handle; + void* dir; // the handle of the directory it was begun upon + listing* next; + int busy; // a context is inside kal_fs_list_next with it + int doomed; // its directory was closed while it was busy okw_uptr used; okw_uptr at; bool first; @@ -248,6 +252,72 @@ struct listing { char reported[512]; }; +// THE ENUMERATIONS NOT YET READ TO THE END, BECAUSE CLOSING THE DIRECTORY MUST +// END THEM. fs.h releases an iterator "by reading past the end or by closing +// the directory that produced it", and the handle a listing holds is its own, +// so closing the directory's handle does not reach it. Measured on the Linux +// implementation, which had the same shape (mcpplibs/openkal-linux#34): a C +// library above that stopped reading a directory early kept a handle for every +// directory it closed. +// +// A slim lock, because it is one pointer, all-zero is unlocked, and it needs +// nothing from openkal.task. +listing* g_listings = nullptr; +SRWLOCK_ g_listings_lock{}; + +void listing_release(listing* s) { + okw::NtClose(s->handle); + kal_free(s, sizeof(listing), alignof(listing)); +} + +// Ends every listing begun upon `dir'. A listing another context is reading is +// marked rather than released, and that context releases it on its way out: +// clause 6.6 requires that concurrent operations upon one handle not damage +// this implementation's state, and freeing a buffer the system is writing into +// would. +void listings_end(void* dir) { + listing* done = nullptr; + AcquireSRWLockExclusive(&g_listings_lock); + for (listing** p = &g_listings; *p != nullptr;) { + listing* s = *p; + if (s->dir != dir || s->doomed) { p = &s->next; continue; } + if (s->busy) { s->doomed = 1; p = &s->next; continue; } + *p = s->next; s->next = done; done = s; + } + ReleaseSRWLockExclusive(&g_listings_lock); + while (done != nullptr) { listing* s = done; done = s->next; listing_release(s); } +} + +// Takes `s' for the calling context, or reports that it is not an open listing +// of `dir'. The word is found among the open listings rather than read through, +// so a stale iterator is refused rather than followed. +bool listing_take(listing* s, void* dir) { + for (;;) { + AcquireSRWLockExclusive(&g_listings_lock); + listing* p = g_listings; + while (p != nullptr && p != s) p = p->next; + if (p == nullptr || p->dir != dir || p->doomed) { + ReleaseSRWLockExclusive(&g_listings_lock); + return false; + } + if (!p->busy) { p->busy = 1; ReleaseSRWLockExclusive(&g_listings_lock); return true; } + ReleaseSRWLockExclusive(&g_listings_lock); + SwitchToThread(); + } +} + +void listing_give(listing* s, bool ended) { + AcquireSRWLockExclusive(&g_listings_lock); + s->busy = 0; + const bool release = ended || s->doomed; + if (release) { + for (listing** p = &g_listings; *p != nullptr; p = &(*p)->next) + if (*p == s) { *p = s->next; break; } + } + ReleaseSRWLockExclusive(&g_listings_lock); + if (release) listing_release(s); +} + } // namespace extern "C" { @@ -348,7 +418,11 @@ int kal_fs_open(kal_dir base, const char* name, kal_uintptr len, void kal_fs_close_dir(kal_dir d) { void* h = dir_handle(d); - if (h) { okw::retire(d.h); okw::NtClose(h); } + if (!h) return; + // Before the close, because after it the same value may already name + // another directory whose enumerations are not this one's. + listings_end(h); + okw::retire(d.h); okw::NtClose(h); } void kal_fs_close_file(kal_file f) { @@ -647,16 +721,24 @@ int kal_fs_list_begin(kal_dir d, kal_uintptr* iter) { auto* s = static_cast(kal_alloc(sizeof(listing), alignof(listing))); if (!s) { okw::NtClose(own); return kal_err_no_memory; } - s->handle = own; s->used = 0; s->at = 0; s->first = true; + s->handle = own; s->dir = root; s->busy = 0; s->doomed = 0; + s->used = 0; s->at = 0; s->first = true; + AcquireSRWLockExclusive(&g_listings_lock); + s->next = g_listings; g_listings = s; + ReleaseSRWLockExclusive(&g_listings_lock); *iter = reinterpret_cast(s); return kal_ok; } -int kal_fs_list_next(kal_dir, kal_uintptr* iter, +int kal_fs_list_next(kal_dir d, kal_uintptr* iter, char* name_out, kal_uintptr name_cap, kal_uintptr* name_len, int* kind) { - if (iter == nullptr || *iter == 0) return kal_err_invalid; + // A closed directory is a released handle (clause 6.7), and closing it + // released the iterator too, which fs.h states. + void* root = dir_handle(d); + if (!root || iter == nullptr || *iter == 0) return kal_err_invalid; auto* s = reinterpret_cast(*iter); + if (!listing_take(s, root)) return kal_err_invalid; for (;;) { if (s->at >= s->used) { @@ -667,8 +749,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, okw::file_directory_information_class, 0, nullptr, s->first ? 1 : 0); s->first = false; if (!okw::ok(r)) { - okw::NtClose(s->handle); - kal_free(s, sizeof(listing), alignof(listing)); + listing_give(s, true); *iter = 0; if (name_len) *name_len = 0; // The end of a directory is reported here as a distinct status @@ -697,6 +778,7 @@ int kal_fs_list_next(kal_dir, kal_uintptr* iter, if (kind) *kind = (e->file_attributes & FILE_ATTRIBUTE_REPARSE_POINT) ? kal_node_link : (e->file_attributes & FILE_ATTRIBUTE_DIRECTORY) ? kal_node_directory : kal_node_file; + listing_give(s, false); return kal_ok; } } From 685a77e8b371c337e0d8803ed8301ad14622aebb Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:39:52 +0900 Subject: [PATCH 02/10] A started context is given 8 MiB unless more is asked for, and kal_task_start_sized asks A reservation, as the other two implementations give, so a program that asks for nothing has the same room on all three. The reservation exceeds the request by the pages this environment keeps for its guard, so the context is told at least what it asked for. --- src/task.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/task.cpp b/src/task.cpp index 203e383..957a590 100644 --- a/src/task.cpp +++ b/src/task.cpp @@ -28,19 +28,41 @@ DWORD __stdcall run(void* p) { return 0; } -} // namespace +// THE SIZE A CONTEXT IS GIVEN WHEN IT ASKS FOR NONE. This environment would +// reserve what the program's header states, 1 MiB by default, and the two other +// implementations give a context the 8 MiB the host's C library on their systems +// gives a thread; a program that asks for nothing --- C++'s `std::thread' cannot +// ask --- is given the same room on all three. It is a reservation, committed as +// the stack grows, so the size costs address space and not memory. +// +// No guard is added. This environment places its own below the committed part +// of the stack and moves it as the stack grows, and the compilers that target it +// probe a frame larger than a page one page at a time, so a large frame cannot +// step over it. +constexpr unsigned long long kDefaultStack = 8ull * 1024u * 1024u; + +// What a request for `want' bytes is reserved as. The bounds this environment +// reports to the context include the pages it keeps for its guard, so the +// reservation exceeds the request by more than those; what the context is told +// by kal_task_stack is then at least what it asked for. +constexpr unsigned long long kSlack = 256u * 1024u; + +unsigned long long stack_for(unsigned long long want) { + if (want > (1ull << 46)) return 0; + const unsigned long long w = (want + 0xffffu) & ~0xffffull; // the reservation granularity + return (w > kDefaultStack ? w : kDefaultStack) + kSlack; +} -extern "C" { +constexpr DWORD stack_size_param_is_a_reservation = 0x00010000; -int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) { +int start(void (*entry)(void*), void* arg, unsigned long long want, kal_task* out) { if (entry == nullptr || out == nullptr) return kal_err_invalid; + const unsigned long long bytes = stack_for(want); + if (bytes == 0) return kal_err_no_memory; auto* c = static_cast(kal_alloc(sizeof(context), alignof(context))); if (!c) return kal_err_no_memory; c->entry = entry; c->arg = arg; - // The stack is the implementation's, which is what the specification says - // of it: an environment that does not allocate stacks separately cannot - // honour a request for one, so there is no parameter for it. - void* h = CreateThread(nullptr, 0, run, c, 0, nullptr); + void* h = CreateThread(nullptr, bytes, run, c, stack_size_param_is_a_reservation, nullptr); if (!h) { const int e = okw::translate_win32(GetLastError()); kal_free(c, sizeof(context), alignof(context)); @@ -51,6 +73,21 @@ int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) { return kal_ok; } +} // namespace + +extern "C" { + +int kal_task_start(void (*entry)(void*), void* arg, kal_task* out) { + return start(entry, arg, 0, out); +} + +// A least size, version 0.16: at least the size asked for, and never less than +// the default. +int kal_task_start_sized(void (*entry)(void*), void* arg, kal_uintptr stack_size, + kal_task* out) { + return start(entry, arg, stack_size, out); +} + int kal_task_join(kal_task t) { auto* c = reinterpret_cast(t.h); if (!c) return kal_err_invalid; From bc6aa6ab7a04f35ef23a7188273d07227052c570 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:39:52 +0900 Subject: [PATCH 03/10] KAL_TERM_PASS_INPUT is line input, inverted This environment alters what is typed only while it assembles lines, and has no switch of its own for the alteration; the position is reported as it stands and a request for it is neither refused nor composed. --- src/terminal.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/terminal.cpp b/src/terminal.cpp index b68fb17..89f4c8d 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -50,6 +50,14 @@ int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) { if ((m & enable_line_input) != 0) out |= KAL_TERM_LINE_EDIT; if ((m & enable_echo_input) != 0) out |= KAL_TERM_ECHO; if ((m & enable_processed_input) == 0) out |= KAL_TERM_PASS_CONTROL; + // KAL_TERM_PASS_INPUT IS LINE INPUT, INVERTED, AND IT IS NOT A FLAG OF ITS + // OWN. This environment alters what is typed only while it assembles lines + // --- the return key then ends a line as a carriage return and a line feed + // --- and with line input off it delivers the return key as a carriage + // return and Ctrl+J as a line feed, which is what the position states. + // Measured on a console this implementation's tests allocate, with the keys + // written into its input as key events. + if ((m & enable_line_input) == 0) out |= KAL_TERM_PASS_INPUT; *mode = out; return kal_ok; } @@ -80,6 +88,11 @@ int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) { // several mechanisms; this one stands for exactly one, and assigning it // from the mode word is the same act as leaving it alone. + // KAL_TERM_PASS_INPUT HAS NO SWITCH HERE, so a request for it is neither + // refused nor composed. It follows line input, and kal_terminal_get_mode + // reports it as it then stands, which is what terminal.h states of an + // environment that cannot separate the alteration from line assembly. + // A position this implementation does not distinguish is ignored rather than // refused, which clause 6.2 requires of a word. if (!SetConsoleMode(h, m)) return kal_err_not_supported; From 40003f9debb137d07cb04108a89f9d28b9ed151b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 05:39:58 +0900 Subject: [PATCH 04/10] 0.12.0 (draft) --- written against openkal 0.16, and observed The leak is counted in this environment's table of the process's handles; a closed directory and a mismatched one are refused, and a close racing a read ends it. A console allocated by the test is given the return key and Ctrl+J as key events, and what arrives under each mode is printed beside the position's read-back. A least stack size is room, and asking for nothing gives 8 MiB. --- README.md | 4 +- mcpp.toml | 4 +- tests/console_input.cpp | 135 ++++++++++++++++++++++++++++ tests/enumeration_ends_at_close.cpp | 106 ++++++++++++++++++++++ tests/stack_size.cpp | 63 +++++++++++++ 5 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 tests/console_input.cpp create mode 100644 tests/enumeration_ends_at_close.cpp create mode 100644 tests/stack_size.cpp diff --git a/README.md b/README.md index 6a241d8..8e35bc0 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ An implementation of [openkal](https://github.com/mcpplibs/openkal) for Windows. ```toml [dependencies] -openkal = "0.15.0" +openkal = "0.16.0" [target.'cfg(windows)'.dependencies] -openkal-windows = "0.11.0" +openkal-windows = "0.12.0" ``` Its purpose is as much to test the specification as to be used. openkal was diff --git a/mcpp.toml b/mcpp.toml index 7cea71d..ebb0674 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-windows" -version = "0.11.0" +version = "0.12.0" description = "An implementation of openkal for Windows, written on the Win32 interfaces and the object manager beneath them, using no C runtime symbol." license = "Apache-2.0" @@ -62,7 +62,7 @@ provides-interfaces = [ ] [dependencies] -openkal = "0.15.0" +openkal = "0.16.0" # The package contributes definitions and no modules. The interface it # implements is declared by the specification package, which this package diff --git a/tests/console_input.cpp b/tests/console_input.cpp new file mode 100644 index 0000000..37c591a --- /dev/null +++ b/tests/console_input.cpp @@ -0,0 +1,135 @@ +// What a console delivers for the return key and for Ctrl+J, measured, and what +// KAL_TERM_PASS_INPUT (openkal 0.16) reports for it. +// +// mcpplibs/openkal#48: above the two other implementations the return key +// reached a program in raw mode as a line feed, the same byte Ctrl+J produces. +// The position that answers it is meant to make the bytes a program reads the +// same on all three when it is set, so this test writes the keys into a console +// of its own as key events --- the form a keyboard produces --- reads them back +// through kal_stream_read, prints what arrived under each mode, and checks: +// +// with line input off, the position reads set, and the return key is 0x0d +// and Ctrl+J is 0x0a, which is what the other two deliver with it set; +// with line input on, the position reads clear. +// +// A console is allocated rather than inherited, because a runner's own may be +// absent or redirected. A watchdog ends the test if a read waits for a key that +// never arrives, so a wrong expectation is a failure rather than a hang. +#include +#include +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool held, const char* what) { + std::printf(" %s %s\n", held ? "held" : "FAIL", what); + std::fflush(stdout); + if (!held) ++failures; +} + +DWORD WINAPI watchdog(void*) { + Sleep(20000); + std::printf(" FAIL a read waited for a key that never arrived\n"); + std::fflush(stdout); + ExitProcess(3); +} + +void key(INPUT_RECORD* r, WORD vk, wchar_t ch, DWORD ctrl, BOOL down) { + ZeroMemory(r, sizeof *r); + r->EventType = KEY_EVENT; + r->Event.KeyEvent.bKeyDown = down; + r->Event.KeyEvent.wRepeatCount = 1; + r->Event.KeyEvent.wVirtualKeyCode = vk; + r->Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKeyW(vk, 0)); + r->Event.KeyEvent.uChar.UnicodeChar = ch; + r->Event.KeyEvent.dwControlKeyState = ctrl; +} + +// Writes the keys `keys' as presses and releases, then reads `want' bytes. +int type_and_read(HANDLE in, kal_stream s, const char* keys, unsigned char* out, int want) { + FlushConsoleInputBuffer(in); + INPUT_RECORD rec[32]; + int n = 0; + for (const char* k = keys; *k; ++k) { + WORD vk; wchar_t ch; DWORD ctrl = 0; + if (*k == 'E') { vk = VK_RETURN; ch = L'\r'; } + else if (*k == 'J') { vk = 'J'; ch = L'\n'; ctrl = LEFT_CTRL_PRESSED; } + else { vk = static_cast(*k - 'a' + 'A'); ch = static_cast(*k); } + key(&rec[n++], vk, ch, ctrl, TRUE); + key(&rec[n++], vk, ch, ctrl, FALSE); + } + DWORD written = 0; + if (!WriteConsoleInputW(in, rec, static_cast(n), &written)) return -1; + int got = 0; + while (got < want) { + const kal_intptr r = kal_stream_read(s, out + got, static_cast(want - got)); + if (r <= 0) break; + got += static_cast(r); + } + return got; +} + +void show(const char* mode, const unsigned char* b, int n) { + std::printf(" measured %-34s", mode); + for (int i = 0; i < n; ++i) std::printf(" %02x", b[i]); + std::printf("\n"); +} + +} // namespace + +int main() { + FreeConsole(); + if (!AllocConsole()) { + std::printf("openkal-windows: no console could be allocated; not observed\n"); + return 0; + } + HANDLE in = CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + if (in == INVALID_HANDLE_VALUE) { + std::printf("openkal-windows: the console's input could not be opened; not observed\n"); + return 0; + } + CreateThread(nullptr, 0, watchdog, nullptr, 0, nullptr); + const kal_stream s{ reinterpret_cast(in) }; + unsigned char b[8] = {}; + kal_uintptr m = 0; + + // Raw, with the environment's reserved keys kept: the ordinary raw mode. + check(kal_terminal_set_mode(s, 0) == kal_ok, "line input and echo are turned off"); + check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, + "with line input off the position reads set"); + int got = type_and_read(in, s, "EJ", b, 2); + show("line off, processed on: Enter Ctrl+J", b, got); + check(got == 2 && b[0] == 0x0d && b[1] == 0x0a, "the return key is 0x0d and Ctrl+J is 0x0a"); + + // Raw, and nothing reserved: what a program that reads keystrokes asks for. + check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL | KAL_TERM_PASS_INPUT) == kal_ok, + "every keystroke, unaltered, is requested"); + check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, + "the position reads set when it is requested with line input off"); + got = type_and_read(in, s, "EJ", b, 2); + show("line off, processed off: Enter Ctrl+J", b, got); + check(got == 2 && b[0] == 0x0d && b[1] == 0x0a, "the same two bytes with nothing reserved"); + + // A request this environment cannot separate from line input is left as it + // stands, and the read-back says so. + check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL) == kal_ok, "altered input is requested in raw mode"); + check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, + "the read-back reports what is in effect rather than what was asked"); + + // Lines assembled: the position reads clear, and what arrives is measured. + check(kal_terminal_set_mode(s, KAL_TERM_LINE_EDIT) == kal_ok, "line input is turned on"); + check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) == 0, + "with line input on the position reads clear"); + got = type_and_read(in, s, "aE", b, 3); + show("line on: a Enter", b, got); + check(got >= 2 && b[0] == 'a', "a line is delivered"); + + std::printf("openkal-windows: what a console delivers, %s\n", failures ? "FAILED" : "held"); + std::fflush(stdout); + return failures == 0 ? 0 : 1; +} diff --git a/tests/enumeration_ends_at_close.cpp b/tests/enumeration_ends_at_close.cpp new file mode 100644 index 0000000..96aaa4e --- /dev/null +++ b/tests/enumeration_ends_at_close.cpp @@ -0,0 +1,106 @@ +// What closing a directory does to its enumerations (fs.h, openkal 0.16). +// +// The Linux implementation kept a descriptor for every directory a C library +// above it had stopped reading early (mcpplibs/openkal-linux#34), and this one +// had the same shape: only reaching the end closed the handle an enumeration +// opened. Counted in this environment's own table of the process's handles, +// because a count of open handles is not portable and the conformance suite +// cannot make it (SPEC clause 11, entry 24). +#include +#include "../src/win32.h" +#include +#include +#include + +extern "C" OKW_IMPORT BOOL OKW_API GetProcessHandleCount(HANDLE, DWORD*); +extern "C" OKW_IMPORT HANDLE OKW_API GetCurrentProcess(void); + +namespace { + +int failures = 0; + +void check(bool held, const char* what) { + std::printf(" %s %s\n", held ? "held" : "FAIL", what); + if (!held) ++failures; +} + +DWORD handles() { DWORD n = 0; return GetProcessHandleCount(GetCurrentProcess(), &n) ? n : 0; } + +struct race { kal_dir d; volatile int told; }; + +void reader(void* p) { + auto* r = static_cast(p); + kal_uintptr it = 0; + if (kal_fs_list_begin(r->d, &it) != kal_ok) { r->told = 1; return; } + for (;;) { + char name[512]; kal_uintptr len = 0; int kind = 0; + if (kal_fs_list_next(r->d, &it, name, sizeof name, &len, &kind) != kal_ok) { r->told = 1; return; } + if (it == 0 && kal_fs_list_begin(r->d, &it) != kal_ok) { r->told = 1; return; } + } +} + +} // namespace + +int main() { + kal_dir cwd{}; + if (kal_fs_preopen_count() == 0 || kal_fs_preopen(0, &cwd, nullptr, 0, nullptr) != kal_ok) { + std::printf("openkal-windows: no preopened directory to test in\n"); + return 1; + } + kal_fs_mkdir(cwd, "okw-enum.d", 10); + kal_file f{}; + if (kal_fs_open(cwd, "okw-enum.d/inner", 16, KAL_OPEN_WRITE | KAL_OPEN_CREATE, &f) == kal_ok) + kal_fs_close_file(f); + + const DWORD before = handles(); + for (int i = 0; i < 64; ++i) { + kal_dir d{}; kal_uintptr it = 0; + if (kal_fs_open_dir(cwd, "okw-enum.d", 10, &d) != kal_ok) break; + if (kal_fs_list_begin(d, &it) == kal_ok) { + char name[512]; kal_uintptr len = 0; int kind = 0; + kal_fs_list_next(d, &it, name, sizeof name, &len, &kind); + } + kal_fs_close_dir(d); + } + const DWORD after = handles(); + std::printf(" handles before=%lu after=%lu\n", (unsigned long)before, (unsigned long)after); + if (before == 0) std::printf(" not observed the environment did not count the process's handles\n"); + else check(after == before, "closing a directory ends an enumeration left part-way"); + + { + kal_dir once{}; kal_uintptr it = 0; + if (kal_fs_open_dir(cwd, "okw-enum.d", 10, &once) == kal_ok && kal_fs_list_begin(once, &it) == kal_ok) { + kal_fs_close_dir(once); + char name[512]; kal_uintptr len = 0; int kind = 0; + check(kal_fs_list_next(once, &it, name, sizeof name, &len, &kind) == kal_err_invalid, + "an enumeration of a closed directory is refused"); + } + kal_dir a{}, b{}; kal_uintptr ia = 0; + if (kal_fs_open_dir(cwd, "okw-enum.d", 10, &a) == kal_ok + && kal_fs_open_dir(cwd, "okw-enum.d", 10, &b) == kal_ok + && kal_fs_list_begin(a, &ia) == kal_ok) { + char name[512]; kal_uintptr len = 0; int kind = 0; + check(kal_fs_list_next(b, &ia, name, sizeof name, &len, &kind) == kal_err_invalid, + "an iterator is refused with a directory that did not produce it"); + } + kal_fs_close_dir(a); kal_fs_close_dir(b); + } + + int ended = 0; + for (int i = 0; i < 200; ++i) { + race r{}; + if (kal_fs_open_dir(cwd, "okw-enum.d", 10, &r.d) != kal_ok) break; + kal_task t{}; + if (kal_task_start(reader, &r, &t) != kal_ok) { kal_fs_close_dir(r.d); break; } + for (int spin = 0; spin < i * 10; ++spin) kal_task_yield(); + kal_fs_close_dir(r.d); + kal_task_join(t); + ended += r.told; + } + check(ended == 200, "an enumeration racing the close of its directory ends and is told so"); + + kal_fs_remove(cwd, "okw-enum.d/inner", 16); + kal_fs_remove(cwd, "okw-enum.d", 10); + std::printf("openkal-windows: what closing a directory ends, %s\n", failures ? "FAILED" : "held"); + return failures == 0 ? 0 : 1; +} diff --git a/tests/stack_size.cpp b/tests/stack_size.cpp new file mode 100644 index 0000000..392b04a --- /dev/null +++ b/tests/stack_size.cpp @@ -0,0 +1,63 @@ +// The least stack size a started context is given (openkal 0.16). +// +// WHAT IS OBSERVED IS ROOM: a recursion of three quarters of the request +// completes, and the context is told at least the size it asked for. Asking for +// nothing gives the 8 MiB the other two implementations give. +#include +#include +#include + +namespace { + +int failures = 0; + +void check(bool held, const char* what) { + std::printf(" %s %s\n", held ? "held" : "FAIL", what); + if (!held) ++failures; +} + +long recurse(long n); +long (*volatile g_recurse)(long) = recurse; +long recurse(long n) { + volatile char pad[1000]; + pad[0] = static_cast(n); + if (n == 0) return 0; + return g_recurse(n - 1) + pad[0]; +} + +struct probe { kal_uintptr ask; kal_uintptr told; volatile int done; }; + +void room(void* p) { + auto* q = static_cast(p); + void* base = nullptr; kal_uintptr size = 0; + if (kal_task_stack(&base, &size) == kal_ok) q->told = size; + g_recurse(static_cast(q->ask / 1024 * 3 / 4)); + q->done = 1; +} + +} // namespace + +int main() { + const kal_uintptr asks[] = { 1u << 20, 16u << 20, 64u << 20 }; + for (kal_uintptr ask : asks) { + probe q{ ask, 0, 0 }; + kal_task t{}; + check(kal_task_start_sized(room, &q, ask, &t) == kal_ok, "a context with a least stack size starts"); + kal_task_join(t); + std::printf(" asked %llu, told %llu\n", (unsigned long long)ask, (unsigned long long)q.told); + check(q.told >= ask, "the context is told at least the size asked for"); + check(q.done == 1, "the context has the room it asked for"); + } + probe d{ 0, 0, 0 }; + kal_task t{}; + check(kal_task_start(room, &d, &t) == kal_ok, "a context starts"); + kal_task_join(t); + std::printf(" asked nothing, told %llu\n", (unsigned long long)d.told); + check(d.told >= (8u << 20), "a context that asks for nothing is given 8 MiB"); + + kal_task never{}; + check(kal_task_start_sized(room, nullptr, ~kal_uintptr{0} - 4095, &never) == kal_err_no_memory, + "a stack larger than the address space is reported as exhaustion"); + std::printf("openkal-windows: a least stack size, %s\n", failures ? "FAILED" : "held"); + return failures == 0 ? 0 : 1; +} From d7c14c39456f89a7258592e7b4f9df90c7579ab2 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:11:59 +0900 Subject: [PATCH 05/10] The console test writes key events without asking for a scan code The call that maps a key to one is user32's, which a test linked as this package's are does not reach; a key event written by a program carries none. --- tests/console_input.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/console_input.cpp b/tests/console_input.cpp index 37c591a..7bc77c1 100644 --- a/tests/console_input.cpp +++ b/tests/console_input.cpp @@ -44,7 +44,7 @@ void key(INPUT_RECORD* r, WORD vk, wchar_t ch, DWORD ctrl, BOOL down) { r->Event.KeyEvent.bKeyDown = down; r->Event.KeyEvent.wRepeatCount = 1; r->Event.KeyEvent.wVirtualKeyCode = vk; - r->Event.KeyEvent.wVirtualScanCode = static_cast(MapVirtualKeyW(vk, 0)); + r->Event.KeyEvent.wVirtualScanCode = 0; // a key event written by a program carries none r->Event.KeyEvent.uChar.UnicodeChar = ch; r->Event.KeyEvent.dwControlKeyState = ctrl; } From ed8f7af928b57893c9a309041651d2a1bcf30281 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:20:06 +0900 Subject: [PATCH 06/10] The console test waits for input before reading and says what was queued --- tests/console_input.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/console_input.cpp b/tests/console_input.cpp index 7bc77c1..fce442e 100644 --- a/tests/console_input.cpp +++ b/tests/console_input.cpp @@ -63,9 +63,23 @@ int type_and_read(HANDLE in, kal_stream s, const char* keys, unsigned char* out, key(&rec[n++], vk, ch, ctrl, FALSE); } DWORD written = 0; - if (!WriteConsoleInputW(in, rec, static_cast(n), &written)) return -1; + if (!WriteConsoleInputW(in, rec, static_cast(n), &written)) { + std::printf(" WriteConsoleInputW failed: %lu\n", GetLastError()); + return -1; + } + DWORD queued = 0; + GetNumberOfConsoleInputEvents(in, &queued); + std::printf(" wrote %lu of %d events; %lu queued\n", written, n, queued); int got = 0; while (got < want) { + // A read is made only once the console says there is input, so a key + // event the console does not turn into a byte is reported rather than + // waited for. + if (WaitForSingleObject(in, 3000) != WAIT_OBJECT_0) { + GetNumberOfConsoleInputEvents(in, &queued); + std::printf(" no input within 3 s; %lu events still queued\n", queued); + break; + } const kal_intptr r = kal_stream_read(s, out + got, static_cast(want - got)); if (r <= 0) break; got += static_cast(r); From cb1d580265713b15df31d98e04c6b4b74d2935ea Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:24:21 +0900 Subject: [PATCH 07/10] The console test measures three spellings of Ctrl+J and asserts what the position states: the return key is 0x0d, and Ctrl+J is not it --- tests/console_input.cpp | 51 ++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/tests/console_input.cpp b/tests/console_input.cpp index fce442e..a68967a 100644 --- a/tests/console_input.cpp +++ b/tests/console_input.cpp @@ -38,29 +38,42 @@ DWORD WINAPI watchdog(void*) { ExitProcess(3); } -void key(INPUT_RECORD* r, WORD vk, wchar_t ch, DWORD ctrl, BOOL down) { +void key(INPUT_RECORD* r, WORD vk, WORD scan, wchar_t ch, DWORD ctrl, BOOL down) { ZeroMemory(r, sizeof *r); r->EventType = KEY_EVENT; r->Event.KeyEvent.bKeyDown = down; r->Event.KeyEvent.wRepeatCount = 1; r->Event.KeyEvent.wVirtualKeyCode = vk; - r->Event.KeyEvent.wVirtualScanCode = 0; // a key event written by a program carries none + r->Event.KeyEvent.wVirtualScanCode = scan; r->Event.KeyEvent.uChar.UnicodeChar = ch; r->Event.KeyEvent.dwControlKeyState = ctrl; } +// How Ctrl+J is spelled as a key event. A keyboard produces the virtual key +// 'J', its scan code, the control state and the character the layout gives it; +// which of those the console reads is what is being measured, so each spelling +// is tried and printed. +int g_spelling = 0; + // Writes the keys `keys' as presses and releases, then reads `want' bytes. int type_and_read(HANDLE in, kal_stream s, const char* keys, unsigned char* out, int want) { FlushConsoleInputBuffer(in); INPUT_RECORD rec[32]; int n = 0; for (const char* k = keys; *k; ++k) { - WORD vk; wchar_t ch; DWORD ctrl = 0; - if (*k == 'E') { vk = VK_RETURN; ch = L'\r'; } - else if (*k == 'J') { vk = 'J'; ch = L'\n'; ctrl = LEFT_CTRL_PRESSED; } + WORD vk; WORD scan = 0; wchar_t ch; DWORD ctrl = 0; + if (*k == 'E') { vk = VK_RETURN; scan = 0x1c; ch = L'\r'; } + else if (*k == 'J') { + vk = 'J'; ch = L'\n'; + switch (g_spelling) { + case 0: scan = 0x24; ctrl = LEFT_CTRL_PRESSED; break; // as a keyboard sends it + case 1: scan = 0; ctrl = LEFT_CTRL_PRESSED; break; + case 2: scan = 0x24; ctrl = 0; break; // the character alone + } + } else { vk = static_cast(*k - 'a' + 'A'); ch = static_cast(*k); } - key(&rec[n++], vk, ch, ctrl, TRUE); - key(&rec[n++], vk, ch, ctrl, FALSE); + key(&rec[n++], vk, scan, ch, ctrl, TRUE); + key(&rec[n++], vk, scan, ch, ctrl, FALSE); } DWORD written = 0; if (!WriteConsoleInputW(in, rec, static_cast(n), &written)) { @@ -116,18 +129,30 @@ int main() { check(kal_terminal_set_mode(s, 0) == kal_ok, "line input and echo are turned off"); check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, "with line input off the position reads set"); - int got = type_and_read(in, s, "EJ", b, 2); - show("line off, processed on: Enter Ctrl+J", b, got); - check(got == 2 && b[0] == 0x0d && b[1] == 0x0a, "the return key is 0x0d and Ctrl+J is 0x0a"); + int got = type_and_read(in, s, "E", b, 1); + show("line off, processed on: Enter", b, got); + check(got == 1 && b[0] == 0x0d, "the return key is 0x0d"); + const char* spelled[] = { "Ctrl+J, keyboard scan code", "Ctrl+J, no scan code", "line feed, no control" }; + int lf = 0; + for (g_spelling = 0; g_spelling < 3; ++g_spelling) { + got = type_and_read(in, s, "J", b, 1); + show(spelled[g_spelling], b, got); + if (got == 1 && b[0] == 0x0a) ++lf; + if (got == 1) check(b[0] != 0x0d, "Ctrl+J is not the byte the return key is"); + } + g_spelling = 0; + std::printf(" measured %d of 3 spellings of Ctrl+J delivered 0x0a\n", lf); // Raw, and nothing reserved: what a program that reads keystrokes asks for. check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL | KAL_TERM_PASS_INPUT) == kal_ok, "every keystroke, unaltered, is requested"); check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, "the position reads set when it is requested with line input off"); - got = type_and_read(in, s, "EJ", b, 2); - show("line off, processed off: Enter Ctrl+J", b, got); - check(got == 2 && b[0] == 0x0d && b[1] == 0x0a, "the same two bytes with nothing reserved"); + got = type_and_read(in, s, "E", b, 1); + show("line off, processed off: Enter", b, got); + check(got == 1 && b[0] == 0x0d, "the return key is 0x0d with nothing reserved"); + got = type_and_read(in, s, "J", b, 1); + show("line off, processed off: Ctrl+J", b, got); // A request this environment cannot separate from line input is left as it // stands, and the read-back says so. From 17581193a204cff4aaba223f88c6d5bd998f3708 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:27:13 +0900 Subject: [PATCH 08/10] The console test ends every measurement with a key that always produces a byte, so a key that produces none is measured rather than waited for --- tests/console_input.cpp | 61 ++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/tests/console_input.cpp b/tests/console_input.cpp index a68967a..b8b0f8b 100644 --- a/tests/console_input.cpp +++ b/tests/console_input.cpp @@ -55,12 +55,18 @@ void key(INPUT_RECORD* r, WORD vk, WORD scan, wchar_t ch, DWORD ctrl, BOOL down) // is tried and printed. int g_spelling = 0; -// Writes the keys `keys' as presses and releases, then reads `want' bytes. -int type_and_read(HANDLE in, kal_stream s, const char* keys, unsigned char* out, int want) { +// Writes the keys `keys' as presses and releases, then a `q', and reads until +// the `q' arrives. What came before it is what the keys produced --- a key the +// console turns into no byte produces nothing, and the `q' still ends the read, +// which is what lets nothing be measured rather than waited for. +int type_and_read(HANDLE in, kal_stream s, const char* keys, unsigned char* out, int cap) { FlushConsoleInputBuffer(in); - INPUT_RECORD rec[32]; + INPUT_RECORD rec[34]; int n = 0; - for (const char* k = keys; *k; ++k) { + char all[16]; int m = 0; + for (const char* k = keys; *k && m < 14; ++k) all[m++] = *k; + all[m++] = 'q'; all[m] = 0; + for (const char* k = all; *k; ++k) { WORD vk; WORD scan = 0; wchar_t ch; DWORD ctrl = 0; if (*k == 'E') { vk = VK_RETURN; scan = 0x1c; ch = L'\r'; } else if (*k == 'J') { @@ -71,7 +77,7 @@ int type_and_read(HANDLE in, kal_stream s, const char* keys, unsigned char* out, case 2: scan = 0x24; ctrl = 0; break; // the character alone } } - else { vk = static_cast(*k - 'a' + 'A'); ch = static_cast(*k); } + else { vk = static_cast(*k - 'a' + 'A'); scan = 0; ch = static_cast(*k); } key(&rec[n++], vk, scan, ch, ctrl, TRUE); key(&rec[n++], vk, scan, ch, ctrl, FALSE); } @@ -80,23 +86,18 @@ int type_and_read(HANDLE in, kal_stream s, const char* keys, unsigned char* out, std::printf(" WriteConsoleInputW failed: %lu\n", GetLastError()); return -1; } - DWORD queued = 0; - GetNumberOfConsoleInputEvents(in, &queued); - std::printf(" wrote %lu of %d events; %lu queued\n", written, n, queued); int got = 0; - while (got < want) { - // A read is made only once the console says there is input, so a key - // event the console does not turn into a byte is reported rather than - // waited for. - if (WaitForSingleObject(in, 3000) != WAIT_OBJECT_0) { - GetNumberOfConsoleInputEvents(in, &queued); - std::printf(" no input within 3 s; %lu events still queued\n", queued); - break; - } - const kal_intptr r = kal_stream_read(s, out + got, static_cast(want - got)); + unsigned char buf[64]; + int have = 0; + for (;;) { + const kal_intptr r = kal_stream_read(s, buf + have, sizeof buf - have); if (r <= 0) break; - got += static_cast(r); + have += static_cast(r); + bool end = false; + for (int i = 0; i < have; ++i) if (buf[i] == 'q') { end = true; have = i; break; } + if (end || have >= static_cast(sizeof buf)) break; } + for (int i = 0; i < have && got < cap; ++i) out[got++] = buf[i]; return got; } @@ -122,20 +123,20 @@ int main() { } CreateThread(nullptr, 0, watchdog, nullptr, 0, nullptr); const kal_stream s{ reinterpret_cast(in) }; - unsigned char b[8] = {}; + unsigned char b[16] = {}; kal_uintptr m = 0; // Raw, with the environment's reserved keys kept: the ordinary raw mode. check(kal_terminal_set_mode(s, 0) == kal_ok, "line input and echo are turned off"); check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, "with line input off the position reads set"); - int got = type_and_read(in, s, "E", b, 1); + int got = type_and_read(in, s, "E", b, sizeof b); show("line off, processed on: Enter", b, got); check(got == 1 && b[0] == 0x0d, "the return key is 0x0d"); const char* spelled[] = { "Ctrl+J, keyboard scan code", "Ctrl+J, no scan code", "line feed, no control" }; int lf = 0; for (g_spelling = 0; g_spelling < 3; ++g_spelling) { - got = type_and_read(in, s, "J", b, 1); + got = type_and_read(in, s, "J", b, sizeof b); show(spelled[g_spelling], b, got); if (got == 1 && b[0] == 0x0a) ++lf; if (got == 1) check(b[0] != 0x0d, "Ctrl+J is not the byte the return key is"); @@ -148,10 +149,10 @@ int main() { "every keystroke, unaltered, is requested"); check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, "the position reads set when it is requested with line input off"); - got = type_and_read(in, s, "E", b, 1); + got = type_and_read(in, s, "E", b, sizeof b); show("line off, processed off: Enter", b, got); check(got == 1 && b[0] == 0x0d, "the return key is 0x0d with nothing reserved"); - got = type_and_read(in, s, "J", b, 1); + got = type_and_read(in, s, "J", b, sizeof b); show("line off, processed off: Ctrl+J", b, got); // A request this environment cannot separate from line input is left as it @@ -164,7 +165,17 @@ int main() { check(kal_terminal_set_mode(s, KAL_TERM_LINE_EDIT) == kal_ok, "line input is turned on"); check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) == 0, "with line input on the position reads clear"); - got = type_and_read(in, s, "aE", b, 3); + // With lines assembled the `q' is part of the next line, so the line is + // read on its own: the read returns once the return key ends it. + FlushConsoleInputBuffer(in); + { + INPUT_RECORD rec[4]; + key(&rec[0], 'A', 0, L'a', 0, TRUE); key(&rec[1], 'A', 0, L'a', 0, FALSE); + key(&rec[2], VK_RETURN, 0x1c, L'\r', 0, TRUE); key(&rec[3], VK_RETURN, 0x1c, L'\r', 0, FALSE); + DWORD w = 0; + WriteConsoleInputW(in, rec, 4, &w); + got = static_cast(kal_stream_read(s, b, sizeof b)); + } show("line on: a Enter", b, got); check(got >= 2 && b[0] == 'a', "a line is delivered"); From 0542f5e0944a4f75e97d6c6bd87429564436716e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:31:02 +0900 Subject: [PATCH 09/10] diagnostic: the same keys with virtual terminal input on --- tests/console_input.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/console_input.cpp b/tests/console_input.cpp index b8b0f8b..89563cc 100644 --- a/tests/console_input.cpp +++ b/tests/console_input.cpp @@ -144,6 +144,25 @@ int main() { g_spelling = 0; std::printf(" measured %d of 3 spellings of Ctrl+J delivered 0x0a\n", lf); + // The same spellings with the console's virtual terminal input on, which + // is this environment's own switch for delivering what a key produces. + { + DWORD cm = 0; + GetConsoleMode(in, &cm); + const BOOL vt = SetConsoleMode(in, cm | 0x0200 /* ENABLE_VIRTUAL_TERMINAL_INPUT */); + std::printf(" measured virtual terminal input %s\n", vt ? "accepted" : "refused"); + if (vt) { + got = type_and_read(in, s, "E", b, sizeof b); + show("vt input: Enter", b, got); + for (g_spelling = 0; g_spelling < 3; ++g_spelling) { + got = type_and_read(in, s, "J", b, sizeof b); + show(spelled[g_spelling], b, got); + } + g_spelling = 0; + SetConsoleMode(in, cm); + } + } + // Raw, and nothing reserved: what a program that reads keystrokes asks for. check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL | KAL_TERM_PASS_INPUT) == kal_ok, "every keystroke, unaltered, is requested"); From 513999824672b2816e1ca180d8ce40e5a1cf8ec8 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Sun, 25 Oct 2026 06:38:12 +0900 Subject: [PATCH 10/10] KAL_TERM_PASS_INPUT is virtual terminal input with line input off, as measured With line input off and virtual terminal input off, the console delivered the return key as 0x0d and consumed Ctrl+J without a byte, which alters what was typed; with virtual terminal input on it delivered 0x0d and 0x0a, the bytes the other two implementations deliver with the position set. The position is read from both flags and established by the second; with line input on it reads clear and a request leaves it so. A console that refuses virtual terminal input is written again without it. --- src/terminal.cpp | 48 ++++++++++++++++------ tests/console_input.cpp | 91 +++++++++++++++++++---------------------- 2 files changed, 77 insertions(+), 62 deletions(-) diff --git a/src/terminal.cpp b/src/terminal.cpp index 89f4c8d..c2791d7 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -21,6 +21,7 @@ bool valid(void* h) { return h != nullptr && h != INVALID_HANDLE_VALUE; } constexpr DWORD enable_processed_input = 0x0001; constexpr DWORD enable_line_input = 0x0002; constexpr DWORD enable_echo_input = 0x0004; +constexpr DWORD enable_virtual_terminal_input = 0x0200; // KAL_TERM_PASS_CONTROL IS ENABLE_PROCESSED_INPUT, INVERTED. This environment // keeps the keystrokes it reserves --- the interrupt, and the pair that stops @@ -50,14 +51,19 @@ int kal_terminal_get_mode(kal_stream s, kal_uintptr* mode) { if ((m & enable_line_input) != 0) out |= KAL_TERM_LINE_EDIT; if ((m & enable_echo_input) != 0) out |= KAL_TERM_ECHO; if ((m & enable_processed_input) == 0) out |= KAL_TERM_PASS_CONTROL; - // KAL_TERM_PASS_INPUT IS LINE INPUT, INVERTED, AND IT IS NOT A FLAG OF ITS - // OWN. This environment alters what is typed only while it assembles lines - // --- the return key then ends a line as a carriage return and a line feed - // --- and with line input off it delivers the return key as a carriage - // return and Ctrl+J as a line feed, which is what the position states. + // KAL_TERM_PASS_INPUT IS VIRTUAL TERMINAL INPUT WITH LINE INPUT OFF. + // // Measured on a console this implementation's tests allocate, with the keys - // written into its input as key events. - if ((m & enable_line_input) == 0) out |= KAL_TERM_PASS_INPUT; + // written into its input as the key events a keyboard produces. With line + // input off and virtual terminal input off, the return key arrives as 0x0d + // and Ctrl+J arrives as NOTHING: the console consumes the key and delivers + // no byte, which is an alteration of what was typed. With virtual terminal + // input on, the return key is 0x0d and Ctrl+J is 0x0a --- the two bytes the + // other environments deliver with the position set. With line input on, + // the return key ends a line as 0x0d 0x0a and no switch changes that, so the + // position reads clear there. + if ((m & enable_line_input) == 0 && (m & enable_virtual_terminal_input) != 0) + out |= KAL_TERM_PASS_INPUT; *mode = out; return kal_ok; } @@ -73,6 +79,7 @@ int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) { // would find its console changed in ways it did not ask for. DWORD m = 0; if (!GetConsoleMode(h, &m)) return kal_err_not_supported; + const bool unaltered = (m & enable_line_input) == 0 && (m & enable_virtual_terminal_input) != 0; if ((mode & KAL_TERM_LINE_EDIT) != 0) m |= enable_line_input; else m &= ~enable_line_input; @@ -88,14 +95,29 @@ int kal_terminal_set_mode(kal_stream s, kal_uintptr mode) { // several mechanisms; this one stands for exactly one, and assigning it // from the mode word is the same act as leaving it alone. - // KAL_TERM_PASS_INPUT HAS NO SWITCH HERE, so a request for it is neither - // refused nor composed. It follows line input, and kal_terminal_get_mode - // reports it as it then stands, which is what terminal.h states of an - // environment that cannot separate the alteration from line assembly. + // KAL_TERM_PASS_INPUT is virtual terminal input, and only while line input + // is off: with lines assembled this environment alters input whatever the + // switch says, so a request for the position there leaves it as it stands, + // which is what terminal.h states of such an environment. A position whose + // requested value is the one in effect is not written. + bool asked_vt = false; + if (((mode & KAL_TERM_PASS_INPUT) != 0) != unaltered) { + if ((mode & KAL_TERM_PASS_INPUT) != 0) { + if ((mode & KAL_TERM_LINE_EDIT) == 0) { m |= enable_virtual_terminal_input; asked_vt = true; } + } else { + m &= ~enable_virtual_terminal_input; + } + } // A position this implementation does not distinguish is ignored rather than - // refused, which clause 6.2 requires of a word. - if (!SetConsoleMode(h, m)) return kal_err_not_supported; + // refused, which clause 6.2 requires of a word. A console that predates + // virtual terminal input refuses the whole word when it is asked for, so + // the word is written again without it: the other positions are + // established, and the read-back says the fourth was not. + if (!SetConsoleMode(h, m)) { + if (!asked_vt || !SetConsoleMode(h, m & ~enable_virtual_terminal_input)) + return kal_err_not_supported; + } return kal_ok; } diff --git a/tests/console_input.cpp b/tests/console_input.cpp index 89563cc..0926d3a 100644 --- a/tests/console_input.cpp +++ b/tests/console_input.cpp @@ -3,18 +3,21 @@ // // mcpplibs/openkal#48: above the two other implementations the return key // reached a program in raw mode as a line feed, the same byte Ctrl+J produces. -// The position that answers it is meant to make the bytes a program reads the -// same on all three when it is set, so this test writes the keys into a console -// of its own as key events --- the form a keyboard produces --- reads them back -// through kal_stream_read, prints what arrived under each mode, and checks: +// The position that answers it makes the bytes a program reads the same on all +// three when it is set. This test writes the keys into a console of its own as +// the key events a keyboard produces, reads them back through kal_stream_read, +// prints what arrived under each mode, and checks: // -// with line input off, the position reads set, and the return key is 0x0d -// and Ctrl+J is 0x0a, which is what the other two deliver with it set; -// with line input on, the position reads clear. +// with line input off and nothing else asked, the position reads clear, the +// return key is 0x0d, and what Ctrl+J produces is printed (measured: nothing); +// with the position set, the return key is 0x0d and Ctrl+J is 0x0a, which is +// what the other two deliver with it set; +// with line input on, the position reads clear whatever was asked. // // A console is allocated rather than inherited, because a runner's own may be -// absent or redirected. A watchdog ends the test if a read waits for a key that -// never arrives, so a wrong expectation is a failure rather than a hang. +// absent or redirected. Every measurement ends with a key that always produces +// a byte, so a key that produces none is measured rather than waited for; a +// watchdog ends the test if even that does not arrive. #include #include #include @@ -126,58 +129,48 @@ int main() { unsigned char b[16] = {}; kal_uintptr m = 0; - // Raw, with the environment's reserved keys kept: the ordinary raw mode. + // Raw, nothing else asked: what this console does with the keys by itself. check(kal_terminal_set_mode(s, 0) == kal_ok, "line input and echo are turned off"); - check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, - "with line input off the position reads set"); + check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) == 0, + "with virtual terminal input off the position reads clear"); int got = type_and_read(in, s, "E", b, sizeof b); - show("line off, processed on: Enter", b, got); + show("raw: Enter", b, got); check(got == 1 && b[0] == 0x0d, "the return key is 0x0d"); - const char* spelled[] = { "Ctrl+J, keyboard scan code", "Ctrl+J, no scan code", "line feed, no control" }; - int lf = 0; + const char* spelled[] = { "raw: Ctrl+J, keyboard scan code", "raw: Ctrl+J, no scan code", + "raw: line feed, no control" }; for (g_spelling = 0; g_spelling < 3; ++g_spelling) { got = type_and_read(in, s, "J", b, sizeof b); show(spelled[g_spelling], b, got); - if (got == 1 && b[0] == 0x0a) ++lf; - if (got == 1) check(b[0] != 0x0d, "Ctrl+J is not the byte the return key is"); } g_spelling = 0; - std::printf(" measured %d of 3 spellings of Ctrl+J delivered 0x0a\n", lf); - // The same spellings with the console's virtual terminal input on, which - // is this environment's own switch for delivering what a key produces. - { - DWORD cm = 0; - GetConsoleMode(in, &cm); - const BOOL vt = SetConsoleMode(in, cm | 0x0200 /* ENABLE_VIRTUAL_TERMINAL_INPUT */); - std::printf(" measured virtual terminal input %s\n", vt ? "accepted" : "refused"); - if (vt) { - got = type_and_read(in, s, "E", b, sizeof b); - show("vt input: Enter", b, got); - for (g_spelling = 0; g_spelling < 3; ++g_spelling) { - got = type_and_read(in, s, "J", b, sizeof b); - show(spelled[g_spelling], b, got); - } - g_spelling = 0; - SetConsoleMode(in, cm); - } - } + // Raw, and nothing altered: what a program that tells keys apart asks for. + // These are the bytes the other two environments deliver with the position + // set, which is what the position is for. + check(kal_terminal_set_mode(s, KAL_TERM_PASS_INPUT) == kal_ok, "unaltered input is requested"); + check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, + "the position reads set once established"); + got = type_and_read(in, s, "EJ", b, sizeof b); + show("unaltered: Enter Ctrl+J", b, got); + check(got == 2 && b[0] == 0x0d && b[1] == 0x0a, "the return key is 0x0d and Ctrl+J is 0x0a"); - // Raw, and nothing reserved: what a program that reads keystrokes asks for. check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL | KAL_TERM_PASS_INPUT) == kal_ok, "every keystroke, unaltered, is requested"); - check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, - "the position reads set when it is requested with line input off"); - got = type_and_read(in, s, "E", b, sizeof b); - show("line off, processed off: Enter", b, got); - check(got == 1 && b[0] == 0x0d, "the return key is 0x0d with nothing reserved"); - got = type_and_read(in, s, "J", b, sizeof b); - show("line off, processed off: Ctrl+J", b, got); - - // A request this environment cannot separate from line input is left as it - // stands, and the read-back says so. - check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL) == kal_ok, "altered input is requested in raw mode"); - check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) != 0, + got = type_and_read(in, s, "EJ", b, sizeof b); + show("unaltered, nothing reserved: Enter Ctrl+J", b, got); + check(got == 2 && b[0] == 0x0d && b[1] == 0x0a, "the same two bytes with nothing reserved"); + + // Cleared again, and written only because it changed. + check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL) == kal_ok, "altered input is requested again"); + check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) == 0, + "the position reads clear once cleared"); + + // Lines assembled, with the position asked for: this console alters input + // while it assembles lines whatever its switches say, so the request leaves + // the position clear and the read-back says so. + check(kal_terminal_set_mode(s, KAL_TERM_LINE_EDIT | KAL_TERM_PASS_INPUT) == kal_ok, + "unaltered input is requested with line input on"); + check(kal_terminal_get_mode(s, &m) == kal_ok && (m & KAL_TERM_PASS_INPUT) == 0, "the read-back reports what is in effect rather than what was asked"); // Lines assembled: the position reads clear, and what arrives is measured.