mirror of
https://github.com/whyour/qinglong.git
synced 2026-06-13 14:37:28 +08:00
增加运行实例
This commit is contained in:
parent
617cf7e5b4
commit
946731ac8d
|
|
@ -5,6 +5,10 @@ import CronService from '../services/cron';
|
||||||
import CronViewService from '../services/cronView';
|
import CronViewService from '../services/cronView';
|
||||||
import { celebrate, Joi } from 'celebrate';
|
import { celebrate, Joi } from 'celebrate';
|
||||||
import { commonCronSchema } from '../validation/schedule';
|
import { commonCronSchema } from '../validation/schedule';
|
||||||
|
import {
|
||||||
|
RunningInstanceModel,
|
||||||
|
InstanceStatus,
|
||||||
|
} from '../data/runningInstance';
|
||||||
|
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
|
|
@ -446,6 +450,49 @@ export default (app: Router) => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/:id/instances',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const instances = await RunningInstanceModel.findAll({
|
||||||
|
where: {
|
||||||
|
cron_id: req.params.id,
|
||||||
|
status: InstanceStatus.running,
|
||||||
|
},
|
||||||
|
order: [['started_at', 'DESC']],
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
return res.send({ code: 200, data: instances });
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/:id/instances/:instanceId/stop',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.number().required(),
|
||||||
|
instanceId: Joi.number().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request<{ id: number; instanceId: number }>, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const cronService = Container.get(CronService);
|
||||||
|
const data = await cronService.stopInstance(req.params.instanceId);
|
||||||
|
return res.send(data);
|
||||||
|
} catch (e) {
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:id/logs',
|
'/:id/logs',
|
||||||
celebrate({
|
celebrate({
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { Container } from 'typedi';
|
||||||
import { fn, col, where, Op } from 'sequelize';
|
import { fn, col, where, Op } from 'sequelize';
|
||||||
import { CrontabModel } from '../data/cron';
|
import { CrontabModel } from '../data/cron';
|
||||||
import { CrontabStatModel } from '../data/cronStats';
|
import { CrontabStatModel } from '../data/cronStats';
|
||||||
|
import {
|
||||||
|
RunningInstanceModel,
|
||||||
|
InstanceStatus,
|
||||||
|
} from '../data/runningInstance';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
|
|
@ -239,9 +243,9 @@ export default (app: Router) => {
|
||||||
'/runtime',
|
'/runtime',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const runningCrons = await CrontabModel.findAll({
|
const runningInstances = await RunningInstanceModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
status: 0, // running
|
status: InstanceStatus.running,
|
||||||
},
|
},
|
||||||
raw: true,
|
raw: true,
|
||||||
});
|
});
|
||||||
|
|
@ -253,15 +257,31 @@ export default (app: Router) => {
|
||||||
raw: true,
|
raw: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const running = runningCrons.map((c: any) => ({
|
// Fetch cron names for running instances
|
||||||
id: c.id,
|
const cronIds = [
|
||||||
name: c.name || c.command || `任务#${c.id}`,
|
...new Set(runningInstances.map((i: any) => i.cron_id)),
|
||||||
pid: c.pid,
|
];
|
||||||
elapsed: c.last_execution_time
|
const crons =
|
||||||
? Math.floor((Date.now() / 1000) - c.last_execution_time)
|
cronIds.length > 0
|
||||||
: 0,
|
? await CrontabModel.findAll({
|
||||||
logPath: c.log_path,
|
where: { id: cronIds },
|
||||||
}));
|
raw: true,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
const cronMap = new Map(crons.map((c: any) => [c.id, c]));
|
||||||
|
|
||||||
|
const now = dayjs().unix();
|
||||||
|
const running = runningInstances.map((inst: any) => {
|
||||||
|
const cron = cronMap.get(inst.cron_id);
|
||||||
|
return {
|
||||||
|
instanceId: inst.id,
|
||||||
|
id: inst.cron_id,
|
||||||
|
name: cron?.name || cron?.command || `任务#${inst.cron_id}`,
|
||||||
|
pid: inst.pid,
|
||||||
|
elapsed: inst.started_at ? now - inst.started_at : 0,
|
||||||
|
logPath: inst.log_path,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const dayAgo = dayjs().subtract(24, 'hour').unix();
|
const dayAgo = dayjs().subtract(24, 'hour').unix();
|
||||||
const idleTasks = await CrontabModel.findAll({
|
const idleTasks = await CrontabModel.findAll({
|
||||||
|
|
|
||||||
81
back/data/runningInstance.ts
Normal file
81
back/data/runningInstance.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { sequelize } from '.';
|
||||||
|
import { DataTypes, Model } from 'sequelize';
|
||||||
|
|
||||||
|
export enum InstanceStatus {
|
||||||
|
'running' = 0,
|
||||||
|
'finished' = 1,
|
||||||
|
'stopped' = 2,
|
||||||
|
'error' = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunningInstanceAttributes {
|
||||||
|
id?: number;
|
||||||
|
cron_id: number;
|
||||||
|
pid?: number;
|
||||||
|
log_path?: string;
|
||||||
|
started_at: number;
|
||||||
|
finished_at?: number;
|
||||||
|
status: InstanceStatus;
|
||||||
|
exit_code?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RunningInstance {
|
||||||
|
id?: number;
|
||||||
|
cron_id!: number;
|
||||||
|
pid?: number;
|
||||||
|
log_path?: string;
|
||||||
|
started_at!: number;
|
||||||
|
finished_at?: number;
|
||||||
|
status!: InstanceStatus;
|
||||||
|
exit_code?: number;
|
||||||
|
|
||||||
|
constructor(options: RunningInstanceAttributes) {
|
||||||
|
this.id = options.id;
|
||||||
|
this.cron_id = options.cron_id;
|
||||||
|
this.pid = options.pid;
|
||||||
|
this.log_path = options.log_path;
|
||||||
|
this.started_at = options.started_at;
|
||||||
|
this.finished_at = options.finished_at;
|
||||||
|
this.status = options.status;
|
||||||
|
this.exit_code = options.exit_code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunningInstanceModel
|
||||||
|
extends Model<RunningInstanceAttributes, RunningInstanceAttributes>,
|
||||||
|
RunningInstanceAttributes {}
|
||||||
|
|
||||||
|
export const RunningInstanceModel = sequelize.define<RunningInstanceModel>(
|
||||||
|
'RunningInstance',
|
||||||
|
{
|
||||||
|
cron_id: {
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
pid: {
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
log_path: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
started_at: {
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
finished_at: {
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: InstanceStatus.running,
|
||||||
|
},
|
||||||
|
exit_code: {
|
||||||
|
type: DataTypes.NUMBER,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -7,6 +7,7 @@ import { SystemModel } from '../data/system';
|
||||||
import { SubscriptionModel } from '../data/subscription';
|
import { SubscriptionModel } from '../data/subscription';
|
||||||
import { CrontabViewModel } from '../data/cronView';
|
import { CrontabViewModel } from '../data/cronView';
|
||||||
import { CrontabStatModel } from '../data/cronStats';
|
import { CrontabStatModel } from '../data/cronStats';
|
||||||
|
import { RunningInstanceModel } from '../data/runningInstance';
|
||||||
import { sequelize } from '../data';
|
import { sequelize } from '../data';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
|
|
@ -19,6 +20,7 @@ export default async () => {
|
||||||
await SubscriptionModel.sync();
|
await SubscriptionModel.sync();
|
||||||
await CrontabViewModel.sync();
|
await CrontabViewModel.sync();
|
||||||
await CrontabStatModel.sync();
|
await CrontabStatModel.sync();
|
||||||
|
await RunningInstanceModel.sync();
|
||||||
|
|
||||||
// 初始化新增字段
|
// 初始化新增字段
|
||||||
const migrations = [
|
const migrations = [
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import OpenService from '../services/open';
|
||||||
import { shareStore } from '../shared/store';
|
import { shareStore } from '../shared/store';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import { AppModel } from '../data/open';
|
import { AppModel } from '../data/open';
|
||||||
|
import { InstanceStatus, RunningInstanceModel } from '../data/runningInstance';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
|
|
@ -139,6 +140,12 @@ export default async () => {
|
||||||
// 初始化更新所有任务状态为空闲
|
// 初始化更新所有任务状态为空闲
|
||||||
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
|
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
|
||||||
|
|
||||||
|
// 清空所有运行中的实例记录(服务重启后进程已不存在)
|
||||||
|
await RunningInstanceModel.update(
|
||||||
|
{ status: InstanceStatus.stopped },
|
||||||
|
{ where: { status: InstanceStatus.running } },
|
||||||
|
);
|
||||||
|
|
||||||
// 初始化时执行一次所有的 ql repo 任务
|
// 初始化时执行一次所有的 ql repo 任务
|
||||||
CrontabModel.findAll({
|
CrontabModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import { Service, Inject } from 'typedi';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
|
import {
|
||||||
|
RunningInstanceModel,
|
||||||
|
InstanceStatus,
|
||||||
|
} from '../data/runningInstance';
|
||||||
import { exec, execSync } from 'child_process';
|
import { exec, execSync } from 'child_process';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import CronExpressionParser from 'cron-parser';
|
import CronExpressionParser from 'cron-parser';
|
||||||
|
|
@ -189,6 +193,35 @@ export default class CronService {
|
||||||
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
|
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
|
||||||
options = omit(options, ['status', 'log_path', 'pid']);
|
options = omit(options, ['status', 'log_path', 'pid']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manage RunningInstance records for status transitions from shell scripts
|
||||||
|
if (status === CrontabStatus.running) {
|
||||||
|
// Create a new running instance record
|
||||||
|
await RunningInstanceModel.create({
|
||||||
|
cron_id: id,
|
||||||
|
pid: pid || undefined,
|
||||||
|
log_path: log_path || undefined,
|
||||||
|
started_at: last_execution_time || dayjs().unix(),
|
||||||
|
status: InstanceStatus.running,
|
||||||
|
});
|
||||||
|
} else if (status === CrontabStatus.idle) {
|
||||||
|
// Mark the matching running instance as finished
|
||||||
|
const finishedAt = dayjs().unix();
|
||||||
|
await RunningInstanceModel.update(
|
||||||
|
{
|
||||||
|
finished_at: finishedAt,
|
||||||
|
status: InstanceStatus.finished,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
cron_id: id,
|
||||||
|
pid: pid || undefined,
|
||||||
|
status: InstanceStatus.running,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{ ...pickBy(options, (v) => v === 0 || !!v) },
|
{ ...pickBy(options, (v) => v === 0 || !!v) },
|
||||||
{ where: { id } },
|
{ where: { id } },
|
||||||
|
|
@ -499,12 +532,53 @@ export default class CronService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark all running instances as stopped
|
||||||
|
const finishedAt = dayjs().unix();
|
||||||
|
await RunningInstanceModel.update(
|
||||||
|
{ status: InstanceStatus.stopped, finished_at: finishedAt },
|
||||||
|
{ where: { cron_id: ids, status: InstanceStatus.running } },
|
||||||
|
);
|
||||||
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.idle, pid: undefined },
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
{ where: { id: ids } },
|
{ where: { id: ids } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async stopInstance(instanceId: number) {
|
||||||
|
const instance = await RunningInstanceModel.findOne({
|
||||||
|
where: { id: instanceId, status: InstanceStatus.running },
|
||||||
|
});
|
||||||
|
if (!instance) {
|
||||||
|
return { code: 400, message: '实例不存在或已停止' };
|
||||||
|
}
|
||||||
|
if (instance.pid) {
|
||||||
|
try {
|
||||||
|
await killTask(instance.pid);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`[panel][停止实例失败] 实例ID: ${instanceId}, PID: ${instance.pid}, 错误: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await RunningInstanceModel.update(
|
||||||
|
{ status: InstanceStatus.stopped, finished_at: dayjs().unix() },
|
||||||
|
{ where: { id: instanceId } },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if there are still other running instances for this cron
|
||||||
|
const otherRunning = await RunningInstanceModel.count({
|
||||||
|
where: { cron_id: instance.cron_id, status: InstanceStatus.running },
|
||||||
|
});
|
||||||
|
if (otherRunning === 0) {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
|
{ where: { id: instance.cron_id } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { code: 200, message: '实例已停止' };
|
||||||
|
}
|
||||||
|
|
||||||
private async runSingle(cronId: number): Promise<number | void> {
|
private async runSingle(cronId: number): Promise<number | void> {
|
||||||
return taskLimit.manualRunWithCronLimit(() => {
|
return taskLimit.manualRunWithCronLimit(() => {
|
||||||
return new Promise(async (resolve: any) => {
|
return new Promise(async (resolve: any) => {
|
||||||
|
|
@ -543,6 +617,15 @@ 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 } },
|
||||||
|
|
@ -574,10 +657,26 @@ 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(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.idle, pid: undefined },
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
{ where: { id } },
|
{ where: { id } },
|
||||||
);
|
);
|
||||||
|
}
|
||||||
resolve({ ...params, pid: cp.pid, code });
|
resolve({ ...params, pid: cp.pid, code });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ import Logger from '../loaders/logger';
|
||||||
import { ICron } from '../protos/cron';
|
import { ICron } from '../protos/cron';
|
||||||
import { CrontabModel, CrontabStatus } from '../data/cron';
|
import { CrontabModel, CrontabStatus } from '../data/cron';
|
||||||
import { killTask } from '../config/util';
|
import { killTask } from '../config/util';
|
||||||
|
import {
|
||||||
|
RunningInstanceModel,
|
||||||
|
InstanceStatus,
|
||||||
|
} from '../data/runningInstance';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
||||||
return taskLimit.runWithCronLimit(cron, () => {
|
return taskLimit.runWithCronLimit(cron, () => {
|
||||||
|
|
@ -29,6 +34,12 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
||||||
`[schedule][停止已运行任务] 任务ID: ${cron.id}, PID: ${existingCron.pid}`,
|
`[schedule][停止已运行任务] 任务ID: ${cron.id}, PID: ${existingCron.pid}`,
|
||||||
);
|
);
|
||||||
await killTask(existingCron.pid);
|
await killTask(existingCron.pid);
|
||||||
|
// Mark old running instances as stopped
|
||||||
|
const stoppedAt = dayjs().unix();
|
||||||
|
await RunningInstanceModel.update(
|
||||||
|
{ status: InstanceStatus.stopped, finished_at: stoppedAt },
|
||||||
|
{ where: { cron_id: Number(cron.id), status: InstanceStatus.running } },
|
||||||
|
);
|
||||||
// Update the status to idle after killing
|
// Update the status to idle after killing
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.idle, pid: undefined },
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -22,6 +22,7 @@ import {
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
|
StopOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { CrontabStatus } from './type';
|
import { CrontabStatus } from './type';
|
||||||
import { diffTime } from '@/utils/date';
|
import { diffTime } from '@/utils/date';
|
||||||
|
|
@ -46,6 +47,10 @@ const tabList = [
|
||||||
key: 'script',
|
key: 'script',
|
||||||
tab: intl.get('脚本'),
|
tab: intl.get('脚本'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'runningInstance',
|
||||||
|
tab: intl.get('运行实例'),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface LogItem {
|
interface LogItem {
|
||||||
|
|
@ -77,6 +82,36 @@ const CronDetailModal = ({
|
||||||
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 needRefreshRef = useRef(false);
|
||||||
|
|
||||||
|
const fetchRunningInstances = async () => {
|
||||||
|
if (!cron.id) return Promise.resolve();
|
||||||
|
return request
|
||||||
|
.get(`${config.apiPrefix}crons/${cron.id}/instances`)
|
||||||
|
.then(({ code, data }) => {
|
||||||
|
if (code === 200 && data) {
|
||||||
|
setRunningInstances(data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRunningInstances();
|
||||||
|
let timer: ReturnType<typeof setTimeout>;
|
||||||
|
let cancelled = false;
|
||||||
|
const poll = async () => {
|
||||||
|
await fetchRunningInstances();
|
||||||
|
if (cancelled) return;
|
||||||
|
timer = setTimeout(poll, 10000);
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [cron.id]);
|
||||||
|
|
||||||
const contentList: any = {
|
const contentList: any = {
|
||||||
log: (
|
log: (
|
||||||
|
|
@ -115,11 +150,106 @@ const CronDetailModal = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
runningInstance: (
|
||||||
|
<div ref={listRef}>
|
||||||
|
<List>
|
||||||
|
<VirtualList
|
||||||
|
data={runningInstances}
|
||||||
|
height={tableScrollHeight}
|
||||||
|
itemHeight={47}
|
||||||
|
itemKey="id"
|
||||||
|
>
|
||||||
|
{(item) => (
|
||||||
|
<List.Item
|
||||||
|
className="log-item"
|
||||||
|
actions={[
|
||||||
|
<Tooltip title={intl.get('查看日志')} key="log">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon={<FileOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
viewInstanceLog(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>,
|
||||||
|
<Tooltip title={intl.get('停止')} key="stop">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon={<StopOutlined />}
|
||||||
|
danger
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
stopRunningInstance(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={
|
||||||
|
<span>
|
||||||
|
PID: <Tag color="processing">{item.pid}</Tag>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<span style={{ color: '#999' }}>
|
||||||
|
{intl.get('启动')}: {dayjs.unix(item.started_at).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
</VirtualList>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopRunningInstance = (instance: any) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: intl.get('确认停止实例'),
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
{intl.get('确认停止运行实例')} PID: {instance.pid}{' '}
|
||||||
|
{intl.get('吗')}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.post(`${config.apiPrefix}crons/${cron.id}/instances/${instance.id}/stop`)
|
||||||
|
.then(({ code }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
message.success(intl.get('实例已停止'));
|
||||||
|
needRefreshRef.current = true;
|
||||||
|
fetchRunningInstances();
|
||||||
|
setTimeout(() => getLogs(), 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewInstanceLog = (instance: any) => {
|
||||||
|
if (!instance.log_path) return;
|
||||||
|
const parts = instance.log_path.split('/');
|
||||||
|
const filename = parts.pop() || '';
|
||||||
|
const directory = parts.join('/');
|
||||||
|
const url = `${config.apiPrefix}logs/detail?file=${filename}&path=${directory}`;
|
||||||
|
localStorage.setItem('logCron', url);
|
||||||
|
setLogUrl(url);
|
||||||
|
request.get(url).then(({ code, data }) => {
|
||||||
|
if (code === 200) {
|
||||||
|
setLog(data);
|
||||||
|
setIsLogModalVisible(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClickItem = (item: LogItem) => {
|
const onClickItem = (item: LogItem) => {
|
||||||
const url = `${config.apiPrefix}logs/detail?file=${item.filename}&path=${
|
const url = `${config.apiPrefix}logs/detail?file=${item.filename}&path=${item.directory || ''
|
||||||
item.directory || ''
|
|
||||||
}`;
|
}`;
|
||||||
localStorage.setItem('logCron', url);
|
localStorage.setItem('logCron', url);
|
||||||
setLogUrl(url);
|
setLogUrl(url);
|
||||||
|
|
@ -256,8 +386,7 @@ const CronDetailModal = ({
|
||||||
|
|
||||||
const enabledOrDisabledCron = () => {
|
const enabledOrDisabledCron = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `确认${
|
title: `确认${currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
|
||||||
currentCron.isDisabled === 1 ? intl.get('启用') : intl.get('禁用')
|
|
||||||
}`,
|
}`,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
|
|
@ -273,8 +402,7 @@ const CronDetailModal = ({
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(
|
.put(
|
||||||
`${config.apiPrefix}crons/${
|
`${config.apiPrefix}crons/${currentCron.isDisabled === 1 ? 'enable' : 'disable'
|
||||||
currentCron.isDisabled === 1 ? 'enable' : 'disable'
|
|
||||||
}`,
|
}`,
|
||||||
[currentCron.id],
|
[currentCron.id],
|
||||||
)
|
)
|
||||||
|
|
@ -292,8 +420,7 @@ const CronDetailModal = ({
|
||||||
|
|
||||||
const pinOrUnPinCron = () => {
|
const pinOrUnPinCron = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: `确认${
|
title: `确认${currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
|
||||||
currentCron.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶')
|
|
||||||
}`,
|
}`,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
|
|
@ -309,8 +436,7 @@ const CronDetailModal = ({
|
||||||
onOk() {
|
onOk() {
|
||||||
request
|
request
|
||||||
.put(
|
.put(
|
||||||
`${config.apiPrefix}crons/${
|
`${config.apiPrefix}crons/${currentCron.isPinned === 1 ? 'unpin' : 'pin'
|
||||||
currentCron.isPinned === 1 ? 'unpin' : 'pin'
|
|
||||||
}`,
|
}`,
|
||||||
[currentCron.id],
|
[currentCron.id],
|
||||||
)
|
)
|
||||||
|
|
@ -441,7 +567,7 @@ const CronDetailModal = ({
|
||||||
open={true}
|
open={true}
|
||||||
forceRender
|
forceRender
|
||||||
footer={false}
|
footer={false}
|
||||||
onCancel={() => handleCancel()}
|
onCancel={() => handleCancel(needRefreshRef.current)}
|
||||||
wrapClassName="crontab-detail"
|
wrapClassName="crontab-detail"
|
||||||
width={!isPhone ? '80vw' : ''}
|
width={!isPhone ? '80vw' : ''}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -365,8 +365,8 @@ const Crontab = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCrons = () => {
|
const getCrons = async (silent?: boolean) => {
|
||||||
setLoading(true);
|
if (!silent) setLoading(true);
|
||||||
const { page, size, sorter, filters } = pageConf;
|
const { page, size, sorter, filters } = pageConf;
|
||||||
let url = `${config.apiPrefix
|
let url = `${config.apiPrefix
|
||||||
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
|
||||||
|
|
@ -385,7 +385,7 @@ const Crontab = () => {
|
||||||
filterRelation: viewConf.filterRelation || 'and',
|
filterRelation: viewConf.filterRelation || 'and',
|
||||||
})}`;
|
})}`;
|
||||||
}
|
}
|
||||||
request
|
return request
|
||||||
.get(url)
|
.get(url)
|
||||||
.then(async ({ code, data: _data }) => {
|
.then(async ({ code, data: _data }) => {
|
||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
|
|
@ -419,9 +419,24 @@ const Crontab = () => {
|
||||||
setTotal(total);
|
setTotal(total);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => !silent && setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: ReturnType<typeof setTimeout>;
|
||||||
|
let cancelled = false;
|
||||||
|
const poll = async () => {
|
||||||
|
await getCrons(true);
|
||||||
|
if (cancelled) return;
|
||||||
|
timer = setTimeout(poll, 10000);
|
||||||
|
};
|
||||||
|
poll();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [pageConf, viewConf, searchText]);
|
||||||
|
|
||||||
const addCron = () => {
|
const addCron = () => {
|
||||||
setEditedCron(null as any);
|
setEditedCron(null as any);
|
||||||
setIsModalVisible(true);
|
setIsModalVisible(true);
|
||||||
|
|
@ -1077,8 +1092,11 @@ const Crontab = () => {
|
||||||
)}
|
)}
|
||||||
{isDetailModalVisible && (
|
{isDetailModalVisible && (
|
||||||
<CronDetailModal
|
<CronDetailModal
|
||||||
handleCancel={() => {
|
handleCancel={(needUpdate?: boolean) => {
|
||||||
setIsDetailModalVisible(false);
|
setIsDetailModalVisible(false);
|
||||||
|
if (needUpdate) {
|
||||||
|
getCrons();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
cron={detailCron}
|
cron={detailCron}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ interface TopItem {
|
||||||
interface Runtime {
|
interface Runtime {
|
||||||
runningCount: number;
|
runningCount: number;
|
||||||
queuedCount: number;
|
queuedCount: number;
|
||||||
running: Array<{ id: number; name: string; pid: number; elapsed: number; logPath: string }>;
|
running: Array<{ instanceId: number; id: number; name: string; pid: number; elapsed: number; logPath: string }>;
|
||||||
idleTasks: Array<{ id: number; name: string; lastRun: string }>;
|
idleTasks: Array<{ id: number; name: string; lastRun: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,12 +282,15 @@ const Dashboard = () => {
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
dataSource={runtime?.running || []}
|
dataSource={runtime?.running || []}
|
||||||
rowKey="id"
|
rowKey="instanceId"
|
||||||
pagination={runtime && runtime.running.length > 5 ? runtimePagination : false}
|
pagination={runtime && runtime.running.length > 5 ? runtimePagination : false}
|
||||||
size="small"
|
size="small"
|
||||||
locale={{ emptyText: <Empty description={intl.get('暂无运行中任务')} /> }}
|
locale={{ emptyText: <Empty description={intl.get('暂无运行中任务')} /> }}
|
||||||
columns={[
|
columns={[
|
||||||
{ title: intl.get('定时任务'), dataIndex: 'name', ellipsis: true },
|
{ title: intl.get('定时任务'), dataIndex: 'name', ellipsis: true, render: (name: string, record) => {
|
||||||
|
const sameTaskCount = (runtime?.running || []).filter(r => r.id === record.id).length;
|
||||||
|
return sameTaskCount > 1 ? <span>{name} <Tag color="processing" style={{ fontSize: 10, lineHeight: '16px' }}>×{sameTaskCount}</Tag></span> : name;
|
||||||
|
} },
|
||||||
{ title: 'PID', dataIndex: 'pid', width: 80 },
|
{ title: 'PID', dataIndex: 'pid', width: 80 },
|
||||||
{ title: intl.get('已运行'), dataIndex: 'elapsed', width: 100, render: (v: number) => v ? formatSeconds(v) : '-' },
|
{ title: intl.get('已运行'), dataIndex: 'elapsed', width: 100, render: (v: number) => v ? formatSeconds(v) : '-' },
|
||||||
{ title: intl.get('日志'), dataIndex: 'id', width: 60, render: (id, record) => <a onClick={() => { localStorage.setItem('logCron', String(id)); setLogCron({ id, name: record.name }); }}>{intl.get('查看')}</a> },
|
{ title: intl.get('日志'), dataIndex: 'id', width: 60, render: (id, record) => <a onClick={() => { localStorage.setItem('logCron', String(id)); setLogCron({ id, name: record.name }); }}>{intl.get('查看')}</a> },
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user