No Description
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.

manager.go 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. package container
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "os"
  7. "os/exec"
  8. "path/filepath"
  9. "sync"
  10. "syscall"
  11. "time"
  12. "git.x2erp.com/qdy/go-svc-code/internal/opencode/config"
  13. "git.x2erp.com/qdy/go-svc-code/internal/util"
  14. )
  15. // InstanceStatus 实例状态
  16. type InstanceStatus string
  17. const (
  18. StatusStarting InstanceStatus = "starting"
  19. StatusRunning InstanceStatus = "running"
  20. StatusStopping InstanceStatus = "stopping"
  21. StatusStopped InstanceStatus = "stopped"
  22. StatusError InstanceStatus = "error"
  23. )
  24. // OpenCodeInstance OpenCode实例
  25. type OpenCodeInstance struct {
  26. ProjectID string `json:"project_id"`
  27. Port int `json:"port"`
  28. PID int `json:"pid"`
  29. Status InstanceStatus `json:"status"`
  30. ConfigPath string `json:"config_path"`
  31. WorkDir string `json:"work_dir"`
  32. ToolURL string `json:"tool_url"`
  33. Token string `json:"token"`
  34. TenantID string `json:"tenant_id"`
  35. StartedAt time.Time `json:"started_at"`
  36. StoppedAt time.Time `json:"stopped_at,omitempty"`
  37. Process *os.Process `json:"-"`
  38. Cmd *exec.Cmd `json:"-"`
  39. LogFile *os.File `json:"-"`
  40. Error string `json:"error,omitempty"`
  41. }
  42. // InstanceManager 实例管理器
  43. type InstanceManager struct {
  44. basePort int // 基础端口(svc-code端口)
  45. basePath string // 项目基础目录
  46. instances map[string]*OpenCodeInstance // key: projectID
  47. nextPort int // 下一个可用端口
  48. mu sync.RWMutex
  49. configGen *config.ConfigGenerator
  50. }
  51. // NewInstanceManager 创建实例管理器
  52. func NewInstanceManager(basePort int, basePath, globalConfigPath string) (*InstanceManager, error) {
  53. // 创建配置生成器
  54. configGen, err := config.NewConfigGenerator(globalConfigPath)
  55. if err != nil {
  56. return nil, fmt.Errorf("创建配置生成器失败: %w", err)
  57. }
  58. // 确保基础目录存在
  59. if err := util.EnsureDirectoryExists(basePath); err != nil {
  60. return nil, fmt.Errorf("创建基础目录失败: %w", err)
  61. }
  62. manager := &InstanceManager{
  63. basePort: basePort,
  64. basePath: basePath,
  65. instances: make(map[string]*OpenCodeInstance),
  66. nextPort: basePort + 1, // 从基础端口+1开始
  67. configGen: configGen,
  68. }
  69. return manager, nil
  70. }
  71. // StartInstance 启动项目OpenCode实例
  72. func (m *InstanceManager) StartInstance(projectID, tenantID, toolURL, token string) (*OpenCodeInstance, error) {
  73. m.mu.Lock()
  74. defer m.mu.Unlock()
  75. // 检查实例是否已存在
  76. if instance, exists := m.instances[projectID]; exists {
  77. if instance.Status == StatusRunning || instance.Status == StatusStarting {
  78. return instance, fmt.Errorf("项目 %s 的实例已在运行中", projectID)
  79. }
  80. // 如果实例存在但已停止,先清理
  81. delete(m.instances, projectID)
  82. }
  83. // 验证项目ID作为目录名的合法性
  84. if err := util.ValidateProjectDirName(projectID); err != nil {
  85. return nil, fmt.Errorf("项目ID验证失败: %w", err)
  86. }
  87. // 创建项目目录
  88. projectPath := util.GetProjectPath(m.basePath, projectID)
  89. if err := util.CreateProjectDirectory(m.basePath, projectID); err != nil {
  90. return nil, fmt.Errorf("创建项目目录失败: %w", err)
  91. }
  92. // 分配端口
  93. port := m.nextPort
  94. m.nextPort++ // 简单递增策略
  95. // 生成配置
  96. projectConfig := config.ProjectConfig{
  97. ProjectID: projectID,
  98. TenantID: tenantID,
  99. ToolURL: toolURL,
  100. Token: token,
  101. Port: port,
  102. BasePath: m.basePath,
  103. }
  104. _, err := m.configGen.GenerateAndWrite(projectConfig)
  105. if err != nil {
  106. return nil, fmt.Errorf("生成配置失败: %w", err)
  107. }
  108. // 创建实例对象
  109. instance := &OpenCodeInstance{
  110. ProjectID: projectID,
  111. Port: port,
  112. Status: StatusStarting,
  113. ConfigPath: util.GetOpenCodeConfigPath(m.basePath, projectID),
  114. WorkDir: projectPath,
  115. ToolURL: toolURL,
  116. Token: token,
  117. TenantID: tenantID,
  118. StartedAt: time.Now(),
  119. }
  120. // 启动OpenCode进程
  121. if err := m.startOpenCodeProcess(instance); err != nil {
  122. instance.Status = StatusError
  123. instance.Error = err.Error()
  124. return instance, fmt.Errorf("启动OpenCode进程失败: %w", err)
  125. }
  126. // 保存实例
  127. m.instances[projectID] = instance
  128. log.Printf("OpenCode实例启动成功: 项目=%s, 端口=%d, PID=%d", projectID, port, instance.PID)
  129. return instance, nil
  130. }
  131. // startOpenCodeProcess 启动OpenCode进程
  132. func (m *InstanceManager) startOpenCodeProcess(instance *OpenCodeInstance) error {
  133. // 构建命令 - opencode serve会自动读取当前目录下的.opencode/opencode.json
  134. // 使用--port参数指定端口,同时确保配置文件中也有正确的端口
  135. cmd := exec.Command("opencode", "serve", "--port", fmt.Sprintf("%d", instance.Port))
  136. cmd.Dir = instance.WorkDir
  137. cmd.SysProcAttr = &syscall.SysProcAttr{
  138. Setpgid: true, // 创建进程组,便于管理
  139. }
  140. // 设置环境变量
  141. cmd.Env = append(os.Environ(),
  142. fmt.Sprintf("OPENCODE_PROJECT_ID=%s", instance.ProjectID),
  143. fmt.Sprintf("OPENCODE_TENANT_ID=%s", instance.TenantID),
  144. fmt.Sprintf("OPENCODE_PORT=%d", instance.Port),
  145. fmt.Sprintf("OPENCODE_TOOL_URL=%s", instance.ToolURL),
  146. )
  147. // 创建日志文件
  148. logsPath := util.GetProjectLogsPath(m.basePath, instance.ProjectID)
  149. logFile, err := os.Create(filepath.Join(logsPath, fmt.Sprintf("opencode-%s.log", time.Now().Format("20060102-150405"))))
  150. if err != nil {
  151. return fmt.Errorf("创建日志文件失败: %w", err)
  152. }
  153. // 重定向输出
  154. cmd.Stdout = logFile
  155. cmd.Stderr = logFile
  156. // 启动进程
  157. if err := cmd.Start(); err != nil {
  158. logFile.Close()
  159. return fmt.Errorf("执行命令失败: %w", err)
  160. }
  161. // 保存进程信息
  162. instance.Process = cmd.Process
  163. instance.Cmd = cmd
  164. instance.LogFile = logFile
  165. instance.PID = cmd.Process.Pid
  166. instance.Status = StatusRunning
  167. // 启动协程监控进程退出
  168. go m.monitorProcess(instance)
  169. return nil
  170. }
  171. // monitorProcess 监控进程状态
  172. func (m *InstanceManager) monitorProcess(instance *OpenCodeInstance) {
  173. err := instance.Cmd.Wait()
  174. m.mu.Lock()
  175. defer m.mu.Unlock()
  176. // 关闭日志文件
  177. if instance.LogFile != nil {
  178. instance.LogFile.Close()
  179. }
  180. // 更新实例状态
  181. if err != nil {
  182. instance.Status = StatusError
  183. instance.Error = err.Error()
  184. log.Printf("OpenCode进程异常退出: 项目=%s, PID=%d, 错误: %v", instance.ProjectID, instance.PID, err)
  185. } else {
  186. instance.Status = StatusStopped
  187. instance.StoppedAt = time.Now()
  188. log.Printf("OpenCode进程正常退出: 项目=%s, PID=%d", instance.ProjectID, instance.PID)
  189. }
  190. // 清理资源
  191. instance.Process = nil
  192. instance.Cmd = nil
  193. instance.LogFile = nil
  194. }
  195. // StopInstance 停止项目OpenCode实例
  196. func (m *InstanceManager) StopInstance(projectID string) error {
  197. m.mu.Lock()
  198. defer m.mu.Unlock()
  199. instance, exists := m.instances[projectID]
  200. if !exists {
  201. return fmt.Errorf("项目 %s 的实例不存在", projectID)
  202. }
  203. if instance.Status != StatusRunning && instance.Status != StatusStarting {
  204. return fmt.Errorf("项目 %s 的实例不在运行状态", projectID)
  205. }
  206. // 更新状态
  207. instance.Status = StatusStopping
  208. // 终止进程组
  209. if instance.Process != nil {
  210. // 终止整个进程组
  211. if err := syscall.Kill(-instance.Process.Pid, syscall.SIGTERM); err != nil {
  212. log.Printf("发送SIGTERM失败: %v,尝试强制终止", err)
  213. syscall.Kill(-instance.Process.Pid, syscall.SIGKILL)
  214. }
  215. // 等待进程退出
  216. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  217. defer cancel()
  218. done := make(chan error, 1)
  219. go func() {
  220. _, err := instance.Process.Wait()
  221. done <- err
  222. }()
  223. select {
  224. case <-ctx.Done():
  225. log.Printf("进程终止超时,强制杀死: 项目=%s, PID=%d", projectID, instance.PID)
  226. syscall.Kill(-instance.Process.Pid, syscall.SIGKILL)
  227. case <-done:
  228. // 进程已退出
  229. }
  230. }
  231. // 关闭日志文件
  232. if instance.LogFile != nil {
  233. instance.LogFile.Close()
  234. instance.LogFile = nil
  235. }
  236. // 更新状态
  237. instance.Status = StatusStopped
  238. instance.StoppedAt = time.Now()
  239. instance.Process = nil
  240. instance.Cmd = nil
  241. log.Printf("OpenCode实例已停止: 项目=%s", projectID)
  242. return nil
  243. }
  244. // GetInstance 获取项目实例
  245. func (m *InstanceManager) GetInstance(projectID string) *OpenCodeInstance {
  246. m.mu.RLock()
  247. defer m.mu.RUnlock()
  248. return m.instances[projectID]
  249. }
  250. // ListInstances 获取所有实例
  251. func (m *InstanceManager) ListInstances() []*OpenCodeInstance {
  252. m.mu.RLock()
  253. defer m.mu.RUnlock()
  254. instances := make([]*OpenCodeInstance, 0, len(m.instances))
  255. for _, instance := range m.instances {
  256. instances = append(instances, instance)
  257. }
  258. return instances
  259. }
  260. // GetInstanceStatus 获取实例状态
  261. func (m *InstanceManager) GetInstanceStatus(projectID string) (InstanceStatus, error) {
  262. m.mu.RLock()
  263. defer m.mu.RUnlock()
  264. instance, exists := m.instances[projectID]
  265. if !exists {
  266. return StatusStopped, fmt.Errorf("项目 %s 的实例不存在", projectID)
  267. }
  268. return instance.Status, nil
  269. }
  270. // Cleanup 清理所有实例
  271. func (m *InstanceManager) Cleanup() {
  272. m.mu.Lock()
  273. defer m.mu.Unlock()
  274. for projectID, instance := range m.instances {
  275. if instance.Status == StatusRunning || instance.Status == StatusStarting {
  276. log.Printf("清理运行中的实例: 项目=%s", projectID)
  277. m.stopInstanceInternal(instance)
  278. }
  279. }
  280. m.instances = make(map[string]*OpenCodeInstance)
  281. }
  282. // stopInstanceInternal 内部停止实例方法(无锁)
  283. func (m *InstanceManager) stopInstanceInternal(instance *OpenCodeInstance) {
  284. if instance.Process != nil {
  285. syscall.Kill(-instance.Process.Pid, syscall.SIGKILL)
  286. }
  287. if instance.LogFile != nil {
  288. instance.LogFile.Close()
  289. }
  290. instance.Status = StatusStopped
  291. instance.StoppedAt = time.Now()
  292. instance.Process = nil
  293. instance.Cmd = nil
  294. instance.LogFile = nil
  295. }
  296. // GetBasePort 获取基础端口
  297. func (m *InstanceManager) GetBasePort() int {
  298. return m.basePort
  299. }
  300. // GetBasePath 获取基础路径
  301. func (m *InstanceManager) GetBasePath() string {
  302. return m.basePath
  303. }
  304. // GetNextPort 获取下一个端口
  305. func (m *InstanceManager) GetNextPort() int {
  306. m.mu.RLock()
  307. defer m.mu.RUnlock()
  308. return m.nextPort
  309. }
  310. // GetInstanceByPort 根据端口获取实例
  311. func (m *InstanceManager) GetInstanceByPort(port int) *OpenCodeInstance {
  312. m.mu.RLock()
  313. defer m.mu.RUnlock()
  314. for _, instance := range m.instances {
  315. if instance.Port == port {
  316. return instance
  317. }
  318. }
  319. return nil
  320. }