重构任务执行逻辑

This commit is contained in:
whyour 2022-05-15 20:40:29 +08:00
parent bb47d67d0b
commit 9dcc547ac7
7 changed files with 204 additions and 190 deletions

View File

@ -232,6 +232,14 @@ export async function fileExist(file: any) {
}); });
} }
export async function createFile(file: string, data: string = '') {
return new Promise((resolve) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, data);
resolve(true);
});
}
export async function concurrentRun( export async function concurrentRun(
fnList: Array<() => Promise<any>> = [], fnList: Array<() => Promise<any>> = [],
max = 5, max = 5,

View File

@ -1,13 +1,14 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { exec } from 'child_process'; import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import { import {
ToadScheduler, ToadScheduler,
LongIntervalJob, LongIntervalJob,
AsyncTask, AsyncTask,
SimpleIntervalSchedule, SimpleIntervalSchedule,
} from 'toad-scheduler'; } from 'toad-scheduler';
import dayjs from 'dayjs';
interface ScheduleTaskType { interface ScheduleTaskType {
id: number; id: number;
@ -16,6 +17,19 @@ interface ScheduleTaskType {
schedule?: string; schedule?: string;
} }
export interface TaskCallbacks {
onStart?: (
cp: ChildProcessWithoutNullStreams,
startTime: dayjs.Dayjs,
) => void;
onEnd?: (
cp: ChildProcessWithoutNullStreams,
endTime: dayjs.Dayjs,
diff: number,
) => void;
onError?: (message: string) => void;
}
@Service() @Service()
export default class ScheduleService { export default class ScheduleService {
private scheduleStacks = new Map<string, nodeSchedule.Job>(); private scheduleStacks = new Map<string, nodeSchedule.Job>();
@ -26,12 +40,63 @@ export default class ScheduleService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async createCronTask({ async runTask(command: string, callbacks: TaskCallbacks = {}) {
id = 0, return new Promise(async (resolve, reject) => {
try {
const startTime = dayjs();
const cp = spawn(command, { shell: '/bin/bash' });
callbacks.onStart?.(cp, startTime);
cp.stderr.on('data', (data) => {
this.logger.error(
'执行任务%s失败时间%s, 错误信息:%j',
command, command,
name, new Date().toLocaleString(),
schedule = '', data.toString(),
}: ScheduleTaskType) { );
callbacks.onError?.(data.toString());
});
cp.on('error', (err) => {
this.logger.error(
'执行任务%s失败时间%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
callbacks.onError?.(JSON.stringify(err));
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
callbacks.onEnd?.(cp, endTime, endTime.diff(startTime, 'seconds'));
resolve(null);
});
} catch (error) {
await this.logger.error(
'执行任务%s失败时间%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
callbacks.onError?.(JSON.stringify(error));
resolve(null);
}
});
}
async createCronTask(
{ id = 0, command, name, schedule = '' }: ScheduleTaskType,
callbacks?: TaskCallbacks,
) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[创建cron任务]任务ID: %scron: %s任务名: %s执行命令: %s', '[创建cron任务]任务ID: %scron: %s任务名: %s执行命令: %s',
@ -44,39 +109,7 @@ export default class ScheduleService {
this.scheduleStacks.set( this.scheduleStacks.set(
_id, _id,
nodeSchedule.scheduleJob(_id, schedule, async () => { nodeSchedule.scheduleJob(_id, schedule, async () => {
try { await this.runTask(command, callbacks);
exec(
command,
{ maxBuffer: this.maxBuffer },
async (error, stdout, stderr) => {
if (error) {
await this.logger.error(
'执行任务%s失败时间%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
}
if (stderr) {
await this.logger.error(
'执行任务%s失败时间%s, 错误信息:%j',
command,
new Date().toLocaleString(),
stderr,
);
}
},
);
} catch (error) {
await this.logger.error(
'执行任务%s失败时间%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
} finally {
}
}), }),
); );
} }
@ -91,6 +124,7 @@ export default class ScheduleService {
{ id = 0, command, name = '' }: ScheduleTaskType, { id = 0, command, name = '' }: ScheduleTaskType,
schedule: SimpleIntervalSchedule, schedule: SimpleIntervalSchedule,
runImmediately = true, runImmediately = true,
callbacks?: TaskCallbacks,
) { ) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
@ -103,34 +137,7 @@ export default class ScheduleService {
name, name,
async () => { async () => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { await this.runTask(command, callbacks);
exec(
command,
{ maxBuffer: this.maxBuffer },
async (error, stdout, stderr) => {
if (error) {
await this.logger.error(
'执行任务%s失败时间%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
}
if (stderr) {
await this.logger.error(
'执行任务%s失败时间%s, 错误信息:%j',
command,
new Date().toLocaleString(),
stderr,
);
}
resolve();
},
);
} catch (error) {
reject(error);
}
}); });
}, },
(err) => { (err) => {

View File

@ -6,15 +6,25 @@ import {
SubscriptionModel, SubscriptionModel,
SubscriptionStatus, SubscriptionStatus,
} from '../data/subscription'; } from '../data/subscription';
import { exec, execSync, spawn } from 'child_process'; import {
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { getFileContentByName, concurrentRun, fileExist } from '../config/util'; import {
getFileContentByName,
concurrentRun,
fileExist,
createFile,
} from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import ScheduleService from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler'; import { SimpleIntervalSchedule } from 'toad-scheduler';
@Service() @Service()
@ -97,7 +107,11 @@ export default class SubscriptionService {
doc.command = this.formatCommand(doc); doc.command = this.formatCommand(doc);
if (doc.schedule_type === 'crontab') { if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any); this.scheduleService.cancelCronTask(doc as any);
needCreate && this.scheduleService.createCronTask(doc as any); needCreate &&
this.scheduleService.createCronTask(
doc as any,
this.taskCallbacks(doc),
);
} else { } else {
this.scheduleService.cancelIntervalTask(doc as any); this.scheduleService.cancelIntervalTask(doc as any);
const { type, value } = doc.interval_schedule as any; const { type, value } = doc.interval_schedule as any;
@ -105,10 +119,64 @@ export default class SubscriptionService {
this.scheduleService.createIntervalTask( this.scheduleService.createIntervalTask(
doc as any, doc as any,
{ [type]: value } as SimpleIntervalSchedule, { [type]: value } as SimpleIntervalSchedule,
true,
this.taskCallbacks(doc),
); );
} }
} }
private async handleLogPath(
logPath: string,
data: string = '',
): Promise<string> {
const absolutePath = path.resolve(config.logPath, logPath);
const logFileExist = await fileExist(absolutePath);
if (!logFileExist) {
await createFile(absolutePath, data);
}
return absolutePath;
}
private taskCallbacks(doc: Subscription): TaskCallbacks {
return {
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
const logTime = startTime.format('YYYY-MM-DD-HH-mm-ss');
const logPath = `${doc.alias}/${logTime}.log`;
await this.handleLogPath(
logPath as string,
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
);
await SubscriptionModel.update(
{
status: SubscriptionStatus.running,
pid: cp.pid,
log_path: logPath,
},
{ where: { id: doc.id } },
);
},
onEnd: async (cp, endTime, diff) => {
const sub = await this.getDb({ id: doc.id });
await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined },
{ where: { id: doc.id } },
);
const absolutePath = await this.handleLogPath(sub.log_path as string);
fs.appendFileSync(
absolutePath,
`\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)} ${diff} `,
);
},
onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
};
}
public async create(payload: Subscription): Promise<Subscription> { public async create(payload: Subscription): Promise<Subscription> {
const tab = new Subscription(payload); const tab = new Subscription(payload);
const doc = await this.insert(tab); const doc = await this.insert(tab);
@ -195,9 +263,7 @@ export default class SubscriptionService {
this.handleTask(doc, false); this.handleTask(doc, false);
const command = this.formatCommand(doc); const command = this.formatCommand(doc);
const err = await this.killTask(command); const err = await this.killTask(command);
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); const absolutePath = await this.handleLogPath(doc.log_path as string);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) {
const str = err ? `\n${err}` : ''; const str = err ? `\n${err}` : '';
fs.appendFileSync( fs.appendFileSync(
`${absolutePath}`, `${absolutePath}`,
@ -206,7 +272,6 @@ export default class SubscriptionService {
.replace(' 24:', ' 00:')} `, .replace(' 24:', ' 00:')} `,
); );
} }
}
await SubscriptionModel.update( await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined }, { status: SubscriptionStatus.idle, pid: undefined },
@ -249,57 +314,18 @@ export default class SubscriptionService {
} }
} }
private async runSingle(cronId: number): Promise<number> { private async runSingle(subscriptionId: number) {
return new Promise(async (resolve: any) => { const subscription = await this.getDb({ id: subscriptionId });
const cron = await this.getDb({ id: cronId }); if (subscription.status !== SubscriptionStatus.queued) {
if (cron.status !== SubscriptionStatus.queued) {
resolve();
return; return;
} }
let { id, log_path, name } = cron; const command = this.formatCommand(subscription);
const command = this.formatCommand(cron);
const absolutePath = path.resolve(config.logPath, `${log_path}`);
const logFileExist = log_path && (await fileExist(absolutePath));
this.logger.silly('Running job' + name); await this.scheduleService.runTask(
this.logger.silly('ID: ' + id); command,
this.logger.silly('Original command: ' + command); this.taskCallbacks(subscription),
const cp = spawn(command, { shell: '/bin/bash' });
await SubscriptionModel.update(
{ status: SubscriptionStatus.running, pid: cp.pid },
{ where: { id } },
); );
cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data}`);
}
});
cp.on('error', (err) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
}
});
cp.on('exit', async (code, signal) => {
this.logger.info(`${''} pid: ${cp.pid} exit ${code} signal ${signal}`);
await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
});
cp.on('close', async (code) => {
this.logger.info(`${''} pid: ${cp.pid} closed ${code}`);
await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
});
});
} }
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
@ -324,11 +350,8 @@ export default class SubscriptionService {
return ''; return '';
} }
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); const absolutePath = await this.handleLogPath(doc.log_path as string);
const logFileExist = doc.log_path && (await fileExist(absolutePath)); return getFileContentByName(absolutePath);
if (logFileExist) {
return getFileContentByName(`${absolutePath}`);
}
} }
public async logs(id: number) { public async logs(id: number) {

View File

@ -47,6 +47,7 @@
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^4.2.1", "cron-parser": "^4.2.1",
"dayjs": "^1.11.2",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.17.3", "express": "^4.17.3",
"express-jwt": "^6.1.1", "express-jwt": "^6.1.1",

View File

@ -478,11 +478,8 @@ main() {
repo) repo)
get_user_info get_user_info
get_uniq_path "$p2" "$p6" get_uniq_path "$p2" "$p6"
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path update_repo "$p2" "$p3" "$p4" "$p5" "$p6"
else else
echo -e "命令输入错误...\n" echo -e "命令输入错误...\n"
usage usage
@ -491,11 +488,8 @@ main() {
raw) raw)
get_user_info get_user_info
get_uniq_path "$p2" get_uniq_path "$p2"
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_raw "$p2" >>$log_path update_raw "$p2"
else else
echo -e "命令输入错误...\n" echo -e "命令输入错误...\n"
usage usage

View File

@ -477,6 +477,13 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => {
: 'subscription'; : 'subscription';
}; };
useEffect(() => {
if (logSubscription) {
localStorage.setItem('logSubscription', logSubscription.id);
setIsLogModalVisible(true);
}
}, [logSubscription]);
useEffect(() => { useEffect(() => {
getSubscriptions(); getSubscriptions();
}, [searchText]); }, [searchText]);
@ -541,7 +548,7 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => {
handleCancel={() => { handleCancel={() => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
}} }}
cron={logSubscription} subscription={logSubscription}
/> />
</PageContainer> </PageContainer>
); );

View File

@ -8,22 +8,14 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
const { Countdown } = Statistic;
const SubscriptionLogModal = ({ const SubscriptionLogModal = ({
cron, subscription,
handleCancel, handleCancel,
visible, visible,
data, data,
logUrl, logUrl,
}: { }: {
cron?: any; subscription?: any;
visible: boolean; visible: boolean;
handleCancel: () => void; handleCancel: () => void;
data?: string; data?: string;
@ -33,49 +25,29 @@ const SubscriptionLogModal = ({
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<any>(true);
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false); const [isPhone, setIsPhone] = useState(false);
const [theme, setTheme] = useState<string>('');
const getCronLog = (isFirst?: boolean) => { const getCronLog = (isFirst?: boolean) => {
if (isFirst) { if (isFirst) {
setLoading(true); setLoading(true);
} }
request request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`) .get(
logUrl
? logUrl
: `${config.apiPrefix}subscriptions/${subscription.id}/log`,
)
.then((data: any) => { .then((data: any) => {
if (localStorage.getItem('logCron') === String(cron.id)) { if (
localStorage.getItem('logSubscription') === String(subscription.id)
) {
const log = data.data as string; const log = data.data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting( setExecuting(log && !log.includes('执行结束'));
log && !log.includes('执行结束') && !log.includes('重启面板'), if (log && !log.includes('执行结束')) {
);
if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
setTimeout(() => { setTimeout(() => {
getCronLog(); getCronLog();
}, 2000); }, 2000);
} }
if (
log &&
log.includes('重启面板') &&
cron.status === CrontabStatus.running
) {
message.warning({
content: (
<span>
<Countdown
className="inline-countdown"
format="ss"
value={Date.now() + 1000 * 30}
/>
</span>
),
duration: 10,
});
setTimeout(() => {
window.location.reload();
}, 30000);
}
} }
}) })
.finally(() => { .finally(() => {
@ -86,7 +58,7 @@ const SubscriptionLogModal = ({
}; };
const cancel = () => { const cancel = () => {
localStorage.removeItem('logCron'); localStorage.removeItem('logSubscription');
handleCancel(); handleCancel();
}; };
@ -95,16 +67,18 @@ const SubscriptionLogModal = ({
<> <>
{(executing || loading) && <Loading3QuartersOutlined spin />} {(executing || loading) && <Loading3QuartersOutlined spin />}
{!executing && !loading && <CheckCircleOutlined />} {!executing && !loading && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '} <span style={{ marginLeft: 5 }}>
-{subscription && subscription.name}
</span>{' '}
</> </>
); );
}; };
useEffect(() => { useEffect(() => {
if (cron && cron.id && visible) { if (subscription && subscription.id && visible) {
getCronLog(true); getCronLog(true);
} }
}, [cron, visible]); }, [subscription, visible]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {