background.go

  1package shell
  2
  3import (
  4	"bytes"
  5	"context"
  6	"fmt"
  7	"sync"
  8
  9	"github.com/charmbracelet/hotdiva2000"
 10
 11	"github.com/charmbracelet/crush/internal/csync"
 12)
 13
 14// BackgroundShell represents a shell running in the background.
 15type BackgroundShell struct {
 16	ID         string
 17	Shell      *Shell
 18	ctx        context.Context
 19	cancel     context.CancelFunc
 20	stdout     *bytes.Buffer
 21	stderr     *bytes.Buffer
 22	mu         sync.RWMutex
 23	done       chan struct{}
 24	exitErr    error
 25	workingDir string
 26}
 27
 28// BackgroundShellManager manages background shell instances.
 29type BackgroundShellManager struct {
 30	shells *csync.Map[string, *BackgroundShell]
 31}
 32
 33var (
 34	backgroundManager     *BackgroundShellManager
 35	backgroundManagerOnce sync.Once
 36)
 37
 38// GetBackgroundShellManager returns the singleton background shell manager.
 39func GetBackgroundShellManager() *BackgroundShellManager {
 40	backgroundManagerOnce.Do(func() {
 41		backgroundManager = &BackgroundShellManager{
 42			shells: csync.NewMap[string, *BackgroundShell](),
 43		}
 44	})
 45	return backgroundManager
 46}
 47
 48// Start creates and starts a new background shell with the given command.
 49func (m *BackgroundShellManager) Start(ctx context.Context, workingDir string, blockFuncs []BlockFunc, command string) (*BackgroundShell, error) {
 50	id := hotdiva2000.Generate()
 51
 52	shell := NewShell(&Options{
 53		WorkingDir: workingDir,
 54		BlockFuncs: blockFuncs,
 55	})
 56
 57	shellCtx, cancel := context.WithCancel(ctx)
 58
 59	bgShell := &BackgroundShell{
 60		ID:         id,
 61		Shell:      shell,
 62		ctx:        shellCtx,
 63		cancel:     cancel,
 64		stdout:     &bytes.Buffer{},
 65		stderr:     &bytes.Buffer{},
 66		done:       make(chan struct{}),
 67		workingDir: workingDir,
 68	}
 69
 70	m.shells.Set(id, bgShell)
 71
 72	go func() {
 73		defer close(bgShell.done)
 74
 75		stdout, stderr, err := shell.Exec(shellCtx, command)
 76
 77		bgShell.mu.Lock()
 78		bgShell.stdout.WriteString(stdout)
 79		bgShell.stderr.WriteString(stderr)
 80		bgShell.exitErr = err
 81		bgShell.mu.Unlock()
 82	}()
 83
 84	return bgShell, nil
 85}
 86
 87// Get retrieves a background shell by ID.
 88func (m *BackgroundShellManager) Get(id string) (*BackgroundShell, bool) {
 89	return m.shells.Get(id)
 90}
 91
 92// Kill terminates a background shell by ID.
 93func (m *BackgroundShellManager) Kill(id string) error {
 94	shell, ok := m.shells.Take(id)
 95	if !ok {
 96		return fmt.Errorf("background shell not found: %s", id)
 97	}
 98
 99	shell.cancel()
100	<-shell.done
101	return nil
102}
103
104// List returns all background shell IDs.
105func (m *BackgroundShellManager) List() []string {
106	ids := make([]string, 0, m.shells.Len())
107	for id := range m.shells.Seq2() {
108		ids = append(ids, id)
109	}
110	return ids
111}
112
113// KillAll terminates all background shells.
114func (m *BackgroundShellManager) KillAll() {
115	shells := make([]*BackgroundShell, 0, m.shells.Len())
116	for shell := range m.shells.Seq() {
117		shells = append(shells, shell)
118	}
119	m.shells.Reset(map[string]*BackgroundShell{})
120
121	for _, shell := range shells {
122		shell.cancel()
123		<-shell.done
124	}
125}
126
127// GetOutput returns the current output of a background shell.
128func (bs *BackgroundShell) GetOutput() (stdout string, stderr string, done bool, err error) {
129	bs.mu.RLock()
130	defer bs.mu.RUnlock()
131
132	select {
133	case <-bs.done:
134		return bs.stdout.String(), bs.stderr.String(), true, bs.exitErr
135	default:
136		return bs.stdout.String(), bs.stderr.String(), false, nil
137	}
138}
139
140// IsDone checks if the background shell has finished execution.
141func (bs *BackgroundShell) IsDone() bool {
142	select {
143	case <-bs.done:
144		return true
145	default:
146		return false
147	}
148}
149
150// Wait blocks until the background shell completes.
151func (bs *BackgroundShell) Wait() {
152	<-bs.done
153}
154
155// GetWorkingDir returns the current working directory of the background shell.
156func (bs *BackgroundShell) GetWorkingDir() string {
157	bs.mu.RLock()
158	defer bs.mu.RUnlock()
159	return bs.workingDir
160}