| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- // orphanaudit — VRTX 孤儿页取证工具。
- //
- // orphanaudit -db <副本路径> [-samples 3] [-noengine]
- //
- // 输出两套口径并排:
- //
- // - analyzer 口径: 与 analyzer/audit.go 完全同批可达根 (不走 blob 链)。
- // 与 analyzer 报告对账用 —— 两个数字必须相等, 不等说明本工具移植有 bug。
- // - blob 感知口径: 再走行内 BlobRef → blob 链 (Type=9)。剩下的才是真实泄漏。
- //
- // 绝不直接对原件跑: 先用 cp 复制数据库文件, 只对副本运行。
- // 引擎只读打开 (O_RDONLY + 共享锁, 跳过 WAL 恢复), 不会写盘。
- package main
- import (
- "encoding/json"
- "flag"
- "fmt"
- "os"
- "git.ycl.pub/yl32940/TsDB2/internal/orphancheck"
- )
- func main() {
- db := flag.String("db", "", "VRTX 文件路径 (必须是副本!)")
- samples := flag.Int("samples", 3, "每种类型的样例页数")
- noEngine := flag.Bool("noengine", false, "不用引擎只读扫描 blob 链 (纯 analyzer 口径)")
- jsonOut := flag.Bool("json", false, "JSON 输出")
- flag.Parse()
- if *db == "" {
- fmt.Fprintln(os.Stderr, "用法: orphanaudit -db <副本路径> [-samples N] [-noengine] [-json]")
- os.Exit(2)
- }
- R, err := orphancheck.Run(*db, orphancheck.Options{
- EngineBlobScan: !*noEngine,
- SamplesPerType: *samples,
- })
- if err != nil {
- fmt.Fprintln(os.Stderr, "取证失败:", err)
- os.Exit(2)
- }
- if *jsonOut {
- enc := map[string]any{
- "file": R.File, "totalPages": R.TotalPages, "nextFreePage": R.NextFreePage,
- "freeCount": R.FreeCount, "freeDeclared": R.FreeDeclared, "freeVerdict": R.FreeVerdict,
- "walPending": R.WALPending, "reachableNoBlob": R.ReachableNoBlob, "blobPages": R.BlobPages,
- "analyzerOrphans": len(R.AnalyzerOrphans), "analyzerByType": R.AnalyzerByType,
- "analyzerWalLeftover": R.AnalyzerWalLeft,
- "orphans": len(R.Orphans), "byType": R.ByType, "walLeftover": R.WalLeft,
- "brokenChains": R.BrokenChains, "notes": R.Notes,
- "orphanIds": ids(R.Orphans), "analyzerOrphanIds": ids(R.AnalyzerOrphans),
- "samples": hexSamples(R),
- }
- writeJSON(enc)
- return
- }
- printReport(R)
- }
- func writeJSON(v any) {
- enc := json.NewEncoder(os.Stdout)
- enc.SetIndent("", " ")
- _ = enc.Encode(v)
- }
- func ids(ps []orphancheck.PageInfo) []uint32 {
- out := make([]uint32, len(ps))
- for i, p := range ps {
- out[i] = p.ID
- }
- return out
- }
- func printReport(R *orphancheck.Result) {
- fmt.Printf("== 文件: %s\n", R.File)
- fmt.Printf("总页数=%d NextFreePage=%d 空闲链=%d 页 (header 声明 %d, 判定: %s)\n",
- R.TotalPages, R.NextFreePage, R.FreeCount, R.FreeDeclared, R.FreeVerdict)
- if R.WALPending > 0 {
- fmt.Printf("未恢复 WAL 链: %d 页 (只读打开跳过恢复)\n", R.WALPending)
- }
- fmt.Printf("结构可达 (analyzer 口径, 含页 1)=%d 页; 引擎扫描认领 blob 链页=%d\n\n",
- R.ReachableNoBlob, R.BlobPages)
- fmt.Printf("-- analyzer 口径孤儿页 = %d\n", len(R.AnalyzerOrphans))
- printHist(R.AnalyzerByType, R.AnalyzerWalLeft)
- fmt.Printf("-- blob 感知口径真正孤儿页 = %d\n", len(R.Orphans))
- printHist(R.ByType, R.WalLeft)
- if len(R.Chains) > 0 {
- fmt.Printf("\nblob 链: 共 %d 条, 断裂 %d 条\n", len(R.Chains), R.BrokenChains)
- }
- for _, n := range R.Notes {
- fmt.Println("备注:", n)
- }
- if len(R.Orphans) > 0 && len(R.Samples) > 0 {
- fmt.Println("\n== 孤儿页样例 (头 64B hex) ==")
- for _, tn := range orphancheck.SortedTypes(R.ByType) {
- for _, p := range R.Samples[tn] {
- fmt.Printf("-- 页 %d 类型 %s UseSize=%d NextPage=%d\n", p.ID, tn, p.UseSize, p.Next)
- fmt.Print(orphancheck.SampleHex(p))
- }
- }
- }
- if d := len(R.AnalyzerOrphans) - len(R.Orphans); d > 0 {
- fmt.Printf("\n结论: analyzer 口径有 %d 页『孤儿』, 其中 %d 页是【活 blob 链页】"+
- " —— analyzer 不走 blob 链造成的误报; 真正泄漏 %d 页。\n", len(R.AnalyzerOrphans), d, len(R.Orphans))
- } else if len(R.Orphans) > 0 {
- fmt.Printf("\n结论: %d 页真实泄漏 (blob 走查后仍不可达), 按上方类型直方图指认路径。\n", len(R.Orphans))
- } else {
- fmt.Println("\n结论: 无真实孤儿页。")
- }
- }
- func printHist(m map[string]int, walLeft int) {
- if len(m) == 0 {
- fmt.Println(" (无)")
- return
- }
- for _, tn := range orphancheck.SortedTypes(m) {
- fmt.Printf(" %-24s %d\n", tn, m[tn])
- }
- if walLeft > 0 {
- fmt.Printf(" 其中 WAL(7) 页 = 已提交 WAL 链未回收 (引擎泄漏)\n")
- }
- }
- func hexSamples(R *orphancheck.Result) map[string]any {
- out := map[string]any{}
- for tn, ps := range R.Samples {
- var rows []map[string]any
- for _, p := range ps {
- rows = append(rows, map[string]any{
- "id": p.ID, "useSize": p.UseSize, "next": p.Next,
- "head64": orphancheck.SampleHex(p),
- })
- }
- out[tn] = rows
- }
- return out
- }
|