feat: add supervisor prototype with embedded frontend

This commit is contained in:
root
2026-03-09 19:15:53 +01:00
parent 96c4ce1697
commit 84de557052
56 changed files with 4044 additions and 10 deletions

158
internal/session/manager.go Normal file
View File

@ -0,0 +1,158 @@
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
}