Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
52e6976
Feat/log command improvements: add support for line count, follow mod…
kooksee May 31, 2026
2a7c413
Feat/log command improvements: remove grep filtering support and upda…
kooksee May 31, 2026
a571a52
Feat: add GitHub Actions workflow for ziginit release process
kooksee May 31, 2026
aca5d93
Feat: 修复 sleepNs 函数中的类型转换问题
kooksee May 31, 2026
0d3a726
Feat/log: 保留空行并增强行计数参数的错误处理
kooksee May 31, 2026
07d0bac
Feat/log: 实现 follow 模式的跨 chunk 行拼接,增强日志输出完整性
kooksee May 31, 2026
e8af566
Feat/log: 添加路径长度检查,防止越界写入
kooksee May 31, 2026
ff42f6f
Feat/log: 优化路径构建,增加路径长度检查并改进文件跟踪逻辑
kooksee May 31, 2026
37bc55f
Feat/log: 增强 sockaddrUn 和 sendCommand 函数的路径长度检查,防止缓冲区溢出
kooksee May 31, 2026
856374c
Feat/log: 增强命令处理和响应格式,确保 CLI 命令失败时正确退出并记录错误信息
kooksee Jun 1, 2026
91d88f1
Feat/log: 增强 CLI 错误处理,确保命令失败时返回详细错误信息并正确退出
kooksee Jun 1, 2026
990d9a7
更新 README.md,修正服务日志托管描述,调整模块行数信息,优化命令启动说明
kooksee Jun 1, 2026
9a453aa
Feat/log: 添加日志轮转功能,支持备份文件管理和流式复制
kooksee Jun 1, 2026
6852115
Feat/log: 将多个函数参数类型从 [*:0]const u8 修改为 c.CStr,以增强 C 字符串处理的安全性和一致性
kooksee Jun 2, 2026
67e22f5
Feat/log: 添加 gzip 可用性检查,确保日志轮转时备份安全有效
kooksee Jun 2, 2026
2920e6a
Feat/log: 优化日志输出处理,确保数据完整性并提升性能
kooksee Jun 3, 2026
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
12 changes: 12 additions & 0 deletions .github/instructions/ziginit.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,15 @@ applyTo: "tools/ziginit/**/*.zig"
5. 确认无孤儿进程、无残留 socket/lock 文件
- **改名/重构类任务**:必须全局搜索旧名称(`grep -r "旧名"`),确认注释、日志、错误提示、argv 显示全部替换完毕
- **信号处理类变更**:手动发送 SIGINT/SIGTERM/SIGKILL 验证,不能只依赖 `quit` 命令的测试路径

## 安全与防御性编程

- **用户输入必须校验后再使用**:CLI 参数、IPC 消息中的服务名等外部输入,必须先通过 `cfg.findService()` 或长度校验,才能传入 `buildServicePaths` 等内部函数。禁止将未校验的输入直接用于 `@memcpy` 到固定大小缓冲区
- **固定缓冲区写入前必须做边界检查**:`@memcpy` 到 `[MAX_PATH]u8` 等固定数组前,计算 `total` 并断言 `total < capacity`。路径拼接溢出属于不可恢复的配置/系统错误,应 `@panic` 而非静默截断——静默截断会导致多个路径指向同一文件,引发更隐蔽的故障
- **禁止静默丢弃数据**:缓冲区满时必须有明确的降级策略(flush 后重试、报错退出、或截断警告),不能 `@min(data.len, avail)` 后默默丢弃超出部分
- **自引用结构体禁用裸指针**:结构体中不得保存指向外部栈变量的指针(如 `path: [*:0]const u8`),应使用自持缓冲区(如 `path_buf: [MAX_PATH+1]u8`)并用 `@memcpy` 拷贝数据,避免生命周期不匹配导致悬空指针

## 资源与流式数据

- **轮询循环中避免重复获取/释放资源**:follow 模式等 poll 循环中,如果文件使用 truncate 轮转(而非 rename),FD 应在循环外打开、循环结束后统一关闭,循环内仅用 `fstat` 检查大小变化。文件可能延迟创建时,用 `-1` 标记并每轮重试 open
- **跨 chunk 数据拼接**:流式读取场景(follow 模式、pipe drain)中,`read()` 返回的 chunk 不保证在行边界切分。必须维护 carry buffer,仅在遇到 `\n` 时输出完整行,残留数据保留到下次 read 后拼接
112 changes: 112 additions & 0 deletions .github/workflows/release-ziginit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
name: Release ziginit

on:
push:
tags:
- "ziginit/v*"

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install Zig
uses: mlugg/setup-zig@v2
with:
version: 0.16.0

- name: Cache Zig build artifacts
uses: actions/cache@v4
with:
path: |
~/.cache/zig
tools/ziginit/.zig-cache
key: ziginit-${{ runner.os }}-zig-${{ hashFiles('tools/ziginit/main.zig', 'tools/ziginit/build.zig') }}
restore-keys: |
ziginit-${{ runner.os }}-zig-

- name: Parse version and channel from tag
id: version
run: |
version="${GITHUB_REF#refs/tags/ziginit/}"
echo "version=${version}" >> "$GITHUB_OUTPUT"
if [[ "$version" =~ -alpha ]]; then
echo "channel=dev" >> "$GITHUB_OUTPUT"
elif [[ "$version" =~ -beta ]]; then
echo "channel=beta" >> "$GITHUB_OUTPUT"
else
echo "channel=prod" >> "$GITHUB_OUTPUT"
fi

- name: Install UPX (production only)
if: steps.version.outputs.channel == 'prod'
run: sudo apt-get update && sudo apt-get install -y upx

- name: Cross-compile ziginit
run: |
cd tools/ziginit
channel="${{ steps.version.outputs.channel }}"
if [[ "$channel" == "prod" ]]; then
optimize="ReleaseSmall"
else
optimize="ReleaseSafe"
fi
echo "Channel: ${channel}, Optimize: ${optimize}"

declare -A targets=(
["x86_64-linux"]="linux_amd64"
["aarch64-linux"]="linux_arm64"
["arm-linux"]="linux_armv7"
["x86_64-macos"]="darwin_amd64"
["aarch64-macos"]="darwin_arm64"
)
mkdir -p ../../release-artifacts
for zig_target in "${!targets[@]}"; do
label="${targets[$zig_target]}"
echo "=== Building ziginit for ${zig_target} (${optimize}) ==="
zig build -Doptimize="${optimize}" -Dtarget="${zig_target}"
if [[ "$channel" == "prod" && "$zig_target" == *-linux ]]; then
upx --best --lzma zig-out/bin/ziginit || true
fi
archive="ziginit_${{ steps.version.outputs.version }}_${label}.tar.gz"
tar -czf "../../release-artifacts/${archive}" -C zig-out/bin ziginit
rm -rf zig-out
done

- name: Generate checksums
run: |
cd release-artifacts
sha256sum *.tar.gz > checksums.txt
cat checksums.txt

- name: Generate changelog (production only)
if: steps.version.outputs.channel == 'prod'
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --latest --strip header
env:
OUTPUT: CHANGES.md

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.RELEASER_TOKEN }}
run: |
prerelease=""
notes_arg="--generate-notes"
if [[ "${{ steps.version.outputs.channel }}" != "prod" ]]; then
prerelease="--prerelease"
else
notes_arg="--notes-file CHANGES.md"
fi
gh release create "${{ github.ref_name }}" \
--title "ziginit ${{ steps.version.outputs.version }}" \
$notes_arg \
$prerelease \
release-artifacts/*
10 changes: 10 additions & 0 deletions tools/ziginit/config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ pub const MAX_LOG_TAIL_SIZE: usize = 4 * 1024 * 1024;
pub const LOG_MAX_SIZE: usize = 10 * 1024 * 1024;
// ZIGINIT_LOG_MAX_KB 环境变量允许的最大值
pub const LOG_MAX_SIZE_UPPER: usize = 100 * 1024 * 1024;
// `log -n` 默认显示行数
pub const DEFAULT_LOG_LINES: u32 = 100;
// `log -f` follow 模式轮询间隔
pub const LOG_FOLLOW_POLL_NS: i64 = 200_000_000; // 200ms

// ── 默认路径 ──

Expand Down Expand Up @@ -244,6 +248,9 @@ pub fn buildServicePaths(workdir: []const u8, name: []const u8) ServicePaths {
};
for (pairs) |p| {
const total = workdir.len + 1 + name.len + p.suffix.len;
if (total >= MAX_PATH) {
@panic("constructed service path exceeds MAX_PATH");
}
Comment thread
kooksee marked this conversation as resolved.
@memcpy(p.dst[0..workdir.len], workdir);
p.dst[workdir.len] = '/';
@memcpy(p.dst[workdir.len + 1 ..][0..name.len], name);
Expand All @@ -265,6 +272,9 @@ pub fn buildDaemonPaths(workdir: []const u8) DaemonPaths {
};
for (pairs) |p| {
const total = workdir.len + p.suffix.len;
if (total >= MAX_PATH) {
@panic("constructed daemon path exceeds MAX_PATH");
}
Comment thread
kooksee marked this conversation as resolved.
@memcpy(p.dst[0..workdir.len], workdir);
@memcpy(p.dst[workdir.len..total], p.suffix);
p.dst[total] = 0;
Expand Down
131 changes: 131 additions & 0 deletions tools/ziginit/docs/log-improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Log 子命令改进

## 1. 动机

原有 `log` 命令通过 Unix Socket 向 daemon 请求日志数据,daemon 读取日志文件后流式写回客户端 socket。这种架构存在以下限制:

- **无法 follow**:socket 是请求-响应模型,无法持续推送新数据
- **增加延迟**:CLI → daemon → 读文件 → 回传 socket → CLI,多一跳中转
- **daemon 耦合**:daemon 停机时无法查看日志
- **按字节截断**:尾部读 64KB 字节,不保证从完整行开始

## 2. 新架构

CLI 直接读取日志文件,绕过 daemon socket。

```mermaid
graph LR
subgraph "旧架构"
A1[CLI] -->|socket| B1[daemon]
B1 -->|read| C1[log files]
B1 -->|stream| A1
end

subgraph "新架构"
A2[CLI] -->|direct read| C2[log files]
end

style A1 fill:#fee2e2,stroke:#dc2626
style A2 fill:#dcfce7,stroke:#16a34a
```

日志文件位于 `{workdir}/{service}.log` 和 `{workdir}/daemon.log`,由 daemon 进程通过 `O_APPEND` 模式写入。CLI 直读不会与 daemon 写入冲突。

## 3. CLI 用法

```
ziginit log <name> # 显示最近 100 行
ziginit log -n <N> <name> # 显示最近 N 行
ziginit log -f <name> # 实时跟踪(类似 tail -f)
ziginit log daemon # 查看 daemon 日志
ziginit log --all # 所有服务 + daemon 日志(带 [name] 前缀)
ziginit log -f --all # follow 所有服务
```

过滤日志使用 Unix 管道:`ziginit log -f svc | grep error`

### 参数说明

| 参数 | 默认值 | 说明 |
| -------- | ------ | ------------------------------------- |
| `-n <N>` | 100 | 显示尾部 N 行 |
| `-f` | off | follow 模式,持续输出新行直到 Ctrl-C |
| `--all` | off | 显示所有服务 + daemon 日志 |
| `daemon` | — | 伪服务名,指向 `{workdir}/daemon.log` |

### `--all` 输出格式

每行加 `[name] ` 前缀(去掉了旧版的读取时间戳,因为它不是写入时间,会产生误导):

```
[daemon] daemon started, 2 services
[svc-a] svc-a-out
[svc-a] svc-a-err
[svc-b] listening on :8080
```

## 4. 实现细节

### 核心函数(journal.zig)

| 函数 | 职责 |
| -------------------------------- | ---------------------------------------------------------------------- |
| `tailLastLines(path, n, prefix)` | 从文件尾部读取,反向扫描定位最后 N 行,应用 prefix 后输出到 stdout |
| `followFiles(targets)` | 轮询文件变化(200ms 间隔),输出新增行,检测 `sigpending` 实现优雅退出 |
| `outputLines(data, prefix)` | 逐行处理:前缀添加 + stdout 写入(过滤交给外部 `grep` 管道) |

### FollowTarget 结构

```zig
pub const FollowTarget = struct {
path_buf: [MAX_PATH + 1]u8, // 自持路径数据,避免悬空指针
path_len: usize,
offset: usize, // 当前读取位置(= tailLastLines 返回的文件大小)
prefix: [MAX_NAME + 4]u8, // "[name] " 前缀
prefix_len: usize,
};
```

> **设计决策**:`FollowTarget` 拷贝路径到内部缓冲区而非存储外部指针。原因是 `ServicePaths` 是栈上临时值,循环迭代结束后失效。存裸指针会导致 Release 模式悬空指针崩溃(与 `ConfigDtoBuild` 同类问题)。

### Follow 模式实现

```mermaid
graph TD
A[tailLastLines 初始输出] --> B[blockTermInt 阻塞信号]
B --> C{sleep 200ms}
C --> D{sigpending?}
D -->|是| E[恢复信号 & 退出]
D -->|否| F[遍历 targets]
F --> G{fstat: 文件增长?}
G -->|否| C
G -->|文件缩小| H[offset = 0 重置]
H --> C
G -->|增长| I[seekTo + read 新数据]
I --> J[outputLines 过滤输出]
J --> K[更新 offset]
K --> C
```

- 使用 `fstat` 检测文件大小变化,而非 `poll()`(`poll` 对普通文件总是立即返回 `POLLIN`)
- 文件大小减小时判定为日志轮转,重置 offset 到 0 重新读取
- 信号检查使用 `sigpending`,不依赖信号 handler 全局状态

## 5. 删除的代码

| 位置 | 删除内容 |
| -------------- | -------------------------------------------------------------------- |
| `journal.zig` | `readServiceLogTail`、`readServiceLogTailPrefixed`(旧 socket 模式) |
| `server.zig` | `ACTION_LOG` 流式转发逻辑(serverLoop + handleCommand) |
| `protocol.zig` | `ACTION_LOG` 常量、`sendCommand` 中的 log 流式读取 |

## 6. 配置常量(config.zig)

```zig
pub const DEFAULT_LOG_LINES: u32 = 100; // -n 默认值
pub const LOG_FOLLOW_POLL_NS: i64 = 200_000_000; // follow 轮询间隔 200ms
```

沿用现有环境变量:
- `ZIGINIT_LOG_TAIL_KB`:控制 tail 读取的最大字节数(默认 64KB,上限 4MB)
- `ZIGINIT_LOG_MAX_KB`:控制日志轮转阈值(默认 10MB)
Loading