mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 19:30:48 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bbff927b1 | ||
|
|
58eb9feec0 | ||
|
|
802ca93a3d | ||
|
|
d53437d169 | ||
|
|
d526602d19 | ||
|
|
91b44914f6 |
@@ -467,7 +467,10 @@ export default class CronService {
|
|||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
// Kill all running instances of this task
|
// Kill all running instances of this task
|
||||||
try {
|
try {
|
||||||
const command = this.makeCommand(doc);
|
if (doc.pid) {
|
||||||
|
await killTask(doc.pid);
|
||||||
|
}
|
||||||
|
const command = doc.command.replace(/\s+/g, ' ').trim();
|
||||||
await killAllTasks(command);
|
await killAllTasks(command);
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`,
|
`[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
stepPosition,
|
stepPosition,
|
||||||
} from '../data/env';
|
} from '../data/env';
|
||||||
import { writeFileWithLock } from '../shared/utils';
|
import { writeFileWithLock } from '../shared/utils';
|
||||||
|
import { sequelize } from '../data';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class EnvService {
|
export default class EnvService {
|
||||||
@@ -146,7 +147,7 @@ export default class EnvService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await this.find(condition, [
|
const result = await this.find(condition, [
|
||||||
['isPinned', 'DESC'],
|
[sequelize.literal('COALESCE(`isPinned`, 0)'), 'DESC'],
|
||||||
['position', 'DESC'],
|
['position', 'DESC'],
|
||||||
['createdAt', 'ASC'],
|
['createdAt', 'ASC'],
|
||||||
]);
|
]);
|
||||||
|
|||||||
+28
-2
@@ -14,6 +14,7 @@ import {
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { credentials } from '@grpc/grpc-js';
|
import { credentials } from '@grpc/grpc-js';
|
||||||
import { ApiClient } from '../protos/api';
|
import { ApiClient } from '../protos/api';
|
||||||
|
import { CrontabModel } from '../data/cron';
|
||||||
|
|
||||||
class TaskLimit {
|
class TaskLimit {
|
||||||
private dependenyLimit = new PQueue({ concurrency: 1 });
|
private dependenyLimit = new PQueue({ concurrency: 1 });
|
||||||
@@ -131,13 +132,38 @@ class TaskLimit {
|
|||||||
let runs = this.queuedCrons.get(cron.id);
|
let runs = this.queuedCrons.get(cron.id);
|
||||||
const result = runs?.length ? [...runs, fn] : [fn];
|
const result = runs?.length ? [...runs, fn] : [fn];
|
||||||
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
|
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
|
||||||
if (result?.length > 5) {
|
|
||||||
|
// Check instance mode from database to determine queue limit
|
||||||
|
let maxQueueSize = 10; // Default for multi-instance mode (increased from 5)
|
||||||
|
let isSingleInstanceMode = false;
|
||||||
|
try {
|
||||||
|
const cronRecord = await CrontabModel.findOne({
|
||||||
|
where: { id: Number(cron.id) },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default to single instance mode (0) for backward compatibility
|
||||||
|
// allow_multiple_instances is 1 for multi-instance, 0 or null/undefined for single instance
|
||||||
|
isSingleInstanceMode = cronRecord?.allow_multiple_instances !== 1;
|
||||||
|
|
||||||
|
if (isSingleInstanceMode) {
|
||||||
|
// For single instance mode, allow up to 2 queued tasks
|
||||||
|
// This allows the new task to be queued while the old one is being killed
|
||||||
|
maxQueueSize = 2;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][检查实例模式失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.length > maxQueueSize) {
|
||||||
if (repeatTimes < 3) {
|
if (repeatTimes < 3) {
|
||||||
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
|
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
|
||||||
|
const modeStr = isSingleInstanceMode ? '单实例' : '多实例';
|
||||||
this.client.systemNotify(
|
this.client.systemNotify(
|
||||||
{
|
{
|
||||||
title: '任务重复运行',
|
title: '任务重复运行',
|
||||||
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
|
content: `任务:${cron.name}(${modeStr}模式),命令:${cron.command},定时:${cron.schedule},处于运行中的超过 ${maxQueueSize} 个,请检查定时设置`,
|
||||||
},
|
},
|
||||||
(err, res) => {
|
(err, res) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
+27
-3
@@ -15,11 +15,12 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Default to single instance mode (0) for backward compatibility
|
// Default to single instance mode (0) for backward compatibility
|
||||||
const allowSingleInstances =
|
// allow_multiple_instances is 1 for multi-instance, 0 or null/undefined for single instance
|
||||||
existingCron?.allow_multiple_instances === 0;
|
const isSingleInstanceMode =
|
||||||
|
existingCron?.allow_multiple_instances !== 1;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
allowSingleInstances &&
|
isSingleInstanceMode &&
|
||||||
existingCron &&
|
existingCron &&
|
||||||
existingCron.pid &&
|
existingCron.pid &&
|
||||||
(existingCron.status === CrontabStatus.running ||
|
(existingCron.status === CrontabStatus.running ||
|
||||||
@@ -49,6 +50,18 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||||||
);
|
);
|
||||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||||
|
|
||||||
|
// Update status to running after spawning the process
|
||||||
|
try {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.running, pid: cp.pid },
|
||||||
|
{ where: { id: Number(cron.id) } },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][更新任务状态失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
cp.stderr.on('data', (data) => {
|
cp.stderr.on('data', (data) => {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
|
||||||
@@ -66,6 +79,17 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
|
|||||||
|
|
||||||
cp.on('exit', async (code) => {
|
cp.on('exit', async (code) => {
|
||||||
taskLimit.removeQueuedCron(cron.id);
|
taskLimit.removeQueuedCron(cron.id);
|
||||||
|
// Update status to idle after task completes
|
||||||
|
try {
|
||||||
|
await CrontabModel.update(
|
||||||
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
|
{ where: { id: Number(cron.id) } },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][更新任务状态失败] 任务ID: ${cron.id}, 错误: ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
|
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
|||||||
+10
-46
@@ -1,47 +1,11 @@
|
|||||||
version: 2.20.0
|
version: 2.20.1
|
||||||
changeLogLink: https://t.me/jiao_long/432
|
changeLogLink: https://t.me/jiao_long/433
|
||||||
publishTime: 2025-12-10 01:05
|
publishTime: 2025-12-26 22:00
|
||||||
changeLog: |
|
changeLog: |
|
||||||
1. 定时任务(cron / task)相关的大量修复 & 增强
|
1. 修复获取依赖管理列表
|
||||||
|
2. notify.js 修复 TG_PROXY_AUTH 参数拼接
|
||||||
修复 cron 解析错误(修复 parse cron / 升级 cron-parser)
|
3. QLAPI.notify larkSecret 参数
|
||||||
修复集群模式下定时任务可能不执行(race condition)
|
4. 修复 cron parser 定时规则校验
|
||||||
定时任务支持订阅筛选
|
5. 修复设置 baseUrl 后无法访问
|
||||||
定时任务支持排序调整
|
6. 修复环境变量排序
|
||||||
定时任务支持自定义日志文件或无日志
|
7. 修复定时任务无法停止
|
||||||
修复任务实例默认值
|
|
||||||
任务支持单实例 / 多实例模式
|
|
||||||
修复 task 命令软链可能失败问题
|
|
||||||
|
|
||||||
2. 日志系统相关的大更新
|
|
||||||
|
|
||||||
修复日志目录逻辑
|
|
||||||
修复 pm2 日志目录
|
|
||||||
优化日志写入(stream pooling)
|
|
||||||
|
|
||||||
3. 环境变量(env)系统的改进与修复
|
|
||||||
|
|
||||||
修复环境变量复制到剪贴板时可能失败
|
|
||||||
添加环境变量“置顶”功能
|
|
||||||
修复 QlPort 与 QlGrpcPort 环境变量在 host network 模式下被忽略
|
|
||||||
增加全局 SSH 私钥配置
|
|
||||||
|
|
||||||
4. Docker / 非 root 用户 / Alpine 兼容性增强
|
|
||||||
|
|
||||||
新增非 root Docker 用户支持,自动初始化命令
|
|
||||||
修复 Alpine 容器 DNS 解析失败(设置 ndots:0)
|
|
||||||
修复 PM2 在 ARM 路由器(Node.js 不兼容)上的启动失败
|
|
||||||
移除 nginx(可能是考虑更轻量的镜像运行)
|
|
||||||
|
|
||||||
5. API 安全与校验增强
|
|
||||||
|
|
||||||
Dependencies GET endpoint 增加校验
|
|
||||||
Script API routes 增加输入校验
|
|
||||||
修复 JWT 认证问题
|
|
||||||
Feishu 机器人通知增加签名校验
|
|
||||||
QLAPI 增加 cron task 管理功能
|
|
||||||
修复 URIError(错误 cookie 导致白屏)
|
|
||||||
|
|
||||||
6. 系统设置
|
|
||||||
|
|
||||||
新增多终端/多平台的并发登录会话支持
|
|
||||||
Reference in New Issue
Block a user