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