Brak opisu
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

process.go 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. package opencode
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net"
  8. "net/http"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "strconv"
  13. "sync"
  14. "time"
  15. )
  16. // Process 表示 opencode 进程
  17. type Process struct {
  18. cmd *exec.Cmd
  19. port int
  20. logChan chan string
  21. mu sync.Mutex
  22. running bool
  23. wg sync.WaitGroup
  24. }
  25. // GetPort 返回 opencode 服务的端口
  26. func (p *Process) GetPort() int {
  27. return p.port
  28. }
  29. // GetLogs 返回日志通道
  30. func (p *Process) GetLogs() <-chan string {
  31. return p.logChan
  32. }
  33. // Close 停止 opencode 进程(实现 io.Closer 接口)
  34. func (p *Process) Close() error {
  35. return p.Stop()
  36. }
  37. // Stop 停止 opencode 进程
  38. func (p *Process) Stop() error {
  39. p.mu.Lock()
  40. defer p.mu.Unlock()
  41. if !p.running || p.cmd == nil {
  42. return nil
  43. }
  44. fmt.Fprintf(os.Stderr, "[opencode] 正在停止 opencode 进程...\n")
  45. // logger.Info("正在停止 opencode 进程...")
  46. // 尝试优雅停止
  47. if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
  48. fmt.Fprintf(os.Stderr, "[opencode] 警告: 发送中断信号失败: %v\n", err)
  49. // logger.Warn(fmt.Sprintf("发送中断信号失败: %v", err))
  50. // 强制终止
  51. if err := p.cmd.Process.Kill(); err != nil {
  52. fmt.Fprintf(os.Stderr, "[opencode] 错误: 强制终止进程失败: %v\n", err)
  53. // logger.Error(fmt.Sprintf("强制终止进程失败: %v", err))
  54. }
  55. }
  56. // 等待进程退出(通过 WaitGroup)
  57. done := make(chan struct{})
  58. go func() {
  59. p.wg.Wait()
  60. close(done)
  61. }()
  62. select {
  63. case <-done:
  64. p.running = false
  65. fmt.Fprintf(os.Stderr, "[opencode] 调试: opencode 进程已停止\n")
  66. // logger.Debug("opencode 进程已停止")
  67. case <-time.After(5 * time.Second):
  68. fmt.Fprintf(os.Stderr, "[opencode] 警告: opencode 进程在5秒内未退出,强制终止\n")
  69. // logger.Warn("opencode 进程在5秒内未退出,强制终止")
  70. if err := p.cmd.Process.Kill(); err != nil {
  71. fmt.Fprintf(os.Stderr, "[opencode] 错误: 最终强制终止失败: %v\n", err)
  72. // logger.Error(fmt.Sprintf("最终强制终止失败: %v", err))
  73. }
  74. p.running = false
  75. }
  76. close(p.logChan)
  77. return nil
  78. }
  79. // Start 启动 opencode 进程
  80. func Start(port int) (*Process, error) {
  81. cmd := exec.Command("opencode", "serve",
  82. "--hostname", "127.0.0.1",
  83. "--port", strconv.Itoa(port),
  84. "--log-level", "INFO",
  85. )
  86. logChan := make(chan string, 100)
  87. // 硬编码配置:设置项目存储路径
  88. // 使用绝对路径确保可靠性,不依赖当前工作目录
  89. workspacePath := "/Users/kenqdy/Documents/v-bdx-workspace"
  90. basePath := filepath.Join(workspacePath, "opencode_projects")
  91. // 确保目录存在
  92. if err := os.MkdirAll(basePath, 0755); err != nil {
  93. return nil, fmt.Errorf("创建项目目录失败: %w", err)
  94. }
  95. config := map[string]interface{}{
  96. "base_path": basePath,
  97. }
  98. configJSON, err := json.Marshal(config)
  99. if err != nil {
  100. return nil, fmt.Errorf("编码配置失败: %w", err)
  101. }
  102. cmd.Env = append(os.Environ(), "OPENCODE_CONFIG_CONTENT="+string(configJSON))
  103. // 记录配置信息
  104. // 注意:logger 可能未初始化,使用 fmt 输出到标准错误避免 panic
  105. fmt.Fprintf(os.Stderr, "[opencode] 启动服务,配置 base_path: %s\n", basePath)
  106. // 捕获标准输出
  107. stdout, err := cmd.StdoutPipe()
  108. if err != nil {
  109. return nil, fmt.Errorf("获取标准输出管道失败: %w", err)
  110. }
  111. // 捕获标准错误
  112. stderr, err := cmd.StderrPipe()
  113. if err != nil {
  114. return nil, fmt.Errorf("获取标准错误管道失败: %w", err)
  115. }
  116. // 启动进程
  117. if err := cmd.Start(); err != nil {
  118. return nil, fmt.Errorf("启动 opencode 进程失败: %w", err)
  119. }
  120. process := &Process{
  121. cmd: cmd,
  122. port: port,
  123. logChan: logChan,
  124. running: true,
  125. }
  126. // 读取日志
  127. process.wg.Add(2)
  128. go func() {
  129. defer process.wg.Done()
  130. readLogs(stdout, logChan, "stdout")
  131. }()
  132. go func() {
  133. defer process.wg.Done()
  134. readLogs(stderr, logChan, "stderr")
  135. }()
  136. // 监控进程退出
  137. process.wg.Add(1)
  138. go func() {
  139. defer process.wg.Done()
  140. err := cmd.Wait()
  141. // 注意:不修改 running 状态,由 Stop() 方法处理
  142. // 仅记录日志
  143. if err != nil {
  144. select {
  145. case logChan <- fmt.Sprintf("进程异常退出: %v", err):
  146. default:
  147. }
  148. } else {
  149. select {
  150. case logChan <- "进程正常退出":
  151. default:
  152. }
  153. }
  154. }()
  155. // logger.Debug(fmt.Sprintf("opencode 进程已启动,端口: %d,PID: %d", port, cmd.Process.Pid))
  156. fmt.Fprintf(os.Stderr, "[opencode] 进程已启动,端口: %d,PID: %d\n", port, cmd.Process.Pid)
  157. return process, nil
  158. }
  159. // readLogs 读取进程输出并发送到日志通道
  160. func readLogs(reader io.Reader, logChan chan<- string, source string) {
  161. scanner := bufio.NewScanner(reader)
  162. for scanner.Scan() {
  163. line := scanner.Text()
  164. select {
  165. case logChan <- fmt.Sprintf("[%s] %s", source, line):
  166. default:
  167. // 如果通道满,丢弃旧日志(创建临时通道)
  168. tempChan := make(chan string, 1)
  169. select {
  170. case tempChan <- fmt.Sprintf("[%s] %s", source, line):
  171. // 无法发送,直接丢弃
  172. default:
  173. }
  174. }
  175. }
  176. if err := scanner.Err(); err != nil {
  177. select {
  178. case logChan <- fmt.Sprintf("[%s] 读取错误: %v", source, err):
  179. default:
  180. }
  181. }
  182. }
  183. // GetAvailablePort 获取可用端口
  184. func GetAvailablePort() (int, error) {
  185. // 使用简化的端口获取,无需解析TCP地址
  186. listener, err := net.Listen("tcp", "127.0.0.1:0")
  187. if err != nil {
  188. return 0, err
  189. }
  190. defer listener.Close()
  191. addr := listener.Addr().(*net.TCPAddr)
  192. return addr.Port, nil
  193. }
  194. // WaitForReady 等待 opencode 服务就绪
  195. func (p *Process) WaitForReady(timeout time.Duration) error {
  196. url := fmt.Sprintf("http://127.0.0.1:%d/global/health", p.port)
  197. client := &http.Client{Timeout: 5 * time.Second}
  198. start := time.Now()
  199. for time.Since(start) < timeout {
  200. resp, err := client.Get(url)
  201. if err == nil && resp.StatusCode == 200 {
  202. resp.Body.Close()
  203. return nil
  204. }
  205. if resp != nil {
  206. resp.Body.Close()
  207. }
  208. time.Sleep(100 * time.Millisecond)
  209. }
  210. return fmt.Errorf("opencode 服务在 %v 内未就绪", timeout)
  211. }