Skip to content

core-cpp 0.2.1

Choose a tag to compare

@github-actions github-actions released this 24 Sep 16:01
· 140 commits to master since this release

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() beside buildSgrSequence(),
    TerminalOutput::resetStyle(), and protocols::SgrReset. They are the bytes writeText already
    ended styled text with, which were private, so Lightweight's dbtool spelled "\033[0m" itself.
  • core::platform::SignalHandler::nativeHandle(): the signal fd as the NativeHandle
    TuiRuntimeOptions::signalFd takes -- the signalfd on Linux while initialized, InvalidHandle
    everywhere else -- so a caller no longer converts initialize()'s int, which on Windows is the
    wrong type for a handle. initialize() keeps its int for 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 of core::tui_output's own. With CORE_CPP_WITH_TUI
    off and this on, core-cpp builds the styled-output leaf by itself and neither finds nor fetches
    libunicode. It defaults to CORE_CPP_WITH_TUI on a first configure, is forced on by it
    (core::tui links 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-output and a consumer-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, so PosixSocket::close() -- and cancelRead(), IocpSocket's,
    WindowsSocket::cancelRead(), and CompletionWait::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 executing close()'s
    caller: contour crashed on it deterministically, in NativeClient::detach (_writer.close(); _connection->close();, where the first close resumed runClient, 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 to EventLoop::resumeSoon; an awaiter whose frame is destroyed while its
    waiter is queued takes it back with cancelPending. The listeners already resumed a closed
    accept through the loop's closed-park list, and TLS's SerialGate its waiters since Task B11.
    CloseResumesThroughLoop_test.cpp holds it over BackendMatrix, 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, a blockOn). 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::InMemorySocket and
      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, a Cancelled
      one or bytes a read already took. A flow must not touch a socket it does not own after its
      operation resumes (ISocket::close says 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 with OperationCancelled where they
      used to write into freed storage -- WFMO's WindowsSocket (its _readWaiter), and the TLS
      layer, whose feedIn wrote 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, UnixListener and WFMO's WindowsListener, a
      defect older than this release. The accept now asks the listener's lifetime token first and
      answers Cancelled ("the listener was destroyed"). IocpListener keeps what an accept reads
      in state it shares, and was not affected.
    • Teardown: ~EventLoop drains 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 (a DetachedTask) 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 INSIDE close(), so code that closes two sockets in a
      row through an object the read flow owns -- contour's NativeClient::detach -- was exposed to
      it. 0.1.0 is released, so the note is recorded here rather than there.
  • On Windows, listenUnix and connectUnix belong to the loop's transport (found through
    contour). listenUnix built the WFMO WindowsListener whatever the loop was, and connectUnix a
    WindowsSocket, while listen and adoptListener branch 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, whose AcceptEx accepts AF_UNIX connections (measured on Windows 11,
    asserted in CI) and hands out IocpSocket, and connectUnix adopts 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: with SIGHUP ignored, 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 than EAGAIN, an end of file on a pipe or a file, an end of file on a terminal
    that poll(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() and nextActivity() throw core::async::OperationCancelled
    without parking once TuiRuntime::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 (tuidu c20bcac) 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's Prompt::read is the example -- it catches
      OperationCancelled and returns an empty line, and its REPL reads again for as long as the
      prompt is ready. tuidu's runModal (tui/runtime/Modal.hpp) is the other: it returns
      std::nullopt on the cancellation, so a caller that shows the modal again on nullopt spins
      the same way.
    • TerminalInput::poll() records the end in inputClosed() too (a hangup with nothing to read on
      POSIX, a failed wait on Windows), but a loop driven by poll() itself must ask it; nothing ends
      that loop for it.
  • Piped output no longer carries synchronized-output sequences -- restores SyncGuard's
    purpose, bracketing a frame for the TERMINAL that renders it. TerminalOutput::syncGuard(), and
    a SyncGuard constructed directly, wrote CSI ? 2026 h / l whatever the destination was, so
    every caller had to test isTerminal() 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's isTerminal() 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.
  • tools/migrate/rewrite.py rewrites 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.json gains the
    crispy::Overloaded -> core::Overloaded row the migration guide already listed (contour).
  • A vendored copy configures with CORE_CPP_TESTING on (found by the contour migration). The
    top-level CMakeLists.txt added tests/ 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.md says the same, and the consumer-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 declare CompletionItem, CompletionProvider, Completer,
    CompletionConfig, FuzzyMatch, FuzzyMatchResult, FuzzyConfig, SmartCaseMatch and
    SmartCaseConfig in namespace tui; tools/migrate/renames.json had no row for them, so the
    tui namespace row rewrote them to core::tui::, which does not compile. Each has a symbol row
    now, and check_renames_test.py fails without them. The provenance record of
    src/core/testing/SuppressWindowsDialogsAtStartup.cpp no 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_output alone no longer fetches libunicode (found by Lightweight's
    dbtool). The leaf was gated on CORE_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.zip from www.unicode.org -- for a target that links core::base alone.
    CORE_CPP_WITH_TUI=OFF with CORE_CPP_WITH_TUI_OUTPUT=ON is the configuration dbtool wants.