fix: logs io buffer combine
This commit is contained in:
+4
-1
@@ -81,6 +81,7 @@ type TaskResult struct {
|
|||||||
AgentID uint `json:"agent_id"` // 仅用于 HTTP 上报时后端补充
|
AgentID uint `json:"agent_id"` // 仅用于 HTTP 上报时后端补充
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Output string `json:"output"`
|
Output string `json:"output"`
|
||||||
|
Error string `json:"error"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Duration int64 `json:"duration"`
|
Duration int64 `json:"duration"`
|
||||||
ExitCode int `json:"exit_code"`
|
ExitCode int `json:"exit_code"`
|
||||||
@@ -175,6 +176,7 @@ func (h *AgentHandler) OnTaskCompleted(req *executor.ExecutionRequest, result *e
|
|||||||
LogID: result.LogID,
|
LogID: result.LogID,
|
||||||
Command: req.Command,
|
Command: req.Command,
|
||||||
Output: result.Output,
|
Output: result.Output,
|
||||||
|
Error: result.Error,
|
||||||
Status: result.Status,
|
Status: result.Status,
|
||||||
Duration: result.Duration,
|
Duration: result.Duration,
|
||||||
ExitCode: result.ExitCode,
|
ExitCode: result.ExitCode,
|
||||||
@@ -203,7 +205,8 @@ func (h *AgentHandler) OnTaskFailed(req *executor.ExecutionRequest, err error) {
|
|||||||
TaskID: taskID,
|
TaskID: taskID,
|
||||||
LogID: req.LogID,
|
LogID: req.LogID,
|
||||||
Command: req.Command,
|
Command: req.Command,
|
||||||
Output: errMsg,
|
Output: "",
|
||||||
|
Error: err.Error(),
|
||||||
Status: "failed",
|
Status: "failed",
|
||||||
Duration: 0,
|
Duration: 0,
|
||||||
ExitCode: 1,
|
ExitCode: 1,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type Request struct {
|
|||||||
// Result 任务执行结果
|
// Result 任务执行结果
|
||||||
type Result struct {
|
type Result struct {
|
||||||
Output string
|
Output string
|
||||||
|
Error string
|
||||||
Status string // success, failed
|
Status string // success, failed
|
||||||
Duration int64 // 毫秒
|
Duration int64 // 毫秒
|
||||||
ExitCode int
|
ExitCode int
|
||||||
@@ -160,6 +161,7 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Status = "failed"
|
result.Status = "failed"
|
||||||
|
result.Error = err.Error()
|
||||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
result.ExitCode = exitErr.ExitCode()
|
result.ExitCode = exitErr.ExitCode()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,6 +10,24 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// safeBuffer 一个线程安全的字节缓冲区,用于合并 stdout 和 stderr
|
||||||
|
type safeBuffer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
buf bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *safeBuffer) Write(p []byte) (n int, err error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.buf.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *safeBuffer) String() string {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
// SchedulerConfig 调度器配置
|
// SchedulerConfig 调度器配置
|
||||||
type SchedulerConfig struct {
|
type SchedulerConfig struct {
|
||||||
WorkerCount int // Worker 数量
|
WorkerCount int // Worker 数量
|
||||||
@@ -319,20 +337,20 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 准备输出缓冲区
|
// 2. 准备输出缓冲区(使用合并缓冲区保证顺序)
|
||||||
var stdoutBuf, stderrBuf bytes.Buffer
|
var combinedBuf safeBuffer
|
||||||
var stdoutWriter, stderrWriter io.Writer
|
var stdoutWriter, stderrWriter io.Writer
|
||||||
|
|
||||||
if stdout != nil {
|
if stdout != nil {
|
||||||
stdoutWriter = io.MultiWriter(&stdoutBuf, stdout)
|
stdoutWriter = io.MultiWriter(&combinedBuf, stdout)
|
||||||
} else {
|
} else {
|
||||||
stdoutWriter = &stdoutBuf
|
stdoutWriter = &combinedBuf
|
||||||
}
|
}
|
||||||
|
|
||||||
if stderr != nil {
|
if stderr != nil {
|
||||||
stderrWriter = io.MultiWriter(&stderrBuf, stderr)
|
stderrWriter = io.MultiWriter(&combinedBuf, stderr)
|
||||||
} else {
|
} else {
|
||||||
stderrWriter = &stderrBuf
|
stderrWriter = &combinedBuf
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 实际开始执行事件 (经过队列和速率限制之后)
|
// 3. 实际开始执行事件 (经过队列和速率限制之后)
|
||||||
@@ -369,7 +387,7 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error)
|
|||||||
|
|
||||||
if execResult != nil {
|
if execResult != nil {
|
||||||
result.Success = execResult.Status == "success"
|
result.Success = execResult.Status == "success"
|
||||||
result.Output = stdoutBuf.String()
|
result.Output = combinedBuf.String()
|
||||||
result.Status = execResult.Status
|
result.Status = execResult.Status
|
||||||
result.Duration = execResult.Duration
|
result.Duration = execResult.Duration
|
||||||
result.ExitCode = execResult.ExitCode
|
result.ExitCode = execResult.ExitCode
|
||||||
@@ -381,17 +399,11 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error)
|
|||||||
result.StartTime = start
|
result.StartTime = start
|
||||||
result.EndTime = time.Now()
|
result.EndTime = time.Now()
|
||||||
result.Duration = result.EndTime.Sub(result.StartTime).Milliseconds()
|
result.Duration = result.EndTime.Sub(result.StartTime).Milliseconds()
|
||||||
|
result.Output = combinedBuf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if execErr != nil {
|
if execErr != nil {
|
||||||
result.Error = execErr.Error()
|
result.Error = execErr.Error()
|
||||||
if result.Output == "" {
|
|
||||||
result.Output = execErr.Error()
|
|
||||||
}
|
|
||||||
errOutput := stderrBuf.String()
|
|
||||||
if errOutput != "" {
|
|
||||||
result.Output += "\n[ERROR]\n" + errOutput
|
|
||||||
}
|
|
||||||
if ctx.Err() == context.Canceled {
|
if ctx.Err() == context.Canceled {
|
||||||
result.Status = "cancelled"
|
result.Status = "cancelled"
|
||||||
} else if ctx.Err() == context.DeadlineExceeded {
|
} else if ctx.Err() == context.DeadlineExceeded {
|
||||||
@@ -401,10 +413,12 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error)
|
|||||||
|
|
||||||
// 6. 执行后事件
|
// 6. 执行后事件
|
||||||
if s.handler != nil {
|
if s.handler != nil {
|
||||||
if execErr != nil {
|
if execResult != nil {
|
||||||
s.handler.OnTaskFailed(req, execErr)
|
// 只要有执行结果(即使执行失败),都认为是任务完成了(包含输出)
|
||||||
} else {
|
|
||||||
s.handler.OnTaskCompleted(req, result)
|
s.handler.OnTaskCompleted(req, result)
|
||||||
|
} else if execErr != nil {
|
||||||
|
// 只有在完全没有结果的情况下(如无法启动、Panic等),才认为是任务失败
|
||||||
|
s.handler.OnTaskFailed(req, execErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ type AgentTaskResult struct {
|
|||||||
AgentID uint `json:"agent_id"`
|
AgentID uint `json:"agent_id"`
|
||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Output string `json:"output"`
|
Output string `json:"output"`
|
||||||
|
Error string `json:"error"` // 额外的系统错误信息
|
||||||
Status string `json:"status"` // success, failed
|
Status string `json:"status"` // success, failed
|
||||||
Duration int64 `json:"duration"` // milliseconds
|
Duration int64 `json:"duration"` // milliseconds
|
||||||
ExitCode int `json:"exit_code"`
|
ExitCode int `json:"exit_code"`
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ type TaskLog struct {
|
|||||||
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
|
||||||
|
Error string `json:"error" gorm:"type:text"` // 额外的系统错误信息
|
||||||
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"`
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
|
|||||||
TaskID: task.ID,
|
TaskID: task.ID,
|
||||||
Command: req.Command,
|
Command: req.Command,
|
||||||
Output: output,
|
Output: output,
|
||||||
|
Error: result.Error,
|
||||||
Status: result.Status,
|
Status: result.Status,
|
||||||
Duration: result.Duration,
|
Duration: result.Duration,
|
||||||
ExitCode: result.ExitCode,
|
ExitCode: result.ExitCode,
|
||||||
@@ -262,13 +263,16 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er
|
|||||||
|
|
||||||
now := models.LocalTime(time.Now())
|
now := models.LocalTime(time.Now())
|
||||||
taskLog := &models.TaskLog{
|
taskLog := &models.TaskLog{
|
||||||
ID: req.LogID,
|
ID: req.LogID,
|
||||||
TaskID: taskID,
|
TaskID: taskID,
|
||||||
Output: output,
|
Command: req.Command,
|
||||||
Status: "failed",
|
Output: output,
|
||||||
Duration: 0,
|
Error: err.Error(),
|
||||||
ExitCode: 1,
|
Status: "failed",
|
||||||
EndTime: &now,
|
Duration: 0,
|
||||||
|
ExitCode: 1,
|
||||||
|
StartTime: &now,
|
||||||
|
EndTime: &now,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 补充 AgentID
|
// 补充 AgentID
|
||||||
@@ -645,6 +649,7 @@ func (es *ExecutorService) ExecuteRemoteForScheduler(task *models.Task, logID ui
|
|||||||
case agentResult := <-resultChan:
|
case agentResult := <-resultChan:
|
||||||
return &executor.Result{
|
return &executor.Result{
|
||||||
Output: agentResult.Output,
|
Output: agentResult.Output,
|
||||||
|
Error: agentResult.Error,
|
||||||
Status: agentResult.Status,
|
Status: agentResult.Status,
|
||||||
Duration: agentResult.Duration,
|
Duration: agentResult.Duration,
|
||||||
ExitCode: agentResult.ExitCode,
|
ExitCode: agentResult.ExitCode,
|
||||||
@@ -655,11 +660,12 @@ func (es *ExecutorService) ExecuteRemoteForScheduler(task *models.Task, logID ui
|
|||||||
end := time.Now()
|
end := time.Now()
|
||||||
return &executor.Result{
|
return &executor.Result{
|
||||||
Status: "failed",
|
Status: "failed",
|
||||||
|
Error: "等待 Agent 结果超时",
|
||||||
Duration: end.Sub(start).Milliseconds(),
|
Duration: end.Sub(start).Milliseconds(),
|
||||||
ExitCode: -1,
|
ExitCode: -1,
|
||||||
StartTime: start,
|
StartTime: start,
|
||||||
EndTime: end,
|
EndTime: end,
|
||||||
}, fmt.Errorf("远程执行超时")
|
}, fmt.Errorf("等待 Agent 结果超时")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ func (s *TaskLogService) CreateTaskLogFromAgentResult(result *models.AgentTaskRe
|
|||||||
AgentID: &result.AgentID,
|
AgentID: &result.AgentID,
|
||||||
Command: result.Command,
|
Command: result.Command,
|
||||||
Output: compressed,
|
Output: compressed,
|
||||||
|
Error: result.Error,
|
||||||
Status: result.Status,
|
Status: result.Status,
|
||||||
Duration: result.Duration,
|
Duration: result.Duration,
|
||||||
ExitCode: result.ExitCode,
|
ExitCode: result.ExitCode,
|
||||||
@@ -175,7 +176,7 @@ func (s *TaskLogService) CreateTaskLogFromAgentResult(result *models.AgentTaskRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateTaskLogFromLocalExecution 从本地执行结果创建任务日志
|
// CreateTaskLogFromLocalExecution 从本地执行结果创建任务日志
|
||||||
func (s *TaskLogService) CreateTaskLogFromLocalExecution(taskID uint, command, output, status string, duration int64, exitCode int, start, end time.Time, isCompressed bool) (*models.TaskLog, error) {
|
func (s *TaskLogService) CreateTaskLogFromLocalExecution(taskID uint, command, output, systemErr, status string, duration int64, exitCode int, start, end time.Time, isCompressed bool) (*models.TaskLog, error) {
|
||||||
var compressed string
|
var compressed string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -197,6 +198,7 @@ func (s *TaskLogService) CreateTaskLogFromLocalExecution(taskID uint, command, o
|
|||||||
TaskID: taskID,
|
TaskID: taskID,
|
||||||
Command: command,
|
Command: command,
|
||||||
Output: compressed,
|
Output: compressed,
|
||||||
|
Error: systemErr,
|
||||||
Status: status,
|
Status: status,
|
||||||
Duration: duration,
|
Duration: duration,
|
||||||
ExitCode: exitCode,
|
ExitCode: exitCode,
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ export interface TaskLog {
|
|||||||
command: string
|
command: string
|
||||||
status: string
|
status: string
|
||||||
duration: number
|
duration: number
|
||||||
|
error: string | null
|
||||||
start_time: string | null
|
start_time: string | null
|
||||||
end_time: string | null
|
end_time: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
@@ -336,6 +337,7 @@ export interface LogDetail {
|
|||||||
task_id: number
|
task_id: number
|
||||||
command: string
|
command: string
|
||||||
output: string
|
output: string
|
||||||
|
error: string | null
|
||||||
status: string
|
status: string
|
||||||
duration: number
|
duration: number
|
||||||
start_time: string | null
|
start_time: string | null
|
||||||
|
|||||||
@@ -340,6 +340,15 @@ watch(() => route.query.task_id, (newTaskId) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col overflow-hidden">
|
<div class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<div v-if="selectedLog.error" class="px-4 py-3 border-b bg-red-500/5 space-y-2 text-sm">
|
||||||
|
<div class="flex items-center gap-2 text-red-500 font-medium">
|
||||||
|
<X class="h-4 w-4" />
|
||||||
|
<span>系统错误</span>
|
||||||
|
</div>
|
||||||
|
<code class="block font-mono bg-red-500/10 text-red-600 px-2 py-1 rounded text-xs break-all">
|
||||||
|
{{ selectedLog.error }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
<div class="px-4 py-2 text-sm text-muted-foreground border-b bg-muted/50 flex items-center justify-between">
|
<div class="px-4 py-2 text-sm text-muted-foreground border-b bg-muted/50 flex items-center justify-between">
|
||||||
<span>输出</span>
|
<span>输出</span>
|
||||||
<Button variant="ghost" size="icon" class="h-6 w-6" @click="showFullscreen = true" title="全屏查看">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="showFullscreen = true" title="全屏查看">
|
||||||
|
|||||||
Reference in New Issue
Block a user