mirror of
https://github.com/whyour/qinglong.git
synced 2025-08-24 04:36:08 +08:00
全新定时任务管理
This commit is contained in:
parent
32be5a6591
commit
8a45599919
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,6 +20,7 @@
|
||||||
/src/.umi-test
|
/src/.umi-test
|
||||||
/.env.local
|
/.env.local
|
||||||
.env
|
.env
|
||||||
|
.history
|
||||||
|
|
||||||
/config
|
/config
|
||||||
/log
|
/log
|
||||||
|
|
167
back/api/cron.ts
167
back/api/cron.ts
|
@ -3,15 +3,180 @@ import { Container } from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import CronService from '../services/cron';
|
||||||
|
import { celebrate, Joi } from 'celebrate';
|
||||||
const route = Router();
|
const route = Router();
|
||||||
|
|
||||||
export default (app: Router) => {
|
export default (app: Router) => {
|
||||||
app.use('/', route);
|
app.use('/', route);
|
||||||
route.get(
|
route.get(
|
||||||
'/cron',
|
'/crons',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
const logger: Logger = Container.get('logger');
|
||||||
try {
|
try {
|
||||||
|
const cookieService = Container.get(CronService);
|
||||||
|
const data = await cookieService.crontabs();
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.post(
|
||||||
|
'/crons',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
command: Joi.string().required(),
|
||||||
|
schedule: Joi.string().required(),
|
||||||
|
name: Joi.string(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cookieService = Container.get(CronService);
|
||||||
|
const data = await cookieService.create(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/crons/:id/run',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cookieService = Container.get(CronService);
|
||||||
|
const data = await cookieService.run(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/crons/:id/disable',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cookieService = Container.get(CronService);
|
||||||
|
const data = await cookieService.disabled(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/crons/:id/enable',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cookieService = Container.get(CronService);
|
||||||
|
const data = await cookieService.enabled(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/crons/:id/log',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cookieService = Container.get(CronService);
|
||||||
|
const data = await cookieService.log(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.put(
|
||||||
|
'/crons',
|
||||||
|
celebrate({
|
||||||
|
body: Joi.object({
|
||||||
|
command: Joi.string().required(),
|
||||||
|
schedule: Joi.string().required(),
|
||||||
|
name: Joi.string(),
|
||||||
|
_id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cookieService = Container.get(CronService);
|
||||||
|
const data = await cookieService.update(req.body);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.delete(
|
||||||
|
'/crons/:id',
|
||||||
|
celebrate({
|
||||||
|
params: Joi.object({
|
||||||
|
id: Joi.string().required(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cookieService = Container.get(CronService);
|
||||||
|
const data = await cookieService.remove(req.params.id);
|
||||||
|
return res.send({ code: 200, data });
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('🔥 error: %o', e);
|
||||||
|
return next(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
route.get(
|
||||||
|
'/crons/import',
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const logger: Logger = Container.get('logger');
|
||||||
|
try {
|
||||||
|
const cookieService = Container.get(CronService);
|
||||||
|
const data = await cookieService.import_crontab();
|
||||||
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('🔥 error: %o', e);
|
logger.error('🔥 error: %o', e);
|
||||||
return next(e);
|
return next(e);
|
||||||
|
|
27
back/data/cron.ts
Normal file
27
back/data/cron.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
export class Crontab {
|
||||||
|
name?: string;
|
||||||
|
command: string;
|
||||||
|
schedule: string;
|
||||||
|
timestamp?: string;
|
||||||
|
created?: number;
|
||||||
|
saved?: boolean;
|
||||||
|
_id?: string;
|
||||||
|
status?: CrontabStatus;
|
||||||
|
|
||||||
|
constructor(options: Crontab) {
|
||||||
|
this.name = options.name;
|
||||||
|
this.command = options.command;
|
||||||
|
this.schedule = options.schedule;
|
||||||
|
this.saved = options.saved;
|
||||||
|
this._id = options._id;
|
||||||
|
this.created = options.created;
|
||||||
|
this.status = options.status || CrontabStatus.idle;
|
||||||
|
this.timestamp = new Date().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CrontabStatus {
|
||||||
|
'idle',
|
||||||
|
'running',
|
||||||
|
'disabled',
|
||||||
|
}
|
183
back/services/cron.ts
Normal file
183
back/services/cron.ts
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
|
import DataStore from 'nedb';
|
||||||
|
import config from '../config';
|
||||||
|
import { Crontab, CrontabStatus } from '../data/cron';
|
||||||
|
import { exec, execSync, spawn } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import cron_parser from 'cron-parser';
|
||||||
|
import { getFileContentByName } from '../config/util';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export default class CronService {
|
||||||
|
private cronDb = new DataStore({ filename: config.cronDbFile });
|
||||||
|
|
||||||
|
constructor(@Inject('logger') private logger: winston.Logger) {
|
||||||
|
this.cronDb.loadDatabase((err) => {
|
||||||
|
if (err) throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(payload: Crontab): Promise<void> {
|
||||||
|
const tab = new Crontab(payload);
|
||||||
|
tab.created = new Date().valueOf();
|
||||||
|
tab.saved = false;
|
||||||
|
this.cronDb.insert(tab);
|
||||||
|
await this.set_crontab();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update(payload: Crontab): Promise<void> {
|
||||||
|
const { _id, ...other } = payload;
|
||||||
|
const doc = await this.get(_id);
|
||||||
|
const tab = new Crontab({ ...doc, ...other });
|
||||||
|
tab.saved = false;
|
||||||
|
this.cronDb.update({ _id }, tab, { returnUpdatedDocs: true });
|
||||||
|
await this.set_crontab();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async status(_id: string, stopped: boolean) {
|
||||||
|
this.cronDb.update({ _id }, { $set: { stopped, saved: false } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async remove(_id: string) {
|
||||||
|
this.cronDb.remove({ _id }, {});
|
||||||
|
await this.set_crontab();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async crontabs(): Promise<Crontab[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.cronDb
|
||||||
|
.find({})
|
||||||
|
.sort({ created: -1 })
|
||||||
|
.exec((err, docs) => {
|
||||||
|
resolve(docs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(_id: string): Promise<Crontab> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.cronDb.find({ _id }).exec((err, docs) => {
|
||||||
|
resolve(docs[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async run(_id: string) {
|
||||||
|
this.cronDb.find({ _id }).exec((err, docs) => {
|
||||||
|
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, `${new Date().toString()}\n\n`);
|
||||||
|
|
||||||
|
const cmd = spawn(res.command, { 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async disabled(_id: string) {
|
||||||
|
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.disabled } });
|
||||||
|
await this.set_crontab();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enabled(_id: string) {
|
||||||
|
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } });
|
||||||
|
}
|
||||||
|
|
||||||
|
public async log(_id: string) {
|
||||||
|
let logFile = `${config.manualLogPath}${_id}.log`;
|
||||||
|
return getFileContentByName(logFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private make_command(tab: Crontab) {
|
||||||
|
const crontab_job_string = `ID=${tab._id} ${tab.command}`;
|
||||||
|
return crontab_job_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async set_crontab() {
|
||||||
|
const tabs = await this.crontabs();
|
||||||
|
var crontab_string = '';
|
||||||
|
tabs.forEach((tab) => {
|
||||||
|
if (tab.status !== CrontabStatus.disabled) {
|
||||||
|
crontab_string += tab.schedule;
|
||||||
|
crontab_string += ' ';
|
||||||
|
crontab_string += this.make_command(tab);
|
||||||
|
crontab_string += '\n';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.silly(crontab_string);
|
||||||
|
fs.writeFileSync(config.crontabFile, crontab_string);
|
||||||
|
|
||||||
|
execSync(`crontab ${config.crontabFile}`);
|
||||||
|
this.cronDb.update({}, { $set: { saved: true } }, { multi: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private reload_db() {
|
||||||
|
this.cronDb.loadDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public import_crontab() {
|
||||||
|
exec('crontab -l', (error, stdout, stderr) => {
|
||||||
|
var lines = stdout.split('\n');
|
||||||
|
var namePrefix = new Date().getTime();
|
||||||
|
|
||||||
|
lines.reverse().forEach((line, index) => {
|
||||||
|
line = line.replace(/\t+/g, ' ');
|
||||||
|
var regex = /^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
|
||||||
|
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) {
|
||||||
|
var name = namePrefix + '_' + index;
|
||||||
|
|
||||||
|
this.cronDb.findOne({ command, schedule }, (err, doc) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
if (!doc) {
|
||||||
|
this.create({ name, command, schedule });
|
||||||
|
} else {
|
||||||
|
doc.command = command;
|
||||||
|
doc.schedule = schedule;
|
||||||
|
this.update(doc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public autosave_crontab() {
|
||||||
|
return this.set_crontab();
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,13 +29,14 @@
|
||||||
"celebrate": "^13.0.3",
|
"celebrate": "^13.0.3",
|
||||||
"codemirror": "^5.59.4",
|
"codemirror": "^5.59.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"cron-parser": "^3.3.0",
|
||||||
"darkreader": "^4.9.27",
|
"darkreader": "^4.9.27",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-jwt": "^6.0.0",
|
"express-jwt": "^6.0.0",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mongoose": "^5.10.6",
|
"nedb": "^1.8.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"react-codemirror2": "^7.2.1",
|
"react-codemirror2": "^7.2.1",
|
||||||
|
@ -51,6 +52,7 @@
|
||||||
"@types/express": "^4.17.8",
|
"@types/express": "^4.17.8",
|
||||||
"@types/express-jwt": "^6.0.1",
|
"@types/express-jwt": "^6.0.1",
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/jsonwebtoken": "^8.5.0",
|
||||||
|
"@types/nedb": "^1.8.11",
|
||||||
"@types/node": "^14.11.2",
|
"@types/node": "^14.11.2",
|
||||||
"@types/node-fetch": "^2.5.8",
|
"@types/node-fetch": "^2.5.8",
|
||||||
"@types/qrcode.react": "^1.0.1",
|
"@types/qrcode.react": "^1.0.1",
|
||||||
|
|
74
shell/api.sh
Executable file
74
shell/api.sh
Executable file
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
get_token() {
|
||||||
|
echo $AuthConf
|
||||||
|
local authInfo=$(cat $AuthConf)
|
||||||
|
token=$(get_json_value "$authInfo" "token")
|
||||||
|
}
|
||||||
|
|
||||||
|
get_json_value() {
|
||||||
|
local json=$1
|
||||||
|
local key=$2
|
||||||
|
|
||||||
|
if [[ -z "$3" ]]; then
|
||||||
|
local num=1
|
||||||
|
else
|
||||||
|
local num=$3
|
||||||
|
fi
|
||||||
|
|
||||||
|
local value=$(echo "${json}" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'${key}'\042/){print $(i+1)}}}' | tr -d '"' | sed -n ${num}p)
|
||||||
|
|
||||||
|
echo ${value}
|
||||||
|
}
|
||||||
|
|
||||||
|
add_cron_api() {
|
||||||
|
local currentTimeStamp=$(date +%s%3)
|
||||||
|
if [ $# -eq 1 ]; then
|
||||||
|
local schedule=$(echo "$1" | awk -F ": " '{print $1}')
|
||||||
|
local command=$(echo "$1" | awk -F ": " '{print $2}')
|
||||||
|
local name=$(echo "$1" | awk -F ": " '{print $3}')
|
||||||
|
else
|
||||||
|
local schedule=$1
|
||||||
|
local command=$2
|
||||||
|
local name=$3
|
||||||
|
fi
|
||||||
|
|
||||||
|
local api=$(curl "http://localhost:5678/api/crons?t=$currentTimeStamp" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||||
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
|
-H "Origin: http://localhost:5700" \
|
||||||
|
-H "Referer: http://localhost:5700/crontab" \
|
||||||
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||||
|
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
|
||||||
|
--compressed)
|
||||||
|
echo $api
|
||||||
|
code=$(get_json_value $api "code")
|
||||||
|
if [[ $code == 200 ]]; then
|
||||||
|
echo -e "$name 添加成功"
|
||||||
|
else
|
||||||
|
echo -e "$name 添加失败"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
del_cron_api() {
|
||||||
|
local id=$1
|
||||||
|
local currentTimeStamp=$(date +%s%3)
|
||||||
|
local api=$(curl "http://localhost:5678/api/crons/$id?t=$currentTimeStamp" \
|
||||||
|
-X 'DELETE' \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-H "Authorization: Bearer $token" \
|
||||||
|
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||||
|
-H "Content-Type: application/json;charset=UTF-8" \
|
||||||
|
-H "Origin: http://localhost:5700" \
|
||||||
|
-H "Referer: http://localhost:5700/crontab" \
|
||||||
|
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7")
|
||||||
|
echo $api
|
||||||
|
code=$(get_json_value $api "code")
|
||||||
|
if [[ $code == 200 ]]; then
|
||||||
|
echo -e "$name 删除成功"
|
||||||
|
else
|
||||||
|
echo -e "$name 删除失败"
|
||||||
|
fi
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ ScriptsDir=$ShellDir/scripts
|
||||||
ConfigDir=$ShellDir/config
|
ConfigDir=$ShellDir/config
|
||||||
FileConf=$ConfigDir/config.sh
|
FileConf=$ConfigDir/config.sh
|
||||||
CookieConf=$ConfigDir/cookie.sh
|
CookieConf=$ConfigDir/cookie.sh
|
||||||
|
AuthConf=$ConfigDir/auth.json
|
||||||
ExtraShell=$ConfigDir/extra.sh
|
ExtraShell=$ConfigDir/extra.sh
|
||||||
FileConfSample=$ShellDir/sample/config.sh.sample
|
FileConfSample=$ShellDir/sample/config.sh.sample
|
||||||
ListCronSample=$ShellDir/sample/crontab.list.sample
|
ListCronSample=$ShellDir/sample/crontab.list.sample
|
||||||
|
@ -44,6 +45,8 @@ Import_Conf() {
|
||||||
fi
|
fi
|
||||||
[ -f $CookieConf ] && . $CookieConf
|
[ -f $CookieConf ] && . $CookieConf
|
||||||
[ -f $FileConf ] && . $FileConf
|
[ -f $FileConf ] && . $FileConf
|
||||||
|
|
||||||
|
. $ShellDir/shell/api.sh
|
||||||
}
|
}
|
||||||
|
|
||||||
# 更新shell
|
# 更新shell
|
||||||
|
@ -198,7 +201,7 @@ Git_Pull_Scripts_Next() {
|
||||||
|
|
||||||
Diff_Cron() {
|
Diff_Cron() {
|
||||||
cat $ListCronRemote | grep -E "node.+j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u >$ListRemoteTask
|
cat $ListCronRemote | grep -E "node.+j[drx]_\w+\.js" | perl -pe "s|.+(j[drx]_\w+)\.js.+|\1|" | sort -u >$ListRemoteTask
|
||||||
cat $ListCronCurrent | grep -E "$ShellJs j[drx]_\w+" | perl -pe "s|.*$ShellJs (j[drx]_\w+)\.*|\1|" | sort -u >$ListCurrentTask
|
cat $ListCronCurrent | grep -E "$ShellJs j[drx]_\w+" | perl -pe "s|.*ID=(.*)$ShellJs (j[drx]_\w+)\.*|\1|" | sort -u >$ListCurrentTask
|
||||||
if [ -s $ListCurrentTask ]; then
|
if [ -s $ListCurrentTask ]; then
|
||||||
grep -vwf $ListCurrentTask $ListRemoteTask >$ListJsAdd
|
grep -vwf $ListCurrentTask $ListRemoteTask >$ListJsAdd
|
||||||
else
|
else
|
||||||
|
@ -218,7 +221,7 @@ Del_Cron() {
|
||||||
echo
|
echo
|
||||||
JsDrop=$(cat $ListJsDrop)
|
JsDrop=$(cat $ListJsDrop)
|
||||||
for Cron in $JsDrop; do
|
for Cron in $JsDrop; do
|
||||||
perl -i -ne "{print unless / $Cron( |$)/}" $ListCronCurrent
|
del_cron_api "$Cron"
|
||||||
done
|
done
|
||||||
crontab $ListCronCurrent
|
crontab $ListCronCurrent
|
||||||
echo -e "成功删除失效的脚本与定时任务\n"
|
echo -e "成功删除失效的脚本与定时任务\n"
|
||||||
|
@ -234,7 +237,8 @@ Add_Cron() {
|
||||||
if [[ $Cron == jd_bean_sign ]]; then
|
if [[ $Cron == jd_bean_sign ]]; then
|
||||||
echo "4 0,9 * * * $ShellJs $Cron" >> $ListCronCurrent
|
echo "4 0,9 * * * $ShellJs $Cron" >> $ListCronCurrent
|
||||||
else
|
else
|
||||||
cat $ListCronRemote | grep -E "\/$Cron\." | perl -pe "s|(^.+)node */scripts/(j[drx]_\w+)\.js.+|\1$ShellJs \2|" >> $ListCronCurrent
|
param=$(cat $ListCronRemote | grep -E "\/$Cron\." | perl -pe "s|(^.+)node */scripts/(j[drx]_\w+)\.js.+|\1\: $ShellJs \2: \2|")
|
||||||
|
add_cron_api "$param"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
@ -274,6 +278,8 @@ echo -e "--------------------------------------------------------------\n"
|
||||||
Import_Conf
|
Import_Conf
|
||||||
Random_Pull_Cron
|
Random_Pull_Cron
|
||||||
|
|
||||||
|
get_token
|
||||||
|
|
||||||
# 更新shell
|
# 更新shell
|
||||||
[ -f $ShellDir/package.json ] && PanelDependOld=$(cat $ShellDir/package.json)
|
[ -f $ShellDir/package.json ] && PanelDependOld=$(cat $ShellDir/package.json)
|
||||||
Git_Pull_Shell
|
Git_Pull_Shell
|
||||||
|
|
|
@ -1,39 +1,311 @@
|
||||||
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
|
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
|
||||||
import { Button, notification, Modal } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
notification,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Space,
|
||||||
|
Tooltip,
|
||||||
|
Dropdown,
|
||||||
|
Menu,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
ClockCircleOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
EllipsisOutlined,
|
||||||
|
PlayCircleOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
StopOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import { PageContainer } from '@ant-design/pro-layout';
|
import { PageContainer } from '@ant-design/pro-layout';
|
||||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
|
import CronModal from './modal';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
enum CrontabStatus {
|
||||||
|
'idle',
|
||||||
|
'running',
|
||||||
|
'disabled',
|
||||||
|
}
|
||||||
|
|
||||||
const Crontab = () => {
|
const Crontab = () => {
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '任务名',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => (
|
||||||
|
<span>{record.name || record._id}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '任务',
|
||||||
|
dataIndex: 'command',
|
||||||
|
key: 'command',
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '任务定时',
|
||||||
|
dataIndex: 'schedule',
|
||||||
|
key: 'schedule',
|
||||||
|
align: 'center' as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
dataIndex: 'status',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any) => (
|
||||||
|
<>
|
||||||
|
{record.status === CrontabStatus.idle && (
|
||||||
|
<Tag icon={<ClockCircleOutlined />} color="default">
|
||||||
|
空闲中
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{record.status === CrontabStatus.running && (
|
||||||
|
<Tag icon={<SyncOutlined spin />} color="processing">
|
||||||
|
运行中
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
{record.status === CrontabStatus.disabled && (
|
||||||
|
<Tag icon={<CloseCircleOutlined />} color="error">
|
||||||
|
已禁用
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
align: 'center' as const,
|
||||||
|
render: (text: string, record: any, index: number) => (
|
||||||
|
<Space size="middle">
|
||||||
|
<Tooltip title="运行">
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
runCron(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlayCircleOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="日志">
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
logCron(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileTextOutlined />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<MoreBtn key="more" record={record} />
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const [width, setWdith] = useState('100%');
|
const [width, setWdith] = useState('100%');
|
||||||
const [marginLeft, setMarginLeft] = useState(0);
|
const [marginLeft, setMarginLeft] = useState(0);
|
||||||
const [marginTop, setMarginTop] = useState(-72);
|
const [marginTop, setMarginTop] = useState(-72);
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const [editedCron, setEditedCron] = useState();
|
||||||
|
|
||||||
const getConfig = () => {
|
const getCrons = () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
request
|
request
|
||||||
.get(`${config.apiPrefix}config/crontab`)
|
.get(`${config.apiPrefix}crons`)
|
||||||
.then((data) => {
|
.then((data: any) => {
|
||||||
setValue(data.data);
|
setValue(data.data);
|
||||||
})
|
})
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateConfig = () => {
|
const addCron = () => {
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editCron = (record: any) => {
|
||||||
|
setEditedCron(record);
|
||||||
|
setIsModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const delCron = (record: any) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认删除定时任务 <Text type="warning">{record.name}</Text> 吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
request
|
request
|
||||||
.post(`${config.apiPrefix}save`, {
|
.delete(`${config.apiPrefix}crons`, {
|
||||||
data: { content: value, name: 'crontab.list' },
|
data: { _id: record._id },
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
notification.success({
|
notification.success({
|
||||||
message: data.msg,
|
message: '删除成功',
|
||||||
|
});
|
||||||
|
getCrons();
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const runCron = (record: any) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认运行',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认运行定时任务 <Text type="warning">{record.name}</Text> 吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}crons/${record._id}/run`)
|
||||||
|
.then((data: any) => {
|
||||||
|
getCrons();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const enabledOrDisabledCron = (record: any) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认禁用',
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
确认禁用定时任务 <Text type="warning">{record.name}</Text> 吗
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
onOk() {
|
||||||
|
request
|
||||||
|
.get(
|
||||||
|
`${config.apiPrefix}crons/${record._id}/${
|
||||||
|
record.status === CrontabStatus.disabled ? 'enable' : 'disable'
|
||||||
|
}`,
|
||||||
|
{
|
||||||
|
data: { _id: record._id },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((data: any) => {
|
||||||
|
if (data.code === 200) {
|
||||||
|
notification.success({
|
||||||
|
message: '禁用成功',
|
||||||
|
});
|
||||||
|
getCrons();
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
console.log('Cancel');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const logCron = (record: any) => {
|
||||||
|
request
|
||||||
|
.get(`${config.apiPrefix}crons/${record._id}/log`)
|
||||||
|
.then((data: any) => {
|
||||||
|
Modal.info({
|
||||||
|
width: 650,
|
||||||
|
title: `${record.name || record._id}`,
|
||||||
|
content: (
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }}>
|
||||||
|
{data.data || '暂无日志'}
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
onOk() {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MoreBtn: React.FC<{
|
||||||
|
record: any;
|
||||||
|
}> = ({ record }) => (
|
||||||
|
<Dropdown
|
||||||
|
arrow
|
||||||
|
trigger={['click', 'hover']}
|
||||||
|
overlay={
|
||||||
|
<Menu onClick={({ key }) => action(key, record)}>
|
||||||
|
<Menu.Item key="edit" icon={<EditOutlined />}>
|
||||||
|
编辑
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
key="enableordisable"
|
||||||
|
icon={
|
||||||
|
record.status === CrontabStatus.disabled ? (
|
||||||
|
<CheckCircleOutlined />
|
||||||
|
) : (
|
||||||
|
<StopOutlined />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{record.status === CrontabStatus.disabled ? '启用' : '禁用'}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="delete" icon={<DeleteOutlined />}>
|
||||||
|
删除
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<a>
|
||||||
|
<EllipsisOutlined />
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
|
||||||
|
const action = (key: string | number, record: any) => {
|
||||||
|
switch (key) {
|
||||||
|
case 'edit':
|
||||||
|
editCron(record);
|
||||||
|
break;
|
||||||
|
case 'enableordisable':
|
||||||
|
enabledOrDisabledCron(record);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
delCron(record);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = (needUpdate?: boolean) => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
if (needUpdate) {
|
||||||
|
getCrons();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (document.body.clientWidth < 768) {
|
if (document.body.clientWidth < 768) {
|
||||||
setWdith('auto');
|
setWdith('auto');
|
||||||
|
@ -44,16 +316,17 @@ const Crontab = () => {
|
||||||
setMarginLeft(0);
|
setMarginLeft(0);
|
||||||
setMarginTop(-72);
|
setMarginTop(-72);
|
||||||
}
|
}
|
||||||
getConfig();
|
getCrons();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer
|
<PageContainer
|
||||||
className="code-mirror-wrapper"
|
className="code-mirror-wrapper"
|
||||||
title="crontab.list"
|
title="定时任务"
|
||||||
|
loading={loading}
|
||||||
extra={[
|
extra={[
|
||||||
<Button key="1" type="primary" onClick={updateConfig}>
|
<Button key="2" type="primary" onClick={() => addCron()}>
|
||||||
保存
|
添加定时
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
header={{
|
header={{
|
||||||
|
@ -68,23 +341,24 @@ const Crontab = () => {
|
||||||
marginLeft,
|
marginLeft,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
style={{
|
|
||||||
height: '100vh',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CodeMirror
|
<Table
|
||||||
value={value}
|
columns={columns}
|
||||||
options={{
|
pagination={{
|
||||||
lineNumbers: true,
|
hideOnSinglePage: true,
|
||||||
lineWrapping: true,
|
showSizeChanger: true,
|
||||||
styleActiveLine: true,
|
defaultPageSize: 20,
|
||||||
matchBrackets: true,
|
|
||||||
mode: 'shell',
|
|
||||||
}}
|
}}
|
||||||
onBeforeChange={(editor, data, value) => {
|
dataSource={value}
|
||||||
setValue(value);
|
rowKey="pin"
|
||||||
}}
|
size="middle"
|
||||||
onChange={(editor, data, value) => {}}
|
bordered
|
||||||
|
scroll={{ x: 768 }}
|
||||||
|
/>
|
||||||
|
<CronModal
|
||||||
|
visible={isModalVisible}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
cron={editedCron}
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|
92
src/pages/crontab/modal.tsx
Normal file
92
src/pages/crontab/modal.tsx
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Modal, notification, Input, Form } from 'antd';
|
||||||
|
import { request } from '@/utils/http';
|
||||||
|
import config from '@/utils/config';
|
||||||
|
import cronParse from 'cron-parser';
|
||||||
|
|
||||||
|
const CronModal = ({
|
||||||
|
cron,
|
||||||
|
handleCancel,
|
||||||
|
visible,
|
||||||
|
}: {
|
||||||
|
cron?: any;
|
||||||
|
visible: boolean;
|
||||||
|
handleCancel: (needUpdate?: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const handleOk = async (values: any) => {
|
||||||
|
const method = cron ? 'put' : 'post';
|
||||||
|
const payload = { ...values };
|
||||||
|
if (cron) {
|
||||||
|
payload._id = cron._id;
|
||||||
|
}
|
||||||
|
const { code, data } = await request[method](`${config.apiPrefix}crons`, {
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
if (code === 200) {
|
||||||
|
notification.success({
|
||||||
|
message: cron ? '更新Cron成功' : '添加Cron成功',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleCancel(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cron) {
|
||||||
|
form.setFieldsValue(cron);
|
||||||
|
}
|
||||||
|
}, [cron]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={cron ? '编辑定时' : '新建定时'}
|
||||||
|
visible={visible}
|
||||||
|
onOk={() => {
|
||||||
|
form
|
||||||
|
.validateFields()
|
||||||
|
.then((values) => {
|
||||||
|
handleOk(values);
|
||||||
|
})
|
||||||
|
.catch((info) => {
|
||||||
|
console.log('Validate Failed:', info);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCancel={() => handleCancel()}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" name="form_in_modal" preserve={false}>
|
||||||
|
<Form.Item name="name" label="名称">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="command" label="任务" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="schedule"
|
||||||
|
label="时间"
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
{
|
||||||
|
validator: (rule, value) => {
|
||||||
|
if (cronParse.parseString(value).expressions.length > 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
return Promise.reject('Cron表达式格式有误');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CronModal;
|
Loading…
Reference in New Issue
Block a user