supervisor.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. package main
  2. import (
  3. "bufio"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "time"
  14. )
  15. // Status 服务状态快照。
  16. type Status struct {
  17. Running bool
  18. PID int
  19. Restarts int
  20. StartedAt time.Time
  21. LastError string
  22. URL string
  23. Updating bool
  24. }
  25. // Supervisor 守护 dsh web:隐藏终端启动,异常退出自动重启。
  26. type Supervisor struct {
  27. mu sync.Mutex
  28. cfg *Config
  29. log *Logger
  30. cmd *exec.Cmd
  31. pid int
  32. running bool
  33. desired bool
  34. restarts int
  35. startedAt time.Time
  36. lastError string
  37. updating bool
  38. binCache string
  39. nodeCache string
  40. webURL string // dsh web 输出的完整 URL(含访问 token)
  41. }
  42. func NewSupervisor(cfg *Config, log *Logger) *Supervisor {
  43. return &Supervisor{cfg: cfg, log: log}
  44. }
  45. // Start 设置期望运行并拉起守护循环。
  46. func (s *Supervisor) Start() {
  47. s.mu.Lock()
  48. if s.desired {
  49. s.mu.Unlock()
  50. return
  51. }
  52. s.desired = true
  53. s.mu.Unlock()
  54. go s.monitor()
  55. }
  56. // Stop 停止守护并结束子进程树。
  57. func (s *Supervisor) Stop() {
  58. s.mu.Lock()
  59. s.desired = false
  60. pid := s.pid
  61. s.webURL = ""
  62. s.mu.Unlock()
  63. if pid > 0 {
  64. s.log.Printf("停止 dsh web pid=%d", pid)
  65. _ = killTree(pid)
  66. }
  67. }
  68. // Restart 重启服务。
  69. func (s *Supervisor) Restart() {
  70. s.log.Printf("重启 dsh web")
  71. s.Stop()
  72. time.Sleep(500 * time.Millisecond)
  73. s.Start()
  74. }
  75. func (s *Supervisor) Status() Status {
  76. s.mu.Lock()
  77. defer s.mu.Unlock()
  78. host := s.cfg.WebHost
  79. if host == "" {
  80. host = "127.0.0.1"
  81. }
  82. url := fmt.Sprintf("http://%s:%d", host, s.cfg.WebPort)
  83. if s.webURL != "" {
  84. url = s.webURL // 优先使用 dsh web 输出的带 token URL
  85. }
  86. return Status{
  87. Running: s.running,
  88. PID: s.pid,
  89. Restarts: s.restarts,
  90. StartedAt: s.startedAt,
  91. LastError: s.lastError,
  92. URL: url,
  93. Updating: s.updating,
  94. }
  95. }
  96. func (s *Supervisor) setError(err error) {
  97. s.mu.Lock()
  98. s.lastError = err.Error()
  99. s.mu.Unlock()
  100. s.log.Printf("守护错误: %v", err)
  101. }
  102. func (s *Supervisor) delay() time.Duration {
  103. sec := s.cfg.RestartDelaySec
  104. if sec <= 0 {
  105. sec = 5
  106. }
  107. return time.Duration(sec) * time.Second
  108. }
  109. // monitor 守护循环:启动 -> 等待退出 -> 期望运行时延时重启。
  110. func (s *Supervisor) monitor() {
  111. for {
  112. s.mu.Lock()
  113. desired := s.desired
  114. s.mu.Unlock()
  115. if !desired {
  116. return
  117. }
  118. exe, args, err := s.resolveCommand()
  119. if err != nil {
  120. s.setError(err)
  121. s.sleepInterruptible(s.delay())
  122. continue
  123. }
  124. cmd := exec.Command(exe, args...)
  125. hideWindow(cmd)
  126. stdout, errOut := cmd.StdoutPipe()
  127. stderr, errErr := cmd.StderrPipe()
  128. if errOut != nil || errErr != nil {
  129. s.setError(errors.New("无法创建输出管道"))
  130. s.sleepInterruptible(s.delay())
  131. continue
  132. }
  133. if err := cmd.Start(); err != nil {
  134. s.setError(err)
  135. s.sleepInterruptible(s.delay())
  136. continue
  137. }
  138. pid := cmd.Process.Pid
  139. s.mu.Lock()
  140. s.cmd = cmd
  141. s.pid = pid
  142. s.running = true
  143. s.startedAt = time.Now()
  144. s.lastError = ""
  145. s.webURL = ""
  146. s.mu.Unlock()
  147. s.log.Printf("dsh web 已启动 pid=%d: %s %s", pid, exe, strings.Join(args, " "))
  148. go s.pipeLog("out", stdout)
  149. go s.pipeLog("err", stderr)
  150. waitErr := cmd.Wait()
  151. s.mu.Lock()
  152. s.running = false
  153. s.cmd = nil
  154. s.pid = 0
  155. stillDesired := s.desired
  156. if stillDesired {
  157. s.restarts++
  158. }
  159. s.mu.Unlock()
  160. s.log.Printf("dsh web 退出(err=%v),期望运行=%v", waitErr, stillDesired)
  161. if !stillDesired {
  162. return
  163. }
  164. sleep := s.delay()
  165. s.log.Printf("%v 后自动重启", sleep)
  166. s.sleepInterruptible(sleep)
  167. }
  168. }
  169. func (s *Supervisor) sleepInterruptible(d time.Duration) {
  170. deadline := time.Now().Add(d)
  171. for time.Now().Before(deadline) {
  172. s.mu.Lock()
  173. desired := s.desired
  174. s.mu.Unlock()
  175. if !desired {
  176. return
  177. }
  178. time.Sleep(200 * time.Millisecond)
  179. }
  180. }
  181. // pipeLog 把子进程输出写入日志文件。
  182. func (s *Supervisor) pipeLog(tag string, r io.ReadCloser) {
  183. if r == nil {
  184. return
  185. }
  186. sc := bufio.NewScanner(r)
  187. sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
  188. for sc.Scan() {
  189. line := sc.Text()
  190. s.log.Printf("[dsh %s] %s", tag, line)
  191. // 捕获 dsh web 启动时输出的访问地址(含 token),供托盘直接打开
  192. if idx := strings.Index(line, "http://"); idx >= 0 {
  193. url := strings.TrimSpace(line[idx:])
  194. if i := strings.IndexAny(url, " \t"); i > 0 {
  195. url = url[:i]
  196. }
  197. if strings.HasPrefix(url, "http://") && len(url) > len("http://") {
  198. s.mu.Lock()
  199. if url != s.webURL {
  200. s.webURL = url
  201. s.log.Printf("捕获 Web 访问地址(含 token)")
  202. }
  203. s.mu.Unlock()
  204. }
  205. }
  206. }
  207. }
  208. // resolveCommand 解析启动命令:优先 node + dsh 入口 JS(无 shell 包装,最小化窗口风险)。
  209. func (s *Supervisor) resolveCommand() (string, []string, error) {
  210. node := s.cfg.NodePath
  211. if node == "" {
  212. node = s.nodePath()
  213. }
  214. binJS := s.cfg.DshBinJS
  215. if binJS == "" {
  216. binJS = s.dshBinJS()
  217. }
  218. if node != "" && binJS != "" {
  219. args := []string{binJS, "web", "--no-open", "--host", s.cfg.WebHost, "--port", strconv.Itoa(s.cfg.WebPort)}
  220. return node, args, nil
  221. }
  222. if p, err := exec.LookPath("dsh.cmd"); err == nil {
  223. args := []string{"/c", p, "web", "--no-open", "--port", strconv.Itoa(s.cfg.WebPort)}
  224. return "cmd.exe", args, nil
  225. }
  226. if p, err := exec.LookPath("dsh"); err == nil {
  227. args := []string{"web", "--no-open", "--port", strconv.Itoa(s.cfg.WebPort)}
  228. return p, args, nil
  229. }
  230. return "", nil, errors.New("未找到 dsh 入口:请确认已全局安装 @deepseek-ai/dsh,或在配置中指定 nodePath / dshBinJs")
  231. }
  232. func (s *Supervisor) nodePath() string {
  233. s.mu.Lock()
  234. if s.nodeCache != "" {
  235. v := s.nodeCache
  236. s.mu.Unlock()
  237. return v
  238. }
  239. s.mu.Unlock()
  240. p, err := exec.LookPath("node")
  241. if err != nil {
  242. return ""
  243. }
  244. s.mu.Lock()
  245. s.nodeCache = p
  246. s.mu.Unlock()
  247. return p
  248. }
  249. // dshBinJS 探测 dsh 的 bin.js:APPDATA\npm 与 npm root -g。
  250. func (s *Supervisor) dshBinJS() string {
  251. s.mu.Lock()
  252. if s.binCache != "" {
  253. v := s.binCache
  254. s.mu.Unlock()
  255. return v
  256. }
  257. s.mu.Unlock()
  258. var candidates []string
  259. if appdata := os.Getenv("APPDATA"); appdata != "" {
  260. candidates = append(candidates, filepath.Join(appdata, "npm", "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"))
  261. }
  262. if root := npmGlobalRoot(); root != "" {
  263. candidates = append(candidates, filepath.Join(root, "@deepseek-ai", "dsh", "lib", "bin.js"))
  264. }
  265. for _, c := range candidates {
  266. if fileExists(c) {
  267. s.mu.Lock()
  268. s.binCache = c
  269. s.mu.Unlock()
  270. s.log.Printf("dsh 入口: %s", c)
  271. return c
  272. }
  273. }
  274. return ""
  275. }
  276. // installedVersion 读取已安装 dsh 版本。
  277. func (s *Supervisor) installedVersion() string {
  278. binJS := s.cfg.DshBinJS
  279. if binJS == "" {
  280. binJS = s.dshBinJS()
  281. }
  282. if binJS == "" {
  283. return ""
  284. }
  285. pkg := filepath.Join(filepath.Dir(filepath.Dir(binJS)), "package.json")
  286. return readPackageVersion(pkg)
  287. }
  288. func fileExists(p string) bool {
  289. st, err := os.Stat(p)
  290. return err == nil && !st.IsDir()
  291. }