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) }