core-cpp 0.2.1
Every behaviour change in this section is a defect fixed, and each entry says which guarantee it
restores and what a caller that depended on the defect changes. None of them is a break of a
documented promise, which is why they are under Fixed in a patch release.
Added
- A public SGR reset in
core::tui_output:buildSgrReset()besidebuildSgrSequence(),
TerminalOutput::resetStyle(), andprotocols::SgrReset. They are the byteswriteTextalready
ended styled text with, which were private, so Lightweight's dbtool spelled"\033[0m"itself. core::platform::SignalHandler::nativeHandle(): the signal fd as theNativeHandle
TuiRuntimeOptions::signalFdtakes -- the signalfd on Linux while initialized,InvalidHandle
everywhere else -- so a caller no longer convertsinitialize()'sint, which on Windows is the
wrong type for a handle.initialize()keeps itsintfor compatibility (found by the endo
migration).- The end of a terminal's input is reported.
TerminalInput::inputClosed(), the virtual
runtime::InputSource::inputClosed()(false by default, so an existing source still compiles),
TerminalInputSource's forward of it,TuiRuntime::inputClosed(), and
runtime::testing::ScriptedInputSource::closeInput()to script it. See Fixed. CORE_CPP_WITH_TUI_OUTPUT, an option ofcore::tui_output's own. WithCORE_CPP_WITH_TUI
off and this on, core-cpp builds the styled-output leaf by itself and neither finds nor fetches
libunicode. It defaults toCORE_CPP_WITH_TUIon a first configure, is forced on by it
(core::tuilinks the leaf), and is forced off under Emscripten. A module's own row switching
off no longer takes a target with a row of its own down with it:core_cpp_add_modules()enters
the directory for that target alone, as the module table always said a row of its own would.
tests/consumer-tui-outputand aconsumer-smoke (tui-output)CI leg assert the configuration.
Fixed
- A parked flow is never resumed inside the call that settled it -- restores guarantee G2,
every resumption happens in the loop's drain step (.agent/rules/async-and-net.md, "a resource
never resumes its consumer inline").ResultAwaitable::complete()resumed the waiter on the
spot, soPosixSocket::close()-- andcancelRead(),IocpSocket's,
WindowsSocket::cancelRead(), andCompletionWait::close()under an IOCP listener -- ran the
closed read's flow before returning. That flow could run to its end and destroy the object still executingclose()'s
caller: contour crashed on it deterministically, inNativeClient::detach(_writer.close(); _connection->close();, where the first close resumedrunClient, which destroyed the client).
Each now settles the operation at once, with the same value (Cancelled, or the data that won),
and hands the waiter toEventLoop::resumeSoon; an awaiter whose frame is destroyed while its
waiter is queued takes it back withcancelPending. The listeners already resumed a closed
accept through the loop's closed-park list, and TLS'sSerialGateits waiters since Task B11.
CloseResumesThroughLoop_test.cppholds it overBackendMatrix, contour's crash included.- Migration: a caller that asserted a parked flow's outcome right after
close()or
cancelRead()runs one loop turn first (runOnce,runUntilIdle, ablockOn). Two
cancelRead()calls in a row no longer retire the read the first victim arms when it runs: the
second finds the slot empty (fastcached#1233's shape).testing::InMemorySocketand
testing::ParkingReadableSocket, which have no loop, still resume inline. - The socket may be gone when the flow runs, whatever the result: the waiter resumes later
in the drain, so an owner that destroys the socket first --conn->close(); connections.erase(id);-- has destroyed it before the flow sees its result, aCancelled
one or bytes a read already took. A flow must not touch a socket it does not own after its
operation resumes (ISocket::closesays so). The transports touch nothing of it: the
frame-free ones settled a value that does not refer to the socket, and the coroutine-shaped
ones ask a lifetime token on every way back and unwind withOperationCancelledwhere they
used to write into freed storage -- WFMO'sWindowsSocket(its_readWaiter), and the TLS
layer, whosefeedInwrote the ciphertext of a read that settled with data into the freed
session's BIO, on every backend. Both were a heap-use-after-free under AddressSanitizer. - A listener closed and destroyed in one turn --
listener->close(); listener.reset();--
woke its parked accept through the loop, and the accept then read the freed listener's
closed flag and descriptor:PosixListener,UnixListenerand WFMO'sWindowsListener, a
defect older than this release. The accept now asks the listener's lifetime token first and
answersCancelled("the listener was destroyed").IocpListenerkeeps what an accept reads
in state it shares, and was not affected. - Teardown:
~EventLoopdrains what destroying the spawned roots queued -- a borrowed flow
whose socket or listener a root owned -- so no flow is left suspended with an operation naming
a destroyed loop; and a chain nobody owns (aDetachedTask) that a socket queued is freed at
teardown as the loop's own, where it used to be resumed and run on. - For contour, missing from 0.1.0's per-consumer summary: from 0.1.0 until this release,
core-cpp's sockets resumed a parked read INSIDEclose(), so code that closes two sockets in a
row through an object the read flow owns -- contour'sNativeClient::detach-- was exposed to
it. 0.1.0 is released, so the note is recorded here rather than there.
- Migration: a caller that asserted a parked flow's outcome right after
- On Windows,
listenUnixandconnectUnixbelong to the loop's transport (found through
contour).listenUnixbuilt the WFMOWindowsListenerwhatever the loop was, andconnectUnixa
WindowsSocket, whilelistenandadoptListenerbranch to IOCP -- so an IOCP loop, the Windows
default, served AF_UNIX through readiness and handed out readiness sockets. It now gets the new
IocpListener::bindUnix, whoseAcceptExaccepts AF_UNIX connections (measured on Windows 11,
asserted in CI) and hands outIocpSocket, andconnectUnixadopts its socket onto the loop's
transport. A WFMO loop is unchanged. The socket-path claim both listeners make moved into a shared
windows/UnixSocketPath.cpp. - A hung-up terminal no longer spins the TUI at 100% CPU
(core-cpp#49, found by the tuidu
migration) -- restores the input wait's promise that it waits: withSIGHUPignored, a terminal
that hangs up leaves its input readable for ever, each read answering EIO or an end of file; the
runtime's input flow read nothing, re-parked, and was resumed at once, every turn, and the
process never exited.TerminalInput::readReadyInput()now tells the end from "nothing yet" -- a
read error other thanEAGAIN, an end of file on a pipe or a file, an end of file on a terminal
thatpoll(2)reports hung up, a Windows console input handle that can no longer be read -- and
the runtime then stops watching the handle, delivers what was already read, and ends its input:
nextEvent(),nextEventFor()andnextActivity()throwcore::async::OperationCancelled
without parking onceTuiRuntime::inputClosed()is true. The same applies when the loop refuses
the input handle (FdRegistrationFailed), where the input flow used to return and leave a
nextEvent()waiting for ever. tuidu fixed the POSIX half in its own copy (tuiduc20bcac) and
it never reached endo, so core-cpp did not have it.- Migration: a consumer that treats a cancelled or empty read as "try again" must treat the
end of input as final, or the spin moves from the runtime into its own loop: it asks
inputClosed()and exits. endo'sPrompt::readis the example -- it catches
OperationCancelledand returns an empty line, and its REPL reads again for as long as the
prompt is ready. tuidu'srunModal(tui/runtime/Modal.hpp) is the other: it returns
std::nullopton the cancellation, so a caller that shows the modal again onnulloptspins
the same way. TerminalInput::poll()records the end ininputClosed()too (a hangup with nothing to read on
POSIX, a failed wait on Windows), but a loop driven bypoll()itself must ask it; nothing ends
that loop for it.
- Migration: a consumer that treats a cancelled or empty read as "try again" must treat the
- Piped output no longer carries synchronized-output sequences -- restores
SyncGuard's
purpose, bracketing a frame for the TERMINAL that renders it.TerminalOutput::syncGuard(), and
aSyncGuardconstructed directly, wroteCSI ? 2026 h/lwhatever the destination was, so
every caller had to testisTerminal()and choose between a guard and none, and one that did not
wrote escape sequences into a pipe or a file. The guard now asks the output'sisTerminal()once
and writes the sequences only when it answers true; it still flushes at both ends.- Migration: a capture that wants the sequences answers
isTerminal()true from its subclass,
as a terminal-emulating capture already should.
- Migration: a capture that wants the sequences answers
tools/migrate/rewrite.pyrewrites the code after a character literal holding a"(found by
the contour migration). Its scanner read'"'as opening a string, so in
os << '"' << crispy::escape(s) << '"'the symbol was masked as data and left unrewritten. A
character literal is now one character or one escape sequence, which also keeps a digit
separator's quotes (1'000'000) from being read as one.renames.jsongains the
crispy::Overloaded->core::Overloadedrow the migration guide already listed (contour).- A vendored copy configures with
CORE_CPP_TESTINGon (found by the contour migration). The
top-levelCMakeLists.txtaddedtests/unconditionally, and the vendoring file set leaves
tests/out, so the module suites the copy carries (src/core/**/*_test.cpp) could never be
switched on.tests/is now added when it exists; a copy without it registers the module suites
and says so.docs/vendoring.mdsays the same, and theconsumer-smoke (vendored)CI leg builds
and runs the exported copy's module suites. - The migration table sends the completion types to
core::tui::completer::
(core-cpp#48, found by the endo
migration). endo and tuidu declareCompletionItem,CompletionProvider,Completer,
CompletionConfig,FuzzyMatch,FuzzyMatchResult,FuzzyConfig,SmartCaseMatchand
SmartCaseConfiginnamespace tui;tools/migrate/renames.jsonhad no row for them, so the
tuinamespace row rewrote them tocore::tui::, which does not compile. Each has a symbol row
now, andcheck_renames_test.pyfails without them. The provenance record of
src/core/testing/SuppressWindowsDialogsAtStartup.cppno longer calls it a verbatim copy of
endo's: endo's product variant, which suppressed only under ctest, was dropped. - A consumer of
core::tui_outputalone no longer fetches libunicode (found by Lightweight's
dbtool). The leaf was gated onCORE_CPP_WITH_TUI, the same option as the libunicode row, so
the only configuration that built it also fetched libunicode and, through libunicode's own
configure,UCD.zipfromwww.unicode.org-- for a target that linkscore::basealone.
CORE_CPP_WITH_TUI=OFFwithCORE_CPP_WITH_TUI_OUTPUT=ONis the configurationdbtoolwants.