mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-06 16:54:33 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 855f591992 | |||
| d30eb2008c | |||
| e1ce0f3fa9 | |||
| 82514e65e1 | |||
| 31261190f0 | |||
| d53437d169 | |||
| d526602d19 | |||
| 91b44914f6 |
@@ -206,6 +206,7 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, content, path } = req.body as {
|
||||
filename: string;
|
||||
@@ -223,6 +224,7 @@ export default (app: Router) => {
|
||||
await writeFileWithLock(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
logger.error('🔥 error saving script: %o', e);
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ import { logStreamManager } from '../shared/logStreamManager';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||
|
||||
private isNodeCron(cron: Crontab) {
|
||||
const { schedule, extra_schedules } = cron;
|
||||
@@ -165,7 +165,7 @@ export default class CronService {
|
||||
let cron;
|
||||
try {
|
||||
cron = await this.getDb({ id });
|
||||
} catch (err) {}
|
||||
} catch (err) { }
|
||||
if (!cron) {
|
||||
continue;
|
||||
}
|
||||
@@ -467,7 +467,10 @@ export default class CronService {
|
||||
for (const doc of docs) {
|
||||
// Kill all running instances of this task
|
||||
try {
|
||||
const command = this.makeCommand(doc);
|
||||
if (doc.pid) {
|
||||
await killTask(doc.pid);
|
||||
}
|
||||
const command = doc.command.replace(/\s+/g, ' ').trim();
|
||||
await killAllTasks(command);
|
||||
this.logger.info(
|
||||
`[panel][停止所有运行中的任务实例] 任务ID: ${doc.id}, 命令: ${command}`,
|
||||
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
stepPosition,
|
||||
} from '../data/env';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
import { sequelize } from '../data';
|
||||
|
||||
@Service()
|
||||
export default class EnvService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||
|
||||
public async create(payloads: Env[]): Promise<Env[]> {
|
||||
const envs = await this.envs();
|
||||
@@ -146,7 +147,7 @@ export default class EnvService {
|
||||
}
|
||||
try {
|
||||
const result = await this.find(condition, [
|
||||
['isPinned', 'DESC'],
|
||||
[sequelize.literal('COALESCE(`isPinned`, 0)'), 'DESC'],
|
||||
['position', 'DESC'],
|
||||
['createdAt', 'ASC'],
|
||||
]);
|
||||
|
||||
@@ -16,6 +16,17 @@ export class HttpServerService {
|
||||
metricsService.record('http_service_start', 1, {
|
||||
port: port.toString(),
|
||||
});
|
||||
|
||||
// Set server timeouts to prevent premature connection drops
|
||||
if (this.server) {
|
||||
// Timeout for receiving the entire request (including body) - 5 minutes
|
||||
this.server.requestTimeout = 300000;
|
||||
// Timeout for headers - 2 minutes
|
||||
this.server.headersTimeout = 120000;
|
||||
// Keep-alive timeout - 65 seconds (slightly more than typical load balancer timeout)
|
||||
this.server.keepAliveTimeout = 65000;
|
||||
}
|
||||
|
||||
resolve(this.server);
|
||||
});
|
||||
|
||||
|
||||
+54
-16
@@ -1,8 +1,9 @@
|
||||
import { lock } from 'proper-lockfile';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { writeFile, open, chmod } from 'fs/promises';
|
||||
import { writeFile, open, chmod, FileHandle } from 'fs/promises';
|
||||
import { fileExist } from '../config/util';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
function getUniqueLockPath(filePath: string) {
|
||||
const sanitizedPath = filePath
|
||||
@@ -19,24 +20,61 @@ export async function writeFileWithLock(
|
||||
if (typeof options === 'string') {
|
||||
options = { encoding: options };
|
||||
}
|
||||
|
||||
// Ensure file exists before locking
|
||||
if (!(await fileExist(filePath))) {
|
||||
const fileHandle = await open(filePath, 'w');
|
||||
fileHandle.close();
|
||||
let fileHandle: FileHandle | undefined;
|
||||
try {
|
||||
fileHandle = await open(filePath, 'w');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to create file ${filePath}: ${errorMessage}`);
|
||||
} finally {
|
||||
if (fileHandle !== undefined) {
|
||||
try {
|
||||
await fileHandle.close();
|
||||
} catch (closeError) {
|
||||
// Log close error but don't throw to avoid masking the original error
|
||||
Logger.error(`Failed to close file handle for ${filePath}:`, closeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const lockfilePath = getUniqueLockPath(filePath);
|
||||
let release: (() => Promise<void>) | undefined;
|
||||
|
||||
const release = await lock(filePath, {
|
||||
retries: {
|
||||
retries: 10,
|
||||
factor: 2,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 3000,
|
||||
},
|
||||
lockfilePath,
|
||||
});
|
||||
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||
if (options?.mode) {
|
||||
await chmod(filePath, options.mode);
|
||||
try {
|
||||
release = await lock(filePath, {
|
||||
retries: {
|
||||
retries: 10,
|
||||
factor: 2,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 3000,
|
||||
},
|
||||
lockfilePath,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to acquire lock for ${filePath}: ${errorMessage}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||
if (options?.mode) {
|
||||
await chmod(filePath, options.mode);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to write to file ${filePath}: ${errorMessage}`);
|
||||
} finally {
|
||||
if (release) {
|
||||
try {
|
||||
await release();
|
||||
} catch (error) {
|
||||
// Log but don't throw on release failure
|
||||
Logger.error(`Failed to release lock for ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
await release();
|
||||
}
|
||||
|
||||
+10
-46
@@ -1,47 +1,11 @@
|
||||
version: 2.20.0
|
||||
changeLogLink: https://t.me/jiao_long/432
|
||||
publishTime: 2025-12-10 01:05
|
||||
version: 2.20.1
|
||||
changeLogLink: https://t.me/jiao_long/433
|
||||
publishTime: 2025-12-26 22:00
|
||||
changeLog: |
|
||||
1. 定时任务(cron / task)相关的大量修复 & 增强
|
||||
|
||||
修复 cron 解析错误(修复 parse cron / 升级 cron-parser)
|
||||
修复集群模式下定时任务可能不执行(race condition)
|
||||
定时任务支持订阅筛选
|
||||
定时任务支持排序调整
|
||||
定时任务支持自定义日志文件或无日志
|
||||
修复任务实例默认值
|
||||
任务支持单实例 / 多实例模式
|
||||
修复 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. 系统设置
|
||||
|
||||
新增多终端/多平台的并发登录会话支持
|
||||
1. 修复获取依赖管理列表
|
||||
2. notify.js 修复 TG_PROXY_AUTH 参数拼接
|
||||
3. QLAPI.notify larkSecret 参数
|
||||
4. 修复 cron parser 定时规则校验
|
||||
5. 修复设置 baseUrl 后无法访问
|
||||
6. 修复环境变量排序
|
||||
7. 修复定时任务无法停止
|
||||
Reference in New Issue
Block a user