定时任务显示完整运行历史

This commit is contained in:
whyour
2026-06-28 15:48:58 +08:00
parent 5fbff0e1c8
commit d3016431ce
8 changed files with 116 additions and 136 deletions
+1 -1
View File
@@ -434,6 +434,7 @@ export default (app: Router) => {
log_path: Joi.string().optional().allow(null),
last_running_time: Joi.number().optional().allow(null),
last_execution_time: Joi.number().optional().allow(null),
exit_code: Joi.number().optional().allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -463,7 +464,6 @@ export default (app: Router) => {
const instances = await RunningInstanceModel.findAll({
where: {
cron_id: req.params.id,
status: InstanceStatus.running,
},
order: [['started_at', 'DESC']],
raw: true,
+11 -1
View File
@@ -11,6 +11,7 @@ import {
rmPath,
} from '../config/util';
import LogService from '../services/log';
import { InstanceStatus, RunningInstanceModel } from '../data/runningInstance';
const route = Router();
const blacklist = ['.tmp'];
@@ -46,8 +47,17 @@ export default (app: Router) => {
message: t('暂无权限'),
});
}
const logPath = `${req.query.path as string}/${req.query.file as string}`;
const runningInstance = await RunningInstanceModel.findOne({
where: { log_path: logPath, status: InstanceStatus.running },
});
const content = await getFileContentByName(finalPath);
res.send({ code: 200, data: removeAnsi(content) });
res.send({
code: 200,
data: removeAnsi(content),
logStatus: runningInstance ? 'running' : undefined,
});
} catch (e) {
return next(e);
}
+9 -31
View File
@@ -166,6 +166,7 @@ export default class CronService {
log_path,
last_running_time = 0,
last_execution_time = 0,
exit_code,
}: {
ids: number[];
status: CrontabStatus;
@@ -173,6 +174,7 @@ export default class CronService {
log_path: string;
last_running_time: number;
last_execution_time: number;
exit_code?: number;
}) {
let options: any = {
status,
@@ -209,10 +211,15 @@ export default class CronService {
} else if (status === CrontabStatus.idle) {
// Mark the matching running instance as finished
const finishedAt = dayjs().unix();
const instanceStatus =
exit_code !== undefined && exit_code !== null && exit_code !== 0
? InstanceStatus.error
: InstanceStatus.finished;
await RunningInstanceModel.update(
{
finished_at: finishedAt,
status: InstanceStatus.finished,
status: instanceStatus,
exit_code: exit_code ?? undefined,
},
{
where: {
@@ -564,7 +571,7 @@ export default class CronService {
}
}
await RunningInstanceModel.update(
{ status: InstanceStatus.stopped, finished_at: dayjs().unix() },
{ status: InstanceStatus.stopped, finished_at: dayjs().unix(), exit_code: 143 },
{ where: { id: instanceId } },
);
@@ -619,15 +626,6 @@ export default class CronService {
{ shell: '/bin/bash' },
);
const startedAt = dayjs().unix();
const instance = await RunningInstanceModel.create({
cron_id: id!,
pid: cp.pid,
log_path: logPath,
started_at: startedAt,
status: InstanceStatus.running,
});
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
{ where: { id } },
@@ -659,26 +657,6 @@ export default class CronService {
code,
);
await logStreamManager.closeStream(absolutePath);
const finishedAt = dayjs().unix();
await RunningInstanceModel.update(
{
finished_at: finishedAt,
status: code === 0 ? InstanceStatus.finished : InstanceStatus.error,
exit_code: code ?? undefined,
},
{ where: { id: instance.id } },
);
// Only set cron to idle if no other running instances exist
const otherRunning = await RunningInstanceModel.count({
where: { cron_id: id!, status: InstanceStatus.running },
});
if (otherRunning === 0) {
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
}
resolve({ ...params, pid: cp.pid, code });
});
});
+7 -1
View File
@@ -141,13 +141,19 @@ update_cron() {
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local exitCode="${7:-}"
local currentTimeStamp=$(date +%s)
local dataRaw="{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime"
if [[ -n $exitCode ]]; then
dataRaw="${dataRaw},\"exit_code\":$exitCode"
fi
dataRaw="${dataRaw}}"
local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \
-H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--data-raw "$dataRaw" \
--compressed
)
code=$(echo "$api" | jq -r .code)
+1 -1
View File
@@ -414,7 +414,7 @@ handle_task_end() {
[[ "$diff_time" == 0 ]] && diff_time=1
if [[ $ID ]]; then
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time")
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time" "$exit_code")
if [[ $error ]]; then
error_message=", 状态更新失败(${error})"
fi
+6 -1
View File
@@ -600,5 +600,10 @@
"确认保存": "Confirm to save",
"上传失败": "Upload failed",
"成功上传": "Successfully uploaded",
"个环境变量": " environment variables"
"个环境变量": " environment variables",
"运行历史": "Run History",
"已完成": "Completed",
"已停止": "Stopped",
"退出码": "Exit Code",
"结束": "End"
}
+6 -1
View File
@@ -600,5 +600,10 @@
"确认保存": "确认保存",
"上传失败": "上传失败",
"成功上传": "成功上传",
"个环境变量": "个环境变量"
"个环境变量": "个环境变量",
"运行历史": "运行历史",
"已完成": "已完成",
"已停止": "已停止",
"退出码": "退出码",
"结束": "结束"
}
+54 -78
View File
@@ -3,8 +3,6 @@ import React, { useEffect, useRef, useState } from 'react';
import {
Modal,
message,
Input,
Form,
Button,
Card,
Tag,
@@ -40,24 +38,15 @@ const { Text } = Typography;
const tabList = [
{
key: 'log',
tab: intl.get('日志'),
key: 'runningHistory',
tab: intl.get('运行历史'),
},
{
key: 'script',
tab: intl.get('脚本'),
},
{
key: 'runningInstance',
tab: intl.get('运行实例'),
},
];
interface LogItem {
directory: string;
filename: string;
}
const CronDetailModal = ({
cron = {},
handleCancel,
@@ -69,21 +58,19 @@ const CronDetailModal = ({
theme: string;
isPhone: boolean;
}) => {
const [activeTabKey, setActiveTabKey] = useState('log');
const [loading, setLoading] = useState(true);
const [logs, setLogs] = useState<LogItem[]>([]);
const [log, setLog] = useState('');
const [activeTabKey, setActiveTabKey] = useState('runningHistory');
const [value, setValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const editorRef = useRef<any>(null);
const [scriptInfo, setScriptInfo] = useState<any>({});
const [logUrl, setLogUrl] = useState('');
const [validTabs, setValidTabs] = useState(tabList);
const [currentCron, setCurrentCron] = useState<any>({});
const listRef = useRef<HTMLDivElement>(null);
const tableScrollHeight = useScrollHeight(listRef);
const [runningInstances, setRunningInstances] = useState<any[]>([]);
const needRefreshRef = useRef(false);
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logData, setLogData] = useState('');
const [logUrl, setLogUrl] = useState('');
const fetchRunningInstances = async () => {
if (!cron.id) return Promise.resolve();
@@ -114,25 +101,6 @@ const CronDetailModal = ({
}, [cron.id]);
const contentList: any = {
log: (
<div ref={listRef}>
<List>
<VirtualList
data={logs}
height={tableScrollHeight}
itemHeight={47}
itemKey="filename"
>
{(item) => (
<List.Item className="log-item" onClick={() => onClickItem(item)}>
<FileOutlined style={{ marginRight: 10 }} />
{item.directory}/{item.filename}
</List.Item>
)}
</VirtualList>
</List>
</div>
),
script: scriptInfo.filename && (
<Editor
language={getEditorMode(scriptInfo.filename)}
@@ -150,7 +118,7 @@ const CronDetailModal = ({
}}
/>
),
runningInstance: (
runningHistory: (
<div ref={listRef}>
<List>
<VirtualList
@@ -163,6 +131,7 @@ const CronDetailModal = ({
<List.Item
className="log-item"
actions={[
item.log_path && (
<Tooltip title={intl.get('查看日志')} key="log">
<Button
type="link"
@@ -173,7 +142,9 @@ const CronDetailModal = ({
viewInstanceLog(item);
}}
/>
</Tooltip>,
</Tooltip>
),
item.status === 0 && (
<Tooltip title={intl.get('停止')} key="stop">
<Button
type="link"
@@ -185,18 +156,52 @@ const CronDetailModal = ({
stopRunningInstance(item);
}}
/>
</Tooltip>,
]}
</Tooltip>
),
].filter(Boolean)}
>
<List.Item.Meta
title={
<span>
PID: <Tag color="processing">{item.pid}</Tag>
{item.log_path && (
<span style={{ marginRight: 8 }}>
{item.log_path.split('/').pop()}
</span>
)}
{item.status === 0 && (
<Tag icon={<Loading3QuartersOutlined spin />} color="processing">
{intl.get('运行中')}
</Tag>
)}
{item.status === 1 && (
<Tag color="success">{intl.get('已完成')}</Tag>
)}
{item.status === 2 && (
<Tag color="default">{intl.get('已停止')}</Tag>
)}
{item.status === 3 && (
<Tag color="error">{intl.get('错误')}</Tag>
)}
</span>
}
description={
<span style={{ color: '#999' }}>
{item.status === 0 && item.pid && (
<span>
PID: {item.pid}{' | '}
</span>
)}
{intl.get('启动')}: {dayjs.unix(item.started_at).format('YYYY-MM-DD HH:mm:ss')}
{item.finished_at && (
<span>
{' | '}{intl.get('结束')}: {dayjs.unix(item.finished_at).format('YYYY-MM-DD HH:mm:ss')}
</span>
)}
{item.exit_code !== undefined && item.exit_code !== null && (
<span>
{' | '}{intl.get('退出码')}: {item.exit_code}
</span>
)}
</span>
}
/>
@@ -225,7 +230,6 @@ const CronDetailModal = ({
message.success(intl.get('实例已停止'));
needRefreshRef.current = true;
fetchRunningInstances();
setTimeout(() => getLogs(), 1000);
}
});
},
@@ -242,20 +246,7 @@ const CronDetailModal = ({
setLogUrl(url);
request.get(url).then(({ code, data }) => {
if (code === 200) {
setLog(data);
setIsLogModalVisible(true);
}
});
};
const onClickItem = (item: LogItem) => {
const url = `${config.apiPrefix}logs/detail?file=${item.filename}&path=${item.directory || ''
}`;
localStorage.setItem('logCron', url);
setLogUrl(url);
request.get(url).then(({ code, data }) => {
if (code === 200) {
setLog(data);
setLogData(data);
setIsLogModalVisible(true);
}
});
@@ -265,22 +256,9 @@ const CronDetailModal = ({
setActiveTabKey(key);
};
const getLogs = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons/${cron.id}/logs`)
.then(({ code, data }) => {
if (code === 200) {
setLogs(data);
}
})
.finally(() => setLoading(false));
};
const getScript = () => {
const result = getCommandScript(cron.command);
if (Array.isArray(result)) {
setValidTabs(validTabs);
const [s, p] = result;
setScriptInfo({ parent: p, filename: s });
request
@@ -291,8 +269,8 @@ const CronDetailModal = ({
}
});
} else {
setValidTabs([validTabs[0]]);
setActiveTabKey('log');
setValidTabs([{ key: 'runningHistory', tab: intl.get('运行历史') }]);
setActiveTabKey('runningHistory');
}
};
@@ -351,9 +329,7 @@ const CronDetailModal = ({
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running });
setTimeout(() => {
getLogs();
}, 1000);
fetchRunningInstances();
}
});
},
@@ -460,7 +436,6 @@ const CronDetailModal = ({
useEffect(() => {
if (cron && cron.id) {
setCurrentCron(cron);
getLogs();
getScript();
}
}, [cron]);
@@ -687,9 +662,10 @@ const CronDetailModal = ({
<CronLogModal
handleCancel={() => {
setIsLogModalVisible(false);
fetchRunningInstances();
}}
cron={cron}
data={log}
data={logData}
logUrl={logUrl}
/>
)}