diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68909fc..ac2faf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,6 +182,8 @@ jobs: run: shell: bash steps: + - uses: actions/checkout@v4 + with: { sparse-checkout: mcpp.toml, sparse-checkout-cone-mode: false } - uses: actions/download-artifact@v4 with: { name: os-test-images, path: out } - name: Install xlings (Unix) @@ -202,7 +204,10 @@ jobs: 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" + # The okrun of the openkal-tools version this package's manifest names. + tools=$(sed -n 's/^openkal-tools = { version = "\([^"]*\)".*/\1/p' mcpp.toml) + [ -n "$tools" ] || { echo "::error::mcpp.toml names no openkal-tools version"; exit 1; } + git clone --quiet --depth 1 --branch "$tools" 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; } diff --git a/README.md b/README.md index 41f8507..3792361 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.1.1" } +os = { namespace = "mcpplibs", version = "0.2.0" } ``` ```cpp @@ -41,15 +41,15 @@ platform 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 (names without case on Windows), 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), `caps`, `lock`, links, `set_executable`, `stamp_of`, `identity_of`, `locate`, `absolute`, `names_a_place`; names in the host's rules without the file system: `generic`, `native`, `is_absolute`, `same`, `within` | -| `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` | +| `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`; 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`, `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` | +| `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` | | `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.machine` | the machine's architecture (through Rosetta and WoW64), memory and the memory available now, processors (logical, physical, of differing speed) | | `mcpplibs.os.random` | bytes from the system's source of unpredictable bytes; `hex` | | `mcpplibs.os.library` | shared libraries at run time: `open` (a path or a name the system searches for), `loaded` (one the process has, without loading it), a handle's `symbol` / `function`, `process_symbol` (as the loader resolves one), `suffix` | | `mcpplibs.os.linux` / `.macos` / `.windows` / `.posix` | platform modules: declared on every target, defined only on their own | diff --git a/mcpp.toml b/mcpp.toml index 2611e20..1da8b06 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "os" -version = "0.1.2" +version = "0.2.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"] @@ -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 = "0.17.0" +openkal = "0.17.1" [target.linux.dependencies] -openkal-linux = "0.18.0" +openkal-linux = "0.18.2" [target.macos.dependencies] -openkal-macos = "0.15.0" +openkal-macos = "0.15.1" [target.windows.dependencies] -openkal-windows = "0.13.0" +openkal-windows = "0.13.3" [targets.os] kind = "lib" @@ -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 = "0.16.0" +openkal-llvm-runtime = "0.16.3" [target.'cfg(os = "openkal")'.build-dependencies] -openkal-tools = { version = "0.1.0", tools = ["okld", "okrun"] } +openkal-tools = { version = "0.1.1", tools = ["okld", "okrun"] } diff --git a/src/fs.cpp b/src/fs.cpp index 1ad9dd5..3f6a215 100644 --- a/src/fs.cpp +++ b/src/fs.cpp @@ -475,3 +475,184 @@ result rename(const std::filesystem::path& from, const std::filesystem::pa } } // namespace mcpplibs::os::fs + +// --- 0.2.0: appending, durability, long names, measured volumes ---------------- + +namespace mcpplibs::os::fs { + +result append(const std::filesystem::path& file, std::string_view bytes) { + const std::string doing = std::format("appending to {}", generic_utf8(file)); + auto l = where(file, "appending to"); + if (!l) return std::unexpected(l.error()); + opened_file f; + const auto flags = kal::fs::open::write | kal::fs::open::create | kal::fs::open::append; + 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)); + // One write: a stream that appends places each write whole at the end. + const kal_intptr n = kal_stream_write(kal_fs_stream(f.f), bytes.data(), bytes.size()); + if (n < 0 || static_cast(n) != bytes.size()) + return std::unexpected(from_kal(n < 0 ? static_cast(-n) : kal_err_io, doing)); + return {}; +} + +result sync(const std::filesystem::path& node) { return sys::sync(node); } + +std::string windows_extended(std::string_view in) { + std::string s(in); + for (auto& c : s) if (c == '/') c = '\\'; + // Already extended (`\\?\`) or a device name (`\\.\`): nothing is rewritten. + if (s.starts_with("\\\\?\\") || s.starts_with("\\\\.\\")) return s; + std::string prefix, rest; + if (s.starts_with("\\\\")) { + prefix = "\\\\?\\UNC\\"; + rest = s.substr(2); + } else if (s.size() >= 3 && std::isalpha(static_cast(s[0])) && s[1] == ':' && s[2] == '\\') { + prefix = "\\\\?\\"; + rest = s; + } else { + return std::string(in); // relative or rootless + } + // The prefix turns off `.' and `..', so they are resolved here; `..' never + // climbs above the drive or the share. + std::vector parts; + const std::size_t keep = prefix.ends_with("UNC\\") ? 2 : 1; + std::size_t start = 0; + while (start <= rest.size()) { + auto end = rest.find('\\', start); + if (end == std::string::npos) end = rest.size(); + std::string comp = rest.substr(start, end - start); + start = end + 1; + if (comp.empty() || comp == ".") continue; + if (comp == "..") { if (parts.size() > keep) parts.pop_back(); continue; } + parts.push_back(std::move(comp)); + } + std::string out = prefix; + for (std::size_t i = 0; i < parts.size(); ++i) { if (i) out += '\\'; out += parts[i]; } + if (parts.size() == 1 && keep == 1) out += '\\'; // `\\?\C:\`, not `\\?\C:` + return out; +} + +std::filesystem::path extended(const std::filesystem::path& name) { + if (host() != platform::windows || name.empty()) return name; + std::error_code ec; + const auto abs = std::filesystem::absolute(name, ec); + if (ec) return name; + const auto u8 = abs.generic_u8string(); + const auto spelled = windows_extended(std::string(reinterpret_cast(u8.data()), u8.size())); + return std::filesystem::path(std::u8string(reinterpret_cast(spelled.data()), spelled.size())); +} + +std::string volume_type(const std::filesystem::path& directory) { return sys::volume_type(directory); } +owner owner_of(const std::filesystem::path& node) { return sys::owner_of(node); } +bool writable(const std::filesystem::path& node) { return sys::writable(node); } + +behaviour measure_now(const std::filesystem::path& directory) { + behaviour b; + b.type = sys::volume_type(directory); + std::error_code ec; + std::random_device random; + const auto scratch = directory / std::format(".os-measure-{:08x}{:08x}", random(), random()); + if (!std::filesystem::create_directory(scratch, ec) || ec) return b; + const auto touch = [&](const std::filesystem::path& p) { return write(p, "x").has_value(); }; + if (touch(scratch / "a") && touch(scratch / "c")) { + b.measured = true; + if (const auto r = rename(scratch / "a", scratch / "b", false); !r) { + b.rename_no_replace = r.error().code != errc::not_supported; + std::filesystem::rename(scratch / "a", scratch / "b", ec); + } + if (const int e = sys::exchange(scratch / "b", scratch / "c"); e != 0) + b.exchange = !sys::exchange_unsupported(e); + ec.clear(); + std::filesystem::create_hard_link(scratch / "b", scratch / "d", ec); + b.hard_links = !ec; + ec.clear(); + std::filesystem::create_symlink("b", scratch / "e", ec); + b.symbolic_links = !ec; + ec.clear(); + if (touch(scratch / "Case")) b.case_sensitive = !std::filesystem::exists(scratch / "cASE", ec); + } + std::filesystem::remove_all(scratch, ec); + return b; +} + +behaviour measure(const std::filesystem::path& directory) { + static std::mutex guard; + static std::map known; + const auto id = identity_of(directory); + const std::uint64_t volume = id ? id->volume : 0; + { + std::scoped_lock g(guard); + if (volume != 0) + if (const auto it = known.find(volume); it != known.end()) return it->second; + } + auto b = measure_now(directory); + if (volume != 0 && b.measured) { + std::scoped_lock g(guard); + known.emplace(volume, b); + } + return b; +} + +result place(const std::filesystem::path& from, const std::filesystem::path& to) { + const std::string doing = std::format("placing {} as {}", generic_utf8(from), generic_utf8(to)); + auto parent = to.parent_path(); + if (parent.empty()) parent = "."; + const auto b = measure(parent); + const auto failed = [&](std::error_code e) { + return std::unexpected(error{ e == std::errc::file_exists ? errc::exists : errc::io, + std::format("{}: {}", doing, e.message()) }); + }; + if (b.rename_no_replace) { + const auto r = rename(from, to, false); + if (r || r.error().code != errc::not_supported) return r; + } + std::error_code ec; + const auto status = std::filesystem::symlink_status(from, ec); + if (ec) return failed(ec); + if (status.type() == std::filesystem::file_type::symlink) { + std::filesystem::copy_symlink(from, to, ec); + if (ec) return failed(ec); + std::filesystem::remove(from, ec); + return {}; + } + if (status.type() != std::filesystem::file_type::directory && b.hard_links) { + std::filesystem::create_hard_link(from, to, ec); + if (ec) return failed(ec); + std::filesystem::remove(from, ec); + return {}; + } + const auto there = std::filesystem::symlink_status(to, ec); + if (ec && ec != std::errc::no_such_file_or_directory) return failed(ec); + if (std::filesystem::exists(there)) return failed(std::make_error_code(std::errc::file_exists)); + std::filesystem::rename(from, to, ec); + if (ec) return failed(ec); + return {}; +} + +result exchange(const std::filesystem::path& a, const std::filesystem::path& b) { + const std::string doing = std::format("exchanging {} and {}", generic_utf8(a), generic_utf8(b)); + auto parent = b.parent_path(); + if (parent.empty()) parent = "."; + if (measure(parent).exchange) { + const int e = sys::exchange(a, b); + if (e == 0) return {}; + if (!sys::exchange_unsupported(e)) + return std::unexpected(error{ errc::io, std::format("{}: {}", doing, std::generic_category().message(e)), e }); + } + std::random_device random; + const auto aside = parent / std::format(".os-exchange-{:08x}{:08x}", random(), random()); + std::error_code ec; + std::filesystem::rename(b, aside, ec); + if (ec) return std::unexpected(error{ errc::io, std::format("{}: {}", doing, ec.message()) }); + std::filesystem::rename(a, b, ec); + if (ec) { + std::error_code back; + std::filesystem::rename(aside, b, back); + return std::unexpected(error{ errc::io, std::format("{}: {}", doing, ec.message()) }); + } + std::filesystem::rename(aside, a, ec); + if (ec) return std::unexpected(error{ errc::io, std::format("{}: {}", doing, ec.message()) }); + return {}; +} + +} // namespace mcpplibs::os::fs diff --git a/src/fs.cppm b/src/fs.cppm index dae62a6..c92730f 100644 --- a/src/fs.cppm +++ b/src/fs.cppm @@ -92,6 +92,24 @@ result write_atomic(const std::filesystem::path& file, std::string_view by struct published { bool atomic = false; }; result publish(const std::filesystem::path& file, std::string_view bytes); +// Appended in one write that another program appending to the same file cannot +// split; the file is created when it is not there. +result append(const std::filesystem::path& file, std::string_view bytes); + +// What was written to the file, or to the directory's entries, is durable: it +// survives the machine stopping. A volume that keeps nothing durable (a +// directory on Windows, an image's preopened tree) has nothing to do. +result sync(const std::filesystem::path& node); + +// The name in the spelling that reaches past the system's limit on a name's +// length: `\\?\' (or `\\?\UNC\') before an absolute name on Windows, with `.' +// and `..' resolved because that spelling no longer resolves them; the name +// itself everywhere else, and for a name that is not absolute. +std::filesystem::path extended(const std::filesystem::path& name); +// The Windows rule of `extended', of an absolute name in either separator, so +// that it is a fact on every system. +std::string windows_extended(std::string_view absolute_name); + // --- what a volume can do ---------------------------------------------------- struct capabilities { @@ -109,6 +127,52 @@ struct capabilities { // Of the volume the directory is on. result caps(const std::filesystem::path& directory); +// WHAT THE VOLUME DOES, MEASURED (0.2.0). A volume is whatever a person has +// --- ext4, APFS, NTFS, and NFSv3, SMB, FAT, a FUSE mount, WSL's drvfs --- and +// what it claims is not always what it does: NFSv3 answers a rename that may +// not replace with EINVAL. Measured once per volume per program, with a +// scratch directory in `directory' that is removed after; `measured' false +// when the directory cannot be written, and the system's own answers stand. +struct behaviour { + std::string type; // "ext4", "nfs", "apfs", "NTFS", ...; "" unknown + bool rename_no_replace = true; // a rename that refuses an existing name, atomically + bool exchange = true; // two entries swapped atomically + bool hard_links = true; + bool symbolic_links = true; + bool case_sensitive = true; + bool measured = false; + // Whether every operation below is the volume's own. + bool native() const { return rename_no_replace && exchange && hard_links && symbolic_links; } +}; +behaviour measure(const std::filesystem::path& directory); +// The same, measured now and not remembered. +behaviour measure_now(const std::filesystem::path& directory); +// The volume's kind, as the system names it; "" where it does not. +std::string volume_type(const std::filesystem::path& directory); + +// Who owns a node: this program's user, the system (the superuser; on Windows +// SYSTEM or the Administrators), or another user. +enum class owner { this_user, system, another_user, unknown }; +owner owner_of(const std::filesystem::path& node); +// Whether this program's user may write the node (POSIX `access(W_OK)'; +// elsewhere what the system's own rules decide when the write is made, so +// true). +bool writable(const std::filesystem::path& node); + +// An entry made beside `to' (on its volume) published under `to', never over an +// entry there: by a rename that refuses replacement where the volume has one; +// else by a hard link (a file) or the same symbolic link again, each refused +// as atomically when the name exists, the scratch name then removed; else, for +// a directory, by a rename onto a name that is not there --- the one window, a +// name made in between, is the caller's under its own exclusion. +result place(const std::filesystem::path& from, const std::filesystem::path& to); + +// `a' and `b' (both present, on one volume) trade places: atomically where the +// volume exchanges; else `b' is moved aside, `a' renamed onto `b''s name and the +// old `b' put at `a' --- a reader of `b' meets the name missing for that moment, +// never a half of either. +result exchange(const std::filesystem::path& a, const std::filesystem::path& b); + // --- exclusion ----------------------------------------------------------------- // A lock upon a file, held until released or until the holder ends however it @@ -164,3 +228,14 @@ result rename(const std::filesystem::path& from, const std::filesystem::pa bool replace = true); } // namespace mcpplibs::os::fs + +// Each system's own (src//fs.cpp). +namespace mcpplibs::os::fs::sys { +result sync(const std::filesystem::path& node); +// 0, or the system's condition; `exchange_unsupported' says the volume declined. +int exchange(const std::filesystem::path& a, const std::filesystem::path& b); +bool exchange_unsupported(int condition); +std::string volume_type(const std::filesystem::path& directory); +owner owner_of(const std::filesystem::path& node); +bool writable(const std::filesystem::path& node); +} // namespace mcpplibs::os::fs::sys diff --git a/src/interrupt.cpp b/src/interrupt.cpp index 9945ec5..27efbb5 100644 --- a/src/interrupt.cpp +++ b/src/interrupt.cpp @@ -51,4 +51,6 @@ void end_as_requested() { kal_process_end_as_requested(); } +void end_as_output_closed() { sys::end_as_output_closed(); } + } // namespace mcpplibs::os::interrupt diff --git a/src/interrupt.cppm b/src/interrupt.cppm index dba4336..9585227 100644 --- a/src/interrupt.cppm +++ b/src/interrupt.cppm @@ -25,4 +25,16 @@ bool wait(std::optional timeout = std::nullopt); // request, and then does nothing. void end_as_requested(); +// Ends this program as a system ends one whose output's reader is gone: by the +// signal for it where the system has one (SIGPIPE, which a shell reports as +// 141). Returns where the system has none: there a write to a closed stream is +// refused and the program goes on (0.2.0). openkal reports a closed reader +// instead of ending the program, so a program that wants the system's ending +// asks for it after it has finished in order. +void end_as_output_closed(); + } // namespace mcpplibs::os::interrupt + +namespace mcpplibs::os::interrupt::sys { +void end_as_output_closed(); +} // namespace mcpplibs::os::interrupt::sys diff --git a/src/linux/fs.cpp b/src/linux/fs.cpp new file mode 100644 index 0000000..f7cc8cf --- /dev/null +++ b/src/linux/fs.cpp @@ -0,0 +1,46 @@ +// Linux's own of mcpplibs.os.fs: an exchange (renameat2 RENAME_EXCHANGE) and +// the volume's kind (statfs). +module; +#include +#include +#include +#include +#include + +module mcpplibs.os.fs; + +import std; + +namespace mcpplibs::os::fs::sys { + +int exchange(const std::filesystem::path& a, const std::filesystem::path& b) { + constexpr unsigned kExchange = 2; // RENAME_EXCHANGE + return ::syscall(SYS_renameat2, AT_FDCWD, a.c_str(), AT_FDCWD, b.c_str(), kExchange) == 0 ? 0 : errno; +} +bool exchange_unsupported(int c) { return c == EINVAL || c == ENOSYS || c == EOPNOTSUPP; } + +std::string volume_type(const std::filesystem::path& directory) { + struct statfs st {}; + if (::statfs(directory.c_str(), &st) != 0) return {}; + switch (static_cast(st.f_type)) { + case 0xEF53: return "ext4"; + case 0x58465342: return "xfs"; + case 0x9123683E: return "btrfs"; + case 0x01021994: return "tmpfs"; + case 0x794C7630: return "overlay"; + case 0x6969: return "nfs"; + case 0xFF534D42: return "cifs"; + case 0xFE534D42: return "smb2"; + case 0x4D44: return "vfat"; + case 0x2011BAB0: return "exfat"; + case 0x7366746E: return "ntfs3"; + case 0x5346544E: return "ntfs"; + case 0x65735546: return "fuse"; + case 0x01021997: return "9p"; + case 0x2FC12FC1: return "zfs"; + case 0x00C36400: return "ceph"; + default: return std::format("0x{:x}", static_cast(st.f_type)); + } +} + +} // namespace mcpplibs::os::fs::sys diff --git a/src/linux/machine.cpp b/src/linux/machine.cpp index 29a0f9d..a702c4c 100644 --- a/src/linux/machine.cpp +++ b/src/linux/machine.cpp @@ -1,6 +1,10 @@ module; +#include +#include +#include #include #include +#include module mcpplibs.os.machine; @@ -22,4 +26,69 @@ std::optional memory() { return static_cast(s.totalram) * s.mem_unit; } +namespace { +bool meminfo(const char* key, std::uint64_t& out) { + FILE* f = ::fopen("/proc/meminfo", "r"); + if (!f) return false; + char line[256]; + bool found = false; + const std::size_t klen = ::strlen(key); + while (::fgets(line, sizeof line, f)) { + if (::strncmp(line, key, klen) != 0) continue; + unsigned long long kb = 0; + if (::sscanf(line + klen, " %llu", &kb) == 1) { out = kb * 1024ull; found = true; } + break; + } + ::fclose(f); + return found; +} +int cores_per_package() { + FILE* f = ::fopen("/proc/cpuinfo", "r"); + if (!f) return 0; + char line[256]; + int cores = 0; + while (::fgets(line, sizeof line, f)) { + if (::strncmp(line, "cpu cores", 9) != 0) continue; + if (const char* colon = ::strchr(line, ':')) cores = ::atoi(colon + 1); + break; + } + ::fclose(f); + return cores; +} +// Cores whose top frequencies differ: performance and efficiency cores. +bool hybrid(long logical) { + if (logical <= 1) return false; + long first = -1; + for (long i = 0; i < logical; ++i) { + char path[128]; + ::snprintf(path, sizeof path, "/sys/devices/system/cpu/cpu%ld/cpufreq/cpuinfo_max_freq", i); + FILE* f = ::fopen(path, "r"); + if (!f) return false; + long v = 0; + const int got = ::fscanf(f, "%ld", &v); + ::fclose(f); + if (got != 1) return false; + if (first < 0) first = v; + else if (v != first) return true; + } + return false; +} +} // namespace + +std::optional memory_available() { + std::uint64_t v = 0; + if (meminfo("MemAvailable:", v)) return v; + return memory(); +} + +processor_count processors() { + processor_count c; + const long n = ::sysconf(_SC_NPROCESSORS_ONLN); + if (n > 0) c.logical = static_cast(n); + const int cores = cores_per_package(); + c.physical = cores > 0 ? static_cast(cores) : c.logical; + c.heterogeneous = hybrid(static_cast(c.logical)); + return c; +} + } // namespace mcpplibs::os::machine diff --git a/src/machine.cppm b/src/machine.cppm index 2c16d06..6a29f30 100644 --- a/src/machine.cppm +++ b/src/machine.cppm @@ -18,4 +18,19 @@ std::string arch(); // The physical memory, in bytes. std::optional memory(); +// The memory the system could give a program now without paging, in bytes: +// MemAvailable on Linux, free and inactive pages on macOS, the available +// physical memory on Windows (0.2.0). +std::optional memory_available(); + +// The processors: those online, the cores beneath them, and whether the cores +// differ in speed (performance and efficiency cores), when a count of cores +// overstates the work they do together (0.2.0). +struct processor_count { + std::uint32_t logical = 1; + std::uint32_t physical = 1; + bool heterogeneous = false; +}; +processor_count processors(); + } // namespace mcpplibs::os::machine diff --git a/src/macos/fs.cpp b/src/macos/fs.cpp new file mode 100644 index 0000000..bfbb4ad --- /dev/null +++ b/src/macos/fs.cpp @@ -0,0 +1,25 @@ +// macOS's own of mcpplibs.os.fs: an exchange (renamex_np RENAME_SWAP) and the +// volume's kind (statfs f_fstypename). +module; +#include +#include +#include +#include + +module mcpplibs.os.fs; + +import std; + +namespace mcpplibs::os::fs::sys { + +int exchange(const std::filesystem::path& a, const std::filesystem::path& b) { + return ::renamex_np(a.c_str(), b.c_str(), RENAME_SWAP) == 0 ? 0 : errno; +} +bool exchange_unsupported(int c) { return c == EINVAL || c == ENOTSUP || c == EOPNOTSUPP || c == ENOSYS; } + +std::string volume_type(const std::filesystem::path& directory) { + struct statfs st {}; + return ::statfs(directory.c_str(), &st) == 0 ? std::string(st.f_fstypename) : std::string{}; +} + +} // namespace mcpplibs::os::fs::sys diff --git a/src/macos/machine.cpp b/src/macos/machine.cpp index 3af8d54..4024288 100644 --- a/src/macos/machine.cpp +++ b/src/macos/machine.cpp @@ -1,6 +1,8 @@ module; +#include #include #include +#include module mcpplibs.os.machine; @@ -31,4 +33,24 @@ std::string arch() { std::optional memory() { return sysctl_value("hw.memsize"); } +std::optional memory_available() { + vm_statistics64_data_t vm{}; + mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; + if (::host_statistics64(::mach_host_self(), HOST_VM_INFO64, reinterpret_cast(&vm), &count) + != KERN_SUCCESS) + return memory(); + const auto page = static_cast(::getpagesize()); + return (static_cast(vm.free_count) + static_cast(vm.inactive_count)) * page; +} + +processor_count processors() { + processor_count c; + const long n = ::sysconf(_SC_NPROCESSORS_ONLN); + if (n > 0) c.logical = static_cast(n); + const int physical = sysctl_value("hw.physicalcpu").value_or(0); + c.physical = physical > 0 ? static_cast(physical) : c.logical; + c.heterogeneous = sysctl_value("hw.nperflevels").value_or(1) > 1; + return c; +} + } // namespace mcpplibs::os::machine diff --git a/src/openkal/exec.cpp b/src/openkal/exec.cpp new file mode 100644 index 0000000..a83784f --- /dev/null +++ b/src/openkal/exec.cpp @@ -0,0 +1,15 @@ +// This system replaces no program's image: `exec' starts the program and ends +// as it ended (src/process.cpp). +module mcpplibs.os.process; + +import std; +import mcpplibs.os.error; + +namespace mcpplibs::os::process::sys { + +result replace_image(const std::filesystem::path&, const std::vector&, + const std::vector&, const std::filesystem::path&) { + return std::unexpected(error{ errc::not_supported, "this system replaces no program's image" }); +} + +} // namespace mcpplibs::os::process::sys diff --git a/src/openkal/fs.cpp b/src/openkal/fs.cpp new file mode 100644 index 0000000..9f36772 --- /dev/null +++ b/src/openkal/fs.cpp @@ -0,0 +1,20 @@ +// An image's own of mcpplibs.os.fs: what openkal answers. Durability is the +// stream's flush where the node is a file; nothing is exchanged; the kind and +// the owner of a volume are not known to an image. +module; + +module mcpplibs.os.fs; + +import std; +import mcpplibs.os.error; + +namespace mcpplibs::os::fs::sys { + +result sync(const std::filesystem::path&) { return {}; } +int exchange(const std::filesystem::path&, const std::filesystem::path&) { return -1; } +bool exchange_unsupported(int c) { return c == -1; } +std::string volume_type(const std::filesystem::path&) { return {}; } +owner owner_of(const std::filesystem::path&) { return owner::unknown; } +bool writable(const std::filesystem::path&) { return true; } + +} // namespace mcpplibs::os::fs::sys diff --git a/src/openkal/interrupt.cpp b/src/openkal/interrupt.cpp new file mode 100644 index 0000000..ac4158a --- /dev/null +++ b/src/openkal/interrupt.cpp @@ -0,0 +1,6 @@ +// No signal ends a program whose reader is gone here: its writes are refused. +module mcpplibs.os.interrupt; + +namespace mcpplibs::os::interrupt::sys { +void end_as_output_closed() {} +} // namespace mcpplibs::os::interrupt::sys diff --git a/src/openkal/machine.cpp b/src/openkal/machine.cpp index fefd9c6..d85efe8 100644 --- a/src/openkal/machine.cpp +++ b/src/openkal/machine.cpp @@ -3,6 +3,8 @@ module mcpplibs.os.machine; import std; import mcpplibs.os.platform; +import openkal.types; +import openkal.task; namespace mcpplibs::os::machine { @@ -10,4 +12,14 @@ std::string arch() { return std::string(name(ARCH)); } std::optional memory() { return std::nullopt; } +std::optional memory_available() { return std::nullopt; } + +// The parallelism the environment offers, which is all an image is told. +processor_count processors() { + processor_count c; + const auto n = kal_task_parallelism(); + c.logical = c.physical = n > 0 ? static_cast(n) : 1u; + return c; +} + } // namespace mcpplibs::os::machine diff --git a/src/openkal/stdio.cpp b/src/openkal/stdio.cpp new file mode 100644 index 0000000..eb49a37 --- /dev/null +++ b/src/openkal/stdio.cpp @@ -0,0 +1,10 @@ +// An image does not rearrange its streams. +module mcpplibs.os.stdio; + +import std; + +namespace mcpplibs::os::stdio::sys { +int divert_output() { return -1; } +void restore_output(int) {} +bool write_descriptor(int, std::string_view) { return false; } +} // namespace mcpplibs::os::stdio::sys diff --git a/src/openkal/terminal.cpp b/src/openkal/terminal.cpp new file mode 100644 index 0000000..dc12a36 --- /dev/null +++ b/src/openkal/terminal.cpp @@ -0,0 +1,6 @@ +// A console has no foreground group to be outside of. +module mcpplibs.os.terminal; + +namespace mcpplibs::os::terminal::sys { +bool foreground() { return true; } +} // namespace mcpplibs::os::terminal::sys diff --git a/src/posix.cppm b/src/posix.cppm index 1a3b57a..76b226d 100644 --- a/src/posix.cppm +++ b/src/posix.cppm @@ -8,4 +8,9 @@ export namespace mcpplibs::os::posix { // The effective user's number; zero is the superuser. std::uint32_t effective_user(); +// The permissions every file this program creates from now on is denied +// (umask): 022 makes them readable by everyone and writable by their owner, +// what a directory shared by a machine's users is written with (0.2.0). +void set_creation_mask(std::uint32_t mask); + } // namespace mcpplibs::os::posix diff --git a/src/posix/exec.cpp b/src/posix/exec.cpp new file mode 100644 index 0000000..3454239 --- /dev/null +++ b/src/posix/exec.cpp @@ -0,0 +1,49 @@ +// The image replaced (execve), on Linux and macOS: the identity, the terminal +// and the place among the processes that wait for this one stay; the status a +// waiter sees is the program's. +module; +#include +#include +#include + +module mcpplibs.os.process; + +import std; +import mcpplibs.os.error; +import mcpplibs.os.platform; + +namespace mcpplibs::os::process::sys { + +result replace_image(const std::filesystem::path& program, + const std::vector& argv, + const std::vector& environment, + const std::filesystem::path& cwd) { + std::vector args, envp; + for (const auto& a : argv) args.push_back(const_cast(a.c_str())); + args.push_back(nullptr); + for (const auto& e : environment) envp.push_back(const_cast(e.c_str())); + envp.push_back(nullptr); + if (!cwd.empty() && ::chdir(cwd.c_str()) != 0) + return std::unexpected(error{ errc::not_found, std::format( + "entering {}: {}", cwd.generic_string(), std::generic_category().message(errno)) }); + // SIGPIPE AT ITS DEFAULT, AS A SHELL STARTS A PROGRAM. openkal ignores it in + // this program so that a write to a closed reader is reported rather than + // fatal, and an ignored disposition survives execve: the program this one + // becomes would begin with it ignored, and `yes | head' inside it would + // report a write error instead of ending. A handler is reset by execve, an + // ignore is not, so the ignore is undone here. + struct sigaction pipe_default {}; + pipe_default.sa_handler = SIG_DFL; + ::sigaction(SIGPIPE, &pipe_default, nullptr); + ::execve(program.c_str(), args.data(), envp.data()); + const int e = errno; + // A file this machine cannot start: no image at all (ENOEXEC), or on + // macOS an image of another processor (EBADARCH 86, EBADMACHO 88), as + // openkal reports a refused start. + const bool not_program = e == ENOEXEC || (is_macos && (e == 86 || e == 88)); + return std::unexpected(error{ not_program ? errc::not_program + : e == ENOENT ? errc::not_found : e == EACCES ? errc::permission : errc::io, + std::format("becoming {}: {}", program.generic_string(), std::generic_category().message(e)) }); +} + +} // namespace mcpplibs::os::process::sys diff --git a/src/posix/fs.cpp b/src/posix/fs.cpp new file mode 100644 index 0000000..7f9128e --- /dev/null +++ b/src/posix/fs.cpp @@ -0,0 +1,40 @@ +// What Linux and macOS share of mcpplibs.os.fs's own: durability, ownership, +// whether a node may be written. +module; +#include +#include +#include +#include + +module mcpplibs.os.fs; + +import std; +import mcpplibs.os.error; + +namespace mcpplibs::os::fs::sys { + +result sync(const std::filesystem::path& node) { + const int fd = ::open(node.c_str(), O_RDONLY | O_CLOEXEC); + if (fd < 0) return std::unexpected(error{ errno == ENOENT ? errc::not_found : errc::io, + std::format("making {} durable: {}", node.generic_string(), std::generic_category().message(errno)), errno }); + // A volume that keeps nothing durable for this node says so; it is not a failure. + if (::fsync(fd) != 0 && errno != EINVAL && errno != EROFS && errno != ENOTSUP && errno != EOPNOTSUPP) { + const int e = errno; + ::close(fd); + return std::unexpected(error{ errc::io, std::format("making {} durable: {}", node.generic_string(), + std::generic_category().message(e)), e }); + } + ::close(fd); + return {}; +} + +owner owner_of(const std::filesystem::path& node) { + struct stat st {}; + if (::stat(node.c_str(), &st) != 0) return owner::unknown; + if (st.st_uid == ::getuid()) return owner::this_user; + return st.st_uid == 0 ? owner::system : owner::another_user; +} + +bool writable(const std::filesystem::path& node) { return ::access(node.c_str(), W_OK) == 0; } + +} // namespace mcpplibs::os::fs::sys diff --git a/src/posix/interrupt.cpp b/src/posix/interrupt.cpp new file mode 100644 index 0000000..d3922db --- /dev/null +++ b/src/posix/interrupt.cpp @@ -0,0 +1,16 @@ +// The system's ending for a closed reader: SIGPIPE at its default, raised. +module; +#include +#include + +module mcpplibs.os.interrupt; + +namespace mcpplibs::os::interrupt::sys { + +void end_as_output_closed() { + std::signal(SIGPIPE, SIG_DFL); + std::raise(SIGPIPE); + ::_exit(128 + SIGPIPE); +} + +} // namespace mcpplibs::os::interrupt::sys diff --git a/src/posix/posix.cpp b/src/posix/posix.cpp index 00156a9..7d3f800 100644 --- a/src/posix/posix.cpp +++ b/src/posix/posix.cpp @@ -1,4 +1,5 @@ module; +#include #include module mcpplibs.os.posix; @@ -9,4 +10,6 @@ namespace mcpplibs::os::posix { std::uint32_t effective_user() { return static_cast(::geteuid()); } +void set_creation_mask(std::uint32_t mask) { ::umask(static_cast(mask)); } + } // namespace mcpplibs::os::posix diff --git a/src/posix/stdio.cpp b/src/posix/stdio.cpp new file mode 100644 index 0000000..c2fcd4b --- /dev/null +++ b/src/posix/stdio.cpp @@ -0,0 +1,35 @@ +// The output descriptor saved (not inherited) and replaced by the error one. +module; +#include +#include +#include + +module mcpplibs.os.stdio; + +import std; + +namespace mcpplibs::os::stdio::sys { + +int divert_output() { + const int saved = ::fcntl(1, F_DUPFD_CLOEXEC, 3); + if (saved >= 0) ::dup2(2, 1); + return saved; +} + +void restore_output(int saved) { + ::dup2(saved, 1); + ::close(saved); +} + +bool write_descriptor(int descriptor, std::string_view bytes) { + std::size_t done = 0; + while (done < bytes.size()) { + const auto n = ::write(descriptor, bytes.data() + done, bytes.size() - done); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) return false; + done += static_cast(n); + } + return true; +} + +} // namespace mcpplibs::os::stdio::sys diff --git a/src/posix/terminal.cpp b/src/posix/terminal.cpp new file mode 100644 index 0000000..bb887a6 --- /dev/null +++ b/src/posix/terminal.cpp @@ -0,0 +1,9 @@ +// Whether this program is in its terminal's foreground process group. +module; +#include + +module mcpplibs.os.terminal; + +namespace mcpplibs::os::terminal::sys { +bool foreground() { return ::tcgetpgrp(0) == ::getpgrp(); } +} // namespace mcpplibs::os::terminal::sys diff --git a/src/process.cpp b/src/process.cpp index 2f00c73..5979e28 100644 --- a/src/process.cpp +++ b/src/process.cpp @@ -159,8 +159,10 @@ 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" }); + if (o.in == stdio::error || o.in == stdio::output) + return std::unexpected(error{ errc::invalid, "a program's input cannot be an output of this program" }); + if (o.out == stdio::output) + return std::unexpected(error{ errc::invalid, "a program's output cannot be its own output" }); // The program, by name or by its place. std::filesystem::path program = o.program; if (!fs::names_a_place(fs::generic_utf8(o.program))) { @@ -220,7 +222,10 @@ result process::spawn(const spawn_options& o) { } else if (o.out == stdio::error) { child_out = kal_stderr(); } - if (o.err == stdio::pipe) { + if (o.err == stdio::output) { + if (o.out == stdio::inherit) child_err = kal_stdout(); + else child_err = child_out; + } else 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); @@ -247,8 +252,12 @@ result process::spawn(const spawn_options& o) { const kal_uintptr props = kal_process_props(); const bool bind = o.bound_lifetime && (props & kal::process::bound_lifetime.bits) != 0; + if (o.one_string && (props & kal::process::one_string.bits) == 0) + return fail_streams(error{ errc::not_supported, + std::format("starting {}: a program here receives a vector, not one string", fs::generic_utf8(program)) }); kal_spawn how{ as_dir(where->directory), work, nullptr, nullptr, 0, - bind ? kal::process::bound_lifetime_flag.bits : kal_uintptr{ 0 } }; + (bind ? kal::process::bound_lifetime_flag.bits : kal_uintptr{ 0 }) + | (o.one_string ? kal::process::one_string_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; @@ -387,6 +396,34 @@ result run(spawn_options options, std::string_view input) { return out; } +result exec(spawn_options o) { + if (o.in != stdio::inherit || o.out != stdio::inherit || o.err != stdio::inherit) + return std::unexpected(error{ errc::invalid, + "a program this one becomes has this program's own streams" }); + std::filesystem::path program = o.program; + if (!fs::names_a_place(fs::generic_utf8(o.program))) { + auto found = env::find_executable(fs::generic_utf8(program)); + if (!found) return std::unexpected(error{ errc::not_found, std::format("{} is not on PATH", fs::generic_utf8(program)) }); + program = *found; + } + program = fs::absolute(program).lexically_normal(); + std::vector argv{ o.argv0.value_or(fs::utf8(program)) }; + for (const auto& a : o.args) argv.push_back(a); + if (auto r = sys::replace_image(program, argv, environment_for(o), o.cwd); + r || r.error().code != errc::not_supported) + return r; // returned: the image was not replaced + // No replacement here: the program runs in a unit bound to this one, and + // this one ends as it ended. + o.own_unit = true; + o.bound_lifetime = true; + o.program = program; + auto p = process::spawn(o); + if (!p) return std::unexpected(p.error()); + auto st = p->wait(); + if (!st) return std::unexpected(st.error()); + std::exit(st->code); +} + 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")); diff --git a/src/process.cppm b/src/process.cppm index ee2f385..9e61861 100644 --- a/src/process.cppm +++ b/src/process.cppm @@ -14,10 +14,12 @@ export import mcpplibs.os.error; export namespace mcpplibs::os::process { // 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 }; +// program, the null device, (`error') this program's standard error --- for a +// program whose standard output carries a protocol, where what a child writes +// may not reach it --- or (`output', for the error stream only) the stream the +// program's standard output is: one pipe carries both, in the order written, +// which is the order a person reads a compiler's diagnostics in. +enum class stdio { inherit, pipe, null, error, output }; struct spawn_options { // An absolute name, or a bare one looked for on PATH (`env::find_executable'). @@ -36,6 +38,12 @@ struct spawn_options { bool own_unit = false; // The program ends when this one ends, however this one ends. bool bound_lifetime = false; + // Where a program receives one string and splits it by rules of its own + // (Windows), the arguments are joined after the program's name as they are, + // unquoted: for the command interpreter, whose `/s /c' takes the rest of + // the line as a command of its own grammar (0.2.0, openkal 0.17.1). + // errc::not_supported where a program receives a vector. + bool one_string = false; }; // How a program ended: the status it returned, or, ended by its environment @@ -110,6 +118,15 @@ struct output { }; result run(spawn_options options, std::string_view input = {}); +// Becomes the program: returns only when it could not be started. Where the +// system replaces a program's image (Linux, macOS) this program's image is +// replaced, keeping its identity, its terminal and its place among the +// processes that wait for it; elsewhere the program is started with this +// program's three streams, in a unit bound to this program, and this program +// ends as it ended. Either way the status a caller sees is the program's own. +// Streams other than `inherit' are refused: nothing remains to read them. +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. @@ -134,3 +151,12 @@ result describe(std::uint64_t id); bool alive(const program& p); } // namespace mcpplibs::os::process + +// Each system's own (src//exec.cpp): the image replaced by the program at +// `program', or errc::not_supported where the system does not replace one. +namespace mcpplibs::os::process::sys { +result replace_image(const std::filesystem::path& program, + const std::vector& argv, + const std::vector& environment, + const std::filesystem::path& cwd); +} // namespace mcpplibs::os::process::sys diff --git a/src/stdio.cpp b/src/stdio.cpp index 393885c..0b756d3 100644 --- a/src/stdio.cpp +++ b/src/stdio.cpp @@ -3,6 +3,7 @@ module mcpplibs.os.stdio; import std; import openkal.types; import openkal.stream; +import openkal.timeout; import mcpplibs.os.error; namespace mcpplibs::os::stdio { @@ -50,4 +51,50 @@ result flush_output() { return {}; } +result> read_input_for(std::chrono::milliseconds timeout, std::size_t at_most) { + std::string out(at_most, '\0'); + // openkal reads a bound of zero as no bound; a zero timeout here asks + // without waiting, as the reads of a started program's streams do. + const auto count = std::chrono::duration_cast(timeout).count(); + const auto ns = static_cast(count < 1 ? 1 : count); + const kal_intptr n = kal_timeout_read(kal_stdin(), out.data(), out.size(), ns); + 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{} }; + return std::unexpected(from_kal(code, "reading standard input")); + } + out.resize(static_cast(n)); + return std::optional{ std::move(out) }; +} + +result output_to_error::begin() { + (void)flush_output(); + const int saved = sys::divert_output(); + if (saved < 0) return std::unexpected(error{ errc::not_supported, "sending standard output to standard error" }); + output_to_error d; + d.saved_ = saved; + return d; +} + +output_to_error::output_to_error(output_to_error&& o) noexcept : saved_(std::exchange(o.saved_, -1)) {} +output_to_error& output_to_error::operator=(output_to_error&& o) noexcept { + if (this != &o) { + if (saved_ >= 0) sys::restore_output(saved_); + saved_ = std::exchange(o.saved_, -1); + } + return *this; +} +output_to_error::~output_to_error() { + if (saved_ < 0) return; + (void)flush_output(); + sys::restore_output(saved_); +} + +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" }); + return {}; +} + } // namespace mcpplibs::os::stdio diff --git a/src/stdio.cppm b/src/stdio.cppm index d111423..5c10d9a 100644 --- a/src/stdio.cppm +++ b/src/stdio.cppm @@ -21,4 +21,39 @@ result write_output(std::string_view bytes); result write_error(std::string_view bytes); result flush_output(); +// One chunk of input that arrives within `timeout'; nothing when none did, +// empty at the end of input (0.2.0). A zero timeout asks without waiting. +// errc::not_supported where the input cannot be read with a bound. +result> read_input_for(std::chrono::milliseconds timeout, + std::size_t at_most = 65536); + +// THIS PROGRAM'S STANDARD OUTPUT IS ITS STANDARD ERROR UNTIL THIS IS DESTROYED +// (0.2.0): what it writes there, and what the programs it starts meanwhile +// write, reaches standard error, and `write_original' reaches the output as it +// was. A program whose output is a document narrates on standard error while +// it works and writes the document alone. The saved output is not inherited by +// the programs it starts. errc::not_supported where the streams cannot be +// rearranged (an image). +class output_to_error { +public: + static result begin(); + output_to_error() = default; + output_to_error(output_to_error&&) noexcept; + output_to_error& operator=(output_to_error&&) noexcept; + output_to_error(const output_to_error&) = delete; + output_to_error& operator=(const output_to_error&) = delete; + ~output_to_error(); + result write_original(std::string_view bytes); +private: + int saved_ = -1; +}; + } // namespace mcpplibs::os::stdio + +// Each system's own (src//stdio.cpp): the output descriptor saved and +// replaced by the error one (-1 when not possible), put back, and written. +namespace mcpplibs::os::stdio::sys { +int divert_output(); +void restore_output(int saved); +bool write_descriptor(int descriptor, std::string_view bytes); +} // namespace mcpplibs::os::stdio::sys diff --git a/src/terminal.cpp b/src/terminal.cpp index d23d408..0d317e1 100644 --- a/src/terminal.cpp +++ b/src/terminal.cpp @@ -48,12 +48,16 @@ bool color(stream s) { return interactive(s) && sequences(s); } -result raw_mode::enter() { +bool foreground() { return interactive(stream::in) && sys::foreground(); } + +result raw_mode::enter(raw_options options) { 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; + const kal_uintptr raw = options.keep_interrupt + ? kal::terminal::pass_input.bits + : (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; diff --git a/src/terminal.cppm b/src/terminal.cppm index 0e9991f..dd31646 100644 --- a/src/terminal.cppm +++ b/src/terminal.cppm @@ -24,11 +24,19 @@ bool sequences(stream s); // otherwise colour is written to a terminal that obeys the sequences. bool color(stream s); +// Whether this program may change the terminal's mode: its input is a terminal +// and, on POSIX, this program is in the terminal's foreground --- a background +// job that changed it would be stopped (SIGTTOU) (0.2.0). +bool foreground(); + // Keystrokes as they are typed --- no line assembly, no echo, none kept by the // system, none altered --- until destroyed, when the previous mode returns. +// `keep_interrupt' leaves the system the keystroke that interrupts the program +// (Ctrl-C), so a program that reads keys still ends as a person asks (0.2.0). +struct raw_options { bool keep_interrupt = false; }; class raw_mode { public: - static result enter(); + static result enter(raw_options options = {}); raw_mode() = default; raw_mode(raw_mode&&) noexcept; raw_mode& operator=(raw_mode&&) noexcept; @@ -41,3 +49,7 @@ private: }; } // namespace mcpplibs::os::terminal + +namespace mcpplibs::os::terminal::sys { +bool foreground(); +} // namespace mcpplibs::os::terminal::sys diff --git a/src/windows.cppm b/src/windows.cppm index 7ca4156..440a210 100644 --- a/src/windows.cppm +++ b/src/windows.cppm @@ -13,4 +13,20 @@ bool elevated(); // build places its output on; nothing when the system cannot say. std::optional on_dev_drive(const std::filesystem::path& directory); +// The ANSI code page the narrow interfaces of this process use (GetACP): 65001 +// under an application manifest that asks for UTF-8 (0.2.0). +unsigned code_page(); +// The output code page of the console the stream writes to; 0 when the stream +// is not a console. +unsigned console_output_code_page(bool error_stream = false); +// Whether the system lets a program that declares itself long-path aware use +// names past MAX_PATH (LongPathsEnabled). +bool long_paths_enabled(); +// A directory named by a string value under HKEY_LOCAL_MACHINE, read in the +// 64-bit view and then the 32-bit one, without a trailing separator. +std::optional machine_registry_path(std::wstring_view key, std::wstring_view value); +// Whether `drive' is a fixed drive on which this user can create a file, and +// `child' under its root is absent or a plain directory. +bool writable_fixed_drive(char drive, std::wstring_view child); + } // namespace mcpplibs::os::windows diff --git a/src/windows/exec.cpp b/src/windows/exec.cpp new file mode 100644 index 0000000..a83784f --- /dev/null +++ b/src/windows/exec.cpp @@ -0,0 +1,15 @@ +// This system replaces no program's image: `exec' starts the program and ends +// as it ended (src/process.cpp). +module mcpplibs.os.process; + +import std; +import mcpplibs.os.error; + +namespace mcpplibs::os::process::sys { + +result replace_image(const std::filesystem::path&, const std::vector&, + const std::vector&, const std::filesystem::path&) { + return std::unexpected(error{ errc::not_supported, "this system replaces no program's image" }); +} + +} // namespace mcpplibs::os::process::sys diff --git a/src/windows/fs.cpp b/src/windows/fs.cpp new file mode 100644 index 0000000..3cadffc --- /dev/null +++ b/src/windows/fs.cpp @@ -0,0 +1,91 @@ +// Windows's own of mcpplibs.os.fs: durability (FlushFileBuffers), the volume's +// kind, the owner of a node (advapi32, reached at run time); no exchange. +module; +#include "win32.h" + +module mcpplibs.os.fs; + +import std; +import mcpplibs.os.error; + +namespace mcpplibs::os::fs::sys { + +namespace { +constexpr okos_dword kNotSupported = 50, kInvalidFunction = 1, kFileNotFound = 2; +okos_handle invalid() { return reinterpret_cast(static_cast(-1)); } +} // namespace + +result sync(const std::filesystem::path& node) { + std::error_code ec; + if (std::filesystem::is_directory(node, ec)) return {}; // nothing of a directory is flushed here + const okos_handle h = CreateFileW(node.c_str(), 0x40000000 /* GENERIC_WRITE */, 0x7 /* share all */, + nullptr, 3 /* OPEN_EXISTING */, 0x80 /* NORMAL */, nullptr); + if (h == invalid() || h == nullptr) { + const auto e = GetLastError(); + return std::unexpected(error{ e == kFileNotFound ? errc::not_found : errc::io, + std::format("making {} durable: {}", mcpplibs::os::fs::generic_utf8(node), std::system_category().message(static_cast(e))), + static_cast(e) }); + } + const bool ok = FlushFileBuffers(h) != 0; + const auto e = GetLastError(); + CloseHandle(h); + if (!ok) return std::unexpected(error{ errc::io, std::format("making {} durable: {}", mcpplibs::os::fs::generic_utf8(node), + std::system_category().message(static_cast(e))), + static_cast(e) }); + return {}; +} + +int exchange(const std::filesystem::path&, const std::filesystem::path&) { return static_cast(kNotSupported); } +bool exchange_unsupported(int c) { + return c == static_cast(kNotSupported) || c == static_cast(kInvalidFunction); +} + +std::string volume_type(const std::filesystem::path& directory) { + wchar_t root[260] {}, name[260] {}; + if (!GetVolumePathNameW(directory.c_str(), root, 260)) return {}; + if (!GetVolumeInformationW(root, nullptr, 0, nullptr, nullptr, nullptr, name, 260)) return {}; + return mcpplibs::os::fs::utf8(std::filesystem::path(name)); +} + +owner owner_of(const std::filesystem::path& node) { + using named_security = okos_dword (__stdcall*)(const wchar_t*, int, okos_dword, void**, void**, void**, void**, void**); + using well_known = okos_bool (__stdcall*)(void*, int); + 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*); + using equal_sid = okos_bool (__stdcall*)(void*, void*); + const okos_handle advapi = LoadLibraryW(L"advapi32.dll"); + if (!advapi) return owner::unknown; + const auto security = reinterpret_cast(GetProcAddress(advapi, "GetNamedSecurityInfoW")); + const auto known = reinterpret_cast(GetProcAddress(advapi, "IsWellKnownSid")); + const auto open = reinterpret_cast(GetProcAddress(advapi, "OpenProcessToken")); + const auto query = reinterpret_cast(GetProcAddress(advapi, "GetTokenInformation")); + const auto equal = reinterpret_cast(GetProcAddress(advapi, "EqualSid")); + if (!security || !known || !open || !query || !equal) return owner::unknown; + void* who = nullptr; + void* descriptor = nullptr; + if (security(node.c_str(), 1 /* SE_FILE_OBJECT */, 0x1 /* OWNER_SECURITY_INFORMATION */, + &who, nullptr, nullptr, nullptr, &descriptor) != 0) + return owner::unknown; + owner found = owner::another_user; + if (known(who, 22 /* WinLocalSystemSid */) || known(who, 26 /* WinBuiltinAdministratorsSid */)) { + found = owner::system; + } else { + okos_handle token = nullptr; + if (open(GetCurrentProcess(), 0x0008 /* TOKEN_QUERY */, &token)) { + okos_dword size = 0; + query(token, 1 /* TokenUser */, nullptr, 0, &size); + std::vector buffer(size); + // TOKEN_USER begins with the user's SID_AND_ATTRIBUTES, whose first member is the SID. + if (size >= sizeof(void*) && query(token, 1, buffer.data(), size, &size) + && equal(who, *reinterpret_cast(buffer.data()))) + found = owner::this_user; + CloseHandle(token); + } + } + LocalFree(descriptor); + return found; +} + +bool writable(const std::filesystem::path&) { return true; } + +} // namespace mcpplibs::os::fs::sys diff --git a/src/windows/interrupt.cpp b/src/windows/interrupt.cpp new file mode 100644 index 0000000..ac4158a --- /dev/null +++ b/src/windows/interrupt.cpp @@ -0,0 +1,6 @@ +// No signal ends a program whose reader is gone here: its writes are refused. +module mcpplibs.os.interrupt; + +namespace mcpplibs::os::interrupt::sys { +void end_as_output_closed() {} +} // namespace mcpplibs::os::interrupt::sys diff --git a/src/windows/machine.cpp b/src/windows/machine.cpp index 7fc2772..ad308f7 100644 --- a/src/windows/machine.cpp +++ b/src/windows/machine.cpp @@ -25,4 +25,40 @@ std::optional memory() { return s.total_physical; } +std::optional memory_available() { + okos_memory_status s{}; + s.length = sizeof s; + if (!GlobalMemoryStatusEx(&s)) return std::nullopt; + return s.available_physical; +} + +processor_count processors() { + processor_count c; + const auto n = GetActiveProcessorCount(0xffff /* ALL_PROCESSOR_GROUPS */); + if (n > 0) c.logical = static_cast(n); + c.physical = c.logical; + // One record per core (RelationProcessorCore): the count, and an + // efficiency class that differs between cores of differing speed. + okos_dword bytes = 0; + GetLogicalProcessorInformationEx(0 /* RelationProcessorCore */, nullptr, &bytes); + if (bytes == 0) return c; + std::vector buf(bytes); + if (!GetLogicalProcessorInformationEx(0, buf.data(), &bytes)) return c; + std::uint32_t count = 0; + int first = -1; + for (okos_dword off = 0; off + 8 < bytes;) { + // SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX: Relationship (4), Size (4), + // then PROCESSOR_RELATIONSHIP: Flags (1), EfficiencyClass (1), ... + const auto size = *reinterpret_cast(buf.data() + off + 4); + if (size == 0) break; + const int cls = buf[off + 9]; + ++count; + if (first < 0) first = cls; + else if (cls != first) c.heterogeneous = true; + off += size; + } + if (count > 0) c.physical = count; + return c; +} + } // namespace mcpplibs::os::machine diff --git a/src/windows/stdio.cpp b/src/windows/stdio.cpp new file mode 100644 index 0000000..295270a --- /dev/null +++ b/src/windows/stdio.cpp @@ -0,0 +1,38 @@ +// The C runtime's output descriptor saved (its handle not inherited) and +// replaced by the error one; `_dup2' to a standard descriptor also sets the +// process's standard handle, which a started program receives. +module; +#include "win32.h" + +module mcpplibs.os.stdio; + +import std; + +namespace mcpplibs::os::stdio::sys { + +int divert_output() { + const int saved = _dup(1); + if (saved >= 0) { + if (const long long h = _get_osfhandle(saved); h != -1) + SetHandleInformation(reinterpret_cast(h), 0x1 /* HANDLE_FLAG_INHERIT */, 0); + _dup2(2, 1); + } + return saved; +} + +void restore_output(int saved) { + _dup2(saved, 1); + _close(saved); +} + +bool write_descriptor(int descriptor, std::string_view bytes) { + std::size_t done = 0; + while (done < bytes.size()) { + const int n = _write(descriptor, bytes.data() + done, static_cast(bytes.size() - done)); + if (n <= 0) return false; + done += static_cast(n); + } + return true; +} + +} // namespace mcpplibs::os::stdio::sys diff --git a/src/windows/terminal.cpp b/src/windows/terminal.cpp new file mode 100644 index 0000000..dc12a36 --- /dev/null +++ b/src/windows/terminal.cpp @@ -0,0 +1,6 @@ +// A console has no foreground group to be outside of. +module mcpplibs.os.terminal; + +namespace mcpplibs::os::terminal::sys { +bool foreground() { return true; } +} // namespace mcpplibs::os::terminal::sys diff --git a/src/windows/win32.h b/src/windows/win32.h index b1adb48..217692e 100644 --- a/src/windows/win32.h +++ b/src/windows/win32.h @@ -35,4 +35,27 @@ __declspec(dllimport) okos_handle __stdcall GetModuleHandleW(const wchar_t*); __declspec(dllimport) okos_dword __stdcall GetLastError(void); __declspec(dllimport) okos_bool __stdcall K32EnumProcessModules(okos_handle, okos_handle*, okos_dword, okos_dword*); __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned, okos_dword, const char*, int, wchar_t*, int); +// 0.2.0 +__declspec(dllimport) okos_dword __stdcall GetLastError(void); +__declspec(dllimport) okos_bool __stdcall FlushFileBuffers(okos_handle); +__declspec(dllimport) okos_bool __stdcall GetVolumePathNameW(const wchar_t*, wchar_t*, okos_dword); +__declspec(dllimport) okos_bool __stdcall GetVolumeInformationW(const wchar_t*, wchar_t*, okos_dword, okos_dword*, + okos_dword*, okos_dword*, wchar_t*, okos_dword); +__declspec(dllimport) void* __stdcall LocalFree(void*); +__declspec(dllimport) okos_dword __stdcall GetActiveProcessorCount(unsigned short); +__declspec(dllimport) okos_bool __stdcall GetLogicalProcessorInformationEx(int, void*, okos_dword*); +__declspec(dllimport) unsigned __stdcall GetACP(void); +__declspec(dllimport) unsigned __stdcall GetConsoleOutputCP(void); +__declspec(dllimport) okos_bool __stdcall GetConsoleMode(okos_handle, okos_dword*); +__declspec(dllimport) unsigned __stdcall GetDriveTypeW(const wchar_t*); +__declspec(dllimport) okos_dword __stdcall GetFileAttributesW(const wchar_t*); +__declspec(dllimport) okos_dword __stdcall GetCurrentProcessId(void); +__declspec(dllimport) unsigned long long __stdcall GetTickCount64(void); +__declspec(dllimport) okos_bool __stdcall SetHandleInformation(okos_handle, okos_dword, okos_dword); +// The C runtime's descriptors (every Windows C environment has these). +int __cdecl _dup(int); +int __cdecl _dup2(int, int); +int __cdecl _close(int); +long long __cdecl _get_osfhandle(int); +int __cdecl _write(int, const void*, unsigned); } diff --git a/src/windows/windows.cpp b/src/windows/windows.cpp index 3eb5489..c4df701 100644 --- a/src/windows/windows.cpp +++ b/src/windows/windows.cpp @@ -45,4 +45,68 @@ std::optional on_dev_drive(const std::filesystem::path& directory) { return (info.volume_flags & 0x00002000) != 0; } +unsigned code_page() { return GetACP(); } + +unsigned console_output_code_page(bool error_stream) { + const long long h = _get_osfhandle(error_stream ? 2 : 1); + okos_dword mode = 0; + if (h == -1 || !GetConsoleMode(reinterpret_cast(h), &mode)) return 0; + return GetConsoleOutputCP(); +} + +namespace { +using reg_get_value = long (__stdcall*)(void*, const wchar_t*, const wchar_t*, okos_dword, okos_dword*, void*, okos_dword*); +reg_get_value registry_reader() { + const okos_handle advapi = LoadLibraryW(L"advapi32.dll"); + return advapi ? reinterpret_cast(GetProcAddress(advapi, "RegGetValueW")) : nullptr; +} +void* const kLocalMachine = reinterpret_cast(static_cast(static_cast(0x80000002))); +} // namespace + +bool long_paths_enabled() { + const auto get = registry_reader(); + if (!get) return false; + okos_dword value = 0, bytes = sizeof value; + return get(kLocalMachine, L"SYSTEM\\CurrentControlSet\\Control\\FileSystem", L"LongPathsEnabled", + 0x10 /* RRF_RT_REG_DWORD */, nullptr, &value, &bytes) == 0 + && value == 1; +} + +std::optional machine_registry_path(std::wstring_view key, std::wstring_view value) { + const auto get = registry_reader(); + if (!get) return std::nullopt; + const std::wstring k(key), v(value); + for (const okos_dword view : { okos_dword{ 0x0100 } /* KEY_WOW64_64KEY */, okos_dword{ 0x0200 } /* 32KEY */ }) { + okos_dword bytes = 0; + if (get(kLocalMachine, k.c_str(), v.c_str(), 0x2 /* RRF_RT_REG_SZ */ | view, nullptr, nullptr, &bytes) != 0 + || bytes < sizeof(wchar_t)) + continue; + std::wstring buf(bytes / sizeof(wchar_t), L'\0'); + if (get(kLocalMachine, k.c_str(), v.c_str(), 0x2 | view, nullptr, buf.data(), &bytes) != 0) continue; + while (!buf.empty() && (buf.back() == L'\0' || buf.back() == L'\\')) buf.pop_back(); + if (!buf.empty()) return std::filesystem::path(buf); + } + return std::nullopt; +} + +bool writable_fixed_drive(char drive, std::wstring_view child) { + const wchar_t root[] = { static_cast(drive), L':', L'\\', L'\0' }; + if (GetDriveTypeW(root) != 3 /* DRIVE_FIXED */) return false; + const auto candidate = std::filesystem::path(root) / child; + const auto attributes = GetFileAttributesW(candidate.c_str()); + if (attributes == 0xFFFFFFFFu) { + const auto e = GetLastError(); + if (e != 2 /* FILE_NOT_FOUND */ && e != 3 /* PATH_NOT_FOUND */) return false; + } else if (!(attributes & 0x10 /* DIRECTORY */) || (attributes & 0x400 /* REPARSE_POINT */)) { + return false; + } + const auto probe = std::filesystem::path(root) + / std::format(".os-write-{}-{}", GetCurrentProcessId(), GetTickCount64()); + const okos_handle h = CreateFileW(probe.c_str(), 0x40000000 /* GENERIC_WRITE */, 0, nullptr, + 1 /* CREATE_NEW */, 0x100 | 0x04000000 /* TEMPORARY | DELETE_ON_CLOSE */, nullptr); + if (h == reinterpret_cast(static_cast(-1)) || h == nullptr) return false; + CloseHandle(h); + return true; +} + } // namespace mcpplibs::os::windows diff --git a/tests/fs_test.cpp b/tests/fs_test.cpp index 4f619fc..d9eec8e 100644 --- a/tests/fs_test.cpp +++ b/tests/fs_test.cpp @@ -118,5 +118,41 @@ int main() { if (again) again->release(); stdfs::remove_all(dir); } + { + // 0.2.0: appending, durability, measured volumes, placing, exchanging. + const auto dir = std::filesystem::temp_directory_path() + / std::format("mcpplibs-os-fs2-{}", os::process::self()->id); + std::filesystem::create_directories(dir); + check(os::fs::append(dir / "log", "a").has_value() && os::fs::append(dir / "log", "b").has_value() + && os::fs::read(dir / "log").value_or("") == "ab", + "appends land whole at the end, the file created by the first"); + check(os::fs::sync(dir / "log").has_value() && os::fs::sync(dir).has_value(), + "a file and a directory are made durable"); + const auto b = os::fs::measure(dir); + check(b.measured, "a writable directory's volume is measured"); + (void)os::fs::write(dir / "x", "x"); + check(os::fs::place(dir / "x", dir / "y").has_value() && os::fs::read(dir / "y").value_or("") == "x" + && !std::filesystem::exists(dir / "x"), + "an entry is placed under a free name"); + (void)os::fs::write(dir / "z", "z"); + const auto refused = os::fs::place(dir / "z", dir / "y"); + check(!refused && os::fs::read(dir / "y").value_or("") == "x", "and never over one that is there"); + (void)os::fs::write(dir / "p", "p"); + (void)os::fs::write(dir / "q", "q"); + check(os::fs::exchange(dir / "p", dir / "q").has_value() + && os::fs::read(dir / "p").value_or("") == "q" && os::fs::read(dir / "q").value_or("") == "p", + "two entries trade places"); + if (!os::is_image) { + check(os::fs::owner_of(dir) == os::fs::owner::this_user, "a directory this program made is its user's"); + check(os::fs::writable(dir), "and this user may write it"); + } + std::error_code ec; + std::filesystem::remove_all(dir, ec); + } + check(os::fs::windows_extended("C:/a/./b/../c") == "\\\\?\\C:\\a\\c", "a drive name in the long spelling, resolved"); + check(os::fs::windows_extended("//server/share/x") == "\\\\?\\UNC\\server\\share\\x", "a share in the long spelling"); + check(os::fs::windows_extended("C:/..") == "\\\\?\\C:\\", "and `..' never climbs above the drive"); + check(os::fs::windows_extended("rel/x") == "rel/x", "a relative name is not this rule's"); + if (os::host() != os::platform::windows) check(os::fs::extended("/a/b") == "/a/b", "elsewhere the name is itself"); return g_failures; } diff --git a/tests/platform_test.cpp b/tests/platform_test.cpp index d7ecc12..b337163 100644 --- a/tests/platform_test.cpp +++ b/tests/platform_test.cpp @@ -26,5 +26,14 @@ int main() { 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"); + { + const auto p = os::machine::processors(); + check(p.logical >= 1 && p.physical >= 1 && p.physical <= p.logical, "processors: logical and physical counted"); + if (!os::is_image) check(os::machine::memory_available().value_or(0) > 0, "and the memory available now"); + } + if constexpr (os::is_windows) { + check(os::windows::code_page() != 0, "Windows: the ANSI code page is known"); + (void)os::windows::long_paths_enabled(); + } return g_failures; } diff --git a/tests/process_test.cpp b/tests/process_test.cpp index 252d1d7..1aa093a 100644 --- a/tests/process_test.cpp +++ b/tests/process_test.cpp @@ -57,6 +57,67 @@ int child(std::string_view errand) { (void)os::stdio::write_output(std::filesystem::current_path().filename().string()); return 0; } + if (errand == "both") { + // Written in turn to both streams: a reader of one pipe for both sees + // them in this order. + (void)os::stdio::write_output("1"); + (void)os::stdio::write_error("2"); + (void)os::stdio::write_output("3"); + return 0; + } + if (errand == "become") { + // This program becomes another copy of itself that returns 7. + auto o = copy_of_self("seven"); + o.out = os::process::stdio::inherit; + o.err = os::process::stdio::inherit; + auto r = os::process::exec(o); + (void)os::stdio::write_error(r ? "returned" : r.error().message()); + return 1; + } + if (errand == "seven") { + (void)os::stdio::write_output("became"); + return 7; + } + if (errand == "wait-input") { + // Started: the starter writes nothing until it reads this, so the + // reads below begin before any input can arrive however long the + // system takes to start a program. + (void)os::stdio::write_output("ready\n"); + (void)os::stdio::flush_output(); + // A zero bound asks without waiting. + const auto asked = std::chrono::steady_clock::now(); + auto zero = os::stdio::read_input_for(std::chrono::milliseconds(0)); + if (zero && zero->has_value()) { + (void)os::stdio::write_output((*zero)->empty() ? std::string("") : "<" + **zero + ">"); + return 6; + } + if (std::chrono::steady_clock::now() - asked > std::chrono::seconds(2)) return 7; + // Nothing arrives within the bound; then the input arrives and is read. + auto none = os::stdio::read_input_for(std::chrono::milliseconds(100)); + if (none && none->has_value()) { + // What arrived where nothing should have, for the report. + (void)os::stdio::write_output((*none)->empty() ? std::string("") : "<" + **none + ">"); + return (*none)->empty() ? 8 : 2; + } + if (!none) { + (void)os::stdio::write_output("<" + none.error().message() + ">"); + return 9; + } + auto some = os::stdio::read_input_for(std::chrono::seconds(20)); + (void)os::stdio::write_output(some && some->has_value() ? **some : std::string("")); + return none ? 0 : 3; + } + if (errand == "divert") { + auto d = os::stdio::output_to_error::begin(); + if (!d) return 4; + (void)os::stdio::write_output("narration"); + (void)d->write_original("document"); + return 0; + } + if (errand == "end-closed") { + os::interrupt::end_as_output_closed(); + return 5; // where the system has no such ending + } if (errand == "sleep") { std::this_thread::sleep_for(std::chrono::seconds(30)); return 0; @@ -169,6 +230,89 @@ int main(int argc, char** argv) { } } } + { + auto o = copy("both"); + o.out = os::process::stdio::pipe; + o.err = os::process::stdio::output; + auto p = os::process::process::spawn(o); + std::string all; + if (p) { + for (;;) { auto c = p->read_output(); if (!c || c->empty()) break; all += *c; } + (void)p->wait(); + } + check(all == "123", "one pipe carries a program's output and error output in the order written"); + } + { + auto o = copy("become"); + auto r = os::process::run(o); + check(r && r->out == "became" && r->status.code == 7, + "a program that becomes another ends with that program's status and output"); + } + if constexpr (os::is_posix) { + // A file that is no image is refused as not a program, before this + // program is replaced. + const auto file = std::filesystem::temp_directory_path() / "os-not-a-program"; + { std::ofstream(file) << "this is not an executable format\n"; } + std::filesystem::permissions(file, std::filesystem::perms::owner_all); + os::process::spawn_options o; + o.program = file; + o.in = o.out = o.err = os::process::stdio::inherit; + auto r = os::process::exec(o); + check(!r && r.error().code == os::errc::not_program, "a file that is no image is not a program to become"); + std::filesystem::remove(file); + } + if (!os::is_image) { + auto o = copy("wait-input"); + o.in = os::process::stdio::pipe; + o.out = os::process::stdio::pipe; + auto p = os::process::process::spawn(o); + if (p) { + std::string out; + while (out.find('\n') == std::string::npos) { + auto c = p->read_output(); + if (!c || c->empty()) break; + out += *c; + } + std::this_thread::sleep_for(std::chrono::milliseconds(400)); + (void)p->write("late"); + p->close_input(); + for (;;) { auto c = p->read_output(); if (!c || c->empty()) break; out += *c; } + auto st = p->wait(); + const bool held = st && st->code == 0 && out == "ready\nlate"; + if (!held) + std::printf(" (the copy ended with %d and wrote '%s')\n", st ? st->code : -1, out.c_str()); + check(held, "a zero bound asks without waiting, a bounded read of input expires, then reads what arrives"); + } + 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"); + } + { + // The command interpreter given a command with quotation marks, joined + // as it is; refused where a program receives a vector. + os::process::spawn_options o; + o.one_string = true; + if (os::host() == os::platform::windows) { + o.program = "cmd.exe"; + o.args = { "/d", "/s", "/c", "\"echo \"a b\" && echo c\"" }; + auto r = os::process::run(o); + check(r && r->out.find("\"a b\"") != std::string::npos && r->out.find('c') != std::string::npos, + "the command interpreter runs a command with quotation marks as written"); + } else { + o.program = again.front(); + auto r = os::process::run(o); + check(!r && r.error().code == os::errc::not_supported, + "a joined command line is refused where a program receives a vector"); + } + } + if (!os::is_image) { + auto r = os::process::run(copy("end-closed")); + if (os::host() == os::platform::windows) + check(r && r->status.code == 5, "Windows has no ending for a closed reader, and the program goes on"); + else + check(r && r->status.by_environment && r->status.shell_code() == 141, + "a program ends as a closed reader ends it: 141 to a shell"); + } { auto me = os::process::self(); check(me && me->id != 0, "this program has an identifier");