Help us shape the future of Go! Take the 2026 Go Developer Survey

session

package
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 8, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Index

Constants

View Source
const (
	SourceCLI = domain.FrontendCLI
	SourceApp = domain.FrontendApp
)

SourceCLI and SourceApp are the canonical session Source values: the rysh command line vs the rysh desktop app. Source is PROVENANCE — which front-end created the session — and is stamped once, at creation. Either front-end may open either kind (see EnsureCanOpen); the value is used to describe what will degrade, not to deny access.

Variables

This section is empty.

Functions

func DegradationSummary added in v0.2.3

func DegradationSummary(rec Record, notes []string) string

DegradationSummary renders EnsureCanOpen's notes as a short block for a terminal or a command's output, or "" when there is nothing to report. The caller supplies the record so the summary can name the creating front-end.

func EnsureCanOpen added in v0.2.3

func EnsureCanOpen(rec Record, want string) ([]string, error)

EnsureCanOpen decides whether the front-end identified by want may open rec, and reports what will render differently if it does.

Both front-ends drive the same daemon over the same subjects, and a workspace layout is relative (flex weights, not pixels), so neither kind of session is structurally off-limits to the other. What differs is the RENDERER: the desktop app is a superset of the terminal, so it opens terminal sessions with nothing lost, while the terminal opens app sessions with the app-only render surfaces degraded to labelled placeholders.

The returned notes describe exactly those degradations (empty when there are none). Callers must surface them — a capability that vanishes silently is the failure mode this whole path exists to avoid.

The error return is reserved for a genuinely unopenable session. No combination of front-ends produces one today; it stays in the signature so the call sites keep handling the possibility rather than having to grow it back later.

A blank record source is legacy/unknown and opens cleanly from either front-end. want is normalized internally.

func FrontendName

func FrontendName(source string) string

FrontendName renders a session source as a human-readable front-end name for error messages.

func NormalizeSource

func NormalizeSource(s string) string

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

func ProcessAlive(pid int) bool

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

func SpawnDaemon(name, workDir, configFile string) error

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

func Terminate(pid int, grace time.Duration) error

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

func UpdateAppClients(cfg config.Config, name string, n int)

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

func UpdateProxyPort(cfg config.Config, name string, port int)

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.

func UpdateWebEndpoint added in v0.2.3

func UpdateWebEndpoint(cfg config.Config, name string, port int)

UpdateWebEndpoint records the daemon's live web-server port on the session record, read-modify-write so attach bookkeeping (TUIPIDs, state, app-client count, proxy port) set by other writers is preserved. port 0 clears it (web server stopped).

This is the hand-off the desktop app adopts a running daemon through: the renderer speaks only HTTP/WebSocket, so without a recorded endpoint the app can reach only daemons it spawned itself and knows the port of. Failures are silent — recording the endpoint must never break the daemon.

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 CREATED the session: "cli" (rysh
	// command line) or "app" (rysh desktop app). It is stamped once, by the
	// daemon that first writes the record, from cfg.SessionSource
	// (RYSH_SESSION_SOURCE), and preserved verbatim by every later daemon —
	// restarting an app session from the terminal must not rewrite its
	// provenance. Either front-end may open either kind; Source only tells
	// EnsureCanOpen which render surfaces to warn about. Empty means
	// legacy/unknown and opens cleanly from either front-end.
	Source string `json:"source,omitempty"`
	// WebPort is the daemon's live web-server port on loopback, or 0 when no
	// web server is running. The desktop app's renderer reaches a daemon only
	// over HTTP/WebSocket, so recording the endpoint here is what lets the app
	// adopt a session it did not spawn — including one created from the command
	// line, whose daemon starts its web server on demand (`##rysh web start`).
	// Maintained live by the daemon on web-server start/stop (see
	// UpdateWebEndpoint), read-modify-write so attach bookkeeping set by other
	// writers is preserved.
	//
	// A web_token used to sit beside it, carrying the access token that guarded
	// the port. Access tokens are gone (internal/web/auth.go): a UI served off
	// this port asks for the workspace's login instead, so the port is the whole
	// endpoint. Records written by older daemons may still carry the field; it
	// is ignored.
	WebPort int `json:"web_port,omitempty"`
}

func SelfHeal

func SelfHeal(existing Record, found bool, self Record) (healed Record, changed bool)

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

func WaitReady(store *Store, name string, timeout time.Duration) (Record, error)

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) AddTUIPID

func (r *Record) AddTUIPID(pid int)

AddTUIPID adds a TUI PID to the active list (idempotent).

func (*Record) AliveTUIPIDs

func (r *Record) AliveTUIPIDs() []int

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

func (r *Record) DaemonAlive() bool

DaemonAlive reports whether this record's daemon process is currently running.

func (*Record) HasAliveTUIs

func (r *Record) HasAliveTUIs() bool

HasAliveTUIs checks if any tracked TUI PIDs are still alive.

func (*Record) RemoveTUIPID

func (r *Record) RemoveTUIPID(pid int)

RemoveTUIPID removes a TUI PID from the active list.

type Store

type Store struct {
	// contains filtered or unexported fields
}

func NewStore

func NewStore(cfg config.Config) (*Store, error)

func (*Store) Delete

func (s *Store) Delete(name string) error

func (*Store) Get

func (s *Store) Get(name string) (Record, error)

func (*Store) List

func (s *Store) List() ([]Record, error)

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).

func (*Store) Upsert

func (s *Store) Upsert(record Record) (Record, error)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL