From 915ce765826b185269ddd4546afbced05aff923e Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 05:38:19 +0900 Subject: [PATCH 1/8] feat: compose declared units with sealed admission --- .github/workflows/ci.yml | 8 +- .github/workflows/unit-lifecycle.yml | 81 +++++++++++++++++ checks/hosted/tests/unit_test.cpp | 1 + src/process.cpp | 82 ++++++++++++++++- src/process.cppm | 49 ++++++++++ tests/unit_test.cpp | 130 +++++++++++++++++++++++++++ 6 files changed, 344 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/unit-lifecycle.yml create mode 100644 checks/hosted/tests/unit_test.cpp create mode 100644 tests/unit_test.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37c6572..c233bca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,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 linux_test; do + for name in platform_test fs_test process_test ipc_test unit_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 @@ -211,7 +211,7 @@ jobs: run: | profile='${{ matrix.profile }}' mcpp test --profile "$profile" ${{ matrix.target }} 2>&1 | tee tests.log - for name in platform_test fs_test process_test ipc_test dirs_terminal_test linux_test library_test; do + for name in platform_test fs_test process_test ipc_test unit_test dirs_terminal_test linux_test library_test; do grep -q "^$name \.\.\. ok" tests.log || { echo "::error::$name did not run or did not pass"; exit 1; } done @@ -260,7 +260,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 linux_test; do + for name in platform_test fs_test process_test ipc_test unit_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 @@ -399,7 +399,7 @@ jobs: run: | for t in tests/*_test.cpp; do n=$(basename "$t") - case "$n" in process_test.cpp|ipc_test.cpp) continue ;; esac + case "$n" in process_test.cpp|ipc_test.cpp|unit_test.cpp) continue ;; esac [ -f "checks/emscripten/tests/$n" ] || { echo "::error::checks/emscripten/tests has no $n"; exit 1; } done - name: Tests, under node diff --git a/.github/workflows/unit-lifecycle.yml b/.github/workflows/unit-lifecycle.yml new file mode 100644 index 0000000..b997c50 --- /dev/null +++ b/.github/workflows/unit-lifecycle.yml @@ -0,0 +1,81 @@ +name: declared unit lifecycle +on: + pull_request: + workflow_dispatch: +concurrency: + group: unit-lifecycle-${{ github.ref }} + cancel-in-progress: true +jobs: + native: + name: declared unit and blocking ACK (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, backend: openkal-linux, toolchain: gcc@16.1.0 } + - { os: macos-15, backend: openkal-macos, toolchain: llvm@23.1.3 } + - { os: windows-2025, backend: openkal-windows, toolchain: msvc@system } + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + - uses: actions/checkout@v4 + with: + repository: mcpplibs/openkal + ref: feat/observable-process-units + path: deps/openkal + - uses: actions/checkout@v4 + with: + repository: mcpplibs/${{ matrix.backend }} + ref: feat/observable-process-units + path: deps/backend + - uses: ilammy/msvc-dev-cmd@v1 + if: runner.os == 'Windows' + - name: Install the published build tool (Unix) + if: runner.os != 'Windows' + run: | + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.26.1 + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + - name: Install the published build tool (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: Install the actual native compiler + run: | + xlings update + xlings install mcpp@2026.11.5.1 -y -g + mcpp toolchain default '${{ matrix.toolchain }}' + - name: Select the coherent source group under review + env: + IMPLEMENTATION: ${{ matrix.backend }} + run: | + python - <<'PY' + from pathlib import Path + import os,re + root=Path('mcpp.toml') + text=root.read_text() + text=re.sub(r'^openkal = .*$', 'openkal = { path = "deps/openkal" }', text, flags=re.M) + text=re.sub(r'^'+re.escape(os.environ['IMPLEMENTATION'])+r' = .*$', os.environ['IMPLEMENTATION']+' = { path = "deps/backend" }', text, flags=re.M) + root.write_text(text) + backend=Path('deps/backend/mcpp.toml') + backend.write_text(re.sub(r'^openkal = .*$', 'openkal = { path = "../openkal" }', backend.read_text(), flags=re.M)) + PY + - name: Compile and run the real lifecycle/ACK consumer + run: | + set -o pipefail + mcpp test unit_test --verbose 2>&1 | tee unit-lifecycle.log + binary=$(find target -type f \( -name unit_test -o -name unit_test.exe \) | head -1) + test -n "$binary" + "$binary" | tee unit-cost.jsonl + - uses: actions/upload-artifact@v4 + if: always() + with: + name: unit-lifecycle-${{ matrix.os }} + path: | + unit-lifecycle.log + unit-cost.jsonl diff --git a/checks/hosted/tests/unit_test.cpp b/checks/hosted/tests/unit_test.cpp new file mode 100644 index 0000000..cdfb715 --- /dev/null +++ b/checks/hosted/tests/unit_test.cpp @@ -0,0 +1 @@ +#include "../../../tests/unit_test.cpp" diff --git a/src/process.cpp b/src/process.cpp index fb7f88a..7c4aa84 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -121,12 +121,76 @@ result> read_chunk_for(kal_stream s, bool open, std:: } // namespace +namespace { +unit_state unit_state_of(kal_u32 value) { + return value == kal::process::job_state::empty ? unit_state::empty + : value == kal::process::job_state::occupied ? unit_state::occupied : unit_state::unknown; +} +} +struct owned_unit::state { + kal_job job{}; + std::string reference; + bool sealed = false; + std::mutex admission; + ~state() { if (job.h) kal_process_job_close(job); } +}; +owned_unit::owned_unit() = default; +owned_unit::owned_unit(owned_unit&&) noexcept = default; +owned_unit& owned_unit::operator=(owned_unit&&) noexcept = default; +owned_unit::~owned_unit() = default; +result owned_unit::create() { + auto s = std::make_shared(); + std::string reference(512, '\0'); kal_uintptr used = 0; + const int rc = kal_process_job_create(&s->job,reference.data(),reference.size(),&used); + if (rc != kal_ok) return std::unexpected(from_kal(rc,"declaring a process unit")); + if (used == 0 || used > reference.size()) return std::unexpected(error{errc::io,"invalid process unit reference"}); + reference.resize(used); s->reference = std::move(reference); + owned_unit result; result.state_ = std::move(s); return result; +} +const std::string& owned_unit::reference() const { + static const std::string empty; + return state_ ? state_->reference : empty; +} +result owned_unit::retire() { + if (!state_) return std::unexpected(error{errc::invalid,"retiring an empty process unit"}); + std::lock_guard guard(state_->admission); state_->sealed = true; + kal_u32 observed = kal::process::job_state::unknown; + const int rc = kal_process_job_retire(state_->job,state_->reference.data(),state_->reference.size(),&observed); + if (rc != kal_ok) return std::unexpected(from_kal(rc,"retiring a process unit")); + return unit_state_of(observed); +} +void owned_unit::terminate() { if (state_) kal_process_job_terminate(state_->job); } +result unit_observer::open(std::string_view reference) { + kal_job job{}; + const int rc = kal_process_job_open(reference.data(),reference.size(),&job); + if (rc != kal_ok) return std::unexpected(from_kal(rc,"opening a process unit reference")); + unit_observer result; result.job_ = job.h; result.reference_ = reference; return result; +} +unit_observer::unit_observer(unit_observer&& other) noexcept + : job_(std::exchange(other.job_,0)), reference_(std::move(other.reference_)) {} +unit_observer& unit_observer::operator=(unit_observer&& other) noexcept { + if (this != &other) { + if (job_) kal_process_job_close(kal_job{job_}); + job_ = std::exchange(other.job_,0); reference_ = std::move(other.reference_); + } + return *this; +} +unit_observer::~unit_observer() { if (job_) kal_process_job_close(kal_job{job_}); } +result unit_observer::observe() const { + if (!job_) return std::unexpected(error{errc::invalid,"observing an empty process unit reference"}); + kal_u32 observed = kal::process::job_state::unknown; + const int rc = kal_process_job_observe(kal_job{job_},reference_.data(),reference_.size(),&observed); + if (rc != kal_ok) return std::unexpected(from_kal(rc,"observing a process unit")); + return unit_state_of(observed); +} + 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::shared_ptr declared; std::optional ended; std::atomic bounded{ true }; std::mutex write_lock, wait_lock; @@ -336,9 +400,19 @@ result process::spawn(const spawn_options& o) { | (o.one_string ? kal::process::one_string_flag.bits : kal_uintptr{ 0 }) | (o.confine ? kal::process::confine_flag.bits : kal_uintptr{ 0 }) | preference | null_at }; + std::unique_lock admission; + if (o.declared_unit) { + if (o.own_unit || !o.declared_unit->state_) + return fail_streams(error{errc::invalid,"a declared process unit conflicts with own_unit or is empty"}); + s->declared = o.declared_unit->state_; + admission = std::unique_lock(s->declared->admission); + if (s->declared->sealed) + return fail_streams(error{errc::closed,"a retired process unit accepts no new program"}); + } kal_job job{}; const bool unit = o.own_unit && (props & kal::process::job.bits) != 0; if (unit) how.job = &job; + else if (s->declared) how.job = &s->declared->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(), @@ -428,15 +502,17 @@ 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); + if (state_->declared) kal_process_job_terminate(state_->declared->job); + else if (state_->has_job) kal_process_job_terminate(state_->job); 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); + if (!state_) return; + if (state_->declared) kal_process_job_terminate(state_->declared->job); + else if (state_->has_job) kal_process_job_terminate(state_->job); } void process::detach() { diff --git a/src/process.cppm b/src/process.cppm index 8c5e639..53a3d3e 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -44,6 +44,51 @@ struct grant { // What a started program spends its machine's resources for (openkal 0.23). enum class resources { unstated, balanced, performance, economy }; +// An observation is deliberately three-valued. A lost native reference is +// unknown, and cannot authorize output recovery or reuse of a compiler slot. +enum class unit_state { unknown, occupied, empty }; +class process; + +class unit_observer { +public: + static result open(std::string_view reference); + unit_observer() = default; + unit_observer(unit_observer&&) noexcept; + unit_observer& operator=(unit_observer&&) noexcept; + unit_observer(const unit_observer&) = delete; + unit_observer& operator=(const unit_observer&) = delete; + ~unit_observer(); + result observe() const; +private: + std::uintptr_t job_ = 0; + std::string reference_; +}; + +// The calling program declares its unit before starting work in it. Its +// reference can be retained by another owner before that work is admitted. +// Closing the object releases a reference and ends no member. +class owned_unit { +public: + static result create(); + owned_unit(); + owned_unit(owned_unit&&) noexcept; + owned_unit& operator=(owned_unit&&) noexcept; + owned_unit(const owned_unit&) = delete; + owned_unit& operator=(const owned_unit&) = delete; + ~owned_unit(); + const std::string& reference() const; + // Seals admission, then asks whether every external member has ended. + // Occupied/unknown can be retried. Future spawns into this unit fail. + // The caller remains alive and may finish its own bookkeeping. + result retire(); + // Ends every member outright, including this program when it is a member. + void terminate(); +private: + friend class process; + struct state; + std::shared_ptr state_; +}; + struct spawn_options { // An absolute name, or a bare one looked for on PATH (`env::find_executable'). std::filesystem::path program; @@ -77,6 +122,10 @@ struct spawn_options { // errc::not_supported where the system cannot hold it. bool confine = false; resources preference = resources::unstated; + // Explicitly declared unit, mutually exclusive with own_unit. It must not + // have begun retirement. end_unit/kill then end this entire unit, including + // its owner if the owner is a member. + owned_unit* declared_unit = nullptr; }; // How a program ended: the status it returned, or, ended by its environment diff --git a/tests/unit_test.cpp b/tests/unit_test.cpp new file mode 100644 index 0000000..7cb22f8 --- /dev/null +++ b/tests/unit_test.cpp @@ -0,0 +1,130 @@ +// Declared units and a retained observer over the existing directory IPC. +#include "check.hpp" +import std; +import mcpplibs.os.process; +import mcpplibs.os.ipc; + +namespace process = mcpplibs::os::process; +namespace ipc = mcpplibs::os::ipc; +using clock_type = std::chrono::steady_clock; +using namespace std::chrono_literals; + +static std::optional line(ipc::connection& c, std::chrono::milliseconds bound) { + const auto until = clock_type::now() + bound; + std::string out; + while (out.size() <= 1024) { + const auto left = std::chrono::duration_cast(until - clock_type::now()); + if (left <= 0ms) return {}; + auto read = c.read_for(left, 1024); + if (!read || !*read || (**read).empty()) return {}; + out += **read; + if (const auto end = out.find('\n'); end != std::string::npos) { + if (end + 1 != out.size()) return {}; + out.resize(end); return out; + } + } + return {}; +} + +int main(int argc, char** argv) { + const auto self = std::filesystem::absolute(argv[0]); + if (argc > 2 && std::string_view(argv[1]) == "writer") { + std::ofstream out(argv[2]); out << "declared-unit writer\n"; return out ? 0 : 40; + } + if (argc > 5 && std::string_view(argv[1]) == "helper") { + auto unit = process::owned_unit::create(); + if (!unit) return 30; + const std::filesystem::path output(argv[4]); + if (std::string_view(argv[5]) != "none") { + auto c = ipc::connection::connect(argv[2],argv[3]); + if (!c || !c->write(unit->reference() + "\n")) return 31; + const auto ack = line(*c, std::string_view(argv[5]) == "timeout" ? 50ms : 2s); + if (!ack || *ack != "ACK") return 31; + } + { + auto writer = process::process::spawn({.program=self, .args={"writer",output.string()}, + .out=process::stdio::null, .err=process::stdio::null, .declared_unit=&*unit}); + if (!writer) return 32; + const auto status = writer->wait(); + if (!status || !status->success()) return 33; + } + const auto limit = clock_type::now() + 2s; + for (;;) { + auto retired = unit->retire(); + if (!retired) return 34; + if (*retired == process::unit_state::empty) break; + if (clock_type::now() >= limit) return 35; + std::this_thread::sleep_for(1ms); + } + if (auto again = unit->retire(); !again || *again != process::unit_state::empty) return 36; + auto forbidden = process::process::spawn({.program=self, .args={"writer",output.string()+".forbidden"}, + .declared_unit=&*unit}); + if (forbidden || forbidden.error().code != mcpplibs::os::errc::closed) return 37; + return 0; + } + + const auto identity = process::self(); + if (!identity) return 1; + const auto root = std::filesystem::temp_directory_path() / std::format("os-unit-{}",identity->id); + std::filesystem::create_directories(root); + struct Remove { std::filesystem::path path; ~Remove() { std::error_code ec; std::filesystem::remove_all(path,ec); } } remove{root}; + auto attempt = [&](std::string_view mode, int repeat) { + const auto name = std::format("unit-{}-{}",mode,repeat); + const auto output = root / (name + ".object"); + std::optional listener; + std::optional observer; + bool server_ok = true; + std::jthread server; + const auto started = clock_type::now(); + if (mode != "none") { + auto listening = ipc::listener::listen(root,name); + if (!listening) { check(false,"a unit registration listener is opened"); return; } + listener.emplace(std::move(*listening)); + server = std::jthread([&] { + auto accepted = listener->accept_for(2s); + if (!accepted || !*accepted) { server_ok = false; return; } + auto reference = line(**accepted,2s); + if (!reference) { server_ok = false; return; } + auto retained = process::unit_observer::open(*reference); + if (!retained) { server_ok = false; return; } + observer.emplace(std::move(*retained)); + if (mode == "ack") server_ok = bool((**accepted).write("ACK\n")); + if (mode == "timeout") std::this_thread::sleep_for(100ms); + }); + } + int exit = -1; + { + auto helper = process::process::spawn({.program=self, + .args={"helper",root.string(),name,output.string(),std::string(mode)}, + .in=process::stdio::null, .out=process::stdio::null, .err=process::stdio::null}); + if (helper) { auto status = helper->wait(); if (status) exit=status->shell_code(); } + } + if (server.joinable()) server.join(); + check(server_ok,"the original unit is retained before acknowledgement"); + const bool rejected = mode == "closed" || mode == "timeout"; + check(exit == (rejected ? 31 : 0),"a compiler starts only after successful registration"); + check(std::filesystem::exists(output) != rejected,"a refused acknowledgement produces no output"); + check(!std::filesystem::exists(output.string()+".forbidden"),"retirement prevents further admission"); + if (observer) { + auto state = observer->observe(); + check(state && *state == process::unit_state::empty,"the original unit is empty after helper completion"); + } + const auto elapsed = std::chrono::duration_cast(clock_type::now()-started).count(); + std::printf("{\"unit_start_mode\":\"%.*s\",\"elapsed_ns\":%lld}\n",int(mode.size()),mode.data(),static_cast(elapsed)); + }; + for (int repeat = 0; repeat < 8; ++repeat) { attempt("none",repeat); attempt("ack",repeat); } + attempt("closed",0); attempt("timeout",0); + for (int repeat = 0; repeat < 8; ++repeat) { + const auto name=std::format("idle-{}",repeat); + const auto started=clock_type::now(); + auto listener=ipc::listener::listen(root,name); + check(bool(listener),"a no-op listener opens"); + if (!listener) continue; + std::jthread server([&] { auto peer=listener->accept_for(2s); }); + { auto peer=ipc::connection::connect(root,name); check(bool(peer),"closure wakes the listener without a polling delay"); } + server.join(); listener->close(); + const auto elapsed=std::chrono::duration_cast(clock_type::now()-started).count(); + std::printf("{\"unit_listener_noop_ns\":%lld}\n",static_cast(elapsed)); + } + return g_failures; +} From e5bca2cf37e533eb4cac4902b7cc82afe5dd4540 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 05:38:31 +0900 Subject: [PATCH 2/8] ci: install the native compiler before selecting it --- .github/workflows/unit-lifecycle.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit-lifecycle.yml b/.github/workflows/unit-lifecycle.yml index b997c50..5f0d582 100644 --- a/.github/workflows/unit-lifecycle.yml +++ b/.github/workflows/unit-lifecycle.yml @@ -49,7 +49,12 @@ jobs: run: | xlings update xlings install mcpp@2026.11.5.1 -y -g - mcpp toolchain default '${{ matrix.toolchain }}' + spec='${{ matrix.toolchain }}' + case "$spec" in + msvc@system) mcpp toolchain default msvc ;; + *) mcpp toolchain install "${spec%@*}" "${spec#*@}" + mcpp toolchain default "$spec" ;; + esac - name: Select the coherent source group under review env: IMPLEMENTATION: ${{ matrix.backend }} From 661d6d1fc3ec4749c54a32f76d5fd686db47d87f Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 05:46:30 +0900 Subject: [PATCH 3/8] test: reject observer control and make bootstrap noninteractive --- .github/workflows/unit-lifecycle.yml | 16 ++++++++++++++++ checks/observer-control/mcpp.toml | 7 +++++++ checks/observer-control/src/main.cpp | 9 +++++++++ 3 files changed, 32 insertions(+) create mode 100644 checks/observer-control/mcpp.toml create mode 100644 checks/observer-control/src/main.cpp diff --git a/.github/workflows/unit-lifecycle.yml b/.github/workflows/unit-lifecycle.yml index 5f0d582..e2dd385 100644 --- a/.github/workflows/unit-lifecycle.yml +++ b/.github/workflows/unit-lifecycle.yml @@ -5,6 +5,9 @@ on: concurrency: group: unit-lifecycle-${{ github.ref }} cancel-in-progress: true +env: + XLINGS_NON_INTERACTIVE: '1' + jobs: native: name: declared unit and blocking ACK (${{ matrix.os }}) @@ -77,6 +80,18 @@ jobs: binary=$(find target -type f \( -name unit_test -o -name unit_test.exe \) | head -1) test -n "$binary" "$binary" | tee unit-cost.jsonl + - name: An observer cannot admit or terminate a program + run: | + set +e + (cd checks/observer-control && mcpp build) > observer-control.log 2>&1 + result=$? + set -e + test "$result" -ne 0 + python - <<'PYCONTROL' + from pathlib import Path + log=Path('observer-control.log').read_text() + assert 'unit_observer' in log and ('declared_unit' in log or 'owned_unit' in log) and 'terminate' in log, log + PYCONTROL - uses: actions/upload-artifact@v4 if: always() with: @@ -84,3 +99,4 @@ jobs: path: | unit-lifecycle.log unit-cost.jsonl + observer-control.log diff --git a/checks/observer-control/mcpp.toml b/checks/observer-control/mcpp.toml new file mode 100644 index 0000000..4820191 --- /dev/null +++ b/checks/observer-control/mcpp.toml @@ -0,0 +1,7 @@ +# A negative compilation check: an observer supplies no unit control authority. +[package] +name = "observer-control-misuse" +version = "0.1.0" + +[dependencies] +os = { path = "../.." } diff --git a/checks/observer-control/src/main.cpp b/checks/observer-control/src/main.cpp new file mode 100644 index 0000000..04b0683 --- /dev/null +++ b/checks/observer-control/src/main.cpp @@ -0,0 +1,9 @@ +import std; +import mcpplibs.os.process; + +int main() { + auto observer = mcpplibs::os::process::unit_observer::open("opaque reference"); + mcpplibs::os::process::spawn_options options; + options.declared_unit = &*observer; // An observer cannot admit a program. + observer->terminate(); // It has no unit control operation. +} From dc796dcdbb58af917fbcefa0b801b836443949c5 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 06:11:37 +0900 Subject: [PATCH 4/8] fix: require a live continuation for unit retirement --- .../2026-11-06-declared-unit-continuation.md | 25 +++++++++++++++++++ .github/workflows/unit-lifecycle.yml | 10 ++++---- src/process.cpp | 5 ++-- src/process.cppm | 20 +++++++++------ tests/unit_test.cpp | 12 ++++++--- 5 files changed, 54 insertions(+), 18 deletions(-) create mode 100644 .agents/docs/2026-11-06-declared-unit-continuation.md diff --git a/.agents/docs/2026-11-06-declared-unit-continuation.md b/.agents/docs/2026-11-06-declared-unit-continuation.md new file mode 100644 index 0000000..09e9fdc --- /dev/null +++ b/.agents/docs/2026-11-06-declared-unit-continuation.md @@ -0,0 +1,25 @@ +# Declared units and live continuation identities + +This record describes the source group under review. The public dependency +versions have not yet been advanced to this ABI. + +`owned_unit::retire` seals admission and takes an explicit `program` from +`self` or `describe`. Both id and birth identify a still-live continuation. +A helper may outlive the publisher that started it, so its previous numeric +unit is not a sufficient return location. The build owner is an appropriate +continuation when it remains alive through retirement. An expired identity, +an inaccessible destination or a different native session yields unknown. +Migration does not authorize termination or admission into the continuation. + +`unit_observer` retains the original native observation reference. Its public +surface contains observation and lifetime operations only; native controls +and the native job are private. A compile-failure consumer verifies that it +cannot be supplied as a declared spawn unit or used to terminate a unit. +The C ABI observe/close restriction is a caller precondition on systems whose +unit is a numeric group, rather than a kernel-enforced read-only handle. + +The source tests use the existing directory-local blocking IPC transport. +The native consumers measure paired startup and listener start/stop samples, +retain outliers, and test sealed admission after both unknown and successful +retirement. Three native source builds are required; cross-compilation alone +does not qualify native lifetime behavior or cost. diff --git a/.github/workflows/unit-lifecycle.yml b/.github/workflows/unit-lifecycle.yml index e2dd385..6804988 100644 --- a/.github/workflows/unit-lifecycle.yml +++ b/.github/workflows/unit-lifecycle.yml @@ -17,9 +17,9 @@ jobs: fail-fast: false matrix: include: - - { os: ubuntu-24.04, backend: openkal-linux, toolchain: gcc@16.1.0 } - - { os: macos-15, backend: openkal-macos, toolchain: llvm@23.1.3 } - - { os: windows-2025, backend: openkal-windows, toolchain: msvc@system } + - { os: ubuntu-24.04, backend: openkal-linux, toolchain: gcc@16.1.0, revision: 5d2217424c25f072eb7bb8a7f6c2ea95b4bfa0df } + - { os: macos-15, backend: openkal-macos, toolchain: llvm@23.1.3, revision: 0ad90826b6097c806bc7db7b0c3579acd231b3b7 } + - { os: windows-2025, backend: openkal-windows, toolchain: msvc@system, revision: 664628d0a01a38d028f59fd8c660891efa218679 } defaults: run: shell: bash @@ -28,12 +28,12 @@ jobs: - uses: actions/checkout@v4 with: repository: mcpplibs/openkal - ref: feat/observable-process-units + ref: cdb3d6f79bb5c3322c1d82d24c4e6443ffb13046 path: deps/openkal - uses: actions/checkout@v4 with: repository: mcpplibs/${{ matrix.backend }} - ref: feat/observable-process-units + ref: ${{ matrix.revision }} path: deps/backend - uses: ilammy/msvc-dev-cmd@v1 if: runner.os == 'Windows' diff --git a/src/process.cpp b/src/process.cpp index 7c4aa84..aec45ae 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -151,11 +151,12 @@ const std::string& owned_unit::reference() const { static const std::string empty; return state_ ? state_->reference : empty; } -result owned_unit::retire() { +result owned_unit::retire(const program& continuation) { if (!state_) return std::unexpected(error{errc::invalid,"retiring an empty process unit"}); std::lock_guard guard(state_->admission); state_->sealed = true; kal_u32 observed = kal::process::job_state::unknown; - const int rc = kal_process_job_retire(state_->job,state_->reference.data(),state_->reference.size(),&observed); + const kal_program_identity anchor{continuation.id,continuation.started_ns}; + const int rc = kal_process_job_retire(state_->job,state_->reference.data(),state_->reference.size(),&anchor,&observed); if (rc != kal_ok) return std::unexpected(from_kal(rc,"retiring a process unit")); return unit_state_of(observed); } diff --git a/src/process.cppm b/src/process.cppm index 53a3d3e..e33f5ef 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -44,6 +44,13 @@ struct grant { // What a started program spends its machine's resources for (openkal 0.23). enum class resources { unstated, balanced, performance, economy }; +// A program identity, obtained from self/describe rather than inferred. +struct program { + std::uint64_t id = 0; + std::uint64_t started_ns = 0; + auto operator<=>(const program&) const = default; +}; + // An observation is deliberately three-valued. A lost native reference is // unknown, and cannot authorize output recovery or reuse of a compiler slot. enum class unit_state { unknown, occupied, empty }; @@ -79,8 +86,11 @@ public: const std::string& reference() const; // Seals admission, then asks whether every external member has ended. // Occupied/unknown can be retried. Future spawns into this unit fail. - // The caller remains alive and may finish its own bookkeeping. - result retire(); + // The caller remains alive and may finish its own bookkeeping in the + // continuation's unit. Its full identity is checked; no stale group is used. + // Dead, reused or inaccessible continuations yield unknown. This conveys + // no admission or termination right over the continuation's unit. + result retire(const program& continuation); // Ends every member outright, including this program when it is a member. void terminate(); private: @@ -238,11 +248,7 @@ result exec(spawn_options options); // --- 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; diff --git a/tests/unit_test.cpp b/tests/unit_test.cpp index 7cb22f8..ef4ed23 100644 --- a/tests/unit_test.cpp +++ b/tests/unit_test.cpp @@ -31,10 +31,11 @@ int main(int argc, char** argv) { if (argc > 2 && std::string_view(argv[1]) == "writer") { std::ofstream out(argv[2]); out << "declared-unit writer\n"; return out ? 0 : 40; } - if (argc > 5 && std::string_view(argv[1]) == "helper") { + if (argc > 7 && std::string_view(argv[1]) == "helper") { auto unit = process::owned_unit::create(); if (!unit) return 30; const std::filesystem::path output(argv[4]); + const process::program continuation{std::stoull(argv[6]),std::stoull(argv[7])}; if (std::string_view(argv[5]) != "none") { auto c = ipc::connection::connect(argv[2],argv[3]); if (!c || !c->write(unit->reference() + "\n")) return 31; @@ -48,15 +49,18 @@ int main(int argc, char** argv) { const auto status = writer->wait(); if (!status || !status->success()) return 33; } + auto wrong=continuation;++wrong.started_ns; + auto refused=unit->retire(wrong); + if(!refused||*refused!=process::unit_state::unknown)return 38; const auto limit = clock_type::now() + 2s; for (;;) { - auto retired = unit->retire(); + auto retired = unit->retire(continuation); if (!retired) return 34; if (*retired == process::unit_state::empty) break; if (clock_type::now() >= limit) return 35; std::this_thread::sleep_for(1ms); } - if (auto again = unit->retire(); !again || *again != process::unit_state::empty) return 36; + if (auto again = unit->retire(continuation); !again || *again != process::unit_state::empty) return 36; auto forbidden = process::process::spawn({.program=self, .args={"writer",output.string()+".forbidden"}, .declared_unit=&*unit}); if (forbidden || forbidden.error().code != mcpplibs::os::errc::closed) return 37; @@ -95,7 +99,7 @@ int main(int argc, char** argv) { int exit = -1; { auto helper = process::process::spawn({.program=self, - .args={"helper",root.string(),name,output.string(),std::string(mode)}, + .args={"helper",root.string(),name,output.string(),std::string(mode),std::to_string(identity->id),std::to_string(identity->started_ns)}, .in=process::stdio::null, .out=process::stdio::null, .err=process::stdio::null}); if (helper) { auto status = helper->wait(); if (status) exit=status->shell_code(); } } From 714771bd5765ee8aac5a7f83f68859f2341fb2a9 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 07:58:15 +0900 Subject: [PATCH 5/8] chore: qualify OS 0.2.10 with the observable-unit dependency group --- .../2026-11-06-declared-unit-continuation.md | 9 +++++++ .github/workflows/ci.yml | 14 +++++----- .github/workflows/unit-lifecycle.yml | 8 +++--- README.md | 27 +++++++++++++++++-- checks/hosted/mcpp.toml | 2 +- mcpp.toml | 12 ++++----- 6 files changed, 51 insertions(+), 21 deletions(-) diff --git a/.agents/docs/2026-11-06-declared-unit-continuation.md b/.agents/docs/2026-11-06-declared-unit-continuation.md index 09e9fdc..217782e 100644 --- a/.agents/docs/2026-11-06-declared-unit-continuation.md +++ b/.agents/docs/2026-11-06-declared-unit-continuation.md @@ -23,3 +23,12 @@ The native consumers measure paired startup and listener start/stop samples, retain outliers, and test sealed admission after both unknown and successful retirement. Three native source builds are required; cross-compilation alone does not qualify native lifetime behavior or cost. + +## 2026-11-06 dependency-group preparation + +The final source group names OpenKal 0.24, Linux 0.25, macOS 0.22, Windows +0.20 and Emscripten 0.13. OS 0.2.10 uses platform 0.24 for its hosted and +image consumers. Its public manifest is advanced only after the lower source +archives and index entries are available. Native source controls establish +API behaviour; the final complete package CI and ordinary public resolution +remain separate qualification gates. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c233bca..3de86d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,9 @@ name: CI # run under node (checks/emscripten); a program that starts one # fails at LINK naming the openkal symbol (checks/absent) # -# The library is built with the released mcpp, 2026.11.2.1. The ok images -# (MCPP_PLATFORM) and checks/hosted (its manifest) select openkal-platform -# 0.23.0 and take their C library and C++ run-time from it (openkal 0.23's -# implementations among them; openkal-musl 0.27.0 embeds its allocator, which -# needs mcpp 2026.11 or later). The library +# The library is built with the released mcpp, 2026.11.5.1. The ok images +# (MCPP_PLATFORM) and checks/hosted select openkal-platform 0.24 and its +# OpenKal 0.24 implementation, musl 0.28 and C++ runtime 0.19 group. The library # itself needs 2026.10.27.1 or later, the first that selects a platform # (SPEC-013). A dispatch may name a branch of mcpp instead (`mcpp_ref'), which # the latest released mcpp builds and which is then used. @@ -37,7 +35,7 @@ env: MCPP_SOURCE_REF: ${{ github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF || '' }} # 2026.11.3.1: the std module compiled with the platform's words (openkal-platform 0.23.0's # `-fcf-protection=full` refused every `import std` of an earlier mcpp's). - MCPP_INSTALL: ${{ (github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF) && 'mcpp' || 'mcpp@2026.11.3.1' }} + MCPP_INSTALL: ${{ (github.event.inputs.mcpp_ref || vars.MCPP_SOURCE_REF) && 'mcpp' || 'mcpp@2026.11.5.1' }} XLINGS_VERSION: v2026.10.26.1 XLINGS_NON_INTERACTIVE: '1' @@ -229,7 +227,7 @@ jobs: # openkal 0.23.0, which this release is written against, is the one # openkal-platform 0.23.3's version group names; mcpp 2026.11.3.1 and # later pin it for their openkal rows. - MCPP_PLATFORM: openkal-platform@0.23.3 + MCPP_PLATFORM: openkal-platform@0.24.0 defaults: run: shell: bash @@ -281,7 +279,7 @@ jobs: # openkal 0.23.0, which this release is written against, is the one # openkal-platform 0.23.3's version group names; mcpp 2026.11.3.1 and # later pin it for their openkal rows. - MCPP_PLATFORM: openkal-platform@0.23.3 + MCPP_PLATFORM: openkal-platform@0.24.0 strategy: fail-fast: false matrix: diff --git a/.github/workflows/unit-lifecycle.yml b/.github/workflows/unit-lifecycle.yml index 6804988..d0c1e15 100644 --- a/.github/workflows/unit-lifecycle.yml +++ b/.github/workflows/unit-lifecycle.yml @@ -17,9 +17,9 @@ jobs: fail-fast: false matrix: include: - - { os: ubuntu-24.04, backend: openkal-linux, toolchain: gcc@16.1.0, revision: 5d2217424c25f072eb7bb8a7f6c2ea95b4bfa0df } - - { os: macos-15, backend: openkal-macos, toolchain: llvm@23.1.3, revision: 0ad90826b6097c806bc7db7b0c3579acd231b3b7 } - - { os: windows-2025, backend: openkal-windows, toolchain: msvc@system, revision: 664628d0a01a38d028f59fd8c660891efa218679 } + - { os: ubuntu-24.04, backend: openkal-linux, toolchain: gcc@16.1.0, revision: b429bac4368d1a3960efd6d9dd93c02f229fbeda } + - { os: macos-15, backend: openkal-macos, toolchain: llvm@23.1.3, revision: 5cad63b22b61df2af38d632ff1590f8cbb527130 } + - { os: windows-2025, backend: openkal-windows, toolchain: msvc@system, revision: 07994ed5d8f7741474b42aa83bcda0256fc71bcc } defaults: run: shell: bash @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v4 with: repository: mcpplibs/openkal - ref: cdb3d6f79bb5c3322c1d82d24c4e6443ffb13046 + ref: 1f0571064ccb537a0f80d72e4d6a75ee534ef5cb path: deps/openkal - uses: actions/checkout@v4 with: diff --git a/README.md b/README.md index 609fb62..f70ba9e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ with compile-time platform constants and a few platform modules. ```toml [dependencies] -os = { namespace = "mcpplibs", version = "0.2.9" } +os = { namespace = "mcpplibs", version = "0.2.10" } ``` ```cpp @@ -34,6 +34,29 @@ what is not a kernel question: the machine beneath the system (`machine`), the d loader (`library`: the C library's on Linux and macOS, kernel32's on Windows), and the platform modules. +## Declared process units + +Version 0.2.10 provides `process::owned_unit` and the observation-only +`process::unit_observer`. Create an owned unit before starting its external +members, retain `reference()` in the supervising invocation, and pass the unit +through `spawn_options::declared_unit`. The existing `process::spawn` performs +the start. The creating program is a member; `terminate()` can end it too. +`own_unit` and `declared_unit` are mutually exclusive. + +An observer opens that opaque reference and reports `unit_state::occupied`, +`empty` or `unknown`. Its type exposes neither admission nor termination. +Closing it terminates nothing. Missing native identity, including loss of all +native reference holders on a system that requires them, remains `unknown`. + +`owned_unit::retire(continuation)` seals admission before observing the end of +external members. The continuation identifies the surviving supervising +program outside the helper's unit, obtained from `self` or `describe` with +both identifier and birth retained. A dead, reused or +unreachable continuation does not authorize membership migration. The caller +can finish its bookkeeping after successful retirement. Retirement does not +grant control of the continuation's unit. Output recovery requires a positive +`empty` observation; leader termination alone is insufficient. + ## The program's C library A program over this package chooses its C library. It can be the system's own (glibc, @@ -87,7 +110,7 @@ dropped (0.2.8). | `mcpplibs.os.error` | `errc` (openkal's closed set of conditions), `error`, `result` | | `mcpplibs.os.env` | variables (names without case on Windows), `NAME=value` entries as overrides (`assignments`), arguments, the program's own image, lists of directories in the host's separator (`split_list`, `join_list`), PATH search with PATHEXT | | `mcpplibs.os.fs` | `write_atomic`, `publish` (no replace), `append`, `sync`, `caps`, `lock`, links, `set_executable`, `stamp_of`, `identity_of`, `owner_of`, `writable`, `locate`, `absolute`, `names_a_place`, `canonical`, a bounded `read`; what a volume does, measured (`measure`, `volume_type`), and the operations that follow it (`place`, `exchange`); names in the host's rules without the file system: `generic`, `native`, `extended`, `is_absolute`, `same`, `within` | -| `mcpplibs.os.process` | `spawn`, `run` (and with bounds: a hard bound, a grace, a drain, output limits), `exec`, a command line joined as it is for the command interpreter (`one_string`), pipes, both outputs on one pipe, output onto this program's error output, units and `end_unit`, bound lifetime, bounded waits and reads; `self`, `describe`, `alive`, `threads` | +| `mcpplibs.os.process` | `spawn`, `run` (and with bounds: a hard bound, a grace, a drain, output limits), `exec`, a command line joined as it is for the command interpreter (`one_string`), pipes, both outputs on one pipe, output onto this program's error output, units and `end_unit`, declared units, observation and retirement, bound lifetime, bounded waits and reads; `self`, `describe`, `alive`, `threads` | | `mcpplibs.os.interrupt` | `requested`, `wait`, `end_as_requested`, `end_as_output_closed` | | `mcpplibs.os.stdio` | the three streams as bytes; a bounded read of input; the output sent to the error stream for a while (`output_to_error`) | | `mcpplibs.os.terminal` | `interactive`, `size`, `sequences`, `color`, `raw_mode` (with or without the interrupting keystroke), `foreground` | diff --git a/checks/hosted/mcpp.toml b/checks/hosted/mcpp.toml index bae0a68..5f5f3ba 100644 --- a/checks/hosted/mcpp.toml +++ b/checks/hosted/mcpp.toml @@ -13,7 +13,7 @@ version = "0.1.0" os = { path = "../.." } [target.'cfg(any(linux, macos, windows))'] -platform = "openkal-platform@0.23.3" +platform = "openkal-platform@0.24.0" # The tests are the package's own, each reached by an include from # tests/.cpp here (a test program is a file of the package that runs it). diff --git a/mcpp.toml b/mcpp.toml index f3b2cf3..acfb808 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "os" -version = "0.2.9" +version = "0.2.10" 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"] @@ -12,23 +12,23 @@ 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 = "0.23.0" +openkal = "0.24.0" [target.linux.dependencies] -openkal-linux = "0.24.1" +openkal-linux = "0.25.0" [target.macos.dependencies] -openkal-macos = "0.21.3" +openkal-macos = "0.22.0" [target.windows.dependencies] -openkal-windows = "0.19.0" +openkal-windows = "0.20.0" # Emscripten (`wasm32-emscripten`): openkal written above the C library, with # twelve of openkal's fifteen interfaces. `openkal.process', `openkal.exec' and # `openkal.space' are absent, so a program that starts one fails at link # naming the symbol; `openkal.task' is present in a `-pthread' link only. [target.'cfg(os = "emscripten")'.dependencies] -openkal-emscripten = "0.12.0" +openkal-emscripten = "0.13.0" [targets.os] kind = "lib" From 4fe13f8fea3b0889fe42d3d2521aec68afd99ee6 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 08:05:34 +0900 Subject: [PATCH 6/8] chore: isolate the new kernel ABI group in OS 0.3 --- .agents/docs/2026-11-06-declared-unit-continuation.md | 2 +- README.md | 4 ++-- mcpp.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/docs/2026-11-06-declared-unit-continuation.md b/.agents/docs/2026-11-06-declared-unit-continuation.md index 217782e..e934fe8 100644 --- a/.agents/docs/2026-11-06-declared-unit-continuation.md +++ b/.agents/docs/2026-11-06-declared-unit-continuation.md @@ -27,7 +27,7 @@ does not qualify native lifetime behavior or cost. ## 2026-11-06 dependency-group preparation The final source group names OpenKal 0.24, Linux 0.25, macOS 0.22, Windows -0.20 and Emscripten 0.13. OS 0.2.10 uses platform 0.24 for its hosted and +0.20 and Emscripten 0.13. OS 0.3.0 uses platform 0.24 for its hosted and image consumers. Its public manifest is advanced only after the lower source archives and index entries are available. Native source controls establish API behaviour; the final complete package CI and ordinary public resolution diff --git a/README.md b/README.md index f70ba9e..469704b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ with compile-time platform constants and a few platform modules. ```toml [dependencies] -os = { namespace = "mcpplibs", version = "0.2.10" } +os = { namespace = "mcpplibs", version = "0.3.0" } ``` ```cpp @@ -36,7 +36,7 @@ platform modules. ## Declared process units -Version 0.2.10 provides `process::owned_unit` and the observation-only +Version 0.3.0 provides `process::owned_unit` and the observation-only `process::unit_observer`. Create an owned unit before starting its external members, retain `reference()` in the supervising invocation, and pass the unit through `spawn_options::declared_unit`. The existing `process::spawn` performs diff --git a/mcpp.toml b/mcpp.toml index acfb808..ebc4091 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "os" -version = "0.2.10" +version = "0.3.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"] From d0aa8a5e776a9cf3dfe54454c18ee3f6cb8a4722 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 12:04:47 +0900 Subject: [PATCH 7/8] ci: verify unit lifetime against the published kernel ABI group --- .github/workflows/unit-lifecycle.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit-lifecycle.yml b/.github/workflows/unit-lifecycle.yml index d0c1e15..f78bb66 100644 --- a/.github/workflows/unit-lifecycle.yml +++ b/.github/workflows/unit-lifecycle.yml @@ -17,18 +17,19 @@ jobs: fail-fast: false matrix: include: - - { os: ubuntu-24.04, backend: openkal-linux, toolchain: gcc@16.1.0, revision: b429bac4368d1a3960efd6d9dd93c02f229fbeda } - - { os: macos-15, backend: openkal-macos, toolchain: llvm@23.1.3, revision: 5cad63b22b61df2af38d632ff1590f8cbb527130 } - - { os: windows-2025, backend: openkal-windows, toolchain: msvc@system, revision: 07994ed5d8f7741474b42aa83bcda0256fc71bcc } + - { os: ubuntu-24.04, backend: openkal-linux, toolchain: gcc@16.1.0, revision: 225c545f8353570c277ebe79d0bb47b882826bca } + - { os: macos-15, backend: openkal-macos, toolchain: llvm@23.1.3, revision: 3b9686e9260f4a14169602c865751eea2af5f9e0 } + - { os: windows-2025, backend: openkal-windows, toolchain: msvc@system, revision: 9e8f5ab1c1202bf6a46929e82bec77e2667db730 } defaults: run: shell: bash steps: - uses: actions/checkout@v4 + # Immutable published OpenKal0.24/backend release commits; no prototype refs. - uses: actions/checkout@v4 with: repository: mcpplibs/openkal - ref: 1f0571064ccb537a0f80d72e4d6a75ee534ef5cb + ref: c17bc255b9672325a726f0afa69f15f20c766b73 path: deps/openkal - uses: actions/checkout@v4 with: From da1d722928646cc8e9441334b0a83da0377d9895 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Fri, 6 Nov 2026 12:30:48 +0900 Subject: [PATCH 8/8] test: run unit lifecycle controls through the image loader --- checks/hosted/tests/unit_test.cpp | 6 ++++++ tests/unit_test.cpp | 32 +++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/checks/hosted/tests/unit_test.cpp b/checks/hosted/tests/unit_test.cpp index cdfb715..8a922e6 100644 --- a/checks/hosted/tests/unit_test.cpp +++ b/checks/hosted/tests/unit_test.cpp @@ -1 +1,7 @@ +// Restate imports for the dependency scanner, as the other hosted wrappers do. +#include "../../../tests/check.hpp" +import std; +import mcpplibs.os.process; +import mcpplibs.os.ipc; +import mcpplibs.os.env; #include "../../../tests/unit_test.cpp" diff --git a/tests/unit_test.cpp b/tests/unit_test.cpp index ef4ed23..776c17a 100644 --- a/tests/unit_test.cpp +++ b/tests/unit_test.cpp @@ -3,6 +3,7 @@ import std; import mcpplibs.os.process; import mcpplibs.os.ipc; +import mcpplibs.os.env; namespace process = mcpplibs::os::process; namespace ipc = mcpplibs::os::ipc; @@ -27,7 +28,15 @@ static std::optional line(ipc::connection& c, std::chrono::millisec } int main(int argc, char** argv) { - const auto self = std::filesystem::absolute(argv[0]); + const auto command = mcpplibs::os::env::self_command(); + if (command.empty()) return 41; + auto self = [&](std::vector args) { + process::spawn_options options; + options.program = std::filesystem::u8path(command.front()); + options.args.assign(command.begin() + 1, command.end()); + options.args.insert(options.args.end(), args.begin(), args.end()); + return options; + }; if (argc > 2 && std::string_view(argv[1]) == "writer") { std::ofstream out(argv[2]); out << "declared-unit writer\n"; return out ? 0 : 40; } @@ -43,8 +52,11 @@ int main(int argc, char** argv) { if (!ack || *ack != "ACK") return 31; } { - auto writer = process::process::spawn({.program=self, .args={"writer",output.string()}, - .out=process::stdio::null, .err=process::stdio::null, .declared_unit=&*unit}); + auto options = self({"writer", output.string()}); + options.out = process::stdio::null; + options.err = process::stdio::null; + options.declared_unit = &*unit; + auto writer = process::process::spawn(options); if (!writer) return 32; const auto status = writer->wait(); if (!status || !status->success()) return 33; @@ -61,8 +73,9 @@ int main(int argc, char** argv) { std::this_thread::sleep_for(1ms); } if (auto again = unit->retire(continuation); !again || *again != process::unit_state::empty) return 36; - auto forbidden = process::process::spawn({.program=self, .args={"writer",output.string()+".forbidden"}, - .declared_unit=&*unit}); + auto forbidden_options = self({"writer", output.string()+".forbidden"}); + forbidden_options.declared_unit = &*unit; + auto forbidden = process::process::spawn(forbidden_options); if (forbidden || forbidden.error().code != mcpplibs::os::errc::closed) return 37; return 0; } @@ -98,9 +111,12 @@ int main(int argc, char** argv) { } int exit = -1; { - auto helper = process::process::spawn({.program=self, - .args={"helper",root.string(),name,output.string(),std::string(mode),std::to_string(identity->id),std::to_string(identity->started_ns)}, - .in=process::stdio::null, .out=process::stdio::null, .err=process::stdio::null}); + auto options = self({"helper", root.string(), name, output.string(), std::string(mode), + std::to_string(identity->id), std::to_string(identity->started_ns)}); + options.in = process::stdio::null; + options.out = process::stdio::null; + options.err = process::stdio::null; + auto helper = process::process::spawn(options); if (helper) { auto status = helper->wait(); if (status) exit=status->shell_code(); } } if (server.joinable()) server.join();