From 9dcc547ac76890de0f2c53ecef107fe149e0bdc5 Mon Sep 17 00:00:00 2001 From: whyour Date: Sun, 15 May 2022 20:40:29 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E4=BB=BB=E5=8A=A1=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/config/util.ts | 8 ++ back/services/schedule.ts | 143 ++++++++++++------------ back/services/subscription.ts | 161 ++++++++++++++++------------ package.json | 1 + shell/update.sh | 10 +- src/pages/subscription/index.tsx | 9 +- src/pages/subscription/logModal.tsx | 62 ++++------- 7 files changed, 204 insertions(+), 190 deletions(-) diff --git a/back/config/util.ts b/back/config/util.ts index f3763426..3be63946 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -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( fnList: Array<() => Promise> = [], max = 5, diff --git a/back/services/schedule.ts b/back/services/schedule.ts index c01a6aca..5794b360 100644 --- a/back/services/schedule.ts +++ b/back/services/schedule.ts @@ -1,13 +1,14 @@ import { Service, Inject } from 'typedi'; import winston from 'winston'; import nodeSchedule from 'node-schedule'; -import { exec } from 'child_process'; +import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process'; import { ToadScheduler, LongIntervalJob, AsyncTask, SimpleIntervalSchedule, } from 'toad-scheduler'; +import dayjs from 'dayjs'; interface ScheduleTaskType { id: number; @@ -16,6 +17,19 @@ interface ScheduleTaskType { 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() export default class ScheduleService { private scheduleStacks = new Map(); @@ -26,12 +40,63 @@ export default class ScheduleService { constructor(@Inject('logger') private logger: winston.Logger) {} - async createCronTask({ - id = 0, - command, - name, - schedule = '', - }: ScheduleTaskType) { + async runTask(command: string, callbacks: TaskCallbacks = {}) { + 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, + new Date().toLocaleString(), + data.toString(), + ); + 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); this.logger.info( '[创建cron任务],任务ID: %s,cron: %s,任务名: %s,执行命令: %s', @@ -44,39 +109,7 @@ export default class ScheduleService { this.scheduleStacks.set( _id, nodeSchedule.scheduleJob(_id, schedule, async () => { - try { - 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 { - } + await this.runTask(command, callbacks); }), ); } @@ -91,6 +124,7 @@ export default class ScheduleService { { id = 0, command, name = '' }: ScheduleTaskType, schedule: SimpleIntervalSchedule, runImmediately = true, + callbacks?: TaskCallbacks, ) { const _id = this.formatId(id); this.logger.info( @@ -103,34 +137,7 @@ export default class ScheduleService { name, async () => { return new Promise(async (resolve, reject) => { - try { - 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); - } + await this.runTask(command, callbacks); }); }, (err) => { diff --git a/back/services/subscription.ts b/back/services/subscription.ts index 7ec9a3c3..d14eaa77 100644 --- a/back/services/subscription.ts +++ b/back/services/subscription.ts @@ -6,15 +6,25 @@ import { SubscriptionModel, SubscriptionStatus, } from '../data/subscription'; -import { exec, execSync, spawn } from 'child_process'; +import { + ChildProcessWithoutNullStreams, + exec, + execSync, + spawn, +} from 'child_process'; import fs from 'fs'; 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 { promisify } from 'util'; import { Op } from 'sequelize'; import path from 'path'; -import ScheduleService from './schedule'; +import ScheduleService, { TaskCallbacks } from './schedule'; import { SimpleIntervalSchedule } from 'toad-scheduler'; @Service() @@ -97,7 +107,11 @@ export default class SubscriptionService { doc.command = this.formatCommand(doc); if (doc.schedule_type === 'crontab') { this.scheduleService.cancelCronTask(doc as any); - needCreate && this.scheduleService.createCronTask(doc as any); + needCreate && + this.scheduleService.createCronTask( + doc as any, + this.taskCallbacks(doc), + ); } else { this.scheduleService.cancelIntervalTask(doc as any); const { type, value } = doc.interval_schedule as any; @@ -105,10 +119,64 @@ export default class SubscriptionService { this.scheduleService.createIntervalTask( doc as any, { [type]: value } as SimpleIntervalSchedule, + true, + this.taskCallbacks(doc), ); } } + private async handleLogPath( + logPath: string, + data: string = '', + ): Promise { + 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 { const tab = new Subscription(payload); const doc = await this.insert(tab); @@ -195,17 +263,14 @@ export default class SubscriptionService { this.handleTask(doc, false); const command = this.formatCommand(doc); const err = await this.killTask(command); - const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); - const logFileExist = doc.log_path && (await fileExist(absolutePath)); - if (logFileExist) { - const str = err ? `\n${err}` : ''; - fs.appendFileSync( - `${absolutePath}`, - `${str}\n## 执行结束... ${new Date() - .toLocaleString('zh', { hour12: false }) - .replace(' 24:', ' 00:')} `, - ); - } + const absolutePath = await this.handleLogPath(doc.log_path as string); + const str = err ? `\n${err}` : ''; + fs.appendFileSync( + `${absolutePath}`, + `${str}\n## 执行结束... ${new Date() + .toLocaleString('zh', { hour12: false }) + .replace(' 24:', ' 00:')} `, + ); } await SubscriptionModel.update( @@ -249,57 +314,18 @@ export default class SubscriptionService { } } - private async runSingle(cronId: number): Promise { - return new Promise(async (resolve: any) => { - const cron = await this.getDb({ id: cronId }); - if (cron.status !== SubscriptionStatus.queued) { - resolve(); - return; - } + private async runSingle(subscriptionId: number) { + const subscription = await this.getDb({ id: subscriptionId }); + if (subscription.status !== SubscriptionStatus.queued) { + return; + } - let { id, log_path, name } = cron; - const command = this.formatCommand(cron); - const absolutePath = path.resolve(config.logPath, `${log_path}`); - const logFileExist = log_path && (await fileExist(absolutePath)); + const command = this.formatCommand(subscription); - this.logger.silly('Running job' + name); - this.logger.silly('ID: ' + id); - this.logger.silly('Original command: ' + command); - - 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(); - }); - }); + await this.scheduleService.runTask( + command, + this.taskCallbacks(subscription), + ); } public async disabled(ids: number[]) { @@ -324,11 +350,8 @@ export default class SubscriptionService { return ''; } - const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); - const logFileExist = doc.log_path && (await fileExist(absolutePath)); - if (logFileExist) { - return getFileContentByName(`${absolutePath}`); - } + const absolutePath = await this.handleLogPath(doc.log_path as string); + return getFileContentByName(absolutePath); } public async logs(id: number) { diff --git a/package.json b/package.json index aa6631f3..9b9f2de7 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "chokidar": "^3.5.3", "cors": "^2.8.5", "cron-parser": "^4.2.1", + "dayjs": "^1.11.2", "dotenv": "^16.0.0", "express": "^4.17.3", "express-jwt": "^6.1.1", diff --git a/shell/update.sh b/shell/update.sh index 6b8e8afd..1e8287b7 100755 --- a/shell/update.sh +++ b/shell/update.sh @@ -478,11 +478,8 @@ main() { repo) get_user_info 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 - update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path + update_repo "$p2" "$p3" "$p4" "$p5" "$p6" else echo -e "命令输入错误...\n" usage @@ -491,11 +488,8 @@ main() { raw) get_user_info 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 - update_raw "$p2" >>$log_path + update_raw "$p2" else echo -e "命令输入错误...\n" usage diff --git a/src/pages/subscription/index.tsx b/src/pages/subscription/index.tsx index dada00e4..b0f86d60 100644 --- a/src/pages/subscription/index.tsx +++ b/src/pages/subscription/index.tsx @@ -477,6 +477,13 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => { : 'subscription'; }; + useEffect(() => { + if (logSubscription) { + localStorage.setItem('logSubscription', logSubscription.id); + setIsLogModalVisible(true); + } + }, [logSubscription]); + useEffect(() => { getSubscriptions(); }, [searchText]); @@ -541,7 +548,7 @@ const Subscription = ({ headerStyle, isPhone, theme }: any) => { handleCancel={() => { setIsLogModalVisible(false); }} - cron={logSubscription} + subscription={logSubscription} /> ); diff --git a/src/pages/subscription/logModal.tsx b/src/pages/subscription/logModal.tsx index 338793c8..37dd589c 100644 --- a/src/pages/subscription/logModal.tsx +++ b/src/pages/subscription/logModal.tsx @@ -8,22 +8,14 @@ import { } from '@ant-design/icons'; import { PageLoading } from '@ant-design/pro-layout'; -enum CrontabStatus { - 'running', - 'idle', - 'disabled', - 'queued', -} -const { Countdown } = Statistic; - const SubscriptionLogModal = ({ - cron, + subscription, handleCancel, visible, data, logUrl, }: { - cron?: any; + subscription?: any; visible: boolean; handleCancel: () => void; data?: string; @@ -33,49 +25,29 @@ const SubscriptionLogModal = ({ const [loading, setLoading] = useState(true); const [executing, setExecuting] = useState(true); const [isPhone, setIsPhone] = useState(false); - const [theme, setTheme] = useState(''); const getCronLog = (isFirst?: boolean) => { if (isFirst) { setLoading(true); } request - .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`) + .get( + logUrl + ? logUrl + : `${config.apiPrefix}subscriptions/${subscription.id}/log`, + ) .then((data: any) => { - if (localStorage.getItem('logCron') === String(cron.id)) { + if ( + localStorage.getItem('logSubscription') === String(subscription.id) + ) { const log = data.data as string; setValue(log || '暂无日志'); - setExecuting( - log && !log.includes('执行结束') && !log.includes('重启面板'), - ); - if (log && !log.includes('执行结束') && !log.includes('重启面板')) { + setExecuting(log && !log.includes('执行结束')); + if (log && !log.includes('执行结束')) { setTimeout(() => { getCronLog(); }, 2000); } - if ( - log && - log.includes('重启面板') && - cron.status === CrontabStatus.running - ) { - message.warning({ - content: ( - - 系统将在 - - 秒后自动刷新 - - ), - duration: 10, - }); - setTimeout(() => { - window.location.reload(); - }, 30000); - } } }) .finally(() => { @@ -86,7 +58,7 @@ const SubscriptionLogModal = ({ }; const cancel = () => { - localStorage.removeItem('logCron'); + localStorage.removeItem('logSubscription'); handleCancel(); }; @@ -95,16 +67,18 @@ const SubscriptionLogModal = ({ <> {(executing || loading) && } {!executing && !loading && } - 日志-{cron && cron.name}{' '} + + 日志-{subscription && subscription.name} + {' '} ); }; useEffect(() => { - if (cron && cron.id && visible) { + if (subscription && subscription.id && visible) { getCronLog(true); } - }, [cron, visible]); + }, [subscription, visible]); useEffect(() => { if (data) {