From e0b28ef40eb6b7dd5c1c740643d9a43f27ef6e75 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 26 Oct 2026 19:47:29 +0900 Subject: [PATCH 01/17] 0.1.0 --- mcpplibs.os over openkal 0.17 Modules: platform, error, env, fs, process, interrupt, stdio, terminal, dirs, ipc, net, machine, and the platform modules linux, macos, windows, posix. Interface units import std and no system header; the openkal implementation units import openkal only; the Windows units declare what they call. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 115 ++++++++++ README.md | 76 ++++++- checks/discarded/mcpp.toml | 6 + checks/discarded/src/main.cpp | 17 ++ checks/misuse/mcpp.toml | 6 + checks/misuse/src/main.cpp | 13 ++ mcpp.toml | 44 ++++ src/dirs.cpp | 73 ++++++ src/dirs.cppm | 30 +++ src/env.cpp | 130 +++++++++++ src/env.cppm | 42 ++++ src/error.cpp | 39 ++++ src/error.cppm | 36 +++ src/fs.cpp | 366 ++++++++++++++++++++++++++++++ src/fs.cppm | 125 +++++++++++ src/interrupt.cpp | 54 +++++ src/interrupt.cppm | 28 +++ src/ipc.cpp | 187 ++++++++++++++++ src/ipc.cppm | 67 ++++++ src/linux.cppm | 15 ++ src/linux/linux.cpp | 30 +++ src/linux/machine.cpp | 25 +++ src/machine.cppm | 21 ++ src/macos.cppm | 13 ++ src/macos/machine.cpp | 34 +++ src/macos/macos.cpp | 16 ++ src/net.cpp | 62 ++++++ src/net.cppm | 34 +++ src/os.cppm | 24 ++ src/platform.cppm | 59 +++++ src/posix.cppm | 11 + src/posix/posix.cpp | 12 + src/process.cpp | 403 ++++++++++++++++++++++++++++++++++ src/process.cppm | 123 +++++++++++ src/stdio.cpp | 53 +++++ src/stdio.cppm | 24 ++ src/terminal.cpp | 81 +++++++ src/terminal.cppm | 43 ++++ src/windows.cppm | 16 ++ src/windows/machine.cpp | 28 +++ src/windows/win32.h | 31 +++ src/windows/windows.cpp | 40 ++++ tests/check.hpp | 8 + tests/dirs_terminal_test.cpp | 22 ++ tests/fs_test.cpp | 60 +++++ tests/ipc_test.cpp | 48 ++++ tests/platform_test.cpp | 27 +++ tests/process_test.cpp | 117 ++++++++++ 48 files changed, 2933 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 checks/discarded/mcpp.toml create mode 100644 checks/discarded/src/main.cpp create mode 100644 checks/misuse/mcpp.toml create mode 100644 checks/misuse/src/main.cpp create mode 100644 mcpp.toml create mode 100644 src/dirs.cpp create mode 100644 src/dirs.cppm create mode 100644 src/env.cpp create mode 100644 src/env.cppm create mode 100644 src/error.cpp create mode 100644 src/error.cppm create mode 100644 src/fs.cpp create mode 100644 src/fs.cppm create mode 100644 src/interrupt.cpp create mode 100644 src/interrupt.cppm create mode 100644 src/ipc.cpp create mode 100644 src/ipc.cppm create mode 100644 src/linux.cppm create mode 100644 src/linux/linux.cpp create mode 100644 src/linux/machine.cpp create mode 100644 src/machine.cppm create mode 100644 src/macos.cppm create mode 100644 src/macos/machine.cpp create mode 100644 src/macos/macos.cpp create mode 100644 src/net.cpp create mode 100644 src/net.cppm create mode 100644 src/os.cppm create mode 100644 src/platform.cppm create mode 100644 src/posix.cppm create mode 100644 src/posix/posix.cpp create mode 100644 src/process.cpp create mode 100644 src/process.cppm create mode 100644 src/stdio.cpp create mode 100644 src/stdio.cppm create mode 100644 src/terminal.cpp create mode 100644 src/terminal.cppm create mode 100644 src/windows.cppm create mode 100644 src/windows/machine.cpp create mode 100644 src/windows/win32.h create mode 100644 src/windows/windows.cpp create mode 100644 tests/check.hpp create mode 100644 tests/dirs_terminal_test.cpp create mode 100644 tests/fs_test.cpp create mode 100644 tests/ipc_test.cpp create mode 100644 tests/platform_test.cpp create mode 100644 tests/process_test.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..aa3a43a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,115 @@ +name: CI + +# What this workflow asserts about mcpplibs.os. +# +# tests every module behaves as stated, on every system and with every +# compiler family a consumer builds it with (the native rows) +# platform a platform module used outside `if constexpr' for its platform +# fails to LINK, and the same use in a discarded branch links (R5) +# +# The library is built with the mcpp under cross-validation: until mcpp +# 2026.10.26.2 is released, a unit with a module map that imports another +# package's module through a third (`mcpplibs.os.terminal' through +# `mcpplibs.os.error') does not compile under GCC (mcpp part 2 design §10.2), +# so the mcpp named by MCPP_SOURCE_REF is built and used. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + inputs: + mcpp_ref: + description: "Branch of mcpp-community/mcpp to build and test with (empty = the released mcpp)" + required: false + default: "" + +env: + MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF || 'release/2026.10.26.2' }} + XLINGS_VERSION: v2026.10.26.1 + XLINGS_NON_INTERACTIVE: '1' + +jobs: + native: + name: ${{ matrix.os }} ${{ matrix.toolchain }} ${{ matrix.profile }} + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, toolchain: 'gcc@16.1.0', profile: dev } + - { os: ubuntu-24.04, toolchain: 'gcc@16.1.0', profile: release } + - { os: ubuntu-24.04, toolchain: 'llvm@23.1.3', profile: dev } + - { os: ubuntu-24.04-arm, toolchain: 'gcc@16.1.0', profile: dev } + - { os: macos-15, toolchain: 'llvm@23.1.3', profile: dev } + - { os: windows-2022, toolchain: 'llvm@23.1.3', profile: dev } + - { os: windows-2022, toolchain: 'msvc@system', profile: dev } + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + + - name: Install xlings and mcpp (Unix) + if: runner.os != 'Windows' + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ + | bash -s "$XLINGS_VERSION" + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + + - name: Install xlings and mcpp (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + irm https://d2learn.org/xlings-install.ps1.txt | iex + "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: The mcpp under cross-validation + run: | + for attempt in 1 2 3 4 5 6; do + xlings update > /dev/null 2>&1 || true + if xlings install mcpp -y -g; then break; fi + [ "$attempt" = 6 ] && { echo "::error::mcpp could not be installed"; exit 1; } + sleep 60 + done + mcpp --version + mcpp self config --mirror GLOBAL + if [ -n "${MCPP_SOURCE_REF:-}" ]; then + src="$RUNNER_TEMP/mcpp-src" + git clone --quiet --depth 1 --branch "$MCPP_SOURCE_REF" https://github.com/mcpp-community/mcpp.git "$src" + rm -f "$src/.xlings.json" + ( cd "$src" && mcpp build --release ) + built=$(find "$src/target" -type f \( -name mcpp -o -name mcpp.exe \) | head -1) + [ -n "$built" ] || { echo "::error::mcpp did not build from $MCPP_SOURCE_REF"; exit 1; } + echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" + echo "under review: $("$built" --version) (from $MCPP_SOURCE_REF)" + fi + + - name: Select the toolchain + run: | + spec='${{ matrix.toolchain }}' + case "$spec" in + msvc*) mcpp toolchain default msvc ;; + *) mcpp toolchain install "${spec%@*}" "${spec#*@}" + mcpp toolchain default "$spec" ;; + esac + + - name: Tests + run: | + profile='${{ matrix.profile }}' + mcpp test --profile "$profile" 2>&1 | tee tests.log + for name in platform_test fs_test process_test ipc_test dirs_terminal_test; do + grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not run or did not pass"; exit 1; } + done + + - name: A platform module is a link error outside its platform, and links in a discarded branch (R5) + run: | + ( cd checks/discarded && mcpp build ) || { echo "::error::a discarded use of a platform module did not link"; exit 1; } + if ( cd checks/misuse && mcpp build ) > misuse.log 2>&1; then + cat misuse.log + echo "::error::a platform module used outside its platform linked" + exit 1 + fi + grep -Eiq "undefined|unresolved" misuse.log || { cat misuse.log; echo "::error::the misuse failed for another reason than an undefined name"; exit 1; } + echo " ok the misuse fails at the link, naming what is undefined" diff --git a/README.md b/README.md index aa9f849..53a7bf6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,77 @@ # mcpplibs.os -A C++ operating-system library over openkal. The first version is under review. +A C++ operating-system library over [openkal](https://github.com/mcpplibs/openkal): +processes, files, the environment, terminals, conventional directories and local IPC, +with compile-time platform constants and a few platform modules. + +```toml +[dependencies] +os = { namespace = "mcpplibs", version = "0.1.0" } +``` + +```cpp +import std; +import mcpplibs.os; +namespace os = mcpplibs::os; + +int main() { + auto r = os::process::run({ .program = "git", .args = { "--version" } }); + if (r) std::print("{}", r->out); + if constexpr (os::is_windows) { /* compiled only for Windows */ } +} +``` + +## Layers + +| Layer | What it is | Where it is | +|---|---|---| +| openkal | the kernel ABI: minimal composable atoms, one implementation per system | `mcpplibs/openkal`, `openkal-` | +| **mcpplibs.os** | composition and conventions: PATH search, atomic and non-replacing publication, process trees, directory conventions, colour policy, local IPC | this package | +| a program | its domain | — | + +The kernel layer is openkal and only openkal. Native code in this package serves only +what is not a kernel question: the machine beneath the system (`machine`), and the platform +modules. + +## Modules + +| Module | Contents | +|---|---| +| `mcpplibs.os.platform` | `PLATFORM`, `is_linux` / `is_macos` / `is_windows` / `is_posix`, `exe_suffix`, `path_list_separator`; `host()`, a constant expression on a native target | +| `mcpplibs.os.error` | `errc` (openkal's closed set of conditions), `error`, `result` | +| `mcpplibs.os.env` | variables, arguments, the program's own image, PATH search with PATHEXT | +| `mcpplibs.os.fs` | `write_atomic`, `publish` (no replace), `caps`, `lock`, links, `set_executable`, `stamp_of`, `identity_of`, `locate` | +| `mcpplibs.os.process` | `spawn`, `run`, pipes, units, bound lifetime, bounded waits; `self`, `describe`, `alive` | +| `mcpplibs.os.interrupt` | `requested`, `wait`, `end_as_requested` | +| `mcpplibs.os.stdio` | the three streams as bytes | +| `mcpplibs.os.terminal` | `interactive`, `size`, `sequences`, `color`, `raw_mode` | +| `mcpplibs.os.dirs` | `home`, `config`, `data`, `state`, `cache`, `runtime`, `temp` | +| `mcpplibs.os.ipc` | a listener and a connection at a name in a directory; `single_instance` | +| `mcpplibs.os.net` | loopback TCP | +| `mcpplibs.os.machine` | the machine's architecture (through Rosetta and WoW64), memory | +| `mcpplibs.os.linux` / `.macos` / `.windows` / `.posix` | platform modules: declared on every target, defined only on their own | + +## Rules + +- **R0.** What std does, this package does not repeat. +- **R1.** A portable facility exists once, in `os::`. +- **R2.** A platform module holds only what has no portable meaning. Each of its declarations + says why. +- **R3.** Compile-time platform constants are first-class. `host()` names the running system: + - on a native target it equals `PLATFORM` and is a constant expression; + - in an image (`-openkal`) it is an ordinary function, and using it in a constant + expression is a compile error. +- **R4.** A native handle does not appear in a portable type. +- **R5.** A platform module is declared on every target and defined only on its own. A call + outside `if constexpr` for that platform is a link error. The same call in a discarded branch + is not an odr-use, so it links. CI asserts both. +- **R6.** Where openkal is not in its shared form, this package's resources do not cross a + shared-object boundary. + +Interface units import `std` and contain no system header and no macro. Implementation units +on openkal import only `openkal.*`. The Windows units declare the functions they call +themselves (`src/windows/win32.h`). + +## License + +Apache-2.0 diff --git a/checks/discarded/mcpp.toml b/checks/discarded/mcpp.toml new file mode 100644 index 0000000..11c893f --- /dev/null +++ b/checks/discarded/mcpp.toml @@ -0,0 +1,6 @@ +[package] +name = "os-check-discarded" +version = "0.1.0" + +[dependencies] +os = { path = "../.." } diff --git a/checks/discarded/src/main.cpp b/checks/discarded/src/main.cpp new file mode 100644 index 0000000..e2fcb12 --- /dev/null +++ b/checks/discarded/src/main.cpp @@ -0,0 +1,17 @@ +// R5: the same calls in branches `if constexpr' discards are not odr-uses +// ([basic.def.odr]), so this program links on every system. +import std; +import mcpplibs.os; +import mcpplibs.os.linux; +import mcpplibs.os.macos; +import mcpplibs.os.windows; + +namespace os = mcpplibs::os; + +int main() { + int n = 0; + if constexpr (os::is_linux) n += static_cast(os::linux::translator().has_value()); + if constexpr (os::is_macos) n += static_cast(os::macos::translated()); + if constexpr (os::is_windows) n += static_cast(os::windows::elevated()); + return n >= 0 ? 0 : 1; +} diff --git a/checks/misuse/mcpp.toml b/checks/misuse/mcpp.toml new file mode 100644 index 0000000..6884549 --- /dev/null +++ b/checks/misuse/mcpp.toml @@ -0,0 +1,6 @@ +[package] +name = "os-check-misuse" +version = "0.1.0" + +[dependencies] +os = { path = "../.." } diff --git a/checks/misuse/src/main.cpp b/checks/misuse/src/main.cpp new file mode 100644 index 0000000..fbd96e9 --- /dev/null +++ b/checks/misuse/src/main.cpp @@ -0,0 +1,13 @@ +// R5: a platform module used where it is not defined is a LINK error. This +// program calls a function of every platform module unconditionally; on each +// system the others are undefined, and the link must fail naming them. +import std; +import mcpplibs.os.linux; +import mcpplibs.os.macos; +import mcpplibs.os.windows; + +int main() { + return static_cast(mcpplibs::os::linux::translator().has_value()) + + static_cast(mcpplibs::os::macos::translated()) + + static_cast(mcpplibs::os::windows::elevated()); +} diff --git a/mcpp.toml b/mcpp.toml new file mode 100644 index 0000000..56148d2 --- /dev/null +++ b/mcpp.toml @@ -0,0 +1,44 @@ +[package] +namespace = "mcpplibs" +name = "os" +version = "0.1.0" +description = "mcpplibs.os: a C++ operating-system library over openkal --- processes, files, environment, terminals, directories and local IPC, with compile-time platform constants and a few platform modules." +license = "Apache-2.0" +authors = ["mcpplibs"] +repo = "https://github.com/mcpplibs/os" + +# The kernel layer is openkal and only openkal (design 2026-10-26 part 2, D3): +# one implementation per target, in the arrangement that borrows the program's +# own C and C++ runtime. A program that wants the standalone arrangement, or +# the image arrangement of `-openkal`, selects it in its own manifest. +[dependencies] +openkal = { git = "https://github.com/mcpplibs/openkal.git", branch = "openkal-0.17.0" } + +[target.linux.dependencies] +openkal-linux = { git = "https://github.com/mcpplibs/openkal-linux.git", branch = "openkal-0.17.0" } + +[target.macos.dependencies] +openkal-macos = { git = "https://github.com/mcpplibs/openkal-macos.git", branch = "openkal-0.17.0" } + +[target.windows.dependencies] +openkal-windows = { git = "https://github.com/mcpplibs/openkal-windows.git", branch = "openkal-0.17.0" } + +[targets.os] +kind = "lib" + +# The interface units and the implementations built on openkal are the same on +# every target. What differs is the compile-time constant of the target, the +# services an environment offers outside the kernel (`machine`), and the +# platform modules, which are declared everywhere and defined only on their own +# platform (R5): a misuse is a link error, never a branch at run time. +[build] +sources = ["src/*.cppm", "src/*.cpp"] + +[target.linux.build] +sources = ["src/target/linux.cppm", "src/linux/*.cpp", "src/posix/*.cpp"] + +[target.macos.build] +sources = ["src/target/macos.cppm", "src/macos/*.cpp", "src/posix/*.cpp"] + +[target.windows.build] +sources = ["src/target/windows.cppm", "src/windows/*.cpp"] diff --git a/src/dirs.cpp b/src/dirs.cpp new file mode 100644 index 0000000..330a597 --- /dev/null +++ b/src/dirs.cpp @@ -0,0 +1,73 @@ +module mcpplibs.os.dirs; + +import std; +import mcpplibs.os.platform; +import mcpplibs.os.env; + +namespace mcpplibs::os::dirs { + +namespace { +std::optional var(std::string_view name) { + auto v = env::get(name); + if (!v || v->empty()) return std::nullopt; + return std::filesystem::path(*v); +} +std::optional under_home(std::string_view rest) { + auto h = home(); + if (!h) return std::nullopt; + return *h / std::filesystem::path(rest); +} +bool windows() { return host() == platform::windows; } +bool macos() { return host() == platform::macos; } +} // namespace + +std::optional home() { + return windows() ? var("USERPROFILE") : var("HOME"); +} + +std::optional config() { + if (windows()) return var("APPDATA"); + if (macos()) return under_home("Library/Application Support"); + if (auto x = var("XDG_CONFIG_HOME")) return x; + return under_home(".config"); +} + +std::optional data() { + if (windows()) return var("LOCALAPPDATA"); + if (macos()) return under_home("Library/Application Support"); + if (auto x = var("XDG_DATA_HOME")) return x; + return under_home(".local/share"); +} + +std::optional state() { + if (windows()) return var("LOCALAPPDATA"); + if (macos()) return under_home("Library/Application Support"); + if (auto x = var("XDG_STATE_HOME")) return x; + return under_home(".local/state"); +} + +std::optional cache() { + if (windows()) return var("LOCALAPPDATA"); + if (macos()) return under_home("Library/Caches"); + if (auto x = var("XDG_CACHE_HOME")) return x; + return under_home(".cache"); +} + +std::optional runtime() { + if (!windows() && !macos()) + if (auto x = var("XDG_RUNTIME_DIR")) return x; + return temp(); +} + +std::filesystem::path temp() { + if (windows()) { + if (auto x = var("TEMP")) return *x; + if (auto x = var("TMP")) return *x; + if (auto h = home()) return *h / "AppData" / "Local" / "Temp"; + return std::filesystem::path("C:/Windows/Temp"); + } + if (auto x = var("TMPDIR")) return *x; + return std::filesystem::path("/tmp"); +} + +} // namespace mcpplibs::os::dirs diff --git a/src/dirs.cppm b/src/dirs.cppm new file mode 100644 index 0000000..a79e2f9 --- /dev/null +++ b/src/dirs.cppm @@ -0,0 +1,30 @@ +// mcpplibs.os.dirs --- where a program keeps what it keeps, by the conventions +// of the system it runs upon. +// +// Linux (XDG) macOS Windows +// home $HOME $HOME %USERPROFILE% +// config $XDG_CONFIG_HOME|~/.config ~/Library/Application Support %APPDATA% +// data $XDG_DATA_HOME|~/.local/share ~/Library/Application Support %LOCALAPPDATA% +// state $XDG_STATE_HOME|~/.local/state ~/Library/Application Support %LOCALAPPDATA% +// cache $XDG_CACHE_HOME|~/.cache ~/Library/Caches %LOCALAPPDATA% +// runtime $XDG_RUNTIME_DIR|temp $TMPDIR %TEMP% +// temp $TMPDIR|/tmp $TMPDIR|/tmp %TEMP%|%TMP% +// +// The system is `host()', so an image follows the conventions of the system it +// was loaded on. A variable that is set and empty is treated as not set, as the +// XDG specification says. +export module mcpplibs.os.dirs; + +import std; + +export namespace mcpplibs::os::dirs { + +std::optional home(); +std::optional config(); +std::optional data(); +std::optional state(); +std::optional cache(); +std::optional runtime(); +std::filesystem::path temp(); + +} // namespace mcpplibs::os::dirs diff --git a/src/env.cpp b/src/env.cpp new file mode 100644 index 0000000..8d485d4 --- /dev/null +++ b/src/env.cpp @@ -0,0 +1,130 @@ +module mcpplibs.os.env; + +import std; +import openkal.types; +import openkal.env; +import openkal.fs; +import mcpplibs.os.platform; +import mcpplibs.os.fs; + +namespace mcpplibs::os::env { + +namespace { +// A value of a counting operation: the length it has, then the bytes. +template +std::optional counted(F f) { + const kal_intptr n = f(nullptr, 0); + if (n < 0) return std::nullopt; + std::string out(static_cast(n), '\0'); + if (n > 0) f(out.data(), out.size()); + return out; +} +} // namespace + +std::optional get(std::string_view name) { + return counted([&](char* out, kal_uintptr cap) { return kal_env_var(name.data(), name.size(), out, cap); }); +} + +std::vector variables() { + std::vector out; + const kal_uintptr n = kal_env_var_count(); + for (kal_uintptr i = 0; i < n; ++i) { + auto name = counted([&](char* o, kal_uintptr cap) { return kal_env_var_at(i, o, cap); }); + if (!name) continue; + auto value = get(*name); + out.push_back(*name + "=" + value.value_or("")); + } + return out; +} + +std::vector arguments() { + std::vector out; + const kal_uintptr n = kal_env_arg_count(); + for (kal_uintptr i = 0; i < n; ++i) + out.push_back(counted([&](char* o, kal_uintptr cap) { return kal_env_arg(i, o, cap); }).value_or("")); + return out; +} + +std::optional image() { + auto s = counted([](char* o, kal_uintptr cap) { return kal_env_image(o, cap); }); + if (!s || s->empty()) return std::nullopt; + return std::filesystem::path(*s); +} + +std::optional system() { + return counted([](char* o, kal_uintptr cap) { return kal_env_system(o, cap); }); +} + +std::vector search_path() { + std::vector out; + const auto value = get("PATH"); + if (!value) return out; + const char sep = conventions().path_list_separator; + std::size_t start = 0; + while (start <= value->size()) { + const auto end = value->find(sep, start); + const auto piece = value->substr(start, end == std::string::npos ? std::string::npos : end - start); + if (!piece.empty()) out.emplace_back(piece); + if (end == std::string::npos) break; + start = end + 1; + } + return out; +} + +namespace { +bool is_program(const std::filesystem::path& p) { + std::error_code ec; + return std::filesystem::is_regular_file(p, ec); +} +} // namespace + +std::optional find_executable(std::string_view name, std::string_view path_list) { + if (name.empty()) return std::nullopt; + const bool windows = host() == platform::windows; + std::vector suffixes{ "" }; + if (windows) { + const auto ext = get("PATHEXT").value_or(".COM;.EXE;.BAT;.CMD"); + std::string lower_name(name); + for (auto& c : lower_name) c = static_cast(std::tolower(static_cast(c))); + std::size_t s = 0; + while (s <= ext.size()) { + const auto e = ext.find(';', s); + auto piece = ext.substr(s, e == std::string::npos ? std::string::npos : e - s); + for (auto& c : piece) c = static_cast(std::tolower(static_cast(c))); + if (!piece.empty() && !lower_name.ends_with(piece)) suffixes.push_back(piece); + if (e == std::string::npos) break; + s = e + 1; + } + // A name the system starts directly comes before one it would hand to + // an interpreter, as the system's own search orders them. + std::ranges::stable_partition(suffixes, [](const std::string& x) { return x.empty() || x == ".exe" || x == ".com"; }); + } + const std::filesystem::path given(name); + if (given.has_parent_path()) { + for (const auto& s : suffixes) { + auto candidate = given; candidate += s; + if (is_program(candidate)) return candidate; + } + return std::nullopt; + } + const char sep = conventions().path_list_separator; + std::size_t start = 0; + while (start <= path_list.size()) { + const auto end = path_list.find(sep, start); + const auto dir = path_list.substr(start, end == std::string_view::npos ? std::string_view::npos : end - start); + if (!dir.empty()) + for (const auto& s : suffixes) { + auto candidate = std::filesystem::path(dir) / given; candidate += s; + if (is_program(candidate)) return candidate; + } + if (end == std::string_view::npos) break; + start = end + 1; + } + return std::nullopt; +} + +std::optional find_executable(std::string_view name) { + return find_executable(name, get("PATH").value_or("")); +} + +} // namespace mcpplibs::os::env diff --git a/src/env.cppm b/src/env.cppm new file mode 100644 index 0000000..4fc738e --- /dev/null +++ b/src/env.cppm @@ -0,0 +1,42 @@ +// mcpplibs.os.env --- the parameters this program was started with. +// +// The set is what the program was started with and does not change while it +// runs (openkal.env); a started program's environment is stated when it is +// started (`process::spawn_options'), and this program's own is not modified +// --- part 2 §6.4. +export module mcpplibs.os.env; + +import std; +export import mcpplibs.os.error; + +export namespace mcpplibs::os::env { + +// A variable's value; nothing when it is not set, which is distinct from set +// and empty. +std::optional get(std::string_view name); + +// Every variable, as `NAME=value', in an order that does not change. +std::vector variables(); + +// The arguments, the first being the name the program was started by. +std::vector arguments(); + +// The image this program was started from, as the system states it (openkal's +// `kal_env_image'); for an image started by a loader, the image's. +std::optional image(); + +// The system the program runs upon, by name (`linux', `macos', `windows'). +// For conventions only, as openkal restricts it. +std::optional system(); + +// The directories `PATH' names, in order. +std::vector search_path(); + +// A program by name, as a shell would find it: a name with a directory is +// itself; a bare name is looked for in each directory of `PATH' (or the list +// given), and where the host's programs carry suffixes, with each suffix of +// `PATHEXT'. +std::optional find_executable(std::string_view name); +std::optional find_executable(std::string_view name, std::string_view path_list); + +} // namespace mcpplibs::os::env diff --git a/src/error.cpp b/src/error.cpp new file mode 100644 index 0000000..9e8f6f2 --- /dev/null +++ b/src/error.cpp @@ -0,0 +1,39 @@ +module mcpplibs.os.error; + +import std; + +namespace mcpplibs::os { + +std::string_view describe(errc code) noexcept { + switch (code) { + case errc::invalid: return "invalid argument"; + case errc::again: return "would block"; + case errc::io: return "input/output error"; + case errc::no_memory: return "out of memory"; + case errc::no_space: return "no space left"; + case errc::permission: return "permission denied"; + case errc::not_supported: return "not supported here"; + case errc::closed: return "the other end is closed"; + case errc::not_found: return "not found"; + case errc::exists: return "already exists"; + case errc::not_empty: return "directory not empty"; + case errc::is_directory: return "is a directory"; + case errc::not_directory: return "not a directory"; + case errc::not_program: return "not a program this system can start"; + case errc::timed_out: return "timed out"; + } + return "unknown condition"; +} + +error from_kal(int code, std::string_view context) { + // openkal's values are 1..14 in the order errc lists them (types.h). + const auto c = (code >= 1 && code <= 14) ? static_cast(code) : errc::io; + return error{ c, std::string(context), code }; +} + +std::string error::message() const { + return context.empty() ? std::string(describe(code)) + : std::format("{}: {}", context, describe(code)); +} + +} // namespace mcpplibs::os diff --git a/src/error.cppm b/src/error.cppm new file mode 100644 index 0000000..3e3ce3b --- /dev/null +++ b/src/error.cppm @@ -0,0 +1,36 @@ +// mcpplibs.os.error --- what went wrong, in a portable classification. +// +// The classification is openkal's closed set of conditions (SPEC clause 5.2), +// so a program acts upon the same value on every system; the system's own +// code is kept beside it for a report, and so is what was being done. +export module mcpplibs.os.error; + +import std; + +export namespace mcpplibs::os { + +enum class errc { + invalid = 1, again, io, no_memory, no_space, permission, not_supported, closed, + not_found, exists, not_empty, is_directory, not_directory, not_program, + timed_out, +}; + +struct error { + errc code = errc::io; + std::string context; // what was being done, and to what + int native = 0; // the kernel layer's value, for a report + + // `context: condition', for a message. + std::string message() const; +}; + +template +using result = std::expected; + +// The condition an openkal operation reported, with what was being done. +error from_kal(int code, std::string_view context); + +// The text of a condition. +std::string_view describe(errc code) noexcept; + +} // namespace mcpplibs::os diff --git a/src/fs.cpp b/src/fs.cpp new file mode 100644 index 0000000..187ea37 --- /dev/null +++ b/src/fs.cpp @@ -0,0 +1,366 @@ +module mcpplibs.os.fs; + +import std; +import openkal.types; +import openkal.fs; +import openkal.stream; +import openkal.random; +import mcpplibs.os.platform; +import mcpplibs.os.error; + +namespace mcpplibs::os::fs { + +namespace { + +struct supplied { kal_dir dir; std::string name; }; + +// The supplied directories. Read once and read again when the count changes: +// an environment may append one while the program runs (openkal 0.17, a volume +// mounted since), and a position once given keeps its directory. +std::mutex g_table_lock; +std::vector g_table; + +std::vector table() { + std::lock_guard guard(g_table_lock); + const kal_uintptr n = kal_fs_preopen_count(); + for (kal_uintptr i = g_table.size(); i < n; ++i) { + kal_dir d{}; + char name[1024]; + kal_uintptr len = 0; + if (kal_fs_preopen(i, &d, name, sizeof name, &len) != kal_ok || len > sizeof name) { + g_table.push_back(supplied{ kal_dir{}, {} }); + continue; + } + g_table.push_back(supplied{ d, std::string(name, len) }); + } + return g_table; +} + +bool separator(char c) { return c == '/' || c == '\\'; } + +std::string generic(std::string s) { + for (auto& c : s) if (c == '\\') c = '/'; + while (s.size() > 1 && s.back() == '/' && !(s.size() == 2 && s[0] == '/')) s.pop_back(); + return s; +} + +bool same_prefix(std::string_view path, std::string_view prefix, bool fold) { + if (path.size() < prefix.size()) return false; + for (std::size_t i = 0; i < prefix.size(); ++i) { + char a = path[i], b = prefix[i]; + if (fold) { + if (a >= 'A' && a <= 'Z') a = static_cast(a - 'A' + 'a'); + if (b >= 'A' && b <= 'Z') b = static_cast(b - 'A' + 'a'); + } + if (a != b) return false; + } + return true; +} + +struct opened_file { + kal_file f{}; + ~opened_file() { if (f.h) kal_fs_close_file(f); } +}; + +result where(const std::filesystem::path& p, std::string_view doing) { + auto l = locate(p); + if (!l) return std::unexpected(error{ errc::not_found, + std::format("{} {}: beneath no directory this program was given", doing, p.generic_string()) }); + return *l; +} + +kal_dir dir_of(const located& l) { return kal_dir{ static_cast(l.directory) }; } + +// The parent's place and the last component, for operations upon a name in a +// directory the caller may not have opened. +std::string parent_of(const std::string& relative) { + const auto at = relative.find_last_of('/'); + return at == std::string::npos ? std::string(".") : relative.substr(0, at); +} + +std::string unique_suffix() { + unsigned char bytes[8] = {}; + kal_random_fill(bytes, sizeof bytes); + std::string s = ".tmp-"; + for (auto b : bytes) s += std::format("{:02x}", b); + return s; +} + +result write_all(kal_file f, std::string_view bytes, std::string_view doing) { + const kal_stream s = kal_fs_stream(f); + std::size_t done = 0; + while (done < bytes.size()) { + const kal_intptr n = kal_stream_write(s, bytes.data() + done, bytes.size() - done); + if (n < 0) return std::unexpected(from_kal(static_cast(-n), doing)); + if (n == 0) return std::unexpected(error{ errc::io, std::string(doing) }); + done += static_cast(n); + } + return {}; +} + +} // namespace + +std::optional locate(const std::filesystem::path& name) { + std::error_code ec; + auto absolute = std::filesystem::absolute(name, ec); + if (ec) absolute = name; + const std::string path = generic(absolute.lexically_normal().generic_string()); + const bool fold = conventions().case_insensitive_names; + std::optional best; + std::size_t best_len = 0; + for (const auto& s : table()) { + if (!s.dir.h || s.name.empty()) continue; + const std::string prefix = generic(s.name); + if (!same_prefix(path, prefix, fold)) continue; + const bool root = separator(prefix.back()); + if (!root && path.size() > prefix.size() && !separator(path[prefix.size()])) continue; + if (best && best_len >= prefix.size()) continue; + std::string_view rest = std::string_view(path).substr(prefix.size()); + while (!rest.empty() && separator(rest.front())) rest.remove_prefix(1); + best = located{ static_cast(s.dir.h), s.name, + rest.empty() ? std::string(".") : std::string(rest) }; + best_len = prefix.size(); + } + return best; +} + +std::vector preopens() { + std::vector names; + for (const auto& s : table()) names.push_back(s.name); + return names; +} + +result read(const std::filesystem::path& file) { + auto l = where(file, "reading"); + if (!l) return std::unexpected(l.error()); + opened_file f; + const int rc = kal_fs_open(dir_of(*l), l->relative.data(), l->relative.size(), + kal::fs::open::read.bits, &f.f); + if (rc != kal_ok) return std::unexpected(from_kal(rc, std::format("reading {}", file.generic_string()))); + std::string out; + char buf[65536]; + for (;;) { + const kal_intptr n = kal_stream_read(kal_fs_stream(f.f), buf, sizeof buf); + if (n < 0) return std::unexpected(from_kal(static_cast(-n), std::format("reading {}", file.generic_string()))); + if (n == 0) break; + out.append(buf, static_cast(n)); + } + return out; +} + +result write(const std::filesystem::path& file, std::string_view bytes) { + auto l = where(file, "writing"); + if (!l) return std::unexpected(l.error()); + opened_file f; + const auto flags = kal::fs::open::write | kal::fs::open::create | kal::fs::open::truncate; + const int rc = kal_fs_open(dir_of(*l), l->relative.data(), l->relative.size(), flags.bits, &f.f); + if (rc != kal_ok) return std::unexpected(from_kal(rc, std::format("writing {}", file.generic_string()))); + return write_all(f.f, bytes, std::format("writing {}", file.generic_string())); +} + +namespace { +// Written beside the name and made durable; the caller renames it. +result write_beside(const located& l, std::string_view bytes, std::string_view doing) { + const std::string temp = l.relative + unique_suffix(); + opened_file f; + const auto flags = kal::fs::open::write | kal::fs::open::create | kal::fs::open::exclusive; + int rc = kal_fs_open(dir_of(l), temp.data(), temp.size(), flags.bits, &f.f); + if (rc != kal_ok) return std::unexpected(from_kal(rc, doing)); + if (auto w = write_all(f.f, bytes, doing); !w) { + kal_fs_close_file(f.f); f.f = {}; + kal_fs_remove(dir_of(l), temp.data(), temp.size()); + return std::unexpected(w.error()); + } + rc = kal_stream_flush(kal_fs_stream(f.f)); + kal_fs_close_file(f.f); f.f = {}; + if (rc != kal_ok) { + kal_fs_remove(dir_of(l), temp.data(), temp.size()); + return std::unexpected(from_kal(rc, doing)); + } + return temp; +} +} // namespace + +result write_atomic(const std::filesystem::path& file, std::string_view bytes) { + const std::string doing = std::format("writing {}", file.generic_string()); + auto l = where(file, "writing"); + if (!l) return std::unexpected(l.error()); + auto temp = write_beside(*l, bytes, doing); + if (!temp) return std::unexpected(temp.error()); + const int rc = kal_fs_rename(dir_of(*l), temp->data(), temp->size(), + dir_of(*l), l->relative.data(), l->relative.size()); + if (rc != kal_ok) { + kal_fs_remove(dir_of(*l), temp->data(), temp->size()); + return std::unexpected(from_kal(rc, doing)); + } + return {}; +} + +result publish(const std::filesystem::path& file, std::string_view bytes) { + const std::string doing = std::format("publishing {}", file.generic_string()); + auto l = where(file, "publishing"); + if (!l) return std::unexpected(l.error()); + kal_dir parent{}; + const std::string up = parent_of(l->relative); + bool close_parent = false; + if (up == ".") parent = dir_of(*l); + else if (kal_fs_open_dir(dir_of(*l), up.data(), up.size(), &parent) == kal_ok) close_parent = true; + const bool atomic = parent.h && (kal_fs_props(parent) & kal::fs::rename_no_replace.bits) != 0; + if (close_parent) kal_fs_close_dir(parent); + + if (atomic) { + auto temp = write_beside(*l, bytes, doing); + if (!temp) return std::unexpected(temp.error()); + const int rc = kal_fs_rename_flags(dir_of(*l), temp->data(), temp->size(), + dir_of(*l), l->relative.data(), l->relative.size(), + kal::fs::no_replace.bits); + if (rc != kal_ok) { + kal_fs_remove(dir_of(*l), temp->data(), temp->size()); + return std::unexpected(from_kal(rc, doing)); + } + return published{ true }; + } + // A volume that cannot rename without replacing: the name is taken + // exclusively, which is atomic everywhere, and then written. + opened_file f; + const auto flags = kal::fs::open::write | kal::fs::open::create | kal::fs::open::exclusive; + const int rc = kal_fs_open(dir_of(*l), l->relative.data(), l->relative.size(), flags.bits, &f.f); + if (rc != kal_ok) return std::unexpected(from_kal(rc, doing)); + if (auto w = write_all(f.f, bytes, doing); !w) return std::unexpected(w.error()); + if (const int fl = kal_stream_flush(kal_fs_stream(f.f)); fl != kal_ok) + return std::unexpected(from_kal(fl, doing)); + return published{ false }; +} + +result caps(const std::filesystem::path& directory) { + auto l = where(directory, "asking about"); + if (!l) return std::unexpected(l.error()); + kal_dir d = dir_of(*l); + bool close = false; + if (l->relative != ".") { + const int rc = kal_fs_open_dir(dir_of(*l), l->relative.data(), l->relative.size(), &d); + if (rc != kal_ok) return std::unexpected(from_kal(rc, std::format("asking about {}", directory.generic_string()))); + close = true; + } + const kal_uintptr p = kal_fs_props(d); + if (close) kal_fs_close_dir(d); + capabilities c; + c.case_sensitive = (p & kal::fs::case_sensitive.bits) != 0; + c.links = (p & kal::fs::links.bits) != 0; + c.make_links = (p & kal::fs::make_links.bits) != 0; + c.rename_no_replace = (p & kal::fs::rename_no_replace.bits) != 0; + c.local_endpoint = (p & kal::fs::local_endpoint.bits) != 0; + c.locks = (p & kal::fs::locks.bits) != 0; + c.executable = (p & kal::fs::executable.bits) != 0; + c.atomic_rename = (p & kal::fs::atomic_rename.bits) != 0; + return c; +} + +result lock::acquire(const std::filesystem::path& file, kind k, bool wait) { + const std::string doing = std::format("locking {}", file.generic_string()); + auto l = where(file, "locking"); + if (!l) return std::unexpected(l.error()); + kal_file f{}; + const auto flags = kal::fs::open::read | kal::fs::open::write | kal::fs::open::create; + int rc = kal_fs_open(dir_of(*l), l->relative.data(), l->relative.size(), flags.bits, &f); + if (rc != kal_ok) return std::unexpected(from_kal(rc, doing)); + auto mode = (k == kind::exclusive ? kal::fs::lock::exclusive : kal::fs::lock::shared); + if (wait) mode = mode | kal::fs::lock::wait; + // The whole of the file, now and as it grows: a length of zero is "to the + // end, wherever that comes to be". + rc = kal_fs_lock(f, 0, 0, mode.bits); + if (rc != kal_ok) { kal_fs_close_file(f); return std::unexpected(from_kal(rc, doing)); } + lock out; + out.file_ = static_cast(f.h); + return out; +} + +lock::lock(lock&& o) noexcept : file_(std::exchange(o.file_, 0)) {} +lock& lock::operator=(lock&& o) noexcept { + if (this != &o) { release(); file_ = std::exchange(o.file_, 0); } + return *this; +} +lock::~lock() { release(); } +void lock::release() noexcept { + if (!file_) return; + const kal_file f{ static_cast(file_) }; + kal_fs_unlock(f, 0, 0); + kal_fs_close_file(f); + file_ = 0; +} + +result create_link(const std::filesystem::path& target, const std::filesystem::path& link, + bool directory) { + const std::string doing = std::format("linking {} to {}", link.generic_string(), target.generic_string()); + auto l = where(link, "linking"); + if (!l) return std::unexpected(l.error()); + const std::string t = target.generic_string(); + const int rc = kal_fs_link_create(dir_of(*l), l->relative.data(), l->relative.size(), t.data(), t.size(), + directory ? kal::fs::link_directory : 0); + if (rc != kal_ok) return std::unexpected(from_kal(rc, doing)); + return {}; +} + +result read_link(const std::filesystem::path& link) { + const std::string doing = std::format("reading the link {}", link.generic_string()); + auto l = where(link, "reading the link"); + if (!l) return std::unexpected(l.error()); + std::string out(4096, '\0'); + for (;;) { + const kal_intptr n = kal_fs_link_read(dir_of(*l), l->relative.data(), l->relative.size(), + out.data(), out.size()); + if (n < 0) return std::unexpected(from_kal(static_cast(-n), doing)); + if (static_cast(n) <= out.size()) { out.resize(static_cast(n)); return out; } + out.resize(static_cast(n)); + } +} + +result set_executable(const std::filesystem::path& file, bool executable) { + auto l = where(file, "marking"); + if (!l) return std::unexpected(l.error()); + const int rc = kal_fs_set_executable_at(dir_of(*l), l->relative.data(), l->relative.size(), executable ? 1 : 0); + if (rc != kal_ok) return std::unexpected(from_kal(rc, std::format("marking {}", file.generic_string()))); + return {}; +} + +namespace { +result info(const std::filesystem::path& node, kal_u32 wanted, std::string_view doing) { + auto l = where(node, doing); + if (!l) return std::unexpected(l.error()); + kal_node_info i{}; + i.self_size = sizeof i; + const int rc = kal_fs_info(dir_of(*l), l->relative.data(), l->relative.size(), 0, wanted, &i); + if (rc != kal_ok) return std::unexpected(from_kal(rc, std::format("{} {}", doing, node.generic_string()))); + if (i.kind == kal_node_absent) return std::unexpected(error{ errc::not_found, std::format("{} {}", doing, node.generic_string()) }); + return i; +} +} // namespace + +result stamp_of(const std::filesystem::path& node) { + auto i = info(node, kal::fs::field::size | kal::fs::field::modified, "asking about"); + if (!i) return std::unexpected(i.error()); + return stamp{ i->size, i->modified_ns }; +} + +result identity_of(const std::filesystem::path& node) { + auto i = info(node, kal::fs::field::identity, "asking about"); + if (!i) return std::unexpected(i.error()); + if ((i->present & kal::fs::field::identity) == 0) + return std::unexpected(error{ errc::not_supported, std::format("the identity of {}", node.generic_string()) }); + return identity{ i->identity[0], i->identity[1] }; +} + +result rename(const std::filesystem::path& from, const std::filesystem::path& to, bool replace) { + const std::string doing = std::format("renaming {} to {}", from.generic_string(), to.generic_string()); + auto a = where(from, "renaming"); + if (!a) return std::unexpected(a.error()); + auto b = where(to, "renaming"); + if (!b) return std::unexpected(b.error()); + const int rc = kal_fs_rename_flags(dir_of(*a), a->relative.data(), a->relative.size(), + dir_of(*b), b->relative.data(), b->relative.size(), + replace ? 0 : kal::fs::no_replace.bits); + if (rc != kal_ok) return std::unexpected(from_kal(rc, doing)); + return {}; +} + +} // namespace mcpplibs::os::fs diff --git a/src/fs.cppm b/src/fs.cppm new file mode 100644 index 0000000..9bb57d7 --- /dev/null +++ b/src/fs.cppm @@ -0,0 +1,125 @@ +// mcpplibs.os.fs --- what std::filesystem does not say. +// +// std::filesystem names, creates, enumerates and removes, and this module does +// not repeat it (R0). What is here is what it cannot express: a write that is +// all or nothing, a publication that does not replace, an exclusion that ends +// with its holder, what a volume can do, links on every system, whether a node +// may be started, and the identity of a node. Every operation reaches the +// kernel through openkal, upon the supplied directory an absolute name lies +// beneath (`locate'). +export module mcpplibs.os.fs; + +import std; +export import mcpplibs.os.error; + +export namespace mcpplibs::os::fs { + +// Where a name lies, in openkal's terms: a supplied directory (`kal_dir', a +// borrowed handle carried as its word) and the remainder beneath it, without a +// leading separator; "." for the directory itself. For a program that also +// calls openkal itself. +struct located { + std::uintptr_t directory = 0; + std::string preopen; + std::string relative; +}; + +// The supplied directory a name lies beneath: the longest whose name is a +// prefix of the name made absolute against the working directory, compared +// without case where the host's names are. Nothing, for a name beneath none. +std::optional locate(const std::filesystem::path& name); + +// The supplied directories' names, in openkal's order. +std::vector preopens(); + +// --- contents -------------------------------------------------------------- + +result read(const std::filesystem::path& file); + +// Written in place: created, or truncated and rewritten. +result write(const std::filesystem::path& file, std::string_view bytes); + +// ALL OR NOTHING. Written beside the name, made durable, then renamed over it: +// a reader sees the old content or the new and never part of either. +result write_atomic(const std::filesystem::path& file, std::string_view bytes); + +// WRITTEN UNDER A NAME ONLY IF NOBODY HAS: fails with errc::exists, changing +// nothing, when the name is there. On a volume that renames without replacing +// (openkal's KAL_FS_PROP_RENAME_NO_REPLACE) it is all or nothing as well; on +// one that cannot --- NFS version 3 among them --- the name is created +// exclusively and then written, so a reader may see it empty, and +// `published::atomic' says which happened. +struct published { bool atomic = false; }; +result publish(const std::filesystem::path& file, std::string_view bytes); + +// --- what a volume can do ---------------------------------------------------- + +struct capabilities { + bool case_sensitive = false; + bool links = false; // nodes whose content is another name are met + bool make_links = false; // and are made + bool rename_no_replace = false; // `publish' is all or nothing + bool local_endpoint = false; // an `ipc::local' name may be here + bool locks = false; + bool executable = false; // whether a node may be started is recorded + bool atomic_rename = false; +}; + +// Of the volume the directory is on. +result caps(const std::filesystem::path& directory); + +// --- exclusion ----------------------------------------------------------------- + +// A lock upon a file, held until released or until the holder ends however it +// ends. The file is created if it is not there. +class lock { +public: + enum class kind { shared, exclusive }; + static result acquire(const std::filesystem::path& file, kind k, bool wait); + + lock() = default; + lock(lock&&) noexcept; + lock& operator=(lock&&) noexcept; + lock(const lock&) = delete; + lock& operator=(const lock&) = delete; + ~lock(); + + bool held() const noexcept { return file_ != 0; } + void release() noexcept; + +private: + std::uintptr_t file_ = 0; +}; + +// --- links, programs, identity ------------------------------------------------ + +// A node at `link' whose content is `target'. `directory' says the target is a +// directory, which one system must be told; there a directory's link that a +// symbolic link cannot be is a junction, whose content is the absolute name. +result create_link(const std::filesystem::path& target, const std::filesystem::path& link, + bool directory); +result read_link(const std::filesystem::path& link); + +// Whether the node may be started, on a volume that records it. +result set_executable(const std::filesystem::path& file, bool executable); + +struct stamp { + std::uint64_t size = 0; + std::uint64_t modified_ns = 0; + auto operator<=>(const stamp&) const = default; +}; +result stamp_of(const std::filesystem::path& node); + +// Two names are one node exactly when their identities are equal. +struct identity { + std::uint64_t volume = 0; + std::uint64_t index = 0; + auto operator<=>(const identity&) const = default; +}; +result identity_of(const std::filesystem::path& node); + +// A rename; `replace' false fails with errc::exists when `to' is there. +result rename(const std::filesystem::path& from, const std::filesystem::path& to, + bool replace = true); + +} // namespace mcpplibs::os::fs diff --git a/src/interrupt.cpp b/src/interrupt.cpp new file mode 100644 index 0000000..9945ec5 --- /dev/null +++ b/src/interrupt.cpp @@ -0,0 +1,54 @@ +module mcpplibs.os.interrupt; + +import std; +import openkal.types; +import openkal.process; +import openkal.task; + +namespace mcpplibs::os::interrupt { + +namespace { +kal_u32 load(const kal_u32* w) { + return std::atomic_ref(*const_cast(w)).load(std::memory_order_acquire); +} + +const kal_u32* word() { + static const kal_u32* w = kal_process_stop_requested(); + return w; +} +} // namespace + +bool available() { return word() != nullptr; } + +bool requested() { + const kal_u32* w = word(); + return w != nullptr && load(w) != 0; +} + +bool wait(std::optional timeout) { + const kal_u32* w = word(); + if (w == nullptr) { + if (timeout) std::this_thread::sleep_for(*timeout); + return false; + } + const auto until = timeout ? std::chrono::steady_clock::now() + *timeout + : std::chrono::steady_clock::time_point::max(); + while (load(w) == 0) { + kal_u64 ns = 0; // zero waits without a bound + if (timeout) { + const auto left = until - std::chrono::steady_clock::now(); + if (left <= std::chrono::steady_clock::duration::zero()) return false; + ns = static_cast(std::max( + 1, std::chrono::duration_cast(left).count())); + } + kal_task_wait(w, 0, ns); + } + return true; +} + +void end_as_requested() { + if (!requested()) return; + kal_process_end_as_requested(); +} + +} // namespace mcpplibs::os::interrupt diff --git a/src/interrupt.cppm b/src/interrupt.cppm new file mode 100644 index 0000000..dba4336 --- /dev/null +++ b/src/interrupt.cppm @@ -0,0 +1,28 @@ +// mcpplibs.os.interrupt --- a request to end this program, and ending as asked. +// +// openkal's stop word (`kal_process_stop_requested'): taken over on first use, +// read whenever the program chooses, waited upon with a bound, and never a +// handler that interrupts what is running (part 2 §6.4). A program that was +// asked to end finishes what it does and then ends as the request would have +// ended it, so a shell sees 130 or 143 and a waiting starter sees an end by +// request (openkal 0.17, `kal_process_end_as_requested'). +export module mcpplibs.os.interrupt; + +import std; + +export namespace mcpplibs::os::interrupt { + +// Whether this environment delivers a request to end as a notice at all. +bool available(); + +// Whether a request has arrived. The first call takes the request over. +bool requested(); + +// Waits until a request arrives, or until `timeout' passes; whether one did. +bool wait(std::optional timeout = std::nullopt); + +// Ends this program as the request ended it; returns only when there was no +// request, and then does nothing. +void end_as_requested(); + +} // namespace mcpplibs::os::interrupt diff --git a/src/ipc.cpp b/src/ipc.cpp new file mode 100644 index 0000000..367fe82 --- /dev/null +++ b/src/ipc.cpp @@ -0,0 +1,187 @@ +module mcpplibs.os.ipc; + +import std; +import openkal.types; +import openkal.fs; +import openkal.stream; +import openkal.net; +import openkal.timeout; +import mcpplibs.os.error; +import mcpplibs.os.fs; +import mcpplibs.os.dirs; + +namespace mcpplibs::os::ipc { + +namespace { + +struct place { kal_dir dir{}; bool owned = false; std::string doing; }; + +// The directory, opened, for a name to be bound in it. +result directory_of(const std::filesystem::path& d, std::string_view name, std::string_view verb) { + place p; + p.doing = std::format("{} {} in {}", verb, name, d.generic_string()); + auto l = fs::locate(d); + if (!l) return std::unexpected(error{ errc::not_found, p.doing + ": beneath no directory this program was given" }); + p.dir = kal_dir{ static_cast(l->directory) }; + if (l->relative != ".") { + if (const int rc = kal_fs_open_dir(p.dir, l->relative.data(), l->relative.size(), &p.dir); rc != kal_ok) + return std::unexpected(from_kal(rc, p.doing)); + p.owned = true; + } + return p; +} + +void release(place& p) { if (p.owned) kal_fs_close_dir(p.dir); p.owned = false; } + +kal_u64 ns(std::chrono::milliseconds t) { + const auto n = std::chrono::duration_cast(t).count(); + return static_cast(n < 1 ? 1 : n); +} + +} // namespace + +result connection::connect(const std::filesystem::path& directory, std::string_view name) { + auto p = directory_of(directory, name, "connecting to"); + if (!p) return std::unexpected(p.error()); + kal_net_conn c{}; + const int rc = kal_net_connect_at(p->dir, name.data(), name.size(), &c); + const std::string doing = p->doing; + release(*p); + if (rc != kal_ok) return std::unexpected(from_kal(rc, doing)); + connection out; + out.conn_ = static_cast(c.h); + return out; +} + +connection connection::adopt(std::uintptr_t c) { + connection out; + out.conn_ = c; + return out; +} + +connection::connection(connection&& o) noexcept : conn_(std::exchange(o.conn_, 0)) {} +connection& connection::operator=(connection&& o) noexcept { + if (this != &o) { close(); conn_ = std::exchange(o.conn_, 0); } + return *this; +} +connection::~connection() { close(); } + +result connection::read(std::size_t at_most) { + if (!conn_) return std::string{}; + std::string out(at_most, '\0'); + const kal_intptr n = kal_stream_read(kal_net_stream(kal_net_conn{ conn_ }), out.data(), out.size()); + if (n < 0) { + if (-n == kal_err_closed) return std::string{}; + return std::unexpected(from_kal(static_cast(-n), "reading a local connection")); + } + out.resize(static_cast(n)); + return out; +} + +result> connection::read_for(std::chrono::milliseconds t, std::size_t at_most) { + if (!conn_) return std::optional{ std::string{} }; + std::string out(at_most, '\0'); + const kal_intptr n = kal_timeout_read(kal_net_stream(kal_net_conn{ conn_ }), out.data(), out.size(), ns(t)); + if (n < 0) { + if (-n == kal_err_again) return std::optional{}; + if (-n == kal_err_closed) return std::optional{ std::string{} }; + return std::unexpected(from_kal(static_cast(-n), "reading a local connection")); + } + out.resize(static_cast(n)); + return std::optional{ std::move(out) }; +} + +result connection::write(std::string_view bytes) { + if (!conn_) return std::unexpected(error{ errc::closed, "writing a local connection" }); + std::lock_guard g(write_lock_); + const kal_intptr n = kal_stream_write(kal_net_stream(kal_net_conn{ conn_ }), bytes.data(), bytes.size()); + if (n < 0) return std::unexpected(from_kal(static_cast(-n), "writing a local connection")); + return {}; +} + +void connection::shutdown_write() { + if (conn_) kal_net_shutdown(kal_net_conn{ conn_ }, 2 /* KAL_SHUT_WRITE */); +} + +void connection::close() { + if (conn_) { kal_net_close(kal_net_conn{ conn_ }); conn_ = 0; } +} + +result listener::listen(const std::filesystem::path& directory, std::string_view name) { + auto p = directory_of(directory, name, "listening at"); + if (!p) return std::unexpected(p.error()); + kal_net_listener l{}; + int rc = kal_net_listen_at(p->dir, name.data(), name.size(), &l); + if (rc == kal_err_exists) { + // Left behind, or in use: in use answers a connection. + kal_net_conn probe{}; + if (kal_net_connect_at(p->dir, name.data(), name.size(), &probe) == kal_ok) { + kal_net_close(probe); + } else { + kal_fs_remove(p->dir, name.data(), name.size()); + rc = kal_net_listen_at(p->dir, name.data(), name.size(), &l); + } + } + const std::string doing = p->doing; + release(*p); + if (rc != kal_ok) return std::unexpected(from_kal(rc, doing)); + listener out; + out.listener_ = static_cast(l.h); + out.where_ = directory / std::filesystem::path(std::string(name)); + return out; +} + +listener::listener(listener&& o) noexcept + : listener_(std::exchange(o.listener_, 0)), where_(std::move(o.where_)) {} +listener& listener::operator=(listener&& o) noexcept { + if (this != &o) { close(); listener_ = std::exchange(o.listener_, 0); where_ = std::move(o.where_); } + return *this; +} +listener::~listener() { close(); } + +result listener::accept() { + if (!listener_) return std::unexpected(error{ errc::closed, "accepting on a closed listener" }); + kal_net_conn c{}; + if (const int rc = kal_net_accept(kal_net_listener{ listener_ }, &c); rc != kal_ok) + return std::unexpected(from_kal(rc, "accepting a local connection")); + connection out; + out.conn_ = static_cast(c.h); + return out; +} + +result> listener::accept_for(std::chrono::milliseconds t) { + if (!listener_) return std::unexpected(error{ errc::closed, "accepting on a closed listener" }); + kal_net_conn c{}; + const int rc = kal_timeout_accept(kal_net_listener{ listener_ }, ns(t), &c); + if (rc == kal_err_again) return std::optional{}; + if (rc != kal_ok) return std::unexpected(from_kal(rc, "accepting a local connection")); + connection out; + out.conn_ = static_cast(c.h); + return std::optional{ std::move(out) }; +} + +void listener::close() { + if (!listener_) return; + kal_net_close_listener(kal_net_listener{ listener_ }); + listener_ = 0; + if (auto l = fs::locate(where_)) { + kal_fs_remove(kal_dir{ static_cast(l->directory) }, l->relative.data(), l->relative.size()); + } +} + +result single_instance(std::string_view name, std::filesystem::path directory) { + if (directory.empty()) { + auto r = dirs::runtime(); + if (!r) return std::unexpected(error{ errc::not_found, "no runtime directory" }); + directory = *r; + } + for (int attempt = 0; attempt < 3; ++attempt) { + if (auto c = connection::connect(directory, name)) return instance{ std::move(*c) }; + auto l = listener::listen(directory, name); + if (l) return instance{ std::move(*l) }; + if (l.error().code != errc::exists) return std::unexpected(l.error()); + } + return std::unexpected(error{ errc::again, std::format("becoming or reaching the instance {}", name) }); +} + +} // namespace mcpplibs::os::ipc diff --git a/src/ipc.cppm b/src/ipc.cppm new file mode 100644 index 0000000..e842285 --- /dev/null +++ b/src/ipc.cppm @@ -0,0 +1,67 @@ +// mcpplibs.os.ipc --- programs on one machine speaking at a name. +// +// A listener and a connection meet at a name in a directory (openkal 0.17, +// `kal_net_listen_at'), reachable by a program that holds the directory and by +// no other --- unlike an address on the loopback, which every program on the +// machine reaches. Composed here: the runtime directory (`dirs::runtime'), +// removal of a name a listener that ended left behind, and the first of +// several instances becoming the one the others connect to. +export module mcpplibs.os.ipc; + +import std; +export import mcpplibs.os.error; + +export namespace mcpplibs::os::ipc { + +class connection { +public: + static result connect(const std::filesystem::path& directory, std::string_view name); + connection() = default; + connection(connection&&) noexcept; + connection& operator=(connection&&) noexcept; + connection(const connection&) = delete; + connection& operator=(const connection&) = delete; + ~connection(); + bool valid() const noexcept { return conn_ != 0; } + // One chunk; empty at the end. + result read(std::size_t at_most = 65536); + result> read_for(std::chrono::milliseconds timeout, std::size_t at_most = 65536); + result write(std::string_view bytes); + void shutdown_write(); + void close(); + // A connection openkal made elsewhere (`kal_net_conn', carried as its word), + // adopted: this object releases it. + static connection adopt(std::uintptr_t kal_net_conn); +private: + friend class listener; + std::uintptr_t conn_ = 0; + std::mutex write_lock_; +}; + +class listener { +public: + // At `name' in `directory'. A name left behind by a listener that has + // ended --- nothing answers at it --- is removed and taken. + static result listen(const std::filesystem::path& directory, std::string_view name); + listener() = default; + listener(listener&&) noexcept; + listener& operator=(listener&&) noexcept; + listener(const listener&) = delete; + listener& operator=(const listener&) = delete; + // Closed, and the name removed. + ~listener(); + bool valid() const noexcept { return listener_ != 0; } + result accept(); + result> accept_for(std::chrono::milliseconds timeout); + void close(); +private: + std::uintptr_t listener_ = 0; + std::filesystem::path where_; +}; + +// The first program to ask becomes the listener; a later one is connected to +// it. The name is in `dirs::runtime()' unless a directory is given. +using instance = std::variant; +result single_instance(std::string_view name, std::filesystem::path directory = {}); + +} // namespace mcpplibs::os::ipc diff --git a/src/linux.cppm b/src/linux.cppm new file mode 100644 index 0000000..e95daf1 --- /dev/null +++ b/src/linux.cppm @@ -0,0 +1,15 @@ +// mcpplibs.os.linux --- what Linux has and the others do not. Declared on every +// target and defined only on Linux (R5): a use outside `if constexpr +// (os::is_linux)' on another system is a link error, never a branch at run time. +export module mcpplibs.os.linux; + +import std; + +export namespace mcpplibs::os::linux { + +// The system-call translator this program runs under, by name (`proot'), for a +// report of why something behaves as it does; nothing when there is none. There +// is no portable notion of a translator, which is why this is here. +std::optional translator(); + +} // namespace mcpplibs::os::linux diff --git a/src/linux/linux.cpp b/src/linux/linux.cpp new file mode 100644 index 0000000..b1c4230 --- /dev/null +++ b/src/linux/linux.cpp @@ -0,0 +1,30 @@ +module mcpplibs.os.linux; + +import std; + +namespace mcpplibs::os::linux { + +namespace { +std::string read_small(const char* path) { + std::ifstream in(path, std::ios::binary); + std::string s((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + return s; +} +} // namespace + +std::optional translator() { + const std::string status = read_small("/proc/self/status"); + long tracer = 0; + if (const auto at = status.find("TracerPid:"); at != std::string::npos) + tracer = std::strtol(status.c_str() + at + 10, nullptr, 10); + if (tracer > 0) { + std::string comm = read_small(std::format("/proc/{}/comm", tracer).c_str()); + while (!comm.empty() && (comm.back() == '\n' || comm.back() == '\r')) comm.pop_back(); + if (comm.starts_with("proot")) return std::string("proot"); + return std::nullopt; + } + if (status.empty() && std::getenv("PROOT_TMP_DIR") != nullptr) return std::string("proot"); + return std::nullopt; +} + +} // namespace mcpplibs::os::linux diff --git a/src/linux/machine.cpp b/src/linux/machine.cpp new file mode 100644 index 0000000..29a0f9d --- /dev/null +++ b/src/linux/machine.cpp @@ -0,0 +1,25 @@ +module; +#include +#include + +module mcpplibs.os.machine; + +import std; + +namespace mcpplibs::os::machine { + +std::string arch() { + utsname u{}; + if (::uname(&u) != 0) return {}; + std::string m = u.machine; + if (m == "arm64") return "aarch64"; + return m; +} + +std::optional memory() { + struct sysinfo s{}; + if (::sysinfo(&s) != 0) return std::nullopt; + return static_cast(s.totalram) * s.mem_unit; +} + +} // namespace mcpplibs::os::machine diff --git a/src/machine.cppm b/src/machine.cppm new file mode 100644 index 0000000..2c16d06 --- /dev/null +++ b/src/machine.cppm @@ -0,0 +1,21 @@ +// mcpplibs.os.machine --- the machine beneath the system, which is not a +// question of the kernel layer (part 2 §7.2, G7 is not in openkal). +// +// Each answer comes from the system's own service, in an implementation unit +// per system (src//machine.cpp); an image answers from none of them. +export module mcpplibs.os.machine; + +import std; + +export namespace mcpplibs::os::machine { + +// The processor architecture the machine has, which is not always the one this +// program was built for: a program translated for another (Rosetta, WoW64, +// an emulator registered with the kernel) is told the machine's. `x86_64', +// `aarch64', `riscv64', or the system's own word. +std::string arch(); + +// The physical memory, in bytes. +std::optional memory(); + +} // namespace mcpplibs::os::machine diff --git a/src/macos.cppm b/src/macos.cppm new file mode 100644 index 0000000..70cc282 --- /dev/null +++ b/src/macos.cppm @@ -0,0 +1,13 @@ +// mcpplibs.os.macos --- what macOS has and the others do not (R5). +export module mcpplibs.os.macos; + +import std; + +export namespace mcpplibs::os::macos { + +// Whether this program is translated (Rosetta). A program built for one +// architecture and run on another has no portable way to ask, and the answer +// matters only here. +bool translated(); + +} // namespace mcpplibs::os::macos diff --git a/src/macos/machine.cpp b/src/macos/machine.cpp new file mode 100644 index 0000000..3af8d54 --- /dev/null +++ b/src/macos/machine.cpp @@ -0,0 +1,34 @@ +module; +#include +#include + +module mcpplibs.os.machine; + +import std; + +namespace mcpplibs::os::machine { + +namespace { +template +std::optional sysctl_value(const char* name) { + T v{}; + std::size_t len = sizeof v; + if (::sysctlbyname(name, &v, &len, nullptr, 0) != 0) return std::nullopt; + return v; +} +} // namespace + +std::string arch() { + // A translated program is told the machine's architecture by asking + // whether it is translated: the translation is to x86_64 from arm64. + if (sysctl_value("sysctl.proc_translated").value_or(0) == 1) return "aarch64"; + char m[64] = {}; + std::size_t len = sizeof m; + if (::sysctlbyname("hw.machine", m, &len, nullptr, 0) != 0) return {}; + std::string s(m); + return s == "arm64" ? "aarch64" : s; +} + +std::optional memory() { return sysctl_value("hw.memsize"); } + +} // namespace mcpplibs::os::machine diff --git a/src/macos/macos.cpp b/src/macos/macos.cpp new file mode 100644 index 0000000..e42673e --- /dev/null +++ b/src/macos/macos.cpp @@ -0,0 +1,16 @@ +module; +#include + +module mcpplibs.os.macos; + +import std; + +namespace mcpplibs::os::macos { + +bool translated() { + int v = 0; + std::size_t len = sizeof v; + return ::sysctlbyname("sysctl.proc_translated", &v, &len, nullptr, 0) == 0 && v == 1; +} + +} // namespace mcpplibs::os::macos diff --git a/src/net.cpp b/src/net.cpp new file mode 100644 index 0000000..e620bce --- /dev/null +++ b/src/net.cpp @@ -0,0 +1,62 @@ +module mcpplibs.os.net; + +import std; +import openkal.types; +import openkal.net; +import mcpplibs.os.error; +import mcpplibs.os.ipc; + +namespace mcpplibs::os::net { + +namespace { +kal_endpoint loopback(std::uint16_t port) { + kal_endpoint e{}; + e.addr[0] = 127; e.addr[1] = 0; e.addr[2] = 0; e.addr[3] = 1; + e.addr_len = 4; + e.port = port; + return e; +} +} // namespace + +result loopback_listener::listen(std::uint16_t port) { + const kal_endpoint at = loopback(port); + kal_net_listener l{}; + if (const int rc = kal_net_listen(&at, &l); rc != kal_ok) + return std::unexpected(from_kal(rc, std::format("listening on 127.0.0.1:{}", port))); + kal_endpoint given{}; + kal_net_listener_local(l, &given); + loopback_listener out; + out.listener_ = static_cast(l.h); + out.port_ = static_cast(given.port); + return out; +} + +loopback_listener::loopback_listener(loopback_listener&& o) noexcept + : listener_(std::exchange(o.listener_, 0)), port_(o.port_) {} +loopback_listener& loopback_listener::operator=(loopback_listener&& o) noexcept { + if (this != &o) { close(); listener_ = std::exchange(o.listener_, 0); port_ = o.port_; } + return *this; +} +loopback_listener::~loopback_listener() { close(); } + +result loopback_listener::accept() { + if (!listener_) return std::unexpected(error{ errc::closed, "accepting on a closed listener" }); + kal_net_conn c{}; + if (const int rc = kal_net_accept(kal_net_listener{ listener_ }, &c); rc != kal_ok) + return std::unexpected(from_kal(rc, "accepting a loopback connection")); + return ipc::connection::adopt(static_cast(c.h)); +} + +void loopback_listener::close() { + if (listener_) { kal_net_close_listener(kal_net_listener{ listener_ }); listener_ = 0; } +} + +result connect_loopback(std::uint16_t port) { + const kal_endpoint to = loopback(port); + kal_net_conn c{}; + if (const int rc = kal_net_connect(&to, &c); rc != kal_ok) + return std::unexpected(from_kal(rc, std::format("connecting to 127.0.0.1:{}", port))); + return ipc::connection::adopt(static_cast(c.h)); +} + +} // namespace mcpplibs::os::net diff --git a/src/net.cppm b/src/net.cppm new file mode 100644 index 0000000..60f2d9b --- /dev/null +++ b/src/net.cppm @@ -0,0 +1,34 @@ +// mcpplibs.os.net --- a connection on this machine's loopback, for a peer that +// speaks TCP (an editor's language client, a debugger). A program that chooses +// both ends uses `ipc', which only the holders of a directory reach. +export module mcpplibs.os.net; + +import std; +export import mcpplibs.os.error; +export import mcpplibs.os.ipc; + +export namespace mcpplibs::os::net { + +// A listener on 127.0.0.1, at the port given or, for zero, one the system +// chooses; and a connection to such a port. Both read and write as +// `ipc::connection' does. +class loopback_listener { +public: + static result listen(std::uint16_t port = 0); + loopback_listener() = default; + loopback_listener(loopback_listener&&) noexcept; + loopback_listener& operator=(loopback_listener&&) noexcept; + loopback_listener(const loopback_listener&) = delete; + loopback_listener& operator=(const loopback_listener&) = delete; + ~loopback_listener(); + std::uint16_t port() const noexcept { return port_; } + result accept(); + void close(); +private: + std::uintptr_t listener_ = 0; + std::uint16_t port_ = 0; +}; + +result connect_loopback(std::uint16_t port); + +} // namespace mcpplibs::os::net diff --git a/src/os.cppm b/src/os.cppm new file mode 100644 index 0000000..2da3c51 --- /dev/null +++ b/src/os.cppm @@ -0,0 +1,24 @@ +// mcpplibs.os --- every module of the library. +// +// The platform modules are re-exported too, so that they are public modules of +// the package (mcpp SPEC-008: the root and what it re-exports): their names are +// in a namespace of their platform (`os::windows::elevated'), and a call outside +// `if constexpr' for that platform is a link error on every other (R5). +export module mcpplibs.os; + +export import mcpplibs.os.platform; +export import mcpplibs.os.error; +export import mcpplibs.os.env; +export import mcpplibs.os.fs; +export import mcpplibs.os.process; +export import mcpplibs.os.interrupt; +export import mcpplibs.os.stdio; +export import mcpplibs.os.terminal; +export import mcpplibs.os.dirs; +export import mcpplibs.os.ipc; +export import mcpplibs.os.net; +export import mcpplibs.os.machine; +export import mcpplibs.os.linux; +export import mcpplibs.os.macos; +export import mcpplibs.os.windows; +export import mcpplibs.os.posix; diff --git a/src/platform.cppm b/src/platform.cppm new file mode 100644 index 0000000..032f3df --- /dev/null +++ b/src/platform.cppm @@ -0,0 +1,59 @@ +// mcpplibs.os.platform --- what the program is built for, and what it runs upon. +// +// The compile-time constants are the first-class interface (design part 2, +// R3): a program written for several systems dispatches with `if constexpr`, +// and a branch for another system is discarded rather than compiled. They come +// from one partition per target (src/target/.cppm), which the manifest +// selects, so no macro reaches this interface. +// +// `host()' names the system the program RUNS upon. Built for a system, that is +// the system it was built for, known when it is compiled, and `host()' is a +// constant expression. Built as an image (`-openkal'), the system is +// known only when the image is loaded; there `host()' is an ordinary function, +// and using it in a constant expression is a compile error that names it. +export module mcpplibs.os.platform; + +import std; +export import :target; + +export namespace mcpplibs::os { + +// The executable suffix and the separator of a list of directories, as the +// target's conventions spell them. A program built as an image asks `host()' +// and `conventions()' instead; these are not provided there. +inline constexpr bool is_linux = PLATFORM == platform::linux; +inline constexpr bool is_macos = PLATFORM == platform::macos; +inline constexpr bool is_windows = PLATFORM == platform::windows; +inline constexpr bool is_posix = is_linux || is_macos; +inline constexpr bool is_image = PLATFORM == platform::openkal; + +// The name of a platform, as mcpp's target vocabulary and openkal's +// `kal_env_system' spell it. +constexpr std::string_view name(platform p) noexcept { + switch (p) { + case platform::linux: return "linux"; + case platform::macos: return "macos"; + case platform::windows: return "windows"; + case platform::openkal: return "openkal"; + } + return "unknown"; +} + +// The conventions a running program follows: the executable suffix, the list +// separator, whether names are compared without case by default. On a native +// target they are the target's; in an image they are the host's, chosen by +// `host()'. +struct conventions_t { + std::string_view exe_suffix; + char path_list_separator; + bool case_insensitive_names; +}; + +constexpr conventions_t conventions_of(platform p) noexcept { + return p == platform::windows ? conventions_t{ ".exe", ';', true } + : conventions_t{ "", ':', p == platform::macos }; +} + +inline conventions_t conventions() { return conventions_of(host()); } + +} // namespace mcpplibs::os diff --git a/src/posix.cppm b/src/posix.cppm new file mode 100644 index 0000000..1a3b57a --- /dev/null +++ b/src/posix.cppm @@ -0,0 +1,11 @@ +// mcpplibs.os.posix --- what Linux and macOS share and Windows does not (R5). +export module mcpplibs.os.posix; + +import std; + +export namespace mcpplibs::os::posix { + +// The effective user's number; zero is the superuser. +std::uint32_t effective_user(); + +} // namespace mcpplibs::os::posix diff --git a/src/posix/posix.cpp b/src/posix/posix.cpp new file mode 100644 index 0000000..00156a9 --- /dev/null +++ b/src/posix/posix.cpp @@ -0,0 +1,12 @@ +module; +#include + +module mcpplibs.os.posix; + +import std; + +namespace mcpplibs::os::posix { + +std::uint32_t effective_user() { return static_cast(::geteuid()); } + +} // namespace mcpplibs::os::posix diff --git a/src/process.cpp b/src/process.cpp new file mode 100644 index 0000000..8235180 --- /dev/null +++ b/src/process.cpp @@ -0,0 +1,403 @@ +module mcpplibs.os.process; + +import std; +import openkal.types; +import openkal.fs; +import openkal.stream; +import openkal.process; +import openkal.timeout; +import mcpplibs.os.platform; +import mcpplibs.os.error; +import mcpplibs.os.env; +import mcpplibs.os.fs; + +namespace mcpplibs::os::process { + +namespace { + +kal_dir as_dir(std::uintptr_t w) { return kal_dir{ static_cast(w) }; } + +bool same_name(std::string_view a, std::string_view b, bool fold) { + if (a.size() != b.size()) return false; + for (std::size_t i = 0; i < a.size(); ++i) { + auto x = static_cast(a[i]), y = static_cast(b[i]); + if (fold) { x = static_cast(std::tolower(x)); y = static_cast(std::tolower(y)); } + if (x != y) return false; + } + return true; +} + +// This program's environment with the overrides applied, as `NAME=value'. +std::vector environment_for(const spawn_options& o) { + std::vector out = o.clear_env ? std::vector{} : env::variables(); + const bool fold = host() == platform::windows; + for (const auto& [name, value] : o.env) { + std::erase_if(out, [&](const std::string& e) { + return same_name(std::string_view(e).substr(0, e.find('=')), name, fold); + }); + if (value) out.push_back(name + "=" + *value); + } + return out; +} + +exit_status status_of(int status, int terminated) { return exit_status{ status, terminated != 0 }; } + +kal_u64 nanoseconds(std::chrono::milliseconds t) { + const auto ns = std::chrono::duration_cast(t).count(); + return static_cast(ns < 1 ? 1 : ns); +} + +// Where a program's stream goes when it is to go nowhere. A file of the +// system's own where there is one; otherwise a pipe whose far end this program +// closes (input) or drains (output). +struct null_stream { + kal_stream child{}; + bool channel = false; // `child' is an end this program must release + kal_file file{}; +}; + +result open_null(bool for_input) { + null_stream n; + if (host() != platform::windows) { + if (auto l = fs::locate("/dev/null")) { + const auto flags = for_input ? kal::fs::open::read : kal::fs::open::write; + if (kal_fs_open(as_dir(l->directory), l->relative.data(), l->relative.size(), flags.bits, &n.file) == kal_ok) { + n.child = kal_fs_stream(n.file); + return n; + } + } + } + kal_stream mine{}, theirs{}; + if (const int rc = kal_process_channel(&mine, &theirs); rc != kal_ok) + return std::unexpected(from_kal(rc, "making a stream that goes nowhere")); + n.channel = true; + if (for_input) { + n.child = mine; // the program reads, and meets the end at once + kal_process_channel_close(theirs); + } else { + n.child = theirs; // the program writes, and a thread discards it + std::thread([mine] { + char sink[4096]; + while (kal_stream_read(mine, sink, sizeof sink) > 0) {} + kal_process_channel_close(mine); + }).detach(); + } + return n; +} + +result read_chunk(kal_stream s, bool open) { + if (!open) return std::string{}; + char buf[16384]; + const kal_intptr n = kal_stream_read(s, buf, sizeof buf); + if (n < 0) { + if (-n == kal_err_closed) return std::string{}; + return std::unexpected(from_kal(static_cast(-n), "reading a program's output")); + } + return std::string(buf, static_cast(n)); +} + +result> read_chunk_for(kal_stream s, bool open, std::chrono::milliseconds t) { + if (!open) return std::optional{ std::string{} }; + char buf[16384]; + const kal_intptr n = kal_timeout_read(s, buf, sizeof buf, nanoseconds(t)); + if (n < 0) { + const int code = static_cast(-n); + if (code == kal_err_again) return std::optional{}; + if (code == kal_err_closed) return std::optional{ std::string{} }; + if (code == kal_err_not_supported) { + auto r = read_chunk(s, open); + if (!r) return std::unexpected(r.error()); + return std::optional{ std::move(*r) }; + } + return std::unexpected(from_kal(code, "reading a program's output")); + } + return std::optional{ std::string(buf, static_cast(n)) }; +} + +} // namespace + +struct process::state { + kal_process handle{}; + kal_stream input{}, output{}, error{}; + bool input_open = false, output_open = false, error_open = false; + kal_job job{}; + bool has_job = false; + std::optional ended; + std::mutex write_lock, wait_lock; +}; + +process::process() = default; +process::process(process&&) noexcept = default; +process& process::operator=(process&&) noexcept = default; + +process::~process() { + if (!state_) return; + close_input(); + if (!state_->ended) { + if (state_->has_job) kal_process_job_terminate(state_->job); + else kal_process_terminate(state_->handle); + int status = 0, terminated = 0; + kal_process_wait(state_->handle, &status, &terminated); + } + if (state_->output_open) kal_process_channel_close(state_->output); + if (state_->error_open) kal_process_channel_close(state_->error); + if (state_->has_job) kal_process_job_close(state_->job); + kal_process_close(state_->handle); +} + +bool process::valid() const noexcept { return static_cast(state_); } + +std::uint64_t process::id() const { + if (!state_) return 0; + kal_program_info info{}; + info.self_size = sizeof info; + if (kal_process_info(state_->handle, kal::process::info::id, &info) != kal_ok) return 0; + return info.id; +} + +result process::spawn(const spawn_options& o) { + // The program, by name or by its place. + std::filesystem::path program = o.program; + if (!program.has_parent_path()) { + auto found = env::find_executable(program.generic_string()); + if (!found) return std::unexpected(error{ errc::not_found, std::format("{} is not on PATH", program.generic_string()) }); + program = *found; + } + std::error_code ec; + program = std::filesystem::absolute(program, ec).lexically_normal(); + const auto where = fs::locate(program); + if (!where) return std::unexpected(error{ errc::not_found, + std::format("starting {}: beneath no directory this program was given", program.generic_string()) }); + + const auto cwd_path = o.cwd.empty() ? std::filesystem::current_path(ec) : o.cwd; + const auto cwd = fs::locate(cwd_path); + if (!cwd) return std::unexpected(error{ errc::not_found, + std::format("starting {}: {} is beneath no directory this program was given", + program.generic_string(), cwd_path.generic_string()) }); + kal_dir work = as_dir(cwd->directory); + bool close_work = false; + if (cwd->relative != ".") { + if (const int rc = kal_fs_open_dir(work, cwd->relative.data(), cwd->relative.size(), &work); rc != kal_ok) + return std::unexpected(from_kal(rc, std::format("starting {} in {}", program.generic_string(), cwd_path.generic_string()))); + close_work = true; + } + struct closer { kal_dir d; bool on; ~closer() { if (on) kal_fs_close_dir(d); } } work_guard{ work, close_work }; + + auto s = std::make_unique(); + std::vector release; // the program's ends, released after the start + std::vector nulls; + kal_stream child_in{}, child_out{}, child_err{}; + auto fail_streams = [&](const error& e) { + for (auto st : release) kal_process_channel_close(st); + if (s->input_open) kal_process_channel_close(s->input); + if (s->output_open) kal_process_channel_close(s->output); + if (s->error_open) kal_process_channel_close(s->error); + return std::unexpected(e); + }; + // A channel is one way: the first end reads what the second writes. + if (o.in == stdio::pipe) { + if (const int rc = kal_process_channel(&child_in, &s->input); rc != kal_ok) + return fail_streams(from_kal(rc, "making the program's input")); + s->input_open = true; release.push_back(child_in); + } else if (o.in == stdio::null) { + auto n = open_null(true); + if (!n) return fail_streams(n.error()); + child_in = n->child; nulls.push_back(*n); + } + if (o.out == stdio::pipe) { + if (const int rc = kal_process_channel(&s->output, &child_out); rc != kal_ok) + return fail_streams(from_kal(rc, "making the program's output")); + s->output_open = true; release.push_back(child_out); + } else if (o.out == stdio::null) { + auto n = open_null(false); + if (!n) return fail_streams(n.error()); + child_out = n->child; nulls.push_back(*n); + } + if (o.err == stdio::pipe) { + if (const int rc = kal_process_channel(&s->error, &child_err); rc != kal_ok) + return fail_streams(from_kal(rc, "making the program's error output")); + s->error_open = true; release.push_back(child_err); + } else if (o.err == stdio::null) { + auto n = open_null(false); + if (!n) return fail_streams(n.error()); + child_err = n->child; nulls.push_back(*n); + } + + // The vector, the first element in the system's own spelling: a program + // that reads its own command line takes a forward slash for a switch. + std::string first = o.argv0.value_or(program.string()); + if (!o.argv0 && host() == platform::windows) std::ranges::replace(first, '/', '\\'); + std::vector argv_storage{ std::move(first) }; + for (const auto& a : o.args) argv_storage.push_back(a); + std::vector argv; std::vector argv_lens; + for (const auto& a : argv_storage) { argv.push_back(a.data()); argv_lens.push_back(a.size()); } + + const auto environment = environment_for(o); + std::vector envp; std::vector envp_lens; + for (const auto& e : environment) { envp.push_back(e.data()); envp_lens.push_back(e.size()); } + + const kal_uintptr props = kal_process_props(); + const bool bind = o.bound_lifetime && (props & kal::process::bound_lifetime.bits) != 0; + kal_spawn how{ as_dir(where->directory), work, nullptr, nullptr, 0, + bind ? kal::process::bound_lifetime_flag.bits : kal_uintptr{ 0 } }; + kal_job job{}; + const bool unit = o.own_unit && (props & kal::process::job.bits) != 0; + if (unit) how.job = &job; + const kal_spawn_streams streams{ child_in, child_out, child_err }; + + const int rc = kal_process_spawn(&how, where->relative.data(), where->relative.size(), + argv.data(), argv_lens.data(), argv.size(), + envp.data(), envp_lens.data(), envp.size(), + &streams, &s->handle); + for (auto st : release) kal_process_channel_close(st); + release.clear(); + for (auto& n : nulls) { + if (n.file.h) kal_fs_close_file(n.file); + else if (n.channel && n.child.h) kal_process_channel_close(n.child); + } + if (rc != kal_ok) return fail_streams(from_kal(rc, std::format("starting {}", program.generic_string()))); + if (unit) { s->job = job; s->has_job = true; } + process p; + p.state_ = std::move(s); + return p; +} + +result process::write(std::string_view bytes) { + if (!state_ || !state_->input_open) return std::unexpected(error{ errc::closed, "writing to a program's input" }); + std::lock_guard g(state_->write_lock); + std::size_t done = 0; + while (done < bytes.size()) { + const kal_intptr n = kal_stream_write(state_->input, bytes.data() + done, bytes.size() - done); + if (n <= 0) return std::unexpected(from_kal(n < 0 ? static_cast(-n) : kal_err_io, "writing to a program's input")); + done += static_cast(n); + } + return {}; +} + +void process::close_input() { + if (!state_) return; + std::lock_guard g(state_->write_lock); + if (state_->input_open) { kal_process_channel_close(state_->input); state_->input_open = false; } +} + +result process::read_output() { + if (!state_) return std::string{}; + return read_chunk(state_->output, state_->output_open); +} +result process::read_error() { + if (!state_) return std::string{}; + return read_chunk(state_->error, state_->error_open); +} +result> process::read_output_for(std::chrono::milliseconds t) { + if (!state_) return std::optional{ std::string{} }; + return read_chunk_for(state_->output, state_->output_open, t); +} +result> process::read_error_for(std::chrono::milliseconds t) { + if (!state_) return std::optional{ std::string{} }; + return read_chunk_for(state_->error, state_->error_open, t); +} + +result process::wait() { + if (!state_) return std::unexpected(error{ errc::invalid, "waiting for no program" }); + std::lock_guard g(state_->wait_lock); + if (state_->ended) return *state_->ended; + int status = 0, terminated = 0; + if (const int rc = kal_process_wait(state_->handle, &status, &terminated); rc != kal_ok) + return std::unexpected(from_kal(rc, "waiting for a program")); + state_->ended = status_of(status, terminated); + return *state_->ended; +} + +result> process::wait_for(std::chrono::milliseconds t) { + if (!state_) return std::unexpected(error{ errc::invalid, "waiting for no program" }); + std::lock_guard g(state_->wait_lock); + if (state_->ended) return std::optional{ *state_->ended }; + int status = 0, terminated = 0; + const int rc = kal_timeout_wait_process(state_->handle, nanoseconds(t), &status, &terminated); + if (rc == kal_err_again) return std::optional{}; + if (rc != kal_ok) return std::unexpected(from_kal(rc, "waiting for a program")); + state_->ended = status_of(status, terminated); + return std::optional{ *state_->ended }; +} + +result> process::try_wait() { return wait_for(std::chrono::milliseconds(0)); } + +void process::terminate() { + if (!state_) return; + std::lock_guard g(state_->wait_lock); + if (!state_->ended) kal_process_terminate(state_->handle); +} + +void process::kill() { + if (!state_) return; + std::lock_guard g(state_->wait_lock); + if (state_->ended) return; + if (state_->has_job) kal_process_job_terminate(state_->job); + else kal_process_terminate(state_->handle); +} + +void process::detach() { + if (!state_) return; + close_input(); + if (state_->output_open) kal_process_channel_close(state_->output); + if (state_->error_open) kal_process_channel_close(state_->error); + if (state_->has_job) kal_process_job_close(state_->job); + kal_process_close(state_->handle); + state_.reset(); +} + +result run(spawn_options options, std::string_view input) { + if (!input.empty()) options.in = stdio::pipe; + options.out = stdio::pipe; + options.err = stdio::pipe; + auto p = process::spawn(options); + if (!p) return std::unexpected(p.error()); + output out; + std::jthread feeder; + if (!input.empty()) { + feeder = std::jthread([&p, input] { (void)p->write(input); p->close_input(); }); + } else { + p->close_input(); + } + std::jthread err_reader([&p, &out] { + for (;;) { auto c = p->read_error(); if (!c || c->empty()) break; out.err += *c; } + }); + for (;;) { auto c = p->read_output(); if (!c || c->empty()) break; out.out += *c; } + err_reader.join(); + if (feeder.joinable()) feeder.join(); + auto st = p->wait(); + if (!st) return std::unexpected(st.error()); + out.status = *st; + return out; +} + +result self() { + kal_process me{}; + if (const int rc = kal_process_self(&me); rc != kal_ok) return std::unexpected(from_kal(rc, "naming this program")); + kal_program_info info{}; + info.self_size = sizeof info; + const int rc = kal_process_info(me, kal::process::info::all, &info); + kal_process_close(me); + if (rc != kal_ok) return std::unexpected(from_kal(rc, "naming this program")); + return program{ info.id, info.started_ns }; +} + +result describe(std::uint64_t id) { + kal_process p{}; + if (const int rc = kal_process_open_id(id, &p); rc != kal_ok) + return std::unexpected(from_kal(rc, std::format("asking about program {}", id))); + kal_program_info info{}; + info.self_size = sizeof info; + const int rc = kal_process_info(p, kal::process::info::all, &info); + kal_process_close(p); + if (rc != kal_ok) return std::unexpected(from_kal(rc, std::format("asking about program {}", id))); + return program_info{ program{ info.id, info.started_ns }, info.cpu_ns, info.ended != 0 }; +} + +bool alive(const program& p) { + auto d = describe(p.id); + if (!d || d->ended) return false; + return p.started_ns == 0 || d->who.started_ns == 0 || d->who.started_ns == p.started_ns; +} + +} // namespace mcpplibs::os::process diff --git a/src/process.cppm b/src/process.cppm new file mode 100644 index 0000000..f86a90b --- /dev/null +++ b/src/process.cppm @@ -0,0 +1,123 @@ +// mcpplibs.os.process --- starting programs, speaking to them, and ending them. +// +// Composed of openkal.process and openkal.timeout (part 2 §4): the argument +// vector arrives unaltered, the started program receives the three streams and +// nothing else, a unit ends with everything its members started, and a bound +// lifetime ends a started program with this one. The convenience here is the +// composition --- a program found on PATH, an environment stated as overrides, +// pipes made and closed in the right order, both outputs read at once. +export module mcpplibs.os.process; + +import std; +export import mcpplibs.os.error; + +export namespace mcpplibs::os::process { + +enum class stdio { inherit, pipe, null }; + +struct spawn_options { + // An absolute name, or a bare one looked for on PATH (`env::find_executable'). + std::filesystem::path program; + // The arguments after the first; the first is `argv0', or the program's name. + std::vector args; + std::optional argv0; + // Where the program runs; empty for this program's own working directory. + std::filesystem::path cwd; + // This program's environment, with each override applied: a value sets + // the variable and nothing removes it. `clear_env' starts from nothing. + std::vector>> env; + bool clear_env = false; + stdio in = stdio::inherit, out = stdio::pipe, err = stdio::pipe; + // A unit of the program and everything it starts, which `kill' ends. + bool own_unit = false; + // The program ends when this one ends, however this one ends. + bool bound_lifetime = false; +}; + +// How a program ended: the status it returned, or, ended by its environment +// (a signal, a console interrupt, a forced end), that condition's number. +struct exit_status { + int code = 0; + bool by_environment = false; + bool success() const noexcept { return !by_environment && code == 0; } + // The number a POSIX shell reports for it (128 + signal when ended so). + int shell_code() const noexcept { return by_environment ? 128 + (code & 0x7f) : code; } +}; + +class process { +public: + static result spawn(const spawn_options& options); + + process(); + process(process&&) noexcept; + process& operator=(process&&) noexcept; + process(const process&) = delete; + process& operator=(const process&) = delete; + // A program still running when its handle is dropped is ended and awaited, + // unless it was detached. + ~process(); + + bool valid() const noexcept; + // The identifier the system gives the program, for a report or a lease. + std::uint64_t id() const; + + // All of it, or the condition that stopped it (errc::closed when the + // program no longer reads). Safe from several threads. + result write(std::string_view bytes); + void close_input(); + + // One chunk of what the program wrote; empty at the end. + result read_output(); + result read_error(); + // The same, giving up after `timeout': nothing when it expired. + result> read_output_for(std::chrono::milliseconds timeout); + result> read_error_for(std::chrono::milliseconds timeout); + + result wait(); + result> try_wait(); + result> wait_for(std::chrono::milliseconds timeout); + + // A request to end the program, which it may observe and decline. + void terminate(); + // The program's unit, or the program, ended outright. + void kill(); + // Released, and left running. + void detach(); + +private: + struct state; + std::unique_ptr state_; +}; + +// Starts a program, gives it `input', and collects both outputs until it ends. +struct output { + exit_status status; + std::string out; + std::string err; +}; +result run(spawn_options options, std::string_view input = {}); + +// --- programs this one did not start (openkal 0.17) ------------------------ + +// A program, named so that a later one given the same identifier is another. +struct program { + std::uint64_t id = 0; + std::uint64_t started_ns = 0; + auto operator<=>(const program&) const = default; +}; + +struct program_info { + program who; + std::uint64_t cpu_ns = 0; + bool ended = false; +}; + +// This program. +result self(); +// What is known of the program with this identifier now; errc::not_found +// when there is none. +result describe(std::uint64_t id); +// Whether that program is still the one that was named. +bool alive(const program& p); + +} // namespace mcpplibs::os::process diff --git a/src/stdio.cpp b/src/stdio.cpp new file mode 100644 index 0000000..393885c --- /dev/null +++ b/src/stdio.cpp @@ -0,0 +1,53 @@ +module mcpplibs.os.stdio; + +import std; +import openkal.types; +import openkal.stream; +import mcpplibs.os.error; + +namespace mcpplibs::os::stdio { + +namespace { +result write_all(kal_stream s, std::string_view bytes, std::string_view doing) { + // kal_stream_write moves the whole buffer or reports why not (clause 7.4). + if (bytes.empty()) return {}; + const kal_intptr n = kal_stream_write(s, bytes.data(), bytes.size()); + if (n < 0) return std::unexpected(from_kal(static_cast(-n), doing)); + if (static_cast(n) != bytes.size()) return std::unexpected(error{ errc::io, std::string(doing) }); + return {}; +} +} // namespace + +result read_input(std::size_t at_most) { + std::string out(at_most, '\0'); + const kal_intptr n = kal_stream_read(kal_stdin(), out.data(), out.size()); + if (n < 0) { + if (-n == kal_err_closed) return std::string{}; + return std::unexpected(from_kal(static_cast(-n), "reading standard input")); + } + out.resize(static_cast(n)); + return out; +} + +result read_exactly(std::size_t n) { + std::string out; + out.reserve(n); + while (out.size() < n) { + auto c = read_input(n - out.size()); + if (!c) return std::unexpected(c.error()); + if (c->empty()) break; + out += *c; + } + return out; +} + +result write_output(std::string_view bytes) { return write_all(kal_stdout(), bytes, "writing standard output"); } +result write_error(std::string_view bytes) { return write_all(kal_stderr(), bytes, "writing standard error"); } + +result flush_output() { + if (const int rc = kal_stream_flush(kal_stdout()); rc != kal_ok && rc != kal_err_not_supported) + return std::unexpected(from_kal(rc, "flushing standard output")); + return {}; +} + +} // namespace mcpplibs::os::stdio diff --git a/src/stdio.cppm b/src/stdio.cppm new file mode 100644 index 0000000..d111423 --- /dev/null +++ b/src/stdio.cppm @@ -0,0 +1,24 @@ +// mcpplibs.os.stdio --- this program's own three streams, as bytes. +// +// openkal's streams are bytes on every system, so a protocol framed by +// lengths (a language server's Content-Length) reads and writes exactly what +// it means; nothing translates line ends. A write to a stream whose reader is +// gone is errc::closed, which a program reports or treats as the end of its +// output --- it is not ended by the system for it (part 2 §6.4). +export module mcpplibs.os.stdio; + +import std; +export import mcpplibs.os.error; + +export namespace mcpplibs::os::stdio { + +// One chunk of input; empty at the end. +result read_input(std::size_t at_most = 65536); +// Exactly `n' bytes, or fewer at the end of input. +result read_exactly(std::size_t n); + +result write_output(std::string_view bytes); +result write_error(std::string_view bytes); +result flush_output(); + +} // namespace mcpplibs::os::stdio diff --git a/src/terminal.cpp b/src/terminal.cpp new file mode 100644 index 0000000..d23d408 --- /dev/null +++ b/src/terminal.cpp @@ -0,0 +1,81 @@ +module mcpplibs.os.terminal; + +import std; +import openkal.types; +import openkal.stream; +import openkal.terminal; +import mcpplibs.os.env; + +namespace mcpplibs::os::terminal { + +namespace { +kal_stream handle(stream s) { + switch (s) { + case stream::in: return kal_stdin(); + case stream::out: return kal_stdout(); + case stream::err: return kal_stderr(); + } + return kal_stdout(); +} +} // namespace + +bool interactive(stream s) { + kal_uintptr mode = 0; + return kal_terminal_get_mode(handle(s), &mode) == kal_ok; +} + +std::optional size(stream s) { + kal_uintptr cols = 0, rows = 0; + if (kal_terminal_size(handle(s), &cols, &rows) != kal_ok || cols == 0) return std::nullopt; + return dimensions{ cols, rows }; +} + +bool sequences(stream s) { + const kal_stream h = handle(s); + kal_uintptr mode = 0; + if (kal_terminal_get_mode(h, &mode) != kal_ok) return false; + const auto want = kal::terminal::sequences.bits; + if ((mode & want) == 0) { + kal_terminal_set_mode(h, mode | want); + if (kal_terminal_get_mode(h, &mode) != kal_ok) return false; + } + return (mode & want) != 0; +} + +bool color(stream s) { + if (const auto v = env::get("NO_COLOR"); v && !v->empty()) return false; + if (const auto v = env::get("CLICOLOR_FORCE"); v && !v->empty() && *v != "0") return true; + return interactive(s) && sequences(s); +} + +result raw_mode::enter() { + const kal_stream in = kal_stdin(); + kal_uintptr previous = 0; + if (const int rc = kal_terminal_get_mode(in, &previous); rc != kal_ok) + return std::unexpected(from_kal(rc, "reading the terminal's mode")); + const kal_uintptr raw = (kal::terminal::pass_control | kal::terminal::pass_input).bits; + if (const int rc = kal_terminal_set_mode(in, raw); rc != kal_ok) + return std::unexpected(from_kal(rc, "setting the terminal's mode")); + raw_mode r; + r.previous_ = previous; + r.active_ = true; + return r; +} + +raw_mode::raw_mode(raw_mode&& o) noexcept + : previous_(o.previous_), active_(std::exchange(o.active_, false)) {} + +raw_mode& raw_mode::operator=(raw_mode&& o) noexcept { + if (this != &o) { + if (active_) kal_terminal_set_mode(kal_stdin(), static_cast(previous_)); + previous_ = o.previous_; + active_ = std::exchange(o.active_, false); + } + return *this; +} + +raw_mode::~raw_mode() { + if (active_) kal_terminal_set_mode(kal_stdin(), static_cast(previous_)); +} + +} // namespace mcpplibs::os::terminal diff --git a/src/terminal.cppm b/src/terminal.cppm new file mode 100644 index 0000000..0e9991f --- /dev/null +++ b/src/terminal.cppm @@ -0,0 +1,43 @@ +// mcpplibs.os.terminal --- whether a person is there, how wide the display is, +// whether colour will be obeyed, and keystrokes read one at a time. +export module mcpplibs.os.terminal; + +import std; +export import mcpplibs.os.error; + +export namespace mcpplibs::os::terminal { + +enum class stream { in, out, err }; + +// Whether the stream is a terminal. +bool interactive(stream s); + +// The display, in character cells; nothing where it is not known. +struct dimensions { std::size_t columns = 0, rows = 0; }; +std::optional size(stream s = stream::out); + +// Whether the terminal obeys ECMA-48 control sequences written to it (openkal +// 0.17, KAL_TERM_SEQUENCES), asking for it where the system must be asked. +bool sequences(stream s); + +// Whether to write colour: NO_COLOR says never, CLICOLOR_FORCE always, and +// otherwise colour is written to a terminal that obeys the sequences. +bool color(stream s); + +// Keystrokes as they are typed --- no line assembly, no echo, none kept by the +// system, none altered --- until destroyed, when the previous mode returns. +class raw_mode { +public: + static result enter(); + raw_mode() = default; + raw_mode(raw_mode&&) noexcept; + raw_mode& operator=(raw_mode&&) noexcept; + raw_mode(const raw_mode&) = delete; + raw_mode& operator=(const raw_mode&) = delete; + ~raw_mode(); +private: + std::uintptr_t previous_ = 0; + bool active_ = false; +}; + +} // namespace mcpplibs::os::terminal diff --git a/src/windows.cppm b/src/windows.cppm new file mode 100644 index 0000000..7ca4156 --- /dev/null +++ b/src/windows.cppm @@ -0,0 +1,16 @@ +// mcpplibs.os.windows --- what Windows has and the others do not (R5). +export module mcpplibs.os.windows; + +import std; + +export namespace mcpplibs::os::windows { + +// Whether this program holds the administrator's token. Other systems have no +// single equivalent: their privileges are not a token a program holds. +bool elevated(); + +// Whether the directory is on a Dev Drive, which the system scans less and a +// build places its output on; nothing when the system cannot say. +std::optional on_dev_drive(const std::filesystem::path& directory); + +} // namespace mcpplibs::os::windows diff --git a/src/windows/machine.cpp b/src/windows/machine.cpp new file mode 100644 index 0000000..7fc2772 --- /dev/null +++ b/src/windows/machine.cpp @@ -0,0 +1,28 @@ +module; +#include "win32.h" + +module mcpplibs.os.machine; + +import std; + +namespace mcpplibs::os::machine { + +std::string arch() { + unsigned short process = 0, native = 0; + if (!IsWow64Process2(GetCurrentProcess(), &process, &native)) return {}; + switch (native) { + case 0x8664: return "x86_64"; // IMAGE_FILE_MACHINE_AMD64 + case 0xAA64: return "aarch64"; // IMAGE_FILE_MACHINE_ARM64 + case 0x014c: return "x86"; // IMAGE_FILE_MACHINE_I386 + default: return std::format("machine-{:04x}", native); + } +} + +std::optional memory() { + okos_memory_status s{}; + s.length = sizeof s; + if (!GlobalMemoryStatusEx(&s)) return std::nullopt; + return s.total_physical; +} + +} // namespace mcpplibs::os::machine diff --git a/src/windows/win32.h b/src/windows/win32.h new file mode 100644 index 0000000..5c66d9b --- /dev/null +++ b/src/windows/win32.h @@ -0,0 +1,31 @@ +// The Windows functions this package's Windows units call, declared here rather +// than taken from (part 2 §6.2): the units then compile the same +// under the MSVC ABI and under any C environment, and what they reach of the +// system is this list. +#pragma once + +extern "C" { +using okos_bool = int; +using okos_dword = unsigned long; +using okos_handle = void*; + +struct okos_memory_status { + okos_dword length, load; + unsigned long long total_physical, available_physical, total_page_file, + available_page_file, total_virtual, available_virtual, available_extended_virtual; +}; +struct okos_persistent_volume { + okos_dword volume_flags, flag_mask, version, reserved; +}; +struct okos_token_elevation { okos_dword elevated; }; + +__declspec(dllimport) okos_bool __stdcall GlobalMemoryStatusEx(okos_memory_status*); +__declspec(dllimport) okos_handle __stdcall CreateFileW(const wchar_t*, okos_dword, okos_dword, void*, okos_dword, okos_dword, okos_handle); +__declspec(dllimport) okos_bool __stdcall DeviceIoControl(okos_handle, okos_dword, void*, okos_dword, void*, okos_dword, okos_dword*, void*); +__declspec(dllimport) okos_bool __stdcall CloseHandle(okos_handle); +__declspec(dllimport) okos_handle __stdcall GetCurrentProcess(void); +__declspec(dllimport) okos_bool __stdcall IsWow64Process2(okos_handle, unsigned short*, unsigned short*); +__declspec(dllimport) okos_bool __stdcall OpenProcessToken(okos_handle, okos_dword, okos_handle*); +__declspec(dllimport) okos_bool __stdcall GetTokenInformation(okos_handle, int, void*, okos_dword, okos_dword*); +__declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned, okos_dword, const char*, int, wchar_t*, int); +} diff --git a/src/windows/windows.cpp b/src/windows/windows.cpp new file mode 100644 index 0000000..5cc1a6f --- /dev/null +++ b/src/windows/windows.cpp @@ -0,0 +1,40 @@ +module; +#include "win32.h" + +module mcpplibs.os.windows; + +import std; + +namespace mcpplibs::os::windows { + +bool elevated() { + okos_handle token = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), 0x0008 /* TOKEN_QUERY */, &token)) return false; + okos_token_elevation e{}; + okos_dword got = 0; + const bool ok = GetTokenInformation(token, 20 /* TokenElevation */, &e, sizeof e, &got) != 0; + CloseHandle(token); + return ok && e.elevated != 0; +} + +std::optional on_dev_drive(const std::filesystem::path& directory) { + const std::string utf8 = directory.string(); + const int n = MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0); + if (n <= 0) return std::nullopt; + std::wstring wide(static_cast(n), L'\0'); + MultiByteToWideChar(65001, 0, utf8.data(), static_cast(utf8.size()), wide.data(), n); + const okos_handle h = CreateFileW(wide.c_str(), 0x0080 /* FILE_READ_ATTRIBUTES */, 0x7, nullptr, + 3 /* OPEN_EXISTING */, 0x02000000 /* BACKUP_SEMANTICS */, nullptr); + if (h == reinterpret_cast(static_cast(-1)) || h == nullptr) return std::nullopt; + okos_persistent_volume info{}; + info.version = 1; + info.flag_mask = 0x00002000; // PERSISTENT_VOLUME_STATE_DEV_VOLUME + okos_dword returned = 0; + const bool asked = DeviceIoControl(h, 0x0009023C /* FSCTL_QUERY_PERSISTENT_VOLUME_STATE */, + &info, sizeof info, &info, sizeof info, &returned, nullptr) != 0; + CloseHandle(h); + if (!asked) return std::nullopt; + return (info.volume_flags & 0x00002000) != 0; +} + +} // namespace mcpplibs::os::windows diff --git a/tests/check.hpp b/tests/check.hpp new file mode 100644 index 0000000..047557d --- /dev/null +++ b/tests/check.hpp @@ -0,0 +1,8 @@ +// A test reports each observation and exits with the number that did not hold. +#pragma once +#include +inline int g_failures = 0; +inline void check(bool held, const char* what) { + std::printf(" %s %s\n", held ? "held" : "FAIL", what); + if (!held) ++g_failures; +} diff --git a/tests/dirs_terminal_test.cpp b/tests/dirs_terminal_test.cpp new file mode 100644 index 0000000..40149a9 --- /dev/null +++ b/tests/dirs_terminal_test.cpp @@ -0,0 +1,22 @@ +// Conventions of the host, and the terminal's colour policy. +#include "check.hpp" +import std; +import mcpplibs.os; + +namespace os = mcpplibs::os; + +int main() { + check(os::dirs::home().has_value(), "a home directory is named"); + check(os::dirs::config().has_value(), "a configuration directory is named"); + check(os::dirs::cache().has_value(), "a cache directory is named"); + check(!os::dirs::temp().empty(), "a temporary directory is named"); + if constexpr (os::is_linux) { + check(os::dirs::cache()->generic_string().ends_with(".cache") || os::env::get("XDG_CACHE_HOME"), + "Linux follows XDG"); + } + // Not a terminal here (CI); colour follows NO_COLOR and CLICOLOR_FORCE. + const bool forced = os::env::get("CLICOLOR_FORCE").value_or("0") != "0"; + check(os::terminal::color(os::terminal::stream::out) == (forced || os::terminal::interactive(os::terminal::stream::out)), + "colour is written to a terminal or when forced"); + return g_failures; +} diff --git a/tests/fs_test.cpp b/tests/fs_test.cpp new file mode 100644 index 0000000..4d1d08e --- /dev/null +++ b/tests/fs_test.cpp @@ -0,0 +1,60 @@ +// mcpplibs.os.fs upon a scratch directory. +#include "check.hpp" +import std; +import mcpplibs.os; + +namespace os = mcpplibs::os; +namespace stdfs = std::filesystem; + +int main() { + const auto dir = stdfs::temp_directory_path() / std::format("mcpplibs-os-fs-{}", os::process::self()->id); + stdfs::remove_all(dir); + stdfs::create_directories(dir); + + const auto l = os::fs::locate(dir); + check(l.has_value(), "the scratch directory lies beneath a supplied directory"); + + const auto file = dir / "a.txt"; + check(os::fs::write(file, "one").has_value(), "a file is written"); + check(os::fs::read(file).value_or("") == "one", "and read back"); + check(os::fs::write_atomic(file, "two").has_value(), "and replaced all at once"); + check(os::fs::read(file).value_or("") == "two", "and the replacement is read"); + + const auto caps = os::fs::caps(dir); + check(caps.has_value(), "the volume says what it can do"); + const auto pub = dir / "published.txt"; + auto first = os::fs::publish(pub, "first"); + check(first.has_value(), "a name nobody has taken is published"); + if (caps && caps->rename_no_replace) check(first && first->atomic, "and atomically where the volume can"); + auto second = os::fs::publish(pub, "second"); + check(!second && second.error().code == os::errc::exists, "a published name is not replaced"); + check(os::fs::read(pub).value_or("") == "first", "and keeps what was published first"); + + { + auto a = os::fs::lock::acquire(dir / "lock", os::fs::lock::kind::exclusive, false); + check(a.has_value() && a->held(), "an exclusive lock is taken"); + } + auto again = os::fs::lock::acquire(dir / "lock", os::fs::lock::kind::exclusive, false); + check(again.has_value(), "and is taken again once released"); + + const auto s = os::fs::stamp_of(file); + check(s && s->size == 3, "a stamp reports the size"); + const auto i1 = os::fs::identity_of(file); + const auto i2 = os::fs::identity_of(dir / "." / "a.txt"); + check(i1 && i2 && *i1 == *i2, "two names of one node have one identity"); + + if (caps && caps->make_links) { + stdfs::create_directories(dir / "target"); + const auto made = os::fs::create_link("target", dir / "link", true); + check(made.has_value() || made.error().code == os::errc::permission, "a directory link is made where the volume can"); + if (made) { + const auto t = os::fs::read_link(dir / "link"); + check(t && t->find("target") != std::string::npos, "and reads back its target"); + check(stdfs::exists(dir / "link" / "."), "and reaches the directory"); + } + } + check(os::fs::rename(file, dir / "b.txt", false).has_value(), "a rename that does not replace renames to a free name"); + + stdfs::remove_all(dir); + return g_failures; +} diff --git a/tests/ipc_test.cpp b/tests/ipc_test.cpp new file mode 100644 index 0000000..11d8e74 --- /dev/null +++ b/tests/ipc_test.cpp @@ -0,0 +1,48 @@ +// mcpplibs.os.ipc and .net: a listener and a connection in one program. +#include "check.hpp" +import std; +import mcpplibs.os; + +namespace os = mcpplibs::os; + +int main() { + const auto dir = std::filesystem::temp_directory_path(); + auto caps = os::fs::caps(dir); + if (caps && caps->local_endpoint) { + const auto name = std::format("os-ipc-{}", os::process::self()->id); + auto l = os::ipc::listener::listen(dir, name); + check(l.has_value(), "a listener is opened at a name"); + if (l) { + std::jthread server([&] { + auto c = l->accept(); + if (!c) return; + auto got = c->read(); + if (got) (void)c->write("re:" + *got); + }); + auto c = os::ipc::connection::connect(dir, name); + check(c.has_value(), "a connection is made at the name"); + if (c) { + check(c->write("ping").has_value(), "bytes are written"); + auto r = c->read(); + check(r && *r == "re:ping", "and the answer is read"); + } + } + auto inst = os::ipc::single_instance(name + "-single", dir); + check(inst && std::holds_alternative(*inst), "the first instance listens"); + auto second = os::ipc::single_instance(name + "-single", dir); + check(second && std::holds_alternative(*second), "a second one connects to it"); + } else { + std::printf(" (the temporary directory's volume holds no local endpoint)\n"); + } + { + auto l = os::net::loopback_listener::listen(); + check(l && l->port() != 0, "a loopback listener is given a port"); + if (l) { + std::jthread server([&] { auto c = l->accept(); if (c) (void)c->write("hi"); }); + auto c = os::net::connect_loopback(l->port()); + auto r = c ? c->read() : os::result{}; + check(r && *r == "hi", "and a loopback connection reads what was written"); + } + } + return g_failures; +} diff --git a/tests/platform_test.cpp b/tests/platform_test.cpp new file mode 100644 index 0000000..2e7e2a5 --- /dev/null +++ b/tests/platform_test.cpp @@ -0,0 +1,27 @@ +// The compile-time constants, and the host they agree with (R3; part 2 E29). +#include "check.hpp" +import std; +import mcpplibs.os; + +namespace os = mcpplibs::os; + +// A constant expression on a native target: these are compiled, not run. +static_assert(os::host() == os::PLATFORM); +static_assert(os::is_linux + os::is_macos + os::is_windows == 1); +static_assert(os::is_posix == (os::is_linux || os::is_macos)); +static_assert(!os::is_image); +static_assert(os::exe_suffix == (os::is_windows ? ".exe" : "")); + +int main() { + if constexpr (os::host() == os::platform::windows) { + check(os::path_list_separator == ';', "a Windows target separates PATH with ';'"); + } else { + check(os::path_list_separator == ':', "a POSIX target separates PATH with ':'"); + } + check(os::conventions().exe_suffix == os::exe_suffix, "the host's conventions are the target's"); + const auto system = os::env::system(); + check(system && *system == os::name(os::host()), "openkal names the system the program runs upon"); + check(!os::machine::arch().empty(), "the machine's architecture is named"); + check(os::machine::memory().value_or(0) > 0, "the machine's memory is known"); + return g_failures; +} diff --git a/tests/process_test.cpp b/tests/process_test.cpp new file mode 100644 index 0000000..c029c63 --- /dev/null +++ b/tests/process_test.cpp @@ -0,0 +1,117 @@ +// mcpplibs.os.process: this test starts copies of itself. +#include "check.hpp" +import std; +import mcpplibs.os; + +namespace os = mcpplibs::os; + +int child(std::string_view errand) { + if (errand == "echo") { + auto in = os::stdio::read_exactly(5); + (void)os::stdio::write_output(in.value_or("")); + (void)os::stdio::write_error("err"); + return 7; + } + if (errand == "env") { + (void)os::stdio::write_output(os::env::get("OS_TEST_VALUE").value_or("")); + return 0; + } + if (errand == "cwd") { + (void)os::stdio::write_output(std::filesystem::current_path().filename().string()); + return 0; + } + if (errand == "sleep") { + std::this_thread::sleep_for(std::chrono::seconds(30)); + return 0; + } + if (errand == "interrupt") { + (void)os::stdio::write_output(os::interrupt::available() ? "armed\n" : "none\n"); + os::interrupt::wait(std::chrono::seconds(20)); + os::interrupt::end_as_requested(); + return 3; + } + return 99; +} + +int main(int argc, char** argv) { + if (argc > 2 && std::string_view(argv[1]) == "--child") return child(argv[2]); + const auto self = os::env::image(); + check(self.has_value(), "this program's image is named"); + if (!self) return g_failures; + + { + os::process::spawn_options o; + o.program = *self; + o.args = { "--child", "echo" }; + auto r = os::process::run(o, "hello"); + check(r && r->out == "hello", "a started program reads its input and writes its output"); + check(r && r->err == "err", "and its error output is collected beside"); + check(r && r->status.code == 7 && !r->status.by_environment, "and its status is the one it returned"); + } + { + os::process::spawn_options o; + o.program = *self; + o.args = { "--child", "env" }; + o.env = { { "OS_TEST_VALUE", std::string("set") } }; + auto r = os::process::run(o); + check(r && r->out == "set", "an environment override reaches the program"); + o.env = { { "OS_TEST_VALUE", std::nullopt } }; + r = os::process::run(o); + check(r && r->out == "", "and a removal removes"); + } + { + const auto dir = std::filesystem::temp_directory_path() / "mcpplibs-os-cwd-probe"; + std::filesystem::create_directories(dir); + os::process::spawn_options o; + o.program = *self; + o.args = { "--child", "cwd" }; + o.cwd = dir; + auto r = os::process::run(o); + check(r && r->out == "mcpplibs-os-cwd-probe", "a program runs in the directory it is given"); + } + { + os::process::spawn_options o; + o.program = *self; + o.args = { "--child", "sleep" }; + o.out = os::process::stdio::null; + o.err = os::process::stdio::null; + o.own_unit = true; + auto p = os::process::process::spawn(o); + check(p.has_value(), "a program is started in a unit of its own"); + if (p) { + check(p->id() != 0, "and has an identifier"); + auto d = os::process::describe(p->id()); + check(d && !d->ended, "which another program can ask about"); + auto w = p->wait_for(std::chrono::milliseconds(50)); + check(w && !w->has_value(), "a wait with a bound expires while it runs"); + p->kill(); + auto st = p->wait(); + check(st && !st->success(), "and the unit, ended, does not report success"); + } + } + if (os::interrupt::available()) { + os::process::spawn_options o; + o.program = *self; + o.args = { "--child", "interrupt" }; + auto p = os::process::process::spawn(o); + if (p) { + auto first = p->read_output(); + check(first && first->starts_with("armed"), "a started program takes requests to end over"); + p->terminate(); + auto st = p->wait(); + if constexpr (os::is_windows) { + check(st.has_value(), "a program asked to end is awaited"); + } else { + check(st && st->by_environment, "and ends as the request would have ended it"); + } + } + } + { + auto me = os::process::self(); + check(me && me->id != 0, "this program has an identifier"); + check(me && os::process::alive(*me), "and is alive"); + } + check(os::env::find_executable(self->filename().string(), self->parent_path().string()).has_value(), + "a program is found by name in a list of directories"); + return g_failures; +} From ae987ce8a89e4745c7fb8f7339956b284560a17a Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 26 Oct 2026 20:07:08 +0900 Subject: [PATCH 02/17] the target partitions are committed: src/targets, which the ignore of build output no longer covers Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .gitignore | 3 ++- mcpp.toml | 6 +++--- src/platform.cppm | 2 +- src/targets/linux.cppm | 19 +++++++++++++++++++ src/targets/macos.cppm | 19 +++++++++++++++++++ src/targets/openkal.cppm | 18 ++++++++++++++++++ src/targets/windows.cppm | 19 +++++++++++++++++++ 7 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 src/targets/linux.cppm create mode 100644 src/targets/macos.cppm create mode 100644 src/targets/openkal.cppm create mode 100644 src/targets/windows.cppm diff --git a/.gitignore b/.gitignore index 2365ae7..1be343c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -target/ +/target/ +/checks/*/target/ compile_commands.json *.log mcpp.lock diff --git a/mcpp.toml b/mcpp.toml index 56148d2..0f6d628 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -35,10 +35,10 @@ kind = "lib" sources = ["src/*.cppm", "src/*.cpp"] [target.linux.build] -sources = ["src/target/linux.cppm", "src/linux/*.cpp", "src/posix/*.cpp"] +sources = ["src/targets/linux.cppm", "src/linux/*.cpp", "src/posix/*.cpp"] [target.macos.build] -sources = ["src/target/macos.cppm", "src/macos/*.cpp", "src/posix/*.cpp"] +sources = ["src/targets/macos.cppm", "src/macos/*.cpp", "src/posix/*.cpp"] [target.windows.build] -sources = ["src/target/windows.cppm", "src/windows/*.cpp"] +sources = ["src/targets/windows.cppm", "src/windows/*.cpp"] diff --git a/src/platform.cppm b/src/platform.cppm index 032f3df..97ff72d 100644 --- a/src/platform.cppm +++ b/src/platform.cppm @@ -3,7 +3,7 @@ // The compile-time constants are the first-class interface (design part 2, // R3): a program written for several systems dispatches with `if constexpr`, // and a branch for another system is discarded rather than compiled. They come -// from one partition per target (src/target/.cppm), which the manifest +// from one partition per target (src/targets/.cppm), which the manifest // selects, so no macro reaches this interface. // // `host()' names the system the program RUNS upon. Built for a system, that is diff --git a/src/targets/linux.cppm b/src/targets/linux.cppm new file mode 100644 index 0000000..1eb70be --- /dev/null +++ b/src/targets/linux.cppm @@ -0,0 +1,19 @@ +// The target of this build: linux. Selected by the manifest's [target.linux.build]. +export module mcpplibs.os.platform:target; + +import std; + +export namespace mcpplibs::os { + +enum class platform { linux, macos, windows, openkal }; + +// What the program is built for. +inline constexpr platform PLATFORM = platform::linux; + +// What the program runs upon: on a native target, what it was built for. +constexpr platform host() noexcept { return PLATFORM; } + +inline constexpr std::string_view exe_suffix = PLATFORM == platform::windows ? ".exe" : ""; +inline constexpr char path_list_separator = PLATFORM == platform::windows ? ';' : ':'; + +} // namespace mcpplibs::os diff --git a/src/targets/macos.cppm b/src/targets/macos.cppm new file mode 100644 index 0000000..5c5428f --- /dev/null +++ b/src/targets/macos.cppm @@ -0,0 +1,19 @@ +// The target of this build: macos. Selected by the manifest's [target.macos.build]. +export module mcpplibs.os.platform:target; + +import std; + +export namespace mcpplibs::os { + +enum class platform { linux, macos, windows, openkal }; + +// What the program is built for. +inline constexpr platform PLATFORM = platform::macos; + +// What the program runs upon: on a native target, what it was built for. +constexpr platform host() noexcept { return PLATFORM; } + +inline constexpr std::string_view exe_suffix = PLATFORM == platform::windows ? ".exe" : ""; +inline constexpr char path_list_separator = PLATFORM == platform::windows ? ';' : ':'; + +} // namespace mcpplibs::os diff --git a/src/targets/openkal.cppm b/src/targets/openkal.cppm new file mode 100644 index 0000000..23319b9 --- /dev/null +++ b/src/targets/openkal.cppm @@ -0,0 +1,18 @@ +// The target of this build: an image (`-openkal'), which runs upon any +// system its architecture has a loader for. Selected by [target.openkal.build]. +export module mcpplibs.os.platform:target; + +import std; + +export namespace mcpplibs::os { + +enum class platform { linux, macos, windows, openkal }; + +inline constexpr platform PLATFORM = platform::openkal; + +// Known when the image is loaded and not before. An ordinary declaration, so +// that a constant expression using it is an error naming it (part 2, E29); +// `if consteval' would say the same and is refused by one compiler (E28). +platform host() noexcept; + +} // namespace mcpplibs::os diff --git a/src/targets/windows.cppm b/src/targets/windows.cppm new file mode 100644 index 0000000..af7b0e1 --- /dev/null +++ b/src/targets/windows.cppm @@ -0,0 +1,19 @@ +// The target of this build: windows. Selected by the manifest's [target.windows.build]. +export module mcpplibs.os.platform:target; + +import std; + +export namespace mcpplibs::os { + +enum class platform { linux, macos, windows, openkal }; + +// What the program is built for. +inline constexpr platform PLATFORM = platform::windows; + +// What the program runs upon: on a native target, what it was built for. +constexpr platform host() noexcept { return PLATFORM; } + +inline constexpr std::string_view exe_suffix = PLATFORM == platform::windows ? ".exe" : ""; +inline constexpr char path_list_separator = PLATFORM == platform::windows ? ';' : ':'; + +} // namespace mcpplibs::os From b82f4450196279de70282c06950d9be1ab9418d5 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 26 Oct 2026 20:15:12 +0900 Subject: [PATCH 03/17] windows: advapi32 reached at run time, so the package links nothing beyond kernel32 Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- src/windows/win32.h | 6 ++++-- src/windows/windows.cpp | 11 +++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/windows/win32.h b/src/windows/win32.h index 5c66d9b..505bdd9 100644 --- a/src/windows/win32.h +++ b/src/windows/win32.h @@ -25,7 +25,9 @@ __declspec(dllimport) okos_bool __stdcall DeviceIoControl(okos_handle, okos_dwor __declspec(dllimport) okos_bool __stdcall CloseHandle(okos_handle); __declspec(dllimport) okos_handle __stdcall GetCurrentProcess(void); __declspec(dllimport) okos_bool __stdcall IsWow64Process2(okos_handle, unsigned short*, unsigned short*); -__declspec(dllimport) okos_bool __stdcall OpenProcessToken(okos_handle, okos_dword, okos_handle*); -__declspec(dllimport) okos_bool __stdcall GetTokenInformation(okos_handle, int, void*, okos_dword, okos_dword*); +// advapi32 is reached at run time, so that a program linking this package +// links nothing beyond kernel32 for a question few programs ask. +__declspec(dllimport) okos_handle __stdcall LoadLibraryW(const wchar_t*); +__declspec(dllimport) void* __stdcall GetProcAddress(okos_handle, const char*); __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned, okos_dword, const char*, int, wchar_t*, int); } diff --git a/src/windows/windows.cpp b/src/windows/windows.cpp index 5cc1a6f..38c9e2b 100644 --- a/src/windows/windows.cpp +++ b/src/windows/windows.cpp @@ -8,11 +8,18 @@ import std; namespace mcpplibs::os::windows { bool elevated() { + using open_token = okos_bool (__stdcall*)(okos_handle, okos_dword, okos_handle*); + using token_information = okos_bool (__stdcall*)(okos_handle, int, void*, okos_dword, okos_dword*); + const okos_handle advapi = LoadLibraryW(L"advapi32.dll"); + if (!advapi) return false; + const auto open = reinterpret_cast(GetProcAddress(advapi, "OpenProcessToken")); + const auto query = reinterpret_cast(GetProcAddress(advapi, "GetTokenInformation")); + if (!open || !query) return false; okos_handle token = nullptr; - if (!OpenProcessToken(GetCurrentProcess(), 0x0008 /* TOKEN_QUERY */, &token)) return false; + if (!open(GetCurrentProcess(), 0x0008 /* TOKEN_QUERY */, &token)) return false; okos_token_elevation e{}; okos_dword got = 0; - const bool ok = GetTokenInformation(token, 20 /* TokenElevation */, &e, sizeof e, &got) != 0; + const bool ok = query(token, 20 /* TokenElevation */, &e, sizeof e, &got) != 0; CloseHandle(token); return ok && e.elevated != 0; } From 0d1c836ad863caa4bd5c1c9c37e94675b93136c0 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 26 Oct 2026 20:16:20 +0900 Subject: [PATCH 04/17] platform: ARCH, the architecture of the build, from a partition the manifest selects Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- README.md | 2 +- mcpp.toml | 10 ++++++++++ src/arch/aarch64.cppm | 13 +++++++++++++ src/arch/riscv64.cppm | 13 +++++++++++++ src/arch/x86_64.cppm | 13 +++++++++++++ src/platform.cppm | 10 ++++++++++ tests/platform_test.cpp | 1 + 7 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/arch/aarch64.cppm create mode 100644 src/arch/riscv64.cppm create mode 100644 src/arch/x86_64.cppm diff --git a/README.md b/README.md index 53a7bf6..3f135d4 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ modules. | Module | Contents | |---|---| -| `mcpplibs.os.platform` | `PLATFORM`, `is_linux` / `is_macos` / `is_windows` / `is_posix`, `exe_suffix`, `path_list_separator`; `host()`, a constant expression on a native target | +| `mcpplibs.os.platform` | `PLATFORM`, `ARCH`, `is_linux` / `is_macos` / `is_windows` / `is_posix`, `exe_suffix`, `path_list_separator`; `host()`, a constant expression on a native target | | `mcpplibs.os.error` | `errc` (openkal's closed set of conditions), `error`, `result` | | `mcpplibs.os.env` | variables, arguments, the program's own image, PATH search with PATHEXT | | `mcpplibs.os.fs` | `write_atomic`, `publish` (no replace), `caps`, `lock`, links, `set_executable`, `stamp_of`, `identity_of`, `locate` | diff --git a/mcpp.toml b/mcpp.toml index 0f6d628..bb172cd 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -34,6 +34,16 @@ kind = "lib" [build] sources = ["src/*.cppm", "src/*.cpp"] +# The architecture of the build, as a constant beside the platform's. +[target.'cfg(arch = "x86_64")'.build] +sources = ["src/arch/x86_64.cppm"] + +[target.'cfg(arch = "aarch64")'.build] +sources = ["src/arch/aarch64.cppm"] + +[target.'cfg(arch = "riscv64")'.build] +sources = ["src/arch/riscv64.cppm"] + [target.linux.build] sources = ["src/targets/linux.cppm", "src/linux/*.cpp", "src/posix/*.cpp"] diff --git a/src/arch/aarch64.cppm b/src/arch/aarch64.cppm new file mode 100644 index 0000000..5f23e09 --- /dev/null +++ b/src/arch/aarch64.cppm @@ -0,0 +1,13 @@ +// The architecture of this build: aarch64. Selected by the manifest's +// [target.'cfg(arch = "aarch64")'.build]. +export module mcpplibs.os.platform:arch; + +export namespace mcpplibs::os { + +enum class architecture { x86_64, aarch64, riscv64 }; + +// What the program is built for. The machine it runs upon may differ (a +// translated program); `machine::arch()' answers that. +inline constexpr architecture ARCH = architecture::aarch64; + +} // namespace mcpplibs::os diff --git a/src/arch/riscv64.cppm b/src/arch/riscv64.cppm new file mode 100644 index 0000000..3881dcb --- /dev/null +++ b/src/arch/riscv64.cppm @@ -0,0 +1,13 @@ +// The architecture of this build: riscv64. Selected by the manifest's +// [target.'cfg(arch = "riscv64")'.build]. +export module mcpplibs.os.platform:arch; + +export namespace mcpplibs::os { + +enum class architecture { x86_64, aarch64, riscv64 }; + +// What the program is built for. The machine it runs upon may differ (a +// translated program); `machine::arch()' answers that. +inline constexpr architecture ARCH = architecture::riscv64; + +} // namespace mcpplibs::os diff --git a/src/arch/x86_64.cppm b/src/arch/x86_64.cppm new file mode 100644 index 0000000..d9ce4ec --- /dev/null +++ b/src/arch/x86_64.cppm @@ -0,0 +1,13 @@ +// The architecture of this build: x86_64. Selected by the manifest's +// [target.'cfg(arch = "x86_64")'.build]. +export module mcpplibs.os.platform:arch; + +export namespace mcpplibs::os { + +enum class architecture { x86_64, aarch64, riscv64 }; + +// What the program is built for. The machine it runs upon may differ (a +// translated program); `machine::arch()' answers that. +inline constexpr architecture ARCH = architecture::x86_64; + +} // namespace mcpplibs::os diff --git a/src/platform.cppm b/src/platform.cppm index 97ff72d..d5de46a 100644 --- a/src/platform.cppm +++ b/src/platform.cppm @@ -15,6 +15,7 @@ export module mcpplibs.os.platform; import std; export import :target; +export import :arch; export namespace mcpplibs::os { @@ -39,6 +40,15 @@ constexpr std::string_view name(platform p) noexcept { return "unknown"; } +constexpr std::string_view name(architecture a) noexcept { + switch (a) { + case architecture::x86_64: return "x86_64"; + case architecture::aarch64: return "aarch64"; + case architecture::riscv64: return "riscv64"; + } + return "unknown"; +} + // The conventions a running program follows: the executable suffix, the list // separator, whether names are compared without case by default. On a native // target they are the target's; in an image they are the host's, chosen by diff --git a/tests/platform_test.cpp b/tests/platform_test.cpp index 2e7e2a5..672b16e 100644 --- a/tests/platform_test.cpp +++ b/tests/platform_test.cpp @@ -10,6 +10,7 @@ static_assert(os::host() == os::PLATFORM); static_assert(os::is_linux + os::is_macos + os::is_windows == 1); static_assert(os::is_posix == (os::is_linux || os::is_macos)); static_assert(!os::is_image); +static_assert(os::name(os::ARCH) == "x86_64" || os::name(os::ARCH) == "aarch64" || os::name(os::ARCH) == "riscv64"); static_assert(os::exe_suffix == (os::is_windows ? ".exe" : "")); int main() { From 4bd017513cdabeb4ba067a6292e0ac1b184590d3 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 26 Oct 2026 22:40:19 +0900 Subject: [PATCH 05/17] the ok-image target: host() when the image is loaded, env::loader and env::self_command, and the tests run as images on four systems Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 109 ++++++++++++++++++++++++++++++++++ .gitignore | 1 + README.md | 15 +++++ checks/constants/mcpp.toml | 10 ++++ checks/constants/src/main.cpp | 17 ++++++ mcpp.toml | 14 +++++ src/env.cpp | 15 +++++ src/env.cppm | 10 ++++ src/openkal/machine.cpp | 13 ++++ src/openkal/openkal.cpp | 32 ++++++++++ tests/platform_test.cpp | 28 ++++----- tests/process_test.cpp | 32 +++++----- tools/image-graph.sh | 23 +++++++ 13 files changed, 290 insertions(+), 29 deletions(-) create mode 100644 checks/constants/mcpp.toml create mode 100644 checks/constants/src/main.cpp create mode 100644 src/openkal/machine.cpp create mode 100644 src/openkal/openkal.cpp create mode 100755 tools/image-graph.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa3a43a..77f7c4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,6 +103,9 @@ jobs: grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not run or did not pass"; exit 1; } done + - name: The compile-time constants of a native target (R3) + run: ( cd checks/constants && mcpp build ) || { echo "::error::a native constant does not hold"; exit 1; } + - name: A platform module is a link error outside its platform, and links in a discarded branch (R5) run: | ( cd checks/discarded && mcpp build ) || { echo "::error::a discarded use of a platform module did not link"; exit 1; } @@ -113,3 +116,109 @@ jobs: fi grep -Eiq "undefined|unresolved" misuse.log || { cat misuse.log; echo "::error::the misuse failed for another reason than an undefined name"; exit 1; } echo " ok the misuse fails at the link, naming what is undefined" + + # ── THE TESTS AS OK IMAGES (`-openkal`, openkal SPEC clause 12) ────── + # + # The same five suites, built once on Linux for both architectures and run + # through okrun on four systems: `os::host()` is known only when an image is + # loaded, the conventions follow it, and a test that starts copies of itself + # starts the loader and the image (`os::env::self_command`). + images: + name: tests as ok images (built once) + runs-on: ubuntu-24.04 + timeout-minutes: 120 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - name: Install xlings + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ + | bash -s "$XLINGS_VERSION" + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + - name: The mcpp that has the target + run: | + xlings update > /dev/null 2>&1 || true + xlings install mcpp -y -g + mcpp self config --mirror GLOBAL + src="$RUNNER_TEMP/mcpp-src" + git clone --quiet --depth 1 --branch "$MCPP_SOURCE_REF" https://github.com/mcpp-community/mcpp.git "$src" + rm -f "$src/.xlings.json" + ( cd "$src" && mcpp build --release ) + built=$(find "$src/target" -type f -name mcpp | head -1) + [ -n "$built" ] || { echo "::error::mcpp did not build"; exit 1; } + echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" + - name: The run-time and the stack beneath it, from the same-named branch + run: bash tools/image-graph.sh openkal-0.17.0 + - name: The suites, run here and built for the other architecture + run: | + set -euo pipefail + mcpp test --target x86_64-openkal 2>&1 | tee tests.log + for name in platform_test fs_test process_test ipc_test dirs_terminal_test; do + grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not pass as an image"; exit 1; } + done + mcpp test --target aarch64-openkal --no-run + for arch in x86_64 aarch64; do + mkdir -p "out/$arch" + cp target/"$arch"-openkal/*/bin/*_test.oke "out/$arch/" + done + (cd out && sha256sum */* | tee SHA256SUMS) + - uses: actions/upload-artifact@v4 + with: { name: os-test-images, path: out } + + images-run: + name: tests as ok images on ${{ matrix.host }} + needs: images + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - { host: linux-x86_64, os: ubuntu-24.04, arch: x86_64 } + - { host: linux-aarch64, os: ubuntu-24.04-arm, arch: aarch64 } + - { host: macos-arm64, os: macos-15, arch: aarch64 } + - { host: windows-x86_64, os: windows-2022, arch: x86_64 } + defaults: + run: + shell: bash + steps: + - uses: actions/download-artifact@v4 + with: { name: os-test-images, path: out } + - name: Install xlings (Unix) + if: runner.os != 'Windows' + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh \ + | bash -s "$XLINGS_VERSION" + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + - name: Install xlings (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + irm https://d2learn.org/xlings-install.ps1.txt | iex + "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: okrun, built here + run: | + xlings install mcpp -y -g + mcpp self config --mirror GLOBAL + mcpp toolchain install llvm 23.1.3 + mcpp toolchain default llvm@23.1.3 + git clone --quiet --depth 1 --branch openkal-0.17.0 https://github.com/mcpplibs/openkal-image.git "$RUNNER_TEMP/oi" + ( cd "$RUNNER_TEMP/oi/tools" && mcpp build --release ) + okrun=$(find "$RUNNER_TEMP/oi/target" -path '*bin*' \( -name okrun -o -name okrun.exe \) -type f | head -1) + [ -n "$okrun" ] || { echo "::error::okrun did not build"; exit 1; } + echo "OKRUN=$okrun" >> "$GITHUB_ENV" + - name: The same bytes, every suite + run: | + set -euo pipefail + a='${{ matrix.arch }}' + if command -v sha256sum > /dev/null; then (cd out && sha256sum -c SHA256SUMS) + else (cd out && shasum -a 256 -c SHA256SUMS); fi + failed=0 + for t in out/"$a"/*_test.oke; do + echo "::group::$t" + if "$OKRUN" "$t"; then echo " ok $t"; else echo "::error::$t"; failed=1; fi + echo "::endgroup::" + done + exit $failed diff --git a/.gitignore b/.gitignore index 1be343c..1c81ff9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ compile_commands.json *.log mcpp.lock +/.runtime/ diff --git a/README.md b/README.md index 3f135d4..922f4b9 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,21 @@ Interface units import `std` and contain no system header and no macro. Implemen on openkal import only `openkal.*`. The Windows units declare the functions they call themselves (`src/windows/win32.h`). +## As an ok image + +Built for `-openkal` (openkal SPEC clause 12), a program over this library +is one image per architecture that `okrun` runs on Linux, macOS and Windows. +`os::PLATFORM` is `platform::openkal` and `os::is_image` is true; `os::host()` +is an ordinary function there, answered when the image is loaded, and +`os::conventions()` follows it; `exe_suffix` and `path_list_separator` are not +provided, so code that reads them is a compile error rather than a wrong answer +on another system. `os::env::loader()` names the program that runs the image, +and `os::env::self_command()` is what starts the same program again --- a +loader and an image, or the image alone when it is built for its system. The +image carries its C library and C++ run-time (`openkal-llvm-runtime`); the +tests are built as images in CI and run through okrun on four systems, and +`checks/constants` holds the compile-time facts of a native target. + ## License Apache-2.0 diff --git a/checks/constants/mcpp.toml b/checks/constants/mcpp.toml new file mode 100644 index 0000000..7774ced --- /dev/null +++ b/checks/constants/mcpp.toml @@ -0,0 +1,10 @@ +# The compile-time constants of a native target (R3; part 2 E29), checked when +# the program is compiled. An ok image has neither `exe_suffix' nor a constant +# `host()', so these facts are a build of their own on the native rows rather +# than part of tests/platform_test.cpp, which is also built as an image. +[package] +name = "os-check-constants" +version = "0.1.0" + +[dependencies] +os = { path = "../.." } diff --git a/checks/constants/src/main.cpp b/checks/constants/src/main.cpp new file mode 100644 index 0000000..c358e41 --- /dev/null +++ b/checks/constants/src/main.cpp @@ -0,0 +1,17 @@ +// Built on every native row: each fact is a constant expression there. +import std; +import mcpplibs.os; + +namespace os = mcpplibs::os; + +static_assert(os::host() == os::PLATFORM); +static_assert(os::is_linux + os::is_macos + os::is_windows == 1); +static_assert(!os::is_image); +static_assert(os::exe_suffix == (os::is_windows ? ".exe" : "")); +static_assert(os::path_list_separator == (os::is_windows ? ';' : ':')); +static_assert(os::conventions_of(os::host()).exe_suffix == os::exe_suffix); + +int main() { + if constexpr (os::host() == os::platform::windows) return os::path_list_separator == ';' ? 0 : 1; + else return os::path_list_separator == ':' ? 0 : 1; +} diff --git a/mcpp.toml b/mcpp.toml index bb172cd..b6be4aa 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -52,3 +52,17 @@ sources = ["src/targets/macos.cppm", "src/macos/*.cpp", "src/posix/*.cpp"] [target.windows.build] sources = ["src/targets/windows.cppm", "src/windows/*.cpp"] + +# AN OK IMAGE (`-openkal', openkal SPEC clause 12): no implementation +# beneath --- the loader binds the image's `kal_*' --- and no platform module +# defined. `host()' and the conventions are answered when the image is loaded. +[target.'cfg(os = "openkal")'.build] +sources = ["src/targets/openkal.cppm", "src/openkal/*.cpp"] + +# The tests of this package, built as images, carry the C library and the C++ +# run-time an image holds, and the loader that runs them. +[target.'cfg(os = "openkal")'.dev-dependencies] +openkal-llvm-runtime = { git = "https://github.com/mcpplibs/openkal-llvm-runtime.git", branch = "openkal-0.17.0" } + +[target.'cfg(os = "openkal")'.build-dependencies] +openkal-tools = { git = "https://github.com/mcpplibs/openkal-image.git", branch = "openkal-0.17.0", tools = ["okld", "okrun"] } diff --git a/src/env.cpp b/src/env.cpp index 8d485d4..18e26d2 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -51,6 +51,21 @@ std::optional image() { return std::filesystem::path(*s); } +std::optional loader() { + auto s = counted([](char* o, kal_uintptr cap) { return kal_env_loader(o, cap); }); + if (!s || s->empty()) return std::nullopt; + return std::filesystem::path(*s); +} + +std::vector self_command() { + std::vector out; + const auto program = loader(); + const auto own = image(); + if (program) out.push_back(program->string()); + if (own && (!program || *own != *program)) out.push_back(own->string()); + return out; +} + std::optional system() { return counted([](char* o, kal_uintptr cap) { return kal_env_system(o, cap); }); } diff --git a/src/env.cppm b/src/env.cppm index 4fc738e..878afac 100644 --- a/src/env.cppm +++ b/src/env.cppm @@ -25,6 +25,16 @@ std::vector arguments(); // `kal_env_image'); for an image started by a loader, the image's. std::optional image(); +// The program that loaded this image and runs it, where this program is an ok +// image (`-openkal'; openkal's `kal_env_loader'); nothing for a program +// built for its environment. +std::optional loader(); + +// What starts this program again, before its own arguments: its image; in an ok +// image, the loader and then the image --- or the loader alone, where the image +// is carried inside it (a self-contained program, M3). +std::vector self_command(); + // The system the program runs upon, by name (`linux', `macos', `windows'). // For conventions only, as openkal restricts it. std::optional system(); diff --git a/src/openkal/machine.cpp b/src/openkal/machine.cpp new file mode 100644 index 0000000..fefd9c6 --- /dev/null +++ b/src/openkal/machine.cpp @@ -0,0 +1,13 @@ +// mcpplibs.os.machine in an ok image: see src/openkal/openkal.cpp. +module mcpplibs.os.machine; + +import std; +import mcpplibs.os.platform; + +namespace mcpplibs::os::machine { + +std::string arch() { return std::string(name(ARCH)); } + +std::optional memory() { return std::nullopt; } + +} // namespace mcpplibs::os::machine diff --git a/src/openkal/openkal.cpp b/src/openkal/openkal.cpp new file mode 100644 index 0000000..66daf3b --- /dev/null +++ b/src/openkal/openkal.cpp @@ -0,0 +1,32 @@ +// mcpplibs.os in an ok image (`-openkal'): what the native targets know +// when they are compiled, an image learns when it is loaded. +// +// host() the system the loader runs upon, by the name openkal +// gives it (`kal_env_system', SPEC clause 11 entry 30) --- +// for conventions only, as openkal restricts it; +// machine::arch() the architecture the image was built for, which is the +// one it runs upon: a loader runs images of its own; +// machine::memory() not known: no openkal operation reports it, and an +// answer guessed here would be wrong somewhere. +module; +#include + +module mcpplibs.os.platform; + +import std; + +namespace mcpplibs::os { + +platform host() noexcept { + static const platform answer = [] { + char name[32] = {}; + const kal_intptr n = kal_env_system(name, sizeof name - 1); + const std::string_view s(name, n > 0 ? static_cast(n) : 0); + if (s == "windows") return platform::windows; + if (s == "macos") return platform::macos; + return platform::linux; + }(); + return answer; +} + +} // namespace mcpplibs::os diff --git a/tests/platform_test.cpp b/tests/platform_test.cpp index 672b16e..8a4842a 100644 --- a/tests/platform_test.cpp +++ b/tests/platform_test.cpp @@ -1,28 +1,28 @@ -// The compile-time constants, and the host they agree with (R3; part 2 E29). +// The platform a program is built for and the host it runs upon (R3; part 2 +// E29). Built for a system the host is a constant expression; built as an ok +// image it is known when the image is loaded. The native rows also build +// checks/constants, where the compile-time facts are static assertions. #include "check.hpp" import std; import mcpplibs.os; namespace os = mcpplibs::os; -// A constant expression on a native target: these are compiled, not run. -static_assert(os::host() == os::PLATFORM); -static_assert(os::is_linux + os::is_macos + os::is_windows == 1); +static_assert(os::is_linux + os::is_macos + os::is_windows + os::is_image == 1); static_assert(os::is_posix == (os::is_linux || os::is_macos)); -static_assert(!os::is_image); static_assert(os::name(os::ARCH) == "x86_64" || os::name(os::ARCH) == "aarch64" || os::name(os::ARCH) == "riscv64"); -static_assert(os::exe_suffix == (os::is_windows ? ".exe" : "")); int main() { - if constexpr (os::host() == os::platform::windows) { - check(os::path_list_separator == ';', "a Windows target separates PATH with ';'"); - } else { - check(os::path_list_separator == ':', "a POSIX target separates PATH with ':'"); - } - check(os::conventions().exe_suffix == os::exe_suffix, "the host's conventions are the target's"); + const auto host = os::host(); + check(host != os::platform::openkal, "the host is a system, whatever the target"); + if constexpr (!os::is_image) check(host == os::PLATFORM, "built for a system, the host is that system"); + else check(os::env::loader().has_value(), "an image is told the program that runs it"); + check(os::conventions().exe_suffix == (host == os::platform::windows ? ".exe" : ""), + "the host's conventions are the host's"); const auto system = os::env::system(); - check(system && *system == os::name(os::host()), "openkal names the system the program runs upon"); + check(system && *system == os::name(host), "openkal names the system the program runs upon"); check(!os::machine::arch().empty(), "the machine's architecture is named"); - check(os::machine::memory().value_or(0) > 0, "the machine's memory is known"); + if constexpr (!os::is_image) + check(os::machine::memory().value_or(0) > 0, "the machine's memory is known"); return g_failures; } diff --git a/tests/process_test.cpp b/tests/process_test.cpp index c029c63..125c4c3 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -38,20 +38,28 @@ int main(int argc, char** argv) { const auto self = os::env::image(); check(self.has_value(), "this program's image is named"); if (!self) return g_failures; + // What starts this program again: its image, or in an ok image the loader + // and the image. + const auto again = os::env::self_command(); + check(!again.empty(), "what starts this program again is known"); + auto copy = [&](std::string errand) { + os::process::spawn_options o; + o.program = again.front(); + o.args.assign(again.begin() + 1, again.end()); + o.args.push_back("--child"); + o.args.push_back(std::move(errand)); + return o; + }; { - os::process::spawn_options o; - o.program = *self; - o.args = { "--child", "echo" }; + auto o = copy("echo"); auto r = os::process::run(o, "hello"); check(r && r->out == "hello", "a started program reads its input and writes its output"); check(r && r->err == "err", "and its error output is collected beside"); check(r && r->status.code == 7 && !r->status.by_environment, "and its status is the one it returned"); } { - os::process::spawn_options o; - o.program = *self; - o.args = { "--child", "env" }; + auto o = copy("env"); o.env = { { "OS_TEST_VALUE", std::string("set") } }; auto r = os::process::run(o); check(r && r->out == "set", "an environment override reaches the program"); @@ -62,17 +70,13 @@ int main(int argc, char** argv) { { const auto dir = std::filesystem::temp_directory_path() / "mcpplibs-os-cwd-probe"; std::filesystem::create_directories(dir); - os::process::spawn_options o; - o.program = *self; - o.args = { "--child", "cwd" }; + auto o = copy("cwd"); o.cwd = dir; auto r = os::process::run(o); check(r && r->out == "mcpplibs-os-cwd-probe", "a program runs in the directory it is given"); } { - os::process::spawn_options o; - o.program = *self; - o.args = { "--child", "sleep" }; + auto o = copy("sleep"); o.out = os::process::stdio::null; o.err = os::process::stdio::null; o.own_unit = true; @@ -90,9 +94,7 @@ int main(int argc, char** argv) { } } if (os::interrupt::available()) { - os::process::spawn_options o; - o.program = *self; - o.args = { "--child", "interrupt" }; + auto o = copy("interrupt"); auto p = os::process::process::spawn(o); if (p) { auto first = p->read_output(); diff --git a/tools/image-graph.sh b/tools/image-graph.sh new file mode 100755 index 0000000..9a0bb68 --- /dev/null +++ b/tools/image-graph.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# image-graph.sh --- the graph an ok image of this package's tests is +# built against, from the same-named branch of each repository: the C++ +# run-time (openkal-llvm-runtime) and the stack beneath it, put in place by that +# repository's own tools/branch-graph.sh, and this manifest pointed at it. +# +# Until those packages are published their versions resolve from no index, so +# the trees are named by path; the manifest that is committed names them by +# their branches, and after the release by their versions. The specification is +# the tree the run-time's graph uses, so the graph names one openkal. +set -euo pipefail +branch="${1:?usage: image-graph.sh }" +here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +rt="$here/.runtime" +rm -rf "$rt" +git clone --quiet --branch "$branch" https://github.com/mcpplibs/openkal-llvm-runtime.git "$rt" +( cd "$rt" && bash tools/branch-graph.sh "$branch" ) +native() { if command -v cygpath > /dev/null 2>&1; then cygpath -m "$1"; else printf '%s\n' "$1"; fi; } +sed -e "s|^openkal-llvm-runtime = .*\$|openkal-llvm-runtime = { path = \"$(native "$rt")\" }|" \ + -e "s|^openkal = .*\$|openkal = { path = \"$(native "$rt/.spec")\" }|" \ + "$here/mcpp.toml" > "$here/mcpp.toml.next" +mv "$here/mcpp.toml.next" "$here/mcpp.toml" +grep -nE '^(openkal|openkal-llvm-runtime) = ' "$here/mcpp.toml" From 787988c6abf227a485ab080349fe4119f6fd73c2 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 26 Oct 2026 23:00:34 +0900 Subject: [PATCH 06/17] an ok image on Windows: names made absolute in the host's spelling (fs::absolute), and self_command absolute In an image the standard library's path is POSIX's whatever the host is, so a drive-letter name was joined to the working directory and every start of a program on Windows failed; a relative image name broke a start in another directory. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- src/env.cpp | 6 ++++-- src/fs.cpp | 20 ++++++++++++++++---- src/fs.cppm | 7 +++++++ tests/process_test.cpp | 3 ++- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/env.cpp b/src/env.cpp index 18e26d2..65a1b43 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -61,8 +61,10 @@ std::vector self_command() { std::vector out; const auto program = loader(); const auto own = image(); - if (program) out.push_back(program->string()); - if (own && (!program || *own != *program)) out.push_back(own->string()); + // Absolute, so that the command starts the program from any directory: an + // image is named as its loader was given it, which may be relative. + if (program) out.push_back(fs::absolute(*program).string()); + if (own && (!program || *own != *program)) out.push_back(fs::absolute(*own).string()); return out; } diff --git a/src/fs.cpp b/src/fs.cpp index 187ea37..64b8538 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -100,11 +100,23 @@ result write_all(kal_file f, std::string_view bytes, std::string_view doin } // namespace -std::optional locate(const std::filesystem::path& name) { +std::filesystem::path absolute(const std::filesystem::path& name) { + const std::string s = name.generic_string(); + const bool windows = host() == platform::windows; + const bool already = windows + ? (s.size() >= 3 && std::isalpha(static_cast(s[0])) && s[1] == ':' && separator(s[2])) + || (s.size() >= 2 && separator(s[0]) && separator(s[1])) + : !s.empty() && s[0] == '/'; + if (already) return name; std::error_code ec; - auto absolute = std::filesystem::absolute(name, ec); - if (ec) absolute = name; - const std::string path = generic(absolute.lexically_normal().generic_string()); + const auto cwd = std::filesystem::current_path(ec); + if (ec) return name; + return std::filesystem::path(generic(cwd.generic_string()) + "/" + s); +} + +std::optional locate(const std::filesystem::path& name) { + const auto whole = mcpplibs::os::fs::absolute(name); + const std::string path = generic(whole.lexically_normal().generic_string()); const bool fold = conventions().case_insensitive_names; std::optional best; std::size_t best_len = 0; diff --git a/src/fs.cppm b/src/fs.cppm index 9bb57d7..1782c4a 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -32,6 +32,13 @@ std::optional locate(const std::filesystem::path& name); // The supplied directories' names, in openkal's order. std::vector preopens(); +// A name made absolute in the host's spelling: one absolute already is kept (on +// Windows a drive and a separator, or two separators; elsewhere a leading `/'), +// and another is joined to the working directory. Asked of `host()' and not of +// the standard library, whose answer in an ok image is POSIX's whatever system +// the image runs upon. +std::filesystem::path absolute(const std::filesystem::path& name); + // --- contents -------------------------------------------------------------- result read(const std::filesystem::path& file); diff --git a/tests/process_test.cpp b/tests/process_test.cpp index 125c4c3..c71b225 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -113,7 +113,8 @@ int main(int argc, char** argv) { check(me && me->id != 0, "this program has an identifier"); check(me && os::process::alive(*me), "and is alive"); } - check(os::env::find_executable(self->filename().string(), self->parent_path().string()).has_value(), + const auto whole = os::fs::absolute(*self); + check(os::env::find_executable(whole.filename().string(), whole.parent_path().string()).has_value(), "a program is found by name in a list of directories"); return g_failures; } From cf6dfbfe65349e8ed9183436e6af84b17cc08453 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 26 Oct 2026 23:20:22 +0900 Subject: [PATCH 07/17] process_test: a failed start says why Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- tests/process_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/process_test.cpp b/tests/process_test.cpp index c71b225..8dd1ce8 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -54,6 +54,7 @@ int main(int argc, char** argv) { { auto o = copy("echo"); auto r = os::process::run(o, "hello"); + if (!r) std::printf(" (the start of %s failed: %s)\n", o.program.string().c_str(), r.error().message().c_str()); check(r && r->out == "hello", "a started program reads its input and writes its output"); check(r && r->err == "err", "and its error output is collected beside"); check(r && r->status.code == 7 && !r->status.by_environment, "and its status is the one it returned"); From db4fbfb6cae78308ac272993299f046dc59cfb64 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 26 Oct 2026 23:42:10 +0900 Subject: [PATCH 08/17] an ok image on Windows: a program named by a Windows path is started from that path, not looked for on PATH (fs::names_a_place) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- README.md | 6 +++++- src/env.cpp | 2 +- src/fs.cpp | 7 +++++++ src/fs.cppm | 5 +++++ src/process.cpp | 4 ++-- tests/fs_test.cpp | 6 ++++++ 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 922f4b9..1b0f7e5 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,11 @@ is an ordinary function there, answered when the image is loaded, and provided, so code that reads them is a compile error rather than a wrong answer on another system. `os::env::loader()` names the program that runs the image, and `os::env::self_command()` is what starts the same program again --- a -loader and an image, or the image alone when it is built for its system. The +loader and an image, or the image alone when it is built for its system. A +name's spelling is the host's as well: `os::fs::absolute()` and +`os::fs::names_a_place()` read `C:\\x` and `bin\\x` as Windows does when the +image runs there, where the standard library inside an image reads names as +POSIX does. The image carries its C library and C++ run-time (`openkal-llvm-runtime`); the tests are built as images in CI and run through okrun on four systems, and `checks/constants` holds the compile-time facts of a native target. diff --git a/src/env.cpp b/src/env.cpp index 65a1b43..86bf6e5 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -117,7 +117,7 @@ std::optional find_executable(std::string_view name, std: std::ranges::stable_partition(suffixes, [](const std::string& x) { return x.empty() || x == ".exe" || x == ".com"; }); } const std::filesystem::path given(name); - if (given.has_parent_path()) { + if (fs::names_a_place(name)) { for (const auto& s : suffixes) { auto candidate = given; candidate += s; if (is_program(candidate)) return candidate; diff --git a/src/fs.cpp b/src/fs.cpp index 64b8538..8c54a20 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -114,6 +114,13 @@ std::filesystem::path absolute(const std::filesystem::path& name) { return std::filesystem::path(generic(cwd.generic_string()) + "/" + s); } +bool names_a_place(std::string_view name) { + if (host() == platform::windows) + return name.find_first_of("/\\") != std::string_view::npos + || (name.size() >= 2 && std::isalpha(static_cast(name[0])) && name[1] == ':'); + return name.find('/') != std::string_view::npos; +} + std::optional locate(const std::filesystem::path& name) { const auto whole = mcpplibs::os::fs::absolute(name); const std::string path = generic(whole.lexically_normal().generic_string()); diff --git a/src/fs.cppm b/src/fs.cppm index 1782c4a..ba9b0d8 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -39,6 +39,11 @@ std::vector preopens(); // the image runs upon. std::filesystem::path absolute(const std::filesystem::path& name); +// Whether a name says where a file is, rather than naming one to look for on a +// list of directories: it holds a separator in the host's spelling (on Windows +// `\' as well as `/', or a drive). Asked of `host()' for the reason above. +bool names_a_place(std::string_view name); + // --- contents -------------------------------------------------------------- result read(const std::filesystem::path& file); diff --git a/src/process.cpp b/src/process.cpp index 8235180..66fd0c5 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -158,13 +158,13 @@ std::uint64_t process::id() const { result process::spawn(const spawn_options& o) { // The program, by name or by its place. std::filesystem::path program = o.program; - if (!program.has_parent_path()) { + if (!fs::names_a_place(o.program.generic_string())) { auto found = env::find_executable(program.generic_string()); if (!found) return std::unexpected(error{ errc::not_found, std::format("{} is not on PATH", program.generic_string()) }); program = *found; } std::error_code ec; - program = std::filesystem::absolute(program, ec).lexically_normal(); + program = fs::absolute(program).lexically_normal(); const auto where = fs::locate(program); if (!where) return std::unexpected(error{ errc::not_found, std::format("starting {}: beneath no directory this program was given", program.generic_string()) }); diff --git a/tests/fs_test.cpp b/tests/fs_test.cpp index 4d1d08e..fa63bca 100644 --- a/tests/fs_test.cpp +++ b/tests/fs_test.cpp @@ -55,6 +55,12 @@ int main() { } check(os::fs::rename(file, dir / "b.txt", false).has_value(), "a rename that does not replace renames to a free name"); + check(os::fs::names_a_place("bin/tool") && !os::fs::names_a_place("tool"), + "a name with a separator says where a file is, a bare one does not"); + check(os::fs::names_a_place("C:tool") == (os::host() == os::platform::windows) + && os::fs::names_a_place("bin\\tool") == (os::host() == os::platform::windows), + "a drive and a backslash say where a file is on Windows and nowhere else"); + stdfs::remove_all(dir); return g_failures; } From 525d64b57e17aa8413217e2b762d97e31ab1385b Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 26 Oct 2026 23:42:24 +0900 Subject: [PATCH 09/17] README: a Windows name's spelling, single backslashes Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b0f7e5..04410c2 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ on another system. `os::env::loader()` names the program that runs the image, and `os::env::self_command()` is what starts the same program again --- a loader and an image, or the image alone when it is built for its system. A name's spelling is the host's as well: `os::fs::absolute()` and -`os::fs::names_a_place()` read `C:\\x` and `bin\\x` as Windows does when the +`os::fs::names_a_place()` read `C:\x` and `bin\x` as Windows does when the image runs there, where the standard library inside an image reads names as POSIX does. The image carries its C library and C++ run-time (`openkal-llvm-runtime`); the From 73036eca447ecf0eef7f85046200792bef2f76c2 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 00:02:12 +0900 Subject: [PATCH 10/17] a name held open is removed at once where the volume claims KAL_FS_PROP_REMOVE_OPEN (openkal 0.17, clause 7.14) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- src/fs.cpp | 1 + src/fs.cppm | 1 + tests/fs_test.cpp | 11 ++++++++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/fs.cpp b/src/fs.cpp index 8c54a20..3cf0bdd 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -272,6 +272,7 @@ result caps(const std::filesystem::path& directory) { c.locks = (p & kal::fs::locks.bits) != 0; c.executable = (p & kal::fs::executable.bits) != 0; c.atomic_rename = (p & kal::fs::atomic_rename.bits) != 0; + c.remove_open = (p & kal::fs::remove_open.bits) != 0; return c; } diff --git a/src/fs.cppm b/src/fs.cppm index ba9b0d8..396ea16 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -75,6 +75,7 @@ struct capabilities { bool locks = false; bool executable = false; // whether a node may be started is recorded bool atomic_rename = false; + bool remove_open = false; // a name held open is removed at once }; // Of the volume the directory is on. diff --git a/tests/fs_test.cpp b/tests/fs_test.cpp index fa63bca..c332cae 100644 --- a/tests/fs_test.cpp +++ b/tests/fs_test.cpp @@ -36,6 +36,8 @@ int main() { } auto again = os::fs::lock::acquire(dir / "lock", os::fs::lock::kind::exclusive, false); check(again.has_value(), "and is taken again once released"); + // A lock held for the rest of the program: the directory is removed below + // while it is held, where the volume removes a held name at once. const auto s = os::fs::stamp_of(file); check(s && s->size == 3, "a stamp reports the size"); @@ -61,6 +63,13 @@ int main() { && os::fs::names_a_place("bin\\tool") == (os::host() == os::platform::windows), "a drive and a backslash say where a file is on Windows and nowhere else"); - stdfs::remove_all(dir); + if (caps && caps->remove_open) { + std::error_code ec; + stdfs::remove_all(dir, ec); + check(!ec && !stdfs::exists(dir), "a directory holding a file held open is removed"); + } else { + if (again) again->release(); + stdfs::remove_all(dir); + } return g_failures; } From 795f384a2d6d9dd248d9a6676cab0d2fdfc851be Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 00:07:31 +0900 Subject: [PATCH 11/17] process: output onto this program's error output (stdio::error), end_unit, reads_bounded; mcpplibs.os.random Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- README.md | 5 ++-- src/os.cppm | 1 + src/process.cpp | 22 ++++++++++++--- src/process.cppm | 15 ++++++++++- src/random.cpp | 31 +++++++++++++++++++++ src/random.cppm | 19 +++++++++++++ tests/platform_test.cpp | 2 ++ tests/process_test.cpp | 60 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 src/random.cpp create mode 100644 src/random.cppm diff --git a/README.md b/README.md index 04410c2..a67f8cd 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ modules. | `mcpplibs.os.platform` | `PLATFORM`, `ARCH`, `is_linux` / `is_macos` / `is_windows` / `is_posix`, `exe_suffix`, `path_list_separator`; `host()`, a constant expression on a native target | | `mcpplibs.os.error` | `errc` (openkal's closed set of conditions), `error`, `result` | | `mcpplibs.os.env` | variables, arguments, the program's own image, PATH search with PATHEXT | -| `mcpplibs.os.fs` | `write_atomic`, `publish` (no replace), `caps`, `lock`, links, `set_executable`, `stamp_of`, `identity_of`, `locate` | -| `mcpplibs.os.process` | `spawn`, `run`, pipes, units, bound lifetime, bounded waits; `self`, `describe`, `alive` | +| `mcpplibs.os.fs` | `write_atomic`, `publish` (no replace), `caps`, `lock`, links, `set_executable`, `stamp_of`, `identity_of`, `locate`, `absolute`, `names_a_place` | +| `mcpplibs.os.process` | `spawn`, `run`, pipes, output onto this program's error output, units and `end_unit`, bound lifetime, bounded waits and reads; `self`, `describe`, `alive` | | `mcpplibs.os.interrupt` | `requested`, `wait`, `end_as_requested` | | `mcpplibs.os.stdio` | the three streams as bytes | | `mcpplibs.os.terminal` | `interactive`, `size`, `sequences`, `color`, `raw_mode` | @@ -49,6 +49,7 @@ modules. | `mcpplibs.os.ipc` | a listener and a connection at a name in a directory; `single_instance` | | `mcpplibs.os.net` | loopback TCP | | `mcpplibs.os.machine` | the machine's architecture (through Rosetta and WoW64), memory | +| `mcpplibs.os.random` | bytes from the system's source of unpredictable bytes; `hex` | | `mcpplibs.os.linux` / `.macos` / `.windows` / `.posix` | platform modules: declared on every target, defined only on their own | ## Rules diff --git a/src/os.cppm b/src/os.cppm index 2da3c51..163cfd9 100644 --- a/src/os.cppm +++ b/src/os.cppm @@ -18,6 +18,7 @@ export import mcpplibs.os.dirs; export import mcpplibs.os.ipc; export import mcpplibs.os.net; export import mcpplibs.os.machine; +export import mcpplibs.os.random; export import mcpplibs.os.linux; export import mcpplibs.os.macos; export import mcpplibs.os.windows; diff --git a/src/process.cpp b/src/process.cpp index 66fd0c5..d43eedc 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -96,7 +96,8 @@ result read_chunk(kal_stream s, bool open) { return std::string(buf, static_cast(n)); } -result> read_chunk_for(kal_stream s, bool open, std::chrono::milliseconds t) { +result> read_chunk_for(kal_stream s, bool open, std::chrono::milliseconds t, + std::atomic& bounded) { if (!open) return std::optional{ std::string{} }; char buf[16384]; const kal_intptr n = kal_timeout_read(s, buf, sizeof buf, nanoseconds(t)); @@ -105,6 +106,7 @@ result> read_chunk_for(kal_stream s, bool open, std:: if (code == kal_err_again) return std::optional{}; if (code == kal_err_closed) return std::optional{ std::string{} }; if (code == kal_err_not_supported) { + bounded = false; auto r = read_chunk(s, open); if (!r) return std::unexpected(r.error()); return std::optional{ std::move(*r) }; @@ -123,6 +125,7 @@ struct process::state { kal_job job{}; bool has_job = false; std::optional ended; + std::atomic bounded{ true }; std::mutex write_lock, wait_lock; }; @@ -156,6 +159,8 @@ std::uint64_t process::id() const { } result process::spawn(const spawn_options& o) { + if (o.in == stdio::error) + return std::unexpected(error{ errc::invalid, "a program's input cannot be this program's error output" }); // The program, by name or by its place. std::filesystem::path program = o.program; if (!fs::names_a_place(o.program.generic_string())) { @@ -212,6 +217,8 @@ result process::spawn(const spawn_options& o) { auto n = open_null(false); if (!n) return fail_streams(n.error()); child_out = n->child; nulls.push_back(*n); + } else if (o.out == stdio::error) { + child_out = kal_stderr(); } if (o.err == stdio::pipe) { if (const int rc = kal_process_channel(&s->error, &child_err); rc != kal_ok) @@ -221,6 +228,8 @@ result process::spawn(const spawn_options& o) { auto n = open_null(false); if (!n) return fail_streams(n.error()); child_err = n->child; nulls.push_back(*n); + } else if (o.err == stdio::error) { + child_err = kal_stderr(); } // The vector, the first element in the system's own spelling: a program @@ -290,11 +299,11 @@ result process::read_error() { } result> process::read_output_for(std::chrono::milliseconds t) { if (!state_) return std::optional{ std::string{} }; - return read_chunk_for(state_->output, state_->output_open, t); + return read_chunk_for(state_->output, state_->output_open, t, state_->bounded); } result> process::read_error_for(std::chrono::milliseconds t) { if (!state_) return std::optional{ std::string{} }; - return read_chunk_for(state_->error, state_->error_open, t); + return read_chunk_for(state_->error, state_->error_open, t, state_->bounded); } result process::wait() { @@ -336,6 +345,13 @@ void process::kill() { else kal_process_terminate(state_->handle); } +bool process::reads_bounded() const noexcept { return !state_ || state_->bounded.load(); } + +void process::end_unit() { + if (!state_ || !state_->has_job) return; + kal_process_job_terminate(state_->job); +} + void process::detach() { if (!state_) return; close_input(); diff --git a/src/process.cppm b/src/process.cppm index f86a90b..ee2f385 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -13,7 +13,11 @@ export import mcpplibs.os.error; export namespace mcpplibs::os::process { -enum class stdio { inherit, pipe, null }; +// What a started program's stream is: this program's own, a pipe to this +// program, the null device, or (`error') this program's standard error --- for +// a program whose standard output carries a protocol, where what a child +// writes may not reach it. +enum class stdio { inherit, pipe, null, error }; struct spawn_options { // An absolute name, or a bare one looked for on PATH (`env::find_executable'). @@ -72,6 +76,10 @@ public: // The same, giving up after `timeout': nothing when it expired. result> read_output_for(std::chrono::milliseconds timeout); result> read_error_for(std::chrono::milliseconds timeout); + // False once a bounded read met a stream the system cannot bound and read + // it unbounded instead: a thread waiting there cannot be stopped by its + // bound, only by the stream's end. + bool reads_bounded() const noexcept; result wait(); result> try_wait(); @@ -81,6 +89,11 @@ public: void terminate(); // The program's unit, or the program, ended outright. void kill(); + // Everything else in the program's unit ended, whether or not the program + // itself still runs: a program that left something behind (a descendant + // holding a pipe open) is exactly the case where it has ended and its unit + // has not. Nothing happens for a program without a unit. + void end_unit(); // Released, and left running. void detach(); diff --git a/src/random.cpp b/src/random.cpp new file mode 100644 index 0000000..ceb6cea --- /dev/null +++ b/src/random.cpp @@ -0,0 +1,31 @@ +module mcpplibs.os.random; + +import std; +import openkal.types; +import openkal.random; +import mcpplibs.os.error; + +namespace mcpplibs::os::random { + +result fill(std::span out) { + if (out.empty()) return {}; + if (const int rc = kal_random_fill(out.data(), out.size()); rc != kal_ok) + return std::unexpected(from_kal(rc, "reading the system's source of unpredictable bytes")); + return {}; +} + +result hex(std::size_t n) { + std::vector bytes(n); + if (auto r = fill(bytes); !r) return std::unexpected(r.error()); + constexpr std::string_view digits = "0123456789abcdef"; + std::string out; + out.reserve(n * 2); + for (const auto b : bytes) { + const auto v = std::to_integer(b); + out.push_back(digits[v >> 4]); + out.push_back(digits[v & 0xf]); + } + return out; +} + +} // namespace mcpplibs::os::random diff --git a/src/random.cppm b/src/random.cppm new file mode 100644 index 0000000..9feceab --- /dev/null +++ b/src/random.cppm @@ -0,0 +1,19 @@ +// mcpplibs.os.random --- bytes no other party can predict, from the system's +// own source (openkal.random): a token a peer must present, a name another +// program cannot guess. Not a generator: what is wanted many times over is +// seeded from here, by the program, with the algorithm it chooses. +export module mcpplibs.os.random; + +import std; +export import mcpplibs.os.error; + +export namespace mcpplibs::os::random { + +// Every byte of `out', or none (errc::again while a source early in boot is +// not yet seeded). +result fill(std::span out); + +// `n' such bytes as lowercase hexadecimal digits. +result hex(std::size_t n); + +} // namespace mcpplibs::os::random diff --git a/tests/platform_test.cpp b/tests/platform_test.cpp index 8a4842a..d7ecc12 100644 --- a/tests/platform_test.cpp +++ b/tests/platform_test.cpp @@ -24,5 +24,7 @@ int main() { check(!os::machine::arch().empty(), "the machine's architecture is named"); if constexpr (!os::is_image) check(os::machine::memory().value_or(0) > 0, "the machine's memory is known"); + const auto a = os::random::hex(16), b = os::random::hex(16); + check(a && a->size() == 32 && b && *a != *b, "the system's source gives bytes that differ from call to call"); return g_failures; } diff --git a/tests/process_test.cpp b/tests/process_test.cpp index 8dd1ce8..63b209b 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -5,7 +5,44 @@ import mcpplibs.os; namespace os = mcpplibs::os; +// What starts this program again with an errand. +os::process::spawn_options copy_of_self(std::string errand) { + const auto again = os::env::self_command(); + os::process::spawn_options o; + if (again.empty()) return o; + o.program = again.front(); + o.args.assign(again.begin() + 1, again.end()); + o.args.push_back("--child"); + o.args.push_back(std::move(errand)); + return o; +} + int child(std::string_view errand) { + if (errand == "say") { + (void)os::stdio::write_output("said"); + return 0; + } + if (errand == "relay") { + // A program whose own output is a protocol: what it starts writes to + // its error output instead. + auto o = copy_of_self("say"); + o.out = os::process::stdio::error; + o.err = os::process::stdio::inherit; + auto p = os::process::process::spawn(o); + if (!p) return 1; + auto st = p->wait(); + return st && st->success() ? 0 : 2; + } + if (errand == "leave") { + // Leaves a program behind that holds this one's output open. + auto o = copy_of_self("sleep"); + o.out = os::process::stdio::inherit; + o.err = os::process::stdio::null; + auto p = os::process::process::spawn(o); + if (!p) return 1; + p->detach(); + return 0; + } if (errand == "echo") { auto in = os::stdio::read_exactly(5); (void)os::stdio::write_output(in.value_or("")); @@ -94,6 +131,29 @@ int main(int argc, char** argv) { check(st && !st->success(), "and the unit, ended, does not report success"); } } + { + auto r = os::process::run(copy("relay")); + check(r && r->status.success() && r->out.empty() && r->err == "said", + "a program started with its output on the error output writes there"); + } + { + auto o = copy("leave"); + o.in = os::process::stdio::null; + o.err = os::process::stdio::null; + o.own_unit = true; + auto p = os::process::process::spawn(o); + check(p.has_value(), "a program that leaves another behind is started in a unit"); + if (p) { + auto st = p->wait(); + check(st && st->success(), "and ends"); + auto held = p->read_output_for(std::chrono::milliseconds(300)); + check(held && !held->has_value(), "while what it left holds its output open"); + p->end_unit(); + auto end = p->read_output_for(std::chrono::seconds(20)); + check(end && end->has_value() && (*end)->empty(), "and ending the rest of its unit closes it"); + check(p->reads_bounded(), "and every bounded read here was bounded"); + } + } if (os::interrupt::available()) { auto o = copy("interrupt"); auto p = os::process::process::spawn(o); From 66b862f4887d4c630a6a64a40b8e6e4e78c60dca Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 00:15:22 +0900 Subject: [PATCH 12/17] find_executable: on Windows a bare name is never looked for as written, and a name with a PATHEXT extension is taken as written Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- src/env.cpp | 18 ++++++++++++++---- tests/process_test.cpp | 26 +++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/env.cpp b/src/env.cpp index 86bf6e5..3d56fa1 100644 --- a/src/env.cpp +++ b/src/env.cpp @@ -98,23 +98,33 @@ bool is_program(const std::filesystem::path& p) { std::optional find_executable(std::string_view name, std::string_view path_list) { if (name.empty()) return std::nullopt; const bool windows = host() == platform::windows; + // On Windows what makes a file a program is its extension. A name that + // carries one of PATHEXT's is looked for as written; any other has each of + // them appended and is never looked for bare: an extension-less file + // beside `npm.cmd' is the POSIX shell script `npm', which this system + // cannot start. std::vector suffixes{ "" }; if (windows) { const auto ext = get("PATHEXT").value_or(".COM;.EXE;.BAT;.CMD"); std::string lower_name(name); for (auto& c : lower_name) c = static_cast(std::tolower(static_cast(c))); + std::vector pieces; std::size_t s = 0; while (s <= ext.size()) { const auto e = ext.find(';', s); auto piece = ext.substr(s, e == std::string::npos ? std::string::npos : e - s); for (auto& c : piece) c = static_cast(std::tolower(static_cast(c))); - if (!piece.empty() && !lower_name.ends_with(piece)) suffixes.push_back(piece); + if (!piece.empty()) pieces.push_back(piece); if (e == std::string::npos) break; s = e + 1; } - // A name the system starts directly comes before one it would hand to - // an interpreter, as the system's own search orders them. - std::ranges::stable_partition(suffixes, [](const std::string& x) { return x.empty() || x == ".exe" || x == ".com"; }); + const bool carries = std::ranges::any_of(pieces, [&](const std::string& p) { return lower_name.ends_with(p); }); + if (!carries) { + suffixes = pieces; + // A name the system starts directly comes before one it would hand + // to an interpreter, as the system's own search orders them. + std::ranges::stable_partition(suffixes, [](const std::string& x) { return x == ".exe" || x == ".com"; }); + } } const std::filesystem::path given(name); if (fs::names_a_place(name)) { diff --git a/tests/process_test.cpp b/tests/process_test.cpp index 63b209b..252d1d7 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -174,8 +174,28 @@ int main(int argc, char** argv) { check(me && me->id != 0, "this program has an identifier"); check(me && os::process::alive(*me), "and is alive"); } - const auto whole = os::fs::absolute(*self); - check(os::env::find_executable(whole.filename().string(), whole.parent_path().string()).has_value(), - "a program is found by name in a list of directories"); + // The program the system starts: this one, or in an ok image its loader. + const auto started = os::is_image ? os::env::loader() : self; + if (started) { + const auto whole = os::fs::absolute(*started); + check(os::env::find_executable(whole.filename().string(), whole.parent_path().string()).has_value(), + "a program is found by name in a list of directories"); + } + { + // npm's directory holds `npm.cmd' and an extension-less `npm', the + // POSIX shell script: on Windows the name finds the first. + const auto dir = std::filesystem::temp_directory_path() / std::format("mcpplibs-os-find-{}", os::process::self()->id); + std::filesystem::create_directories(dir); + (void)os::fs::write(dir / "tool", "#!/bin/sh\n"); + (void)os::fs::write(dir / "tool.cmd", "@echo off\n"); + const auto found = os::env::find_executable("tool", os::fs::absolute(dir).string()); + const bool windows = os::host() == os::platform::windows; + check(found && found->filename() == (windows ? "tool.cmd" : "tool"), + "a bare name finds what the system starts: on Windows by an extension, never the bare file"); + const auto named = os::env::find_executable("tool.cmd", os::fs::absolute(dir).string()); + check(named && named->filename() == "tool.cmd", "and a name with its extension is taken as written"); + std::error_code ec; + std::filesystem::remove_all(dir, ec); + } return g_failures; } From bf243fb421ee4e672ac55963cb915b693e8d6720 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 00:17:56 +0900 Subject: [PATCH 13/17] linux: the translator is classified from its facts (classify_translator), and the PROOT_ variables decide only where the tracer cannot be read Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- src/linux.cppm | 13 +++++++++++++ src/linux/linux.cpp | 45 +++++++++++++++++++++++++++++++++----------- tests/linux_test.cpp | 29 ++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 tests/linux_test.cpp diff --git a/src/linux.cppm b/src/linux.cppm index e95daf1..0c6e437 100644 --- a/src/linux.cppm +++ b/src/linux.cppm @@ -12,4 +12,17 @@ export namespace mcpplibs::os::linux { // is no portable notion of a translator, which is why this is here. std::optional translator(); +// What `translator' reads, and its answer from them: this program's +// /proc/self/status, the name of the program tracing it (/proc//comm, +// when there is one and it can be read), and the environment as `NAME=value'. +// A tracer named proot* is PRoot and another tracer (a debugger) is none; the +// PROOT_ variables decide only when the tracer cannot be read (a hidden +// /proc), and in a program nothing traces they mean nothing. +struct translator_facts { + std::string status; + std::string tracer_name; + std::vector environment; +}; +std::optional classify_translator(const translator_facts& facts); + } // namespace mcpplibs::os::linux diff --git a/src/linux/linux.cpp b/src/linux/linux.cpp index b1c4230..424b347 100644 --- a/src/linux/linux.cpp +++ b/src/linux/linux.cpp @@ -1,6 +1,7 @@ module mcpplibs.os.linux; import std; +import mcpplibs.os.env; namespace mcpplibs::os::linux { @@ -12,19 +13,41 @@ std::string read_small(const char* path) { } } // namespace -std::optional translator() { - const std::string status = read_small("/proc/self/status"); - long tracer = 0; - if (const auto at = status.find("TracerPid:"); at != std::string::npos) - tracer = std::strtol(status.c_str() + at + 10, nullptr, 10); - if (tracer > 0) { - std::string comm = read_small(std::format("/proc/{}/comm", tracer).c_str()); - while (!comm.empty() && (comm.back() == '\n' || comm.back() == '\r')) comm.pop_back(); - if (comm.starts_with("proot")) return std::string("proot"); - return std::nullopt; +// The tracer's number in a status text: its `TracerPid:' line, or 0. +long tracer_of(std::string_view status) { + const auto at = status.find("TracerPid:"); + if (at == std::string_view::npos) return 0; + std::size_t i = at + 10; + while (i < status.size() && (status[i] == ' ' || status[i] == '\t')) ++i; + long v = 0; + const auto [end, ec] = std::from_chars(status.data() + i, status.data() + status.size(), v); + if (ec != std::errc{} || (end != status.data() + status.size() && *end != '\n' && *end != '\r')) return 0; + return v > 0 ? v : 0; +} + +std::optional classify_translator(const translator_facts& facts) { + std::string_view tracer = facts.tracer_name; + while (!tracer.empty() && std::isspace(static_cast(tracer.back()))) tracer.remove_suffix(1); + while (!tracer.empty() && std::isspace(static_cast(tracer.front()))) tracer.remove_prefix(1); + if (tracer_of(facts.status) != 0) { + if (tracer.starts_with("proot")) return std::string("proot"); + if (!tracer.empty()) return std::nullopt; // another tracer, named: a debugger + } else if (facts.status.find("TracerPid:") != std::string::npos) { + return std::nullopt; // traced by nothing: a leaked PROOT_ means nothing } - if (status.empty() && std::getenv("PROOT_TMP_DIR") != nullptr) return std::string("proot"); + // The status or the tracer's name could not be read: the variables are what is left. + for (const auto& v : facts.environment) + if (v.starts_with("PROOT_")) return std::string("proot"); return std::nullopt; } +std::optional translator() { + translator_facts facts; + facts.status = read_small("/proc/self/status"); + if (const long tracer = tracer_of(facts.status); tracer != 0) + facts.tracer_name = read_small(std::format("/proc/{}/comm", tracer).c_str()); + facts.environment = env::variables(); + return classify_translator(facts); +} + } // namespace mcpplibs::os::linux diff --git a/tests/linux_test.cpp b/tests/linux_test.cpp new file mode 100644 index 0000000..128b563 --- /dev/null +++ b/tests/linux_test.cpp @@ -0,0 +1,29 @@ +// mcpplibs.os.linux: the system-call translator a program runs under, from +// what it reads (the facts), and for this program. +#include "check.hpp" +import std; +import mcpplibs.os; + +namespace os = mcpplibs::os; + +int main() { + if constexpr (os::is_linux) { + using facts = os::linux::translator_facts; + auto is = [](const facts& f) { return os::linux::classify_translator(f).value_or(""); }; + check(is(facts{ "TracerPid:\t812\n", "proot\n", {} }) == "proot", "a program traced by proot is under PRoot"); + check(is(facts{ "TracerPid:\t812\n", "proot-static", {} }) == "proot", "whatever proot* is called"); + check(is(facts{ "TracerPid:\t812\n", "gdb\n", {} }).empty(), "a debugger is not a translator"); + check(is(facts{ "TracerPid:\t812\n", "", { "PATH=/bin", "PROOT_TMP_DIR=/tmp" } }) == "proot", + "the PROOT_ variables decide when the tracer cannot be read"); + check(is(facts{ "", "", { "PROOT_TMP_DIR=/tmp" } }) == "proot", "and when the status cannot be read"); + check(is(facts{ "", "", { "PATH=/bin", "NOT_PROOT_X=1" } }).empty(), "and name nothing without them"); + check(is(facts{ "Name:\tx\nTracerPid:\t0\n", "", { "PROOT_NO_SECCOMP=1" } }).empty(), + "a PROOT_ variable in a program nothing traces means nothing"); + check(is(facts{ "TracerPid:\tnot-a-number\n", "proot", {} }).empty(), "a status that names no tracer names none"); + const auto here = os::linux::translator(); + check(!here || *here == "proot", "this program's translator is PRoot or none"); + if (const auto expected = os::env::get("OS_EXPECT_TRANSLATOR")) + check(here.value_or("") == *expected, "this program is under the translator its environment says"); + } + return g_failures; +} From f4c2a07bb5ed69ff6cc2096011766e1c49be20f5 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 00:18:07 +0900 Subject: [PATCH 14/17] ci: linux_test is one of the suites that must run Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77f7c4c..23dbc1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: run: | profile='${{ matrix.profile }}' mcpp test --profile "$profile" 2>&1 | tee tests.log - for name in platform_test fs_test process_test ipc_test dirs_terminal_test; do + for name in platform_test fs_test process_test ipc_test dirs_terminal_test linux_test; do grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not run or did not pass"; exit 1; } done @@ -155,7 +155,7 @@ jobs: run: | set -euo pipefail mcpp test --target x86_64-openkal 2>&1 | tee tests.log - for name in platform_test fs_test process_test ipc_test dirs_terminal_test; do + for name in platform_test fs_test process_test ipc_test dirs_terminal_test linux_test; do grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not pass as an image"; exit 1; } done mcpp test --target aarch64-openkal --no-run From 5476bb62613ea6b7e29bf268632e4bb6670835ed Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 00:19:53 +0900 Subject: [PATCH 15/17] ipc: connection::shutdown, safe from another thread, wakes a reader; tested with a write while a read waits Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- src/ipc.cpp | 4 ++++ src/ipc.cppm | 4 ++++ tests/ipc_test.cpp | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/src/ipc.cpp b/src/ipc.cpp index 367fe82..914c32b 100644 --- a/src/ipc.cpp +++ b/src/ipc.cpp @@ -103,6 +103,10 @@ void connection::shutdown_write() { if (conn_) kal_net_shutdown(kal_net_conn{ conn_ }, 2 /* KAL_SHUT_WRITE */); } +void connection::shutdown() { + if (conn_) kal_net_shutdown(kal_net_conn{ conn_ }, 3 /* KAL_SHUT_BOTH */); +} + void connection::close() { if (conn_) { kal_net_close(kal_net_conn{ conn_ }); conn_ = 0; } } diff --git a/src/ipc.cppm b/src/ipc.cppm index e842285..c946590 100644 --- a/src/ipc.cppm +++ b/src/ipc.cppm @@ -28,6 +28,10 @@ public: result> read_for(std::chrono::milliseconds timeout, std::size_t at_most = 65536); result write(std::string_view bytes); void shutdown_write(); + // Both directions ended: a thread reading is woken with the end, a thread + // writing fails. Safe from another thread while one reads or writes; the + // handle itself is released by `close', once nothing uses it. + void shutdown(); void close(); // A connection openkal made elsewhere (`kal_net_conn', carried as its word), // adopted: this object releases it. diff --git a/tests/ipc_test.cpp b/tests/ipc_test.cpp index 11d8e74..e2eeae3 100644 --- a/tests/ipc_test.cpp +++ b/tests/ipc_test.cpp @@ -44,5 +44,28 @@ int main() { check(r && *r == "hi", "and a loopback connection reads what was written"); } } + { + // A reader woken by another thread's shutdown, while its peer writes nothing; and both + // directions at once, a write while a read waits. + auto l = os::net::loopback_listener::listen(); + if (l) { + std::optional accepted; + std::jthread server([&] { if (auto c = l->accept()) accepted.emplace(std::move(*c)); }); + auto c = os::net::connect_loopback(l->port()); + server.join(); + check(c && accepted, "a connection is accepted"); + if (c && accepted) { + std::atomic woken{ false }; + std::jthread reader([&] { auto r = c->read(); woken = r && r->empty(); }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + check(c->write("while a read waits").has_value(), "a write proceeds while a read on the same connection waits"); + auto echoed = accepted->read(); + check(echoed && *echoed == "while a read waits", "and arrives"); + c->shutdown(); + reader.join(); + check(woken.load(), "a shutdown from another thread wakes the reader with the end"); + } + } + } return g_failures; } From d16d185451047491dcc127f633d6024ac995b92e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 03:14:03 +0900 Subject: [PATCH 16/17] openkal 0.17.0 and its implementations, by version Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- mcpp.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mcpp.toml b/mcpp.toml index b6be4aa..03d809b 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -12,16 +12,16 @@ repo = "https://github.com/mcpplibs/os" # own C and C++ runtime. A program that wants the standalone arrangement, or # the image arrangement of `-openkal`, selects it in its own manifest. [dependencies] -openkal = { git = "https://github.com/mcpplibs/openkal.git", branch = "openkal-0.17.0" } +openkal = "0.17.0" [target.linux.dependencies] -openkal-linux = { git = "https://github.com/mcpplibs/openkal-linux.git", branch = "openkal-0.17.0" } +openkal-linux = "0.18.0" [target.macos.dependencies] -openkal-macos = { git = "https://github.com/mcpplibs/openkal-macos.git", branch = "openkal-0.17.0" } +openkal-macos = "0.15.0" [target.windows.dependencies] -openkal-windows = { git = "https://github.com/mcpplibs/openkal-windows.git", branch = "openkal-0.17.0" } +openkal-windows = "0.13.0" [targets.os] kind = "lib" From d4b5ad6dd9f792d85583b0787eec78dd3cd35535 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Tue, 27 Oct 2026 03:46:36 +0900 Subject: [PATCH 17/17] the ok-image graph by version: openkal-llvm-runtime 0.16.0 and openkal-tools 0.1.0; the branch graph is gone Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- .github/workflows/ci.yml | 2 -- .gitignore | 2 +- mcpp.toml | 4 ++-- tools/image-graph.sh | 23 ----------------------- 4 files changed, 3 insertions(+), 28 deletions(-) delete mode 100755 tools/image-graph.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23dbc1b..68909fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,8 +149,6 @@ jobs: built=$(find "$src/target" -type f -name mcpp | head -1) [ -n "$built" ] || { echo "::error::mcpp did not build"; exit 1; } echo "$(cd "$(dirname "$built")" && pwd)" >> "$GITHUB_PATH" - - name: The run-time and the stack beneath it, from the same-named branch - run: bash tools/image-graph.sh openkal-0.17.0 - name: The suites, run here and built for the other architecture run: | set -euo pipefail diff --git a/.gitignore b/.gitignore index 1c81ff9..08360ef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ compile_commands.json *.log mcpp.lock -/.runtime/ + diff --git a/mcpp.toml b/mcpp.toml index 03d809b..8725191 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -62,7 +62,7 @@ sources = ["src/targets/openkal.cppm", "src/openkal/*.cpp"] # The tests of this package, built as images, carry the C library and the C++ # run-time an image holds, and the loader that runs them. [target.'cfg(os = "openkal")'.dev-dependencies] -openkal-llvm-runtime = { git = "https://github.com/mcpplibs/openkal-llvm-runtime.git", branch = "openkal-0.17.0" } +openkal-llvm-runtime = "0.16.0" [target.'cfg(os = "openkal")'.build-dependencies] -openkal-tools = { git = "https://github.com/mcpplibs/openkal-image.git", branch = "openkal-0.17.0", tools = ["okld", "okrun"] } +openkal-tools = { version = "0.1.0", tools = ["okld", "okrun"] } diff --git a/tools/image-graph.sh b/tools/image-graph.sh deleted file mode 100755 index 9a0bb68..0000000 --- a/tools/image-graph.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -# image-graph.sh --- the graph an ok image of this package's tests is -# built against, from the same-named branch of each repository: the C++ -# run-time (openkal-llvm-runtime) and the stack beneath it, put in place by that -# repository's own tools/branch-graph.sh, and this manifest pointed at it. -# -# Until those packages are published their versions resolve from no index, so -# the trees are named by path; the manifest that is committed names them by -# their branches, and after the release by their versions. The specification is -# the tree the run-time's graph uses, so the graph names one openkal. -set -euo pipefail -branch="${1:?usage: image-graph.sh }" -here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -rt="$here/.runtime" -rm -rf "$rt" -git clone --quiet --branch "$branch" https://github.com/mcpplibs/openkal-llvm-runtime.git "$rt" -( cd "$rt" && bash tools/branch-graph.sh "$branch" ) -native() { if command -v cygpath > /dev/null 2>&1; then cygpath -m "$1"; else printf '%s\n' "$1"; fi; } -sed -e "s|^openkal-llvm-runtime = .*\$|openkal-llvm-runtime = { path = \"$(native "$rt")\" }|" \ - -e "s|^openkal = .*\$|openkal = { path = \"$(native "$rt/.spec")\" }|" \ - "$here/mcpp.toml" > "$here/mcpp.toml.next" -mv "$here/mcpp.toml.next" "$here/mcpp.toml" -grep -nE '^(openkal|openkal-llvm-runtime) = ' "$here/mcpp.toml"