main.go 4.8 KB

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