feat: SessionLifecycleManager — auto-detect and repair dead tmux sessions
- Add internal/lifecycle/manager.go with Manager struct, Run() ticker loop (15s interval), EnsureAllSessions() for boot-time session creation, and reconcile() that recreates idle sessions and recovers working ones via SetFailed + CreateSession - Add state.SetFailed() to record crash timestamp on SessionState - Add internal/lifecycle/manager_test.go with mock tmux client and 3 tests: TestReconcileCreatesDeadSession, TestReconcileRecoversCrashedSession, TestEnsureAllSessions — all pass - Wire lifecycle.Manager into cmd/claude-failover/main.go after state init Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2d43580c18
commit
978b60ccf7
10 changed files with 810 additions and 32 deletions
89
internal/tmux/client.go
Normal file
89
internal/tmux/client.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Package tmux provides an interface and implementation for controlling tmux sessions.
|
||||
package tmux
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Client defines the operations the daemon needs on tmux sessions.
|
||||
type Client interface {
|
||||
// HasSession returns true if the named session exists.
|
||||
HasSession(name string) bool
|
||||
// CreateSession creates a new detached session with an optional working directory.
|
||||
CreateSession(name, workdir string) error
|
||||
// KillSession destroys the named session.
|
||||
KillSession(name string) error
|
||||
// SendKeys sends key strokes to the first window of a session.
|
||||
SendKeys(session, keys string) error
|
||||
// CapturePaneTail returns the last n lines from the session's active pane.
|
||||
CapturePaneTail(session string, lines int) (string, error)
|
||||
}
|
||||
|
||||
// ExecClient implements Client by shelling out to the tmux binary.
|
||||
type ExecClient struct{}
|
||||
|
||||
// NewExecClient returns a ready-to-use ExecClient.
|
||||
func NewExecClient() *ExecClient {
|
||||
return &ExecClient{}
|
||||
}
|
||||
|
||||
// run executes a tmux subcommand and returns combined output.
|
||||
func run(args ...string) (string, error) {
|
||||
cmd := exec.Command("tmux", args...)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
err := cmd.Run()
|
||||
return strings.TrimSpace(out.String()), err
|
||||
}
|
||||
|
||||
// HasSession returns true when `tmux has-session` exits 0.
|
||||
func (c *ExecClient) HasSession(name string) bool {
|
||||
_, err := run("has-session", "-t", name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// CreateSession creates a new detached tmux session.
|
||||
// If workdir is non-empty, the session starts in that directory.
|
||||
func (c *ExecClient) CreateSession(name, workdir string) error {
|
||||
args := []string{"new-session", "-d", "-s", name}
|
||||
if workdir != "" {
|
||||
args = append(args, "-c", workdir)
|
||||
}
|
||||
out, err := run(args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tmux new-session %q: %w (%s)", name, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// KillSession destroys the named session.
|
||||
func (c *ExecClient) KillSession(name string) error {
|
||||
out, err := run("kill-session", "-t", name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tmux kill-session %q: %w (%s)", name, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendKeys sends key strokes to the first pane of the session.
|
||||
func (c *ExecClient) SendKeys(session, keys string) error {
|
||||
out, err := run("send-keys", "-t", session, keys, "Enter")
|
||||
if err != nil {
|
||||
return fmt.Errorf("tmux send-keys %q: %w (%s)", session, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CapturePaneTail captures the last n lines from the session's active pane.
|
||||
func (c *ExecClient) CapturePaneTail(session string, lines int) (string, error) {
|
||||
out, err := run("capture-pane", "-p", "-t", session,
|
||||
"-S", fmt.Sprintf("-%d", lines))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("tmux capture-pane %q: %w (%s)", session, err, out)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue