diff --git a/internal/constant/constant.go b/internal/constant/constant.go index 695a4cb..763b964 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -81,11 +81,12 @@ const ( EventPasswordChanged = "password_changed" // 任务事件类型 - EventTaskSuccess = "task_success" - EventTaskFailed = "task_failed" - EventTaskTimeout = "task_timeout" - EventTaskRunning = "task_running" - EventTaskQueued = "task_queued" + EventTaskSuccess = "task_success" + EventTaskFailed = "task_failed" + EventTaskTimeout = "task_timeout" + EventTaskRunning = "task_running" + EventTaskQueued = "task_queued" + EventTaskCancelled = "task_cancelled" // 其他事件类型 EventSystemNotice = "system_notice" diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 1158c68..514aed7 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -161,6 +161,9 @@ func ExecuteWithHooks(ctx context.Context, req Request, stdout, stderr io.Writer shell, args := utils.GetShellCommand(req.Command) cmd := exec.CommandContext(execCtx, shell, args...) + usePty := runtime.GOOS != "windows" && stdout != nil && (stdout == stderr || stdout == io.Discard) + SetProcessGroupAndCancel(cmd, usePty) + // 设置工作目录 // 设置工作目录 workDir := strings.TrimSpace(req.WorkDir) diff --git a/internal/executor/proc_unix.go b/internal/executor/proc_unix.go new file mode 100644 index 0000000..959c875 --- /dev/null +++ b/internal/executor/proc_unix.go @@ -0,0 +1,21 @@ +//go:build !windows + +package executor + +import ( + "os/exec" + "syscall" +) + +func SetProcessGroupAndCancel(cmd *exec.Cmd, usePty bool) { + if !usePty { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + } + cmd.Cancel = func() error { + if cmd.Process != nil { + // Kill the entire process group by sending SIGKILL to negative PID + return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } + return nil + } +} diff --git a/internal/executor/proc_windows.go b/internal/executor/proc_windows.go new file mode 100644 index 0000000..ddfc1c2 --- /dev/null +++ b/internal/executor/proc_windows.go @@ -0,0 +1,18 @@ +//go:build windows + +package executor + +import ( + "fmt" + "os/exec" +) + +func SetProcessGroupAndCancel(cmd *exec.Cmd, usePty bool) { + cmd.Cancel = func() error { + if cmd.Process != nil { + killCmd := exec.Command("taskkill", "/F", "/T", "/PID", fmt.Sprintf("%d", cmd.Process.Pid)) + return killCmd.Run() + } + return nil + } +} diff --git a/internal/services/system_ws_service.go b/internal/services/system_ws_service.go index 9589106..409df7d 100644 --- a/internal/services/system_ws_service.go +++ b/internal/services/system_ws_service.go @@ -100,6 +100,7 @@ func (m *SystemWSManager) SubscribeEvents(bus *eventbus.EventBus) { constant.EventTaskTimeout, constant.EventTaskRunning, constant.EventTaskQueued, + constant.EventTaskCancelled, } for _, evt := range taskEvents { diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index d039b4f..0e26141 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -304,6 +304,8 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest, eventType = constant.EventTaskFailed case constant.TaskStatusTimeout: eventType = constant.EventTaskTimeout + case constant.TaskStatusCancelled: + eventType = constant.EventTaskCancelled } if eventType != "" { eventbus.DefaultBus.Publish(eventbus.Event{ diff --git a/web/src/components/LogDetailCard.vue b/web/src/components/LogDetailCard.vue index 3d37e58..a65a387 100644 --- a/web/src/components/LogDetailCard.vue +++ b/web/src/components/LogDetailCard.vue @@ -96,19 +96,19 @@ watch(() => props.log?.duration, (newVal) => { function getStatusBadgeClass(status: string) { switch (status) { case TASK_STATUS.SUCCESS: - return 'bg-green-500/10 text-green-600 border-green-500/20 dark:bg-green-500/20 dark:text-green-400 dark:border-green-500/30 shadow-[0_0_8px_-2px_rgba(34,197,94,0.15)]' + return 'bg-emerald-500/5 text-emerald-500 border-emerald-500/15 dark:bg-emerald-500/10 dark:text-emerald-400 dark:border-emerald-500/20' case TASK_STATUS.FAILED: - return 'bg-red-500/10 text-red-600 border-red-500/20 dark:bg-red-500/20 dark:text-red-400 dark:border-red-500/30' + return 'bg-rose-500/5 text-rose-500 border-rose-500/15 dark:bg-rose-500/10 dark:text-rose-400 dark:border-rose-500/20' case TASK_STATUS.RUNNING: - return 'bg-blue-500/10 text-blue-600 border-blue-500/20 dark:bg-blue-500/20 dark:text-blue-400 dark:border-blue-500/30' + return 'bg-sky-500/5 text-sky-500 border-sky-500/15 dark:bg-sky-500/10 dark:text-sky-400 dark:border-sky-500/20' case TASK_STATUS.PENDING: - return 'bg-amber-500/10 text-amber-600 border-amber-500/20 dark:bg-amber-500/20 dark:text-amber-400 dark:border-amber-500/30' + return 'bg-amber-500/5 text-amber-500 border-amber-500/15 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/20' case TASK_STATUS.TIMEOUT: - return 'bg-orange-500/10 text-orange-600 border-orange-500/20 dark:bg-orange-500/20 dark:text-orange-400 dark:border-orange-500/30' + return 'bg-orange-500/5 text-orange-500 border-orange-500/15 dark:bg-orange-500/10 dark:text-orange-400 dark:border-orange-500/20' case TASK_STATUS.CANCELLED: - return 'bg-muted/50 text-muted-foreground border-muted-foreground/10' + return 'bg-zinc-500/5 text-zinc-400 border-zinc-500/15 dark:bg-zinc-500/10 dark:text-zinc-400 dark:border-zinc-500/20' case 'UNEXECUTED': - return 'bg-muted/30 text-muted-foreground/60 border-dashed border-muted-foreground/20 shadow-none' + return 'bg-zinc-500/5 text-zinc-400 border-zinc-500/15 border-dashed dark:bg-zinc-500/10 dark:text-zinc-400 dark:border-zinc-500/20' default: return 'bg-secondary text-secondary-foreground border-transparent' } @@ -135,7 +135,7 @@ function getStatusBadgeClass(status: string) { @@ -143,7 +143,7 @@ function getStatusBadgeClass(status: string) {