| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- package opencode
-
- import (
- "bufio"
- "encoding/json"
- "fmt"
- "io"
- "net"
- "net/http"
- "os"
- "os/exec"
- "path/filepath"
- "strconv"
- "sync"
- "time"
- )
-
- // Process 表示 opencode 进程
- type Process struct {
- cmd *exec.Cmd
- port int
- logChan chan string
- mu sync.Mutex
- running bool
- wg sync.WaitGroup
- }
-
- // GetPort 返回 opencode 服务的端口
- func (p *Process) GetPort() int {
- return p.port
- }
-
- // GetLogs 返回日志通道
- func (p *Process) GetLogs() <-chan string {
- return p.logChan
- }
-
- // Close 停止 opencode 进程(实现 io.Closer 接口)
- func (p *Process) Close() error {
- return p.Stop()
- }
-
- // Stop 停止 opencode 进程
- func (p *Process) Stop() error {
- p.mu.Lock()
- defer p.mu.Unlock()
-
- if !p.running || p.cmd == nil {
- return nil
- }
-
- fmt.Fprintf(os.Stderr, "[opencode] 正在停止 opencode 进程...\n")
- // logger.Info("正在停止 opencode 进程...")
-
- // 尝试优雅停止
- if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
- fmt.Fprintf(os.Stderr, "[opencode] 警告: 发送中断信号失败: %v\n", err)
- // logger.Warn(fmt.Sprintf("发送中断信号失败: %v", err))
- // 强制终止
- if err := p.cmd.Process.Kill(); err != nil {
- fmt.Fprintf(os.Stderr, "[opencode] 错误: 强制终止进程失败: %v\n", err)
- // logger.Error(fmt.Sprintf("强制终止进程失败: %v", err))
- }
- }
-
- // 等待进程退出(通过 WaitGroup)
- done := make(chan struct{})
- go func() {
- p.wg.Wait()
- close(done)
- }()
-
- select {
- case <-done:
- p.running = false
- fmt.Fprintf(os.Stderr, "[opencode] 调试: opencode 进程已停止\n")
- // logger.Debug("opencode 进程已停止")
- case <-time.After(5 * time.Second):
- fmt.Fprintf(os.Stderr, "[opencode] 警告: opencode 进程在5秒内未退出,强制终止\n")
- // logger.Warn("opencode 进程在5秒内未退出,强制终止")
- if err := p.cmd.Process.Kill(); err != nil {
- fmt.Fprintf(os.Stderr, "[opencode] 错误: 最终强制终止失败: %v\n", err)
- // logger.Error(fmt.Sprintf("最终强制终止失败: %v", err))
- }
- p.running = false
- }
-
- close(p.logChan)
- return nil
- }
-
- // Start 启动 opencode 进程
- func Start(port int) (*Process, error) {
- cmd := exec.Command("opencode", "serve",
- "--hostname", "127.0.0.1",
- "--port", strconv.Itoa(port),
- "--log-level", "INFO",
- )
-
- logChan := make(chan string, 100)
-
- // 硬编码配置:设置项目存储路径
- // 使用绝对路径确保可靠性,不依赖当前工作目录
- workspacePath := "/Users/kenqdy/Documents/v-bdx-workspace"
- basePath := filepath.Join(workspacePath, "opencode_projects")
-
- // 确保目录存在
- if err := os.MkdirAll(basePath, 0755); err != nil {
- return nil, fmt.Errorf("创建项目目录失败: %w", err)
- }
-
- config := map[string]interface{}{
- "base_path": basePath,
- }
- configJSON, err := json.Marshal(config)
- if err != nil {
- return nil, fmt.Errorf("编码配置失败: %w", err)
- }
- cmd.Env = append(os.Environ(), "OPENCODE_CONFIG_CONTENT="+string(configJSON))
-
- // 记录配置信息
- // 注意:logger 可能未初始化,使用 fmt 输出到标准错误避免 panic
- fmt.Fprintf(os.Stderr, "[opencode] 启动服务,配置 base_path: %s\n", basePath)
-
- // 捕获标准输出
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- return nil, fmt.Errorf("获取标准输出管道失败: %w", err)
- }
-
- // 捕获标准错误
- stderr, err := cmd.StderrPipe()
- if err != nil {
- return nil, fmt.Errorf("获取标准错误管道失败: %w", err)
- }
-
- // 启动进程
- if err := cmd.Start(); err != nil {
- return nil, fmt.Errorf("启动 opencode 进程失败: %w", err)
- }
-
- process := &Process{
- cmd: cmd,
- port: port,
- logChan: logChan,
- running: true,
- }
-
- // 读取日志
- process.wg.Add(2)
- go func() {
- defer process.wg.Done()
- readLogs(stdout, logChan, "stdout")
- }()
- go func() {
- defer process.wg.Done()
- readLogs(stderr, logChan, "stderr")
- }()
-
- // 监控进程退出
- process.wg.Add(1)
- go func() {
- defer process.wg.Done()
- err := cmd.Wait()
- // 注意:不修改 running 状态,由 Stop() 方法处理
- // 仅记录日志
- if err != nil {
- select {
- case logChan <- fmt.Sprintf("进程异常退出: %v", err):
- default:
- }
- } else {
- select {
- case logChan <- "进程正常退出":
- default:
- }
- }
- }()
-
- // logger.Debug(fmt.Sprintf("opencode 进程已启动,端口: %d,PID: %d", port, cmd.Process.Pid))
- fmt.Fprintf(os.Stderr, "[opencode] 进程已启动,端口: %d,PID: %d\n", port, cmd.Process.Pid)
- return process, nil
- }
-
- // readLogs 读取进程输出并发送到日志通道
- func readLogs(reader io.Reader, logChan chan<- string, source string) {
- scanner := bufio.NewScanner(reader)
- for scanner.Scan() {
- line := scanner.Text()
- select {
- case logChan <- fmt.Sprintf("[%s] %s", source, line):
- default:
- // 如果通道满,丢弃旧日志(创建临时通道)
- tempChan := make(chan string, 1)
- select {
- case tempChan <- fmt.Sprintf("[%s] %s", source, line):
- // 无法发送,直接丢弃
- default:
- }
- }
- }
- if err := scanner.Err(); err != nil {
- select {
- case logChan <- fmt.Sprintf("[%s] 读取错误: %v", source, err):
- default:
- }
- }
- }
-
- // GetAvailablePort 获取可用端口
- func GetAvailablePort() (int, error) {
- // 使用简化的端口获取,无需解析TCP地址
- listener, err := net.Listen("tcp", "127.0.0.1:0")
- if err != nil {
- return 0, err
- }
- defer listener.Close()
-
- addr := listener.Addr().(*net.TCPAddr)
- return addr.Port, nil
- }
-
- // WaitForReady 等待 opencode 服务就绪
- func (p *Process) WaitForReady(timeout time.Duration) error {
- url := fmt.Sprintf("http://127.0.0.1:%d/global/health", p.port)
- client := &http.Client{Timeout: 5 * time.Second}
-
- start := time.Now()
- for time.Since(start) < timeout {
- resp, err := client.Get(url)
- if err == nil && resp.StatusCode == 200 {
- resp.Body.Close()
- return nil
- }
- if resp != nil {
- resp.Body.Close()
- }
- time.Sleep(100 * time.Millisecond)
- }
-
- return fmt.Errorf("opencode 服务在 %v 内未就绪", timeout)
- }
|