130 lines
3.0 KiB
Go
130 lines
3.0 KiB
Go
package shell
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Manager manages multiple named shell sessions, one per terminal ID.
|
|
type Manager struct {
|
|
workDir string
|
|
logger *logger.Logger
|
|
terminals map[string]*Session
|
|
mu sync.RWMutex
|
|
stopping bool
|
|
}
|
|
|
|
// BeginStop closes terminal admission after any start already holding the
|
|
// manager lock has either committed or failed.
|
|
func (m *Manager) BeginStop() {
|
|
m.mu.Lock()
|
|
m.stopping = true
|
|
m.mu.Unlock()
|
|
}
|
|
|
|
// NewManager creates a new shell session manager.
|
|
func NewManager(workDir string, log *logger.Logger) *Manager {
|
|
return &Manager{
|
|
workDir: workDir,
|
|
logger: log.WithFields(zap.String("component", "shell-manager")),
|
|
terminals: make(map[string]*Session),
|
|
}
|
|
}
|
|
|
|
// Start creates and starts a new shell session for the given terminal ID.
|
|
// If a session already exists for this terminal, it is returned as-is.
|
|
func (m *Manager) Start(terminalID string, cfg Config) (*Session, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.stopping {
|
|
return nil, fmt.Errorf("shell manager is stopping")
|
|
}
|
|
|
|
if s, ok := m.terminals[terminalID]; ok {
|
|
return s, nil
|
|
}
|
|
|
|
if cfg.WorkDir == "" {
|
|
cfg.WorkDir = m.workDir
|
|
}
|
|
|
|
s, err := NewSession(cfg, m.logger)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create shell for terminal %s: %w", terminalID, err)
|
|
}
|
|
|
|
m.terminals[terminalID] = s
|
|
m.logger.Info("terminal shell started", zap.String("terminal_id", terminalID))
|
|
return s, nil
|
|
}
|
|
|
|
// Get returns the shell session for the given terminal ID.
|
|
func (m *Manager) Get(terminalID string) (*Session, bool) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
s, ok := m.terminals[terminalID]
|
|
return s, ok
|
|
}
|
|
|
|
// Stop stops and removes the shell session for the given terminal ID.
|
|
func (m *Manager) Stop(terminalID string) error {
|
|
m.mu.RLock()
|
|
s, ok := m.terminals[terminalID]
|
|
m.mu.RUnlock()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
m.logger.Info("stopping terminal shell", zap.String("terminal_id", terminalID))
|
|
if err := s.Stop(); err != nil {
|
|
return err
|
|
}
|
|
m.mu.Lock()
|
|
if m.terminals[terminalID] == s {
|
|
delete(m.terminals, terminalID)
|
|
}
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Buffer returns the buffered output for the given terminal ID.
|
|
func (m *Manager) Buffer(terminalID string) ([]byte, error) {
|
|
m.mu.RLock()
|
|
s, ok := m.terminals[terminalID]
|
|
m.mu.RUnlock()
|
|
|
|
if !ok {
|
|
return nil, fmt.Errorf("terminal %s not found", terminalID)
|
|
}
|
|
return s.GetBufferedOutput(), nil
|
|
}
|
|
|
|
// StopAll stops all terminal shell sessions.
|
|
func (m *Manager) StopAll() error {
|
|
m.mu.Lock()
|
|
terminals := make(map[string]*Session, len(m.terminals))
|
|
for id, s := range m.terminals {
|
|
terminals[id] = s
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
var errs []error
|
|
for id, s := range terminals {
|
|
m.logger.Info("stopping terminal shell (shutdown)", zap.String("terminal_id", id))
|
|
if err := s.Stop(); err != nil {
|
|
errs = append(errs, fmt.Errorf("stop terminal %s: %w", id, err))
|
|
continue
|
|
}
|
|
m.mu.Lock()
|
|
if m.terminals[id] == s {
|
|
delete(m.terminals, id)
|
|
}
|
|
m.mu.Unlock()
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|