| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- package main
- import (
- stdjson "encoding/json" // 标准库,避免 json-iterator nil map panic
- "fmt"
- "io"
- "net/http"
- "os"
- "path/filepath"
- "runtime/debug"
- "time"
- "lc/common/util"
- )
- // StartDeployHTTP 启动本地部署 HTTP 服务(端口 9998)
- func StartDeployHTTP() {
- mux := http.NewServeMux()
- // 简易健康探针:无依赖直接返回 OK,用于验证 HTTP 服务是否正常
- mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/plain; charset=utf-8")
- w.Write([]byte("OK"))
- })
- mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path == "/favicon.ico" {
- w.WriteHeader(204)
- return
- }
- if r.URL.Path != "/" {
- http.NotFound(w, r)
- return
- }
- util.GetTagLog().Infof("sys", "DeployHTTP: GET / from %s", r.RemoteAddr)
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- w.Write([]byte(deployHTML))
- })
- mux.HandleFunc("/deploy/upload", withRecover(handleDeployUpload))
- mux.HandleFunc("/deploy/status", handleDeployStatusSSE)
- mux.HandleFunc("/deploy/result", withRecover(handleDeployResult))
- server := &http.Server{
- Addr: ":9998",
- Handler: withLogging(mux),
- ReadTimeout: 30 * time.Second,
- WriteTimeout: 60 * time.Second,
- IdleTimeout: 120 * time.Second,
- }
- util.GetTagLog().Infof("sys", "本地部署服务启动在 :9998")
- go func() {
- if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- util.GetTagLog().Errorf("sys", "本地部署服务启动失败:%s", err.Error())
- }
- }()
- }
- // withLogging 请求日志中间件
- func withLogging(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- util.GetTagLog().Infof("sys", "DeployHTTP: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
- next.ServeHTTP(w, r)
- })
- }
- // withRecover panic 恢复包装
- func withRecover(fn http.HandlerFunc) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- defer func() {
- if err := recover(); err != nil {
- util.GetTagLog().Errorf("sys", "DeployHTTP: panic in %s %s: %v\n%s", r.Method, r.URL.Path, err, string(debug.Stack()))
- http.Error(w, "Internal Server Error", 500)
- }
- }()
- fn(w, r)
- }
- }
- func handleDeployUpload(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- writeJSON(w, 405, map[string]interface{}{"success": false, "error": "Method not allowed"})
- return
- }
- r.Body = http.MaxBytesReader(w, r.Body, 50<<20)
- file, header, err := r.FormFile("file")
- if err != nil {
- writeJSON(w, 400, map[string]interface{}{"success": false, "error": "读取文件失败: " + err.Error()})
- return
- }
- defer file.Close()
- tmpDir := filepath.Join(util.GetPath(4), deployTmpDir)
- os.MkdirAll(tmpDir, os.ModePerm)
- tmpFile := filepath.Join(tmpDir, header.Filename)
- out, err := os.Create(tmpFile)
- if err != nil {
- writeJSON(w, 500, map[string]interface{}{"success": false, "error": "创建临时文件失败: " + err.Error()})
- return
- }
- defer out.Close()
- if _, err := io.Copy(out, file); err != nil {
- writeJSON(w, 500, map[string]interface{}{"success": false, "error": "写入文件失败: " + err.Error()})
- return
- }
- if !isELF(tmpFile) {
- os.Remove(tmpFile)
- writeJSON(w, 400, map[string]interface{}{"success": false, "error": "文件格式错误:非Linux可执行文件"})
- return
- }
- md5Hash, _ := fileMD5(tmpFile)
- cwd, _ := os.Getwd()
- targetPath := filepath.Join(cwd, appname)
- backupPath := targetPath + ".bak"
- version := fmt.Sprintf("local-%s", time.Now().Format("20060102-150405"))
- os.Remove(backupPath)
- if _, err := os.Stat(targetPath); err == nil {
- if err := os.Rename(targetPath, backupPath); err != nil {
- writeJSON(w, 500, map[string]interface{}{"success": false, "error": "备份旧版失败: " + err.Error()})
- return
- }
- }
- if err := os.Rename(tmpFile, targetPath); err != nil {
- os.Rename(backupPath, targetPath)
- writeJSON(w, 500, map[string]interface{}{"success": false, "error": "替换文件失败: " + err.Error()})
- return
- }
- os.Chmod(targetPath, 0755)
- marker := DeployMarker{Version: version, Timestamp: time.Now().Unix(), Action: "deploy"}
- markerContent, _ := json.MarshalToString(marker)
- os.WriteFile(filepath.Join(util.GetPath(0), "deploy_marker.json"), []byte(markerContent), os.ModePerm)
- util.GetTagLog().Infof("sys", "DeployHTTP: 本地上传部署完成 version=%s md5=%s", version, md5Hash)
- writeJSON(w, 200, map[string]interface{}{
- "success": true,
- "version": version,
- "md5": md5Hash,
- "message": "文件已部署,即将重启...",
- })
- go func() {
- time.Sleep(500 * time.Millisecond)
- os.Exit(0)
- }()
- }
- func handleDeployStatusSSE(w http.ResponseWriter, r *http.Request) {
- util.GetTagLog().Infof("sys", "DeployHTTP: SSE连接 from %s", r.RemoteAddr)
- w.Header().Set("Content-Type", "text/event-stream")
- w.Header().Set("Cache-Control", "no-cache")
- w.Header().Set("Connection", "keep-alive")
- w.Header().Set("Access-Control-Allow-Origin", "*")
- flusher, ok := w.(http.Flusher)
- if !ok {
- http.Error(w, "SSE not supported", 500)
- return
- }
- result := DeployResultGet()
- if result != nil {
- data, _ := json.MarshalToString(result)
- fmt.Fprintf(w, "data: %s\n\n", data)
- flusher.Flush()
- } else {
- fmt.Fprintf(w, "data: {\"status\":\"waiting\"}\n\n")
- flusher.Flush()
- }
- ticker := time.NewTicker(15 * time.Second)
- defer ticker.Stop()
- for {
- select {
- case <-r.Context().Done():
- util.GetTagLog().Infof("sys", "DeployHTTP: SSE断开 from %s", r.RemoteAddr)
- return
- case <-ticker.C:
- result := DeployResultGet()
- if result != nil {
- data, _ := json.MarshalToString(result)
- fmt.Fprintf(w, "data: %s\n\n", data)
- flusher.Flush()
- }
- }
- }
- }
- func handleDeployResult(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Access-Control-Allow-Origin", "*")
- result := DeployResultGet()
- if result == nil {
- writeJSON(w, 200, map[string]interface{}{"status": "no_deployment"})
- return
- }
- writeJSON(w, 200, result)
- }
- func writeJSON(w http.ResponseWriter, code int, data interface{}) {
- w.Header().Set("Content-Type", "application/json; charset=utf-8")
- w.WriteHeader(code)
- body, _ := stdjson.Marshal(data)
- w.Write(body)
- }
|