Documentation
¶
Index ¶
- Constants
- func EnsureSourceMatch(rec Record, want string) error
- func FrontendName(source string) string
- func NormalizeSource(s string) string
- func ProcessAlive(pid int) bool
- func SpawnDaemon(name, workDir, configFile string) error
- func Terminate(pid int, grace time.Duration) error
- func UpdateAppClients(cfg config.Config, name string, n int)
- func UpdateProxyPort(cfg config.Config, name string, port int)
- type ReapAction
- type ReapOptions
- type Record
- type Store
Constants ¶
const ( SourceCLI = "cli" SourceApp = "app" )
SourceCLI and SourceApp are the canonical session Source values: the rysh command line vs the rysh desktop app. They are compared by the open guards so the two front-ends never drive the same saved session/KV state.
Variables ¶
This section is empty.
Functions ¶
func EnsureSourceMatch ¶
EnsureSourceMatch refuses to open a session that belongs to a different front-end. A blank record source is legacy/unknown and treated as compatible with either front-end. want is the current invocation's source (normalized internally).
func FrontendName ¶
FrontendName renders a session source as a human-readable front-end name for error messages.
func NormalizeSource ¶
NormalizeSource maps a configured/recorded session source to its canonical value. Only "app" (the desktop app) is recognised explicitly; everything else — including the empty default — is the command line ("cli").
func ProcessAlive ¶
ProcessAlive reports whether a process with the given PID is still running. A zombie (exited but not yet reaped by its parent) counts as DEAD: signal 0 succeeds on zombies, but the process can never run again. Treating zombies as alive made Terminate report "still alive after SIGKILL" (delete-session bailed without removing the record) and made spawn/attach believe a dead daemon was still serving its session.
func SpawnDaemon ¶
SpawnDaemon launches a detached `rysh daemon <name>` process for an existing (but currently stopped) session, mirroring the spawn path the CLI uses on attach. workDir is the working directory the daemon starts in (normally the session's recorded Path); configFile, when non-empty, is propagated via --config so the spawned daemon resolves the same rysh directory / registry as the caller.
The child inherits the caller's environment (so RYSH_SESSION_SOURCE and other RYSH_* settings carry over) with RYSH_SESSION pinned to name. It starts in its own session/process group (see daemonSysProcAttr) with no controlling terminal and is released, so it outlives the caller — exactly like the CLI's own daemon spawn.
func Terminate ¶
Terminate gracefully stops the process with the given PID and never sends a bare SIGKILL. It sends SIGTERM first so the process can run its shutdown handlers — a rysh daemon flushes its actor state to JetStream KV and marks its session record stopped on SIGTERM — waits up to grace for it to exit, and only then escalates to SIGKILL so a wedged process that ignores SIGTERM still dies (and never lingers as an orphan holding the NATS port). Returns nil once the process is gone (or was already gone).
func UpdateAppClients ¶
UpdateAppClients records the daemon's live desktop-app (WebSocket) client count on its session record, read-modify-write so attach bookkeeping (TUIPIDs, state) set by other writers is preserved. Failures are silent — presence display must never break the daemon. Called by the web hub on every client connect/disconnect transition.
func UpdateProxyPort ¶
UpdateProxyPort records the governance proxy's port (design 001) on the session record, read-modify-write so attach bookkeeping (TUIPIDs, state, app-client count) set by other writers is preserved. port 0 clears it (proxy stopped). Failures are silent — recording the endpoint must never break the daemon. Called from ##proxy on/off.
Types ¶
type ReapAction ¶
type ReapAction struct {
Name string
Killed bool // a live-but-wedged daemon was terminated
Reason string // human-readable explanation
}
ReapAction records what the reaper did to a single session, for logging.
type ReapOptions ¶
type ReapOptions struct {
// NATSProbeTimeout bounds a single NATS reachability probe.
NATSProbeTimeout time.Duration
// NATSProbeAttempts is how many times to probe before declaring a live
// daemon unreachable, guarding against a transient blip killing a healthy
// daemon.
NATSProbeAttempts int
// StartupGrace skips records updated within this window, so a daemon that is
// still coming up (PID alive, NATS not yet listening) is never mistaken for a
// wedged orphan and killed.
StartupGrace time.Duration
// contains filtered or unexported fields
}
ReapOptions tunes orphan detection. The zero value is sensible — every field falls back to a default.
type Record ¶
type Record struct {
Name string `json:"name"`
Path string `json:"path"`
State string `json:"state"`
PID int `json:"pid,omitempty"` // daemon process PID
TUIPIDs []int `json:"tui_pids,omitempty"` // all attached TUI process PIDs
// AppClients is the number of desktop-app (WebSocket) clients currently
// connected to the daemon's web server. The registry's State field tracks
// TUI attachment only, so app sessions read "detached" even while the
// desktop app is driving them — this field lets list/info render them as
// "attached (app)". Maintained live by the daemon's web hub.
AppClients int `json:"app_clients,omitempty"`
NATSPort int `json:"nats_port,omitempty"`
// ProxyPort is the loopback governance proxy's port (design 001) while it is
// running, or 0 when it is off. Maintained live by the daemon on ##proxy
// on/off (read-modify-write, so attach bookkeeping is preserved), so tools
// inspecting the registry — and ##session info — can see the governed
// endpoint (http://127.0.0.1:{ProxyPort}) without attaching a TUI.
ProxyPort int `json:"proxy_port,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
// Version and BinHash identify the rysh binary that the running daemon was
// launched from. They are stamped by the daemon at startup and compared on
// attach so a rebuilt binary can detect that the live daemon is outdated and
// (with --upgrade) restart it. Version is the human-readable build string
// (main.version, e.g. a git-describe tag); BinHash is a short content hash of
// the executable, which changes on every rebuild even when Version does not
// (e.g. repeated "-dirty" dev builds at the same commit).
Version string `json:"version,omitempty"`
BinHash string `json:"bin_hash,omitempty"`
// ConfigFile is the rysh.config.yaml the daemon loaded (empty when none was
// found), and RyshDir is the rysh directory that config location implied
// (see config.resolveConfig). They are recorded so the session — and tools
// that inspect the registry (e.g. "rysh list-sessions") — are aware of which
// configuration and rysh state directory this session resolved at startup.
ConfigFile string `json:"config_file,omitempty"`
RyshDir string `json:"rysh_dir,omitempty"`
// Source identifies the front-end that owns the session: "cli" (rysh command
// line) or "app" (rysh desktop app). It is stamped by the daemon at startup
// from cfg.SessionSource (RYSH_SESSION_SOURCE). The two front-ends refuse to
// open each other's sessions so the CLI TUI and the desktop app never drive
// the same saved layout. Empty means legacy/unknown and is treated as
// compatible with either front-end.
Source string `json:"source,omitempty"`
}
func SelfHeal ¶
SelfHeal returns the record a live daemon should have on disk, given what is actually there. It is the daemon-side truth check behind the boot Upsert's standing guarantee (see startSessionRecordGuard in main): existing is the record read from the registry (found reports whether one exists), self is the record the daemon wrote at boot (name, path, PID, NATS port, version, config identity, source).
The record is left untouched (changed=false) when it already reflects this daemon: same PID, same NATS port, and a live state (running/detached) — the attach flow owns TUIPIDs and the running/detached flip, so a truthful record is never rewritten. Anything else (missing file, foreign or stale PID, wrong NATS port, state "stopped" while the daemon is alive) is replaced with the daemon's own identity; the TUI bookkeeping is preserved when the record was otherwise ours.
func WaitReady ¶
WaitReady polls the registry until the named session's daemon has recorded a live PID and NATS port (i.e. it is ready to accept connections), or until the timeout elapses. It is the same readiness gate the CLI uses after spawning a daemon.
func (*Record) AliveTUIPIDs ¶
AliveTUIPIDs returns only the PIDs that are still alive.
func (*Record) CleanStalePIDs ¶
func (r *Record) CleanStalePIDs()
CleanStalePIDs removes dead PIDs from TUIPIDs.
func (*Record) DaemonAlive ¶
DaemonAlive reports whether this record's daemon process is currently running.
func (*Record) HasAliveTUIs ¶
HasAliveTUIs checks if any tracked TUI PIDs are still alive.
func (*Record) RemoveTUIPID ¶
RemoveTUIPID removes a TUI PID from the active list.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
func (*Store) Reap ¶
func (s *Store) Reap(opts ReapOptions) ([]ReapAction, error)
Reap scans the registry and cleans up orphaned daemons:
- A record whose daemon PID is dead is reconciled to stopped (this happens as a side effect of List; nothing is killed).
- A daemon that is alive but unreachable on its recorded NATS port — i.e. wedged or zombied, no longer serving — is gracefully terminated (SIGTERM→SIGKILL via Terminate) and its record marked stopped.
Healthy daemons (PID alive AND NATS reachable) are deliberately left running, even when detached and idle: a long-lived detached session that you can re-attach to is an intended feature, so the reaper never kills a daemon that is still serving. To stop such a session, use "rysh stop" / "rysh delete-session".
Reaping is best-effort: a probe or terminate failure is recorded in the returned actions but never aborts the sweep. The returned slice contains only sessions the reaper acted on (or tried to).