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

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), log_path: Joi.string().optional().allow(null),
last_running_time: Joi.number().optional().allow(null), last_running_time: Joi.number().optional().allow(null),
last_execution_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) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -463,7 +464,6 @@ export default (app: Router) => {
const instances = await RunningInstanceModel.findAll({ const instances = await RunningInstanceModel.findAll({
where: { where: {
cron_id: req.params.id, cron_id: req.params.id,
status: InstanceStatus.running,
}, },
order: [['started_at', 'DESC']], order: [['started_at', 'DESC']],
raw: true, raw: true,
+11 -1
View File
@@ -11,6 +11,7 @@ import {
rmPath, rmPath,
} from '../config/util'; } from '../config/util';
import LogService from '../services/log'; import LogService from '../services/log';
import { InstanceStatus, RunningInstanceModel } from '../data/runningInstance';
const route = Router(); const route = Router();
const blacklist = ['.tmp']; const blacklist = ['.tmp'];
@@ -46,8 +47,17 @@ export default (app: Router) => {
message: t('暂无权限'), 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); 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) { } catch (e) {
return next(e); return next(e);
} }
+9 -31
View File
@@ -166,6 +166,7 @@ export default class CronService {
log_path, log_path,
last_running_time = 0, last_running_time = 0,
last_execution_time = 0, last_execution_time = 0,
exit_code,
}: { }: {
ids: number[]; ids: number[];
status: CrontabStatus; status: CrontabStatus;
@@ -173,6 +174,7 @@ export default class CronService {
log_path: string; log_path: string;
last_running_time: number; last_running_time: number;
last_execution_time: number; last_execution_time: number;
exit_code?: number;
}) { }) {
let options: any = { let options: any = {
status, status,
@@ -209,10 +211,15 @@ export default class CronService {
} else if (status === CrontabStatus.idle) { } else if (status === CrontabStatus.idle) {
// Mark the matching running instance as finished // Mark the matching running instance as finished
const finishedAt = dayjs().unix(); const finishedAt = dayjs().unix();
const instanceStatus =
exit_code !== undefined && exit_code !== null && exit_code !== 0
? InstanceStatus.error
: InstanceStatus.finished;
await RunningInstanceModel.update( await RunningInstanceModel.update(
{ {
finished_at: finishedAt, finished_at: finishedAt,
status: InstanceStatus.finished, status: instanceStatus,
exit_code: exit_code ?? undefined,
}, },
{ {
where: { where: {
@@ -564,7 +571,7 @@ export default class CronService {
} }
} }
await RunningInstanceModel.update( await RunningInstanceModel.update(
{ status: InstanceStatus.stopped, finished_at: dayjs().unix() }, { status: InstanceStatus.stopped, finished_at: dayjs().unix(), exit_code: 143 },
{ where: { id: instanceId } }, { where: { id: instanceId } },
); );
@@ -619,15 +626,6 @@ export default class CronService {
{ shell: '/bin/bash' }, { 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( await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath }, { status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
{ where: { id } }, { where: { id } },
@@ -659,26 +657,6 @@ export default class CronService {
code, code,
); );
await logStreamManager.closeStream(absolutePath); 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 }); resolve({ ...params, pid: cp.pid, code });
}); });
}); });
+7 -1
View File
@@ -141,13 +141,19 @@ update_cron() {
local logPath="$4" local logPath="$4"
local lastExecutingTime="${5:-0}" local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}" local runningTime="${6:-0}"
local exitCode="${7:-}"
local currentTimeStamp=$(date +%s) 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=$( local api=$(
curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons/status?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://localhost:${ql_port}/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Authorization: Bearer ${__ql_token__}" \ -H "Authorization: Bearer ${__ql_token__}" \
-H "Content-Type: application/json;charset=UTF-8" \ -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 --compressed
) )
code=$(echo "$api" | jq -r .code) code=$(echo "$api" | jq -r .code)
+1 -1
View File
@@ -414,7 +414,7 @@ handle_task_end() {
[[ "$diff_time" == 0 ]] && diff_time=1 [[ "$diff_time" == 0 ]] && diff_time=1
if [[ $ID ]]; then 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 if [[ $error ]]; then
error_message=", 状态更新失败(${error})" error_message=", 状态更新失败(${error})"
fi fi
+6 -1
View File
@@ -600,5 +600,10 @@
"确认保存": "Confirm to save", "确认保存": "Confirm to save",
"上传失败": "Upload failed", "上传失败": "Upload failed",
"成功上传": "Successfully uploaded", "成功上传": "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 { import {
Modal, Modal,
message, message,
Input,
Form,
Button, Button,
Card, Card,
Tag, Tag,
@@ -40,24 +38,15 @@ const { Text } = Typography;
const tabList = [ const tabList = [
{ {
key: 'log', key: 'runningHistory',
tab: intl.get('日志'), tab: intl.get('运行历史'),
}, },
{ {
key: 'script', key: 'script',
tab: intl.get('脚本'), tab: intl.get('脚本'),
}, },
{
key: 'runningInstance',
tab: intl.get('运行实例'),
},
]; ];
interface LogItem {
directory: string;
filename: string;
}
const CronDetailModal = ({ const CronDetailModal = ({
cron = {}, cron = {},
handleCancel, handleCancel,
@@ -69,21 +58,19 @@ const CronDetailModal = ({
theme: string; theme: string;
isPhone: boolean; isPhone: boolean;
}) => { }) => {
const [activeTabKey, setActiveTabKey] = useState('log'); const [activeTabKey, setActiveTabKey] = useState('runningHistory');
const [loading, setLoading] = useState(true);
const [logs, setLogs] = useState<LogItem[]>([]);
const [log, setLog] = useState('');
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [scriptInfo, setScriptInfo] = useState<any>({}); const [scriptInfo, setScriptInfo] = useState<any>({});
const [logUrl, setLogUrl] = useState('');
const [validTabs, setValidTabs] = useState(tabList); const [validTabs, setValidTabs] = useState(tabList);
const [currentCron, setCurrentCron] = useState<any>({}); const [currentCron, setCurrentCron] = useState<any>({});
const listRef = useRef<HTMLDivElement>(null); const listRef = useRef<HTMLDivElement>(null);
const tableScrollHeight = useScrollHeight(listRef); const tableScrollHeight = useScrollHeight(listRef);
const [runningInstances, setRunningInstances] = useState<any[]>([]); const [runningInstances, setRunningInstances] = useState<any[]>([]);
const needRefreshRef = useRef(false); const needRefreshRef = useRef(false);
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logData, setLogData] = useState('');
const [logUrl, setLogUrl] = useState('');
const fetchRunningInstances = async () => { const fetchRunningInstances = async () => {
if (!cron.id) return Promise.resolve(); if (!cron.id) return Promise.resolve();
@@ -114,25 +101,6 @@ const CronDetailModal = ({
}, [cron.id]); }, [cron.id]);
const contentList: any = { 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 && ( script: scriptInfo.filename && (
<Editor <Editor
language={getEditorMode(scriptInfo.filename)} language={getEditorMode(scriptInfo.filename)}
@@ -150,7 +118,7 @@ const CronDetailModal = ({
}} }}
/> />
), ),
runningInstance: ( runningHistory: (
<div ref={listRef}> <div ref={listRef}>
<List> <List>
<VirtualList <VirtualList
@@ -163,6 +131,7 @@ const CronDetailModal = ({
<List.Item <List.Item
className="log-item" className="log-item"
actions={[ actions={[
item.log_path && (
<Tooltip title={intl.get('查看日志')} key="log"> <Tooltip title={intl.get('查看日志')} key="log">
<Button <Button
type="link" type="link"
@@ -173,7 +142,9 @@ const CronDetailModal = ({
viewInstanceLog(item); viewInstanceLog(item);
}} }}
/> />
</Tooltip>, </Tooltip>
),
item.status === 0 && (
<Tooltip title={intl.get('停止')} key="stop"> <Tooltip title={intl.get('停止')} key="stop">
<Button <Button
type="link" type="link"
@@ -185,18 +156,52 @@ const CronDetailModal = ({
stopRunningInstance(item); stopRunningInstance(item);
}} }}
/> />
</Tooltip>, </Tooltip>
]} ),
].filter(Boolean)}
> >
<List.Item.Meta <List.Item.Meta
title={ title={
<span> <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> </span>
} }
description={ description={
<span style={{ color: '#999' }}> <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')} {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> </span>
} }
/> />
@@ -225,7 +230,6 @@ const CronDetailModal = ({
message.success(intl.get('实例已停止')); message.success(intl.get('实例已停止'));
needRefreshRef.current = true; needRefreshRef.current = true;
fetchRunningInstances(); fetchRunningInstances();
setTimeout(() => getLogs(), 1000);
} }
}); });
}, },
@@ -242,20 +246,7 @@ const CronDetailModal = ({
setLogUrl(url); setLogUrl(url);
request.get(url).then(({ code, data }) => { request.get(url).then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setLog(data); setLogData(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);
setIsLogModalVisible(true); setIsLogModalVisible(true);
} }
}); });
@@ -265,22 +256,9 @@ const CronDetailModal = ({
setActiveTabKey(key); 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 getScript = () => {
const result = getCommandScript(cron.command); const result = getCommandScript(cron.command);
if (Array.isArray(result)) { if (Array.isArray(result)) {
setValidTabs(validTabs);
const [s, p] = result; const [s, p] = result;
setScriptInfo({ parent: p, filename: s }); setScriptInfo({ parent: p, filename: s });
request request
@@ -291,8 +269,8 @@ const CronDetailModal = ({
} }
}); });
} else { } else {
setValidTabs([validTabs[0]]); setValidTabs([{ key: 'runningHistory', tab: intl.get('运行历史') }]);
setActiveTabKey('log'); setActiveTabKey('runningHistory');
} }
}; };
@@ -351,9 +329,7 @@ const CronDetailModal = ({
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running }); setCurrentCron({ ...currentCron, status: CrontabStatus.running });
setTimeout(() => { fetchRunningInstances();
getLogs();
}, 1000);
} }
}); });
}, },
@@ -460,7 +436,6 @@ const CronDetailModal = ({
useEffect(() => { useEffect(() => {
if (cron && cron.id) { if (cron && cron.id) {
setCurrentCron(cron); setCurrentCron(cron);
getLogs();
getScript(); getScript();
} }
}, [cron]); }, [cron]);
@@ -687,9 +662,10 @@ const CronDetailModal = ({
<CronLogModal <CronLogModal
handleCancel={() => { handleCancel={() => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
fetchRunningInstances();
}} }}
cron={cron} cron={cron}
data={log} data={logData}
logUrl={logUrl} logUrl={logUrl}
/> />
)} )}