deployhttp.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. package main
  2. import (
  3. stdjson "encoding/json" // 标准库,避免 json-iterator nil map panic
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "os"
  8. "path/filepath"
  9. "runtime/debug"
  10. "time"
  11. "lc/common/util"
  12. )
  13. // StartDeployHTTP 启动本地部署 HTTP 服务(端口 9998)
  14. func StartDeployHTTP() {
  15. mux := http.NewServeMux()
  16. // 简易健康探针:无依赖直接返回 OK,用于验证 HTTP 服务是否正常
  17. mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
  18. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  19. w.Write([]byte("OK"))
  20. })
  21. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  22. if r.URL.Path == "/favicon.ico" {
  23. w.WriteHeader(204)
  24. return
  25. }
  26. if r.URL.Path != "/" {
  27. http.NotFound(w, r)
  28. return
  29. }
  30. util.GetTagLog().Infof("sys", "DeployHTTP: GET / from %s", r.RemoteAddr)
  31. w.Header().Set("Content-Type", "text/html; charset=utf-8")
  32. w.Write([]byte(deployHTML))
  33. })
  34. mux.HandleFunc("/deploy/upload", withRecover(handleDeployUpload))
  35. mux.HandleFunc("/deploy/status", handleDeployStatusSSE)
  36. mux.HandleFunc("/deploy/result", withRecover(handleDeployResult))
  37. server := &http.Server{
  38. Addr: ":9998",
  39. Handler: withLogging(mux),
  40. ReadTimeout: 30 * time.Second,
  41. WriteTimeout: 60 * time.Second,
  42. IdleTimeout: 120 * time.Second,
  43. }
  44. util.GetTagLog().Infof("sys", "本地部署服务启动在 :9998")
  45. go func() {
  46. if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  47. util.GetTagLog().Errorf("sys", "本地部署服务启动失败:%s", err.Error())
  48. }
  49. }()
  50. }
  51. // withLogging 请求日志中间件
  52. func withLogging(next http.Handler) http.Handler {
  53. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  54. util.GetTagLog().Infof("sys", "DeployHTTP: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
  55. next.ServeHTTP(w, r)
  56. })
  57. }
  58. // withRecover panic 恢复包装
  59. func withRecover(fn http.HandlerFunc) http.HandlerFunc {
  60. return func(w http.ResponseWriter, r *http.Request) {
  61. defer func() {
  62. if err := recover(); err != nil {
  63. util.GetTagLog().Errorf("sys", "DeployHTTP: panic in %s %s: %v\n%s", r.Method, r.URL.Path, err, string(debug.Stack()))
  64. http.Error(w, "Internal Server Error", 500)
  65. }
  66. }()
  67. fn(w, r)
  68. }
  69. }
  70. func handleDeployUpload(w http.ResponseWriter, r *http.Request) {
  71. if r.Method != http.MethodPost {
  72. writeJSON(w, 405, map[string]interface{}{"success": false, "error": "Method not allowed"})
  73. return
  74. }
  75. r.Body = http.MaxBytesReader(w, r.Body, 50<<20)
  76. file, header, err := r.FormFile("file")
  77. if err != nil {
  78. writeJSON(w, 400, map[string]interface{}{"success": false, "error": "读取文件失败: " + err.Error()})
  79. return
  80. }
  81. defer file.Close()
  82. tmpDir := filepath.Join(util.GetPath(4), deployTmpDir)
  83. os.MkdirAll(tmpDir, os.ModePerm)
  84. tmpFile := filepath.Join(tmpDir, header.Filename)
  85. out, err := os.Create(tmpFile)
  86. if err != nil {
  87. writeJSON(w, 500, map[string]interface{}{"success": false, "error": "创建临时文件失败: " + err.Error()})
  88. return
  89. }
  90. defer out.Close()
  91. if _, err := io.Copy(out, file); err != nil {
  92. writeJSON(w, 500, map[string]interface{}{"success": false, "error": "写入文件失败: " + err.Error()})
  93. return
  94. }
  95. if !isELF(tmpFile) {
  96. os.Remove(tmpFile)
  97. writeJSON(w, 400, map[string]interface{}{"success": false, "error": "文件格式错误:非Linux可执行文件"})
  98. return
  99. }
  100. md5Hash, _ := fileMD5(tmpFile)
  101. cwd, _ := os.Getwd()
  102. targetPath := filepath.Join(cwd, appname)
  103. backupPath := targetPath + ".bak"
  104. version := fmt.Sprintf("local-%s", time.Now().Format("20060102-150405"))
  105. os.Remove(backupPath)
  106. if _, err := os.Stat(targetPath); err == nil {
  107. if err := os.Rename(targetPath, backupPath); err != nil {
  108. writeJSON(w, 500, map[string]interface{}{"success": false, "error": "备份旧版失败: " + err.Error()})
  109. return
  110. }
  111. }
  112. if err := os.Rename(tmpFile, targetPath); err != nil {
  113. os.Rename(backupPath, targetPath)
  114. writeJSON(w, 500, map[string]interface{}{"success": false, "error": "替换文件失败: " + err.Error()})
  115. return
  116. }
  117. os.Chmod(targetPath, 0755)
  118. marker := DeployMarker{Version: version, Timestamp: time.Now().Unix(), Action: "deploy"}
  119. markerContent, _ := json.MarshalToString(marker)
  120. os.WriteFile(filepath.Join(util.GetPath(0), "deploy_marker.json"), []byte(markerContent), os.ModePerm)
  121. util.GetTagLog().Infof("sys", "DeployHTTP: 本地上传部署完成 version=%s md5=%s", version, md5Hash)
  122. writeJSON(w, 200, map[string]interface{}{
  123. "success": true,
  124. "version": version,
  125. "md5": md5Hash,
  126. "message": "文件已部署,即将重启...",
  127. })
  128. go func() {
  129. time.Sleep(500 * time.Millisecond)
  130. os.Exit(0)
  131. }()
  132. }
  133. func handleDeployStatusSSE(w http.ResponseWriter, r *http.Request) {
  134. util.GetTagLog().Infof("sys", "DeployHTTP: SSE连接 from %s", r.RemoteAddr)
  135. w.Header().Set("Content-Type", "text/event-stream")
  136. w.Header().Set("Cache-Control", "no-cache")
  137. w.Header().Set("Connection", "keep-alive")
  138. w.Header().Set("Access-Control-Allow-Origin", "*")
  139. flusher, ok := w.(http.Flusher)
  140. if !ok {
  141. http.Error(w, "SSE not supported", 500)
  142. return
  143. }
  144. result := DeployResultGet()
  145. if result != nil {
  146. data, _ := json.MarshalToString(result)
  147. fmt.Fprintf(w, "data: %s\n\n", data)
  148. flusher.Flush()
  149. } else {
  150. fmt.Fprintf(w, "data: {\"status\":\"waiting\"}\n\n")
  151. flusher.Flush()
  152. }
  153. ticker := time.NewTicker(15 * time.Second)
  154. defer ticker.Stop()
  155. for {
  156. select {
  157. case <-r.Context().Done():
  158. util.GetTagLog().Infof("sys", "DeployHTTP: SSE断开 from %s", r.RemoteAddr)
  159. return
  160. case <-ticker.C:
  161. result := DeployResultGet()
  162. if result != nil {
  163. data, _ := json.MarshalToString(result)
  164. fmt.Fprintf(w, "data: %s\n\n", data)
  165. flusher.Flush()
  166. }
  167. }
  168. }
  169. }
  170. func handleDeployResult(w http.ResponseWriter, r *http.Request) {
  171. w.Header().Set("Access-Control-Allow-Origin", "*")
  172. result := DeployResultGet()
  173. if result == nil {
  174. writeJSON(w, 200, map[string]interface{}{"status": "no_deployment"})
  175. return
  176. }
  177. writeJSON(w, 200, result)
  178. }
  179. func writeJSON(w http.ResponseWriter, code int, data interface{}) {
  180. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  181. w.WriteHeader(code)
  182. body, _ := stdjson.Marshal(data)
  183. w.Write(body)
  184. }