Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
# ── Native build + test ───────────────────────────────────────
native:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -50,6 +51,7 @@ jobs:
# ── Release build with size report ───────────────────────────
release-size:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -94,6 +96,7 @@ jobs:
# ── Cross-compilation (aarch64 + armhf) ──────────────────────
cross-compile:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
include:
Expand Down Expand Up @@ -173,6 +176,7 @@ jobs:
qemu-user-test:
needs: cross-compile
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
target: [aarch64, armhf]
Expand Down Expand Up @@ -219,6 +223,7 @@ jobs:
qemu-system-test:
needs: cross-compile
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
matrix:
target: [aarch64, armhf]
Expand Down
8 changes: 5 additions & 3 deletions document/ai/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Tier 3(批级,易变)。单一事实源(批级)。全树见 [ROADMAP.md](ROADMAP.md),铁律见 [DIRECTIVES.md](DIRECTIVES.md)。
> **Phase 1.5 代码质量审查 ✅ 完成**(体积 -14%、消 iostream/stoi、统一错误宏、fs 封装扩展,379 测试全绿)。
> **当前焦点 = Phase 2 核心命令深化** 🔄:tail -f、cp -a、test POSIX、ls -R/--color 为第一批
> **当前焦点 = Phase 2 核心命令深化** 🔄:批1 tail -f ✅(381 测试 / 410 KB);焦点 → 批2 cp -a
> 状态:✅ DONE / 🔄 NEXT / ⏳ PENDING / ⛔ BLOCKED。每批≈一 commit,完成门 `cmake --build build -j$(nproc) && ctest --test-dir build --output-on-failure` 全绿 + `bash tests/integration/run_all.sh`。

## ✅ Phase 1.5(代码质量审查)已完成 — 2026-05-26
Expand All @@ -18,8 +18,8 @@

| 批 | 范围 | 状态 | Commit | 测试 |
|----|------|------|--------|------|
| 批1 | `tail -f`(follow 模式 + inotify/轮询、SIGINT 退出) | 🔄 NEXT | — | |
| 批2 | `cp -a`(归档模式:保权限/属主/时间戳/symlink/递归) | | — | — |
| 批1 | `tail -f/-F`(fd-based follow:fstat 轮询 + 64KiB quantum + -F drain-switch + SIGINT 退出 0) | ✅ | bff34e9 | 381/0 |
| 批2 | `cp -a`(归档模式:保权限/属主/时间戳/symlink/递归) | 🔄 NEXT | — | — |
| 批3 | `test` POSIX 子集(文件测试/字符串/整数/复合表达式,退出码语义) | ⏳ | — | — |
| 批4 | `ls -R` 递归 + `--color`(LS_COLORS 感知、递归缩进) | ⏳ | — | — |
| 批5+ | grep -A/-B/-C、find 布尔表达式、sh 深化(按运维频率排) | ⏳ | — | — |
Expand All @@ -33,6 +33,8 @@
3. **体积回归**:新增 `<filesystem>`/iostream/include 膨胀会撑大 size-opt 二进制(预算 ≤ 550 KB,当前 406 KB);批量改动后跑 `cmake -B build-size -DCMAKE_BUILD_TYPE=Release -DCFBOX_OPTIMIZE_FOR_SIZE=ON && cmake --build build-size -j$(nproc)` + strip 核查。
4. **TOCTOU / symlink**:cp/rm/mv/chmod 的 check-then-act 与 `-R` 跟随 symlink 是安全高危(质量扫描 C 维度);改这些 applet 先看既有防御。
5. **流式 vs 全量**:大文件优先 `cfbox::io::for_each_line()`;滥用 `read_all()` 会内存爆炸(grep/cat/wc 已流式化,参考)。
6. **multi-call binary 全局状态**:装 signal handler / 改进程全局状态的 applet 必须 RAII 恢复(如 tail -f 的 sigaction guard),否则污染同进程后续 applet 调用。
7. **底层 fd 操作不走 fs_util**:follow/字节流消费等需 `fstat`/`lseek`/`read` 的场景用 `<sys/stat.h>` raw POSIX(fs_util::status 拉 `std::filesystem` 撑体积);公共 fs 封装是高层路径操作。

## 回到仓库
`/resume`(读本文件 + `git log --oneline -15`)。Codex 等价粘贴 prompt 见 [prompts.md](prompts.md)。
46 changes: 46 additions & 0 deletions document/notes/2026-06-17-tail-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# tail -f follow 模式(2026-06-17,Phase 2 批1)

## 背景
`tail.cpp` 此前只有静态 tail(`-n`/`-c`/`+N`),无 follow。日志跟踪是运维高频场景,缺失。PLAN 批1 要求 `-f`(+ `-F` retry + 干净退出)。

## 目标
- `-f` 跟踪文件追加数据
- `-F` 跟踪 + 轮转/truncate 后重开
- SIGINT/SIGTERM 干净退出
- 完成门:ctest + 集成全绿,size-opt ≤ 550 KB

## 设计 / 决策

**follow 方案:外部大模型仲裁后采纳 A′(fd-based stat 轮询),否决 inotify。**

起初拟 `stat(path)` 比 size 读区间,仲裁指出核心缺陷:path 替换竞态 + `-f` 本质是 follow descriptor。修正为:保持 fd 打开、offset 为消费位置;`fstat(fd)` 查同 inode 截断 → `lseek(0)`;`read()` 到 EOF;仅 `-F` `stat(path)` 核 dev/ino 检测轮转。

**否决 inotify**:Linux-only、第二套状态机、体积;1s 轮询是 BusyBox 级取舍(BusyBox tail 本身无 inotify 后端,`inotifyd` 是独立 applet)。inotify 未来只作 wake-hint(构建开关隔离),不维护第二套状态机。

**多文件公平**:round-robin sweep + 每文件每轮 64 KiB quantum + backlog 标志;快写文件达 quantum 未 EOF 则下轮不 sleep,防饥饿。

**-F retry**:初始缺失 → fd=-1 每 interval 重试、首次出现从字节 0 读(非尾部 N 行,免丢早期内容);文件消失 → 保留旧 fd(被 unlink 仍可能被写)+ pathname 重试;inode 变化 → open pending_fd → 旧 fd drain 到 EOF → 切换(比立即 close 更贴轮转实际);同 inode 截断 → `fstat` size<offset → `lseek(0)`。

**等待原语**:`nanosleep`+EINTR(第一版)。仲裁指出 flag+nanosleep 检查-睡眠间有微小竞态(最坏多等一个 interval);ppoll 原子 mask 可闭,但代码量大,第一版接受 nanosleep,ppoll 后置。

**信号**:`sigaction`(非 `signal`,可移植语义弱)+ RAII guard 恢复旧 handler(CFBox 是 multi-call binary,必须不污染同进程后续 applet)。handler 只设 `volatile sig_atomic_t`。退出码 0 是 CFBox 产品语义——**非** coreutils 兼容(GNU tail 默认终止,shell 得 130/143);装 handler 的真实理由是干净 flush stdout。

**CFBox 特化适配**:
- 新建 `unique_fd`(io.hpp):fd 是值非指针,用轻量 class 而非 `unique_ptr<int,...>`(免堆分配,全 noexcept 兼容禁异常)。
- stat 走 `<sys/stat.h>` raw,**不走 fs_util**(fs_util::status 拉 `std::filesystem`,体积红线);follow 是字节流消费状态机,非通用文件系统操作,不违反 DIRECTIVES A 维度。
- 初始 tail 用同一 fd(read_fd_all)避免 reopen 竞态丢字节。

## 陷阱(GOTCHA)
- **poll 对磁盘文件无效**(POLLIN 永远就绪):tui.hpp 的 poll 封装是 stdin 按键,不能套用文件 follow。必须 stat 轮询或 inotify。
- **轮询无法可靠检测 truncate 后一周期内长过旧 offset**(`st_size<offset` 启发式失效)——固有限制,非否决项。
- **multi-call binary 改全局信号必须 RAII 恢复**(已回填 PLAN GOTCHA #6)。
- **初始 tail 全量读**(read_fd_all):沿用现有 tail 行为,大文件内存风险(与现有 tail 同,后续可优化尾部读)。

## 验证
- ctest:**381/381** 全绿(379 原有 + 2 新 `TailFollow.NoFilesErrors` / `BadIntervalErrors`)。
- 集成 run_all.sh:**All passed**,含 tail follow 4 用例(-f 追加输出 / -f SIGINT 退出 0 / -F retry / 多文件 header)。
- size-opt:**410 KB** stripped(基线 406,+4 KB,预算 550)。

## 后续
- **后置(P2/下批可选)**:inotify wake-hint(构建开关)、ppoll 严格即时退出、`-F` 错误日志去重打磨、stdin follow、初始 tail 尾部读优化(免大文件全量内存)。
- 衔接:Phase 2 批2 `cp -a`。
31 changes: 31 additions & 0 deletions include/cfbox/io.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <memory>
#include <string>
#include <string_view>
#include <unistd.h>
#include <vector>

#include <cfbox/error.hpp>
Expand All @@ -19,6 +20,36 @@ struct FileCloser {
};
using unique_file = std::unique_ptr<std::FILE, FileCloser>;

// RAII wrapper for a raw POSIX file descriptor. Not unique_ptr<int, ...>: a
// descriptor is a value, not a pointer — no heap alloc, every method noexcept.
class unique_fd {
public:
explicit unique_fd(int fd = -1) noexcept : fd_{fd} {}
~unique_fd() noexcept { reset(); }
unique_fd(unique_fd&& other) noexcept : fd_{other.release()} {}
auto operator=(unique_fd&& other) noexcept -> unique_fd& {
reset(other.release());
return *this;
}
unique_fd(const unique_fd&) = delete;
auto operator=(const unique_fd&) = delete;

[[nodiscard]] auto get() const noexcept -> int { return fd_; }
[[nodiscard]] auto release() noexcept -> int {
int fd = fd_;
fd_ = -1;
return fd;
}
auto reset(int fd = -1) noexcept -> void {
if (fd_ >= 0) ::close(fd_);
fd_ = fd;
}
[[nodiscard]] explicit operator bool() const noexcept { return fd_ >= 0; }

private:
int fd_{-1};
};

[[nodiscard]] inline auto open_file(std::string_view path, const char* mode)
-> base::Result<unique_file> {
auto* f = std::fopen(path.data(), mode);
Expand Down
Loading
Loading