package cli import ( "context" "errors" "fmt" "os" "os/exec" "runtime" "strconv" "strings" crawlstore "github.com/openclaw/crawlkit/store" ) type lockDiagnostic struct { DBPath string `json:"db_path"` DBExists bool `json:"db_exists"` DBBytes int64 `json:"db_bytes"` WALPath string `json:"wal_path"` WALExists bool `json:"wal_exists"` WALBytes int64 `json:"wal_bytes"` SHMPath string `json:"shm_path"` SHMExists bool `json:"shm_exists"` SHMBytes int64 `json:"shm_bytes"` JournalPath string `json:"journal_path"` JournalExists bool `json:"journal_exists"` JournalBytes int64 `json:"journal_bytes"` ReadOnlyOpen string `json:"read_only_open"` QuickCheck string `json:"quick_check"` SafeReadOnlyInspection bool `json:"safe_read_only_inspection"` WriterActivity string `json:"writer_activity"` AppearsIdle bool `json:"appears_idle"` ProcessDetection processDetectionReport `json:"process_detection"` ArchiveHealth string `json:"archive_health"` FileStateErrors []string `json:"file_state_errors,omitempty"` Error string `json:"error,omitempty"` } type processDetectionReport struct { Method string `json:"method"` Platform string `json:"platform"` Available bool `json:"available"` Error string `json:"error,omitempty"` WriterProcesses []lockProcess `json:"writer_processes"` } type lockProcess struct { PID int `json:"pid"` Command string `json:"command,omitempty"` Access string `json:"access"` } var detectDBProcesses = defaultDetectDBProcesses func sqliteLockDiagnostic(ctx context.Context, dbPath string) lockDiagnostic { out := lockDiagnostic{ DBPath: dbPath, WALPath: dbPath + "-wal", SHMPath: dbPath + "-shm", JournalPath: dbPath + "-journal", ReadOnlyOpen: "missing", QuickCheck: "missing", ArchiveHealth: "missing", WriterActivity: "unknown", ProcessDetection: processDetectionReport{ Method: "not_run", Platform: runtime.GOOS, WriterProcesses: []lockProcess{}, }, } if strings.TrimSpace(dbPath) == "" { out.Error = "database path is empty" out.ArchiveHealth = "error" out.ReadOnlyOpen = "error" out.QuickCheck = "skipped" return out } var err error out.DBExists, out.DBBytes, err = fileExistsAndSize(dbPath) if err != nil { out.Error = fmt.Sprintf("inspect database file: %v", err) out.ArchiveHealth = "error" out.ReadOnlyOpen = "error" out.QuickCheck = "skipped" return out } for _, file := range []struct { name string path string exists *bool bytes *int64 }{ {name: "WAL", path: out.WALPath, exists: &out.WALExists, bytes: &out.WALBytes}, {name: "SHM", path: out.SHMPath, exists: &out.SHMExists, bytes: &out.SHMBytes}, {name: "rollback journal", path: out.JournalPath, exists: &out.JournalExists, bytes: &out.JournalBytes}, } { *file.exists, *file.bytes, err = fileExistsAndSize(file.path) if err != nil { out.FileStateErrors = append(out.FileStateErrors, fmt.Sprintf("inspect %s: %v", file.name, err)) } } if !out.DBExists { return out } out.ProcessDetection = detectDBProcesses(ctx, dbPath) switch { case !out.ProcessDetection.Available: out.WriterActivity = "unknown" case len(out.ProcessDetection.WriterProcesses) > 0: out.WriterActivity = "detected" default: out.WriterActivity = "none_detected" } st, err := crawlstore.OpenReadOnly(ctx, dbPath) if err != nil { out.ReadOnlyOpen = "error" out.QuickCheck = "skipped" out.ArchiveHealth = "error" out.Error = err.Error() return out } out.ReadOnlyOpen = "ok" if err := sqliteQuickCheck(ctx, st); err != nil { out.QuickCheck = "error" out.ArchiveHealth = "error" out.Error = err.Error() _ = st.Close() return out } out.QuickCheck = "ok" out.ArchiveHealth = "ok" out.SafeReadOnlyInspection = true out.AppearsIdle = out.WriterActivity == "none_detected" _ = st.Close() return out } func sqliteQuickCheck(ctx context.Context, st *crawlstore.Store) error { rows, err := st.DB().QueryContext(ctx, `pragma quick_check`) if err != nil { return err } defer rows.Close() var problems []string for rows.Next() { var line string if err := rows.Scan(&line); err != nil { return err } if strings.TrimSpace(line) != "ok" { problems = append(problems, line) } } if err := rows.Err(); err != nil { return err } if len(problems) > 0 { return fmt.Errorf("sqlite quick_check failed: %s", strings.Join(problems, "; ")) } return nil } func fileExistsAndSize(path string) (bool, int64, error) { info, err := os.Stat(path) if err != nil { if errors.Is(err, os.ErrNotExist) { return false, 0, nil } return false, 0, err } return true, info.Size(), nil } func defaultDetectDBProcesses(ctx context.Context, dbPath string) processDetectionReport { report := processDetectionReport{Method: "lsof-writable-fd", Platform: runtime.GOOS, WriterProcesses: []lockProcess{}} if runtime.GOOS == "windows" { report.Method = "unsupported" report.Error = "process lock detection is not implemented on windows" return report } if _, err := exec.LookPath("lsof"); err != nil { report.Error = "lsof not found" return report } cmd := exec.CommandContext(ctx, "lsof", "-w", "-Fpcfa", "--", dbPath) data, err := cmd.Output() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { report.Available = true return report } report.Error = err.Error() return report } report.Available = true report.WriterProcesses = parseLsofWriterOutput(string(data), os.Getpid()) return report } func parseLsofWriterOutput(raw string, ignoredPID int) []lockProcess { out := make([]lockProcess, 0) current := lockProcess{} writable := false flush := func() { if current.PID != 0 && current.PID != ignoredPID && writable { current.Access = "read_write" out = append(out, current) } current = lockProcess{} writable = false } for _, line := range strings.Split(raw, "\n") { if line == "" { continue } switch line[0] { case 'p': flush() pid, _ := strconv.Atoi(strings.TrimSpace(line[1:])) current.PID = pid case 'c': current.Command = strings.TrimSpace(line[1:]) case 'a': access := strings.TrimSpace(line[1:]) writable = writable || access == "u" || access == "w" } } flush() return out }