116 lines
3.3 KiB
Go
116 lines
3.3 KiB
Go
package session
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// Lease is the on-disk format of .ctask/session.json. It identifies the ctask
|
|
// process holding this workspace open and records a heartbeat timestamp so
|
|
// that crashed sessions can be detected and cleaned up by a later session.
|
|
type Lease struct {
|
|
SessionID string `json:"session_id"`
|
|
PID int `json:"pid"`
|
|
Hostname string `json:"hostname"`
|
|
Username string `json:"username"`
|
|
Agent string `json:"agent"`
|
|
Mode string `json:"mode"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
LastHeartbeatAt time.Time `json:"last_heartbeat_at"`
|
|
Terminal string `json:"terminal"`
|
|
}
|
|
|
|
// LeasePath returns the absolute path of the lease file for the given workspace.
|
|
func LeasePath(wsDir string) string {
|
|
return filepath.Join(wsDir, ".ctask", "session.json")
|
|
}
|
|
|
|
// WriteLease marshals a lease to JSON and writes it to path.
|
|
// The .ctask/ directory is created if needed.
|
|
func WriteLease(path string, l *Lease) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
|
return err
|
|
}
|
|
data, err := json.MarshalIndent(l, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, data, 0644)
|
|
}
|
|
|
|
// ReadLease reads and parses a lease file. A missing file returns the
|
|
// stdlib os.ErrNotExist (via os.ReadFile), which callers should detect
|
|
// with errors.Is.
|
|
func ReadLease(path string) (*Lease, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var l Lease
|
|
if err := json.Unmarshal(data, &l); err != nil {
|
|
return nil, fmt.Errorf("parsing lease: %w", err)
|
|
}
|
|
return &l, nil
|
|
}
|
|
|
|
// currentUsername returns the current OS username, falling back to "unknown"
|
|
// on error. Never panics.
|
|
func currentUsername() string {
|
|
u, err := user.Current()
|
|
if err != nil || u == nil {
|
|
return "unknown"
|
|
}
|
|
if u.Username == "" {
|
|
return "unknown"
|
|
}
|
|
return u.Username
|
|
}
|
|
|
|
// currentHostname returns the current hostname, falling back to "unknown".
|
|
func currentHostname() string {
|
|
h, err := os.Hostname()
|
|
if err != nil || h == "" {
|
|
return "unknown"
|
|
}
|
|
return h
|
|
}
|
|
|
|
// currentTerminal is a best-effort terminal identifier based on common env vars.
|
|
// Returns "unknown" if none are set.
|
|
func currentTerminal() string {
|
|
for _, k := range []string{"TERM_PROGRAM", "WT_SESSION", "TERM"} {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// NewSessionID returns a session identifier in the format
|
|
// "<hostname>-<pid>-<YYYYMMDDHHMMSS>" using the given time in UTC.
|
|
func NewSessionID(hostname string, pid int, t time.Time) string {
|
|
return fmt.Sprintf("%s-%d-%s", hostname, pid, t.UTC().Format("20060102150405"))
|
|
}
|
|
|
|
// NewLease constructs a fresh lease for the current process. startedAt is used
|
|
// for both StartedAt and LastHeartbeatAt.
|
|
func NewLease(startedAt time.Time, agent, mode string) *Lease {
|
|
hostname := currentHostname()
|
|
pid := os.Getpid()
|
|
return &Lease{
|
|
SessionID: NewSessionID(hostname, pid, startedAt),
|
|
PID: pid,
|
|
Hostname: hostname,
|
|
Username: currentUsername(),
|
|
Agent: agent,
|
|
Mode: mode,
|
|
StartedAt: startedAt.UTC().Truncate(time.Second),
|
|
LastHeartbeatAt: startedAt.UTC().Truncate(time.Second),
|
|
Terminal: currentTerminal(),
|
|
}
|
|
}
|