main.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. package main
  2. import (
  3. "context"
  4. "log/slog"
  5. "os"
  6. "path/filepath"
  7. "sync"
  8. "time"
  9. tea "github.com/charmbracelet/bubbletea"
  10. zone "github.com/lrstanley/bubblezone"
  11. "github.com/sst/opencode/internal/app"
  12. "github.com/sst/opencode/internal/pubsub"
  13. "github.com/sst/opencode/internal/tui"
  14. "github.com/sst/opencode/pkg/client"
  15. )
  16. func main() {
  17. url := os.Getenv("OPENCODE_SERVER")
  18. httpClient, err := client.NewClientWithResponses(url)
  19. if err != nil {
  20. slog.Error("Failed to create client", "error", err)
  21. os.Exit(1)
  22. }
  23. paths, err := httpClient.PostPathGetWithResponse(context.Background())
  24. if err != nil {
  25. panic(err)
  26. }
  27. logfile := filepath.Join(paths.JSON200.Data, "log", "tui.log")
  28. if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
  29. err := os.MkdirAll(filepath.Dir(logfile), 0755)
  30. if err != nil {
  31. slog.Error("Failed to create log directory", "error", err)
  32. os.Exit(1)
  33. }
  34. }
  35. file, err := os.Create(logfile)
  36. if err != nil {
  37. slog.Error("Failed to create log file", "error", err)
  38. os.Exit(1)
  39. }
  40. defer file.Close()
  41. logger := slog.New(slog.NewTextHandler(file, &slog.HandlerOptions{Level: slog.LevelDebug}))
  42. slog.SetDefault(logger)
  43. // Create main context for the application
  44. ctx, cancel := context.WithCancel(context.Background())
  45. defer cancel()
  46. app_, err := app.New(ctx, httpClient)
  47. if err != nil {
  48. panic(err)
  49. }
  50. // Set up the TUI
  51. zone.NewGlobal()
  52. program := tea.NewProgram(
  53. tui.NewModel(app_),
  54. tea.WithAltScreen(),
  55. )
  56. eventClient, err := client.NewClient(url)
  57. if err != nil {
  58. slog.Error("Failed to create event client", "error", err)
  59. os.Exit(1)
  60. }
  61. evts, err := eventClient.Event(ctx)
  62. if err != nil {
  63. slog.Error("Failed to subscribe to events", "error", err)
  64. os.Exit(1)
  65. }
  66. go func() {
  67. for item := range evts {
  68. program.Send(item)
  69. }
  70. }()
  71. // Setup the subscriptions, this will send services events to the TUI
  72. ch, cancelSubs := setupSubscriptions(app_, ctx)
  73. // Create a context for the TUI message handler
  74. tuiCtx, tuiCancel := context.WithCancel(ctx)
  75. var tuiWg sync.WaitGroup
  76. tuiWg.Add(1)
  77. // Set up message handling for the TUI
  78. go func() {
  79. defer tuiWg.Done()
  80. // defer logging.RecoverPanic("TUI-message-handler", func() {
  81. // attemptTUIRecovery(program)
  82. // })
  83. for {
  84. select {
  85. case <-tuiCtx.Done():
  86. slog.Info("TUI message handler shutting down")
  87. return
  88. case msg, ok := <-ch:
  89. if !ok {
  90. slog.Info("TUI message channel closed")
  91. return
  92. }
  93. program.Send(msg)
  94. }
  95. }
  96. }()
  97. // Cleanup function for when the program exits
  98. cleanup := func() {
  99. // Cancel subscriptions first
  100. cancelSubs()
  101. // Then shutdown the app
  102. app_.Shutdown()
  103. // Then cancel TUI message handler
  104. tuiCancel()
  105. // Wait for TUI message handler to finish
  106. tuiWg.Wait()
  107. slog.Info("All goroutines cleaned up")
  108. }
  109. // Run the TUI
  110. result, err := program.Run()
  111. cleanup()
  112. if err != nil {
  113. slog.Error("TUI error", "error", err)
  114. // return fmt.Errorf("TUI error: %v", err)
  115. }
  116. slog.Info("TUI exited", "result", result)
  117. }
  118. func setupSubscriber[T any](
  119. ctx context.Context,
  120. wg *sync.WaitGroup,
  121. name string,
  122. subscriber func(context.Context) <-chan pubsub.Event[T],
  123. outputCh chan<- tea.Msg,
  124. ) {
  125. wg.Add(1)
  126. go func() {
  127. defer wg.Done()
  128. // defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
  129. subCh := subscriber(ctx)
  130. if subCh == nil {
  131. slog.Warn("subscription channel is nil", "name", name)
  132. return
  133. }
  134. for {
  135. select {
  136. case event, ok := <-subCh:
  137. if !ok {
  138. slog.Info("subscription channel closed", "name", name)
  139. return
  140. }
  141. var msg tea.Msg = event
  142. select {
  143. case outputCh <- msg:
  144. case <-time.After(2 * time.Second):
  145. slog.Warn("message dropped due to slow consumer", "name", name)
  146. case <-ctx.Done():
  147. slog.Info("subscription cancelled", "name", name)
  148. return
  149. }
  150. case <-ctx.Done():
  151. slog.Info("subscription cancelled", "name", name)
  152. return
  153. }
  154. }
  155. }()
  156. }
  157. func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) {
  158. ch := make(chan tea.Msg, 100)
  159. wg := sync.WaitGroup{}
  160. ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
  161. setupSubscriber(ctx, &wg, "status", app.Status.Subscribe, ch)
  162. cleanupFunc := func() {
  163. slog.Info("Cancelling all subscriptions")
  164. cancel() // Signal all goroutines to stop
  165. waitCh := make(chan struct{})
  166. go func() {
  167. // defer logging.RecoverPanic("subscription-cleanup", nil)
  168. wg.Wait()
  169. close(waitCh)
  170. }()
  171. select {
  172. case <-waitCh:
  173. slog.Info("All subscription goroutines completed successfully")
  174. close(ch) // Only close after all writers are confirmed done
  175. case <-time.After(5 * time.Second):
  176. slog.Warn("Timed out waiting for some subscription goroutines to complete")
  177. close(ch)
  178. }
  179. }
  180. return ch, cleanupFunc
  181. }