From 29b784e76c6040e619eac084c2d14fd365d799fc Mon Sep 17 00:00:00 2001 From: helson Date: Mon, 20 Mar 2023 22:35:04 +0800 Subject: [PATCH 1/3] fix: auto detect dead ffmpeg process and kill it --- .DS_Store | Bin 0 -> 6148 bytes bin/app.js | 4 +- bin/utils.js | 350 ------------------------------------------- bin/utils/config.js | 77 ++++++++++ bin/utils/core.js | 159 ++++++++++++++++++++ bin/utils/env.js | 76 ++++++++++ bin/utils/index.js | 10 ++ bin/utils/message.js | 103 +++++++++++++ bin/utils/process.js | 91 +++++++++++ config/config.yml | 4 +- config2/config.yml | 7 + index.js | 17 ++- package.json | 5 +- pnpm-lock.yaml | 27 +++- 14 files changed, 567 insertions(+), 363 deletions(-) create mode 100644 .DS_Store delete mode 100644 bin/utils.js create mode 100644 bin/utils/config.js create mode 100644 bin/utils/core.js create mode 100644 bin/utils/env.js create mode 100644 bin/utils/index.js create mode 100644 bin/utils/message.js create mode 100644 bin/utils/process.js create mode 100644 config2/config.yml diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..594ddc5d6ecc1fe1e4af38d6c6b195a1dc7ec620 GIT binary patch literal 6148 zcmeHKOH0E*5Z<+|O({YS3Oz1(Em&)P6fYsxKVU=;Dm5WNgK4((VGgB`v;HA}iND90 z-3_!DJc`&E*!^bbv77lI`(TW5cOD)xW;4btXowt@5dKD)`RyfGjHb4=2O?7+~Me2$|NXs zKe&mbY-sOZ$aLn%X*5v@aTr0!{cW6vGIQlT4W}yC(+;a)HHLO;u{b;Kw#8BZblDb* zez(&Wy_4Q@*|7HZ56-WKFUcsCZ<vjtqyjf!WnloQcnyJ1M3V_^w7rh{}O(g z(ntP!3XO;XV&I=Kz^emq;6hR6Z2eXqp0xtnBQz9@%TWOVeeDte1KdYi%BlSVb%=8e W<{EJpw5xPLx(Fyjs3Qh`fq^fBBucaZ literal 0 HcmV?d00001 diff --git a/bin/app.js b/bin/app.js index d316f68..bebd902 100644 --- a/bin/app.js +++ b/bin/app.js @@ -6,7 +6,7 @@ const colors = require('colors') const bodyParser = require('body-parser') const logger = require('./log') const app = express() -const { download, getNetwork } = require('./utils.js') +const { download, getNetwork } = require('./utils/index') app.use(express.static(path.join(__dirname, '../public'))) const jsonParser = bodyParser.json() @@ -15,7 +15,7 @@ const createServer = (option) => { app.post('/down', jsonParser, (req, res) => { const { name, url } = req.body const filePath = path.join(option.downloadDir, (name || new Date().getTime()) + '.mp4') - logger.info(`file download path: ${filePath}`) + logger.info(`online m3u8 url: ${url}, file download path: ${filePath}`) if (!url) { res.send('{"code": 2, "message":"url cant be null"}') } else { diff --git a/bin/utils.js b/bin/utils.js deleted file mode 100644 index b975949..0000000 --- a/bin/utils.js +++ /dev/null @@ -1,350 +0,0 @@ - -const path = require('path') -const YAML = require('yamljs') -const json2yaml = require('js-yaml') -const colors = require('colors') -const request = require('request') -const fs = require('fs') -const fse = require('fs-extra') -const si = require('systeminformation') -const childProcess = require('child_process') -const DOWNLOADZIP = require('download') -const os = require('os') -const m3u8ToMp4 = require('./m3u8') -const converter = new m3u8ToMp4() -const logger = require('./log') -const cpuNum = os.cpus().length -// this is a temporary file used to store downloaded files, https://nn.oimi.space/ is a cfworker -const GITHUBURL = 'https://nn.oimi.space/https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.4.1' -/** - * @description: find config.yaml location - * @return {string} - */ -const getConfigPath = () => { - const configPathList = [ - path.join(process.cwd(), './config/config.yml'), - path.join(process.cwd(), '../config/config.yml')] - return configPathList.find(_path => fse.pathExistsSync(_path)) -} - -const getNetwork = () => { - return new Promise((resolve, reject) => { - si.networkInterfaces().then(data => { - const list = data.filter(i => i.ip4).map(i => i.ip4) - resolve(list || []) - }).catch(error => { - reject(error) - }) - }) -} - -/** - * @description create yml option file - * @date 3/14/2023 - 6:01:54 PM - * @param {object} obj - */ -const createYml = (obj) => { - const yamlString = json2yaml.dump(obj, { lineWidth: -1 }) - const filePath = path.join(process.cwd(), './config/config.yml') - fse.outputFileSync(filePath, yamlString) -} - -/** - * @description: make sure download directory exists - * @param {string} _path configuration path - * @return {string} real download directory - */ -const EnsureDonwloadPath = (_path) => { - if (_path.startsWith('@')) { - const relPath = _path.replace('@', '') - fse.ensureDirSync(relPath) - return relPath - } - const relPath = path.join(process.cwd(), _path) - fse.ensureDirSync(relPath) - return relPath -} - -/** - * @description: read configuration file and return configuration - * @return {object} configuration object - */ -const readConfig = (option = { - port: 8081, - downloadDir: path.join(process.cwd(), 'media'), - webhooks: '', - webhookType: 'bark', - thread: false, - downloadThread: true, - useFFmpegLib: true }) => { - const configPath = getConfigPath() - if (!configPath) { - logger.info('not found config file, auto create config.yml') - // make sure download dir is exists - EnsureDonwloadPath('/media/') - createYml({ ...option, downloadDir: '/media/' }) - } else { - const data = YAML.parse(fs.readFileSync(configPath).toString()) - const { port, downloadDir, webhooks, webhookType, thread, useFFmpegLib, downloadThread } = data - if (port) option.port = port - if (downloadDir) option.downloadDir = EnsureDonwloadPath(downloadDir) - if (webhooks) option.webhooks = webhooks - if (webhookType) option.webhookType = webhookType - if (thread !== undefined) option.thread = thread - if (downloadThread !== undefined) option.downloadThread = downloadThread - if (useFFmpegLib !== undefined) option.useFFmpegLib = useFFmpegLib - } - return option -} -/** - * @description: generate feishu hooks request body - * @param {string} text title - * @param {string} More body - * @return {object} Request body - */ -const getFeiShuBody = (text, More) => { - const content = [] - if (text) { - content.push([{ - tag: 'text', - text: `${text}`, - }]) - } - if (More) { - content.push([{ - tag: 'text', - text: `${More}`, - }]) - } - return { - msg_type: 'post', - content: { - post: { - zh_cn: { - title: '文件下载通知', - content, - }, - }, - }, - } -} - -const getDingDingBody = (text, More) => { - const obj = { - msgtype: 'text', - text: { - content: `文件下载通知: \n ${text} ${More || ''}`, - }, - at: { - isAtAll: true, - }, - } - return obj -} - -/** - * @description handler url contains unescaped characters - * @date 3/16/2023 - 11:46:23 AM - * @param {string} url - * @returns {*} - */ -const handlerURL = (url) => { - const cnList = Array.from(url.matchAll(/[\u4e00-\u9fa5]+/g)) - for (let match of cnList) { - url = url.replace(match[0], encodeURIComponent(match[0])) - } - return url -} - -/** - * @description get bark request url - * @date 3/16/2023 - 11:45:35 AM - * @param {string} url bark URL - * @param {string} text video name - * @returns {*} - */ -const getBarkUrl = (url, text) => handlerURL(String(url).replace(/\$TEXT/g, text)) - -/** - * @description: judege input path is a directory - * @param {string} pathDir path - * @return {boolean} true: path is a file - */ -const isFile = (pathDir) => fse.pathExistsSync(pathDir) -/** - * @description: send message notice to user - * @param {string} url hooks url - * @param {string} type hooks type - * @param {string} Text title text - * @param {string} More body text - * @return {void} - */ -const msg = (url, type, Text, More) => { - const URL = type === 'bark' ? getBarkUrl(url, Text) : url - const method = type === 'bark' ? 'GET' : 'POST' - const bodyHanler = { bark: () => ({}), feishu: getFeiShuBody, dingding: getDingDingBody } - const data = bodyHanler[type](Text, More) - request({ - url: URL, - method, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }, (error, _, body) => { - if (error) { - logger.error(error + '') - } - if (body) { - logger.info('notification success !') - } - }) -} - -/** - * @description exec command - * @date 3/16/2023 - 11:52:03 AM - * @param {string} cmd - * @returns {*} - */ -const execCmd = (cmd) => { - return new Promise((resolve, reject) => { - childProcess.exec( - cmd, - (error) => { - if (error) { - logger.warn(error) - reject(error) - } else { - resolve() - } - }, - ) - }) -} - -/** - * @description File authorization - * @date 3/16/2023 - 11:52:33 AM - * @param {string} file - */ -const chmod = (file) => { - // if(process.platform !== 'linux' || process.platform !== 'darwin') return - const cmd = `chmod +x ${file}` - execCmd(cmd) -} - -/** - * @description auto donwload ffmpeg file - * @date 3/16/2023 - 11:53:57 AM - * @async - * @param {string} type - * @returns {Promise} - */ -const downloadFfmpeg = async (type) => { - const typeLink = { - win32: 'ffmpeg-4.4.1-win-64', - darwin: 'ffmpeg-4.4.1-osx-64', - 'linux-x64': 'ffmpeg-4.4.1-linux-64', - 'linux-arm64': 'ffmpeg-4.4.1-linux-arm-64', - 'linux-amd64': 'ffmpeg-4.4.1-linux-armel-32', - } - const suffix = typeLink[type] - const executableFileSuffix = typeLink[type].startsWith('win') ? 'ffmpeg.exe' : 'ffmpeg' - const libPath = path.join(process.cwd(), `lib/${executableFileSuffix}`) - const isExist = isFile(libPath) - if (isExist) { - return Promise.resolve(libPath) - } - // judge file is exists - if (!suffix) { - console.log(colors.italic.red('[ffdown] can\'t auto download ffmpeg \n')) - return Promise.reject(new Error('can\'t download ffmpeg')) - } - try { - console.log(colors.italic.green('[ffdown] downloading ffmpeg:' + `${GITHUBURL}/${suffix}.zip`)) - await DOWNLOADZIP(`${GITHUBURL}/${suffix}.zip`, 'lib', { extract: true }) - chmod(libPath) - return Promise.resolve(libPath) - } catch (e) { - return Promise.reject(e) - } -} - -/** - * @description: download m3u8 video to local storage - * @param {string} url m3u8 url - * @param {string} name fielName - * @param {string} filePath file output path - * @param {string} webhooks webhooks url - * @param {string} webhookType webhooks type - * @return {Promise} - */ -const download = (url, name, filePath, { webhooks, webhookType, downloadThread }) => { - return new Promise((resolve, reject) => { - converter - .setInputFile(url) - .setThreads(downloadThread ? cpuNum : 0) - .setOutputFile(filePath) - .start() - .then(res => { - if (webhooks) { - console.log('下载成功:' + name) - msg(webhooks, webhookType, `${name}.mp4 下载成功`) - } - resolve() - }).catch(err => { - console.log('下载失败', webhooks) - console.log('下载失败:' + err) - if (webhooks) { - msg(webhooks, webhookType, `${name}.mp4 下载失败`, err + '') - } - reject(err) - }) - }) -} - -/** - * @description: find ffmpeg executable file path - * @param {string} suffixPath - * @return {string} ffmpeg path - */ -const FFMPEGPath = (suffixPath) => { - const cwdPath = process.cwd() + suffixPath - const cdPath = path.join(process.cwd(), '..' + suffixPath) - try { - return isFile(cwdPath) ? cwdPath : cdPath - } catch (e) { - console.log(e) - } -} - -/** - * @description: setting ffmpeg environment variables - * @return {void} - */ -const setFfmpegEnv = async () => { - const platform = process.platform - const arch = process.arch - const type = platform + (platform === 'linux' ? `-${arch}` : '') - let baseURL = '' - try { - baseURL = await downloadFfmpeg(type) - process.env.FFMPEG_PATH = baseURL - logger.info('Setting FFMPEG_PATH:' + baseURL) - if (process.env.FFMPEG_PATH !== baseURL) { - console.log(colors.italic.cyan('[ffdown] ffmpeg: 环境变量设置成功')) - } - } catch (e) { - console.log('download Failed', e) - } -} - -module.exports = { - readConfig, - download, - msg, - setFfmpegEnv, - FFMPEGPath, - getNetwork, -} \ No newline at end of file diff --git a/bin/utils/config.js b/bin/utils/config.js new file mode 100644 index 0000000..77a3297 --- /dev/null +++ b/bin/utils/config.js @@ -0,0 +1,77 @@ +const path = require('path') +const fs = require('fs') +const fse = require('fs-extra') +const YAML = require('yamljs') +const json2yaml = require('js-yaml') +const logger = require('../log') +/** + * @description: find config.yaml location + * @return {string} + */ +const getConfigPath = () => { + const configPathList = [ + path.join(process.cwd(), './config/config.yml'), + path.join(process.cwd(), '../config/config.yml')] + return configPathList.find(_path => fse.pathExistsSync(_path)) +} + +/** + * @description create yml option file + * @date 3/14/2023 - 6:01:54 PM + * @param {object} obj + */ +const createYml = (obj) => { + const yamlString = json2yaml.dump(obj, { lineWidth: -1 }) + const filePath = path.join(process.cwd(), './config/config.yml') + fse.outputFileSync(filePath, yamlString) +} + +/** + * @description: make sure download directory exists + * @param {string} _path configuration path + * @return {string} real download directory + */ +const EnsureDonwloadPath = (_path) => { + if (_path.startsWith('@')) { + const relPath = _path.replace('@', '') + fse.ensureDirSync(relPath) + return relPath + } + const relPath = path.join(process.cwd(), _path) + fse.ensureDirSync(relPath) + return relPath +} + +/** + * @description: read configuration file and return configuration + * @return {object} configuration object + */ +const readConfig = (option = { + port: 8081, + downloadDir: path.join(process.cwd(), 'media'), + webhooks: '', + webhookType: 'bark', + thread: false, + downloadThread: true, + useFFmpegLib: true }) => { + const configPath = getConfigPath() + if (!configPath) { + logger.info('not found config file, auto create config.yml') + // make sure download dir is exists + EnsureDonwloadPath('/media/') + createYml({ ...option, downloadDir: '/media/' }) + } else { + const data = YAML.parse(fs.readFileSync(configPath).toString()) + const { port, downloadDir, webhooks, webhookType, thread, useFFmpegLib, downloadThread } = data + if (port) option.port = port + if (downloadDir) option.downloadDir = EnsureDonwloadPath(downloadDir) + if (webhooks) option.webhooks = webhooks + if (webhookType) option.webhookType = webhookType + if (thread !== undefined) option.thread = thread + if (downloadThread !== undefined) option.downloadThread = downloadThread + if (useFFmpegLib !== undefined) option.useFFmpegLib = useFFmpegLib + } + return option +} + +module.exports = { readConfig } \ No newline at end of file diff --git a/bin/utils/core.js b/bin/utils/core.js new file mode 100644 index 0000000..4e9c79a --- /dev/null +++ b/bin/utils/core.js @@ -0,0 +1,159 @@ +/** process operation、file download、cmd exec */ +const childProcess = require('child_process') +const process = require('process') +const pidusage = require('pidusage') +const si = require('systeminformation') +const os = require('os') +const m3u8ToMp4 = require('../m3u8') +const logger = require('../log') +const { msg } = require('./message') +const converter = new m3u8ToMp4() +const cpuNum = os.cpus().length + +const KILLPROCEETIMEOUT = 3000 // 300000 +/** + * @description kill a process by pid + * @param {string} pid process pid + */ +const killPidAtLowusage = (pid) => { + pidusage(pid, function (err, result) { + if (err) { + console.log('get pid usage error:' + err) + } else if (result.cpu + '' === '0') { + console.log('CPU usage is too low, killing process:' + pid, result.cpu) + logger.warn('CPU usage is too low, killing process:' + pid, result.cpu) + process.kill(pid) + } else { + console.log('FFMPEG PID:' + pid + ', CPU:' + result.cpu) + } + }) +} +/** + * @description query process pid by keywords + * @param {string} query + * @param {function} cb callback + */ +const getProcessPidByQuery = (query, cb) => { + let platform = process.platform + let cmd = '' + switch (platform) { + case 'win32': + cmd = 'tasklist' + break + case 'darwin': + cmd = `ps -ax | grep ${query}` + break + case 'linux': + cmd = 'ps -A' + break + default: + break + } + childProcess.exec(cmd, (err, stdout, stderr) => { + if (err) { + console.log('Exec findProcess error:' + err) + } + if (stdout) { + const list = stdout + .split(/[\r\n\t]/) + .filter((i) => i && i.indexOf('grep') === -1) + const queryList = list + .filter((i) => i.includes(query)) + .map((string) => string.match(/\d+/)[0]) + cb(queryList) + } + }) +} +/** + * @description kill death ffmpeg process + */ +const killToDeathFfmeg = () => { + const ffmpegKill = () => getProcessPidByQuery('ffmpeg -i', list => list.forEach((i) => killPidAtLowusage(i))) + ffmpegKill() + setInterval(() => { + ffmpegKill() + }, KILLPROCEETIMEOUT) +} + +/** + * @description exec command + * @date 3/16/2023 - 11:52:03 AM + * @param {string} cmd + * @returns {*} + */ +const execCmd = (cmd) => { + return new Promise((resolve, reject) => { + childProcess.exec( + cmd, + (error) => { + if (error) { + logger.warn(error) + reject(error) + } else { + resolve() + } + }, + ) + }) +} + +/** + * @description File authorization + * @date 3/16/2023 - 11:52:33 AM + * @param {string} file + */ +const chmod = (file) => { + // if(process.platform !== 'linux' || process.platform !== 'darwin') return + const cmd = `chmod +x ${file}` + execCmd(cmd) +} + +/** + * @description get all network interface ip + * @returns {Array} list + */ +const getNetwork = () => { + return new Promise((resolve, reject) => { + si.networkInterfaces().then(data => { + const list = data.filter(i => i.ip4).map(i => i.ip4) + resolve(list || []) + }).catch(error => { + reject(error) + }) + }) +} + +/** + * @description: download m3u8 video to local storage + * @param {string} url m3u8 url + * @param {string} name fielName + * @param {string} filePath file output path + * @param {string} webhooks webhooks url + * @param {string} webhookType webhooks type + * @return {Promise} + */ +const download = (url, name, filePath, { webhooks, webhookType, downloadThread }) => { + return new Promise((resolve, reject) => { + converter + .setInputFile(url) + .setThreads(downloadThread ? cpuNum : 0) + .setOutputFile(filePath) + .start() + .then(res => { + if (webhooks) { + console.log('下载成功:' + name) + msg(webhooks, webhookType, `${name}.mp4 下载成功`) + } + resolve() + }).catch(err => { + console.log('下载失败', webhooks) + console.log('下载失败:' + err) + if (webhooks) { + msg(webhooks, webhookType, `${name}.mp4 下载失败`, err + '') + } + reject(err) + }) + }) +} + +module.exports = { killToDeathFfmeg, chmod, getNetwork, download } diff --git a/bin/utils/env.js b/bin/utils/env.js new file mode 100644 index 0000000..8f9b896 --- /dev/null +++ b/bin/utils/env.js @@ -0,0 +1,76 @@ +const logger = require('../log') +const colors = require('colors') +const path = require('path') +const DOWNLOADZIP = require('download') +const fse = require('fs-extra') +const { chmod } = require('./core') + +// this is a temporary file used to store downloaded files, https://nn.oimi.space/ is a cfworker +const GITHUBURL = 'https://nn.oimi.space/https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.4.1' + +/** + * @description: judege input path is a directory + * @param {string} pathDir path + * @return {boolean} true: path is a file + */ +const isFile = (pathDir) => fse.pathExistsSync(pathDir) + +/** + * @description auto donwload ffmpeg file + * @date 3/16/2023 - 11:53:57 AM + * @async + * @param {string} type + * @returns {Promise} + */ +const downloadFfmpeg = async (type) => { + const typeLink = { + win32: 'ffmpeg-4.4.1-win-64', + darwin: 'ffmpeg-4.4.1-osx-64', + 'linux-x64': 'ffmpeg-4.4.1-linux-64', + 'linux-arm64': 'ffmpeg-4.4.1-linux-arm-64', + 'linux-amd64': 'ffmpeg-4.4.1-linux-armel-32', + } + const suffix = typeLink[type] + const executableFileSuffix = typeLink[type].startsWith('win') ? 'ffmpeg.exe' : 'ffmpeg' + const libPath = path.join(process.cwd(), `lib/${executableFileSuffix}`) + const isExist = isFile(libPath) + if (isExist) { + return Promise.resolve(libPath) + } + // judge file is exists + if (!suffix) { + console.log(colors.italic.red('[ffdown] can\'t auto download ffmpeg \n')) + return Promise.reject(new Error('can\'t download ffmpeg')) + } + try { + console.log(colors.italic.green('[ffdown] downloading ffmpeg:' + `${GITHUBURL}/${suffix}.zip`)) + await DOWNLOADZIP(`${GITHUBURL}/${suffix}.zip`, 'lib', { extract: true }) + chmod(libPath) + return Promise.resolve(libPath) + } catch (e) { + return Promise.reject(e) + } +} + +/** + * @description: setting ffmpeg environment variables + * @return {void} + */ +const setFfmpegEnv = async () => { + const platform = process.platform + const arch = process.arch + const type = platform + (platform === 'linux' ? `-${arch}` : '') + let baseURL = '' + try { + baseURL = await downloadFfmpeg(type) + process.env.FFMPEG_PATH = baseURL + logger.info('Setting FFMPEG_PATH:' + baseURL) + if (process.env.FFMPEG_PATH !== baseURL) { + console.log(colors.italic.cyan('[ffdown] ffmpeg: 环境变量设置成功')) + } + } catch (e) { + console.log('download Failed', e) + } +} + +module.exports = { setFfmpegEnv } \ No newline at end of file diff --git a/bin/utils/index.js b/bin/utils/index.js new file mode 100644 index 0000000..cbf4a91 --- /dev/null +++ b/bin/utils/index.js @@ -0,0 +1,10 @@ +const CONFIG = require('./config') +const ENV = require('./env') +const CORE = require('./core') +const PROCESS = require('./process') +module.exports = { + ...CONFIG, + ...ENV, + ...CORE, + ...PROCESS, +} \ No newline at end of file diff --git a/bin/utils/message.js b/bin/utils/message.js new file mode 100644 index 0000000..240aefa --- /dev/null +++ b/bin/utils/message.js @@ -0,0 +1,103 @@ +const request = require('request') +const logger = require('../log') + +/** + * @description: generate feishu hooks request body + * @param {string} text title + * @param {string} More body + * @return {object} Request body + */ +const getFeiShuBody = (text, More) => { + const content = [] + if (text) { + content.push([{ + tag: 'text', + text: `${text}`, + }]) + } + if (More) { + content.push([{ + tag: 'text', + text: `${More}`, + }]) + } + return { + msg_type: 'post', + content: { + post: { + zh_cn: { + title: '文件下载通知', + content, + }, + }, + }, + } +} + +const getDingDingBody = (text, More) => { + const obj = { + msgtype: 'text', + text: { + content: `文件下载通知: \n ${text} ${More || ''}`, + }, + at: { + isAtAll: true, + }, + } + return obj +} + +/** + * @description handler url contains unescaped characters + * @date 3/16/2023 - 11:46:23 AM + * @param {string} url + * @returns {*} + */ +const handlerURL = (url) => { + const cnList = Array.from(url.matchAll(/[\u4e00-\u9fa5]+/g)) + for (let match of cnList) { + url = url.replace(match[0], encodeURIComponent(match[0])) + } + return url +} + +/** + * @description get bark request url + * @date 3/16/2023 - 11:45:35 AM + * @param {string} url bark URL + * @param {string} text video name + * @returns {*} + */ +const getBarkUrl = (url, text) => handlerURL(String(url).replace(/\$TEXT/g, text)) + +/** + * @description: send message notice to user + * @param {string} url hooks url + * @param {string} type hooks type + * @param {string} Text title text + * @param {string} More body text + * @return {void} + */ +const msg = (url, type, Text, More) => { + const URL = type === 'bark' ? getBarkUrl(url, Text) : url + const method = type === 'bark' ? 'GET' : 'POST' + const bodyHanler = { bark: () => ({}), feishu: getFeiShuBody, dingding: getDingDingBody } + const data = bodyHanler[type](Text, More) + request({ + url: URL, + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }, (error, _, body) => { + if (error) { + logger.error(error + '') + } + if (body) { + logger.info('notification success !') + } + }) +} + +module.exports = { msg } \ No newline at end of file diff --git a/bin/utils/process.js b/bin/utils/process.js new file mode 100644 index 0000000..9319614 --- /dev/null +++ b/bin/utils/process.js @@ -0,0 +1,91 @@ +/** process operation、file download、cmd exec */ +const childProcess = require('child_process') +const process = require('process') +const pidusage = require('pidusage') +const logger = require('../log') + +const KILLPROCEETIMEOUT = 300000 // 300000 + +class FFmpegKiller { + constructor () { + this.pidCpu = {} + this.timer = null + this.time = KILLPROCEETIMEOUT + } + + killPidAtLowusage (pid) { + const _this = this + pidusage(pid, function (err, result) { + if (err) { + console.log('get pid usage error:' + err) + } else { + if (_this.pidCpu[pid]) { + _this.pidCpu[pid].push(result.cpu) + } else { + _this.pidCpu[pid] = [result.cpu] + } + if (_this.pidCpu[pid].length > 4) { + const isDeadth = _this.pidCpu[pid].slice(-4).join('') === '0000' + if (!isDeadth) return + console.log('CPU usage is too low, killing process:' + pid, result.cpu) + logger.warn('CPU usage is too low, killing process:' + pid, result.cpu) + process.kill(pid) + delete _this.pidCpu[pid] + } + } + }) + } + + getProcessPidByQuery (query, cb) { + let platform = process.platform + let cmd = '' + switch (platform) { + case 'win32': + cmd = 'tasklist' + break + case 'darwin': + cmd = `ps -ax | grep ${query}` + break + case 'linux': + cmd = 'ps -A' + break + default: + break + } + childProcess.exec(cmd, (err, stdout, stderr) => { + if (err) { + console.log('Exec findProcess error:' + err) + } + if (stdout) { + const list = stdout + .split(/[\r\n\t]/) + .filter((i) => i && i.indexOf('grep') === -1) + const queryList = list + .filter((i) => i.includes(query)) + .map((string) => string.match(/\d+/)[0]) + cb(queryList) + } + }) + } + + ffmpegKill () { + this.getProcessPidByQuery('ffmpeg -i', list => list.forEach((i) => this.killPidAtLowusage(i))) + } + + killToDeathFfmeg () { + this.ffmpegKill() + this.timer = setInterval(() => { + this.ffmpegKill() + }, this.time) + } + + clear () { + if (this.timer) { + clearInterval(this.timer) + } + } +} + +const ffmpegKiller = new FFmpegKiller() + +module.exports = { ffmpegKiller } diff --git a/config/config.yml b/config/config.yml index 4b99a62..c1e0f22 100644 --- a/config/config.yml +++ b/config/config.yml @@ -1,7 +1,7 @@ port: 8081 downloadDir: /media/ -webhooks: 'https://oapi.dingtalk.com/robot/send?access_token=b1d3594015d5e2dfa5e0bc35ee2fe37c7b50407f30ce98a8ff0bffab6e66c379' -webhookType: 'dingding' +webhooks: 'https://api.day.app/wmKLMC4xdxL9djWCMc3SWP/文件下载通知/$TEXT' +webhookType: bark thread: false downloadThread: true useFFmpegLib: true diff --git a/config2/config.yml b/config2/config.yml new file mode 100644 index 0000000..4b99a62 --- /dev/null +++ b/config2/config.yml @@ -0,0 +1,7 @@ +port: 8081 +downloadDir: /media/ +webhooks: 'https://oapi.dingtalk.com/robot/send?access_token=b1d3594015d5e2dfa5e0bc35ee2fe37c7b50407f30ce98a8ff0bffab6e66c379' +webhookType: 'dingding' +thread: false +downloadThread: true +useFFmpegLib: true diff --git a/index.js b/index.js index f3f147e..47e93e4 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,9 @@ const createServer = require('./bin/app.js') -const { setFfmpegEnv, readConfig } = require('./bin/utils.js') +const { setFfmpegEnv, readConfig, ffmpegKiller } = require('./bin/utils/index') const cluster = require('cluster') const os = require('os') const colors = require('colors') +const process = require('process') const cpuNum = os.cpus().length // read local config options const option = readConfig() @@ -17,7 +18,7 @@ const createCluster = async () => { await setFfmpegEnv() } for (let i = 0; i < cpuNum; i++) { - cluster.fork() + process.nextTick(() => cluster.fork()) } cluster.on('exit', (worker, code, signal) => { console.log(colors.red(`worker ${worker.process.pid} died, code: ${code}, signal: ${signal}`)) @@ -34,6 +35,7 @@ const createCluster = async () => { const threadRun = async () => { if (cluster.isMaster) { console.log(colors.blue(`Master ${process.pid} is running`)) + ffmpegKiller.killToDeathFfmeg() await createCluster() } else { createServer(option) @@ -53,5 +55,14 @@ const threadRun = async () => { await setFfmpegEnv() } createServer(option) + ffmpegKiller.killToDeathFfmeg() } -})() \ No newline at end of file +})() + +process.on('SIGINT', function () { + if (cluster.isMaster) { + ffmpegKiller.clear() + console.log('\n Pressed Control-C to exit.') + process.exit(0) + } +}) \ No newline at end of file diff --git a/package.json b/package.json index 48eaf30..04c5751 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "fluent-ffmpeg": "^2.1.2", "fs-extra": "^11.1.0", "js-yaml": "^4.1.0", + "pidusage": "^3.0.2", "request": "^2.88.2", "systeminformation": "^5.17.12", "winston": "^3.8.2", @@ -60,7 +61,7 @@ "eslint-plugin-import": "^2.25.2", "eslint-plugin-n": "^15.0.0", "eslint-plugin-promise": "^6.0.0", - "rimraf": "^4.4.0", - "pkg": "^5.8.0" + "pkg": "^5.8.0", + "rimraf": "^4.4.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68a67d5..c9a5843 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,7 @@ specifiers: fluent-ffmpeg: ^2.1.2 fs-extra: ^11.1.0 js-yaml: ^4.1.0 + pidusage: ^3.0.2 pkg: ^5.8.0 request: ^2.88.2 rimraf: ^4.4.0 @@ -28,6 +29,7 @@ dependencies: fluent-ffmpeg: registry.npmmirror.com/fluent-ffmpeg/2.1.2 fs-extra: registry.npmmirror.com/fs-extra/11.1.0 js-yaml: registry.npmmirror.com/js-yaml/4.1.0 + pidusage: 3.0.2 request: registry.npmmirror.com/request/2.88.2 systeminformation: registry.npmmirror.com/systeminformation/5.17.12 winston: registry.npmmirror.com/winston/3.8.2 @@ -44,6 +46,23 @@ devDependencies: packages: + /graceful-fs/4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + optional: true + + /pidusage/3.0.2: + resolution: {integrity: sha512-g0VU+y08pKw5M8EZ2rIGiEBaB8wrQMjYGFfW2QVIfyT8V+fq8YFLkvlz4bz5ljvFDJYNFCWT3PWqcRr2FKO81w==} + engines: {node: '>=10'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /safe-buffer/5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + registry.npmmirror.com/@babel/generator/7.18.2: resolution: {integrity: sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@babel/generator/-/generator-7.18.2.tgz} name: '@babel/generator' @@ -511,7 +530,7 @@ packages: version: 1.2.3 dependencies: readable-stream: registry.npmmirror.com/readable-stream/2.3.8 - safe-buffer: registry.npmmirror.com/safe-buffer/5.2.1 + safe-buffer: 5.2.1 dev: false registry.npmmirror.com/bl/4.1.0: @@ -2611,7 +2630,7 @@ packages: dependencies: universalify: registry.npmmirror.com/universalify/2.0.0 optionalDependencies: - graceful-fs: registry.npmmirror.com/graceful-fs/4.2.10 + graceful-fs: 4.2.10 registry.npmmirror.com/jsprim/1.4.2: resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/jsprim/-/jsprim-1.4.2.tgz} @@ -3816,14 +3835,14 @@ packages: name: string_decoder version: 1.1.1 dependencies: - safe-buffer: registry.npmmirror.com/safe-buffer/5.1.2 + safe-buffer: 5.1.2 registry.npmmirror.com/string_decoder/1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz} name: string_decoder version: 1.3.0 dependencies: - safe-buffer: registry.npmmirror.com/safe-buffer/5.2.1 + safe-buffer: 5.2.1 registry.npmmirror.com/strip-ansi/6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz} From 18ce02acdacf37039f2514ef483f3349ae0ca894 Mon Sep 17 00:00:00 2001 From: helson Date: Mon, 20 Mar 2023 22:35:52 +0800 Subject: [PATCH 2/3] fix: add DS_Store to ignore --- .gitignore | 3 ++- config2/config.yml | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 config2/config.yml diff --git a/.gitignore b/.gitignore index 5eab208..b290368 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ dist/** media/* config/* lib/* -*.mp4 \ No newline at end of file +*.mp4 +.DS_Store \ No newline at end of file diff --git a/config2/config.yml b/config2/config.yml deleted file mode 100644 index 4b99a62..0000000 --- a/config2/config.yml +++ /dev/null @@ -1,7 +0,0 @@ -port: 8081 -downloadDir: /media/ -webhooks: 'https://oapi.dingtalk.com/robot/send?access_token=b1d3594015d5e2dfa5e0bc35ee2fe37c7b50407f30ce98a8ff0bffab6e66c379' -webhookType: 'dingding' -thread: false -downloadThread: true -useFFmpegLib: true From 880e5b175adc859251e082ed49d4a2966599aa95 Mon Sep 17 00:00:00 2001 From: hejianglin Date: Tue, 21 Mar 2023 13:24:03 +0800 Subject: [PATCH 3/3] fix: change readme.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 9b4e97a..1e5242e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ docker pulls + + release downloads + docker image size