From 756ba1e0eaf07b0ce9a6bdad2d0500d84e8f05d0 Mon Sep 17 00:00:00 2001 From: engigu Date: Thu, 1 Jan 2026 14:42:33 +0800 Subject: [PATCH] feat: opt exec log --- .github/workflows/deploy.yml | 22 ++++++++-------- agent/agent.go | 12 +++++++-- agent/main.go | 16 ++++++++++- agent/service.go | 38 +++++++++++++++++++++++++++ internal/models/task.go | 20 +++++++------- internal/services/agent_service.go | 10 +++++++ internal/services/executor_service.go | 15 +++++++---- web/src/api/index.ts | 4 +++ web/src/views/history/History.vue | 8 ++++-- 9 files changed, 115 insertions(+), 30 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bfdada0..e3cea6b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -72,14 +72,14 @@ jobs: docker-compose pull docker-compose up -d - # - name: Deploy to X server - # uses: appleboy/ssh-action@v1.2.0 - # with: - # host: ${{ secrets.DEPLOY_X_HOST }} - # username: ${{ secrets.DEPLOY_X_USER }} - # password: ${{ secrets.DEPLOY_X_PASSWORD }} - # port: ${{ secrets.DEPLOY_X_PORT }} - # script: | - # cd ${{ secrets.DEPLOY_X_PATH }} - # docker-compose pull - # docker-compose up -d \ No newline at end of file + - name: Deploy to X server + uses: appleboy/ssh-action@v1.2.0 + with: + host: ${{ secrets.DEPLOY_HOST }} + username: ${{ secrets.DEPLOY_USER }} + password: ${{ secrets.DEPLOY_PASSWORD }} + port: ${{ secrets.DEPLOY_PORT }} + script: | + cd ${{ secrets.DEPLOY_X_PATH }} + docker-compose pull + docker-compose up -d \ No newline at end of file diff --git a/agent/agent.go b/agent/agent.go index 1e66f4a..01a50c5 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -215,7 +215,10 @@ func (a *Agent) closeWS() { } func (a *Agent) readWS() { - defer a.closeWS() // 读取结束时关闭连接,停止 heartbeatLoop + defer func() { + log.Info("readWS 退出,准备关闭连接") + a.closeWS() + }() for { a.wsMu.Lock() @@ -223,6 +226,7 @@ func (a *Agent) readWS() { a.wsMu.Unlock() if conn == nil { + log.Warn("readWS: wsConn 为 nil") return } @@ -329,7 +333,11 @@ func (a *Agent) sendWSMessage(msgType string, data interface{}) error { msgBytes, _ := json.Marshal(msg) a.wsConn.SetWriteDeadline(time.Now().Add(10 * time.Second)) - return a.wsConn.WriteMessage(websocket.TextMessage, msgBytes) + if err := a.wsConn.WriteMessage(websocket.TextMessage, msgBytes); err != nil { + log.Warnf("发送消息失败 (%s): %v", msgType, err) + return err + } + return nil } func (a *Agent) heartbeatLoop() { diff --git a/agent/main.go b/agent/main.go index b4d9de5..b025ab7 100644 --- a/agent/main.go +++ b/agent/main.go @@ -134,7 +134,7 @@ var isDaemon = false var isRestart = false func cmdStart() { - // 检查是否已经在运行 + // 检查是否已经在运行(使用文件锁) pid := readPidFile() if pid != 0 && isProcessRunning(pid) { fmt.Printf("Agent 已在运行 (PID: %d)\n", pid) @@ -148,6 +148,13 @@ func cmdStart() { } // 以下是 daemon 子进程的逻辑 + // 尝试获取文件锁 + if !tryLock() { + fmt.Println("Agent 已在运行(无法获取锁)") + return + } + defer unlock() + initLogger(logFile, true) config := &Config{Interval: 30} @@ -207,6 +214,13 @@ func cmdRun() { } } + // 尝试获取文件锁 + if !tryLock() { + fmt.Println("Agent 已在运行(无法获取锁)") + return + } + defer unlock() + // 重启模式下只输出到文件(因为是从 daemon 进程 exec 过来的) initLogger(logFile, isRestart) diff --git a/agent/service.go b/agent/service.go index 6be1342..d23fc90 100644 --- a/agent/service.go +++ b/agent/service.go @@ -12,10 +12,48 @@ import ( // ========== PID 文件管理 ========== +var pidFileLock *os.File + func getPidFile() string { return filepath.Join(dataDir, "agent.pid") } +func getLockFile() string { + return filepath.Join(dataDir, "agent.lock") +} + +// tryLock 尝试获取文件锁,确保只有一个实例运行 +func tryLock() bool { + os.MkdirAll(dataDir, 0755) + lockFile := getLockFile() + + var err error + pidFileLock, err = os.OpenFile(lockFile, os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return false + } + + // 尝试获取排他锁(非阻塞) + err = syscall.Flock(int(pidFileLock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) + if err != nil { + pidFileLock.Close() + pidFileLock = nil + return false + } + + return true +} + +// unlock 释放文件锁 +func unlock() { + if pidFileLock != nil { + syscall.Flock(int(pidFileLock.Fd()), syscall.LOCK_UN) + pidFileLock.Close() + pidFileLock = nil + os.Remove(getLockFile()) + } +} + func writePidFile() { os.MkdirAll(dataDir, 0755) pidFile := getPidFile() diff --git a/internal/models/task.go b/internal/models/task.go index 0ad5393..c3a009f 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -52,15 +52,17 @@ func (Task) TableName() string { // TaskLog represents a log entry for task execution type TaskLog struct { - ID uint `json:"id" gorm:"primaryKey"` - TaskID uint `json:"task_id" gorm:"index"` - AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行 - Command string `json:"command" gorm:"type:text"` - Output string `json:"-" gorm:"type:longtext"` // gzip+base64 compressed - Status string `json:"status" gorm:"size:20"` // success, failed - Duration int64 `json:"duration"` // milliseconds - ExitCode int `json:"exit_code"` - CreatedAt LocalTime `json:"created_at"` + ID uint `json:"id" gorm:"primaryKey"` + TaskID uint `json:"task_id" gorm:"index"` + AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行 + Command string `json:"command" gorm:"type:text"` + Output string `json:"-" gorm:"type:longtext"` // gzip+base64 compressed + Status string `json:"status" gorm:"size:20"` // success, failed + Duration int64 `json:"duration"` // milliseconds + ExitCode int `json:"exit_code"` + StartTime *LocalTime `json:"start_time"` + EndTime *LocalTime `json:"end_time"` + CreatedAt LocalTime `json:"created_at"` } func (TaskLog) TableName() string { diff --git a/internal/services/agent_service.go b/internal/services/agent_service.go index ff02abf..0b4d573 100644 --- a/internal/services/agent_service.go +++ b/internal/services/agent_service.go @@ -339,6 +339,16 @@ func (s *AgentService) ReportResult(result *models.AgentTaskResult) error { ExitCode: result.ExitCode, } + // 处理开始和结束时间 + if result.StartTime > 0 { + startTime := models.LocalTime(time.Unix(result.StartTime, 0)) + taskLog.StartTime = &startTime + } + if result.EndTime > 0 { + endTime := models.LocalTime(time.Unix(result.EndTime, 0)) + taskLog.EndTime = &endTime + } + if err := database.DB.Create(taskLog).Error; err != nil { return err } diff --git a/internal/services/executor_service.go b/internal/services/executor_service.go index 27bd9cd..599547f 100644 --- a/internal/services/executor_service.go +++ b/internal/services/executor_service.go @@ -195,12 +195,17 @@ func (es *ExecutorService) saveTaskLogCallback(taskID uint, command string, resu status = "failed" } + startTime := models.LocalTime(result.Start) + endTime := models.LocalTime(result.End) + taskLog := &models.TaskLog{ - TaskID: taskID, - Command: command, - Output: compressed, - Status: status, - Duration: result.End.Sub(result.Start).Milliseconds(), + TaskID: taskID, + Command: command, + Output: compressed, + Status: status, + Duration: result.End.Sub(result.Start).Milliseconds(), + StartTime: &startTime, + EndTime: &endTime, } if err := database.DB.Create(taskLog).Error; err != nil { diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 0071ef0..a92f6c2 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -305,6 +305,8 @@ export interface TaskLog { command: string status: string duration: number + start_time: string | null + end_time: string | null created_at: string } @@ -322,6 +324,8 @@ export interface LogDetail { output: string status: string duration: number + start_time: string | null + end_time: string | null created_at: string } diff --git a/web/src/views/history/History.vue b/web/src/views/history/History.vue index 513530f..03acab4 100644 --- a/web/src/views/history/History.vue +++ b/web/src/views/history/History.vue @@ -248,8 +248,12 @@ watch(() => route.query.task_id, (newTaskId) => { {{ formatDuration(selectedLog.duration) }}
- 执行时间 - {{ selectedLog.created_at }} + 开始时间 + {{ selectedLog.start_time || '-' }} +
+
+ 结束时间 + {{ selectedLog.end_time || '-' }}
命令