From 55b47fe968c9648a6400f6ba8930f873669c01fc Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 9 Nov 2026 05:03:26 +0900 Subject: [PATCH 1/3] 0.4.1: a standard stream kept while it is captured (stdio::capture) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 11 +++++++++ mcpp.toml | 2 +- src/emscripten/stdio.cpp | 18 ++++++++++++++ src/linux/arch/aarch64.cpp | 8 ++++++- src/linux/arch/riscv64.cpp | 8 ++++++- src/linux/arch/x86_64.cpp | 8 ++++++- src/macos/arch/aarch64.cpp | 7 +++++- src/macos/arch/x86_64.cpp | 7 +++++- src/openkal/stdio.cpp | 2 ++ src/posix/kernel.h | 3 +++ src/posix/stdio.cpp | 18 ++++++++++++++ src/stdio.cpp | 47 +++++++++++++++++++++++++++++++++++++ src/stdio.cppm | 30 +++++++++++++++++++++++ src/windows/crt/stdio.cpp | 22 +++++++++++++++++ src/windows/posix/stdio.cpp | 44 ++++++++++++++++++++++++++++++++++ tests/process_test.cpp | 24 +++++++++++++++++++ 16 files changed, 253 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d2ee74..1850244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.4.1 + +- **A standard stream kept (`stdio::capture`).** `capture::begin(stream::error)` + (or `stream::output`) makes the stream a file until `text()` or destruction puts + it back, and `text()` gives what was written meanwhile: through openkal, through + the C library and the C++ streams, and by the programs started meanwhile. A + test reads what a program prints with it. Each system rearranges its own + descriptor (the kernel's on Linux and macOS, the C runtime's and the standard + handle on Windows, the C library's under Emscripten); an image answers + `not_supported`, as it does for `output_to_error`. + ## 0.4.0 - **`is_openkal`, named as its platform is.** A build for the openkal target diff --git a/mcpp.toml b/mcpp.toml index a6407b6..7d92b89 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "os" -version = "0.4.0" +version = "0.4.1" 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"] diff --git a/src/emscripten/stdio.cpp b/src/emscripten/stdio.cpp index 3db221d..c16b14e 100644 --- a/src/emscripten/stdio.cpp +++ b/src/emscripten/stdio.cpp @@ -27,6 +27,24 @@ void restore_output(int saved) { ::close(saved); } +int divert_to_file(int descriptor, const std::filesystem::path& file) { + const int target = ::open(file.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600); + if (target < 0) return -1; + const int saved = ::fcntl(descriptor, F_DUPFD_CLOEXEC, 3); + if (saved < 0 || ::dup2(target, descriptor) < 0) { + if (saved >= 0) ::close(saved); + ::close(target); + return -1; + } + ::close(target); + return saved; +} + +void restore_descriptor(int descriptor, int saved) { + ::dup2(saved, descriptor); + ::close(saved); +} + bool write_descriptor(int descriptor, std::string_view bytes) { std::size_t done = 0; while (done < bytes.size()) { diff --git a/src/linux/arch/aarch64.cpp b/src/linux/arch/aarch64.cpp index 900f10e..f58dd23 100644 --- a/src/linux/arch/aarch64.cpp +++ b/src/linux/arch/aarch64.cpp @@ -17,7 +17,8 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return x0; } -constexpr long nr_write = 64, +constexpr long nr_openat = 56, + nr_write = 64, nr_close = 57, nr_fcntl = 25, nr_dup3 = 24, @@ -50,6 +51,11 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long open_write(const char* path) { + // openat(AT_FDCWD, path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) + return call(nr_openat, -100, reinterpret_cast(path), 01 | 0100 | 01000 | 02000000, 0600); +} + void default_signal(int signal) { // The kernel's `struct sigaction', zero: SIG_DFL, no flags, nothing masked. alignas(8) unsigned long long action[4] = {}; diff --git a/src/linux/arch/riscv64.cpp b/src/linux/arch/riscv64.cpp index c578871..fc9324a 100644 --- a/src/linux/arch/riscv64.cpp +++ b/src/linux/arch/riscv64.cpp @@ -17,7 +17,8 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return a0; } -constexpr long nr_write = 64, +constexpr long nr_openat = 56, + nr_write = 64, nr_close = 57, nr_fcntl = 25, nr_dup3 = 24, @@ -50,6 +51,11 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long open_write(const char* path) { + // openat(AT_FDCWD, path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) + return call(nr_openat, -100, reinterpret_cast(path), 01 | 0100 | 01000 | 02000000, 0600); +} + void default_signal(int signal) { // The kernel's `struct sigaction', zero: SIG_DFL, no flags, nothing masked. alignas(8) unsigned long long action[4] = {}; diff --git a/src/linux/arch/x86_64.cpp b/src/linux/arch/x86_64.cpp index 68c08e2..5171d40 100644 --- a/src/linux/arch/x86_64.cpp +++ b/src/linux/arch/x86_64.cpp @@ -17,7 +17,8 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return r; } -constexpr long nr_write = 1, +constexpr long nr_openat = 257, + nr_write = 1, nr_close = 3, nr_fcntl = 72, nr_dup3 = 292, @@ -50,6 +51,11 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long open_write(const char* path) { + // openat(AT_FDCWD, path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) + return call(nr_openat, -100, reinterpret_cast(path), 01 | 0100 | 01000 | 02000000, 0600); +} + void default_signal(int signal) { // The kernel's `struct sigaction', zero: SIG_DFL, no flags, nothing masked. alignas(8) unsigned long long action[4] = {}; diff --git a/src/macos/arch/aarch64.cpp b/src/macos/arch/aarch64.cpp index c613d13..0a84156 100644 --- a/src/macos/arch/aarch64.cpp +++ b/src/macos/arch/aarch64.cpp @@ -24,7 +24,7 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return failed ? -x0 : x0; } -constexpr long nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, +constexpr long nr_open = 5, nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, nr_sigprocmask = 48, nr_dup2 = 90, nr_fcntl = 92, nr_statfs64 = 345, nr_renameatx_np = 488, nr_geteuid = 25, nr_umask = 60; @@ -44,6 +44,11 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long open_write(const char* path) { + // open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) + return call(nr_open, reinterpret_cast(path), 0x1 | 0x200 | 0x400 | 0x1000000, 0600); +} + void default_signal(int signal) { // The kernel's `struct __sigaction' (handler, trampoline, mask, flags), // zero: SIG_DFL, which the kernel enters no trampoline for. diff --git a/src/macos/arch/x86_64.cpp b/src/macos/arch/x86_64.cpp index 7066384..23ca933 100644 --- a/src/macos/arch/x86_64.cpp +++ b/src/macos/arch/x86_64.cpp @@ -21,7 +21,7 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return failed ? -r : r; } -constexpr long nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, +constexpr long nr_open = 5, nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, nr_sigprocmask = 48, nr_dup2 = 90, nr_fcntl = 92, nr_statfs64 = 345, nr_renameatx_np = 488, nr_geteuid = 25, nr_umask = 60; @@ -41,6 +41,11 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long open_write(const char* path) { + // open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) + return call(nr_open, reinterpret_cast(path), 0x1 | 0x200 | 0x400 | 0x1000000, 0600); +} + void default_signal(int signal) { // The kernel's `struct __sigaction' (handler, trampoline, mask, flags), // zero: SIG_DFL, which the kernel enters no trampoline for. diff --git a/src/openkal/stdio.cpp b/src/openkal/stdio.cpp index eb49a37..1b1ab6e 100644 --- a/src/openkal/stdio.cpp +++ b/src/openkal/stdio.cpp @@ -7,4 +7,6 @@ namespace mcpplibs::os::stdio::sys { int divert_output() { return -1; } void restore_output(int) {} bool write_descriptor(int, std::string_view) { return false; } +int divert_to_file(int, const std::filesystem::path&) { return -1; } +void restore_descriptor(int, int) {} } // namespace mcpplibs::os::stdio::sys diff --git a/src/posix/kernel.h b/src/posix/kernel.h index 2f511d2..70d017f 100644 --- a/src/posix/kernel.h +++ b/src/posix/kernel.h @@ -24,6 +24,9 @@ long dup_above(int fd, int lowest); long dup_onto(int from, int to); long write(int fd, const void* bytes, unsigned long count); long close(int fd); +// A new descriptor for writing the file `path' names, created (mode 0600) or +// emptied, closed on exec. +long open_write(const char* path); // The signal at its default disposition. void default_signal(int signal); // The signal unblocked in this thread and sent to this process. diff --git a/src/posix/stdio.cpp b/src/posix/stdio.cpp index 0455128..af27004 100644 --- a/src/posix/stdio.cpp +++ b/src/posix/stdio.cpp @@ -24,6 +24,24 @@ void restore_output(int saved) { k::close(saved); } +int divert_to_file(int descriptor, const std::filesystem::path& file) { + const long target = k::open_write(file.c_str()); + if (target < 0) return -1; + const long saved = k::dup_above(descriptor, 3); + if (saved < 0) { + k::close(static_cast(target)); + return -1; + } + k::dup_onto(static_cast(target), descriptor); + k::close(static_cast(target)); + return static_cast(saved); +} + +void restore_descriptor(int descriptor, int saved) { + k::dup_onto(saved, descriptor); + k::close(saved); +} + bool write_descriptor(int descriptor, std::string_view bytes) { std::size_t done = 0; while (done < bytes.size()) { diff --git a/src/stdio.cpp b/src/stdio.cpp index 0b756d3..6a0f684 100644 --- a/src/stdio.cpp +++ b/src/stdio.cpp @@ -91,6 +91,53 @@ output_to_error::~output_to_error() { sys::restore_output(saved_); } +result capture::begin(stream which) { + std::fflush(nullptr); + (void)flush_output(); + capture c; + c.descriptor_ = which == stream::output ? 1 : 2; + c.file_ = std::filesystem::temp_directory_path() + / std::format("mcpplibs-os-capture-{}-{:x}", c.descriptor_, std::random_device{}()); + c.saved_ = sys::divert_to_file(c.descriptor_, c.file_); + if (c.saved_ < 0) { + std::error_code ec; + std::filesystem::remove(c.file_, ec); + return std::unexpected(error{ errc::not_supported, "keeping what a standard stream is written" }); + } + return c; +} + +capture::capture(capture&& o) noexcept + : descriptor_(std::exchange(o.descriptor_, -1)), saved_(std::exchange(o.saved_, -1)), file_(std::move(o.file_)) {} +capture& capture::operator=(capture&& o) noexcept { + if (this != &o) { + end(); + descriptor_ = std::exchange(o.descriptor_, -1); + saved_ = std::exchange(o.saved_, -1); + file_ = std::move(o.file_); + } + return *this; +} +capture::~capture() { end(); } + +void capture::end() { + if (saved_ < 0) return; + std::fflush(nullptr); + (void)flush_output(); + sys::restore_descriptor(descriptor_, std::exchange(saved_, -1)); +} + +std::string capture::text() { + if (file_.empty()) return {}; + end(); + std::string kept; + if (std::ifstream in(file_, std::ios::binary); in) kept.assign(std::istreambuf_iterator(in), {}); + std::error_code ec; + std::filesystem::remove(file_, ec); + file_.clear(); + return kept; +} + result output_to_error::write_original(std::string_view bytes) { if (saved_ < 0) return write_output(bytes); if (!sys::write_descriptor(saved_, bytes)) return std::unexpected(error{ errc::io, "writing standard output" }); diff --git a/src/stdio.cppm b/src/stdio.cppm index 5c10d9a..a385dea 100644 --- a/src/stdio.cppm +++ b/src/stdio.cppm @@ -48,6 +48,32 @@ private: int saved_ = -1; }; +// WHAT THIS PROGRAM WRITES TO ONE OF ITS STANDARD STREAMS, KEPT (0.4.1): from +// `begin' the stream is a file, so what the program writes there -- through +// openkal, its C library or the C++ streams -- and what the programs it starts +// meanwhile write is kept; `text' (or destruction) puts the stream back and +// gives what was kept. A test reads what a program prints with it. +// errc::not_supported where the streams cannot be rearranged (an image). +enum class stream { output, error }; + +class capture { +public: + static result begin(stream which); + capture() = default; + capture(capture&&) noexcept; + capture& operator=(capture&&) noexcept; + capture(const capture&) = delete; + capture& operator=(const capture&) = delete; + ~capture(); + // The stream put back, and what was written to it; empty after the first call. + std::string text(); +private: + void end(); + int descriptor_ = -1; + int saved_ = -1; + std::filesystem::path file_; +}; + } // namespace mcpplibs::os::stdio // Each system's own (src//stdio.cpp): the output descriptor saved and @@ -56,4 +82,8 @@ namespace mcpplibs::os::stdio::sys { int divert_output(); void restore_output(int saved); bool write_descriptor(int descriptor, std::string_view bytes); +// `descriptor' (1 or 2) saved (-1 when not possible) and made one for writing +// `file', created or emptied; put back. +int divert_to_file(int descriptor, const std::filesystem::path& file); +void restore_descriptor(int descriptor, int saved); } // namespace mcpplibs::os::stdio::sys diff --git a/src/windows/crt/stdio.cpp b/src/windows/crt/stdio.cpp index 27ff7d9..fbef533 100644 --- a/src/windows/crt/stdio.cpp +++ b/src/windows/crt/stdio.cpp @@ -13,6 +13,7 @@ int __cdecl _dup2(int, int); int __cdecl _close(int); long long __cdecl _get_osfhandle(int); int __cdecl _write(int, const void*, unsigned); +int __cdecl _wopen(const wchar_t*, int, ...); } module mcpplibs.os.stdio; @@ -36,6 +37,27 @@ void restore_output(int saved) { _close(saved); } +int divert_to_file(int descriptor, const std::filesystem::path& file) { + // _O_WRONLY | _O_CREAT | _O_TRUNC | _O_BINARY | _O_NOINHERIT, _S_IREAD | _S_IWRITE + const int target = _wopen(file.c_str(), 0x1 | 0x100 | 0x200 | 0x8000 | 0x80, 0x100 | 0x80); + if (target < 0) return -1; + const int saved = _dup(descriptor); + if (saved < 0) { + _close(target); + return -1; + } + if (const long long h = _get_osfhandle(saved); h != -1) + SetHandleInformation(reinterpret_cast(h), 0x1 /* HANDLE_FLAG_INHERIT */, 0); + _dup2(target, descriptor); // also the process's standard handle + _close(target); + return saved; +} + +void restore_descriptor(int descriptor, int saved) { + _dup2(saved, descriptor); + _close(saved); +} + bool write_descriptor(int descriptor, std::string_view bytes) { std::size_t done = 0; while (done < bytes.size()) { diff --git a/src/windows/posix/stdio.cpp b/src/windows/posix/stdio.cpp index ccf5e31..33cb3bf 100644 --- a/src/windows/posix/stdio.cpp +++ b/src/windows/posix/stdio.cpp @@ -51,6 +51,50 @@ void restore_output(int saved) { SetHandleInformation(k.handle, kInherit, k.flags & kInherit); } +// The C library's descriptor and the process's standard handle are made two +// writers of one file, each appending, so neither overwrites what the other wrote. +int divert_to_file(int descriptor, const std::filesystem::path& file) { + const auto name = file.string(); + const int target = ::open(name.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_APPEND | O_CLOEXEC, 0600); + if (target < 0) return -1; + std::u16string wide; + for (unsigned char c : name) wide.push_back(static_cast(c)); + // FILE_APPEND_DATA | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING + const okos_handle appended = CreateFileW(wide.c_str(), 0x4 | 0x100000, 0x1 | 0x2, nullptr, 3, 0x80, nullptr); + const int saved = ::fcntl(descriptor, F_DUPFD_CLOEXEC, 3); + if (saved < 0 || appended == reinterpret_cast(-1)) { + if (saved >= 0) ::close(saved); + if (appended != reinterpret_cast(-1)) CloseHandle(appended); + ::close(target); + return -1; + } + ::dup2(target, descriptor); + ::close(target); + const okos_dword which = descriptor == 1 ? kOutput : kError; + const std::lock_guard lock(guard); + const okos_handle was = GetStdHandle(which); + okos_dword flags = 0; + if (!GetHandleInformation(was, &flags)) flags = 0; + given.push_back({ was, flags }); + SetHandleInformation(was, kInherit, 0); + late::SetStdHandle(which, appended); + return saved; +} + +void restore_descriptor(int descriptor, int saved) { + ::dup2(saved, descriptor); + ::close(saved); + const okos_dword which = descriptor == 1 ? kOutput : kError; + const std::lock_guard lock(guard); + if (given.empty()) return; + const kept k = given.back(); + given.pop_back(); + const okos_handle appended = GetStdHandle(which); + late::SetStdHandle(which, k.handle); + SetHandleInformation(k.handle, kInherit, k.flags & kInherit); + if (appended != k.handle) CloseHandle(appended); +} + bool write_descriptor(int descriptor, std::string_view bytes) { std::size_t done = 0; while (done < bytes.size()) { diff --git a/tests/process_test.cpp b/tests/process_test.cpp index e5725d7..ac24cd0 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -114,6 +114,24 @@ int child(std::string_view errand) { (void)d->write_original("document"); return 0; } + if (errand == "capture") { + // Both streams kept, written through openkal, the C library and a started program. + auto e = os::stdio::capture::begin(os::stdio::stream::error); + if (!e) { + (void)os::stdio::write_output("unsupported"); + return 0; + } + (void)os::stdio::write_error("kal|"); + std::fputs("libc|", stderr); + auto o = copy_of_self("say"); + o.out = os::process::stdio::error; + o.err = os::process::stdio::inherit; + if (auto p = os::process::process::spawn(o)) (void)p->wait(); + const auto kept = e->text(); + (void)os::stdio::write_error("after"); + (void)os::stdio::write_output("[" + kept + "]"); + return 0; + } if (errand == "end-closed") { os::interrupt::end_as_output_closed(); return 5; // where the system has no such ending @@ -389,6 +407,12 @@ int main(int argc, char** argv) { auto d = os::process::run(copy("divert")); check(d && d->out == "document" && d->err == "narration", "output sent to the error stream for a while, and the document to the output as it was"); + auto c = os::process::run(copy("capture")); + const bool kept = c && c->out == "[kal|libc|said]" && c->err == "after"; + const bool refused = c && c->out == "unsupported" && os::is_openkal; + if (!kept && !refused && c) + std::printf(" (the copy wrote '%s' and '%s')\n", c->out.c_str(), c->err.c_str()); + check(kept || refused, "a standard stream kept while it is captured, with what a started program wrote, then put back"); } { // The command interpreter given a command with quotation marks, joined From da628a7711828ceeba30c322dfa7bf45bc273a77 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 9 Nov 2026 05:57:56 +0900 Subject: [PATCH 2/3] 0.4.1: what a program used (process::resources) Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 8 ++++++++ src/emscripten/exec.cpp | 4 ++++ src/linux/arch/aarch64.cpp | 9 ++++++++- src/linux/arch/riscv64.cpp | 9 ++++++++- src/linux/arch/x86_64.cpp | 9 ++++++++- src/macos/arch/aarch64.cpp | 8 +++++++- src/macos/arch/x86_64.cpp | 8 +++++++- src/openkal/exec.cpp | 4 ++++ src/posix/exec.cpp | 22 ++++++++++++++++++++++ src/posix/kernel.h | 5 +++++ src/process.cpp | 2 ++ src/process.cppm | 17 +++++++++++++++++ src/windows/exec.cpp | 25 +++++++++++++++++++++++++ src/windows/win32.h | 22 ++++++++++++++++++++++ tests/process_test.cpp | 23 +++++++++++++++++++++++ 15 files changed, 170 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1850244..da3ba00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ descriptor (the kernel's on Linux and macOS, the C runtime's and the standard handle on Windows, the C library's under Emscripten); an image answers `not_supported`, as it does for `output_to_error`. +- **What a program used (`process::resources`).** `resources(of::self)` gives + this program's user and system processor time, its largest resident set and + the bytes its reads and writes moved; `resources(of::children)` the same of + the programs it started and waited for, together, as the kernel accounts them + (Linux and macOS, by `getrusage`). A field the system does not tell is absent; + Windows tells a program's own use and not its children's; an image and the Web + answer `not_supported`. A test runner measures a program by starting it from a + process of its own and asking for that process's children. ## 0.4.0 diff --git a/src/emscripten/exec.cpp b/src/emscripten/exec.cpp index 2738cf3..efab607 100644 --- a/src/emscripten/exec.cpp +++ b/src/emscripten/exec.cpp @@ -11,4 +11,8 @@ result replace_image(const std::filesystem::path&, const std::vector resources(of) { + return std::unexpected(error{ errc::not_supported, "this system tells no program's use of its resources" }); +} + } // namespace mcpplibs::os::process::sys diff --git a/src/linux/arch/aarch64.cpp b/src/linux/arch/aarch64.cpp index f58dd23..ad7ad1b 100644 --- a/src/linux/arch/aarch64.cpp +++ b/src/linux/arch/aarch64.cpp @@ -17,7 +17,8 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return x0; } -constexpr long nr_openat = 56, +constexpr long nr_getrusage = 165, + nr_openat = 56, nr_write = 64, nr_close = 57, nr_fcntl = 25, @@ -51,6 +52,12 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long resource_usage(int who, long (&out)[18]) { + return call(nr_getrusage, who, reinterpret_cast(out)); +} + +unsigned long maxrss_unit() { return 1024; } // kilobytes + long open_write(const char* path) { // openat(AT_FDCWD, path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) return call(nr_openat, -100, reinterpret_cast(path), 01 | 0100 | 01000 | 02000000, 0600); diff --git a/src/linux/arch/riscv64.cpp b/src/linux/arch/riscv64.cpp index fc9324a..2583318 100644 --- a/src/linux/arch/riscv64.cpp +++ b/src/linux/arch/riscv64.cpp @@ -17,7 +17,8 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return a0; } -constexpr long nr_openat = 56, +constexpr long nr_getrusage = 165, + nr_openat = 56, nr_write = 64, nr_close = 57, nr_fcntl = 25, @@ -51,6 +52,12 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long resource_usage(int who, long (&out)[18]) { + return call(nr_getrusage, who, reinterpret_cast(out)); +} + +unsigned long maxrss_unit() { return 1024; } // kilobytes + long open_write(const char* path) { // openat(AT_FDCWD, path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) return call(nr_openat, -100, reinterpret_cast(path), 01 | 0100 | 01000 | 02000000, 0600); diff --git a/src/linux/arch/x86_64.cpp b/src/linux/arch/x86_64.cpp index 5171d40..1e220e8 100644 --- a/src/linux/arch/x86_64.cpp +++ b/src/linux/arch/x86_64.cpp @@ -17,7 +17,8 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return r; } -constexpr long nr_openat = 257, +constexpr long nr_getrusage = 98, + nr_openat = 257, nr_write = 1, nr_close = 3, nr_fcntl = 72, @@ -51,6 +52,12 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long resource_usage(int who, long (&out)[18]) { + return call(nr_getrusage, who, reinterpret_cast(out)); +} + +unsigned long maxrss_unit() { return 1024; } // kilobytes + long open_write(const char* path) { // openat(AT_FDCWD, path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) return call(nr_openat, -100, reinterpret_cast(path), 01 | 0100 | 01000 | 02000000, 0600); diff --git a/src/macos/arch/aarch64.cpp b/src/macos/arch/aarch64.cpp index 0a84156..7a28eb3 100644 --- a/src/macos/arch/aarch64.cpp +++ b/src/macos/arch/aarch64.cpp @@ -24,7 +24,7 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return failed ? -x0 : x0; } -constexpr long nr_open = 5, nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, +constexpr long nr_getrusage = 117, nr_open = 5, nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, nr_sigprocmask = 48, nr_dup2 = 90, nr_fcntl = 92, nr_statfs64 = 345, nr_renameatx_np = 488, nr_geteuid = 25, nr_umask = 60; @@ -44,6 +44,12 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long resource_usage(int who, long (&out)[18]) { + return call(nr_getrusage, who, reinterpret_cast(out)); +} + +unsigned long maxrss_unit() { return 1; } // bytes + long open_write(const char* path) { // open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) return call(nr_open, reinterpret_cast(path), 0x1 | 0x200 | 0x400 | 0x1000000, 0600); diff --git a/src/macos/arch/x86_64.cpp b/src/macos/arch/x86_64.cpp index 23ca933..014aa79 100644 --- a/src/macos/arch/x86_64.cpp +++ b/src/macos/arch/x86_64.cpp @@ -21,7 +21,7 @@ long call(long n, long a = 0, long b = 0, long c = 0, long d = 0, long e = 0) { return failed ? -r : r; } -constexpr long nr_open = 5, nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, +constexpr long nr_getrusage = 117, nr_open = 5, nr_write = 4, nr_close = 6, nr_getpid = 20, nr_kill = 37, nr_sigaction = 46, nr_sigprocmask = 48, nr_dup2 = 90, nr_fcntl = 92, nr_statfs64 = 345, nr_renameatx_np = 488, nr_geteuid = 25, nr_umask = 60; @@ -41,6 +41,12 @@ long write(int fd, const void* bytes, unsigned long count) { long close(int fd) { return call(nr_close, fd); } +long resource_usage(int who, long (&out)[18]) { + return call(nr_getrusage, who, reinterpret_cast(out)); +} + +unsigned long maxrss_unit() { return 1; } // bytes + long open_write(const char* path) { // open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600) return call(nr_open, reinterpret_cast(path), 0x1 | 0x200 | 0x400 | 0x1000000, 0600); diff --git a/src/openkal/exec.cpp b/src/openkal/exec.cpp index a83784f..39ebe74 100644 --- a/src/openkal/exec.cpp +++ b/src/openkal/exec.cpp @@ -12,4 +12,8 @@ result replace_image(const std::filesystem::path&, const std::vector resources(of) { + return std::unexpected(error{ errc::not_supported, "this system tells no program's use of its resources" }); +} + } // namespace mcpplibs::os::process::sys diff --git a/src/posix/exec.cpp b/src/posix/exec.cpp index 675f515..66e5450 100644 --- a/src/posix/exec.cpp +++ b/src/posix/exec.cpp @@ -14,6 +14,28 @@ import mcpplibs.os.platform; namespace mcpplibs::os::process::sys { +result resources(of who) { + long r[18] = {}; + if (kernel::resource_usage(who == of::self ? 0 : -1, r) < 0) + return std::unexpected(error{ errc::io, "asking the kernel what was used" }); + usage u; + u.user = std::chrono::seconds(r[0]) + std::chrono::microseconds(r[1]); + u.system = std::chrono::seconds(r[2]) + std::chrono::microseconds(r[3]); + u.peak_memory = static_cast(r[4]) * kernel::maxrss_unit(); + // The bytes moved by reads and writes, where the kernel counts them for + // this process: /proc/self/io on Linux. The children's are not counted. + if (who == of::self) + if (std::ifstream io("/proc/self/io"); io) { + std::string key; + std::uint64_t value = 0; + while (io >> key >> value) { + if (key == "rchar:") u.read_bytes = value; + else if (key == "wchar:") u.written_bytes = value; + } + } + return u; +} + result replace_image(const std::filesystem::path& program, const std::vector& argv, const std::vector& environment, diff --git a/src/posix/kernel.h b/src/posix/kernel.h index 70d017f..4e7ed0b 100644 --- a/src/posix/kernel.h +++ b/src/posix/kernel.h @@ -27,6 +27,11 @@ long close(int fd); // A new descriptor for writing the file `path' names, created (mode 0600) or // emptied, closed on exec. long open_write(const char* path); +// The kernel's `struct rusage' of this process (`who' 0) or of its waited-for +// children (`who' -1): two timevals and fourteen longs, as on every 64-bit +// Linux and macOS. `ru_maxrss' is in units of `maxrss_unit()' bytes. +long resource_usage(int who, long (&out)[18]); +unsigned long maxrss_unit(); // The signal at its default disposition. void default_signal(int signal); // The signal unblocked in this thread and sent to this process. diff --git a/src/process.cpp b/src/process.cpp index aec45ae..35699b8 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -690,6 +690,8 @@ result self() { return program{ info.id, info.started_ns }; } +result resources(of who) { return sys::resources(who); } + result describe(std::uint64_t id) { kal_process p{}; if (const int rc = kal_process_open_id(id, &p); rc != kal_ok) diff --git a/src/process.cppm b/src/process.cppm index e33f5ef..5e9435b 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -256,6 +256,21 @@ struct program_info { bool ended = false; }; +// WHAT A PROGRAM HAS USED (0.4.1): processor time, the largest resident set and +// the bytes moved by its reads and writes. `of::self' is this program so far; +// `of::children' the programs it started and has waited for, together, as the +// kernel accounts them (Linux and macOS). A runner that starts one program and +// asks for its children has that program's use. A field the system does not +// tell is absent; errc::not_supported where nothing is told (an image, the Web, +// a Windows program's children). +enum class of { self, children }; +struct usage { + std::chrono::nanoseconds user{0}, system{0}; + std::optional peak_memory; // bytes + std::optional read_bytes, written_bytes; +}; +result resources(of who = of::self); + // This program. result self(); // What is known of the program with this identifier now; errc::not_found @@ -283,4 +298,6 @@ result replace_image(const std::filesystem::path& program, const std::vector& argv, const std::vector& environment, const std::filesystem::path& cwd); +// What `resources' reports, asked of the system (src//exec.cpp). +result resources(of who); } // namespace mcpplibs::os::process::sys diff --git a/src/windows/exec.cpp b/src/windows/exec.cpp index a83784f..a3a0572 100644 --- a/src/windows/exec.cpp +++ b/src/windows/exec.cpp @@ -1,5 +1,8 @@ // This system replaces no program's image: `exec' starts the program and ends // as it ended (src/process.cpp). +module; +#include "win32.h" + module mcpplibs.os.process; import std; @@ -12,4 +15,26 @@ result replace_image(const std::filesystem::path&, const std::vector resources(of who) { + if (who == of::children) + return std::unexpected(error{ errc::not_supported, "Windows does not account a program's children together" }); + usage u; + unsigned long long created = 0, exited = 0, kernel = 0, user = 0; + if (late::GetProcessTimes(GetCurrentProcess(), &created, &exited, &kernel, &user)) { + u.user = std::chrono::nanoseconds(user * 100); // 100 ns units + u.system = std::chrono::nanoseconds(kernel * 100); + } + okos_memory_counters m{}; + m.cb = sizeof m; + if (late::K32GetProcessMemoryInfo(GetCurrentProcess(), &m, sizeof m)) u.peak_memory = m.peak_working_set; + unsigned long long io[6] = {}; + if (late::GetProcessIoCounters(GetCurrentProcess(), &io)) { + u.read_bytes = io[3]; + u.written_bytes = io[4]; + } + return u; +} + } // namespace mcpplibs::os::process::sys diff --git a/src/windows/win32.h b/src/windows/win32.h index 7a496c4..04f9a75 100644 --- a/src/windows/win32.h +++ b/src/windows/win32.h @@ -119,6 +119,28 @@ inline unsigned long long GetTickCount64() { static const auto f = find("GetTickCount64"); return f(); } +// The process's times (FILETIME: two dwords each), its memory counters +// (PROCESS_MEMORY_COUNTERS) and its I/O counters (IO_COUNTERS: six 64-bit counts). +inline okos_bool GetProcessTimes(okos_handle a0, unsigned long long* a1, unsigned long long* a2, + unsigned long long* a3, unsigned long long* a4) { + static const auto f = find("GetProcessTimes"); + return f(a0, a1, a2, a3, a4); +} +struct okos_memory_counters { + okos_dword cb, page_faults; + unsigned long long peak_working_set, working_set, quota_peak_paged, quota_paged, + quota_peak_nonpaged, quota_nonpaged, pagefile, peak_pagefile; +}; +inline okos_bool K32GetProcessMemoryInfo(okos_handle a0, okos_memory_counters* a1, okos_dword a2) { + static const auto f = find( + "K32GetProcessMemoryInfo"); + return f(a0, a1, a2); +} +inline okos_bool GetProcessIoCounters(okos_handle a0, unsigned long long (*a1)[6]) { + static const auto f = find("GetProcessIoCounters"); + return f(a0, a1); +} inline okos_bool SetStdHandle(okos_dword a0, okos_handle a1) { static const auto f = find("SetStdHandle"); return f(a0, a1); diff --git a/tests/process_test.cpp b/tests/process_test.cpp index ac24cd0..9663578 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -132,6 +132,15 @@ int child(std::string_view errand) { (void)os::stdio::write_output("[" + kept + "]"); return 0; } + if (errand == "use") { + // 64 MiB touched and some processor time, for the parent's account of + // its children. + std::vector block(64u << 20); + for (std::size_t i = 0; i < block.size(); i += 4096) block[i] = static_cast(i); + volatile std::uint64_t x = 0; + for (std::uint64_t i = 0; i < 50'000'000; ++i) x = x + i * static_cast(block[i % block.size()]); + return x == 1 ? 9 : 0; + } if (errand == "end-closed") { os::interrupt::end_as_output_closed(); return 5; // where the system has no such ending @@ -413,6 +422,20 @@ int main(int argc, char** argv) { if (!kept && !refused && c) std::printf(" (the copy wrote '%s' and '%s')\n", c->out.c_str(), c->err.c_str()); check(kept || refused, "a standard stream kept while it is captured, with what a started program wrote, then put back"); + // What a program used: its own, and its waited-for children's together. + auto mine = os::process::resources(os::process::of::self); + check(mine ? mine->peak_memory.value_or(0) > 0 : os::is_openkal || os::is_emscripten, + "this program's largest resident set is known"); + auto u = os::process::run(copy("use")); + auto theirs = os::process::resources(os::process::of::children); + const bool counted = theirs && theirs->peak_memory.value_or(0) >= (60u << 20) + && theirs->user > std::chrono::nanoseconds(0); + const bool untold = !theirs && (os::host() == os::platform::windows || os::is_openkal || os::is_emscripten); + if (!counted && !untold && theirs) + std::printf(" (children: peak %llu, user %lld ns)\n", + static_cast(theirs->peak_memory.value_or(0)), + static_cast(theirs->user.count())); + check(u && (counted || untold), "a waited-for child's processor time and memory are accounted to its parent"); } { // The command interpreter given a command with quotation marks, joined From a80f99675856fdc9fd6afe85d82ae074226c6560 Mon Sep 17 00:00:00 2001 From: Sunrisepeak Date: Mon, 9 Nov 2026 06:19:55 +0900 Subject: [PATCH 3/3] 0.4.1: PROCESS_MEMORY_COUNTERS beside the other system structures Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com> --- CHANGELOG.md | 3 ++- src/process.cppm | 5 +++-- src/windows/win32.h | 11 ++++++----- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da3ba00..eff0318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ - **What a program used (`process::resources`).** `resources(of::self)` gives this program's user and system processor time, its largest resident set and the bytes its reads and writes moved; `resources(of::children)` the same of - the programs it started and waited for, together, as the kernel accounts them + the programs it started, waited for and whose handles it closed (which reaps + them), together, as the kernel accounts them (Linux and macOS, by `getrusage`). A field the system does not tell is absent; Windows tells a program's own use and not its children's; an image and the Web answer `not_supported`. A test runner measures a program by starting it from a diff --git a/src/process.cppm b/src/process.cppm index 5e9435b..5347a8c 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -258,8 +258,9 @@ struct program_info { // WHAT A PROGRAM HAS USED (0.4.1): processor time, the largest resident set and // the bytes moved by its reads and writes. `of::self' is this program so far; -// `of::children' the programs it started and has waited for, together, as the -// kernel accounts them (Linux and macOS). A runner that starts one program and +// `of::children' the programs it started, waited for and whose handles it has +// closed (which reaps them), together, as the kernel accounts them (Linux and +// macOS). A runner that starts one program and // asks for its children has that program's use. A field the system does not // tell is absent; errc::not_supported where nothing is told (an image, the Web, // a Windows program's children). diff --git a/src/windows/win32.h b/src/windows/win32.h index 04f9a75..8855fe1 100644 --- a/src/windows/win32.h +++ b/src/windows/win32.h @@ -35,6 +35,12 @@ struct okos_persistent_volume { okos_dword volume_flags, flag_mask, version, reserved; }; struct okos_token_elevation { okos_dword elevated; }; +// PROCESS_MEMORY_COUNTERS. +struct okos_memory_counters { + okos_dword cb, page_faults; + unsigned long long peak_working_set, working_set, quota_peak_paged, quota_paged, + quota_peak_nonpaged, quota_nonpaged, pagefile, peak_pagefile; +}; __declspec(dllimport) okos_handle __stdcall CreateFileW(const okos_wchar*, 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*); @@ -127,11 +133,6 @@ inline okos_bool GetProcessTimes(okos_handle a0, unsigned long long* a1, unsigne unsigned long long*, unsigned long long*)>("GetProcessTimes"); return f(a0, a1, a2, a3, a4); } -struct okos_memory_counters { - okos_dword cb, page_faults; - unsigned long long peak_working_set, working_set, quota_peak_paged, quota_paged, - quota_peak_nonpaged, quota_nonpaged, pagefile, peak_pagefile; -}; inline okos_bool K32GetProcessMemoryInfo(okos_handle a0, okos_memory_counters* a1, okos_dword a2) { static const auto f = find( "K32GetProcessMemoryInfo");