diff --git a/back/api/script.ts b/back/api/script.ts index 372c78d1..d13e4245 100644 --- a/back/api/script.ts +++ b/back/api/script.ts @@ -170,7 +170,7 @@ export default (app: Router) => { body: Joi.object({ filename: Joi.string().required(), path: Joi.string().allow(''), - type: Joi.string().optional() + type: Joi.string().optional(), }), }), async (req: Request, res: Response, next: NextFunction) => { @@ -183,7 +183,7 @@ export default (app: Router) => { }; const filePath = join(config.scriptPath, path, filename); if (type === 'directory') { - emptyDir(filePath); + emptyDir(filePath); } else { fs.unlinkSync(filePath); } @@ -255,19 +255,19 @@ export default (app: Router) => { celebrate({ body: Joi.object({ filename: Joi.string().required(), - content: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''), + pid: Joi.number().optional().allow(''), }), }), async (req: Request, res: Response, next: NextFunction) => { const logger: Logger = Container.get('logger'); try { - let { filename, content, path } = req.body; + let { filename, path, pid } = req.body; const { name, ext } = parse(filename); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const scriptService = Container.get(ScriptService); - const result = await scriptService.stopScript(filePath); + const result = await scriptService.stopScript(filePath, pid); res.send(result); } catch (e) { return next(e); diff --git a/back/config/util.ts b/back/config/util.ts index e3fc0271..dc908398 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -4,6 +4,8 @@ import got from 'got'; import iconv from 'iconv-lite'; import { exec } from 'child_process'; import FormData from 'form-data'; +import psTreeFun from 'pstree.remy'; +import { promisify } from 'util'; export function getFileContentByName(fileName: string) { if (fs.existsSync(fileName)) { @@ -287,15 +289,15 @@ enum FileType { interface IFile { title: string; key: string; - type: 'directory' | 'file', + type: 'directory' | 'file'; parent: string; mtime: number; - children?: IFile[], + children?: IFile[]; } export function dirSort(a: IFile, b: IFile) { - if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1 - else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1 + if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1; + else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1; } export function readDirs( @@ -452,3 +454,30 @@ export function parseBody( return parsed; } + +export function psTree(pid: number): Promise { + return new Promise((resolve, reject) => { + psTreeFun(pid, (err: any, pids: number[]) => { + if (err) { + reject(err); + } + resolve(pids.filter((x) => !isNaN(x))); + }); + }); +} + +export async function killTask(pid: number): Promise { + const pids = await psTree(pid); + if (pids.length) { + process.kill(pids[0], 2); + } else { + process.kill(pid, 2); + } +} + +export async function getPid(name: string) { + let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`; + const execAsync = promisify(exec); + let pid = (await execAsync(taskCommand)).stdout; + return Number(pid); +} diff --git a/back/services/cron.ts b/back/services/cron.ts index 9f7e34fe..105ce646 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -5,13 +5,16 @@ import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; import { 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, + killTask, +} from '../config/util'; import { promises, existsSync } from 'fs'; import { promisify } from 'util'; import { Op } from 'sequelize'; import path from 'path'; -import dayjs from 'dayjs'; -import { LOG_END_SYMBOL } from '../config/const'; @Service() export default class CronService { @@ -315,31 +318,11 @@ export default class CronService { for (const doc of docs) { if (doc.pid) { try { - process.kill(-doc.pid); + await killTask(doc.pid); } catch (error) { this.logger.silly(error); } } - const err = await this.killTask(doc.command); - const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); - const logFileExist = doc.log_path && (await fileExist(absolutePath)); - - const endTime = dayjs(); - const diffTimeStr = doc.last_execution_time - ? ` 耗时 ${endTime.diff( - dayjs(doc.last_execution_time * 1000), - 'second', - )} 秒` - : ''; - if (logFileExist) { - const str = err ? `\n${err}` : ''; - fs.appendFileSync( - `${absolutePath}`, - `${str}\n## 执行结束... ${endTime.format( - 'YYYY-MM-DD HH:mm:ss', - )}${diffTimeStr}${LOG_END_SYMBOL}`, - ); - } } await CrontabModel.update( @@ -348,42 +331,6 @@ export default class CronService { ); } - public async killTask(name: string) { - let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`; - const execAsync = promisify(exec); - try { - let pid = (await execAsync(taskCommand)).stdout; - if (pid) { - pid = (await execAsync(`pstree -p ${pid}`)).stdout; - } else { - return; - } - let pids = pid.match(/\(\d+/g); - const killLogs = []; - if (pids && pids.length > 0) { - // node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个 - pids = pids.slice(0, 3); - for (const id of pids) { - const c = `kill -9 ${id.slice(1)}`; - try { - const { stdout, stderr } = await execAsync(c); - if (stderr) { - killLogs.push(stderr); - } - if (stdout) { - killLogs.push(stdout); - } - } catch (error: any) { - killLogs.push(error.message); - } - } - } - return killLogs.length > 0 ? JSON.stringify(killLogs) : ''; - } catch (e) { - return JSON.stringify(e); - } - } - private async runSingle(cronId: number): Promise { return new Promise(async (resolve: any) => { const cron = await this.getDb({ id: cronId }); diff --git a/back/services/schedule.ts b/back/services/schedule.ts index 0703a7ee..7bab03d6 100644 --- a/back/services/schedule.ts +++ b/back/services/schedule.ts @@ -44,12 +44,11 @@ export default class ScheduleService { async runTask(command: string, callbacks: TaskCallbacks = {}) { return new Promise(async (resolve, reject) => { + const startTime = dayjs(); + await callbacks.onBefore?.(startTime); + + const cp = spawn(command, { shell: '/bin/bash' }); try { - const startTime = dayjs(); - await callbacks.onBefore?.(startTime); - - const cp = spawn(command, { shell: '/bin/bash' }); - // TODO: callbacks.onStart?.(cp, startTime); @@ -100,8 +99,8 @@ export default class ScheduleService { error, ); await callbacks.onError?.(JSON.stringify(error)); - resolve(null); } + resolve(cp.pid); }); } diff --git a/back/services/script.ts b/back/services/script.ts index 74178a4e..e486b7a5 100644 --- a/back/services/script.ts +++ b/back/services/script.ts @@ -7,6 +7,7 @@ import CronService from './cron'; import ScheduleService, { TaskCallbacks } from './schedule'; import config from '../config'; import { LOG_END_SYMBOL } from '../config/const'; +import { getPid, killTask } from '../config/util'; @Service() export default class ScriptService { @@ -42,16 +43,24 @@ export default class ScriptService { public async runScript(filePath: string) { const relativePath = path.relative(config.scriptPath, filePath); const command = `task -l ${relativePath} now`; - this.scheduleService.runTask(command, this.taskCallbacks(filePath)); + const pid = this.scheduleService.runTask( + command, + this.taskCallbacks(filePath), + ); - return { code: 200 }; + return { code: 200, data: pid }; } - public async stopScript(filePath: string) { - const relativePath = path.relative(config.scriptPath, filePath); - const err = await this.cronService.killTask(`task -l ${relativePath} now`); + public async stopScript(filePath: string, pid: number) { + let str = ''; + if (!pid) { + const relativePath = path.relative(config.scriptPath, filePath); + pid = await getPid(`task -l ${relativePath} now`); + } + try { + await killTask(pid); + } catch (error) {} - const str = err ? `\n${err}` : ''; this.sockService.sendMessage({ type: 'manuallyRunScript', message: `${str}\n## 执行结束... ${new Date() diff --git a/back/services/subscription.ts b/back/services/subscription.ts index 99d7c31d..3a17a90e 100644 --- a/back/services/subscription.ts +++ b/back/services/subscription.ts @@ -19,9 +19,9 @@ import { concurrentRun, fileExist, createFile, + killTask, } from '../config/util'; import { promises, existsSync } from 'fs'; -import { promisify } from 'util'; import { Op } from 'sequelize'; import path from 'path'; import ScheduleService, { TaskCallbacks } from './schedule'; @@ -351,19 +351,16 @@ export default class SubscriptionService { for (const doc of docs) { if (doc.pid) { try { - process.kill(-doc.pid); + await killTask(doc.pid); } catch (error) { this.logger.silly(error); } } - const command = this.formatCommand(doc); - const err = await this.killTask(command); const absolutePath = await this.handleLogPath(doc.log_path as string); - const str = err ? `\n${err}` : ''; fs.appendFileSync( `${absolutePath}`, - `${str}\n## 执行结束... ${dayjs().format( + `\n## 执行结束... ${dayjs().format( 'YYYY-MM-DD HH:mm:ss', )}${LOG_END_SYMBOL}`, ); @@ -375,41 +372,6 @@ export default class SubscriptionService { ); } - public async killTask(name: string) { - let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`; - const execAsync = promisify(exec); - try { - let pid = (await execAsync(taskCommand)).stdout; - if (pid) { - pid = (await execAsync(`pstree -p ${pid}`)).stdout; - } else { - return; - } - let pids = pid.match(/\(\d+/g); - const killLogs = []; - if (pids && pids.length > 0) { - // node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个 - for (const id of pids) { - const c = `kill -9 ${id.slice(1)}`; - try { - const { stdout, stderr } = await execAsync(c); - if (stderr) { - killLogs.push(stderr); - } - if (stdout) { - killLogs.push(stdout); - } - } catch (error: any) { - killLogs.push(error.message); - } - } - } - return killLogs.length > 0 ? JSON.stringify(killLogs) : ''; - } catch (e) { - return JSON.stringify(e); - } - } - private async runSingle(subscriptionId: number) { const subscription = await this.getDb({ id: subscriptionId }); if (subscription.status !== SubscriptionStatus.queued) { diff --git a/package.json b/package.json index feb624b3..d67d92f1 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "nedb": "^1.8.0", "node-schedule": "^2.1.0", "nodemailer": "^6.7.2", + "pstree.remy": "^1.1.8", "reflect-metadata": "^0.1.13", "sequelize": "^6.25.5", "serve-handler": "^6.1.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d46748d..9b346847 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,7 @@ specifiers: nodemailer: ^6.7.2 nodemon: ^2.0.15 prettier: ^2.5.1 + pstree.remy: ^1.1.8 qiniu: ^7.4.0 qrcode.react: ^1.0.1 query-string: ^7.1.1 @@ -112,6 +113,7 @@ dependencies: nedb: 1.8.0 node-schedule: 2.1.0 nodemailer: 6.7.8 + pstree.remy: 1.1.8 reflect-metadata: 0.1.13 sequelize: 6.25.6_@louislam+sqlite3@15.0.6 serve-handler: 6.1.3 @@ -10352,7 +10354,6 @@ packages: /pstree.remy/1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} - dev: true /public-encrypt/4.0.3: resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} diff --git a/src/pages/script/editModal.tsx b/src/pages/script/editModal.tsx index d91beebd..1349c6a3 100644 --- a/src/pages/script/editModal.tsx +++ b/src/pages/script/editModal.tsx @@ -49,6 +49,7 @@ const EditModal = ({ const { theme } = useTheme(); const editorRef = useRef(null); const [isRunning, setIsRunning] = useState(false); + const [currentPid, setCurrentPid] = useState(null); const cancel = () => { handleCancel(); @@ -94,6 +95,7 @@ const EditModal = ({ .then(({ code, data }) => { if (code === 200) { setIsRunning(true); + setCurrentPid(data); } }); }; @@ -102,13 +104,12 @@ const EditModal = ({ if (!cNode || !cNode.title) { return; } - const content = editorRef.current.getValue().replace(/\r\n/g, '\n'); request .put(`${config.apiPrefix}scripts/stop`, { data: { filename: cNode.title, path: cNode.parent || '', - content, + pid: currentPid, }, }) .then(({ code, data }) => { diff --git a/typings.d.ts b/typings.d.ts index 06c8a5b8..7ccfa2d4 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -8,3 +8,5 @@ declare module '*.svg' { const url: string; export default url; } + +declare module 'pstree.remy';