d575ddd0f5
InspectLease, CleanupStaleLease, runActiveLeaseCheck, and statusAt now use the PID-aware IsStale predicate. A dead local owner PID makes a lease stale immediately; SessionStatus / list / info reflect this with no display-code change. Corrects three cmd-package session-display test fixtures that built "active" leases with the local hostname but synthetic PIDs — now that freshness is PID-aware, an active session must be owned by a live process, so the fixtures use os.Getpid().
95 lines
2.9 KiB
Go
95 lines
2.9 KiB
Go
package session
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// SessionState is the display-only classification surfaced by SessionStatus.
|
|
type SessionState string
|
|
|
|
const (
|
|
// SessionStateNone: no lease file is present.
|
|
SessionStateNone SessionState = "none"
|
|
// SessionStateActive: lease file is present and fresh (heartbeat within StaleLeaseAfter).
|
|
SessionStateActive SessionState = "active"
|
|
// SessionStateStale: lease file is present but stale, or present-and-malformed.
|
|
SessionStateStale SessionState = "stale"
|
|
)
|
|
|
|
// Status is the derived display-only view of a workspace's session lease.
|
|
//
|
|
// It is intentionally narrower than InspectLease/LeaseState: callers that
|
|
// need behavioral decisions (adoption, dispatch) must keep using the
|
|
// existing primitives. Status exists for `ctask info` and `ctask list` to
|
|
// surface session visibility without touching tmux, PID liveness, or any
|
|
// lock state.
|
|
type Status struct {
|
|
State SessionState
|
|
Mode string // "direct" | "persistent" | "" when malformed
|
|
PID int
|
|
Hostname string
|
|
Diagnostic string // human-readable note for the malformed case; empty otherwise
|
|
}
|
|
|
|
// SessionStatus returns the display-only session summary for wsDir.
|
|
//
|
|
// It performs only one file read (.ctask/session.json) and never invokes
|
|
// tmux, checks PID liveness, modifies lease state, acquires locks, or
|
|
// otherwise mutates the workspace. PID liveness is intentionally deferred
|
|
// to v0.6's lazy-cleanup redesign, where it will have behavioral
|
|
// consequences; building it display-only here would mean building it
|
|
// twice.
|
|
//
|
|
// Display-only contract: do NOT call SessionStatus from lifecycle or
|
|
// adoption code. The "missing Mode defaults to direct" rule and the
|
|
// malformed-lease "stale" classification are display choices, not
|
|
// behavioral truths. Use ReadLease / IsFresh / InspectLease for
|
|
// lifecycle decisions.
|
|
func SessionStatus(wsDir string) Status {
|
|
return statusAt(wsDir, time.Now())
|
|
}
|
|
|
|
// statusAt is the test entry point with an injected clock. Production
|
|
// code goes through SessionStatus.
|
|
func statusAt(wsDir string, now time.Time) Status {
|
|
data, err := os.ReadFile(LeasePath(wsDir))
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return Status{State: SessionStateNone}
|
|
}
|
|
if err != nil {
|
|
return Status{
|
|
State: SessionStateStale,
|
|
Diagnostic: "lease exists but could not be read",
|
|
}
|
|
}
|
|
var l Lease
|
|
if jsonErr := json.Unmarshal(data, &l); jsonErr != nil {
|
|
return Status{
|
|
State: SessionStateStale,
|
|
Diagnostic: "lease exists but could not be read",
|
|
}
|
|
}
|
|
|
|
state := SessionStateStale
|
|
if !IsStale(&l, now, StaleLeaseAfter) {
|
|
state = SessionStateActive
|
|
}
|
|
|
|
// Pre-v0.5.3 leases predate the mode field; treat them as direct so
|
|
// `info` and `list` render a meaningful value rather than blank.
|
|
mode := l.Mode
|
|
if mode == "" {
|
|
mode = "direct"
|
|
}
|
|
|
|
return Status{
|
|
State: state,
|
|
Mode: mode,
|
|
PID: l.PID,
|
|
Hostname: l.Hostname,
|
|
}
|
|
}
|