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/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; } } 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; diff --git a/src/terminal.cpp b/src/terminal.cpp index b68fb17..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,6 +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 VIRTUAL TERMINAL INPUT WITH LINE INPUT OFF. + // + // Measured on a console this implementation's tests allocate, with the keys + // 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; } @@ -65,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; @@ -80,9 +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 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 new file mode 100644 index 0000000..0926d3a --- /dev/null +++ b/tests/console_input.cpp @@ -0,0 +1,197 @@ +// 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 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 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. 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 +#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, 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 = 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 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[34]; + int n = 0; + 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') { + 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'); scan = 0; ch = static_cast(*k); } + 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)) { + std::printf(" WriteConsoleInputW failed: %lu\n", GetLastError()); + return -1; + } + int got = 0; + 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; + 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; +} + +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[16] = {}; + kal_uintptr m = 0; + + // 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 virtual terminal input off the position reads clear"); + int got = type_and_read(in, s, "E", b, sizeof b); + show("raw: Enter", b, got); + check(got == 1 && b[0] == 0x0d, "the return key is 0x0d"); + 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); + } + g_spelling = 0; + + // 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"); + + check(kal_terminal_set_mode(s, KAL_TERM_PASS_CONTROL | KAL_TERM_PASS_INPUT) == kal_ok, + "every keystroke, unaltered, is requested"); + 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. + 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"); + // 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"); + + 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; +}