supervisor.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. package main
  2. import (
  3. "bufio"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "net"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "time"
  15. )
  16. // 稳定运行的判定阈值:超过它则清零失败计数(退避复位)。
  17. const healthyUptime = 30 * time.Second
  18. // Status 服务状态快照。
  19. type Status struct {
  20. Running bool
  21. PID int
  22. Restarts int
  23. Uptime time.Duration
  24. LastError string
  25. URL string
  26. Updating bool
  27. External bool // 端口被外部实例占用(非本程序启动)
  28. }
  29. // Supervisor 守护 dsh web:隐藏终端启动,异常退出自动重启(指数退避)。
  30. type Supervisor struct {
  31. mu sync.Mutex
  32. probeMu sync.Mutex // 串行化入口探测,避免并发重复探测
  33. cfg *Config
  34. log *Logger
  35. pid int
  36. running bool
  37. desired bool
  38. restarts int
  39. failures int
  40. startedAt time.Time
  41. lastError string
  42. updating bool
  43. external bool
  44. binCache string
  45. nodeCache string
  46. webURL string // dsh web 输出的完整 URL(含访问 token,仅存内存)
  47. autoMu sync.Mutex
  48. autoLoop bool // 定期更新循环是否在运行(保证单例)
  49. }
  50. func NewSupervisor(cfg *Config, log *Logger) *Supervisor {
  51. return &Supervisor{cfg: cfg, log: log}
  52. }
  53. // Start 设置期望运行并拉起守护循环。
  54. func (s *Supervisor) Start() {
  55. s.mu.Lock()
  56. if s.desired {
  57. s.mu.Unlock()
  58. return
  59. }
  60. s.desired = true
  61. s.failures = 0
  62. s.mu.Unlock()
  63. go s.monitor()
  64. }
  65. // Stop 同步停止:结束进程树并等待其真正退出(最多 8 秒)。
  66. func (s *Supervisor) Stop() {
  67. s.mu.Lock()
  68. s.desired = false
  69. pid := s.pid
  70. s.webURL = ""
  71. s.mu.Unlock()
  72. if pid > 0 {
  73. s.log.Printf("停止 dsh web pid=%d", pid)
  74. _ = killTree(pid)
  75. s.waitStopped(pid, 8*time.Second)
  76. }
  77. }
  78. // Restart 重启并等待端口释放,避免新旧实例争抢端口。
  79. func (s *Supervisor) Restart() {
  80. s.log.Printf("重启 dsh web")
  81. s.Stop()
  82. s.waitPortFree(5 * time.Second)
  83. s.Start()
  84. }
  85. func (s *Supervisor) Status() Status {
  86. s.mu.Lock()
  87. defer s.mu.Unlock()
  88. url := s.baseURL()
  89. if s.webURL != "" {
  90. url = s.webURL // 优先使用带 token 的地址
  91. }
  92. var uptime time.Duration
  93. if s.running && !s.startedAt.IsZero() {
  94. uptime = time.Since(s.startedAt)
  95. }
  96. return Status{
  97. Running: s.running,
  98. PID: s.pid,
  99. Restarts: s.restarts,
  100. Uptime: uptime,
  101. LastError: s.lastError,
  102. URL: url,
  103. Updating: s.updating,
  104. External: s.external,
  105. }
  106. }
  107. func (s *Supervisor) baseURL() string {
  108. host := s.cfg.WebHost
  109. if host == "" {
  110. host = "127.0.0.1"
  111. }
  112. return fmt.Sprintf("http://%s:%d", host, s.cfg.WebPort)
  113. }
  114. func (s *Supervisor) setError(err error) {
  115. s.mu.Lock()
  116. s.lastError = err.Error()
  117. s.mu.Unlock()
  118. s.log.Printf("守护错误: %v", err)
  119. }
  120. // backoff 指数退避:base * 2^failures(上限 2 分钟),避免故障时重启风暴。
  121. func (s *Supervisor) backoff() time.Duration {
  122. s.mu.Lock()
  123. failures := s.failures
  124. base := s.cfg.RestartDelaySec
  125. s.mu.Unlock()
  126. if base <= 0 {
  127. base = 5
  128. }
  129. d := time.Duration(base) * time.Second
  130. for i := 0; i < failures && i < 5; i++ {
  131. d *= 2
  132. }
  133. if d > 2*time.Minute {
  134. d = 2 * time.Minute
  135. }
  136. return d
  137. }
  138. // portBusyBackoff 端口占用时的温和退避(上限 30 秒):不是故障,只是等待外部实例释放。
  139. func (s *Supervisor) portBusyBackoff() time.Duration {
  140. d := s.backoff()
  141. if d > 30*time.Second {
  142. d = 30 * time.Second
  143. }
  144. return d
  145. }
  146. // monitor 守护循环:端口预检 -> 启动 -> 等待退出 -> 期望运行时退避重启。
  147. func (s *Supervisor) monitor() {
  148. for {
  149. if !s.isDesired() {
  150. return
  151. }
  152. // 端口预检:已有实例监听时不再启动,避免端口冲突导致的重启风暴
  153. if isPortOpen(s.cfg.WebHost, s.cfg.WebPort) {
  154. s.mu.Lock()
  155. s.running = false
  156. s.pid = 0
  157. s.external = true
  158. s.lastError = "端口已被占用(可能已有 dsh web 在运行)"
  159. s.mu.Unlock()
  160. s.log.Printf("端口 %s 已被占用,跳过启动(等待释放)", s.baseURL())
  161. s.bumpFailure()
  162. s.sleepInterruptible(s.portBusyBackoff())
  163. continue
  164. }
  165. s.mu.Lock()
  166. s.external = false
  167. s.mu.Unlock()
  168. exe, args, err := s.resolveCommand()
  169. if err != nil {
  170. s.setError(err)
  171. s.bumpFailure()
  172. s.sleepInterruptible(s.backoff())
  173. continue
  174. }
  175. cmd := exec.Command(exe, args...)
  176. hideWindow(cmd)
  177. stdout, errOut := cmd.StdoutPipe()
  178. stderr, errErr := cmd.StderrPipe()
  179. if errOut != nil || errErr != nil {
  180. s.setError(errors.New("无法创建输出管道"))
  181. s.bumpFailure()
  182. s.sleepInterruptible(s.backoff())
  183. continue
  184. }
  185. if err := cmd.Start(); err != nil {
  186. s.setError(err)
  187. s.bumpFailure()
  188. s.sleepInterruptible(s.backoff())
  189. continue
  190. }
  191. pid := cmd.Process.Pid
  192. startedAt := time.Now()
  193. s.mu.Lock()
  194. s.pid = pid
  195. s.running = true
  196. s.startedAt = startedAt
  197. s.lastError = ""
  198. s.webURL = ""
  199. s.mu.Unlock()
  200. s.log.Printf("dsh web 已启动 pid=%d: %s %s", pid, exe, strings.Join(args, " "))
  201. go s.pipeLog("out", stdout)
  202. go s.pipeLog("err", stderr)
  203. waitErr := cmd.Wait()
  204. ranFor := time.Since(startedAt)
  205. s.mu.Lock()
  206. s.running = false
  207. s.pid = 0
  208. stillDesired := s.desired
  209. if ranFor >= healthyUptime {
  210. s.failures = 0 // 稳定运行过,退避复位
  211. } else {
  212. s.failures++
  213. }
  214. if stillDesired {
  215. s.restarts++
  216. }
  217. s.mu.Unlock()
  218. s.log.Printf("dsh web 退出(err=%v,运行 %s),期望运行=%v", waitErr, ranFor.Round(time.Second), stillDesired)
  219. if !stillDesired {
  220. return
  221. }
  222. delay := s.backoff()
  223. s.log.Printf("%s 后自动重启", delay)
  224. s.sleepInterruptible(delay)
  225. }
  226. }
  227. func (s *Supervisor) isDesired() bool {
  228. s.mu.Lock()
  229. defer s.mu.Unlock()
  230. return s.desired
  231. }
  232. // bumpFailure 增加失败计数(探测/启动阶段失败)。
  233. func (s *Supervisor) bumpFailure() {
  234. s.mu.Lock()
  235. s.failures++
  236. s.mu.Unlock()
  237. }
  238. func (s *Supervisor) sleepInterruptible(d time.Duration) {
  239. deadline := time.Now().Add(d)
  240. for time.Now().Before(deadline) {
  241. if !s.isDesired() {
  242. return
  243. }
  244. time.Sleep(200 * time.Millisecond)
  245. }
  246. }
  247. func (s *Supervisor) waitStopped(pid int, timeout time.Duration) {
  248. deadline := time.Now().Add(timeout)
  249. for time.Now().Before(deadline) {
  250. if !processAlive(pid) {
  251. return
  252. }
  253. time.Sleep(100 * time.Millisecond)
  254. }
  255. s.log.Printf("等待 pid=%d 退出超时(继续)", pid)
  256. }
  257. func (s *Supervisor) waitPortFree(timeout time.Duration) {
  258. deadline := time.Now().Add(timeout)
  259. for time.Now().Before(deadline) {
  260. if !isPortOpen(s.cfg.WebHost, s.cfg.WebPort) {
  261. return
  262. }
  263. time.Sleep(150 * time.Millisecond)
  264. }
  265. }
  266. // pipeLog 把子进程输出写入日志(token 由 Logger 统一脱敏),并捕获访问地址。
  267. func (s *Supervisor) pipeLog(tag string, r io.ReadCloser) {
  268. if r == nil {
  269. return
  270. }
  271. sc := bufio.NewScanner(r)
  272. sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
  273. for sc.Scan() {
  274. line := sc.Text()
  275. s.log.Printf("[dsh %s] %s", tag, line)
  276. if url := extractHTTPURL(line); url != "" {
  277. s.mu.Lock()
  278. if url != s.webURL {
  279. s.webURL = url
  280. s.log.Printf("捕获 Web 访问地址(含 token,仅存内存)")
  281. }
  282. s.mu.Unlock()
  283. }
  284. }
  285. }
  286. // extractHTTPURL 从一行输出里取出 http(s) URL(去除尾随分隔符)。
  287. func extractHTTPURL(line string) string {
  288. idx := strings.Index(line, "http")
  289. if idx < 0 {
  290. return ""
  291. }
  292. url := strings.TrimSpace(line[idx:])
  293. if i := strings.IndexAny(url, " \t"); i > 0 {
  294. url = url[:i]
  295. }
  296. if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
  297. return url
  298. }
  299. return ""
  300. }
  301. // resolveCommand 解析启动命令:优先 node + dsh 入口 JS(无 shell 包装,避免弹窗与注入面)。
  302. func (s *Supervisor) resolveCommand() (string, []string, error) {
  303. node := s.cfg.NodePath
  304. if node == "" {
  305. node = s.nodePath()
  306. }
  307. binJS := s.cfg.DshBinJS
  308. if binJS == "" {
  309. binJS = s.dshBinJS()
  310. }
  311. port := strconv.Itoa(s.cfg.WebPort)
  312. if node != "" && binJS != "" {
  313. return node, []string{binJS, "web", "--no-open", "--host", s.cfg.WebHost, "--port", port}, nil
  314. }
  315. if p, err := exec.LookPath("dsh.cmd"); err == nil {
  316. return "cmd.exe", []string{"/c", p, "web", "--no-open", "--port", port}, nil
  317. }
  318. if p, err := exec.LookPath("dsh"); err == nil {
  319. return p, []string{"web", "--no-open", "--port", port}, nil
  320. }
  321. return "", nil, errors.New("未找到 dsh 入口:请确认已全局安装 @deepseek-ai/dsh,或在配置中指定 nodePath / dshBinJs")
  322. }
  323. func (s *Supervisor) nodePath() string {
  324. s.mu.Lock()
  325. if s.nodeCache != "" {
  326. v := s.nodeCache
  327. s.mu.Unlock()
  328. return v
  329. }
  330. s.mu.Unlock()
  331. p, err := exec.LookPath("node")
  332. if err != nil {
  333. return ""
  334. }
  335. s.mu.Lock()
  336. s.nodeCache = p
  337. s.mu.Unlock()
  338. return p
  339. }
  340. // dshBinJS 探测 dsh 的 bin.js(APPDATA\npm 与 npm root -g);probeMu 保证并发只探测一次。
  341. func (s *Supervisor) dshBinJS() string {
  342. s.probeMu.Lock()
  343. defer s.probeMu.Unlock()
  344. s.mu.Lock()
  345. if s.binCache != "" {
  346. v := s.binCache
  347. s.mu.Unlock()
  348. return v
  349. }
  350. s.mu.Unlock()
  351. var candidates []string
  352. if appdata := os.Getenv("APPDATA"); appdata != "" {
  353. candidates = append(candidates, filepath.Join(appdata, "npm", "node_modules", "@deepseek-ai", "dsh", "lib", "bin.js"))
  354. }
  355. if root := npmGlobalRoot(); root != "" {
  356. candidates = append(candidates, filepath.Join(root, "@deepseek-ai", "dsh", "lib", "bin.js"))
  357. }
  358. for _, c := range candidates {
  359. if fileExists(c) {
  360. s.mu.Lock()
  361. s.binCache = c
  362. s.mu.Unlock()
  363. s.log.Printf("dsh 入口: %s", c)
  364. return c
  365. }
  366. }
  367. return ""
  368. }
  369. // invalidateProbe 使入口缓存失效(更新后调用)。
  370. func (s *Supervisor) invalidateProbe() {
  371. s.mu.Lock()
  372. s.binCache = ""
  373. s.mu.Unlock()
  374. }
  375. // installedVersion 读取已安装 dsh 版本。
  376. func (s *Supervisor) installedVersion() string {
  377. binJS := s.cfg.DshBinJS
  378. if binJS == "" {
  379. binJS = s.dshBinJS()
  380. }
  381. if binJS == "" {
  382. return ""
  383. }
  384. return readPackageVersion(filepath.Join(filepath.Dir(filepath.Dir(binJS)), "package.json"))
  385. }
  386. func fileExists(p string) bool {
  387. st, err := os.Stat(p)
  388. return err == nil && !st.IsDir()
  389. }
  390. // isPortOpen 检测端口是否有监听者。
  391. func isPortOpen(host string, port int) bool {
  392. if host == "" {
  393. host = "127.0.0.1"
  394. }
  395. conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", host, port), 1200*time.Millisecond)
  396. if err != nil {
  397. return false
  398. }
  399. _ = conn.Close()
  400. return true
  401. }