deploymgr.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. package main
  2. import (
  3. "crypto/md5"
  4. "encoding/base64"
  5. "encoding/hex"
  6. "fmt"
  7. "io"
  8. "os"
  9. "path/filepath"
  10. "strings"
  11. "sync"
  12. "time"
  13. "lc/common/mqtt"
  14. "lc/common/protocol"
  15. "lc/common/util"
  16. )
  17. const (
  18. deployChunkTimeout = 10 * time.Minute
  19. deployTmpDir = "deploy"
  20. )
  21. // DeployMgr 部署管理器
  22. type DeployMgr struct {
  23. mu sync.Mutex
  24. active bool
  25. version string
  26. fileName string
  27. totalChunks int
  28. chunkSize int
  29. expectedMD5 string
  30. chunkBits []bool
  31. receivedAt time.Time
  32. deployTmp string
  33. cancel chan struct{}
  34. }
  35. var _deployMgrOnce sync.Once
  36. var _deployMgr *DeployMgr
  37. func GetDeployMgr() *DeployMgr {
  38. _deployMgrOnce.Do(func() {
  39. _deployMgr = &DeployMgr{}
  40. })
  41. return _deployMgr
  42. }
  43. // HandleTpDeploy 处理来自 MQTT 的部署指令/分片
  44. func HandleTpDeploy(m mqtt.Message) {
  45. var obj protocol.Pack_DeployCmd
  46. if err := obj.DeCode(string(m.Payload())); err != nil {
  47. util.GetTagLog().Errorf("sys", "HandleTpDeploy:DeCode失败,err=%v", err)
  48. return
  49. }
  50. mgr := GetDeployMgr()
  51. switch obj.Data.Action {
  52. case "start":
  53. mgr.start(&obj)
  54. case "chunk":
  55. mgr.receiveChunk(&obj)
  56. case "abort":
  57. mgr.abort(&obj)
  58. default:
  59. util.GetTagLog().Warnf("sys", "HandleTpDeploy:未知Action=%s", obj.Data.Action)
  60. }
  61. }
  62. func (o *DeployMgr) start(cmd *protocol.Pack_DeployCmd) {
  63. o.mu.Lock()
  64. defer o.mu.Unlock()
  65. if o.active {
  66. util.GetTagLog().Warnf("sys", "DeployMgr:已有部署进行中,拒绝新部署指令 version=%s", cmd.Data.Version)
  67. o.sendAck(cmd.Data.Version, false, "已有部署进行中")
  68. return
  69. }
  70. o.active = true
  71. o.version = cmd.Data.Version
  72. o.fileName = cmd.Data.FileName
  73. o.totalChunks = cmd.Data.TotalChunks
  74. o.chunkSize = cmd.Data.ChunkSize
  75. o.expectedMD5 = cmd.Data.MD5
  76. o.chunkBits = make([]bool, cmd.Data.TotalChunks)
  77. o.receivedAt = time.Now()
  78. o.deployTmp = filepath.Join(util.GetPath(4), deployTmpDir)
  79. o.cancel = make(chan struct{}, 1)
  80. if err := os.MkdirAll(o.deployTmp, os.ModePerm); err != nil {
  81. util.GetTagLog().Errorf("sys", "DeployMgr:创建临时目录失败,path=%s,err=%v", o.deployTmp, err)
  82. o.cleanup("创建临时目录失败: " + err.Error())
  83. return
  84. }
  85. util.GetTagLog().Infof("sys", "DeployMgr:开始部署 version=%s totalChunks=%d chunkSize=%d md5=%s",
  86. cmd.Data.Version, cmd.Data.TotalChunks, cmd.Data.ChunkSize, cmd.Data.MD5)
  87. // 启动超时监控 goroutine(独立于 gopool,避免 worker 饥饿问题)
  88. go o.watchTimeout()
  89. }
  90. func (o *DeployMgr) receiveChunk(cmd *protocol.Pack_DeployCmd) {
  91. o.mu.Lock()
  92. defer o.mu.Unlock()
  93. if !o.active {
  94. return
  95. }
  96. if cmd.Data.Version != o.version {
  97. util.GetTagLog().Warnf("sys", "DeployMgr:分片版本不匹配,expect=%s,got=%s", o.version, cmd.Data.Version)
  98. return
  99. }
  100. idx := cmd.Data.ChunkIndex
  101. if idx < 0 || idx >= o.totalChunks {
  102. util.GetTagLog().Errorf("sys", "DeployMgr:分片索引越界,idx=%d,total=%d", idx, o.totalChunks)
  103. return
  104. }
  105. // base64 decode the chunk data
  106. decoded, err := base64.StdEncoding.DecodeString(cmd.Data.Data)
  107. if err != nil {
  108. util.GetTagLog().Errorf("sys", "DeployMgr:base64解码分片%d失败,err=%v", idx, err)
  109. return
  110. }
  111. chunkFile := filepath.Join(o.deployTmp, fmt.Sprintf("chunk_%d", idx))
  112. if err := os.WriteFile(chunkFile, decoded, os.ModePerm); err != nil {
  113. util.GetTagLog().Errorf("sys", "DeployMgr:写分片失败,idx=%d,err=%v", idx, err)
  114. return
  115. }
  116. o.chunkBits[idx] = true
  117. o.receivedAt = time.Now()
  118. allReceived := true
  119. for _, b := range o.chunkBits {
  120. if !b {
  121. allReceived = false
  122. break
  123. }
  124. }
  125. if allReceived {
  126. go o.assembleAndDeploy()
  127. }
  128. }
  129. func (o *DeployMgr) abort(cmd *protocol.Pack_DeployCmd) {
  130. o.mu.Lock()
  131. defer o.mu.Unlock()
  132. if !o.active || cmd.Data.Version != o.version {
  133. return
  134. }
  135. util.GetTagLog().Infof("sys", "DeployMgr:收到取消指令 version=%s", o.version)
  136. if o.cancel != nil {
  137. close(o.cancel)
  138. }
  139. o.cleanup("已取消")
  140. }
  141. func (o *DeployMgr) assembleAndDeploy() {
  142. o.mu.Lock()
  143. defer o.mu.Unlock()
  144. if !o.active {
  145. return
  146. }
  147. util.GetTagLog().Infof("sys", "DeployMgr:所有分片已收齐,开始重组 version=%s", o.version)
  148. // 1. 重组文件
  149. assembledFile := filepath.Join(o.deployTmp, o.fileName)
  150. outFile, err := os.Create(assembledFile)
  151. if err != nil {
  152. o.cleanup("创建重组文件失败: " + err.Error())
  153. return
  154. }
  155. defer outFile.Close()
  156. for i := 0; i < o.totalChunks; i++ {
  157. chunkFile := filepath.Join(o.deployTmp, fmt.Sprintf("chunk_%d", i))
  158. data, err := os.ReadFile(chunkFile)
  159. if err != nil {
  160. outFile.Close()
  161. o.cleanup(fmt.Sprintf("读取分片%d失败: %s", i, err.Error()))
  162. return
  163. }
  164. if _, err := outFile.Write(data); err != nil {
  165. outFile.Close()
  166. o.cleanup(fmt.Sprintf("写入重组文件失败: %s", err.Error()))
  167. return
  168. }
  169. }
  170. // 2. MD5 校验
  171. actualMD5, err := fileMD5(assembledFile)
  172. if err != nil {
  173. o.cleanup("计算MD5失败: " + err.Error())
  174. return
  175. }
  176. if !strings.EqualFold(actualMD5, o.expectedMD5) {
  177. o.cleanup(fmt.Sprintf("MD5校验失败:期望%s 实际%s", o.expectedMD5, actualMD5))
  178. return
  179. }
  180. util.GetTagLog().Infof("sys", "DeployMgr:MD5校验通过 md5=%s", actualMD5)
  181. // 3. ELF 魔数校验
  182. if !isELF(assembledFile) {
  183. o.cleanup("文件格式错误:非Linux可执行文件")
  184. return
  185. }
  186. // 4. 磁盘空间检查(rename 不消耗额外空间,只需 1x 文件大小)
  187. cwd, _ := os.Getwd()
  188. targetPath := filepath.Join(cwd, o.fileName)
  189. fi, _ := os.Stat(assembledFile)
  190. needed := fi.Size() + 1*1024*1024 // 文件大小 + 1MB 安全余量
  191. free, err := getFreeSpace(cwd)
  192. if err == nil && free < needed {
  193. o.cleanup(fmt.Sprintf("磁盘空间不足:需要%dMB 可用%dMB", needed/(1024*1024), free/(1024*1024)))
  194. return
  195. }
  196. // 5. 备份当前二进制,替换新文件
  197. backupPath := targetPath + ".bak"
  198. os.Remove(backupPath)
  199. if _, err := os.Stat(targetPath); err == nil {
  200. if err := os.Rename(targetPath, backupPath); err != nil {
  201. o.cleanup("备份旧版失败: " + err.Error())
  202. return
  203. }
  204. util.GetTagLog().Infof("sys", "DeployMgr:旧版已备份到 %s", backupPath)
  205. }
  206. if err := os.Rename(assembledFile, targetPath); err != nil {
  207. os.Rename(backupPath, targetPath)
  208. o.cleanup("替换文件失败: " + err.Error())
  209. return
  210. }
  211. if err := os.Chmod(targetPath, 0755); err != nil {
  212. util.GetTagLog().Warnf("sys", "DeployMgr:chmod失败,err=%v", err)
  213. }
  214. util.GetTagLog().Infof("sys", "DeployMgr:文件替换成功 version=%s path=%s", o.version, targetPath)
  215. // 6. 写部署标记
  216. marker := DeployMarker{
  217. Version: o.version,
  218. Timestamp: time.Now().Unix(),
  219. Action: "deploy",
  220. }
  221. markerPath := filepath.Join(util.GetPath(0), "deploy_marker.json")
  222. markerContent, _ := json.MarshalToString(marker)
  223. if err := os.WriteFile(markerPath, []byte(markerContent), os.ModePerm); err != nil {
  224. util.GetTagLog().Errorf("sys", "DeployMgr:写部署标记失败,err=%v", err)
  225. }
  226. // 7. 发送 ACK
  227. o.active = false
  228. o.sendAck(o.version, true, "")
  229. util.GetTagLog().Infof("sys", "DeployMgr:即将退出进程以完成部署")
  230. // 8. 退出让 goforever 拉起新版本
  231. time.Sleep(500 * time.Millisecond)
  232. os.Exit(0)
  233. }
  234. // watchTimeout 分片接收超时监控(独立 goroutine,不走 gopool 避免 worker 饥饿)
  235. func (o *DeployMgr) watchTimeout() {
  236. ticker := time.NewTicker(30 * time.Second)
  237. defer ticker.Stop()
  238. for {
  239. select {
  240. case <-o.cancel:
  241. return
  242. case <-ticker.C:
  243. o.mu.Lock()
  244. if !o.active {
  245. o.mu.Unlock()
  246. return
  247. }
  248. elapsed := time.Since(o.receivedAt)
  249. if elapsed > deployChunkTimeout {
  250. received := o.countReceived()
  251. o.mu.Unlock()
  252. o.cleanup(fmt.Sprintf("分片接收超时:已收 %d/%d", received, o.totalChunks))
  253. return
  254. }
  255. o.mu.Unlock()
  256. }
  257. }
  258. }
  259. func (o *DeployMgr) cleanup(errMsg string) {
  260. util.GetTagLog().Errorf("sys", "DeployMgr:部署失败,原因=%s", errMsg)
  261. if o.deployTmp != "" {
  262. os.RemoveAll(o.deployTmp)
  263. }
  264. if o.active {
  265. o.sendAck(o.version, false, errMsg)
  266. }
  267. o.active = false
  268. o.version = ""
  269. o.chunkBits = nil
  270. o.cancel = nil
  271. }
  272. func (o *DeployMgr) sendAck(version string, success bool, errMsg string) {
  273. var ack protocol.Pack_DeployAck
  274. seq := GetNextUint64()
  275. if str, err := ack.EnCode(appConfig.GID, appConfig.GID, seq, version, success, errMsg); err == nil {
  276. topic := GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_DEPLOY_ACK)
  277. GetMQTTMgr().Publish(topic, str, 0, ToCloud)
  278. util.GetTagLog().Infof("sys", "DeployMgr:发送ACK version=%s success=%v", version, success)
  279. }
  280. }
  281. func (o *DeployMgr) IsActive() bool {
  282. o.mu.Lock()
  283. defer o.mu.Unlock()
  284. return o.active
  285. }
  286. func (o *DeployMgr) countReceived() int {
  287. n := 0
  288. for _, b := range o.chunkBits {
  289. if b {
  290. n++
  291. }
  292. }
  293. return n
  294. }
  295. // DeployMarker 部署/回滚标记
  296. type DeployMarker struct {
  297. Version string `json:"version"`
  298. Timestamp int64 `json:"timestamp"`
  299. Action string `json:"action"`
  300. }
  301. // ---- 工具函数 ----
  302. func fileMD5(path string) (string, error) {
  303. f, err := os.Open(path)
  304. if err != nil {
  305. return "", err
  306. }
  307. defer f.Close()
  308. h := md5.New()
  309. if _, err := io.Copy(h, f); err != nil {
  310. return "", err
  311. }
  312. return hex.EncodeToString(h.Sum(nil)), nil
  313. }
  314. func isELF(path string) bool {
  315. f, err := os.Open(path)
  316. if err != nil {
  317. return false
  318. }
  319. defer f.Close()
  320. header := make([]byte, 4)
  321. if _, err := io.ReadFull(f, header); err != nil {
  322. return false
  323. }
  324. return header[0] == 0x7f && header[1] == 'E' && header[2] == 'L' && header[3] == 'F'
  325. }
  326. // init registers the deploy handler
  327. func init() {
  328. // Deploy handler will be registered in InitCloudMqttSubscribeTopics after appConfig is loaded
  329. }