feat: opt exec log
This commit is contained in:
@@ -72,14 +72,14 @@ jobs:
|
|||||||
docker-compose pull
|
docker-compose pull
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
# - name: Deploy to X server
|
- name: Deploy to X server
|
||||||
# uses: appleboy/ssh-action@v1.2.0
|
uses: appleboy/ssh-action@v1.2.0
|
||||||
# with:
|
with:
|
||||||
# host: ${{ secrets.DEPLOY_X_HOST }}
|
host: ${{ secrets.DEPLOY_HOST }}
|
||||||
# username: ${{ secrets.DEPLOY_X_USER }}
|
username: ${{ secrets.DEPLOY_USER }}
|
||||||
# password: ${{ secrets.DEPLOY_X_PASSWORD }}
|
password: ${{ secrets.DEPLOY_PASSWORD }}
|
||||||
# port: ${{ secrets.DEPLOY_X_PORT }}
|
port: ${{ secrets.DEPLOY_PORT }}
|
||||||
# script: |
|
script: |
|
||||||
# cd ${{ secrets.DEPLOY_X_PATH }}
|
cd ${{ secrets.DEPLOY_X_PATH }}
|
||||||
# docker-compose pull
|
docker-compose pull
|
||||||
# docker-compose up -d
|
docker-compose up -d
|
||||||
+10
-2
@@ -215,7 +215,10 @@ func (a *Agent) closeWS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) readWS() {
|
func (a *Agent) readWS() {
|
||||||
defer a.closeWS() // 读取结束时关闭连接,停止 heartbeatLoop
|
defer func() {
|
||||||
|
log.Info("readWS 退出,准备关闭连接")
|
||||||
|
a.closeWS()
|
||||||
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
a.wsMu.Lock()
|
a.wsMu.Lock()
|
||||||
@@ -223,6 +226,7 @@ func (a *Agent) readWS() {
|
|||||||
a.wsMu.Unlock()
|
a.wsMu.Unlock()
|
||||||
|
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
|
log.Warn("readWS: wsConn 为 nil")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +333,11 @@ func (a *Agent) sendWSMessage(msgType string, data interface{}) error {
|
|||||||
msgBytes, _ := json.Marshal(msg)
|
msgBytes, _ := json.Marshal(msg)
|
||||||
|
|
||||||
a.wsConn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
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() {
|
func (a *Agent) heartbeatLoop() {
|
||||||
|
|||||||
+15
-1
@@ -134,7 +134,7 @@ var isDaemon = false
|
|||||||
var isRestart = false
|
var isRestart = false
|
||||||
|
|
||||||
func cmdStart() {
|
func cmdStart() {
|
||||||
// 检查是否已经在运行
|
// 检查是否已经在运行(使用文件锁)
|
||||||
pid := readPidFile()
|
pid := readPidFile()
|
||||||
if pid != 0 && isProcessRunning(pid) {
|
if pid != 0 && isProcessRunning(pid) {
|
||||||
fmt.Printf("Agent 已在运行 (PID: %d)\n", pid)
|
fmt.Printf("Agent 已在运行 (PID: %d)\n", pid)
|
||||||
@@ -148,6 +148,13 @@ func cmdStart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 以下是 daemon 子进程的逻辑
|
// 以下是 daemon 子进程的逻辑
|
||||||
|
// 尝试获取文件锁
|
||||||
|
if !tryLock() {
|
||||||
|
fmt.Println("Agent 已在运行(无法获取锁)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
initLogger(logFile, true)
|
initLogger(logFile, true)
|
||||||
|
|
||||||
config := &Config{Interval: 30}
|
config := &Config{Interval: 30}
|
||||||
@@ -207,6 +214,13 @@ func cmdRun() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 尝试获取文件锁
|
||||||
|
if !tryLock() {
|
||||||
|
fmt.Println("Agent 已在运行(无法获取锁)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
// 重启模式下只输出到文件(因为是从 daemon 进程 exec 过来的)
|
// 重启模式下只输出到文件(因为是从 daemon 进程 exec 过来的)
|
||||||
initLogger(logFile, isRestart)
|
initLogger(logFile, isRestart)
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,48 @@ import (
|
|||||||
|
|
||||||
// ========== PID 文件管理 ==========
|
// ========== PID 文件管理 ==========
|
||||||
|
|
||||||
|
var pidFileLock *os.File
|
||||||
|
|
||||||
func getPidFile() string {
|
func getPidFile() string {
|
||||||
return filepath.Join(dataDir, "agent.pid")
|
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() {
|
func writePidFile() {
|
||||||
os.MkdirAll(dataDir, 0755)
|
os.MkdirAll(dataDir, 0755)
|
||||||
pidFile := getPidFile()
|
pidFile := getPidFile()
|
||||||
|
|||||||
+11
-9
@@ -52,15 +52,17 @@ func (Task) TableName() string {
|
|||||||
|
|
||||||
// TaskLog represents a log entry for task execution
|
// TaskLog represents a log entry for task execution
|
||||||
type TaskLog struct {
|
type TaskLog struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
TaskID uint `json:"task_id" gorm:"index"`
|
TaskID uint `json:"task_id" gorm:"index"`
|
||||||
AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行
|
AgentID *uint `json:"agent_id" gorm:"index"` // Agent ID,为空表示本地执行
|
||||||
Command string `json:"command" gorm:"type:text"`
|
Command string `json:"command" gorm:"type:text"`
|
||||||
Output string `json:"-" gorm:"type:longtext"` // gzip+base64 compressed
|
Output string `json:"-" gorm:"type:longtext"` // gzip+base64 compressed
|
||||||
Status string `json:"status" gorm:"size:20"` // success, failed
|
Status string `json:"status" gorm:"size:20"` // success, failed
|
||||||
Duration int64 `json:"duration"` // milliseconds
|
Duration int64 `json:"duration"` // milliseconds
|
||||||
ExitCode int `json:"exit_code"`
|
ExitCode int `json:"exit_code"`
|
||||||
CreatedAt LocalTime `json:"created_at"`
|
StartTime *LocalTime `json:"start_time"`
|
||||||
|
EndTime *LocalTime `json:"end_time"`
|
||||||
|
CreatedAt LocalTime `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (TaskLog) TableName() string {
|
func (TaskLog) TableName() string {
|
||||||
|
|||||||
@@ -339,6 +339,16 @@ func (s *AgentService) ReportResult(result *models.AgentTaskResult) error {
|
|||||||
ExitCode: result.ExitCode,
|
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 {
|
if err := database.DB.Create(taskLog).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,12 +195,17 @@ func (es *ExecutorService) saveTaskLogCallback(taskID uint, command string, resu
|
|||||||
status = "failed"
|
status = "failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startTime := models.LocalTime(result.Start)
|
||||||
|
endTime := models.LocalTime(result.End)
|
||||||
|
|
||||||
taskLog := &models.TaskLog{
|
taskLog := &models.TaskLog{
|
||||||
TaskID: taskID,
|
TaskID: taskID,
|
||||||
Command: command,
|
Command: command,
|
||||||
Output: compressed,
|
Output: compressed,
|
||||||
Status: status,
|
Status: status,
|
||||||
Duration: result.End.Sub(result.Start).Milliseconds(),
|
Duration: result.End.Sub(result.Start).Milliseconds(),
|
||||||
|
StartTime: &startTime,
|
||||||
|
EndTime: &endTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := database.DB.Create(taskLog).Error; err != nil {
|
if err := database.DB.Create(taskLog).Error; err != nil {
|
||||||
|
|||||||
@@ -305,6 +305,8 @@ export interface TaskLog {
|
|||||||
command: string
|
command: string
|
||||||
status: string
|
status: string
|
||||||
duration: number
|
duration: number
|
||||||
|
start_time: string | null
|
||||||
|
end_time: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +324,8 @@ export interface LogDetail {
|
|||||||
output: string
|
output: string
|
||||||
status: string
|
status: string
|
||||||
duration: number
|
duration: number
|
||||||
|
start_time: string | null
|
||||||
|
end_time: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -248,8 +248,12 @@ watch(() => route.query.task_id, (newTaskId) => {
|
|||||||
<span>{{ formatDuration(selectedLog.duration) }}</span>
|
<span>{{ formatDuration(selectedLog.duration) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<span class="text-muted-foreground">执行时间</span>
|
<span class="text-muted-foreground">开始时间</span>
|
||||||
<span>{{ selectedLog.created_at }}</span>
|
<span>{{ selectedLog.start_time || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-muted-foreground">结束时间</span>
|
||||||
|
<span>{{ selectedLog.end_time || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-1">
|
<div class="pt-1">
|
||||||
<span class="text-muted-foreground">命令</span>
|
<span class="text-muted-foreground">命令</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user