package session import ( "context" "errors" "sync" "time" "supervisor/internal/domain" "supervisor/internal/store" "supervisor/internal/util" ) var ErrSessionNotFound = errors.New("session not found") var ErrSessionRunning = errors.New("session is running") type Manager struct { mu sync.RWMutex store store.SessionStore factory PTYFactory runtimes map[string]*Session scrollbackLimit int } func NewManager(store store.SessionStore, factory PTYFactory) *Manager { if factory == nil { factory = DefaultPTYFactory{} } return &Manager{ store: store, factory: factory, runtimes: make(map[string]*Session), scrollbackLimit: 512 * 1024, } } func (m *Manager) CreateSession(ctx context.Context, params CreateSessionParams) (domain.Session, error) { command := params.Command if command == "" { command = "bash" } s := domain.Session{ ID: util.NewID("sess"), Name: params.Name, AgentID: params.AgentID, Command: command, Status: domain.SessionStatusCreated, CreatedAt: time.Now().UTC(), } if s.Name == "" { s.Name = s.ID } runtime := NewSession(s, m.scrollbackLimit, func(session domain.Session) { _ = m.store.Upsert(context.Background(), session) }) m.mu.Lock() m.runtimes[s.ID] = runtime m.mu.Unlock() if err := m.store.Upsert(ctx, s); err != nil { return domain.Session{}, err } return s, nil } func (m *Manager) StartSession(_ context.Context, id string) error { runtime, err := m.runtimeByID(id) if err != nil { return err } return runtime.Start(m.factory) } func (m *Manager) StopSession(_ context.Context, id string) error { runtime, err := m.runtimeByID(id) if err != nil { return err } return runtime.Stop() } func (m *Manager) ListSessions(ctx context.Context) ([]domain.Session, error) { return m.store.List(ctx) } func (m *Manager) GetSession(ctx context.Context, id string) (domain.Session, error) { s, ok, err := m.store.Get(ctx, id) if err != nil { return domain.Session{}, err } if !ok { return domain.Session{}, ErrSessionNotFound } return s, nil } func (m *Manager) WriteInput(_ context.Context, id string, input string) error { runtime, err := m.runtimeByID(id) if err != nil { return err } return runtime.WriteInput(input) } func (m *Manager) Subscribe(id string) (<-chan domain.Event, func(), error) { runtime, err := m.runtimeByID(id) if err != nil { return nil, nil, err } _, ch, cancel := runtime.Subscribe() return ch, cancel, nil } func (m *Manager) Resize(_ context.Context, id string, cols, rows int) error { runtime, err := m.runtimeByID(id) if err != nil { return err } return runtime.Resize(cols, rows) } func (m *Manager) Scrollback(id string) ([]byte, error) { runtime, err := m.runtimeByID(id) if err != nil { return nil, err } return runtime.Scrollback(), nil } func (m *Manager) DeleteSession(ctx context.Context, id string) error { runtime, err := m.runtimeByID(id) if err != nil { return err } snapshot := runtime.Snapshot() if snapshot.Status == domain.SessionStatusRunning { return ErrSessionRunning } m.mu.Lock() delete(m.runtimes, id) m.mu.Unlock() return m.store.Delete(ctx, id) } func (m *Manager) runtimeByID(id string) (*Session, error) { m.mu.RLock() defer m.mu.RUnlock() runtime, ok := m.runtimes[id] if !ok { return nil, ErrSessionNotFound } return runtime, nil }