| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- package container
-
- import (
- "context"
- "fmt"
- "log"
- "os"
- "os/exec"
- "path/filepath"
- "sync"
- "syscall"
- "time"
-
- "git.x2erp.com/qdy/go-svc-code/internal/opencode/config"
- "git.x2erp.com/qdy/go-svc-code/internal/util"
- )
-
- // InstanceStatus 实例状态
- type InstanceStatus string
-
- const (
- StatusStarting InstanceStatus = "starting"
- StatusRunning InstanceStatus = "running"
- StatusStopping InstanceStatus = "stopping"
- StatusStopped InstanceStatus = "stopped"
- StatusError InstanceStatus = "error"
- )
-
- // OpenCodeInstance OpenCode实例
- type OpenCodeInstance struct {
- ProjectID string `json:"project_id"`
- Port int `json:"port"`
- PID int `json:"pid"`
- Status InstanceStatus `json:"status"`
- ConfigPath string `json:"config_path"`
- WorkDir string `json:"work_dir"`
- ToolURL string `json:"tool_url"`
- Token string `json:"token"`
- TenantID string `json:"tenant_id"`
- StartedAt time.Time `json:"started_at"`
- StoppedAt time.Time `json:"stopped_at,omitempty"`
- Process *os.Process `json:"-"`
- Cmd *exec.Cmd `json:"-"`
- LogFile *os.File `json:"-"`
- Error string `json:"error,omitempty"`
- }
-
- // InstanceManager 实例管理器
- type InstanceManager struct {
- basePort int // 基础端口(svc-code端口)
- basePath string // 项目基础目录
- instances map[string]*OpenCodeInstance // key: projectID
- nextPort int // 下一个可用端口
- mu sync.RWMutex
- configGen *config.ConfigGenerator
- }
-
- // NewInstanceManager 创建实例管理器
- func NewInstanceManager(basePort int, basePath, globalConfigPath string) (*InstanceManager, error) {
- // 创建配置生成器
- configGen, err := config.NewConfigGenerator(globalConfigPath)
- if err != nil {
- return nil, fmt.Errorf("创建配置生成器失败: %w", err)
- }
-
- // 确保基础目录存在
- if err := util.EnsureDirectoryExists(basePath); err != nil {
- return nil, fmt.Errorf("创建基础目录失败: %w", err)
- }
-
- manager := &InstanceManager{
- basePort: basePort,
- basePath: basePath,
- instances: make(map[string]*OpenCodeInstance),
- nextPort: basePort + 1, // 从基础端口+1开始
- configGen: configGen,
- }
-
- return manager, nil
- }
-
- // StartInstance 启动项目OpenCode实例
- func (m *InstanceManager) StartInstance(projectID, tenantID, toolURL, token string) (*OpenCodeInstance, error) {
- m.mu.Lock()
- defer m.mu.Unlock()
-
- // 检查实例是否已存在
- if instance, exists := m.instances[projectID]; exists {
- if instance.Status == StatusRunning || instance.Status == StatusStarting {
- return instance, fmt.Errorf("项目 %s 的实例已在运行中", projectID)
- }
- // 如果实例存在但已停止,先清理
- delete(m.instances, projectID)
- }
-
- // 验证项目ID作为目录名的合法性
- if err := util.ValidateProjectDirName(projectID); err != nil {
- return nil, fmt.Errorf("项目ID验证失败: %w", err)
- }
-
- // 创建项目目录
- projectPath := util.GetProjectPath(m.basePath, projectID)
- if err := util.CreateProjectDirectory(m.basePath, projectID); err != nil {
- return nil, fmt.Errorf("创建项目目录失败: %w", err)
- }
-
- // 分配端口
- port := m.nextPort
- m.nextPort++ // 简单递增策略
-
- // 生成配置
- projectConfig := config.ProjectConfig{
- ProjectID: projectID,
- TenantID: tenantID,
- ToolURL: toolURL,
- Token: token,
- Port: port,
- BasePath: m.basePath,
- }
-
- _, err := m.configGen.GenerateAndWrite(projectConfig)
- if err != nil {
- return nil, fmt.Errorf("生成配置失败: %w", err)
- }
-
- // 创建实例对象
- instance := &OpenCodeInstance{
- ProjectID: projectID,
- Port: port,
- Status: StatusStarting,
- ConfigPath: util.GetOpenCodeConfigPath(m.basePath, projectID),
- WorkDir: projectPath,
- ToolURL: toolURL,
- Token: token,
- TenantID: tenantID,
- StartedAt: time.Now(),
- }
-
- // 启动OpenCode进程
- if err := m.startOpenCodeProcess(instance); err != nil {
- instance.Status = StatusError
- instance.Error = err.Error()
- return instance, fmt.Errorf("启动OpenCode进程失败: %w", err)
- }
-
- // 保存实例
- m.instances[projectID] = instance
-
- log.Printf("OpenCode实例启动成功: 项目=%s, 端口=%d, PID=%d", projectID, port, instance.PID)
- return instance, nil
- }
-
- // startOpenCodeProcess 启动OpenCode进程
- func (m *InstanceManager) startOpenCodeProcess(instance *OpenCodeInstance) error {
- // 构建命令 - opencode serve会自动读取当前目录下的.opencode/opencode.json
- // 使用--port参数指定端口,同时确保配置文件中也有正确的端口
- cmd := exec.Command("opencode", "serve", "--port", fmt.Sprintf("%d", instance.Port))
- cmd.Dir = instance.WorkDir
- cmd.SysProcAttr = &syscall.SysProcAttr{
- Setpgid: true, // 创建进程组,便于管理
- }
-
- // 设置环境变量
- cmd.Env = append(os.Environ(),
- fmt.Sprintf("OPENCODE_PROJECT_ID=%s", instance.ProjectID),
- fmt.Sprintf("OPENCODE_TENANT_ID=%s", instance.TenantID),
- fmt.Sprintf("OPENCODE_PORT=%d", instance.Port),
- fmt.Sprintf("OPENCODE_TOOL_URL=%s", instance.ToolURL),
- )
-
- // 创建日志文件
- logsPath := util.GetProjectLogsPath(m.basePath, instance.ProjectID)
- logFile, err := os.Create(filepath.Join(logsPath, fmt.Sprintf("opencode-%s.log", time.Now().Format("20060102-150405"))))
- if err != nil {
- return fmt.Errorf("创建日志文件失败: %w", err)
- }
-
- // 重定向输出
- cmd.Stdout = logFile
- cmd.Stderr = logFile
-
- // 启动进程
- if err := cmd.Start(); err != nil {
- logFile.Close()
- return fmt.Errorf("执行命令失败: %w", err)
- }
-
- // 保存进程信息
- instance.Process = cmd.Process
- instance.Cmd = cmd
- instance.LogFile = logFile
- instance.PID = cmd.Process.Pid
- instance.Status = StatusRunning
-
- // 启动协程监控进程退出
- go m.monitorProcess(instance)
-
- return nil
- }
-
- // monitorProcess 监控进程状态
- func (m *InstanceManager) monitorProcess(instance *OpenCodeInstance) {
- err := instance.Cmd.Wait()
-
- m.mu.Lock()
- defer m.mu.Unlock()
-
- // 关闭日志文件
- if instance.LogFile != nil {
- instance.LogFile.Close()
- }
-
- // 更新实例状态
- if err != nil {
- instance.Status = StatusError
- instance.Error = err.Error()
- log.Printf("OpenCode进程异常退出: 项目=%s, PID=%d, 错误: %v", instance.ProjectID, instance.PID, err)
- } else {
- instance.Status = StatusStopped
- instance.StoppedAt = time.Now()
- log.Printf("OpenCode进程正常退出: 项目=%s, PID=%d", instance.ProjectID, instance.PID)
- }
-
- // 清理资源
- instance.Process = nil
- instance.Cmd = nil
- instance.LogFile = nil
- }
-
- // StopInstance 停止项目OpenCode实例
- func (m *InstanceManager) StopInstance(projectID string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
-
- instance, exists := m.instances[projectID]
- if !exists {
- return fmt.Errorf("项目 %s 的实例不存在", projectID)
- }
-
- if instance.Status != StatusRunning && instance.Status != StatusStarting {
- return fmt.Errorf("项目 %s 的实例不在运行状态", projectID)
- }
-
- // 更新状态
- instance.Status = StatusStopping
-
- // 终止进程组
- if instance.Process != nil {
- // 终止整个进程组
- if err := syscall.Kill(-instance.Process.Pid, syscall.SIGTERM); err != nil {
- log.Printf("发送SIGTERM失败: %v,尝试强制终止", err)
- syscall.Kill(-instance.Process.Pid, syscall.SIGKILL)
- }
-
- // 等待进程退出
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- done := make(chan error, 1)
- go func() {
- _, err := instance.Process.Wait()
- done <- err
- }()
-
- select {
- case <-ctx.Done():
- log.Printf("进程终止超时,强制杀死: 项目=%s, PID=%d", projectID, instance.PID)
- syscall.Kill(-instance.Process.Pid, syscall.SIGKILL)
- case <-done:
- // 进程已退出
- }
- }
-
- // 关闭日志文件
- if instance.LogFile != nil {
- instance.LogFile.Close()
- instance.LogFile = nil
- }
-
- // 更新状态
- instance.Status = StatusStopped
- instance.StoppedAt = time.Now()
- instance.Process = nil
- instance.Cmd = nil
-
- log.Printf("OpenCode实例已停止: 项目=%s", projectID)
- return nil
- }
-
- // GetInstance 获取项目实例
- func (m *InstanceManager) GetInstance(projectID string) *OpenCodeInstance {
- m.mu.RLock()
- defer m.mu.RUnlock()
-
- return m.instances[projectID]
- }
-
- // ListInstances 获取所有实例
- func (m *InstanceManager) ListInstances() []*OpenCodeInstance {
- m.mu.RLock()
- defer m.mu.RUnlock()
-
- instances := make([]*OpenCodeInstance, 0, len(m.instances))
- for _, instance := range m.instances {
- instances = append(instances, instance)
- }
-
- return instances
- }
-
- // GetInstanceStatus 获取实例状态
- func (m *InstanceManager) GetInstanceStatus(projectID string) (InstanceStatus, error) {
- m.mu.RLock()
- defer m.mu.RUnlock()
-
- instance, exists := m.instances[projectID]
- if !exists {
- return StatusStopped, fmt.Errorf("项目 %s 的实例不存在", projectID)
- }
-
- return instance.Status, nil
- }
-
- // Cleanup 清理所有实例
- func (m *InstanceManager) Cleanup() {
- m.mu.Lock()
- defer m.mu.Unlock()
-
- for projectID, instance := range m.instances {
- if instance.Status == StatusRunning || instance.Status == StatusStarting {
- log.Printf("清理运行中的实例: 项目=%s", projectID)
- m.stopInstanceInternal(instance)
- }
- }
-
- m.instances = make(map[string]*OpenCodeInstance)
- }
-
- // stopInstanceInternal 内部停止实例方法(无锁)
- func (m *InstanceManager) stopInstanceInternal(instance *OpenCodeInstance) {
- if instance.Process != nil {
- syscall.Kill(-instance.Process.Pid, syscall.SIGKILL)
- }
-
- if instance.LogFile != nil {
- instance.LogFile.Close()
- }
-
- instance.Status = StatusStopped
- instance.StoppedAt = time.Now()
- instance.Process = nil
- instance.Cmd = nil
- instance.LogFile = nil
- }
-
- // GetBasePort 获取基础端口
- func (m *InstanceManager) GetBasePort() int {
- return m.basePort
- }
-
- // GetBasePath 获取基础路径
- func (m *InstanceManager) GetBasePath() string {
- return m.basePath
- }
-
- // GetNextPort 获取下一个端口
- func (m *InstanceManager) GetNextPort() int {
- m.mu.RLock()
- defer m.mu.RUnlock()
- return m.nextPort
- }
-
- // GetInstanceByPort 根据端口获取实例
- func (m *InstanceManager) GetInstanceByPort(port int) *OpenCodeInstance {
- m.mu.RLock()
- defer m.mu.RUnlock()
-
- for _, instance := range m.instances {
- if instance.Port == port {
- return instance
- }
- }
-
- return nil
- }
|