chore: try to add stop task more info #75
This commit is contained in:
@@ -194,6 +194,14 @@ func (m *AgentWSManager) GetConnection(agentID string) *AgentConnection {
|
|||||||
return m.connections[agentID]
|
return m.connections[agentID]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsAgentOnline 检查指定 Agent 是否在线
|
||||||
|
func (m *AgentWSManager) IsAgentOnline(agentID string) bool {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
_, exists := m.connections[agentID]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
// SendToAgent 发送消息给指定 Agent
|
// SendToAgent 发送消息给指定 Agent
|
||||||
func (m *AgentWSManager) SendToAgent(agentID string, msgType string, data interface{}) error {
|
func (m *AgentWSManager) SendToAgent(agentID string, msgType string, data interface{}) error {
|
||||||
conn := m.GetConnection(agentID)
|
conn := m.GetConnection(agentID)
|
||||||
|
|||||||
@@ -414,13 +414,14 @@ func (s *NotificationService) handleEvent(bindingType string) eventbus.Handler {
|
|||||||
tmplTextKey = constant.KeyNotifyTemplatePasswordChangedText
|
tmplTextKey = constant.KeyNotifyTemplatePasswordChangedText
|
||||||
|
|
||||||
case constant.EventTaskSuccess, constant.EventTaskFailed, constant.EventTaskTimeout:
|
case constant.EventTaskSuccess, constant.EventTaskFailed, constant.EventTaskTimeout:
|
||||||
if e.Type == constant.EventTaskSuccess {
|
switch e.Type {
|
||||||
|
case constant.EventTaskSuccess:
|
||||||
tmplTitleKey = constant.KeyNotifyTemplateTaskSuccessTitle
|
tmplTitleKey = constant.KeyNotifyTemplateTaskSuccessTitle
|
||||||
tmplTextKey = constant.KeyNotifyTemplateTaskSuccessText
|
tmplTextKey = constant.KeyNotifyTemplateTaskSuccessText
|
||||||
} else if e.Type == constant.EventTaskFailed {
|
case constant.EventTaskFailed:
|
||||||
tmplTitleKey = constant.KeyNotifyTemplateTaskFailedTitle
|
tmplTitleKey = constant.KeyNotifyTemplateTaskFailedTitle
|
||||||
tmplTextKey = constant.KeyNotifyTemplateTaskFailedText
|
tmplTextKey = constant.KeyNotifyTemplateTaskFailedText
|
||||||
} else {
|
case constant.EventTaskTimeout:
|
||||||
tmplTitleKey = constant.KeyNotifyTemplateTaskTimeoutTitle
|
tmplTitleKey = constant.KeyNotifyTemplateTaskTimeoutTitle
|
||||||
tmplTextKey = constant.KeyNotifyTemplateTaskTimeoutText
|
tmplTextKey = constant.KeyNotifyTemplateTaskTimeoutText
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type AgentWSManager interface {
|
|||||||
RegisterRemoteWaiter(logID string) chan *models.AgentTaskResult
|
RegisterRemoteWaiter(logID string) chan *models.AgentTaskResult
|
||||||
UnregisterRemoteWaiter(logID string)
|
UnregisterRemoteWaiter(logID string)
|
||||||
SendToAgent(agentID string, msgType string, data interface{}) error
|
SendToAgent(agentID string, msgType string, data interface{}) error
|
||||||
|
IsAgentOnline(agentID string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SettingsService 接口定义(避免循环依赖)
|
// SettingsService 接口定义(避免循环依赖)
|
||||||
@@ -633,33 +634,66 @@ func (es *ExecutorService) StopTaskExecution(logID string) error {
|
|||||||
var taskLog models.TaskLog
|
var taskLog models.TaskLog
|
||||||
res := database.DB.Where("id = ?", logID).Limit(1).Find(&taskLog)
|
res := database.DB.Where("id = ?", logID).Limit(1).Find(&taskLog)
|
||||||
if res.Error != nil || res.RowsAffected == 0 {
|
if res.Error != nil || res.RowsAffected == 0 {
|
||||||
return fmt.Errorf("日志不存在")
|
return fmt.Errorf("停止失败:找不到指定的执行记录 (LogID: %s)", logID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. 状态预校验
|
||||||
if taskLog.Status != constant.TaskStatusRunning {
|
if taskLog.Status != constant.TaskStatusRunning {
|
||||||
return fmt.Errorf("任务已结束")
|
statusText := "已结束"
|
||||||
|
switch taskLog.Status {
|
||||||
|
case constant.TaskStatusSuccess:
|
||||||
|
statusText = "执行成功"
|
||||||
|
case constant.TaskStatusFailed:
|
||||||
|
statusText = "执行失败"
|
||||||
|
case constant.TaskStatusTimeout:
|
||||||
|
statusText = "执行超时"
|
||||||
|
case constant.TaskStatusCancelled:
|
||||||
|
statusText = "已取消"
|
||||||
|
}
|
||||||
|
return fmt.Errorf("操作无效:任务当前状态为 [%s],无需停止", statusText)
|
||||||
}
|
}
|
||||||
|
|
||||||
task := es.taskService.GetTaskByID(taskLog.TaskID)
|
task := es.taskService.GetTaskByID(taskLog.TaskID)
|
||||||
if task == nil {
|
if task == nil {
|
||||||
return fmt.Errorf("任务不存在")
|
return fmt.Errorf("停止失败:关联的任务信息已丢失")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 远程任务:发送停止指令到 Agent
|
// 2. 远程任务逻辑
|
||||||
if task.AgentID != nil && *task.AgentID != "" {
|
if task.AgentID != nil && *task.AgentID != "" {
|
||||||
|
// 校验 Agent 是否在线
|
||||||
|
if !es.agentWSManager.IsAgentOnline(*task.AgentID) {
|
||||||
|
return fmt.Errorf("停止失败:目标 Agent (%s) 当前离线,无法下发指令", *task.AgentID)
|
||||||
|
}
|
||||||
|
|
||||||
logger.Infof("[Executor] 请求停止远程任务 #%s (Agent #%s, LogID: %s)", task.ID, *task.AgentID, logID)
|
logger.Infof("[Executor] 请求停止远程任务 #%s (Agent #%s, LogID: %s)", task.ID, *task.AgentID, logID)
|
||||||
return es.agentWSManager.SendToAgent(*task.AgentID, constant.WSTypeStop, map[string]interface{}{
|
err := es.agentWSManager.SendToAgent(*task.AgentID, constant.WSTypeStop, map[string]interface{}{
|
||||||
"log_id": logID,
|
"log_id": logID,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("下发停止指令失败: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本地任务:直接停止调度器中的执行实例
|
// 3. 本地任务逻辑
|
||||||
logger.Infof("[Executor] 请求停止本地任务 #%s (LogID: %s)", task.ID, logID)
|
logger.Infof("[Executor] 请求停止本地任务 #%s (LogID: %s)", task.ID, logID)
|
||||||
if es.scheduler.StopLog(logID) {
|
if es.scheduler.StopLog(logID) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("任务当前不在运行队列中或已完成")
|
// 4. 容错处理:如果调度器中没有句柄,但数据库状态还是 running
|
||||||
|
// 这通常发生在程序异常重启后,需要手动清理掉这个“僵尸状态”
|
||||||
|
taskLog.Status = constant.TaskStatusFailed
|
||||||
|
errorMessage := "任务执行实例已丢失(可能由于系统重启导致),已自动同步状态为失败"
|
||||||
|
taskLog.Error = models.BigText(errorMessage)
|
||||||
|
|
||||||
|
// 更新数据库状态
|
||||||
|
database.DB.Model(&taskLog).Updates(map[string]interface{}{
|
||||||
|
"status": taskLog.Status,
|
||||||
|
"error": taskLog.Error,
|
||||||
|
})
|
||||||
|
|
||||||
|
return fmt.Errorf("停止失败:%s", errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRunningCount 获取正在运行任务数量
|
// GetRunningCount 获取正在运行任务数量
|
||||||
|
|||||||
+10
-6
@@ -387,12 +387,16 @@ export interface RepoConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecutionResult {
|
export interface ExecutionResult {
|
||||||
TaskID: string
|
task_id: string
|
||||||
Success: boolean
|
log_id?: string
|
||||||
Output: string
|
success: boolean
|
||||||
Error: string
|
status?: string
|
||||||
Start: string
|
output?: string
|
||||||
End: string
|
error?: string
|
||||||
|
duration?: number
|
||||||
|
exit_code?: number
|
||||||
|
start_time?: string
|
||||||
|
end_time?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskListResponse {
|
export interface TaskListResponse {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const props = withDefaults(defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:open': [value: boolean]
|
'update:open': [value: boolean]
|
||||||
|
'stop': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isFullscreen = ref(false)
|
const isFullscreen = ref(false)
|
||||||
@@ -74,6 +75,7 @@ onUnmounted(() => {
|
|||||||
:empty-description="emptyDescription"
|
:empty-description="emptyDescription"
|
||||||
@close="close"
|
@close="close"
|
||||||
@maximize="isFullscreen = !isFullscreen"
|
@maximize="isFullscreen = !isFullscreen"
|
||||||
|
@stop="$emit('stop')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -205,23 +205,41 @@ async function deleteTask() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const executingTaskId = ref<string | null>(null)
|
const executingTaskId = ref<string | null>(null)
|
||||||
|
const isStopping = ref(false)
|
||||||
|
|
||||||
async function runTask(id: string) {
|
async function runTask(id: string) {
|
||||||
|
if (executingTaskId.value) return
|
||||||
executingTaskId.value = id
|
executingTaskId.value = id
|
||||||
toast.message('正在执行...', { id: 'executing' })
|
|
||||||
try {
|
try {
|
||||||
const res = await api.tasks.execute(id)
|
const res = await api.tasks.execute(id)
|
||||||
if (res.Success === false) {
|
toast.success('执行指令已发送')
|
||||||
throw new Error(res.Error || '执行失败')
|
if (res.log_id) {
|
||||||
|
// 开启日志查看器
|
||||||
|
viewLogs(id)
|
||||||
}
|
}
|
||||||
toast.success('触发成功', { id: 'executing' })
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(error?.message || '执行失败', { id: 'executing' })
|
toast.error(error.message || '执行失败')
|
||||||
} finally {
|
} finally {
|
||||||
executingTaskId.value = null
|
executingTaskId.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleStopTask() {
|
||||||
|
if (!selectedLog.value || isStopping.value) return
|
||||||
|
|
||||||
|
isStopping.value = true
|
||||||
|
try {
|
||||||
|
await api.tasks.stop(selectedLog.value.id)
|
||||||
|
toast.success('停止指令已发送')
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || '停止失败')
|
||||||
|
// 出错时也尝试刷新,因为后端可能已经自动修正了“僵尸”状态
|
||||||
|
loadTasks()
|
||||||
|
} finally {
|
||||||
|
isStopping.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleTask(task: Task, enabled: boolean) {
|
async function toggleTask(task: Task, enabled: boolean) {
|
||||||
try {
|
try {
|
||||||
await api.tasks.update(task.id, { ...task, enabled })
|
await api.tasks.update(task.id, { ...task, enabled })
|
||||||
@@ -830,8 +848,10 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
variant="full"
|
variant="full"
|
||||||
:log="selectedLog"
|
:log="selectedLog"
|
||||||
:content="displayLogContent"
|
:content="displayLogContent"
|
||||||
|
:is-stopping="isStopping"
|
||||||
:empty-title="logEmptyTitle"
|
:empty-title="logEmptyTitle"
|
||||||
:empty-description="logEmptyDesc" />
|
:empty-description="logEmptyDesc"
|
||||||
|
@stop="handleStopTask" />
|
||||||
|
|
||||||
|
|
||||||
<!-- 删除确认 (批量) -->
|
<!-- 删除确认 (批量) -->
|
||||||
|
|||||||
Reference in New Issue
Block a user