main.go 4.7 KB

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