diff --git a/CMakeLists.txt b/CMakeLists.txt index 8eb1adf..7bf4ea2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,11 +38,19 @@ if(CFBOX_ENABLE_AWK) file(GLOB_RECURSE AWK_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/applets/awk/*.cpp) list(APPEND CFBOX_APPLET_SOURCES ${AWK_SOURCES}) endif() +if(CFBOX_ENABLE_INIT) + file(GLOB_RECURSE INIT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/applets/init/*.cpp) + list(APPEND CFBOX_APPLET_SOURCES ${INIT_SOURCES}) +endif() +if(CFBOX_ENABLE_TOP) + file(GLOB_RECURSE TOP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/applets/top/*.cpp) + list(APPEND CFBOX_APPLET_SOURCES ${TOP_SOURCES}) +endif() # Single-file applets foreach(applet IN LISTS CFBOX_APPLETS) string(TOUPPER "${applet}" APPLET_UPPER) - if(NOT applet STREQUAL "sh" AND NOT applet STREQUAL "awk" AND CFBOX_ENABLE_${APPLET_UPPER}) + if(NOT applet STREQUAL "sh" AND NOT applet STREQUAL "awk" AND NOT applet STREQUAL "init" AND NOT applet STREQUAL "top" AND CFBOX_ENABLE_${APPLET_UPPER}) list(APPEND CFBOX_APPLET_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/applets/${applet}.cpp) endif() endforeach() diff --git a/Roadmap.md b/Roadmap.md index 99e2946..d6bc307 100644 --- a/Roadmap.md +++ b/Roadmap.md @@ -18,12 +18,12 @@ CFBox 是一个 C++23 BusyBox 替代品,当前版本有 78 个 applet。项目 | 1 | POSIX Shell + Coreutils I ✅ | ~17 | Shell 引擎、进程管理、信号处理 | ~34 | | 2 | Coreutils II + findutils ✅ | ~44 | 流处理管线、校验和框架 | ~78 | | 3 | 归档 + 压缩 + 文本处理 ✅ | ~15 | 终端抽象、压缩框架 | ~93 | -| 4 | vi 可视化编辑器 | 1 | TUI 框架、屏幕渲染、键盘映射 | ~94 | -| 5 | 进程/Init + util-linux | ~38 | /proc 解析器 | ~132 | -| 6 | 网络 + 登录 + 日志 | ~35 | Socket 抽象、HTTP 解析、shadow 密码 | ~167 | +| 4 | 进程/Init + util-linux 🔧 | ~21/38 | /proc 解析器、init 系统、TUI 框架 | ~114 | +| 5 | vi 可视化编辑器 | 1 | TUI 框架、屏幕渲染、键盘映射 | ~133 | +| 6 | 网络 + 登录 + 日志 | ~35 | Socket 抽象、HTTP 解析、shadow 密码 | ~168 | | 7 | 剩余组件 + 集成验证 | ~40+ | POSIX 验证、容器替换测试 | ~200+ | -**当前状态**:Phase 0-3 已完成,93 个 applet,259 单元测试 + 54 集成测试全部通过。 +**当前状态**:Phase 0-3 已完成,Phase 4 进行中。114 个 applet,318 单元测试全部通过。CFBox 已可在 QEMU 中作为 PID 1 运行完整 init 系统。TUI 框架已就绪,为 Phase 5 vi 编辑器奠定基础。 --- @@ -154,7 +154,48 @@ Shell 已实现为第一个多文件 applet(`src/applets/sh/`,8 个模块, --- -## Phase 4:vi 可视化编辑器 +## Phase 4:进程管理 + Init 系统 + util-linux 🔧 + +**目标**:构建让 CFBox 适合作为完整 init 环境的系统级工具,applet 数量翻倍。 + +### 基础设施 ✅ +- **`/proc` 解析器** `include/cfbox/proc.hpp` ✅:集中解析 /proc/meminfo, /proc/stat, /proc/[pid]/stat, /proc/[pid]/cmdline, /proc/[pid]/status, /proc/loadavg, /proc/uptime, /proc/mounts, /proc/diskstats, /proc/partitions +- **TUI 框架** `include/cfbox/tui.hpp` ✅:全屏终端应用抽象(ScreenBuffer、Key 解析、TuiApp 虚基类、SIGWINCH 处理),top 和后续 vi/less 共用 + +### Init 系统 ✅ +- **inittab 解析器** ✅:解析 `/etc/inittab`,支持运行级别、respawn、once 条目 +- **运行级别管理** ✅:sysinit, boot, single-user, multi-user +- **服务监控** ✅:进程监控 + respawn 能力(指数退避) +- **关机/重启** ✅:SIGTERM → 等待 → SIGKILL → sync → 卸载 → reboot +- **QEMU 兼容** ✅:无 inittab 时自动回退 smoke test 模式,保持 CI 兼容 +- **getty 集成**:在 TTY 上生成登录提示 — 待 Phase 6 + +### procps(已完成 15/16) +`ps` ✅, `kill` ✅, `free` ✅, `uptime` ✅, `pgrep`/`pkill` ✅, `pidof` ✅, `sysctl` ✅, `pwdx` ✅, `pstree` ✅, `pmap` ✅, `fuser` ✅, `iostat` ✅, `watch` ✅, `top` ✅ + +`lsof` — 待实现 + +### util-linux(已完成 6/30) +`dmesg` ✅, `hexdump` ✅, `more` ✅, `rev` ✅, `cal` ✅, `renice` ✅ + +**存储/块设备**(待实现):`mount`/`umount`, `blkid`, `blockdev`, `fdisk`, `mkfs`, `fsck`, `losetup`, `pivot_root`, `switch_root`, `swapon`/`swapoff` + +**系统工具**(待实现):`flock`, `getopt`, `setsid`, `nsenter`, `unshare`, `mdev`, `lspci`, `lsusb`, `hwclock`, `rtcwake`, `taskset`, `chrt`, `ionice`, `last`, `mesg`, `wall`, `script` + +### 验证 ✅(已通过) +- CFBox 作为 PID 1 在 QEMU aarch64 中启动,运行 inittab,执行 sysinit 命令,spawn shell(respawn),处理关机 ✅ +- `ps aux` 输出与 procps 格式匹配 ✅ +- `free -h`、`uptime`、`kill -l`、`pidof`、`sysctl` 在 QEMU 中正常工作 ✅ +- 288 单元测试全部通过 ✅ +- `top -b -n 1` 在批处理模式下输出进程表 ✅ +- `pstree -p` 显示进程树和 PID ✅ +- `hexdump -C /dev/null`、`cal`、`rev` 功能正确 ✅ +- 318 单元测试全部通过 ✅ +- 容器测试:CFBox 替换 Alpine 容器中的 BusyBox — 待实现 + +--- + +## Phase 5:vi 可视化编辑器 **目标**:实现完整的 vi 可视化编辑器——CFBox 中最复杂的单一组件,需要独立的终端交互框架。 @@ -171,7 +212,7 @@ Shell 已实现为第一个多文件 applet(`src/applets/sh/`,8 个模块, - **滚动**:半页/整页滚动(Ctrl-D/Ctrl-U/Ctrl-F/Ctrl-B),长行折行显示 ### 基础设施 -- **TUI 框架** `include/cfbox/tui.hpp`:全屏终端应用抽象,vi/top/less 共用 +- TUI 框架已在 Phase 4 实现 ✅,vi 直接复用 - 屏幕缓冲区管理、增量渲染 - 键盘事件映射(普通键 + 转义序列解析) - 信号处理(SIGWINCH 终端大小变化) @@ -197,36 +238,6 @@ src/applets/vi/ --- -## Phase 5:进程管理 + Init 系统 + util-linux - -**目标**:构建让 CFBox 适合作为完整 init 环境的系统级工具。 - -### Init 系统(完整) -- **inittab 解析器**:解析 `/etc/inittab`,支持运行级别、respawn、once 条目 -- **运行级别管理**:sysinit, boot, single-user, multi-user -- **服务监控**:进程监控 + respawn 能力 -- **关机/重启**:SIGTERM → 等待 → SIGKILL → sync → 卸载 → 重启 -- **getty 集成**:在 TTY 上生成登录提示 - -### procps(解析 `/proc` 文件系统) -`ps`, `top`, `kill`, `free`, `uptime`, `pgrep`/`pkill`, `pidof`, `pmap`, `iostat`, `lsof`, `watch`, `sysctl`, `pstree`, `fuser`, `pwdx` - -### util-linux -**存储/块设备**:`mount`/`umount`, `blkid`, `blockdev`, `dmesg`, `fdisk`, `mkfs`, `fsck`, `losetup`, `pivot_root`, `switch_root`, `swapon`/`swapoff` - -**系统工具**:`hexdump`, `more`, `flock`, `getopt`, `cal`, `rev`, `setsid`, `nsenter`, `unshare`, `mdev`, `lspci`, `lsusb`, `hwclock`, `rtcwake`, `taskset`, `chrt`, `ionice`, `renice`, `last`, `mesg`, `wall`, `script` - -### 基础设施 -- **`/proc` 解析器** `include/cfbox/proc.hpp`:集中解析 /proc/meminfo, /proc/stat, /proc/[pid]/stat 等 -- TUI 框架已在 Phase 4 实现,top/less 可直接复用 - -### 验证 -- CFBox 作为 PID 1 在 QEMU 中启动,运行 inittab,生成 getty,处理关机 -- `ps aux` 输出与 procps 格式匹配 -- 容器测试:CFBox 替换 Alpine 容器中的 BusyBox,`docker run` 成功 - ---- - ## Phase 6:网络 + 登录管理 + 系统日志 **目标**:添加网络工具和用户/会话管理,使 CFBox 适用于网络启动系统和多用户环境。 @@ -297,7 +308,7 @@ src/applets/vi/ |------|------|---------| | **Shell 复杂度** | 最高——5000+ 行代码 | 增量构建:先非交互 → 行编辑 → 作业控制,从第一天开始测试 POSIX shell 测试套件 | | **AWK 复杂度** | 高——第二复杂组件 | 从 POSIX awk 子集开始,实现解释器而非编译器 | -| **vi 复杂度** | 高——终端处理微妙、独立 Phase 4 | 使用 Phase 3 的终端抽象 + 自建 TUI 框架,自动化按键注入测试,双缓冲 diff 驱动渲染 | +| **vi 复杂度** | 高——终端处理微妙、独立 Phase 5 | 使用 Phase 3 的终端抽象 + Phase 4 的 TUI 框架,自动化按键注入测试,双缓冲 diff 驱动渲染 | | **二进制体积膨胀** | 中——200+ applet | Phase 0 的 CMake 配置允许裁剪,LTO 和死代码消除,每阶段监控体积 | | **跨平台边界情况** | 中——ioctl、/proc 格式差异 | 平台特性抽象到 `include/cfbox/` 头文件,每阶段三平台测试 | | **网络安全** | 中——wget/httpd 需要 TLS | 先做 HTTP-only,HTTPS 通过可选 mbedTLS 依赖,作为 CMake 选项 | diff --git a/cmake/Config.cmake b/cmake/Config.cmake index a9a97aa..5264031 100644 --- a/cmake/Config.cmake +++ b/cmake/Config.cmake @@ -23,6 +23,10 @@ set(CFBOX_APPLETS xargs gzip gunzip diff cmp patch ed tar cpio ar unzip awk + free uptime kill pidof ps pgrep sysctl + pwdx pstree pmap fuser iostat + watch top + dmesg hexdump more rev cal renice ) foreach(applet IN LISTS CFBOX_APPLETS) diff --git a/cmake/compile/CompilerFlag.cmake b/cmake/compile/CompilerFlag.cmake index c729abf..1fc0c3a 100644 --- a/cmake/compile/CompilerFlag.cmake +++ b/cmake/compile/CompilerFlag.cmake @@ -36,6 +36,10 @@ target_compile_options(cfbox_compiler_flags INTERFACE $<$:-fno-sanitize-recover=all> ) +target_link_options(cfbox_compiler_flags INTERFACE + $<$:-fsanitize=address,undefined> +) + # ── Release-specific flags ──────────────────────────────────── if(CFBOX_OPTIMIZE_FOR_SIZE) target_compile_options(cfbox_compiler_flags INTERFACE diff --git a/configs/qemu-inittab b/configs/qemu-inittab new file mode 100644 index 0000000..ff1c857 --- /dev/null +++ b/configs/qemu-inittab @@ -0,0 +1,21 @@ +# CFBox /etc/inittab — QEMU aarch64 virt machine test +# Format: id:runlevels:action:process + +# Note: /proc, /sys, /dev are mounted automatically by init's sysinit phase. +# (mount applet is not yet available, init does it internally) + +# Boot test — verify applets work under init +::sysinit:/bin/echo "=== CFBox Init (inittab mode) ===" +::sysinit:/bin/uname -a +::sysinit:/bin/free -h +::sysinit:/bin/uptime +::sysinit:/bin/ps +::sysinit:/bin/echo "=== Inittab tests PASSED ===" + +# Phase 3: Spawn shell on serial console (respawn if killed) +ttyAMA0::respawn:/bin/sh + +# Signals +::ctrlaltdel:/bin/echo "Ctrl-Alt-Del pressed, rebooting..." +::shutdown:/bin/echo "Shutting down..." +::shutdown:/bin/sync diff --git a/include/cfbox/applet_config.hpp.in b/include/cfbox/applet_config.hpp.in index c4db151..bb3b757 100644 --- a/include/cfbox/applet_config.hpp.in +++ b/include/cfbox/applet_config.hpp.in @@ -93,3 +93,23 @@ #cmakedefine01 CFBOX_ENABLE_AR #cmakedefine01 CFBOX_ENABLE_UNZIP #cmakedefine01 CFBOX_ENABLE_AWK +#cmakedefine01 CFBOX_ENABLE_FREE +#cmakedefine01 CFBOX_ENABLE_UPTIME +#cmakedefine01 CFBOX_ENABLE_KILL +#cmakedefine01 CFBOX_ENABLE_PIDOF +#cmakedefine01 CFBOX_ENABLE_PS +#cmakedefine01 CFBOX_ENABLE_PGREP +#cmakedefine01 CFBOX_ENABLE_SYSCTL +#cmakedefine01 CFBOX_ENABLE_PWDX +#cmakedefine01 CFBOX_ENABLE_PSTREE +#cmakedefine01 CFBOX_ENABLE_PMAP +#cmakedefine01 CFBOX_ENABLE_FUSER +#cmakedefine01 CFBOX_ENABLE_IOSTAT +#cmakedefine01 CFBOX_ENABLE_WATCH +#cmakedefine01 CFBOX_ENABLE_TOP +#cmakedefine01 CFBOX_ENABLE_DMESG +#cmakedefine01 CFBOX_ENABLE_HEXDUMP +#cmakedefine01 CFBOX_ENABLE_MORE +#cmakedefine01 CFBOX_ENABLE_REV +#cmakedefine01 CFBOX_ENABLE_CAL +#cmakedefine01 CFBOX_ENABLE_RENICE diff --git a/include/cfbox/applets.hpp b/include/cfbox/applets.hpp index 4599510..9eee43e 100644 --- a/include/cfbox/applets.hpp +++ b/include/cfbox/applets.hpp @@ -271,6 +271,66 @@ extern auto unzip_main(int argc, char* argv[]) -> int; #if CFBOX_ENABLE_AWK extern auto awk_main(int argc, char* argv[]) -> int; #endif +#if CFBOX_ENABLE_FREE +extern auto free_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_UPTIME +extern auto uptime_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_KILL +extern auto kill_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_PIDOF +extern auto pidof_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_PS +extern auto ps_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_PGREP +extern auto pgrep_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_SYSCTL +extern auto sysctl_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_PWDX +extern auto pwdx_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_PSTREE +extern auto pstree_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_PMAP +extern auto pmap_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_FUSER +extern auto fuser_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_IOSTAT +extern auto iostat_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_WATCH +extern auto watch_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_TOP +extern auto top_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_DMESG +extern auto dmesg_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_HEXDUMP +extern auto hexdump_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_MORE +extern auto more_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_REV +extern auto rev_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_CAL +extern auto cal_main(int argc, char* argv[]) -> int; +#endif +#if CFBOX_ENABLE_RENICE +extern auto renice_main(int argc, char* argv[]) -> int; +#endif // registry — one line per applet, conditionally compiled constexpr auto APPLET_REGISTRY = std::to_array({ @@ -542,4 +602,65 @@ constexpr auto APPLET_REGISTRY = std::to_array({ #if CFBOX_ENABLE_AWK {"awk", awk_main, "pattern-directed scanning and processing language"}, #endif +#if CFBOX_ENABLE_FREE + {"free", free_main, "display amount of free and used memory"}, +#endif +#if CFBOX_ENABLE_UPTIME + {"uptime", uptime_main, "tell how long the system has been running"}, +#endif +#if CFBOX_ENABLE_KILL + {"kill", kill_main, "send a signal to a process"}, +#endif +#if CFBOX_ENABLE_PIDOF + {"pidof", pidof_main, "find the process ID of a running program"}, +#endif +#if CFBOX_ENABLE_PS + {"ps", ps_main, "report a snapshot of current processes"}, +#endif +#if CFBOX_ENABLE_PGREP + {"pgrep", pgrep_main, "look up processes based on name"}, + {"pkill", pgrep_main, "signal processes based on name"}, +#endif +#if CFBOX_ENABLE_SYSCTL + {"sysctl", sysctl_main, "configure kernel parameters at runtime"}, +#endif +#if CFBOX_ENABLE_PWDX + {"pwdx", pwdx_main, "print working directory of a process"}, +#endif +#if CFBOX_ENABLE_PSTREE + {"pstree", pstree_main, "display a tree of processes"}, +#endif +#if CFBOX_ENABLE_PMAP + {"pmap", pmap_main, "display memory map of a process"}, +#endif +#if CFBOX_ENABLE_FUSER + {"fuser", fuser_main, "identify processes using files or sockets"}, +#endif +#if CFBOX_ENABLE_IOSTAT + {"iostat", iostat_main, "report CPU and I/O statistics"}, +#endif +#if CFBOX_ENABLE_WATCH + {"watch", watch_main, "execute a program periodically"}, +#endif +#if CFBOX_ENABLE_TOP + {"top", top_main, "display Linux processes"}, +#endif +#if CFBOX_ENABLE_DMESG + {"dmesg", dmesg_main, "print kernel ring buffer"}, +#endif +#if CFBOX_ENABLE_HEXDUMP + {"hexdump", hexdump_main, "display file contents in hexadecimal"}, +#endif +#if CFBOX_ENABLE_MORE + {"more", more_main, "file perusal filter for crt viewing"}, +#endif +#if CFBOX_ENABLE_REV + {"rev", rev_main, "reverse lines characterwise"}, +#endif +#if CFBOX_ENABLE_CAL + {"cal", cal_main, "display a calendar"}, +#endif +#if CFBOX_ENABLE_RENICE + {"renice", renice_main, "alter priority of running processes"}, +#endif }); diff --git a/include/cfbox/proc.hpp b/include/cfbox/proc.hpp new file mode 100644 index 0000000..19ee4fe --- /dev/null +++ b/include/cfbox/proc.hpp @@ -0,0 +1,433 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cfbox::proc { + +// Cached system constants +inline auto clock_ticks_per_second() -> long { + static long ticks = sysconf(_SC_CLK_TCK); + return ticks; +} + +inline auto page_size() -> long { + static long ps = sysconf(_SC_PAGE_SIZE); + return ps; +} + +inline auto total_memory_kb() -> std::uint64_t { + static std::uint64_t mem = static_cast(sysconf(_SC_PHYS_PAGES)) + * static_cast(sysconf(_SC_PAGE_SIZE)) / 1024; + return mem; +} + +// --- /proc/meminfo --- +struct MemInfo { + std::uint64_t total = 0; + std::uint64_t free = 0; + std::uint64_t available = 0; + std::uint64_t buffers = 0; + std::uint64_t cached = 0; + std::uint64_t swap_total = 0; + std::uint64_t swap_free = 0; + std::uint64_t shmem = 0; + std::uint64_t s_reclaimable = 0; +}; + +inline auto read_meminfo() -> base::Result { + std::ifstream f("/proc/meminfo"); + if (!f) return std::unexpected(base::Error{1, "cannot open /proc/meminfo"}); + + MemInfo mi{}; + std::string line; + while (std::getline(f, line)) { + auto colon = line.find(':'); + if (colon == std::string::npos) continue; + auto key = line.substr(0, colon); + // Parse number after colon + auto p = line.c_str() + colon + 1; + while (*p == ' ') ++p; + std::uint64_t val = 0; + std::sscanf(p, "%llu", reinterpret_cast(&val)); + + if (key == "MemTotal") mi.total = val; + else if (key == "MemFree") mi.free = val; + else if (key == "MemAvailable") mi.available = val; + else if (key == "Buffers") mi.buffers = val; + else if (key == "Cached") mi.cached = val; + else if (key == "SwapTotal") mi.swap_total = val; + else if (key == "SwapFree") mi.swap_free = val; + else if (key == "Shmem") mi.shmem = val; + else if (key == "SReclaimable") mi.s_reclaimable = val; + } + + // Adjust cached to include SReclaimable (like procps does) + mi.cached += mi.s_reclaimable; + + return mi; +} + +// --- /proc/stat (CPU) --- +struct CpuStats { + std::uint64_t user = 0, nice = 0, system = 0, idle = 0; + std::uint64_t iowait = 0, irq = 0, softirq = 0, steal = 0; + auto total() const -> std::uint64_t { + return user + nice + system + idle + iowait + irq + softirq + steal; + } + auto idle_time() const -> std::uint64_t { + return idle + iowait; + } +}; + +inline auto read_cpu_stats() -> base::Result { + std::ifstream f("/proc/stat"); + if (!f) return std::unexpected(base::Error{1, "cannot open /proc/stat"}); + + std::string line; + if (!std::getline(f, line) || line.substr(0, 4) != "cpu ") + return std::unexpected(base::Error{1, "unexpected /proc/stat format"}); + + CpuStats cs{}; + // "cpu user nice system idle iowait irq softirq steal [guest guest_nice]" + std::istringstream iss(line.substr(5)); + iss >> cs.user >> cs.nice >> cs.system >> cs.idle + >> cs.iowait >> cs.irq >> cs.softirq >> cs.steal; + return cs; +} + +// --- /proc/[pid]/stat --- +struct ProcessInfo { + pid_t pid = 0; + std::string comm; + char state = '?'; + pid_t ppid = 0; + int priority = 0; + int nice_val = 0; + std::uint64_t vsize = 0; // bytes + std::uint64_t rss = 0; // pages + std::uint64_t utime = 0; // clock ticks + std::uint64_t stime = 0; // clock ticks + std::uint64_t start_time = 0; // clock ticks since boot + uid_t uid = static_cast(-1); + gid_t gid = static_cast(-1); + std::vector cmdline; + std::string tty; +}; + +namespace detail { + +inline auto read_file_str(std::string_view path) -> std::string { + auto p = std::string(path); + std::ifstream f{p}; + if (!f) return {}; + std::ostringstream ss; + ss << f.rdbuf(); + return ss.str(); +} + +inline auto parse_uid_gid(std::string_view path, uid_t& uid, gid_t& gid) -> void { + auto p = std::string(path); + std::ifstream f{p}; + if (!f) return; + std::string line; + while (std::getline(f, line)) { + if (line.starts_with("Uid:")) { + std::sscanf(line.c_str() + 4, " %u", &uid); + } else if (line.starts_with("Gid:")) { + std::sscanf(line.c_str() + 4, " %u", &gid); + } + } +} + +inline auto parse_cmdline(std::string_view path) -> std::vector { + auto p = std::string(path); + std::ifstream f{p, std::ios::binary}; + if (!f) return {}; + std::string data((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); + if (data.empty()) return {}; + + std::vector args; + std::string::size_type start = 0; + for (std::string::size_type i = 0; i <= data.size(); ++i) { + if (i == data.size() || data[i] == '\0') { + if (i > start) args.emplace_back(data.substr(start, i - start)); + start = i + 1; + } + } + return args; +} + +inline auto tty_from_dev(std::uint64_t tty_nr) -> std::string { + int major = static_cast(tty_nr >> 8); + int minor = static_cast(tty_nr & 0xFF); + if (major == 4) { + if (minor < 64) return "tty" + std::to_string(minor); + return "pts/" + std::to_string(minor - 64 > 255 ? minor : minor - 64); + } + if (major == 136) return "pts/" + std::to_string(minor); + if (major == 5 && minor == 0) return "tty"; + return "?"; +} + +} // namespace detail + +inline auto read_process(pid_t pid) -> base::Result { + ProcessInfo pi; + pi.pid = pid; + + // Parse /proc/[pid]/stat + auto stat_path = "/proc/" + std::to_string(pid) + "/stat"; + auto content = detail::read_file_str(stat_path); + if (content.empty()) + return std::unexpected(base::Error{1, "cannot read " + stat_path}); + + // comm is in parentheses and may contain spaces — find last ')' + auto open_paren = content.find('('); + auto close_paren = content.rfind(')'); + if (open_paren == std::string::npos || close_paren == std::string::npos || close_paren <= open_paren) + return std::unexpected(base::Error{1, "unexpected /proc/[pid]/stat format"}); + + pi.comm = content.substr(open_paren + 1, close_paren - open_paren - 1); + + // Fields after close paren: state ppid pgrp session tty_nr tpgid ... + // Field 3 (state) is right after ') ' + const char* p = content.c_str() + close_paren + 2; + + // field 3: state (char) + pi.state = *p; + ++p; // skip state char + if (*p == ' ') ++p; + + // field 4: ppid + pi.ppid = static_cast(std::strtoull(p, nullptr, 10)); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // field 5: pgrp (skip) + std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // field 6: session (skip) + std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // field 7: tty_nr + std::uint64_t tty_nr = std::strtoull(p, nullptr, 10); + pi.tty = detail::tty_from_dev(tty_nr); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // field 8: tpgid (skip) + std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // fields 9-13: flags, minflt, cminflt, majflt, cmajflt (skip 5 fields) + for (int i = 0; i < 5; ++i) { + std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + } + + // field 14: utime + pi.utime = std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // field 15: stime + pi.stime = std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // field 16-17: cutime, cstime (skip) + for (int i = 0; i < 2; ++i) { + std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + } + + // field 18: priority + pi.priority = static_cast(std::strtol(p, nullptr, 10)); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // field 19: nice + pi.nice_val = static_cast(std::strtol(p, nullptr, 10)); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // fields 20-21: num_threads, itrealvalue (skip) + for (int i = 0; i < 2; ++i) { + std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + } + + // field 22: starttime + pi.start_time = std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // field 23: vsize (bytes) + pi.vsize = std::strtoull(p, nullptr, 10); + while (*p && *p != ' ') ++p; + if (*p == ' ') ++p; + + // field 24: rss (pages) + pi.rss = std::strtoull(p, nullptr, 10); + + // uid/gid from /proc/[pid]/status + auto status_path = "/proc/" + std::to_string(pid) + "/status"; + detail::parse_uid_gid(status_path, pi.uid, pi.gid); + + // cmdline from /proc/[pid]/cmdline + auto cmdline_path = "/proc/" + std::to_string(pid) + "/cmdline"; + pi.cmdline = detail::parse_cmdline(cmdline_path); + + return pi; +} + +inline auto read_all_processes() -> base::Result> { + std::vector procs; + std::error_code ec; + for (const auto& entry : std::filesystem::directory_iterator("/proc", ec)) { + if (!entry.is_directory()) continue; + auto name = entry.path().filename().string(); + // Check if directory name is numeric (PID) + if (name.empty() || name[0] < '0' || name[0] > '9') continue; + pid_t pid = static_cast(std::stoi(name)); + auto result = read_process(pid); + if (result) procs.push_back(std::move(*result)); + } + return procs; +} + +// --- /proc/loadavg --- +struct LoadAvg { + double avg1 = 0, avg5 = 0, avg15 = 0; + int running = 0, total = 0; + pid_t last_pid = 0; +}; + +inline auto read_loadavg() -> base::Result { + auto content = detail::read_file_str("/proc/loadavg"); + if (content.empty()) + return std::unexpected(base::Error{1, "cannot read /proc/loadavg"}); + + LoadAvg la{}; + std::sscanf(content.c_str(), "%lf %lf %lf %d/%d %d", + &la.avg1, &la.avg5, &la.avg15, &la.running, &la.total, reinterpret_cast(&la.last_pid)); + return la; +} + +// --- /proc/uptime --- +inline auto read_uptime() -> base::Result> { + auto content = detail::read_file_str("/proc/uptime"); + if (content.empty()) + return std::unexpected(base::Error{1, "cannot read /proc/uptime"}); + + double up = 0, idle = 0; + std::sscanf(content.c_str(), "%lf %lf", &up, &idle); + return std::make_pair(up, idle); +} + +// --- /proc/version --- +inline auto read_version() -> base::Result { + auto content = detail::read_file_str("/proc/version"); + if (content.empty()) + return std::unexpected(base::Error{1, "cannot read /proc/version"}); + // Trim trailing newline + while (!content.empty() && content.back() == '\n') content.pop_back(); + return content; +} + +// --- /proc/mounts --- +struct MountEntry { + std::string device; + std::string mountpoint; + std::string fstype; + std::string options; +}; + +inline auto read_mounts() -> base::Result> { + std::ifstream f("/proc/mounts"); + if (!f) return std::unexpected(base::Error{1, "cannot open /proc/mounts"}); + + std::vector mounts; + std::string line; + while (std::getline(f, line)) { + if (line.empty()) continue; + // device mountpoint fstype options freq passno + MountEntry me; + std::istringstream iss(line); + iss >> me.device >> me.mountpoint >> me.fstype >> me.options; + if (!me.device.empty()) mounts.push_back(std::move(me)); + } + return mounts; +} + +// --- /proc/diskstats --- +struct DiskStat { + std::string device; + std::uint64_t reads = 0, reads_merged = 0, sectors_read = 0, ms_reading = 0; + std::uint64_t writes = 0, writes_merged = 0, sectors_written = 0, ms_writing = 0; + std::uint64_t ios_in_progress = 0, ms_ios = 0, weighted_ms_ios = 0; +}; + +inline auto read_diskstats() -> base::Result> { + std::ifstream f("/proc/diskstats"); + if (!f) return std::unexpected(base::Error{1, "cannot open /proc/diskstats"}); + + std::vector stats; + std::string line; + while (std::getline(f, line)) { + if (line.empty()) continue; + DiskStat ds; + std::istringstream iss(line); + std::string maj_str, min_str; + iss >> maj_str >> min_str >> ds.device + >> ds.reads >> ds.reads_merged >> ds.sectors_read >> ds.ms_reading + >> ds.writes >> ds.writes_merged >> ds.sectors_written >> ds.ms_writing + >> ds.ios_in_progress >> ds.ms_ios >> ds.weighted_ms_ios; + + if (!ds.device.empty()) stats.push_back(std::move(ds)); + } + return stats; +} + +// --- /proc/partitions --- +inline auto read_partitions() -> base::Result> { + std::ifstream f("/proc/partitions"); + if (!f) return std::unexpected(base::Error{1, "cannot open /proc/partitions"}); + + std::vector parts; + std::string line; + // Skip header line + std::getline(f, line); + while (std::getline(f, line)) { + if (line.empty()) continue; + std::istringstream iss(line); + int major, minor; + std::uint64_t blocks; + std::string name; + iss >> major >> minor >> blocks >> name; + if (!name.empty()) parts.push_back(std::move(name)); + } + return parts; +} + +} // namespace cfbox::proc diff --git a/include/cfbox/tui.hpp b/include/cfbox/tui.hpp new file mode 100644 index 0000000..8a796a1 --- /dev/null +++ b/include/cfbox/tui.hpp @@ -0,0 +1,284 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace cfbox::tui { + +// --- Key representation --- + +enum class KeyType : unsigned short { + Char, + Escape, + Enter, + Tab, + Backspace, + Delete, + Up, Down, Left, Right, + Home, End, + PageUp, PageDown, + F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, + Ctrl_A, Ctrl_B, Ctrl_C, Ctrl_D, Ctrl_E, Ctrl_F, + Ctrl_G, Ctrl_H, Ctrl_K, Ctrl_L, Ctrl_N, Ctrl_O, + Ctrl_P, Ctrl_Q, Ctrl_R, Ctrl_S, Ctrl_T, Ctrl_U, + Ctrl_V, Ctrl_W, Ctrl_X, Ctrl_Y, Ctrl_Z, + Unknown, +}; + +struct Key { + KeyType type = KeyType::Unknown; + char32_t ch = 0; + + auto is_char() const -> bool { return type == KeyType::Char; } + auto is_quit() const -> bool { + return type == KeyType::Escape || type == KeyType::Ctrl_C || type == KeyType::Ctrl_Q; + } + auto ctrl_char() const -> char { + if (type >= KeyType::Ctrl_A && type <= KeyType::Ctrl_Z) { + auto idx = static_cast(type) - static_cast(KeyType::Ctrl_A); + return static_cast('a' + idx); + } + return '\0'; + } +}; + +inline auto read_key(int fd = 0, int timeout_ms = -1) -> std::optional { + struct pollfd pfd{fd, POLLIN, 0}; + int pret = poll(&pfd, 1, timeout_ms); + if (pret <= 0) return std::nullopt; + + unsigned char buf[16]; + auto n = ::read(fd, buf, 1); + if (n <= 0) return std::nullopt; + + auto c = static_cast(buf[0]); + + // Ctrl+letter: 1-26 + if (c >= 1 && c <= 26) { + auto idx = static_cast(c) - 1; + Key k; + k.type = static_cast(static_cast(KeyType::Ctrl_A) + idx); + return k; + } + + if (c == 13 || c == 10) return Key{KeyType::Enter, 0}; + if (c == 9) return Key{KeyType::Tab, 0}; + if (c == 127 || c == 8) return Key{KeyType::Backspace, 0}; + + if (c == 27) { + auto seq0 = read_key(fd, 10); + if (!seq0) return Key{KeyType::Escape, 0}; + + auto c1 = static_cast(seq0->ch); + + if (c1 != '[' && c1 != 'O') { + return Key{KeyType::Escape, 0}; + } + + auto seq1 = read_key(fd, 10); + if (!seq1) return Key{KeyType::Escape, 0}; + + auto c2 = static_cast(seq1->ch); + + if (c1 == '[') { + switch (c2) { + case 'A': return Key{KeyType::Up, 0}; + case 'B': return Key{KeyType::Down, 0}; + case 'C': return Key{KeyType::Right, 0}; + case 'D': return Key{KeyType::Left, 0}; + case 'H': return Key{KeyType::Home, 0}; + case 'F': return Key{KeyType::End, 0}; + case '5': { + auto next = read_key(fd, 10); + if (next && next->ch == '~') return Key{KeyType::PageUp, 0}; + return Key{KeyType::Unknown, 0}; + } + case '6': { + auto next = read_key(fd, 10); + if (next && next->ch == '~') return Key{KeyType::PageDown, 0}; + return Key{KeyType::Unknown, 0}; + } + default: return Key{KeyType::Unknown, 0}; + } + } + if (c1 == 'O') { + switch (c2) { + case 'P': return Key{KeyType::F1, 0}; + case 'Q': return Key{KeyType::F2, 0}; + case 'R': return Key{KeyType::F3, 0}; + case 'S': return Key{KeyType::F4, 0}; + default: return Key{KeyType::Unknown, 0}; + } + } + return Key{KeyType::Unknown, 0}; + } + + if (c >= 32 && c < 127) return Key{KeyType::Char, static_cast(c)}; + + return Key{KeyType::Unknown, 0}; +} + +// --- Screen buffer --- + +struct Cell { + char ch = ' '; + bool bold = false; + bool reverse = false; +}; + +class ScreenBuffer { + int rows_ = 0; + int cols_ = 0; + std::vector cells_; + std::vector prev_; + bool first_frame_ = true; + + auto idx(int r, int c) const -> int { return r * cols_ + c; } +public: + ScreenBuffer() = default; + ScreenBuffer(int rows, int cols) : rows_(rows), cols_(cols), + cells_(static_cast(rows * cols)), + prev_(static_cast(rows * cols)) {} + + auto resize(int rows, int cols) -> void { + rows_ = rows; + cols_ = cols; + cells_.assign(static_cast(rows * cols), Cell{}); + prev_.assign(static_cast(rows * cols), Cell{}); + first_frame_ = true; + } + + auto rows() const -> int { return rows_; } + auto cols() const -> int { return cols_; } + + auto set(int row, int col, char ch, bool bold = false, bool reverse = false) -> void { + if (row < 0 || row >= rows_ || col < 0 || col >= cols_) return; + auto& c = cells_[idx(row, col)]; + c.ch = ch; + c.bold = bold; + c.reverse = reverse; + } + + auto set_string(int row, int col, std::string_view s, bool bold = false, bool reverse = false) -> void { + for (int i = 0; i < static_cast(s.size()); ++i) { + set(row, col + i, s[static_cast(i)], bold, reverse); + } + } + + auto clear() -> void { + cells_.assign(cells_.size(), Cell{}); + } + + auto render() -> void { + if (first_frame_) { + terminal::clear_screen(); + for (int r = 0; r < rows_; ++r) { + terminal::move_cursor(r + 1, 1); + for (int c = 0; c < cols_; ++c) { + auto& cell = cells_[idx(r, c)]; + if (cell.bold) terminal::bold(true); + if (cell.reverse) terminal::invert_video(true); + std::putchar(cell.ch); + if (cell.bold || cell.reverse) terminal::reset_attr(); + } + } + prev_ = cells_; + first_frame_ = false; + std::fflush(stdout); + return; + } + + for (int r = 0; r < rows_; ++r) { + bool row_dirty = false; + for (int c = 0; c < cols_; ++c) { + auto& cur = cells_[idx(r, c)]; + auto& prv = prev_[idx(r, c)]; + bool changed = cur.ch != prv.ch || cur.bold != prv.bold || cur.reverse != prv.reverse; + if (changed || row_dirty) { + if (changed && !row_dirty) { + terminal::move_cursor(r + 1, c + 1); + row_dirty = true; + } + if (cur.bold) terminal::bold(true); + if (cur.reverse) terminal::invert_video(true); + std::putchar(cur.ch); + if (cur.bold || cur.reverse) terminal::reset_attr(); + } + } + } + prev_ = cells_; + std::fflush(stdout); + } +}; + +// --- Global resize flag --- + +inline auto global_resized_flag() -> std::atomic& { + static std::atomic flag{false}; + return flag; +} + +inline auto request_resize() -> void { + global_resized_flag().store(true); +} + +// --- TUI Application base --- + +class TuiApp { + ScreenBuffer screen_; + int tick_ms_ = 1000; + +public: + explicit TuiApp(int tick_ms = 1000) : tick_ms_(tick_ms) {} + virtual ~TuiApp() = default; + + virtual auto on_key(Key k) -> bool = 0; + virtual auto on_tick() -> void = 0; + virtual auto on_resize(int rows, int cols) -> void = 0; + + auto screen() -> ScreenBuffer& { return screen_; } + + auto run() -> int { + auto [rows, cols] = terminal::get_size(); + screen_.resize(rows, cols); + + terminal::RawMode raw_mode; + terminal::enter_alt_screen(); + terminal::hide_cursor(); + + on_resize(rows, cols); + on_tick(); + screen_.render(); + + while (true) { + auto key = read_key(0, tick_ms_); + if (key) { + if (!on_key(*key)) break; + screen_.render(); + } else { + on_tick(); + if (global_resized_flag().exchange(false)) { + auto [nr, nc] = terminal::get_size(); + screen_.resize(nr, nc); + on_resize(nr, nc); + } + screen_.render(); + } + } + + terminal::show_cursor(); + terminal::leave_alt_screen(); + return 0; + } +}; + +} // namespace cfbox::tui diff --git a/scripts/build_initramfs_inittab.sh b/scripts/build_initramfs_inittab.sh new file mode 100644 index 0000000..92bb1e1 --- /dev/null +++ b/scripts/build_initramfs_inittab.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# Build initramfs with /etc/inittab for full init system testing. +# Usage: scripts/build_initramfs_inittab.sh --arch aarch64 --cfbox [--inittab ] +set -euo pipefail + +script_dir="$(cd "$(dirname "$0")" && pwd)" +project_dir="$(cd "$script_dir/.." && pwd)" + +arch="" +cfbox_path="" +inittab_path="$project_dir/configs/qemu-inittab" +output_path="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --arch) arch="$2"; shift 2 ;; + --cfbox) cfbox_path="$2"; shift 2 ;; + --inittab) inittab_path="$2"; shift 2 ;; + --output) output_path="$2"; shift 2 ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +[[ -z "$arch" ]] && { echo "ERROR: --arch required"; exit 1; } +[[ -z "$cfbox_path" ]] && { echo "ERROR: --cfbox required"; exit 1; } + +if [[ -z "$output_path" ]]; then + output_path="$project_dir/build/${arch}-initramfs-inittab.cpio" +fi +mkdir -p "$(dirname "$output_path")" + +echo "=== Building initramfs with inittab ($arch) ===" + +initramfs="$(mktemp -d)" +trap 'rm -rf "$initramfs"' EXIT + +# Directory structure +mkdir -p "$initramfs"/{bin,dev,proc,sys,tmp,etc,root} + +# CFBox binary + symlinks +cp "$cfbox_path" "$initramfs/bin/cfbox" +chmod +x "$initramfs/bin/cfbox" + +# Create applet symlinks +case "$arch" in + aarch64) qemu_bin="qemu-aarch64-static" ;; + armhf) qemu_bin="qemu-arm-static" ;; + *) qemu_bin="qemu-$arch-static" ;; +esac + +if "$initramfs/bin/cfbox" --list >/dev/null 2>&1; then + list_cmd="$initramfs/bin/cfbox" +elif command -v "$qemu_bin" &>/dev/null; then + list_cmd="$qemu_bin $initramfs/bin/cfbox" +else + echo "ERROR: $qemu_bin needed"; exit 1 +fi + +while IFS= read -r line; do + name=$(echo "$line" | awk '{print $1}') + [[ -z "$name" ]] && continue + ln -sf cfbox "$initramfs/bin/$name" +done < <($list_cmd --list) + +# /etc/inittab +if [[ -f "$inittab_path" ]]; then + cp "$inittab_path" "$initramfs/etc/inittab" + echo " /etc/inittab installed" +else + echo "WARNING: inittab not found: $inittab_path" +fi + +# /init symlink +ln -sf bin/cfbox "$initramfs/init" + +# Build cpio +(cd "$initramfs" && find . | cpio -o -H newc --quiet > "$output_path") + +echo "" +echo "Initramfs: $output_path ($(du -h "$output_path" | cut -f1))" +echo "" +echo "Boot with:" +echo " qemu-system-aarch64 -machine virt -cpu cortex-a57 -m 256M -nographic \\" +echo " -kernel -initrd $output_path \\" +echo " -append 'console=ttyAMA0'" diff --git a/src/applets/ar.cpp b/src/applets/ar.cpp index 1ea76b5..3382440 100644 --- a/src/applets/ar.cpp +++ b/src/applets/ar.cpp @@ -94,7 +94,10 @@ auto ar_main(int argc, char* argv[]) -> int { std::puts(name.c_str()); } else if (extract) { auto content = data.substr(offset + 60, fsize); - cfbox::io::write_all(name, content); + if (!cfbox::io::write_all(name, content)) { + std::fprintf(stderr, "cfbox ar: write failed: %s\n", name.c_str()); + return 1; + } } offset += 60 + fsize; if (fsize % 2) ++offset; diff --git a/src/applets/cal.cpp b/src/applets/cal.cpp new file mode 100644 index 0000000..0a60a87 --- /dev/null +++ b/src/applets/cal.cpp @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "cal", + .version = CFBOX_VERSION_STRING, + .one_line = "display a calendar", + .usage = "cal [[MONTH] YEAR]", + .options = " -3 show prev/curr/next month\n" + " -y show whole year", + .extra = "", +}; + +auto days_in_month(int year, int month) -> int { + // month is 1-12 + static const int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month == 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) + return 29; + return days[month]; +} + +auto day_of_week(int year, int month, int day) -> int { + // 0=Sunday, 1=Monday, ..., 6=Saturday (Zeller's congruence simplified) + struct tm tm{}; + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + std::mktime(&tm); + return tm.tm_wday; // 0=Sunday +} + +auto month_name(int month) -> const char* { + static const char* names[] = { + "", "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + }; + return names[month]; +} + +auto print_month(int year, int month) -> void { + char header[32]; + std::snprintf(header, sizeof(header), "%s %d", month_name(month), year); + auto header_len = static_cast(std::strlen(header)); + + // Center the header over "Su Mo Tu We Th Fr Sa" (20 chars) + auto pad = (20 - header_len) / 2; + if (pad < 0) pad = 0; + std::printf("%*s%s\n", pad, "", header); + std::printf("Su Mo Tu We Th Fr Sa\n"); + + auto first_day = day_of_week(year, month, 1); + auto days = days_in_month(year, month); + + // Leading spaces + for (int i = 0; i < first_day; ++i) { + std::printf(" "); + } + + for (int d = 1; d <= days; ++d) { + std::printf("%2d ", d); + if ((d + first_day) % 7 == 0 || d == days) { + std::printf("\n"); + } + } +} + +} // anonymous namespace + +auto cal_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'3', false, "three"}, + cfbox::args::OptSpec{'y', false, "year"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + auto now = std::time(nullptr); + auto tm = std::localtime(&now); + int year = tm->tm_year + 1900; + int month = tm->tm_mon + 1; + + const auto& pos = parsed.positional(); + if (pos.size() == 1) { + auto val = std::stoi(std::string(pos[0])); + if (val >= 1 && val <= 12) { + month = val; + } else { + year = val; + month = 1; + } + } else if (pos.size() >= 2) { + month = std::stoi(std::string(pos[0])); + year = std::stoi(std::string(pos[1])); + } + + bool three = parsed.has('3') || parsed.has_long("three"); + bool whole_year = parsed.has('y') || parsed.has_long("year"); + + if (whole_year) { + for (int m = 1; m <= 12; ++m) { + print_month(year, m); + std::printf("\n"); + } + } else if (three) { + // Previous month + int pm = month - 1, py = year; + if (pm < 1) { pm = 12; --py; } + int nm = month + 1, ny = year; + if (nm > 12) { nm = 1; ++ny; } + + print_month(py, pm); + std::printf("\n"); + print_month(year, month); + std::printf("\n"); + print_month(ny, nm); + } else { + print_month(year, month); + } + + return 0; +} diff --git a/src/applets/dmesg.cpp b/src/applets/dmesg.cpp new file mode 100644 index 0000000..919888d --- /dev/null +++ b/src/applets/dmesg.cpp @@ -0,0 +1,79 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "dmesg", + .version = CFBOX_VERSION_STRING, + .one_line = "print kernel ring buffer", + .usage = "dmesg [-T]", + .options = " -T show timestamps in human-readable format", + .extra = "", +}; + +auto read_kmsg() -> std::vector { + std::vector lines; + // Try /var/log/dmesg first (works without CAP_SYSLOG) + FILE* f = std::fopen("/var/log/dmesg", "r"); + if (!f) f = std::fopen("/var/log/kern.log", "r"); + if (!f) { + std::fprintf(stderr, "cfbox dmesg: cannot open kernel log\n"); + return lines; + } + + char buf[4096]; + while (std::fgets(buf, sizeof(buf), f)) { + auto len = std::strlen(buf); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { + buf[--len] = '\0'; + } + lines.emplace_back(buf); + } + std::fclose(f); + return lines; +} + +} // anonymous namespace + +auto dmesg_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'T', false, "time"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool timestamps = parsed.has('T') || parsed.has_long("time"); + auto lines = read_kmsg(); + + for (const auto& line : lines) { + if (timestamps) { + // Try to extract timestamp from [ 1234.567] format + auto bracket = line.find('['); + auto close_bracket = line.find(']'); + if (bracket != std::string::npos && close_bracket != std::string::npos && + close_bracket > bracket) { + auto ts_str = line.substr(bracket + 1, close_bracket - bracket - 1); + auto ts = std::stod(ts_str); + auto secs = static_cast(ts); + auto tm = std::localtime(&secs); + char time_buf[32]; + std::strftime(time_buf, sizeof(time_buf), "%b %d %H:%M:%S", tm); + std::printf("%s %s\n", time_buf, line.c_str()); + } else { + std::printf("%s\n", line.c_str()); + } + } else { + std::printf("%s\n", line.c_str()); + } + } + + return 0; +} diff --git a/src/applets/free.cpp b/src/applets/free.cpp new file mode 100644 index 0000000..429d271 --- /dev/null +++ b/src/applets/free.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "free", + .version = CFBOX_VERSION_STRING, + .one_line = "display amount of free and used memory", + .usage = "free [-b|-k|-m|-g|-h]", + .options = " -b show bytes\n" + " -k show kilobytes (default)\n" + " -m show megabytes\n" + " -g show gigabytes\n" + " -h human-readable", + .extra = "", +}; + +auto human_size(std::uint64_t kb) -> std::string { + if (kb >= 1024ULL * 1024 * 1024) { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.1fGi", static_cast(kb) / (1024.0 * 1024.0 * 1024.0)); + return buf; + } + if (kb >= 1024ULL * 1024) { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.1fMi", static_cast(kb) / (1024.0 * 1024.0)); + return buf; + } + if (kb >= 1024) { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.1fMi", static_cast(kb) / 1024.0); + return buf; + } + return std::to_string(kb) + "Ki"; +} + +auto format_val(std::uint64_t kb, bool human, char unit) -> std::string { + if (human) return human_size(kb); + switch (unit) { + case 'b': return std::to_string(kb * 1024); + case 'm': { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.0f", static_cast(kb) / 1024.0); + return buf; + } + case 'g': { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.1f", static_cast(kb) / (1024.0 * 1024.0)); + return buf; + } + default: return std::to_string(kb); + } +} + +} // anonymous namespace + +auto free_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'b', false}, + cfbox::args::OptSpec{'k', false}, + cfbox::args::OptSpec{'m', false}, + cfbox::args::OptSpec{'g', false}, + cfbox::args::OptSpec{'h', false, "human"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + auto result = cfbox::proc::read_meminfo(); + if (!result) { + std::fprintf(stderr, "cfbox free: %s\n", result.error().msg.c_str()); + return 1; + } + const auto& mi = *result; + + bool human = parsed.has('h') || parsed.has_long("human"); + char unit = 'k'; + if (parsed.has('b')) unit = 'b'; + else if (parsed.has('m')) unit = 'm'; + else if (parsed.has('g')) unit = 'g'; + + auto used = mi.total - mi.free - mi.buffers - mi.cached; + auto buff_cache = mi.buffers + mi.cached; + + std::printf("%-16s %12s %12s %12s %12s %12s\n", + "total", "used", "free", "shared", "buff/cache", "available"); + + auto fmt = [&](std::uint64_t v) { return format_val(v, human, unit); }; + + std::printf("Mem: %12s %12s %12s %12s %12s %12s\n", + fmt(mi.total).c_str(), fmt(used).c_str(), fmt(mi.free).c_str(), + fmt(mi.shmem).c_str(), fmt(buff_cache).c_str(), fmt(mi.available).c_str()); + + if (mi.swap_total > 0) { + auto swap_used = mi.swap_total - mi.swap_free; + std::printf("Swap: %12s %12s %12s\n", + fmt(mi.swap_total).c_str(), fmt(swap_used).c_str(), fmt(mi.swap_free).c_str()); + } + + return 0; +} diff --git a/src/applets/fuser.cpp b/src/applets/fuser.cpp new file mode 100644 index 0000000..bbdd7bc --- /dev/null +++ b/src/applets/fuser.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "fuser", + .version = CFBOX_VERSION_STRING, + .one_line = "identify processes using files or sockets", + .usage = "fuser [-k] FILE...", + .options = " -k kill processes accessing the file\n" + " -v verbose output", + .extra = "", +}; + +} // anonymous namespace + +auto fuser_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'k', false, "kill"}, + cfbox::args::OptSpec{'v', false, "verbose"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool do_kill = parsed.has('k') || parsed.has_long("kill"); + bool verbose = parsed.has('v') || parsed.has_long("verbose"); + const auto& targets = parsed.positional(); + if (targets.empty()) { + std::fprintf(stderr, "cfbox fuser: no file specified\n"); + return 1; + } + + int rc = 1; + + for (const auto& target : targets) { + auto target_str = std::string(target); + struct stat target_stat {}; + if (stat(target_str.c_str(), &target_stat) != 0) { + std::fprintf(stderr, "cfbox fuser: cannot stat %s\n", target_str.c_str()); + continue; + } + + dev_t target_dev = target_stat.st_dev; + ino_t target_ino = target_stat.st_ino; + std::vector found_pids; + + std::error_code ec; + for (const auto& proc_entry : std::filesystem::directory_iterator("/proc", ec)) { + if (!proc_entry.is_directory()) continue; + auto name = proc_entry.path().filename().string(); + if (name.empty() || name[0] < '0' || name[0] > '9') continue; + + pid_t pid = static_cast(std::stoi(name)); + auto fd_dir = proc_entry.path() / "fd"; + + for (const auto& fd_entry : std::filesystem::directory_iterator(fd_dir, ec)) { + auto link = std::filesystem::read_symlink(fd_entry.path(), ec); + if (ec) continue; + + struct stat fd_stat {}; + if (stat(fd_entry.path().c_str(), &fd_stat) != 0) continue; + if (fd_stat.st_dev == target_dev && fd_stat.st_ino == target_ino) { + found_pids.push_back(pid); + break; + } + } + } + + if (found_pids.empty()) continue; + rc = 0; + + if (verbose) { + std::printf("%s:", target_str.c_str()); + for (auto p : found_pids) std::printf(" %d", p); + std::printf("\n"); + } + + if (do_kill) { + for (auto p : found_pids) { + if (::kill(p, SIGKILL) != 0) { + std::fprintf(stderr, "cfbox fuser: cannot kill %d\n", p); + } + } + } else if (!verbose) { + for (auto p : found_pids) std::printf("%d ", p); + std::printf("\n"); + } + } + + return rc; +} diff --git a/src/applets/hexdump.cpp b/src/applets/hexdump.cpp new file mode 100644 index 0000000..b3d2946 --- /dev/null +++ b/src/applets/hexdump.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "hexdump", + .version = CFBOX_VERSION_STRING, + .one_line = "display file contents in hexadecimal", + .usage = "hexdump [-C] [-n LENGTH] [-s OFFSET] [FILE]", + .options = " -C canonical hex+ASCII display\n" + " -n N interpret only N bytes\n" + " -s N skip first N bytes", + .extra = "", +}; + +auto hexdump_canonical(const unsigned char* data, std::size_t len, + std::uint64_t base_offset) -> void { + for (std::size_t i = 0; i < len; i += 16) { + auto chunk = static_cast(len - i > 16 ? 16 : len - i); + + // Offset + std::printf("%08llx ", static_cast(base_offset + i)); + + // Hex bytes (two groups of 8) + for (int j = 0; j < 16; ++j) { + if (j == 8) std::printf(" "); + if (j < chunk) { + std::printf("%02x ", data[i + static_cast(j)]); + } else { + std::printf(" "); + } + } + + // ASCII + std::printf(" |"); + for (int j = 0; j < chunk; ++j) { + auto c = data[i + static_cast(j)]; + std::putchar(static_cast(c >= 32 && c < 127 ? c : '.')); + } + std::printf("|\n"); + } +} + +} // anonymous namespace + +auto hexdump_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'C', false, "canonical"}, + cfbox::args::OptSpec{'n', true, "length"}, + cfbox::args::OptSpec{'s', true, "skip"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool canonical = parsed.has('C') || parsed.has_long("canonical"); + std::size_t max_bytes = 0; + std::uint64_t skip_bytes = 0; + if (auto v = parsed.get('n')) max_bytes = static_cast(std::stoull(std::string(*v))); + if (auto v = parsed.get('s')) skip_bytes = std::stoull(std::string(*v)); + + const auto& pos = parsed.positional(); + std::string filename = pos.empty() ? "" : std::string(pos[0]); + + auto do_dump = [&](std::FILE* f) -> int { + if (skip_bytes > 0) { + std::fseek(f, static_cast(skip_bytes), SEEK_SET); + } + + unsigned char buf[16]; + std::uint64_t offset = skip_bytes; + std::size_t total = 0; + + while (true) { + auto to_read = static_cast(16); + if (max_bytes > 0 && total + to_read > max_bytes) { + to_read = max_bytes - total; + } + if (to_read == 0) break; + + auto n = std::fread(buf, 1, to_read, f); + if (n == 0) break; + + if (canonical) { + hexdump_canonical(buf, n, offset); + } else { + // Default format: similar but without ASCII + for (std::size_t i = 0; i < n; i += 16) { + auto chunk = static_cast(n - i > 16 ? 16 : n - i); + std::printf("%08llx ", static_cast(offset + i)); + for (int j = 0; j < chunk; ++j) { + std::printf("%02x ", buf[i + static_cast(j)]); + } + std::printf("\n"); + } + } + + offset += n; + total += n; + } + return 0; + }; + + if (filename.empty() || filename == "-") { + return do_dump(stdin); + } + + std::FILE* f = std::fopen(filename.c_str(), "rb"); + if (!f) { + std::fprintf(stderr, "cfbox hexdump: cannot open %s\n", filename.c_str()); + return 1; + } + auto rc = do_dump(f); + std::fclose(f); + return rc; +} diff --git a/src/applets/init.cpp b/src/applets/init.cpp deleted file mode 100644 index c11d78a..0000000 --- a/src/applets/init.cpp +++ /dev/null @@ -1,155 +0,0 @@ -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include - -namespace { - -constexpr cfbox::help::HelpEntry HELP = { - .name = "init", - .version = CFBOX_VERSION_STRING, - .one_line = "system init for boot testing (PID 1)", - .usage = "init", - .options = "", - .extra = "Designed for QEMU-based boot testing. Mounts /proc, /sys, /dev\n" - "and runs applet smoke tests before powering off.", -}; - -auto printf_flush [[gnu::format(printf, 1, 2)]] (const char* fmt, ...) -> int { - va_list ap; - va_start(ap, fmt); - int n = std::vfprintf(stdout, fmt, ap); - va_end(ap); - std::fflush(stdout); - return n; -} - -struct TestResult { - int pass; - int fail; -}; - -auto run_smoke_tests(TestResult& result) -> void { - int tested = 0; - -#if CFBOX_ENABLE_ECHO - // Build argv on the stack (no heap allocation) - char echo_name[] = "echo"; - char echo_arg[] = "hello"; - char* echo_argv[] = { echo_name, echo_arg, nullptr }; - - if (echo_main(2, echo_argv) == 0) { - printf_flush(" PASS: echo\n"); - ++result.pass; - } else { - printf_flush(" FAIL: echo\n"); - ++result.fail; - } - ++tested; -#endif - -#if CFBOX_ENABLE_CAT - char cat_name[] = "cat"; - char cat_arg[] = "/proc/version"; - char* cat_argv[] = { cat_name, cat_arg, nullptr }; - - if (cat_main(2, cat_argv) == 0) { - printf_flush(" PASS: cat\n"); - ++result.pass; - } else { - printf_flush(" FAIL: cat\n"); - ++result.fail; - } - ++tested; -#endif - -#if CFBOX_ENABLE_LS - char ls_name[] = "ls"; - char ls_arg[] = "/"; - char* ls_argv[] = { ls_name, ls_arg, nullptr }; - - if (ls_main(2, ls_argv) == 0) { - printf_flush(" PASS: ls\n"); - ++result.pass; - } else { - printf_flush(" FAIL: ls\n"); - ++result.fail; - } - ++tested; -#endif - -#if CFBOX_ENABLE_WC - char wc_name[] = "wc"; - char wc_arg1[] = "-l"; - char wc_arg2[] = "/proc/cpuinfo"; - char* wc_argv[] = { wc_name, wc_arg1, wc_arg2, nullptr }; - - if (wc_main(3, wc_argv) == 0) { - printf_flush(" PASS: wc\n"); - ++result.pass; - } else { - printf_flush(" FAIL: wc\n"); - ++result.fail; - } - ++tested; -#endif - - // Mark remaining applets as "tested via Level 1" - const int skipped = 34 - tested - 1; // 34 total - tested - init itself - if (skipped > 0) { - printf_flush(" (remaining %d applets verified by Level 1 QEMU user-mode)\n", skipped); - result.pass += skipped; - } -} - -} // anonymous namespace - -auto init_main(int argc, char* argv[]) -> int { - // Handle --help/--version (no args::parse for init) - for (int i = 1; i < argc; ++i) { - std::string_view arg{argv[i]}; - if (arg == "--help") { cfbox::help::print_help(HELP); return 0; } - if (arg == "--version") { cfbox::help::print_version(HELP); return 0; } - } - - bool is_pid1 = (getpid() == 1); - - if (is_pid1) { - mount("proc", "/proc", "proc", 0, nullptr); - mount("sysfs", "/sys", "sysfs", 0, nullptr); - mount("devtmpfs", "/dev", "devtmpfs", 0, nullptr); - } - - printf_flush("=== CFBox Init / System-Level Boot Test ===\n"); - - struct utsname info {}; - if (uname(&info) == 0) { - printf_flush("Kernel: %s %s %s\n", info.sysname, info.release, info.machine); - } - printf_flush("PID: %d\n", getpid()); - printf_flush("\n"); - - printf_flush("--- Applet Smoke Tests ---\n"); - TestResult result{0, 0}; - run_smoke_tests(result); - printf_flush("\n"); - - printf_flush("=== RESULTS: %d passed, %d failed ===\n", result.pass, result.fail); - printf_flush("=== ALL TESTS COMPLETE ===\n"); - - if (is_pid1) { - std::fflush(nullptr); - sync(); - reboot(RB_POWER_OFF); - } - - return (result.fail > 0) ? 1 : 0; -} diff --git a/src/applets/init/init.hpp b/src/applets/init/init.hpp new file mode 100644 index 0000000..942d6b1 --- /dev/null +++ b/src/applets/init/init.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace cfbox::init { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "init", + .version = CFBOX_VERSION_STRING, + .one_line = "system init (PID 1) with inittab support", + .usage = "init", + .options = "", + .extra = "If /etc/inittab exists, runs full init system.\n" + "Otherwise falls back to boot-test mode for QEMU.", +}; + +// inittab entry: id:runlevels:action:process +struct InittabEntry { + std::string id; // 1-4 char identifier + std::string runlevels; // which runlevels (empty = all) + std::string action; // sysinit, boot, once, respawn, wait, ctrlaltdel, shutdown + std::string process; // command to run +}; + +enum class RunLevel { + SysInit, + Boot, + Single, + MultiUser, // runlevel 2-5 + Shutdown, +}; + +// Tracks a spawned child process and its inittab entry +struct SpawnedProcess { + pid_t pid = 0; + std::string process; // command + bool respawn = false; // true if this is a respawn entry + std::chrono::steady_clock::time_point last_spawn; + int respawn_delay_ms = 2000; // initial backoff delay +}; + +struct InitState { + bool is_pid1 = false; + RunLevel runlevel = RunLevel::SysInit; + std::vector entries; + std::vector children; + bool shutting_down = false; + bool sigchld_received = false; + bool sigint_received = false; +}; + +// --- Module interfaces --- + +// init_inittab.cpp +auto parse_inittab(const std::string& path) -> std::vector; +auto parse_inittab_line(std::string_view line) -> InittabEntry; + +// init_runlevel.cpp +auto run_sysinit(InitState& state) -> void; +auto run_boot_entries(InitState& state) -> void; +auto run_level_entries(InitState& state, const std::string& level) -> void; + +// init_spawn.cpp +auto spawn_process(InitState& state, const InittabEntry& entry, bool respawn) -> pid_t; +auto reap_children(InitState& state) -> void; +auto check_respawn(InitState& state) -> void; + +// init_shutdown.cpp +auto do_shutdown(InitState& state, bool reboot) -> void; +auto install_signal_handlers(InitState& state) -> void; + +// Smoke test mode (QEMU compatibility) +auto run_smoke_tests(InitState& state) -> void; + +} // namespace cfbox::init diff --git a/src/applets/init/init_inittab.cpp b/src/applets/init/init_inittab.cpp new file mode 100644 index 0000000..9bc3561 --- /dev/null +++ b/src/applets/init/init_inittab.cpp @@ -0,0 +1,54 @@ +#include "init.hpp" + +namespace cfbox::init { + +auto parse_inittab_line(std::string_view line) -> InittabEntry { + InittabEntry entry; + + // Skip empty lines and comments + if (line.empty() || line[0] == '#') return entry; + + // Format: id:runlevels:action:process + // Find colons + auto c1 = line.find(':'); + if (c1 == std::string_view::npos) return entry; + auto c2 = line.find(':', c1 + 1); + if (c2 == std::string_view::npos) return entry; + auto c3 = line.find(':', c2 + 1); + if (c3 == std::string_view::npos) return entry; + + entry.id = std::string(line.substr(0, c1)); + entry.runlevels = std::string(line.substr(c1 + 1, c2 - c1 - 1)); + entry.action = std::string(line.substr(c2 + 1, c3 - c2 - 1)); + entry.process = std::string(line.substr(c3 + 1)); + + // Trim trailing whitespace from process + while (!entry.process.empty() && (entry.process.back() == ' ' || entry.process.back() == '\t' || entry.process.back() == '\r')) + entry.process.pop_back(); + + return entry; +} + +auto parse_inittab(const std::string& path) -> std::vector { + std::vector entries; + FILE* f = std::fopen(path.c_str(), "r"); + if (!f) return entries; + + char buf[1024]; + while (std::fgets(buf, sizeof(buf), f)) { + std::string_view line(buf); + // Remove newline + while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) + line = line.substr(0, line.size() - 1); + + auto entry = parse_inittab_line(line); + if (!entry.action.empty() && !entry.process.empty()) { + entries.push_back(std::move(entry)); + } + } + + std::fclose(f); + return entries; +} + +} // namespace cfbox::init diff --git a/src/applets/init/init_main.cpp b/src/applets/init/init_main.cpp new file mode 100644 index 0000000..005fafb --- /dev/null +++ b/src/applets/init/init_main.cpp @@ -0,0 +1,154 @@ +#include "init.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +namespace cfbox::init { + +auto run_smoke_tests(InitState& /*state*/) -> void { + struct TestResult { int pass = 0; int fail = 0; }; + TestResult result; + + std::printf("=== CFBox Init / System-Level Boot Test ===\n"); + + struct utsname info {}; + if (uname(&info) == 0) { + std::printf("Kernel: %s %s %s\n", info.sysname, info.release, info.machine); + } + std::printf("PID: %d\n\n", getpid()); + + std::printf("--- Applet Smoke Tests ---\n"); + +#if CFBOX_ENABLE_ECHO + { char n[] = "echo", a[] = "hello"; char* av[] = {n, a, nullptr}; + if (echo_main(2, av) == 0) { std::printf(" PASS: echo\n"); result.pass++; } + else { std::printf(" FAIL: echo\n"); result.fail++; } } +#endif + +#if CFBOX_ENABLE_CAT + { char n[] = "cat", a[] = "/proc/version"; char* av[] = {n, a, nullptr}; + if (cat_main(2, av) == 0) { std::printf(" PASS: cat\n"); result.pass++; } + else { std::printf(" FAIL: cat\n"); result.fail++; } } +#endif + +#if CFBOX_ENABLE_LS + { char n[] = "ls", a[] = "/"; char* av[] = {n, a, nullptr}; + if (ls_main(2, av) == 0) { std::printf(" PASS: ls\n"); result.pass++; } + else { std::printf(" FAIL: ls\n"); result.fail++; } } +#endif + +#if CFBOX_ENABLE_WC + { char n[] = "wc", a1[] = "-l", a2[] = "/proc/cpuinfo"; char* av[] = {n, a1, a2, nullptr}; + if (wc_main(3, av) == 0) { std::printf(" PASS: wc\n"); result.pass++; } + else { std::printf(" FAIL: wc\n"); result.fail++; } } +#endif + +#if CFBOX_ENABLE_FREE + { char n[] = "free"; char* av[] = {n, nullptr}; + if (free_main(1, av) == 0) { std::printf(" PASS: free\n"); result.pass++; } + else { std::printf(" FAIL: free\n"); result.fail++; } } +#endif + +#if CFBOX_ENABLE_UPTIME + { char n[] = "uptime"; char* av[] = {n, nullptr}; + if (uptime_main(1, av) == 0) { std::printf(" PASS: uptime\n"); result.pass++; } + else { std::printf(" FAIL: uptime\n"); result.fail++; } } +#endif + +#if CFBOX_ENABLE_PS + { char n[] = "ps"; char* av[] = {n, nullptr}; + if (ps_main(1, av) == 0) { std::printf(" PASS: ps\n"); result.pass++; } + else { std::printf(" FAIL: ps\n"); result.fail++; } } +#endif + + std::printf("\n=== RESULTS: %d passed, %d failed ===\n", result.pass, result.fail); + std::printf("=== ALL TESTS COMPLETE ===\n"); + std::fflush(nullptr); +} + +} // namespace cfbox::init + +auto init_main(int argc, char* argv[]) -> int { + // Handle --help/--version + for (int i = 1; i < argc; ++i) { + std::string_view arg{argv[i]}; + if (arg == "--help") { cfbox::help::print_help(cfbox::init::HELP); return 0; } + if (arg == "--version") { cfbox::help::print_version(cfbox::init::HELP); return 0; } + } + + cfbox::init::InitState state; + state.is_pid1 = (getpid() == 1); + + // Check if /etc/inittab exists + FILE* inittab = std::fopen("/etc/inittab", "r"); + if (!inittab) { + // Fallback: QEMU smoke test mode (preserves CI compatibility) + if (state.is_pid1) { + mount("proc", "/proc", "proc", 0, nullptr); + mount("sysfs", "/sys", "sysfs", 0, nullptr); + mount("devtmpfs", "/dev", "devtmpfs", 0, nullptr); + } + cfbox::init::run_smoke_tests(state); + if (state.is_pid1) { + std::fflush(nullptr); + sync(); + reboot(RB_POWER_OFF); + } + return 0; + } + std::fclose(inittab); + + // Full init mode + state.entries = cfbox::init::parse_inittab("/etc/inittab"); + + cfbox::init::install_signal_handlers(state); + + // Phase 1: sysinit (mount filesystems, run sysinit commands) + cfbox::init::run_sysinit(state); + + // Phase 2: boot entries + cfbox::init::run_boot_entries(state); + + // Phase 3: multi-user entries (respawn, once, wait) + cfbox::init::run_level_entries(state, "2345"); + + // Main loop: wait for signals, reap children, handle respawn + while (!state.shutting_down) { + // Handle SIGCHLD: reap children + if (state.sigchld_received) { + state.sigchld_received = false; + cfbox::init::reap_children(state); + cfbox::init::check_respawn(state); + } + + // Handle SIGINT (ctrlaltdel) + if (state.sigint_received) { + state.sigint_received = false; + // Run ctrlaltdel entries + for (const auto& e : state.entries) { + if (e.action == "ctrlaltdel") { + cfbox::init::InittabEntry fake; + fake.process = e.process; + auto pid = cfbox::init::spawn_process(state, fake, false); + if (pid > 0) { + int status = 0; + waitpid(pid, &status, 0); + } + } + } + } + + // Sleep briefly, then check again + usleep(100000); // 100ms + } + + // Shutdown + cfbox::init::do_shutdown(state, false); + return 0; +} diff --git a/src/applets/init/init_runlevel.cpp b/src/applets/init/init_runlevel.cpp new file mode 100644 index 0000000..9df56a9 --- /dev/null +++ b/src/applets/init/init_runlevel.cpp @@ -0,0 +1,66 @@ +#include "init.hpp" + +#include +#include + +namespace cfbox::init { + +auto run_sysinit(InitState& state) -> void { + std::fflush(nullptr); + + // Mount essential filesystems if PID 1 + if (state.is_pid1) { + mount("proc", "/proc", "proc", 0, nullptr); + mount("sysfs", "/sys", "sysfs", 0, nullptr); + mount("devtmpfs", "/dev", "devtmpfs", 0, nullptr); + } + + // Run sysinit entries (sequentially, wait for each) + for (const auto& e : state.entries) { + if (e.action == "sysinit") { + auto pid = spawn_process(state, e, false); + if (pid > 0) { + int status = 0; + waitpid(pid, &status, 0); + } + } + } + + state.runlevel = RunLevel::Boot; +} + +auto run_boot_entries(InitState& state) -> void { + for (const auto& e : state.entries) { + if (e.action == "boot" || e.action == "bootwait") { + auto pid = spawn_process(state, e, false); + if (pid > 0 && e.action == "bootwait") { + int status = 0; + waitpid(pid, &status, 0); + } + } + } + + state.runlevel = RunLevel::MultiUser; +} + +auto run_level_entries(InitState& state, const std::string& level) -> void { + for (const auto& e : state.entries) { + // Check if entry applies to current runlevel + if (!e.runlevels.empty() && e.runlevels.find(level[0]) == std::string::npos) + continue; + + bool should_respawn = (e.action == "respawn"); + bool should_wait = (e.action == "wait"); + bool is_once = (e.action == "once"); + + if (should_respawn || is_once || should_wait) { + auto pid = spawn_process(state, e, should_respawn); + if (pid > 0 && should_wait) { + int status = 0; + waitpid(pid, &status, 0); + } + } + } +} + +} // namespace cfbox::init diff --git a/src/applets/init/init_shutdown.cpp b/src/applets/init/init_shutdown.cpp new file mode 100644 index 0000000..c0d9d25 --- /dev/null +++ b/src/applets/init/init_shutdown.cpp @@ -0,0 +1,99 @@ +#include "init.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace cfbox::init { + +// Global state pointer for signal handlers +static InitState* g_state = nullptr; + +static void sigchld_handler(int) { + if (g_state) g_state->sigchld_received = true; +} + +static void sigint_handler(int) { + if (g_state) g_state->sigint_received = true; +} + +static void sigterm_handler(int) { + if (g_state) g_state->shutting_down = true; +} + +auto install_signal_handlers(InitState& state) -> void { + g_state = &state; + + struct sigaction sa {}; + sa.sa_handler = sigchld_handler; + sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; + sigaction(SIGCHLD, &sa, nullptr); + + sa.sa_handler = sigint_handler; + sa.sa_flags = 0; + sigaction(SIGINT, &sa, nullptr); + + sa.sa_handler = sigterm_handler; + sigaction(SIGTERM, &sa, nullptr); +} + +auto do_shutdown(InitState& state, bool do_reboot) -> void { + state.shutting_down = true; + std::fflush(nullptr); + + std::printf("cfbox init: shutting down\n"); + std::fflush(stdout); + + // Run shutdown action entries + for (const auto& e : state.entries) { + if (e.action == "shutdown") { + InittabEntry fake; + fake.process = e.process; + auto pid = spawn_process(state, fake, false); + if (pid > 0) { + int status = 0; + waitpid(pid, &status, 0); + } + } + } + + // Send SIGTERM to all processes + kill(-1, SIGTERM); + std::printf("cfbox init: sent SIGTERM to all processes\n"); + std::fflush(stdout); + + // Wait up to 2 seconds + for (int i = 0; i < 20; ++i) { + usleep(100000); // 100ms + } + + // Send SIGKILL to remaining + kill(-1, SIGKILL); + std::printf("cfbox init: sent SIGKILL to remaining processes\n"); + std::fflush(stdout); + usleep(100000); + + // Sync filesystems + sync(); + + // Unmount all filesystems (in reverse order) + if (state.is_pid1) { + umount("/dev"); + umount("/sys"); + umount("/proc"); + } + + if (state.is_pid1) { + if (do_reboot) { + reboot(RB_AUTOBOOT); + } else { + reboot(RB_POWER_OFF); + } + } +} + +} // namespace cfbox::init diff --git a/src/applets/init/init_spawn.cpp b/src/applets/init/init_spawn.cpp new file mode 100644 index 0000000..30e9b1a --- /dev/null +++ b/src/applets/init/init_spawn.cpp @@ -0,0 +1,106 @@ +#include "init.hpp" + +#include +#include +#include +#include +#include + +namespace cfbox::init { + +auto spawn_process(InitState& state, const InittabEntry& entry, bool respawn) -> pid_t { + std::fflush(nullptr); + + pid_t pid = fork(); + if (pid < 0) { + std::fprintf(stderr, "cfbox init: fork failed for '%s': %s\n", + entry.process.c_str(), std::strerror(errno)); + return -1; + } + + if (pid == 0) { + // Child: exec the command via sh -c + // Create a new session for the child + if (state.is_pid1) { + setsid(); + } + + // Build argv for: /bin/sh -c "process" + char shell[] = "/bin/sh"; + char dash_c[] = "-c"; + std::string cmd = entry.process; + char* argv[] = { shell, dash_c, cmd.data(), nullptr }; + + execv(shell, argv); + // If execv fails, try /bin/cfbox sh + std::fprintf(stderr, "cfbox init: exec failed for '%s': %s\n", + entry.process.c_str(), std::strerror(errno)); + _exit(127); + } + + // Parent: track the child + if (respawn) { + SpawnedProcess sp; + sp.pid = pid; + sp.process = entry.process; + sp.respawn = true; + sp.last_spawn = std::chrono::steady_clock::now(); + sp.respawn_delay_ms = 2000; + state.children.push_back(std::move(sp)); + } + + return pid; +} + +auto reap_children(InitState& state) -> void { + int status = 0; + pid_t pid; + while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { + // Remove from children list if it was tracked + for (auto it = state.children.begin(); it != state.children.end(); ++it) { + if (it->pid == pid) { + if (it->respawn && !state.shutting_down) { + // Mark for respawn (don't remove) + it->pid = 0; + } else { + state.children.erase(it); + } + break; + } + } + } +} + +auto check_respawn(InitState& state) -> void { + auto now = std::chrono::steady_clock::now(); + + for (auto& child : state.children) { + if (child.pid != 0 || !child.respawn || state.shutting_down) continue; + + auto elapsed_ms = std::chrono::duration_cast( + now - child.last_spawn).count(); + + // If the process lived more than 60s, reset backoff + if (elapsed_ms > 60000) { + child.respawn_delay_ms = 2000; + } + + // Check if enough time has passed for respawn + auto wait_ms = child.respawn_delay_ms; + if (elapsed_ms < wait_ms) continue; + + // Respawn + InittabEntry fake_entry; + fake_entry.process = child.process; + + auto new_pid = spawn_process(state, fake_entry, false); + if (new_pid > 0) { + child.pid = new_pid; + child.last_spawn = now; + // Exponential backoff, cap at 60s + child.respawn_delay_ms = std::min(child.respawn_delay_ms * 2, 60000); + } + } +} + +} // namespace cfbox::init diff --git a/src/applets/iostat.cpp b/src/applets/iostat.cpp new file mode 100644 index 0000000..70efa93 --- /dev/null +++ b/src/applets/iostat.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "iostat", + .version = CFBOX_VERSION_STRING, + .one_line = "report CPU and I/O statistics", + .usage = "iostat [-c COUNT] [-d DELAY]", + .options = " -c N number of reports (default 1)\n" + " -d N delay in seconds between reports (default 1)", + .extra = "", +}; + +auto print_iostat(const std::vector& stats) -> void { + std::printf("%-10s %10s %10s %10s %10s %10s\n", + "Device", "r/s", "w/s", "rkB/s", "wkB/s", "await"); + for (const auto& ds : stats) { + if (ds.device.find("ram") != std::string::npos || + ds.device.find("loop") != std::string::npos) continue; + + std::printf("%-10s %10llu %10llu %10llu %10llu %10llu\n", + ds.device.c_str(), + static_cast(ds.reads), + static_cast(ds.writes), + static_cast(ds.sectors_read / 2), + static_cast(ds.sectors_written / 2), + static_cast(ds.ms_ios)); + } +} + +auto print_delta(const std::vector& prev, + const std::vector& curr, + double interval) -> void { + std::printf("%-10s %10s %10s %10s %10s\n", + "Device", "r/s", "w/s", "rkB/s", "wkB/s"); + for (const auto& c : curr) { + if (c.device.find("ram") != std::string::npos || + c.device.find("loop") != std::string::npos) continue; + + double dr = static_cast(c.reads); + double dw = static_cast(c.writes); + double drkb = static_cast(c.sectors_read / 2); + double dwkb = static_cast(c.sectors_written / 2); + for (const auto& p : prev) { + if (p.device == c.device) { + dr = static_cast(c.reads - p.reads) / interval; + dw = static_cast(c.writes - p.writes) / interval; + drkb = static_cast((c.sectors_read - p.sectors_read) / 2) / interval; + dwkb = static_cast((c.sectors_written - p.sectors_written) / 2) / interval; + break; + } + } + std::printf("%-10s %10.0f %10.0f %10.0f %10.0f\n", + c.device.c_str(), dr, dw, drkb, dwkb); + } +} + +} // anonymous namespace + +auto iostat_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'c', true, "count"}, + cfbox::args::OptSpec{'d', true, "delay"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + int count = 1; + double delay = 1.0; + if (auto v = parsed.get('c')) count = std::stoi(std::string(*v)); + if (auto v = parsed.get('d')) delay = std::stod(std::string(*v)); + + auto first = cfbox::proc::read_diskstats(); + if (!first) { + std::fprintf(stderr, "cfbox iostat: %s\n", first.error().msg.c_str()); + return 1; + } + + auto cpu = cfbox::proc::read_cpu_stats(); + if (cpu) { + double total = static_cast(cpu->total()); + if (total > 0.0) { + auto pct = [&](std::uint64_t v) -> double { + return 100.0 * static_cast(v) / total; + }; + double idle_pct = pct(cpu->idle_time()); + std::printf("avg-cpu: %%user %%nice %%system %%iowait %%steal %%idle\n"); + std::printf(" %6.1f %6.1f %9.1f %8.1f %7.1f %7.1f\n", + pct(cpu->user), pct(cpu->nice), pct(cpu->system), + pct(cpu->iowait), pct(cpu->steal), idle_pct); + } + std::printf("\n"); + } + + if (count <= 1) { + print_iostat(*first); + return 0; + } + + auto prev = *first; + for (int i = 1; i < count; ++i) { + std::this_thread::sleep_for( + std::chrono::milliseconds(static_cast(delay * 1000))); + + auto curr = cfbox::proc::read_diskstats(); + if (!curr) break; + + std::printf("\n"); + print_delta(prev, *curr, delay); + prev = *curr; + } + + return 0; +} diff --git a/src/applets/kill.cpp b/src/applets/kill.cpp new file mode 100644 index 0000000..7ac67d7 --- /dev/null +++ b/src/applets/kill.cpp @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "kill", + .version = CFBOX_VERSION_STRING, + .one_line = "send a signal to a process", + .usage = "kill [-s SIGNAL] PID...\nkill -l [SIGNAL]", + .options = " -s SIG signal to send (default: TERM)\n" + " -l list signal names", + .extra = "", +}; + +struct SignalEntry { + const char* name; + int num; +}; + +const SignalEntry SIGNALS[] = { + {"HUP", SIGHUP}, {"INT", SIGINT}, {"QUIT", SIGQUIT}, {"ILL", SIGILL}, + {"ABRT", SIGABRT}, {"FPE", SIGFPE}, {"KILL", SIGKILL}, {"SEGV", SIGSEGV}, + {"PIPE", SIGPIPE}, {"ALRM", SIGALRM}, {"TERM", SIGTERM}, {"USR1", SIGUSR1}, + {"USR2", SIGUSR2}, {"CHLD", SIGCHLD}, {"CONT", SIGCONT}, {"STOP", SIGSTOP}, + {"TSTP", SIGTSTP}, {"TTIN", SIGTTIN}, {"TTOU", SIGTTOU}, {"BUS", SIGBUS}, + {"PROF", SIGPROF}, {"SYS", SIGSYS}, {"TRAP", SIGTRAP}, {"URG", SIGURG}, + {"VTALRM", SIGVTALRM}, {"XCPU", SIGXCPU}, {"XFSZ", SIGXFSZ}, +}; + +auto signal_from_name(std::string_view name) -> int { + // Strip optional SIG prefix + if (name.size() > 3 && name.substr(0, 3) == "SIG") + name = name.substr(3); + + // Try numeric + if (!name.empty() && name[0] >= '0' && name[0] <= '9') { + return std::atoi(name.data()); + } + + for (const auto& s : SIGNALS) { + if (name == s.name) return s.num; + } + return -1; +} + +auto list_signals() -> void { + bool first = true; + for (const auto& s : SIGNALS) { + if (!first) std::printf(" "); + std::printf("%2d) %-6s", s.num, s.name); + first = false; + } + std::printf("\n"); +} + +} // anonymous namespace + +auto kill_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'s', true, "signal"}, + cfbox::args::OptSpec{'l', false, "list"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + if (parsed.has('l') || parsed.has_long("list")) { + list_signals(); + return 0; + } + + int sig = SIGTERM; + + // Handle -s SIGNAL or -SIGNAL (e.g. -9, -SIGTERM) + if (auto s = parsed.get('s')) { + sig = signal_from_name(*s); + } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox kill: no PID specified\n"); + return 1; + } + + // If first positional looks like a signal spec (negative number or starts with SIG) + // and no -s was given, treat it as signal + std::size_t start = 0; + if (!parsed.get('s') && !pos.empty()) { + if (pos[0][0] == '-') { + // -N or -SIGNAME + std::string_view spec(pos[0].substr(1)); + int maybe_sig = signal_from_name(spec); + if (maybe_sig > 0) { + sig = maybe_sig; + start = 1; + } + } + } + + if (sig <= 0) { + std::fprintf(stderr, "cfbox kill: invalid signal\n"); + return 1; + } + + int errors = 0; + for (std::size_t i = start; i < pos.size(); ++i) { + const auto& arg = pos[i]; + // Handle special case: 0 means all processes in process group + pid_t pid = static_cast(std::strtol(arg.data(), nullptr, 10)); + if (kill(pid, sig) != 0) { + std::fprintf(stderr, "cfbox kill: (%d) - %s\n", pid, std::strerror(errno)); + ++errors; + } + } + + return errors > 0 ? 1 : 0; +} diff --git a/src/applets/more.cpp b/src/applets/more.cpp new file mode 100644 index 0000000..8e56fec --- /dev/null +++ b/src/applets/more.cpp @@ -0,0 +1,126 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "more", + .version = CFBOX_VERSION_STRING, + .one_line = "file perusal filter for crt viewing", + .usage = "more [FILE]", + .options = "", + .extra = "Space=next page Enter=next line q=quit", +}; + +auto read_lines(std::FILE* f) -> std::vector { + std::vector lines; + char buf[4096]; + while (std::fgets(buf, sizeof(buf), f)) { + auto len = std::strlen(buf); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { + buf[--len] = '\0'; + } + lines.emplace_back(buf); + } + return lines; +} + +} // anonymous namespace + +auto more_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + std::string filename = pos.empty() ? "" : std::string(pos[0]); + + std::FILE* f = stdin; + if (!filename.empty() && filename != "-") { + f = std::fopen(filename.c_str(), "r"); + if (!f) { + std::fprintf(stderr, "cfbox more: cannot open %s\n", filename.c_str()); + return 1; + } + } + + auto lines = read_lines(f); + if (f != stdin) std::fclose(f); + + if (lines.empty()) return 0; + + // Check if output is a terminal + if (!isatty(STDOUT_FILENO)) { + for (const auto& line : lines) std::printf("%s\n", line.c_str()); + return 0; + } + + auto [rows, cols] = cfbox::terminal::get_size(); + int usable_rows = rows - 1; // Leave room for status line + if (usable_rows < 1) usable_rows = 1; + + cfbox::terminal::RawMode raw_mode; + std::size_t top_line = 0; + + while (top_line < lines.size()) { + cfbox::terminal::clear_screen(); + cfbox::terminal::move_cursor(1, 1); + + // Display a page + auto end = std::min(top_line + static_cast(usable_rows), lines.size()); + for (auto i = top_line; i < end; ++i) { + cfbox::terminal::move_cursor(static_cast(i - top_line) + 1, 1); + const auto& line = lines[i]; + // Truncate to terminal width + auto print_len = std::min(static_cast(line.size()), cols); + std::printf("%.*s", print_len, line.c_str()); + cfbox::terminal::clear_line(); + } + + // Status line + cfbox::terminal::invert_video(true); + cfbox::terminal::move_cursor(rows, 1); + std::printf("--More--(%zu%%)", std::min(end * 100 / lines.size(), static_cast(100))); + cfbox::terminal::clear_line(); + cfbox::terminal::invert_video(false); + std::fflush(stdout); + + // Wait for key + while (true) { + auto key = cfbox::tui::read_key(0, -1); + if (!key) continue; + + if (key->is_char() && key->ch == 'q') return 0; + if (key->type == cfbox::tui::KeyType::Escape) return 0; + + if (key->type == cfbox::tui::KeyType::Enter) { + top_line += 1; + break; + } + if ((key->is_char() && key->ch == ' ') || + key->type == cfbox::tui::KeyType::PageDown) { + top_line += static_cast(usable_rows); + break; + } + if (key->type == cfbox::tui::KeyType::PageUp) { + if (top_line >= static_cast(usable_rows)) { + top_line -= static_cast(usable_rows); + } else { + top_line = 0; + } + break; + } + } + } + + cfbox::terminal::clear_screen(); + return 0; +} diff --git a/src/applets/pgrep.cpp b/src/applets/pgrep.cpp new file mode 100644 index 0000000..76d6cf0 --- /dev/null +++ b/src/applets/pgrep.cpp @@ -0,0 +1,126 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP_PGREP = { + .name = "pgrep", + .version = CFBOX_VERSION_STRING, + .one_line = "look up processes based on name", + .usage = "pgrep [-flx] [-P PPID] [-u UID] PATTERN", + .options = " -f match against full command line\n" + " -l list PID and name\n" + " -x exact match\n" + " -P match by parent PID\n" + " -u match by effective UID", + .extra = "", +}; + +auto get_program_name(char* argv0) -> std::string { + std::string_view sv(argv0); + auto slash = sv.rfind('/'); + return std::string(slash == std::string_view::npos ? sv : sv.substr(slash + 1)); +} + +} // anonymous namespace + +auto pgrep_main(int argc, char* argv[]) -> int { + bool is_pkill = false; + if (argc > 0) { + auto name = get_program_name(argv[0]); + if (name == "pkill") is_pkill = true; + } + + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'f', false, "full"}, + cfbox::args::OptSpec{'l', false, "list"}, + cfbox::args::OptSpec{'x', false, "exact"}, + cfbox::args::OptSpec{'P', true, "parent"}, + cfbox::args::OptSpec{'u', true, "euid"}, + cfbox::args::OptSpec{'s', true, "signal"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP_PGREP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP_PGREP); return 0; } + + bool full_match = parsed.has('f') || parsed.has_long("full"); + bool list_names = parsed.has('l') || parsed.has_long("list"); + bool exact = parsed.has('x') || parsed.has_long("exact"); + + pid_t filter_ppid = -1; + if (auto v = parsed.get('P')) filter_ppid = static_cast(std::stoi(std::string(*v))); + + uid_t filter_uid = static_cast(-1); + if (auto v = parsed.get('u')) filter_uid = static_cast(std::stoi(std::string(*v))); + + int sig = SIGTERM; + if (auto v = parsed.get('s')) { + std::string_view sname = *v; + if (sname.size() > 3 && sname.substr(0, 3) == "SIG") sname = sname.substr(3); + if (sname == "HUP") sig = SIGHUP; + else if (sname == "INT") sig = SIGINT; + else if (sname == "KILL") sig = SIGKILL; + else if (sname == "TERM") sig = SIGTERM; + else if (sname == "USR1") sig = SIGUSR1; + else if (sname == "USR2") sig = SIGUSR2; + else sig = std::stoi(std::string(*v)); + } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox %s: no pattern specified\n", is_pkill ? "pkill" : "pgrep"); + return 1; + } + const auto& pattern = pos[0]; + + auto result = cfbox::proc::read_all_processes(); + if (!result) { + std::fprintf(stderr, "cfbox %s: %s\n", is_pkill ? "pkill" : "pgrep", result.error().msg.c_str()); + return 1; + } + + auto matches_name = [&](const cfbox::proc::ProcessInfo& p) -> bool { + if (full_match) { + std::string cmdline; + for (const auto& a : p.cmdline) { + if (!cmdline.empty()) cmdline += ' '; + cmdline += a; + } + if (cmdline.empty()) cmdline = p.comm; + if (exact) return cmdline == pattern; + return cmdline.find(pattern) != std::string::npos; + } + if (exact) return p.comm == pattern; + return p.comm.find(pattern) != std::string::npos; + }; + + bool found = false; + for (const auto& p : *result) { + if (filter_ppid >= 0 && p.ppid != filter_ppid) continue; + if (filter_uid != static_cast(-1) && p.uid != filter_uid) continue; + if (!matches_name(p)) continue; + + found = true; + if (is_pkill) { + kill(p.pid, sig); + } else { + if (list_names) { + std::printf("%d %s\n", p.pid, p.comm.c_str()); + } else { + std::printf("%d\n", p.pid); + } + } + } + + return found ? 0 : 1; +} diff --git a/src/applets/pidof.cpp b/src/applets/pidof.cpp new file mode 100644 index 0000000..feb47b2 --- /dev/null +++ b/src/applets/pidof.cpp @@ -0,0 +1,60 @@ +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "pidof", + .version = CFBOX_VERSION_STRING, + .one_line = "find the process ID of a running program", + .usage = "pidof [-s] NAME...", + .options = " -s single shot — return one PID only", + .extra = "", +}; + +} // anonymous namespace + +auto pidof_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'s', false, "single"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool single = parsed.has('s') || parsed.has_long("single"); + const auto& names = parsed.positional(); + if (names.empty()) { + std::fprintf(stderr, "cfbox pidof: no program name specified\n"); + return 1; + } + + auto result = cfbox::proc::read_all_processes(); + if (!result) { + std::fprintf(stderr, "cfbox pidof: %s\n", result.error().msg.c_str()); + return 1; + } + + bool found = false; + bool first = true; + for (const auto& proc : *result) { + for (const auto& name : names) { + if (proc.comm == name) { + if (!first) std::printf(" "); + std::printf("%d", proc.pid); + first = false; + found = true; + if (single) goto done; + break; + } + } + } +done: + if (found) std::printf("\n"); + return found ? 0 : 1; +} diff --git a/src/applets/pmap.cpp b/src/applets/pmap.cpp new file mode 100644 index 0000000..43b0bd0 --- /dev/null +++ b/src/applets/pmap.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "pmap", + .version = CFBOX_VERSION_STRING, + .one_line = "display memory map of a process", + .usage = "pmap [-x] PID", + .options = " -x extended format", + .extra = "", +}; + +struct MapEntry { + std::uint64_t address = 0; + std::uint64_t end_address = 0; + std::string perms; + std::uint64_t offset = 0; + std::string dev; + std::uint64_t inode = 0; + std::string pathname; +}; + +auto parse_maps(pid_t pid) -> std::vector { + auto path = "/proc/" + std::to_string(pid) + "/maps"; + std::ifstream f(path); + if (!f) return {}; + + std::vector entries; + std::string line; + while (std::getline(f, line)) { + if (line.empty()) continue; + MapEntry e; + + auto dash = line.find('-'); + if (dash == std::string::npos) continue; + e.address = std::strtoull(line.substr(0, dash).c_str(), nullptr, 16); + + // Find the space after perms to get end_address + auto rest = line.substr(dash + 1); + std::istringstream iss(rest); + std::string perms_str; + iss >> perms_str; + e.end_address = e.address + 1; // Will be properly calculated from next line + e.perms = perms_str; + iss >> std::hex >> e.offset; + + std::string dev_str; + iss >> dev_str; + e.dev = dev_str; + + iss >> std::dec >> e.inode; + + // Remaining is pathname (may be empty) + std::string pn; + while (iss.peek() == ' ') iss.get(); + if (std::getline(iss, pn)) { + // Trim leading space + auto start = pn.find_first_not_of(' '); + if (start != std::string::npos) e.pathname = pn.substr(start); + } + + entries.push_back(std::move(e)); + } + + // Calculate end_address from next entry + for (size_t i = 0; i + 1 < entries.size(); ++i) { + entries[i].end_address = entries[i + 1].address; + } + + return entries; +} + +} // anonymous namespace + +auto pmap_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'x', false, "extended"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool extended = parsed.has('x') || parsed.has_long("extended"); + + const auto& args = parsed.positional(); + if (args.empty()) { + std::fprintf(stderr, "cfbox pmap: no PID specified\n"); + return 1; + } + + pid_t pid = static_cast(std::stoi(std::string(args[0]))); + auto entries = parse_maps(pid); + if (entries.empty()) { + std::fprintf(stderr, "cfbox pmap: cannot read maps for PID %d\n", pid); + return 1; + } + + std::printf("%d: %s\n", pid, std::string(args[0]).c_str()); + + std::uint64_t total_kb = 0; + for (const auto& e : entries) { + auto kb = (e.end_address - e.address) / 1024; + total_kb += kb; + + if (extended) { + std::printf("%016lx %5llu %08lx %-5s %-6s %6llu %s\n", + static_cast(e.address), + static_cast(kb), + static_cast(e.offset), + e.perms.c_str(), + e.dev.c_str(), + static_cast(e.inode), + e.pathname.c_str()); + } else { + std::printf("%016lx %5luK %s %s\n", + static_cast(e.address), + static_cast(kb), + e.perms.c_str(), + e.pathname.empty() ? "" : e.pathname.c_str()); + } + } + + std::printf(" mapped: %lluK\n", + static_cast(total_kb)); + + return 0; +} diff --git a/src/applets/ps.cpp b/src/applets/ps.cpp new file mode 100644 index 0000000..e52b6d2 --- /dev/null +++ b/src/applets/ps.cpp @@ -0,0 +1,169 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "ps", + .version = CFBOX_VERSION_STRING, + .one_line = "report a snapshot of current processes", + .usage = "ps [aux] [-e]", + .options = " aux show all processes with user-oriented format\n" + " -e show all processes (simple format)", + .extra = "", +}; + +auto uid_to_name(uid_t uid) -> std::string { + // Simple numeric representation + return std::to_string(uid); +} + +auto format_time_ticks(std::uint64_t ticks) -> std::string { + auto total_secs = static_cast(ticks / cfbox::proc::clock_ticks_per_second()); + auto mins = total_secs / 60; + auto secs = total_secs % 60; + char buf[32]; + std::snprintf(buf, sizeof(buf), "%ld:%02ld", mins, secs); + return buf; +} + +auto format_start_time(std::uint64_t start_ticks) -> std::string { + auto start_secs = static_cast(start_ticks / static_cast(cfbox::proc::clock_ticks_per_second())); + + auto up_result = cfbox::proc::read_uptime(); + if (!up_result) return "?"; + + time_t boot_time = static_cast(static_cast(std::time(nullptr)) - up_result->first); + time_t proc_start = boot_time + start_secs; + + auto now = std::time(nullptr); + auto tm = std::localtime(&proc_start); + char buf[16]; + + if (now - proc_start < 86400) { + // Today: show HH:MM + std::strftime(buf, sizeof(buf), "%H:%M", tm); + } else { + // Older: show MonDD + std::strftime(buf, sizeof(buf), "%b%d", tm); + } + return buf; +} + +auto format_state(char state) -> std::string { + switch (state) { + case 'R': return "R"; + case 'S': return "S"; + case 'D': return "D"; + case 'Z': return "Z"; + case 'T': return "T"; + case 't': return "t"; + case 'W': return "W"; + default: return std::string(1, state); + } +} + +} // anonymous namespace + +auto ps_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'e', false}, + cfbox::args::OptSpec{'f', false}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + // Detect format: "aux" as a single arg, or flags + bool aux_format = false; + + for (int i = 1; i < argc; ++i) { + std::string_view arg(argv[i]); + if (arg == "aux" || arg == "-aux" || arg == "auxf") { + aux_format = true; + } + } + (void)parsed.has('e'); // -e currently unused + + auto result = cfbox::proc::read_all_processes(); + if (!result) { + std::fprintf(stderr, "cfbox ps: %s\n", result.error().msg.c_str()); + return 1; + } + + auto total_mem = cfbox::proc::total_memory_kb(); + auto ticks = cfbox::proc::clock_ticks_per_second(); + + if (aux_format) { + std::printf("%-8s %5s %4s %4s %8s %8s %-6s %-4s %-6s %5s %s\n", + "USER", "PID", "%CPU", "%MEM", "VSZ", "RSS", "TTY", "STAT", + "START", "TIME", "COMMAND"); + + for (const auto& p : *result) { + auto user = uid_to_name(p.uid); + + // CPU%: rough approximation from utime+stime / total time since start + double cpu_pct = 0.0; + if (ticks > 0 && p.start_time > 0) { + auto up_result = cfbox::proc::read_uptime(); + if (up_result) { + double uptime = up_result->first; + double dticks = static_cast(p.start_time); + double secs = uptime - dticks / static_cast(ticks); + if (secs > 0) { + cpu_pct = static_cast(p.utime + p.stime) / static_cast(ticks) / secs * 100.0; + } + } + } + + double mem_pct = 0.0; + if (total_mem > 0) { + double rss_kb = static_cast(p.rss) * static_cast(cfbox::proc::page_size()) / 1024.0; + mem_pct = rss_kb / static_cast(total_mem) * 100.0; + } + + auto vsz = p.vsize / 1024; // to KiB + auto rss = p.rss * static_cast(cfbox::proc::page_size()) / 1024; + auto stat = format_state(p.state); + auto start = format_start_time(p.start_time); + auto time = format_time_ticks(p.utime + p.stime); + + // Command: use cmdline if available, otherwise [comm] + std::string cmd; + if (!p.cmdline.empty()) { + for (std::size_t i = 0; i < p.cmdline.size(); ++i) { + if (i > 0) cmd += ' '; + cmd += p.cmdline[i]; + } + } else { + cmd = "[" + p.comm + "]"; + } + + std::printf("%-8s %5d %4.1f %4.1f %8llu %8llu %-6s %-4s %-6s %5s %s\n", + user.c_str(), p.pid, cpu_pct, mem_pct, + static_cast(vsz), + static_cast(rss), + p.tty.c_str(), stat.c_str(), + start.c_str(), time.c_str(), cmd.c_str()); + } + } else { + // Simple format: PID TTY TIME CMD + std::printf(" PID TTY TIME CMD\n"); + for (const auto& p : *result) { + auto time = format_time_ticks(p.utime + p.stime); + std::string cmd = p.cmdline.empty() ? ("[" + p.comm + "]") : p.cmdline[0]; + std::printf("%5d %-6s %10s %s\n", p.pid, p.tty.c_str(), time.c_str(), cmd.c_str()); + } + } + + return 0; +} diff --git a/src/applets/pstree.cpp b/src/applets/pstree.cpp new file mode 100644 index 0000000..86ac031 --- /dev/null +++ b/src/applets/pstree.cpp @@ -0,0 +1,109 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "pstree", + .version = CFBOX_VERSION_STRING, + .one_line = "display a tree of processes", + .usage = "pstree [-p]", + .options = " -p show PIDs", + .extra = "", +}; + +struct PNode { + pid_t pid = 0; + pid_t ppid = 0; + std::string comm; + std::vector children; +}; + +auto print_tree(const std::map& nodes, pid_t pid, + const std::string& prefix, bool show_pids, bool is_last) -> void { + auto it = nodes.find(pid); + if (it == nodes.end()) return; + const auto& node = it->second; + + // Connector for this level + std::string connector; + if (pid != 1 || !prefix.empty()) { + connector = is_last ? "\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80" // └── + : "\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80"; // ├── + } + + std::printf("%s%s%s", prefix.c_str(), connector.c_str(), node.comm.c_str()); + if (show_pids) std::printf("(%d)", node.pid); + std::printf("\n"); + + // Child prefix: vertical line or spaces + std::string child_prefix = prefix; + if (pid != 1 || !prefix.empty()) { + child_prefix += is_last ? " " + : "\xe2\x94\x82 "; // │ + } + + const auto& kids = node.children; + for (size_t i = 0; i < kids.size(); ++i) { + print_tree(nodes, kids[i], child_prefix, show_pids, i + 1 == kids.size()); + } +} + +} // anonymous namespace + +auto pstree_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'p', false, "show-pids"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool show_pids = parsed.has('p') || parsed.has_long("show-pids"); + + auto result = cfbox::proc::read_all_processes(); + if (!result) { + std::fprintf(stderr, "cfbox pstree: %s\n", result.error().msg.c_str()); + return 1; + } + + // Build node map + std::map nodes; + for (const auto& proc : *result) { + PNode node; + node.pid = proc.pid; + node.ppid = proc.ppid; + node.comm = proc.comm; + nodes[proc.pid] = std::move(node); + } + + // Link children + std::vector roots; + for (auto& [pid, node] : nodes) { + if (node.ppid == 0 || nodes.find(node.ppid) == nodes.end()) { + roots.push_back(pid); + } else { + nodes[node.ppid].children.push_back(pid); + } + } + + // Sort children by PID for deterministic output + for (auto& [pid, node] : nodes) { + std::sort(node.children.begin(), node.children.end()); + } + std::sort(roots.begin(), roots.end()); + + // Print trees from root nodes + for (size_t i = 0; i < roots.size(); ++i) { + print_tree(nodes, roots[i], "", show_pids, i + 1 == roots.size()); + } + + return 0; +} diff --git a/src/applets/pwdx.cpp b/src/applets/pwdx.cpp new file mode 100644 index 0000000..1bdca0a --- /dev/null +++ b/src/applets/pwdx.cpp @@ -0,0 +1,48 @@ +#include +#include +#include + +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "pwdx", + .version = CFBOX_VERSION_STRING, + .one_line = "print working directory of a process", + .usage = "pwdx PID...", + .options = "", + .extra = "", +}; + +} // anonymous namespace + +auto pwdx_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& args = parsed.positional(); + if (args.empty()) { + std::fprintf(stderr, "cfbox pwdx: no PID specified\n"); + return 1; + } + + int rc = 0; + for (const auto& arg : args) { + auto link = "/proc/" + std::string(arg) + "/cwd"; + std::error_code ec; + auto target = std::filesystem::read_symlink(link, ec); + if (ec) { + std::fprintf(stderr, "cfbox pwdx: %.*s: %s\n", + static_cast(arg.size()), arg.data(), ec.message().c_str()); + rc = 1; + } else { + std::printf("%.*s: %s\n", + static_cast(arg.size()), arg.data(), target.c_str()); + } + } + return rc; +} diff --git a/src/applets/renice.cpp b/src/applets/renice.cpp new file mode 100644 index 0000000..d9005a4 --- /dev/null +++ b/src/applets/renice.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "renice", + .version = CFBOX_VERSION_STRING, + .one_line = "alter priority of running processes", + .usage = "renice [-n INCREMENT] {-p PID|-g PGRP|-u USER}...", + .options = " -n N increment (default 1)\n" + " -p interpret args as PIDs (default)\n" + " -g interpret args as process groups\n" + " -u interpret args as user names/IDs", + .extra = "", +}; + +} // anonymous namespace + +auto renice_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'n', true, "priority"}, + cfbox::args::OptSpec{'p', false, "pid"}, + cfbox::args::OptSpec{'g', false, "pgrp"}, + cfbox::args::OptSpec{'u', false, "user"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + int increment = 1; + if (auto v = parsed.get('n')) increment = std::stoi(std::string(*v)); + + const auto& args = parsed.positional(); + if (args.empty()) { + std::fprintf(stderr, "cfbox renice: no ID specified\n"); + return 1; + } + + int rc = 0; + for (const auto& id_str : args) { + auto id = static_cast(std::stoi(std::string(id_str))); + + int which = PRIO_PROCESS; + if (parsed.has('g') || parsed.has_long("pgrp")) which = PRIO_PGRP; + if (parsed.has('u') || parsed.has_long("user")) which = PRIO_USER; + + errno = 0; + auto current = getpriority(which, id); + if (errno != 0) { + std::fprintf(stderr, "cfbox renice: %d: %s\n", static_cast(id), std::strerror(errno)); + rc = 1; + continue; + } + + auto new_pri = current + increment; + if (setpriority(which, id, new_pri) != 0) { + std::fprintf(stderr, "cfbox renice: %d: %s\n", static_cast(id), std::strerror(errno)); + rc = 1; + } else { + std::printf("%d: old priority %d, new priority %d\n", static_cast(id), current, new_pri); + } + } + + return rc; +} diff --git a/src/applets/rev.cpp b/src/applets/rev.cpp new file mode 100644 index 0000000..bd277e3 --- /dev/null +++ b/src/applets/rev.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "rev", + .version = CFBOX_VERSION_STRING, + .one_line = "reverse lines characterwise", + .usage = "rev [FILE...]", + .options = "", + .extra = "", +}; + +auto process_stream(std::istream& in) -> void { + std::string line; + while (std::getline(in, line)) { + std::reverse(line.begin(), line.end()); + std::printf("%s\n", line.c_str()); + } +} + +} // anonymous namespace + +auto rev_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + process_stream(std::cin); + return 0; + } + + int rc = 0; + for (const auto& filename : pos) { + auto fn = std::string(filename); + if (fn == "-") { + process_stream(std::cin); + } else { + std::ifstream f(fn); + if (!f) { + std::fprintf(stderr, "cfbox rev: cannot open %s\n", fn.c_str()); + rc = 1; + continue; + } + process_stream(f); + } + } + return rc; +} diff --git a/src/applets/sh/sh_builtins.cpp b/src/applets/sh/sh_builtins.cpp index 8b0f7f6..28f20c2 100644 --- a/src/applets/sh/sh_builtins.cpp +++ b/src/applets/sh/sh_builtins.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace cfbox::sh { diff --git a/src/applets/sleep.cpp b/src/applets/sleep.cpp index 36ed2e6..c00f113 100644 --- a/src/applets/sleep.cpp +++ b/src/applets/sleep.cpp @@ -32,9 +32,10 @@ auto sleep_main(int argc, char* argv[]) -> int { double total = 0.0; for (auto arg : pos) { + auto s = std::string{arg}; char* end = nullptr; - double val = std::strtod(std::string{arg}.c_str(), &end); - if (end == std::string{arg}.c_str() || val < 0) { + double val = std::strtod(s.c_str(), &end); + if (end == s.c_str() || val < 0) { std::fprintf(stderr, "cfbox sleep: invalid time interval '%.*s'\n", static_cast(arg.size()), arg.data()); return 1; diff --git a/src/applets/sysctl.cpp b/src/applets/sysctl.cpp new file mode 100644 index 0000000..67ec129 --- /dev/null +++ b/src/applets/sysctl.cpp @@ -0,0 +1,174 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "sysctl", + .version = CFBOX_VERSION_STRING, + .one_line = "configure kernel parameters at runtime", + .usage = "sysctl [-a] [-n] [-w KEY=VALUE] [-p FILE] [KEY]", + .options = " -a display all values\n" + " -n show only values (no keys)\n" + " -w set a value\n" + " -p load values from file", + .extra = "", +}; + +auto key_to_path(std::string_view key) -> std::string { + std::string path = "/proc/sys/"; + for (auto c : key) { + if (c == '.') path += '/'; + else path += c; + } + return path; +} + +auto path_to_key(std::string_view path) -> std::string { + // Strip /proc/sys/ prefix and convert / to . + const char* prefix = "/proc/sys/"; + if (path.size() > 10 && path.substr(0, 10) == prefix) + path = path.substr(10); + std::string key; + for (auto c : path) { + if (c == '/') key += '.'; + else key += c; + } + return key; +} + +auto read_sysctl_value(const std::string& path) -> std::string { + std::ifstream f(path); + if (!f) return {}; + std::string val; + std::getline(f, val); + return val; +} + +auto write_sysctl_value(const std::string& path, std::string_view value) -> bool { + std::ofstream f(path, std::ios::trunc); + if (!f) return false; + f << value << "\n"; + return static_cast(f); +} + +auto show_key(std::string_view key, bool no_name) -> bool { + auto path = key_to_path(key); + auto val = read_sysctl_value(path); + if (val.empty()) return false; + if (no_name) std::printf("%s\n", val.c_str()); + else std::printf("%.*s = %s\n", static_cast(key.size()), key.data(), val.c_str()); + return true; +} + +auto show_all(bool no_name) -> void { + std::error_code ec; + for (const auto& entry : std::filesystem::recursive_directory_iterator("/proc/sys", ec)) { + if (!entry.is_regular_file()) continue; + auto key = path_to_key(entry.path().string()); + auto val = read_sysctl_value(entry.path().string()); + if (val.empty()) continue; + if (no_name) std::printf("%s\n", val.c_str()); + else std::printf("%s = %s\n", key.c_str(), val.c_str()); + } +} + +auto load_file(const std::string& filepath, bool no_name) -> int { + std::ifstream f(filepath); + if (!f) { + std::fprintf(stderr, "cfbox sysctl: cannot open %s\n", filepath.c_str()); + return 1; + } + + int errors = 0; + std::string line; + while (std::getline(f, line)) { + // Skip comments and empty lines + if (line.empty() || line[0] == '#' || line[0] == ';') continue; + auto eq = line.find('='); + if (eq == std::string::npos) continue; + auto key = line.substr(0, eq); + auto val = line.substr(eq + 1); + // Trim whitespace + while (!key.empty() && (key.back() == ' ' || key.back() == '\t')) key.pop_back(); + while (!val.empty() && (val.front() == ' ' || val.front() == '\t')) val.erase(val.begin()); + + auto path = key_to_path(key); + if (!write_sysctl_value(path, val)) { + std::fprintf(stderr, "cfbox sysctl: cannot set %s\n", key.c_str()); + ++errors; + } else if (!no_name) { + std::printf("%s = %s\n", key.c_str(), val.c_str()); + } + } + return errors > 0 ? 1 : 0; +} + +} // anonymous namespace + +auto sysctl_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'a', false, "all"}, + cfbox::args::OptSpec{'n', false}, + cfbox::args::OptSpec{'w', false}, + cfbox::args::OptSpec{'e', false}, + cfbox::args::OptSpec{'p', true, "load"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + bool no_name = parsed.has('n'); + bool do_write = parsed.has('w'); + + if (parsed.has('a') || parsed.has_long("all")) { + show_all(no_name); + return 0; + } + + if (auto file = parsed.get_any('p', "load")) { + return load_file(std::string(*file), no_name); + } + + const auto& pos = parsed.positional(); + if (pos.empty()) { + // Default: show all (like sysctl without args on some systems) + show_all(no_name); + return 0; + } + + int errors = 0; + for (const auto& arg : pos) { + auto s = std::string(arg); + if (do_write) { + auto eq = s.find('='); + if (eq == std::string::npos) { + std::fprintf(stderr, "cfbox sysctl: invalid setting: %s\n", s.c_str()); + ++errors; + continue; + } + auto key = s.substr(0, eq); + auto val = s.substr(eq + 1); + auto path = key_to_path(key); + if (!write_sysctl_value(path, val)) { + std::fprintf(stderr, "cfbox sysctl: cannot set %s\n", key.c_str()); + ++errors; + } else if (!no_name) { + std::printf("%s = %s\n", key.c_str(), val.c_str()); + } + } else { + if (!show_key(s, no_name)) { + std::fprintf(stderr, "cfbox sysctl: cannot stat %s\n", s.c_str()); + ++errors; + } + } + } + + return errors > 0 ? 1 : 0; +} diff --git a/src/applets/top/top.hpp b/src/applets/top/top.hpp new file mode 100644 index 0000000..f20a6f0 --- /dev/null +++ b/src/applets/top/top.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace top { + +enum class SortField { + Cpu, Mem, Pid, Time +}; + +struct ProcEntry { + pid_t pid = 0; + std::string user; + int priority = 0; + int nice_val = 0; + std::uint64_t vsize = 0; // bytes + std::uint64_t rss_kb = 0; // KB + char state = '?'; + double cpu_pct = 0.0; + double mem_pct = 0.0; + std::uint64_t total_time = 0; // ticks + std::string command; + std::string tty; +}; + +inline auto uid_to_name(uid_t uid) -> std::string { + // Simple approach: just return the uid as string + // Full implementation would read /etc/passwd + return std::to_string(uid); +} + +inline auto build_entries(std::uint64_t total_mem_kb) -> std::vector { + auto result = cfbox::proc::read_all_processes(); + if (!result) return {}; + + std::vector entries; + entries.reserve(result->size()); + + for (const auto& p : *result) { + ProcEntry e; + e.pid = p.pid; + e.user = uid_to_name(p.uid); + e.priority = p.priority; + e.nice_val = p.nice_val; + e.vsize = p.vsize; + e.rss_kb = p.rss * static_cast(cfbox::proc::page_size()) / 1024; + e.state = p.state; + e.total_time = p.utime + p.stime; + e.tty = p.tty; + + // Build command string + if (!p.cmdline.empty()) { + e.command = p.cmdline[0]; + // Truncate long commands + if (e.command.size() > 30) { + e.command.resize(27); + e.command += "..."; + } + } else { + e.command = "[" + p.comm + "]"; + } + + // CPU% and MEM% will be calculated by sort logic + if (total_mem_kb > 0) { + e.mem_pct = 100.0 * static_cast(e.rss_kb) / static_cast(total_mem_kb); + } + + entries.push_back(std::move(e)); + } + return entries; +} + +inline auto sort_entries(std::vector& entries, SortField field) -> void { + switch (field) { + case SortField::Cpu: + std::sort(entries.begin(), entries.end(), + [](const auto& a, const auto& b) { return a.cpu_pct > b.cpu_pct; }); + break; + case SortField::Mem: + std::sort(entries.begin(), entries.end(), + [](const auto& a, const auto& b) { return a.rss_kb > b.rss_kb; }); + break; + case SortField::Pid: + std::sort(entries.begin(), entries.end(), + [](const auto& a, const auto& b) { return a.pid < b.pid; }); + break; + case SortField::Time: + std::sort(entries.begin(), entries.end(), + [](const auto& a, const auto& b) { return a.total_time > b.total_time; }); + break; + } +} + +} // namespace top diff --git a/src/applets/top/top_main.cpp b/src/applets/top/top_main.cpp new file mode 100644 index 0000000..99c1861 --- /dev/null +++ b/src/applets/top/top_main.cpp @@ -0,0 +1,262 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "top.hpp" + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "top", + .version = CFBOX_VERSION_STRING, + .one_line = "display Linux processes", + .usage = "top [-d DELAY]", + .options = " -d N delay between updates in seconds (default 3)\n" + " -b batch mode (non-interactive)", + .extra = "Keys: q=quit M=sort MEM P=sort CPU T=sort TIME N=sort PID", +}; + +class TopApp : public cfbox::tui::TuiApp { + top::SortField sort_ = top::SortField::Cpu; + int delay_sec_ = 3; + bool batch_ = false; + int iteration_ = 0; + cfbox::proc::CpuStats prev_cpu_{}; + std::vector entries_; +public: + explicit TopApp(int delay, bool batch) + : TuiApp(delay * 1000), delay_sec_(delay), batch_(batch) { + prev_cpu_ = cfbox::proc::read_cpu_stats().value_or(cfbox::proc::CpuStats{}); + } + + auto on_key(cfbox::tui::Key k) -> bool override { + if (k.type == cfbox::tui::KeyType::Escape || k.type == cfbox::tui::KeyType::Ctrl_C) + return false; + if (k.is_char()) { + switch (static_cast(k.ch)) { + case 'q': return false; + case 'M': sort_ = top::SortField::Mem; break; + case 'P': sort_ = top::SortField::Cpu; break; + case 'T': sort_ = top::SortField::Time; break; + case 'N': sort_ = top::SortField::Pid; break; + } + } + return true; + } + + auto on_tick() -> void override { + auto mem = cfbox::proc::read_meminfo(); + auto total_kb = mem ? mem->total : 1; + entries_ = top::build_entries(total_kb); + + auto cpu = cfbox::proc::read_cpu_stats(); + if (cpu) { + double d_total = static_cast(cpu->total() - prev_cpu_.total()); + if (d_total > 0) { + double ticks = static_cast(cfbox::proc::clock_ticks_per_second()); + for (auto& e : entries_) { + e.cpu_pct = static_cast(e.total_time) * ticks / d_total * 100.0; + } + } + prev_cpu_ = *cpu; + } + + top::sort_entries(entries_, sort_); + ++iteration_; + } + + auto on_resize(int /*rows*/, int /*cols*/) -> void override { + // Nothing extra needed — render() uses screen buffer + } + + auto sort_field_char() const -> char { + switch (sort_) { + case top::SortField::Cpu: return 'P'; + case top::SortField::Mem: return 'M'; + case top::SortField::Pid: return 'N'; + case top::SortField::Time: return 'T'; + } + return 'P'; + } + + auto run_batch(int count) -> int { + for (int i = 0; i < count || count <= 0; ++i) { + on_tick(); + render_to_stdout(); + } + return 0; + } + + auto render_to_stdout() -> void { + auto mem = cfbox::proc::read_meminfo(); + auto up = cfbox::proc::read_uptime(); + auto la = cfbox::proc::read_loadavg(); + if (up && la) { + auto secs = static_cast(up->first); + auto hrs = secs / 3600; + auto mins = (secs % 3600) / 60; + std::printf("top - up %ld:%02ld, load average: %.2f, %.2f, %.2f\n", + hrs, mins, la->avg1, la->avg5, la->avg15); + } + + std::printf("Tasks: %zu total\n", entries_.size()); + + auto cpu = cfbox::proc::read_cpu_stats(); + if (cpu) { + double t = static_cast(cpu->total()); + if (t > 0) { + std::printf("%%Cpu: %.1f us, %.1f sy, %.1f id\n", + 100.0 * static_cast(cpu->user) / t, + 100.0 * static_cast(cpu->system) / t, + 100.0 * static_cast(cpu->idle) / t); + } + } + + if (mem) { + std::printf("MiB Mem: %llu total, %llu free, %llu used, %llu cache\n", + static_cast(mem->total / 1024), + static_cast(mem->free / 1024), + static_cast((mem->total - mem->available) / 1024), + static_cast(mem->cached / 1024)); + } + + std::printf(" PID USER PR NI VIRT RES %%CPU %%MEM S COMMAND\n"); + + for (const auto& e : entries_) { + std::printf("%5d %-8s %3d %3d %6lluM %5lluM %5.1f %5.1f %c %s\n", + e.pid, + e.user.c_str(), + e.priority, + e.nice_val, + static_cast(e.vsize / (1024 * 1024)), + static_cast(e.rss_kb / 1024), + e.cpu_pct, + e.mem_pct, + e.state, + e.command.c_str()); + } + } + + auto render_to_buffer(cfbox::tui::ScreenBuffer& scr) -> void { + scr.clear(); + int row = 0; + + // Header: uptime and load + auto up = cfbox::proc::read_uptime(); + auto la = cfbox::proc::read_loadavg(); + if (up && la) { + auto secs = static_cast(up->first); + auto mins = secs / 60; + auto hrs = mins / 60; + char buf[128]; + std::snprintf(buf, sizeof(buf), + "top - up %ld:%02ld, load average: %.2f, %.2f, %.2f", + hrs, mins % 60, la->avg1, la->avg5, la->avg15); + scr.set_string(row, 0, buf); + } + ++row; + + // Tasks summary + char task_buf[128]; + std::snprintf(task_buf, sizeof(task_buf), + "Tasks: %zu total", entries_.size()); + scr.set_string(row, 0, task_buf); + ++row; + + // CPU summary + auto cpu = cfbox::proc::read_cpu_stats(); + if (cpu) { + char cpu_buf[128]; + auto t = static_cast(cpu->total()); + if (t > 0) { + std::snprintf(cpu_buf, sizeof(cpu_buf), + "%%Cpu: %.1f us, %.1f sy, %.1f id", + 100.0 * static_cast(cpu->user) / t, + 100.0 * static_cast(cpu->system) / t, + 100.0 * static_cast(cpu->idle) / t); + scr.set_string(row, 0, cpu_buf); + } + } + ++row; + + // Memory summary + auto mem = cfbox::proc::read_meminfo(); + if (mem) { + char mem_buf[128]; + std::snprintf(mem_buf, sizeof(mem_buf), + "MiB Mem: %llu total, %llu free, %llu used, %llu cache", + static_cast(mem->total / 1024), + static_cast(mem->free / 1024), + static_cast((mem->total - mem->available) / 1024), + static_cast(mem->cached / 1024)); + scr.set_string(row, 0, mem_buf); + } + ++row; + + // Column headers + const char* header = " PID USER PR NI VIRT RES %%CPU %%MEM S COMMAND"; + scr.set_string(row, 0, header, false, true); + ++row; + + // Process rows + auto rows = scr.rows(); + for (size_t i = 0; i < entries_.size() && row < rows - 1; ++i) { + const auto& e = entries_[i]; + char line[256]; + std::snprintf(line, sizeof(line), + "%5d %-8s %3d %3d %6lluM %5lluM %5.1f %5.1f %c %s", + e.pid, + e.user.c_str(), + e.priority, + e.nice_val, + static_cast(e.vsize / (1024 * 1024)), + static_cast(e.rss_kb / 1024), + e.cpu_pct, + e.mem_pct, + e.state, + e.command.c_str()); + scr.set_string(row, 0, line); + ++row; + } + + // Footer + if (row < rows) { + char foot[64]; + std::snprintf(foot, sizeof(foot), "Sort: %c q=quit M/P/T/N=sort", sort_field_char()); + scr.set_string(rows - 1, 0, foot, false, true); + } + } +}; + +} // anonymous namespace + +auto top_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'d', true, "delay"}, + cfbox::args::OptSpec{'b', false, "batch"}, + cfbox::args::OptSpec{'n', true, "iterations"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + int delay = 3; + bool batch = parsed.has('b') || parsed.has_long("batch"); + int iterations = 0; + if (auto v = parsed.get('d')) delay = std::stoi(std::string(*v)); + if (auto v = parsed.get('n')) iterations = std::stoi(std::string(*v)); + if (delay < 1) delay = 1; + + if (batch) { + TopApp app(delay, true); + return app.run_batch(iterations); + } + + TopApp app(delay, false); + return app.run(); +} diff --git a/src/applets/uptime.cpp b/src/applets/uptime.cpp new file mode 100644 index 0000000..f3d28e1 --- /dev/null +++ b/src/applets/uptime.cpp @@ -0,0 +1,73 @@ +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "uptime", + .version = CFBOX_VERSION_STRING, + .one_line = "tell how long the system has been running", + .usage = "uptime", + .options = "", + .extra = "", +}; + +auto format_duration(double seconds) -> std::string { + auto total_secs = static_cast(seconds); + auto days = total_secs / 86400; + auto hours = (total_secs % 86400) / 3600; + auto mins = (total_secs % 3600) / 60; + + char buf[64]; + if (days > 0) { + std::snprintf(buf, sizeof(buf), "%ld day%s, %02ld:%02ld", + days, days > 1 ? "s" : "", hours, mins); + } else { + std::snprintf(buf, sizeof(buf), "%02ld:%02ld", hours, mins); + } + return buf; +} + +} // anonymous namespace + +auto uptime_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, {}); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + // Current time + auto now = std::time(nullptr); + auto tm = std::localtime(&now); + char time_buf[16]; + std::strftime(time_buf, sizeof(time_buf), " %H:%M:%S", tm); + + // Uptime + auto up_result = cfbox::proc::read_uptime(); + if (!up_result) { + std::fprintf(stderr, "cfbox uptime: %s\n", up_result.error().msg.c_str()); + return 1; + } + double uptime_secs = up_result->first; + + // Load average + auto la_result = cfbox::proc::read_loadavg(); + double avg1 = 0, avg5 = 0, avg15 = 0; + if (la_result) { + avg1 = la_result->avg1; + avg5 = la_result->avg5; + avg15 = la_result->avg15; + } + + std::printf("%s up %s, load average: %.2f, %.2f, %.2f\n", + time_buf, + format_duration(uptime_secs).c_str(), + avg1, avg5, avg15); + + return 0; +} diff --git a/src/applets/watch.cpp b/src/applets/watch.cpp new file mode 100644 index 0000000..21acfc1 --- /dev/null +++ b/src/applets/watch.cpp @@ -0,0 +1,145 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr cfbox::help::HelpEntry HELP = { + .name = "watch", + .version = CFBOX_VERSION_STRING, + .one_line = "execute a program periodically", + .usage = "watch [-n SECS] COMMAND [ARGS...]", + .options = " -n N seconds between updates (default 2)", + .extra = "", +}; + +auto run_command(const std::vector& cmd) -> std::string { + int pipefd[2]; + if (pipe(pipefd) != 0) return "(pipe failed)\n"; + + auto pid = fork(); + if (pid < 0) { + close(pipefd[0]); + close(pipefd[1]); + return "(fork failed)\n"; + } + + if (pid == 0) { + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + dup2(pipefd[1], STDERR_FILENO); + close(pipefd[1]); + + std::vector argv; + for (auto& a : cmd) argv.push_back(const_cast(a.c_str())); + argv.push_back(nullptr); + + execvp(argv[0], argv.data()); + _exit(127); + } + + close(pipefd[1]); + std::string output; + char buf[4096]; + ssize_t n; + while ((n = read(pipefd[0], buf, sizeof(buf))) > 0) { + output.append(buf, static_cast(n)); + } + close(pipefd[0]); + + int status = 0; + waitpid(pid, &status, 0); + return output; +} + +auto format_time() -> std::string { + auto now = std::time(nullptr); + auto tm = std::localtime(&now); + char buf[32]; + std::strftime(buf, sizeof(buf), "%H:%M:%S", tm); + return buf; +} + +} // anonymous namespace + +auto watch_main(int argc, char* argv[]) -> int { + auto parsed = cfbox::args::parse(argc, argv, { + cfbox::args::OptSpec{'n', true, "interval"}, + }); + if (parsed.has_long("help")) { cfbox::help::print_help(HELP); return 0; } + if (parsed.has_long("version")) { cfbox::help::print_version(HELP); return 0; } + + int interval = 2; + if (auto v = parsed.get('n')) interval = std::stoi(std::string(*v)); + if (interval < 1) interval = 1; + + const auto& pos = parsed.positional(); + if (pos.empty()) { + std::fprintf(stderr, "cfbox watch: no command specified\n"); + return 1; + } + + std::vector cmd; + for (const auto& a : pos) cmd.emplace_back(a); + + std::string cmd_str; + for (size_t i = 0; i < cmd.size(); ++i) { + if (i > 0) cmd_str += ' '; + cmd_str += cmd[i]; + } + + cfbox::terminal::RawMode raw_mode; + + while (true) { + auto output = run_command(cmd); + auto [rows, cols] = cfbox::terminal::get_size(); + + cfbox::terminal::clear_screen(); + cfbox::terminal::move_cursor(1, 1); + cfbox::terminal::invert_video(true); + std::printf("Every %ds: %s %s", interval, cmd_str.c_str(), format_time().c_str()); + cfbox::terminal::clear_line(); + cfbox::terminal::invert_video(false); + + int line = 2; + int col = 0; + for (size_t i = 0; i < output.size() && line <= rows; ++i) { + auto ch = output[i]; + if (ch == '\n') { + cfbox::terminal::move_cursor(line, col + 1); + cfbox::terminal::clear_line(); + ++line; + col = 0; + } else if (ch == '\r') { + col = 0; + } else if (col < cols) { + cfbox::terminal::move_cursor(line, col + 1); + std::putchar(ch); + ++col; + } + } + std::fflush(stdout); + + for (int waited = 0; waited < interval * 1000; waited += 100) { + auto key = cfbox::tui::read_key(0, 100); + if (key && (key->type == cfbox::tui::KeyType::Ctrl_C || + key->type == cfbox::tui::KeyType::Escape || + (key->is_char() && key->ch == 'q'))) { + cfbox::terminal::clear_screen(); + return 0; + } + } + } +} diff --git a/tests/unit/test_cal.cpp b/tests/unit/test_cal.cpp new file mode 100644 index 0000000..182454a --- /dev/null +++ b/tests/unit/test_cal.cpp @@ -0,0 +1,56 @@ +#include +#include + +#if CFBOX_ENABLE_CAL +#include + +TEST(CalTest, HelpAndVersion) { + char a0[] = "cal"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = cal_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("cal"), std::string::npos); +} + +TEST(CalTest, CurrentMonth) { + char a0[] = "cal"; + char* argv[] = {a0, nullptr}; + + testing::internal::CaptureStdout(); + int rc = cal_main(1, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("Su Mo Tu We Th Fr Sa"), std::string::npos); +} + +TEST(CalTest, SpecificMonth) { + char a0[] = "cal"; + char a1[] = "1"; + char a2[] = "2025"; + char* argv[] = {a0, a1, a2, nullptr}; + + testing::internal::CaptureStdout(); + int rc = cal_main(3, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("January 2025"), std::string::npos); +} + +TEST(CalTest, WholeYear) { + char a0[] = "cal"; + char a1[] = "-y"; + char a2[] = "2025"; + char* argv[] = {a0, a1, a2, nullptr}; + + testing::internal::CaptureStdout(); + int rc = cal_main(3, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("January 2025"), std::string::npos); + EXPECT_NE(out.find("December 2025"), std::string::npos); +} +#endif diff --git a/tests/unit/test_capture.hpp b/tests/unit/test_capture.hpp index 2e53a9a..ab8e5f3 100644 --- a/tests/unit/test_capture.hpp +++ b/tests/unit/test_capture.hpp @@ -48,7 +48,7 @@ struct TempDir { // Write a file inside the temp dir auto write_file(const std::string& name, const std::string& content) const -> std::string { auto fp = path / name; - cfbox::io::write_all(fp.string(), content); + static_cast(cfbox::io::write_all(fp.string(), content)); return fp.string(); } diff --git a/tests/unit/test_dmesg.cpp b/tests/unit/test_dmesg.cpp new file mode 100644 index 0000000..63b3e63 --- /dev/null +++ b/tests/unit/test_dmesg.cpp @@ -0,0 +1,18 @@ +#include +#include + +#if CFBOX_ENABLE_DMESG +#include + +TEST(DmesgTest, HelpAndVersion) { + char a0[] = "dmesg"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = dmesg_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("dmesg"), std::string::npos); +} +#endif diff --git a/tests/unit/test_free.cpp b/tests/unit/test_free.cpp new file mode 100644 index 0000000..d4ca362 --- /dev/null +++ b/tests/unit/test_free.cpp @@ -0,0 +1,26 @@ +#include +#include + +#if CFBOX_ENABLE_FREE +TEST(FreeTest, RunsAndOutputs) { + char a0[] = "free"; + char* argv[] = {a0, nullptr}; + testing::internal::CaptureStdout(); + int rc = free_main(1, argv); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(output.find("total"), std::string::npos); + EXPECT_NE(output.find("Mem:"), std::string::npos); +} + +TEST(FreeTest, HumanFlag) { + char a0[] = "free"; + char a1[] = "-h"; + char* argv[] = {a0, a1, nullptr}; + testing::internal::CaptureStdout(); + int rc = free_main(2, argv); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(output.find("total"), std::string::npos); +} +#endif diff --git a/tests/unit/test_fuser.cpp b/tests/unit/test_fuser.cpp new file mode 100644 index 0000000..290c335 --- /dev/null +++ b/tests/unit/test_fuser.cpp @@ -0,0 +1,41 @@ +#include +#include + +#if CFBOX_ENABLE_FUSER +#include + +TEST(FuserTest, HelpAndVersion) { + char a0[] = "fuser"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = fuser_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("fuser"), std::string::npos); +} + +TEST(FuserTest, NoArgs) { + char a0[] = "fuser"; + char* argv[] = {a0, nullptr}; + + testing::internal::CaptureStdout(); + testing::internal::CaptureStderr(); + int rc = fuser_main(1, argv); + testing::internal::GetCapturedStdout(); + testing::internal::GetCapturedStderr(); + EXPECT_EQ(rc, 1); +} + +TEST(FuserTest, NonExistentFile) { + char a0[] = "fuser"; + char a1[] = "/nonexistent_file_xyz"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = fuser_main(2, argv); + // Should fail to stat or find no processes + EXPECT_NE(rc, 0); +} +#endif diff --git a/tests/unit/test_hexdump.cpp b/tests/unit/test_hexdump.cpp new file mode 100644 index 0000000..83bbc1f --- /dev/null +++ b/tests/unit/test_hexdump.cpp @@ -0,0 +1,31 @@ +#include +#include + +#if CFBOX_ENABLE_HEXDUMP +#include + +TEST(HexdumpTest, HelpAndVersion) { + char a0[] = "hexdump"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = hexdump_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("hexdump"), std::string::npos); +} + +TEST(HexdumpTest, CanonicalStdin) { + char a0[] = "hexdump"; + char a1[] = "-C"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = hexdump_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + // Empty stdin produces no output + EXPECT_TRUE(out.empty()); +} +#endif diff --git a/tests/unit/test_init_inittab.cpp b/tests/unit/test_init_inittab.cpp new file mode 100644 index 0000000..6803ac3 --- /dev/null +++ b/tests/unit/test_init_inittab.cpp @@ -0,0 +1,90 @@ +#include +#include +#include + +// Inline the inittab parser for unit testing (it's in src/applets/init/ which +// is not on the include path for tests). The actual implementation is in +// init_inittab.cpp and tested via integration tests. + +namespace { + +struct InittabEntry { + std::string id; + std::string runlevels; + std::string action; + std::string process; +}; + +auto parse_inittab_line(std::string_view line) -> InittabEntry { + InittabEntry entry; + if (line.empty() || line[0] == '#') return entry; + + auto c1 = line.find(':'); + if (c1 == std::string_view::npos) return entry; + auto c2 = line.find(':', c1 + 1); + if (c2 == std::string_view::npos) return entry; + auto c3 = line.find(':', c2 + 1); + if (c3 == std::string_view::npos) return entry; + + entry.id = std::string(line.substr(0, c1)); + entry.runlevels = std::string(line.substr(c1 + 1, c2 - c1 - 1)); + entry.action = std::string(line.substr(c2 + 1, c3 - c2 - 1)); + entry.process = std::string(line.substr(c3 + 1)); + + while (!entry.process.empty() && (entry.process.back() == ' ' || entry.process.back() == '\t' || entry.process.back() == '\r')) + entry.process.pop_back(); + + return entry; +} + +} // anonymous namespace + +TEST(InittabTest, ParseFullLine) { + auto entry = parse_inittab_line("tty1::respawn:/bin/sh"); + EXPECT_EQ(entry.id, "tty1"); + EXPECT_EQ(entry.runlevels, ""); + EXPECT_EQ(entry.action, "respawn"); + EXPECT_EQ(entry.process, "/bin/sh"); +} + +TEST(InittabTest, ParseWithRunlevels) { + auto entry = parse_inittab_line("id:3:once:/usr/bin/foo"); + EXPECT_EQ(entry.id, "id"); + EXPECT_EQ(entry.runlevels, "3"); + EXPECT_EQ(entry.action, "once"); + EXPECT_EQ(entry.process, "/usr/bin/foo"); +} + +TEST(InittabTest, ParseSysinit) { + auto entry = parse_inittab_line("::sysinit:/bin/mount -a"); + EXPECT_EQ(entry.id, ""); + EXPECT_EQ(entry.action, "sysinit"); + EXPECT_EQ(entry.process, "/bin/mount -a"); +} + +TEST(InittabTest, ParseComment) { + auto entry = parse_inittab_line("# this is a comment"); + EXPECT_TRUE(entry.action.empty()); +} + +TEST(InittabTest, ParseEmpty) { + auto entry = parse_inittab_line(""); + EXPECT_TRUE(entry.action.empty()); +} + +TEST(InittabTest, ParseTrimsWhitespace) { + auto entry = parse_inittab_line("id::once:/bin/true "); + EXPECT_EQ(entry.process, "/bin/true"); +} + +TEST(InittabTest, ParseCtrlAltDel) { + auto entry = parse_inittab_line("::ctrlaltdel:/sbin/reboot"); + EXPECT_EQ(entry.action, "ctrlaltdel"); + EXPECT_EQ(entry.process, "/sbin/reboot"); +} + +TEST(InittabTest, ParseShutdown) { + auto entry = parse_inittab_line("::shutdown:/bin/umount -a"); + EXPECT_EQ(entry.action, "shutdown"); + EXPECT_EQ(entry.process, "/bin/umount -a"); +} diff --git a/tests/unit/test_io.cpp b/tests/unit/test_io.cpp index e980d04..1f48a60 100644 --- a/tests/unit/test_io.cpp +++ b/tests/unit/test_io.cpp @@ -53,7 +53,7 @@ TEST_F(IOTest, WriteNonexistentDir) { TEST_F(IOTest, SplitLines) { std::string path = test_file("lines.txt"); - write_all(path, "line1\nline2\nline3"); + static_cast(write_all(path, "line1\nline2\nline3")); auto r = read_all(path); ASSERT_TRUE(r.has_value()); @@ -67,7 +67,7 @@ TEST_F(IOTest, SplitLines) { TEST_F(IOTest, SplitLinesTrailingNewline) { std::string path = test_file("trailing.txt"); - write_all(path, "a\nb\n"); + static_cast(write_all(path, "a\nb\n")); auto r = read_all(path); ASSERT_TRUE(r.has_value()); @@ -80,7 +80,7 @@ TEST_F(IOTest, SplitLinesTrailingNewline) { TEST_F(IOTest, ReadLines) { std::string path = test_file("readlines.txt"); - write_all(path, "foo\nbar"); + static_cast(write_all(path, "foo\nbar")); auto lines = read_lines(path); ASSERT_TRUE(lines.has_value()); @@ -91,7 +91,7 @@ TEST_F(IOTest, ReadLines) { TEST_F(IOTest, EmptyFile) { std::string path = test_file("empty.txt"); - write_all(path, ""); + static_cast(write_all(path, "")); auto r = read_all(path); ASSERT_TRUE(r.has_value()); diff --git a/tests/unit/test_iostat.cpp b/tests/unit/test_iostat.cpp new file mode 100644 index 0000000..8beab18 --- /dev/null +++ b/tests/unit/test_iostat.cpp @@ -0,0 +1,29 @@ +#include +#include + +#if CFBOX_ENABLE_IOSTAT +#include + +TEST(IostatTest, HelpAndVersion) { + char a0[] = "iostat"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = iostat_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("iostat"), std::string::npos); +} + +TEST(IostatTest, SingleRun) { + char a0[] = "iostat"; + char* argv[] = {a0, nullptr}; + + testing::internal::CaptureStdout(); + int rc = iostat_main(1, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("Device"), std::string::npos); +} +#endif diff --git a/tests/unit/test_kill.cpp b/tests/unit/test_kill.cpp new file mode 100644 index 0000000..5486483 --- /dev/null +++ b/tests/unit/test_kill.cpp @@ -0,0 +1,26 @@ +#include +#include + +#if CFBOX_ENABLE_KILL +TEST(KillTest, ListSignals) { + char a0[] = "kill"; + char a1[] = "-l"; + char* argv[] = {a0, a1, nullptr}; + testing::internal::CaptureStdout(); + int rc = kill_main(2, argv); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(output.find("TERM"), std::string::npos); + EXPECT_NE(output.find("KILL"), std::string::npos); +} + +TEST(KillTest, NoPidError) { + char a0[] = "kill"; + char* argv[] = {a0, nullptr}; + testing::internal::CaptureStderr(); + int rc = kill_main(1, argv); + std::string err = testing::internal::GetCapturedStderr(); + EXPECT_NE(rc, 0); + EXPECT_NE(err.find("no PID"), std::string::npos); +} +#endif diff --git a/tests/unit/test_more.cpp b/tests/unit/test_more.cpp new file mode 100644 index 0000000..7b751f0 --- /dev/null +++ b/tests/unit/test_more.cpp @@ -0,0 +1,18 @@ +#include +#include + +#if CFBOX_ENABLE_MORE +#include + +TEST(MoreTest, HelpAndVersion) { + char a0[] = "more"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = more_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("more"), std::string::npos); +} +#endif diff --git a/tests/unit/test_pmap.cpp b/tests/unit/test_pmap.cpp new file mode 100644 index 0000000..f3eb7f8 --- /dev/null +++ b/tests/unit/test_pmap.cpp @@ -0,0 +1,44 @@ +#include +#include + +#if CFBOX_ENABLE_PMAP +#include + +TEST(PmapTest, HelpAndVersion) { + char a0[] = "pmap"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = pmap_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("pmap"), std::string::npos); +} + +TEST(PmapTest, NoArgs) { + char a0[] = "pmap"; + char* argv[] = {a0, nullptr}; + + testing::internal::CaptureStdout(); + testing::internal::CaptureStderr(); + int rc = pmap_main(1, argv); + testing::internal::GetCapturedStdout(); + testing::internal::GetCapturedStderr(); + EXPECT_EQ(rc, 1); +} + +TEST(PmapTest, SelfMaps) { + char a0[] = "pmap"; + char a1[] = "1"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = pmap_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + // PID 1 may not exist in all test environments + if (rc == 0) { + EXPECT_FALSE(out.empty()); + } +} +#endif diff --git a/tests/unit/test_proc.cpp b/tests/unit/test_proc.cpp new file mode 100644 index 0000000..800a15b --- /dev/null +++ b/tests/unit/test_proc.cpp @@ -0,0 +1,110 @@ +#include +#include + +TEST(ProcTest, ReadMeminfo) { + auto result = cfbox::proc::read_meminfo(); + ASSERT_TRUE(result.has_value()) << result.error().msg; + const auto& mi = *result; + EXPECT_GT(mi.total, 0u); + EXPECT_GT(mi.free, 0u); + EXPECT_LE(mi.free, mi.total); + EXPECT_LE(mi.available, mi.total); + EXPECT_LE(mi.swap_free, mi.swap_total); +} + +TEST(ProcTest, ReadCpuStats) { + auto result = cfbox::proc::read_cpu_stats(); + ASSERT_TRUE(result.has_value()) << result.error().msg; + const auto& cs = *result; + EXPECT_GT(cs.total(), 0u); + EXPECT_GT(cs.idle_time(), 0u); + EXPECT_LE(cs.idle_time(), cs.total()); +} + +TEST(ProcTest, ClockTicksPerSecond) { + auto ticks = cfbox::proc::clock_ticks_per_second(); + EXPECT_GT(ticks, 0); +} + +TEST(ProcTest, TotalMemoryKb) { + auto mem = cfbox::proc::total_memory_kb(); + EXPECT_GT(mem, 0u); +} + +TEST(ProcTest, ReadLoadavg) { + auto result = cfbox::proc::read_loadavg(); + ASSERT_TRUE(result.has_value()) << result.error().msg; + const auto& la = *result; + EXPECT_GE(la.avg1, 0.0); + EXPECT_GE(la.avg5, 0.0); + EXPECT_GE(la.avg15, 0.0); + EXPECT_GE(la.running, 0); + EXPECT_GE(la.total, 0); +} + +TEST(ProcTest, ReadUptime) { + auto result = cfbox::proc::read_uptime(); + ASSERT_TRUE(result.has_value()) << result.error().msg; + auto [up, idle] = *result; + EXPECT_GT(up, 0.0); + EXPECT_GE(idle, 0.0); +} + +TEST(ProcTest, ReadVersion) { + auto result = cfbox::proc::read_version(); + ASSERT_TRUE(result.has_value()) << result.error().msg; + EXPECT_FALSE(result->empty()); + // Should contain "Linux" + EXPECT_NE(result->find("Linux"), std::string::npos) + << "version: " << *result; +} + +TEST(ProcTest, ReadMounts) { + auto result = cfbox::proc::read_mounts(); + ASSERT_TRUE(result.has_value()) << result.error().msg; + EXPECT_FALSE(result->empty()); + // At least /proc should be mounted + bool found_proc = false; + for (const auto& m : *result) { + if (m.mountpoint == "/proc") { found_proc = true; break; } + } + EXPECT_TRUE(found_proc) << "/proc not found in mount list"; +} + +TEST(ProcTest, ReadProcessSelf) { + auto result = cfbox::proc::read_process(getpid()); + ASSERT_TRUE(result.has_value()) << result.error().msg; + const auto& pi = *result; + EXPECT_EQ(pi.pid, getpid()); + EXPECT_EQ(pi.uid, getuid()); + EXPECT_EQ(pi.gid, getgid()); + EXPECT_NE(pi.state, '?'); +} + +TEST(ProcTest, ReadAllProcesses) { + auto result = cfbox::proc::read_all_processes(); + ASSERT_TRUE(result.has_value()) << result.error().msg; + EXPECT_FALSE(result->empty()); + // Self should be in the list + bool found_self = false; + for (const auto& p : *result) { + if (p.pid == getpid()) { found_self = true; break; } + } + EXPECT_TRUE(found_self); +} + +TEST(ProcTest, ReadDiskstats) { + auto result = cfbox::proc::read_diskstats(); + // May be empty in some environments (containers), just verify it parses + if (result.has_value()) { + for (const auto& ds : *result) { + EXPECT_FALSE(ds.device.empty()); + } + } +} + +TEST(ProcTest, ReadPartitions) { + auto result = cfbox::proc::read_partitions(); + // May be empty in some environments, just verify no error + EXPECT_TRUE(result.has_value()) << result.error().msg; +} diff --git a/tests/unit/test_ps.cpp b/tests/unit/test_ps.cpp new file mode 100644 index 0000000..97b065e --- /dev/null +++ b/tests/unit/test_ps.cpp @@ -0,0 +1,28 @@ +#include +#include + +#if CFBOX_ENABLE_PS +TEST(PsTest, RunsAndOutputs) { + char a0[] = "ps"; + char* argv[] = {a0, nullptr}; + testing::internal::CaptureStdout(); + int rc = ps_main(1, argv); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(output.find("PID"), std::string::npos); + EXPECT_NE(output.find("CMD"), std::string::npos); +} + +TEST(PsTest, AuxFormat) { + char a0[] = "ps"; + char a1[] = "aux"; + char* argv[] = {a0, a1, nullptr}; + testing::internal::CaptureStdout(); + int rc = ps_main(2, argv); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(output.find("USER"), std::string::npos); + EXPECT_NE(output.find("%CPU"), std::string::npos); + EXPECT_NE(output.find("%MEM"), std::string::npos); +} +#endif diff --git a/tests/unit/test_pstree.cpp b/tests/unit/test_pstree.cpp new file mode 100644 index 0000000..d8a4ee8 --- /dev/null +++ b/tests/unit/test_pstree.cpp @@ -0,0 +1,42 @@ +#include +#include + +#if CFBOX_ENABLE_PSTREE +#include + +TEST(PstreeTest, HelpAndVersion) { + char a0[] = "pstree"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = pstree_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("pstree"), std::string::npos); +} + +TEST(PstreeTest, Runs) { + char a0[] = "pstree"; + char* argv[] = {a0, nullptr}; + + testing::internal::CaptureStdout(); + int rc = pstree_main(1, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_FALSE(out.empty()); +} + +TEST(PstreeTest, ShowPids) { + char a0[] = "pstree"; + char a1[] = "-p"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = pstree_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + // Should contain PID in parentheses + EXPECT_NE(out.find('('), std::string::npos); +} +#endif diff --git a/tests/unit/test_pwdx.cpp b/tests/unit/test_pwdx.cpp new file mode 100644 index 0000000..e419d01 --- /dev/null +++ b/tests/unit/test_pwdx.cpp @@ -0,0 +1,44 @@ +#include +#include + +#if CFBOX_ENABLE_PWDX +#include + +TEST(PwdxTest, HelpAndVersion) { + char a0[] = "pwdx"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = pwdx_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("pwdx"), std::string::npos); +} + +TEST(PwdxTest, NoArgs) { + char a0[] = "pwdx"; + char* argv[] = {a0, nullptr}; + + testing::internal::CaptureStdout(); + testing::internal::CaptureStderr(); + int rc = pwdx_main(1, argv); + testing::internal::GetCapturedStdout(); + testing::internal::GetCapturedStderr(); + EXPECT_EQ(rc, 1); +} + +TEST(PwdxTest, SelfCwd) { + char a0[] = "pwdx"; + char a1[] = "1"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = pwdx_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + // PID 1 may not exist in all test environments; just verify format if it does + if (rc == 0) { + EXPECT_NE(out.find("1:"), std::string::npos); + } +} +#endif diff --git a/tests/unit/test_renice.cpp b/tests/unit/test_renice.cpp new file mode 100644 index 0000000..da61adf --- /dev/null +++ b/tests/unit/test_renice.cpp @@ -0,0 +1,30 @@ +#include +#include + +#if CFBOX_ENABLE_RENICE +#include + +TEST(ReniceTest, HelpAndVersion) { + char a0[] = "renice"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = renice_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("renice"), std::string::npos); +} + +TEST(ReniceTest, NoArgs) { + char a0[] = "renice"; + char* argv[] = {a0, nullptr}; + + testing::internal::CaptureStdout(); + testing::internal::CaptureStderr(); + int rc = renice_main(1, argv); + testing::internal::GetCapturedStdout(); + testing::internal::GetCapturedStderr(); + EXPECT_EQ(rc, 1); +} +#endif diff --git a/tests/unit/test_rev.cpp b/tests/unit/test_rev.cpp new file mode 100644 index 0000000..2265f17 --- /dev/null +++ b/tests/unit/test_rev.cpp @@ -0,0 +1,31 @@ +#include +#include + +#if CFBOX_ENABLE_REV +#include + +TEST(RevTest, HelpAndVersion) { + char a0[] = "rev"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = rev_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("rev"), std::string::npos); +} + +TEST(RevTest, NonExistentFile) { + char a0[] = "rev"; + char a1[] = "/nonexistent_file_xyz"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + testing::internal::CaptureStderr(); + int rc = rev_main(2, argv); + testing::internal::GetCapturedStdout(); + testing::internal::GetCapturedStderr(); + EXPECT_EQ(rc, 1); +} +#endif diff --git a/tests/unit/test_top.cpp b/tests/unit/test_top.cpp new file mode 100644 index 0000000..23a0060 --- /dev/null +++ b/tests/unit/test_top.cpp @@ -0,0 +1,32 @@ +#include +#include + +#if CFBOX_ENABLE_TOP +#include + +TEST(TopTest, HelpAndVersion) { + char a0[] = "top"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = top_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("top"), std::string::npos); +} + +TEST(TopTest, BatchMode) { + char a0[] = "top"; + char a1[] = "-b"; + char a2[] = "-n"; + char a3[] = "1"; + char* argv[] = {a0, a1, a2, a3, nullptr}; + + testing::internal::CaptureStdout(); + int rc = top_main(4, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("PID"), std::string::npos); +} +#endif diff --git a/tests/unit/test_uptime.cpp b/tests/unit/test_uptime.cpp new file mode 100644 index 0000000..31edf7f --- /dev/null +++ b/tests/unit/test_uptime.cpp @@ -0,0 +1,15 @@ +#include +#include + +#if CFBOX_ENABLE_UPTIME +TEST(UptimeTest, RunsAndOutputs) { + char a0[] = "uptime"; + char* argv[] = {a0, nullptr}; + testing::internal::CaptureStdout(); + int rc = uptime_main(1, argv); + std::string output = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(output.find("up"), std::string::npos); + EXPECT_NE(output.find("load average"), std::string::npos); +} +#endif diff --git a/tests/unit/test_watch.cpp b/tests/unit/test_watch.cpp new file mode 100644 index 0000000..f829d9a --- /dev/null +++ b/tests/unit/test_watch.cpp @@ -0,0 +1,30 @@ +#include +#include + +#if CFBOX_ENABLE_WATCH +#include + +TEST(WatchTest, HelpAndVersion) { + char a0[] = "watch"; + char a1[] = "--help"; + char* argv[] = {a0, a1, nullptr}; + + testing::internal::CaptureStdout(); + int rc = watch_main(2, argv); + std::string out = testing::internal::GetCapturedStdout(); + EXPECT_EQ(rc, 0); + EXPECT_NE(out.find("watch"), std::string::npos); +} + +TEST(WatchTest, NoCommand) { + char a0[] = "watch"; + char* argv[] = {a0, nullptr}; + + testing::internal::CaptureStdout(); + testing::internal::CaptureStderr(); + int rc = watch_main(1, argv); + testing::internal::GetCapturedStdout(); + testing::internal::GetCapturedStderr(); + EXPECT_EQ(rc, 1); +} +#endif