全新青龙2.0 (#65)

* 重构shell (#17)

* 更新正则

* 更新update命令

* 移除测试代码

* 重构删除日志命令

* 更新entrypoint

* 更新dockerfile

* 完善shell调用

* 修复share shell引用

* 修复entrypoint

* 修复share shell

* 修复share.sh

* 修改依赖重装逻辑

* 更新docker entrypoint

* curl 使用静默模式

* 更新ql raw

* 修复添加单个任务

* 修复shell语法

* 添加定时任务进程

* 更新默认定时任务

* 更新定时任务重启schedule

* 更新青龙重启逻辑

* 修复定时任务列表创建

* 修复schedule进程

* 修复entrypoint

* 更新task命令

* pm2 restart替换成reload

* 修复task命令参数引入

* 完善ql repo命令

* 修复update.sh

* 更新ql repo命令

* ql repo添加登录验证,修复package.json示例

* 修复定时任务命令补全

* 修改默认cron端口

* 修复cron日志弹框异常

* 修改cron新建label

* 修复ql repo命令

* 修复cron格式验证

* 修改日志目录格式

* 修改青龙remote url

* 修复添加定时cron匹配

* 添加定时任务超时时间设置

* 暂时移除timeout命令

* 恢复定时任务timeout

* 修复cookie.sh引用

* 修复shell变量自加

* 修复ck更新状态同步

* 增加tg bot测试,修改增删任务通知

* 修复shell函数返回值

* 修改添加任务日志打印

* 修改entrypoint日志

* 修复api日志打印

* 修改api日志打印

* 定时任务支持批量启用禁用删除运行

* 修改cron管理操作按钮响应样式

* 更新bot启动脚本

* 更新bot启动脚本

* 增加timeout默认值,修改session管理逻辑

* 更新config示例和通知日志

* 更新bot.sh

* 更新启动bot命令

* 更新启动bot命令

* 修复task运行参数合并

* 增加停止定时任务功能

* 修复停止定时任务api

* 更新停止定时任务日志

* 更新停止任务日志

* 修复删除cron api

* 更新删除cron通知文本

* 更新命令提示

* 更新bot启动脚本
This commit is contained in:
whyour
2021-05-10 20:47:23 +08:00
committed by GitHub
parent 489846d2e6
commit 5b3687f7b6
44 changed files with 2868 additions and 1923 deletions
-4
View File
@@ -26,10 +26,6 @@ export default (app: Router) => {
case 'crontab':
content = getFileContentByName(config.crontabFile);
break;
case 'shareCode':
let shareCodeFile = getLastModifyFilePath(config.shareCodeDir);
content = getFileContentByName(shareCodeFile);
break;
case 'extra':
content = getFileContentByName(config.extraFile);
break;
+48 -31
View File
@@ -1,10 +1,9 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import CronService from '../services/cron';
import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser';
const route = Router();
export default (app: Router) => {
@@ -32,14 +31,36 @@ export default (app: Router) => {
body: Joi.object({
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string(),
name: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
const cronService = Container.get(CronService);
const data = await cronService.create(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/crons/run',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.create(req.body);
const data = await cronService.run(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
@@ -48,18 +69,16 @@ export default (app: Router) => {
},
);
route.get(
'/crons/:id/run',
route.put(
'/crons/stop',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.run(req.params.id);
const data = await cronService.stop(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
@@ -68,18 +87,16 @@ export default (app: Router) => {
},
);
route.get(
'/crons/:id/disable',
route.put(
'/crons/disable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.disabled(req.params.id);
const data = await cronService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
@@ -88,18 +105,16 @@ export default (app: Router) => {
},
);
route.get(
'/crons/:id/enable',
route.put(
'/crons/enable',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.enabled(req.params.id);
const data = await cronService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
@@ -134,16 +149,20 @@ export default (app: Router) => {
body: Joi.object({
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string(),
name: Joi.string().optional(),
_id: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.update(req.body);
return res.send({ code: 200, data });
if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
const cronService = Container.get(CronService);
const data = await cronService.update(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
@@ -152,17 +171,15 @@ export default (app: Router) => {
);
route.delete(
'/crons/:id',
'/crons',
celebrate({
params: Joi.object({
id: Joi.string().required(),
}),
body: Joi.array().items(Joi.string().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.remove(req.params.id);
const data = await cronService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
+2 -3
View File
@@ -7,11 +7,10 @@ const envFound = dotenv.config();
const rootPath = path.resolve(__dirname, '../../');
const cookieFile = path.join(rootPath, 'config/cookie.sh');
const confFile = path.join(rootPath, 'config/config.sh');
const sampleFile = path.join(rootPath, 'sample/config.sh.sample');
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
const crontabFile = path.join(rootPath, 'config/crontab.list');
const confBakDir = path.join(rootPath, 'config/bak/');
const authConfigFile = path.join(rootPath, 'config/auth.json');
const shareCodeDir = path.join(rootPath, 'log/export_sharecodes/');
const extraFile = path.join(rootPath, 'config/extra.sh');
const logPath = path.join(rootPath, 'log/');
const authError = '错误的用户名密码,请重试';
@@ -28,6 +27,7 @@ if (envFound.error) {
export default {
port: parseInt(process.env.PORT as string, 10),
cronPort: parseInt(process.env.CRON_PORT as string, 10),
secret: process.env.SECRET,
logs: {
level: process.env.LOG_LEVEL || 'silly',
@@ -40,7 +40,6 @@ export default {
authError,
logPath,
extraFile,
shareCodeDir,
authConfigFile,
confBakDir,
crontabFile,
+2
View File
@@ -8,6 +8,7 @@ export class Crontab {
_id?: string;
status?: CrontabStatus;
isSystem?: 1 | 0;
pid?: number;
constructor(options: Crontab) {
this.name = options.name;
@@ -19,6 +20,7 @@ export class Crontab {
this.status = options.status || CrontabStatus.idle;
this.timestamp = new Date().toString();
this.isSystem = options.isSystem || 0;
this.pid = options.pid;
}
}
+5 -1
View File
@@ -49,7 +49,10 @@ export default ({ app }: { app: Application }) => {
next: NextFunction,
) => {
if (err.name === 'UnauthorizedError') {
return res.status(err.status).send({ message: err.message }).end();
return res
.status(err.status)
.send({ code: 401, message: err.message })
.end();
}
return next(err);
},
@@ -64,6 +67,7 @@ export default ({ app }: { app: Application }) => {
) => {
res.status(err.status || 500);
res.json({
code: err.status || 500,
message: err.message,
});
},
+9 -36
View File
@@ -5,10 +5,7 @@ import CronService from '../services/cron';
const initData = [
{
name: '更新面板',
command: `sleep ${randomSchedule(
60,
1,
)} && git_pull >> $QL_DIR/log/git_pull.log 2>&1`,
command: `sleep ${randomSchedule(60, 1)} && ql update`,
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
24,
7,
@@ -16,46 +13,22 @@ const initData = [
status: CrontabStatus.idle,
},
{
name: 'build面板',
command: 'rebuild >> ${QL_DIR}/log/rebuild.log 2>&1',
name: '重启并编译面板',
command: 'ql restart',
schedule: '30 7 */7 * *',
status: CrontabStatus.disabled,
},
{
name: '自定义仓库',
command: `sleep ${randomSchedule(
60,
1,
)} && diy https://ghproxy.com/https://github.com/whyour/hundun.git "quanx/jx|quanx/jd" tokens >> $QL_DIR/log/diy_pull.log 2>&1`,
schedule: `${randomSchedule(60, 1)} ${randomSchedule(
24,
6,
).toString()} * * *`,
status: CrontabStatus.idle,
},
{
name: '互助码导出',
command: 'export_sharecodes',
schedule: '48 5 * * *',
status: CrontabStatus.idle,
},
{
name: '删除日志',
command: 'rm_log >/dev/null 2>&1',
command: 'ql rmlog 7',
schedule: '30 7 */7 * *',
status: CrontabStatus.disabled,
status: CrontabStatus.idle,
},
{
name: '重置密码',
command: 'js resetpwd',
schedule: '33 6 */7 * *',
status: CrontabStatus.disabled,
},
{
name: '运行所有脚本(慎用)',
command: 'js runall',
schedule: '33 6 */7 * *',
status: CrontabStatus.disabled,
name: '互助码',
command: 'ql code',
schedule: '30 7 * * *',
status: CrontabStatus.idle,
},
];
+59
View File
@@ -0,0 +1,59 @@
import schedule from 'node-schedule';
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import { Container } from 'typedi';
import CronService from './services/cron';
import { CrontabStatus } from './data/cron';
import config from './config';
const app = express();
const run = async () => {
const cronService = Container.get(CronService);
const cronDb = cronService.getDb();
cronDb
.find({})
.sort({ created: 1 })
.exec((err, docs) => {
if (err) {
Logger.error(err);
process.exit(1);
}
if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) {
const task = docs[i];
const _schedule = task.schedule && task.schedule.split(' ');
if (
_schedule &&
_schedule.length > 5 &&
task.status !== CrontabStatus.disabled
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`;
}
exec(command);
});
}
}
}
});
};
app
.listen(config.cronPort, () => {
run();
Logger.info(`
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
################################################
`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
+21 -5
View File
@@ -121,6 +121,16 @@ export default class CookieService {
});
}
private async formatCookies(cookies: Cookie[]) {
const result = [];
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
const { status, nickname } = await this.getJdInfo(cookie.value);
result.push({ ...cookie, status, nickname });
}
return result;
}
public async create(payload: string[]): Promise<Cookie[]> {
const cookies = await this.cookies('');
let position = initCookiePosition;
@@ -135,7 +145,7 @@ export default class CookieService {
});
const docs = await this.insert(tabs);
await this.set_cookies();
return docs;
return await this.formatCookies(docs);
}
public async insert(payload: Cookie[]): Promise<Cookie[]> {
@@ -156,20 +166,21 @@ export default class CookieService {
const tab = new Cookie({ ...doc, ...other });
const newDoc = await this.updateDb(tab);
await this.set_cookies();
return newDoc;
const [newCookie] = await this.formatCookies([newDoc]);
return newCookie;
}
public async updateDb(payload: Cookie): Promise<Cookie> {
private async updateDb(payload: Cookie): Promise<Cookie> {
return new Promise((resolve) => {
this.cronDb.update(
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, docs) => {
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs as Cookie);
resolve(doc as Cookie);
}
},
);
@@ -228,6 +239,11 @@ export default class CookieService {
],
};
}
const newDocs = await this.find(query, sort);
return await this.formatCookies(newDocs);
}
private async find(query: any, sort: any): Promise<Cookie[]> {
return new Promise((resolve) => {
this.cronDb
.find(query)
+108 -66
View File
@@ -22,12 +22,20 @@ export default class CronService {
return this.cronDb;
}
private isSixCron(cron: Crontab) {
const { schedule } = cron;
if (schedule.split(' ').length === 6) {
return true;
}
return false;
}
public async create(payload: Crontab): Promise<Crontab> {
const tab = new Crontab(payload);
tab.created = new Date().valueOf();
tab.saved = false;
const doc = await this.insert(tab);
await this.set_crontab();
await this.set_crontab(this.isSixCron(doc));
return doc;
}
@@ -74,9 +82,9 @@ export default class CronService {
this.cronDb.update({ _id }, { $set: { stopped, saved: false } });
}
public async remove(_id: string) {
this.cronDb.remove({ _id }, {});
await this.set_crontab();
public async remove(ids: string[]) {
this.cronDb.remove({ _id: { $in: ids } }, { multi: true });
await this.set_crontab(true);
}
public async crontabs(searchText?: string): Promise<Crontab[]> {
@@ -112,68 +120,98 @@ export default class CronService {
});
}
public async run(_id: string) {
this.cronDb.find({ _id }).exec((err, docs: Crontab[]) => {
let res = docs[0];
this.logger.silly('Running job');
this.logger.silly('ID: ' + _id);
this.logger.silly('Original command: ' + res.command);
let logFile = `${config.manualLogPath}${res._id}.log`;
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`);
let cmdStr = res.command;
if (res.command.startsWith('js') && !res.command.endsWith('now')) {
cmdStr = `${res.command} now`;
} else if (/&& (.*) >>/.test(res.command)) {
cmdStr = res.command.match(/&& (.*) >>/)[1];
public async run(ids: string[]) {
this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
this.runSingle(doc);
}
const cmd = spawn(cmdStr, { shell: true });
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.running } });
cmd.stdout.on('data', (data) => {
this.logger.silly(`stdout: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.stderr.on('data', (data) => {
this.logger.error(`stderr: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.on('close', (code) => {
this.logger.silly(`child process exited with code ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
});
cmd.on('error', (err) => {
this.logger.silly(err);
fs.appendFileSync(logFile, err.stack);
});
cmd.on('exit', (code: number, signal: any) => {
this.logger.silly(`cmd exit ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n执行结束...`);
});
cmd.on('disconnect', () => {
this.logger.silly(`cmd disconnect`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n连接断开...`);
});
});
}
public async disabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.disabled } });
public async stop(ids: string[]) {
this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc.pid) {
exec(`kill -9 ${doc.pid}`);
}
}
});
}
private async runSingle(cron: Crontab) {
let { _id, command } = cron;
this.logger.silly('Running job');
this.logger.silly('ID: ' + _id);
this.logger.silly('Original command: ' + command);
let logFile = `${config.manualLogPath}${_id}.log`;
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`);
let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`;
}
if (cmdStr.endsWith('.js')) {
cmdStr = `${cmdStr} now`;
}
const cmd = spawn(cmdStr, { shell: true });
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.running, pid: cmd.pid } },
);
cmd.stdout.on('data', (data) => {
this.logger.silly(`stdout: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.stderr.on('data', (data) => {
this.logger.error(`stderr: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.on('close', (code) => {
this.logger.silly(`child process exited with code ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
});
cmd.on('error', (err) => {
this.logger.silly(err);
fs.appendFileSync(logFile, err.stack);
});
cmd.on('exit', (code: number, signal: any) => {
this.logger.silly(`cmd exit ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n执行结束...`);
});
cmd.on('disconnect', () => {
this.logger.silly(`cmd disconnect`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
fs.appendFileSync(logFile, `\n\n连接断开...`);
});
}
public async disabled(ids: string[]) {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.disabled } },
{ multi: true },
);
await this.set_crontab();
}
public async enabled(_id: string) {
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
public async enabled(ids: string[]) {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle } },
{ multi: true },
);
}
public async log(_id: string) {
@@ -186,11 +224,12 @@ export default class CronService {
return crontab_job_string;
}
private async set_crontab() {
private async set_crontab(needReloadSchedule: boolean = false) {
const tabs = await this.crontabs();
var crontab_string = '';
tabs.forEach((tab) => {
if (tab.status === CrontabStatus.disabled) {
const _schedule = tab.schedule && tab.schedule.split(' ');
if (tab.status === CrontabStatus.disabled || _schedule.length !== 5) {
crontab_string += '# ';
crontab_string += tab.schedule;
crontab_string += ' ';
@@ -208,6 +247,9 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`);
if (needReloadSchedule) {
exec(`pm2 reload schedule`);
}
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
}
@@ -226,11 +268,11 @@ export default class CronService {
var command = line.replace(regex, '').trim();
var schedule = line.replace(command, '').trim();
var is_valid = false;
try {
is_valid = cron_parser.parseString(line).expressions.length > 0;
} catch (e) {}
if (command && schedule && is_valid) {
if (
command &&
schedule &&
cron_parser.parseExpression(schedule).hasNext()
) {
var name = namePrefix + '_' + index;
this.cronDb.findOne({ command, schedule }, (err, doc) => {