mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-30 22:33:03 +00:00
两个工具,都是为了让「这个工具能不能并发」有**可复现的依据**,而不是靠人眼扫。
## tools/audit_parallel.go —— 审计
为什么需要它:正则扫 `ToolDef` 字面量**不可靠**。用它审计主仓 27 个工具时,
把 `config_set` 判成"无共享写",而它的 handler 其实在 `p.handleSet` 里且无锁 ——
原因:RegisterTool 第三个参数是方法名/闭包,正则看不到执行体。
本工具用 go/ast 从**注册点跟进到 handler 实现**,递归 3 层(带环检测),
输出三态:SAFE / SERIAL / UNKNOWN。
★ UNKNOWN 一律不声明并发安全 —— 追不到实现就不能声称安全。
### 开发它时踩的坑(都写在代码注释里)
- **通用 HTTP 包装器**:vanblog 的 manage_social / manage_settings 全走
`p.do("GET"|"POST"|"PUT", ...)`,方法名毫无写操作迹象,纯靠名字匹配
**全判成 SAFE** —— 而它们明确含 POST/PUT。审计工具给出与代码相反的结论,
比不给结论更危险(人会信它)。改为按 HTTP 动词判定后,
统计从 72/37 变为 46/63。
- **链式调用丢方法**:`p.sdk.Settings().Set(...)` 是三段链,只看最外层只得到
"Settings",`.Set` 整个丢失 ⇒ handleConfigure(写配置 + 启停服务)被判只读。
改为收集整条链的所有方法名。
- **变量名 ≠ 类型名**:调用点写 `p.handleRead()`,定义处是
`func (p *Plugin) handleRead()`。拿变量名去查类型索引**永远匹配不上**,
143 个工具全报 UNKNOWN。须建"变量名 → 接收者类型"索引。
- **方法接收者不是局部变量**:example 里根本没有 `p := &Plugin{}`,
p 是 Start 的接收者,局部变量索引全空。须把接收者变量名也纳入索引。
- **枚举被字符串污染**:verdict = "Serial:true" 而 report 只认三个枚举值
⇒ 全部落进 UNKNOWN,输出"共 13:UNKNOWN 13",看着像工具没在工作。
- **作者声明必须优先**:我把"已声明"当 finding 记录后照常跑写入检测并
**覆盖** verdict,于是 plugin_install(已标 Serial:true)被判 SAFE。
## tools/annotate_parallel/ —— 标注
按 SDK 风格插入声明项:Name 在首位,声明项在末尾(Parameters 之后、
handler 之前),不打散 gofmt 对齐。
插入点必须用**括号深度 + 记录进入深度 3 的行号**定位:
- `RegisterTool( =1, ToolDef{ =2, Parameters{ =3`
- 只判 depth==2 会在 `Name:` 行就返回(那行本来就是深度 2),
插入点跑到 RegisterTool 之前,编译报 "expected 1 expression"
- 空 `properties: map[string]interface{}{}` 让深度**在同一行**进出平衡,
所以"曾触及深度 3"也不能作门控,必须记行号
- 试过用正则找"最后一个顶层字段",被嵌套 map 里的同形文本骗到,
823 处错误重排把文件改坏 —— 文本匹配改结构体字面量就是这条路
用法:go build -o /tmp/annotate ./tools/annotate_parallel
/tmp/annotate <file> <tool:parallel|serial:说明> ...
173 lines
4.9 KiB
Go
173 lines
4.9 KiB
Go
// 命令 annotate_parallel 按 SDK 声明风格为工具加并发安全声明。
|
||
//
|
||
// 风格要求(照 SDK 的 NoMemory 走,不自创):
|
||
//
|
||
// · 声明项是**结构体字段**(ParallelSafe / Serial),不是注释标记;
|
||
// · 插在 Parameters 之后、handler 之前 —— 即字面量的**末尾**,
|
||
// 与 SDK 里 NoMemory/ContextPolicy/RecallPolicy 的位置一致;
|
||
// · Name 保持在首位,不打散 gofmt 对齐。
|
||
//
|
||
// 为什么用括号深度定位插入点:之前用正则找"最后一个顶层字段",
|
||
// 会被嵌套 map 里的同形文本骗到,结果把声明插到 Parameters 中间,
|
||
// 甚至把文件改坏(823 处重排)。深度计数是唯一可靠的。
|
||
//
|
||
// 用法:annotate_parallel <file> <tool:kind:note> ...
|
||
//
|
||
// kind: parallel | serial
|
||
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
)
|
||
|
||
func main() {
|
||
if len(os.Args) < 3 {
|
||
fmt.Fprintln(os.Stderr, "用法: annotate_parallel <file> <tool:kind:note>...")
|
||
os.Exit(2)
|
||
}
|
||
path := os.Args[1]
|
||
lines := readLines(path)
|
||
// 从后往前改,避免行号漂移
|
||
type job struct {
|
||
tool, kind, note string
|
||
}
|
||
var jobs []job
|
||
for _, arg := range os.Args[2:] {
|
||
p := strings.SplitN(arg, ":", 3)
|
||
if len(p) != 3 {
|
||
fmt.Fprintf(os.Stderr, "参数格式错: %q\n", arg)
|
||
os.Exit(2)
|
||
}
|
||
jobs = append(jobs, job{p[0], p[1], p[2]})
|
||
}
|
||
// 反序处理(同一文件里多个工具,位置互不影响,但保守起见从后往前)
|
||
for i := len(jobs) - 1; i >= 0; i-- {
|
||
j := jobs[i]
|
||
at, err := findInsertPoint(lines, j.tool)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, " 跳过 %s: %v\n", j.tool, err)
|
||
continue
|
||
}
|
||
field := "ParallelSafe: true,"
|
||
if j.kind == "serial" {
|
||
field = "Serial: true,"
|
||
}
|
||
ins := []string{"\t\t// " + j.note, "\t\t" + field}
|
||
out := append([]string{}, lines[:at]...)
|
||
out = append(out, ins...)
|
||
out = append(out, lines[at:]...)
|
||
lines = out
|
||
fmt.Printf(" %s @line %d (%s)\n", j.tool, at+1, j.kind)
|
||
}
|
||
writeLines(path, lines)
|
||
}
|
||
|
||
// findInsertPoint 找到该 RegisterTool 字面量中,Parameters 闭合之后的位置。
|
||
func findInsertPoint(lines []string, tool string) (int, error) {
|
||
head := fmt.Sprintf(`RegisterTool("%s"`, tool)
|
||
start := -1
|
||
for i, l := range lines {
|
||
if strings.Contains(l, head) {
|
||
start = i
|
||
break
|
||
}
|
||
}
|
||
if start < 0 {
|
||
return 0, fmt.Errorf("找不到 RegisterTool(%q)", tool)
|
||
}
|
||
// 从 RegisterTool( 开始做括号深度追踪,找到 ToolDef 字面量的闭合 "}," 行
|
||
depth := 0
|
||
started := false
|
||
enteredAt := -1
|
||
inStr := false
|
||
esc := false
|
||
for i := start; i < len(lines); i++ {
|
||
for _, ch := range lines[i] {
|
||
if esc {
|
||
esc = false
|
||
continue
|
||
}
|
||
if ch == '\\' && inStr {
|
||
esc = true
|
||
continue
|
||
}
|
||
if ch == '"' {
|
||
inStr = !inStr
|
||
continue
|
||
}
|
||
if inStr {
|
||
continue
|
||
}
|
||
switch ch {
|
||
case '(', '{', '[':
|
||
depth++
|
||
started = true
|
||
case ')', '}', ']':
|
||
depth--
|
||
}
|
||
}
|
||
// 插入点 = Parameters 字段的**闭合之后**。
|
||
//
|
||
// 精确定位法:Parameters 起始处的深度是 3(RegisterTool( → ToolDef{ →
|
||
// Parameters{);它的闭合就是深度**首次从 3 回到 2** 的那一行。
|
||
//
|
||
// ⚠️ 我第一版没有这样做,而是"找 required 行的下一行,没有就返回字面量
|
||
// 闭合行"。对于没有 required 的工具(如 config_list_plugins),
|
||
// 后者落在 properties{} 内部 —— 生成的代码是
|
||
// "properties": map[string]interface{}{},
|
||
// ParallelSafe: true, ← 跑到 map 里去了
|
||
// 编译报 undefined: ParallelSafe,症状离原因很远。
|
||
// 插入点 = Parameters 字段闭合的**下一行**。
|
||
//
|
||
// 深度:RegisterTool( =1, ToolDef{ =2, Parameters{ =3。
|
||
//
|
||
// ★ 两个坑都是"同一行内深度进出平衡"造成的:
|
||
//
|
||
// 1. 空 properties:`"properties": map[string]interface{}{},`
|
||
// 深度 3→2 在**同一行**完成。所以不能用"曾触及 3"作门控,
|
||
// 必须记住**行号**:进入 3 的那行之后,首个回到 2 的行才是闭合行。
|
||
//
|
||
// 2. Name:/Description: 本来就在深度 2,早于 Parameters。
|
||
// 只判 depth==2 会在 Name 行就返回,插入点跑到 RegisterTool 之前,
|
||
// 编译报 "expected 1 expression"。
|
||
if started && depth >= 3 {
|
||
enteredAt = i
|
||
}
|
||
if enteredAt >= 0 && i > enteredAt && depth <= 2 {
|
||
return i + 1, nil
|
||
}
|
||
}
|
||
return 0, fmt.Errorf("括号深度追踪未收敛")
|
||
}
|
||
|
||
func readLines(p string) []string {
|
||
f, err := os.Open(p)
|
||
if err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
os.Exit(1)
|
||
}
|
||
defer f.Close()
|
||
var out []string
|
||
sc := bufio.NewScanner(f)
|
||
sc.Buffer(make([]byte, 1<<20), 1<<20)
|
||
for sc.Scan() {
|
||
out = append(out, sc.Text())
|
||
}
|
||
return out
|
||
}
|
||
|
||
func writeLines(p string, lines []string) {
|
||
var sb strings.Builder
|
||
for _, l := range lines {
|
||
sb.WriteString(l)
|
||
sb.WriteString("\n")
|
||
}
|
||
if err := os.WriteFile(p, []byte(sb.String()), 0644); err != nil {
|
||
fmt.Fprintln(os.Stderr, err)
|
||
os.Exit(1)
|
||
}
|
||
}
|