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}