diff --git a/CHANGELOG.md b/CHANGELOG.md index a998666..7a3ef71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixed +- Doctor: report lock owner PID and distinguish paired stores locked by another process. (#105 — thanks @artemgetmann) - Media: recover panics per download job so one bad payload no longer drains the worker pool. (#179 — thanks @shaun0927) - Messages: attribute history messages from LID-addressed groups to the top-level participant sender. (#19 — thanks @entropyy0) - Messages: show display text for replies, reactions, and media in `messages context`. (#183 — thanks @fuleinist) diff --git a/cmd/wacli/doctor.go b/cmd/wacli/doctor.go index f656724..6a7f7e2 100644 --- a/cmd/wacli/doctor.go +++ b/cmd/wacli/doctor.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" "text/tabwriter" @@ -14,6 +15,31 @@ import ( "github.com/steipete/wacli/internal/out" ) +func parseLockOwnerPID(lockInfo string) int { + for _, line := range strings.Split(lockInfo, "\n") { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "pid=") { + continue + } + pid, err := strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, "pid="))) + if err == nil && pid > 0 { + return pid + } + } + return 0 +} + +func doctorConnectionState(authed, connected, lockHeld, connect bool) string { + switch { + case connected: + return "connected" + case authed && lockHeld && !connect: + return "locked_by_other_process" + default: + return "disconnected" + } +} + func newDoctorCmd(flags *rootFlags) *cobra.Command { var connect bool @@ -57,23 +83,28 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command { connected = true } } + lockOwnerPID := parseLockOwnerPID(lockInfo) type report struct { - StoreDir string `json:"store_dir"` - LockHeld bool `json:"lock_held"` - LockInfo string `json:"lock_info,omitempty"` - Authed bool `json:"authenticated"` - Connected bool `json:"connected"` - FTSEnabled bool `json:"fts_enabled"` + StoreDir string `json:"store_dir"` + LockHeld bool `json:"lock_held"` + LockInfo string `json:"lock_info,omitempty"` + LockOwnerPID int `json:"lock_owner_pid,omitempty"` + Authed bool `json:"authenticated"` + Connected bool `json:"connected"` + ConnectionState string `json:"connection_state"` + FTSEnabled bool `json:"fts_enabled"` } rep := report{ - StoreDir: storeDir, - LockHeld: lockHeld, - LockInfo: lockInfo, - Authed: authed, - Connected: connected, - FTSEnabled: a.DB().HasFTS(), + StoreDir: storeDir, + LockHeld: lockHeld, + LockInfo: lockInfo, + LockOwnerPID: lockOwnerPID, + Authed: authed, + Connected: connected, + ConnectionState: doctorConnectionState(authed, connected, lockHeld, connect), + FTSEnabled: a.DB().HasFTS(), } if flags.asJSON { @@ -86,8 +117,12 @@ func newDoctorCmd(flags *rootFlags) *cobra.Command { if rep.LockHeld && rep.LockInfo != "" { fmt.Fprintf(w, "LOCK_INFO\t%s\n", rep.LockInfo) } + if rep.LockOwnerPID > 0 { + fmt.Fprintf(w, "LOCK_OWNER_PID\t%d\n", rep.LockOwnerPID) + } fmt.Fprintf(w, "AUTHENTICATED\t%v\n", rep.Authed) fmt.Fprintf(w, "CONNECTED\t%v\n", rep.Connected) + fmt.Fprintf(w, "CONNECTION_STATE\t%s\n", rep.ConnectionState) fmt.Fprintf(w, "FTS5\t%v\n", rep.FTSEnabled) _ = w.Flush() diff --git a/cmd/wacli/doctor_test.go b/cmd/wacli/doctor_test.go new file mode 100644 index 0000000..59c1fe1 --- /dev/null +++ b/cmd/wacli/doctor_test.go @@ -0,0 +1,48 @@ +package main + +import "testing" + +func TestParseLockOwnerPID(t *testing.T) { + tests := []struct { + name string + info string + want int + }{ + {name: "pid line", info: "pid=50394\nacquired_at=2026-04-05T12:30:11Z", want: 50394}, + {name: "trimmed pid", info: " pid= 42 ", want: 42}, + {name: "missing pid", info: "acquired_at=2026-04-05T12:30:11Z"}, + {name: "invalid pid", info: "pid=abc"}, + {name: "zero pid", info: "pid=0"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := parseLockOwnerPID(tc.info); got != tc.want { + t.Fatalf("parseLockOwnerPID() = %d, want %d", got, tc.want) + } + }) + } +} + +func TestDoctorConnectionState(t *testing.T) { + tests := []struct { + name string + authed bool + connected bool + lockHeld bool + connect bool + want string + }{ + {name: "connected wins", authed: true, connected: true, lockHeld: true, want: "connected"}, + {name: "locked paired session", authed: true, lockHeld: true, want: "locked_by_other_process"}, + {name: "connect requested stays disconnected", authed: true, lockHeld: true, connect: true, want: "disconnected"}, + {name: "plain disconnected", authed: true, want: "disconnected"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := doctorConnectionState(tc.authed, tc.connected, tc.lockHeld, tc.connect) + if got != tc.want { + t.Fatalf("doctorConnectionState() = %q, want %q", got, tc.want) + } + }) + } +}