background.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. package shell
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "sync"
  7. "sync/atomic"
  8. "time"
  9. "github.com/charmbracelet/crush/internal/csync"
  10. )
  11. const (
  12. // MaxBackgroundJobs is the maximum number of concurrent background jobs allowed
  13. MaxBackgroundJobs = 50
  14. // CompletedJobRetentionMinutes is how long to keep completed jobs before auto-cleanup (8 hours)
  15. CompletedJobRetentionMinutes = 8 * 60
  16. )
  17. // BackgroundShell represents a shell running in the background.
  18. type BackgroundShell struct {
  19. ID string
  20. Command string
  21. Description string
  22. Shell *Shell
  23. WorkingDir string
  24. ctx context.Context
  25. cancel context.CancelFunc
  26. stdout *bytes.Buffer
  27. stderr *bytes.Buffer
  28. done chan struct{}
  29. exitErr error
  30. completedAt int64 // Unix timestamp when job completed (0 if still running)
  31. }
  32. // BackgroundShellManager manages background shell instances.
  33. type BackgroundShellManager struct {
  34. shells *csync.Map[string, *BackgroundShell]
  35. }
  36. var (
  37. backgroundManager *BackgroundShellManager
  38. backgroundManagerOnce sync.Once
  39. idCounter atomic.Uint64
  40. )
  41. // GetBackgroundShellManager returns the singleton background shell manager.
  42. func GetBackgroundShellManager() *BackgroundShellManager {
  43. backgroundManagerOnce.Do(func() {
  44. backgroundManager = &BackgroundShellManager{
  45. shells: csync.NewMap[string, *BackgroundShell](),
  46. }
  47. })
  48. return backgroundManager
  49. }
  50. // Start creates and starts a new background shell with the given command.
  51. func (m *BackgroundShellManager) Start(ctx context.Context, workingDir string, blockFuncs []BlockFunc, command string, description string) (*BackgroundShell, error) {
  52. // Check job limit
  53. if m.shells.Len() >= MaxBackgroundJobs {
  54. return nil, fmt.Errorf("maximum number of background jobs (%d) reached. Please terminate or wait for some jobs to complete", MaxBackgroundJobs)
  55. }
  56. id := fmt.Sprintf("%03X", idCounter.Add(1))
  57. shell := NewShell(&Options{
  58. WorkingDir: workingDir,
  59. BlockFuncs: blockFuncs,
  60. })
  61. shellCtx, cancel := context.WithCancel(ctx)
  62. bgShell := &BackgroundShell{
  63. ID: id,
  64. Command: command,
  65. Description: description,
  66. WorkingDir: workingDir,
  67. Shell: shell,
  68. ctx: shellCtx,
  69. cancel: cancel,
  70. stdout: &bytes.Buffer{},
  71. stderr: &bytes.Buffer{},
  72. done: make(chan struct{}),
  73. }
  74. m.shells.Set(id, bgShell)
  75. go func() {
  76. defer close(bgShell.done)
  77. err := shell.ExecStream(shellCtx, command, bgShell.stdout, bgShell.stderr)
  78. bgShell.exitErr = err
  79. atomic.StoreInt64(&bgShell.completedAt, time.Now().Unix())
  80. }()
  81. return bgShell, nil
  82. }
  83. // Get retrieves a background shell by ID.
  84. func (m *BackgroundShellManager) Get(id string) (*BackgroundShell, bool) {
  85. return m.shells.Get(id)
  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.
  89. func (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. // Kill terminates a background shell by ID.
  97. func (m *BackgroundShellManager) Kill(id string) error {
  98. shell, ok := m.shells.Take(id)
  99. if !ok {
  100. return fmt.Errorf("background shell not found: %s", id)
  101. }
  102. shell.cancel()
  103. <-shell.done
  104. return nil
  105. }
  106. // BackgroundShellInfo contains information about a background shell.
  107. type BackgroundShellInfo struct {
  108. ID string
  109. Command string
  110. Description string
  111. }
  112. // List returns all background shell IDs.
  113. func (m *BackgroundShellManager) List() []string {
  114. ids := make([]string, 0, m.shells.Len())
  115. for id := range m.shells.Seq2() {
  116. ids = append(ids, id)
  117. }
  118. return ids
  119. }
  120. // Cleanup removes completed jobs that have been finished for more than the retention period
  121. func (m *BackgroundShellManager) Cleanup() int {
  122. now := time.Now().Unix()
  123. retentionSeconds := int64(CompletedJobRetentionMinutes * 60)
  124. var toRemove []string
  125. for shell := range m.shells.Seq() {
  126. completedAt := atomic.LoadInt64(&shell.completedAt)
  127. if completedAt > 0 && now-completedAt > retentionSeconds {
  128. toRemove = append(toRemove, shell.ID)
  129. }
  130. }
  131. for _, id := range toRemove {
  132. m.Remove(id)
  133. }
  134. return len(toRemove)
  135. }
  136. // KillAll terminates all background shells.
  137. func (m *BackgroundShellManager) KillAll() {
  138. shells := make([]*BackgroundShell, 0, m.shells.Len())
  139. for shell := range m.shells.Seq() {
  140. shells = append(shells, shell)
  141. }
  142. m.shells.Reset(map[string]*BackgroundShell{})
  143. for _, shell := range shells {
  144. shell.cancel()
  145. <-shell.done
  146. }
  147. }
  148. // GetOutput returns the current output of a background shell.
  149. func (bs *BackgroundShell) GetOutput() (stdout string, stderr string, done bool, err error) {
  150. select {
  151. case <-bs.done:
  152. return bs.stdout.String(), bs.stderr.String(), true, bs.exitErr
  153. default:
  154. return bs.stdout.String(), bs.stderr.String(), false, nil
  155. }
  156. }
  157. // IsDone checks if the background shell has finished execution.
  158. func (bs *BackgroundShell) IsDone() bool {
  159. select {
  160. case <-bs.done:
  161. return true
  162. default:
  163. return false
  164. }
  165. }
  166. // Wait blocks until the background shell completes.
  167. func (bs *BackgroundShell) Wait() {
  168. <-bs.done
  169. }