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 }