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