From ca5cb88eca3b096283ed2adc9e2a3709c2233494 Mon Sep 17 00:00:00 2001 From: whyour Date: Sun, 7 Jul 2024 20:17:36 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=20cli?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build_docker_image.yml | 1 + back/loaders/deps.ts | 23 +- cli/api.mjs | 274 +++++++++++++ cli/commands/update.ts | 13 - cli/env.mjs | 22 + cli/index.ts | 13 - cli/otask.mjs | 297 ++++++++++++++ cli/share.mjs | 500 +++++++++++++++++++++++ cli/task.mjs | 94 +++++ package.json | 5 +- pnpm-lock.yaml | 46 ++- shell/share.sh | 27 +- 12 files changed, 1253 insertions(+), 62 deletions(-) create mode 100755 cli/api.mjs delete mode 100644 cli/commands/update.ts create mode 100644 cli/env.mjs delete mode 100644 cli/index.ts create mode 100755 cli/otask.mjs create mode 100755 cli/share.mjs create mode 100755 cli/task.mjs diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml index e3033820..876a38fa 100644 --- a/.github/workflows/build_docker_image.yml +++ b/.github/workflows/build_docker_image.yml @@ -7,6 +7,7 @@ on: branches: - "master" - "develop" + - "test-cli" tags: - "v*" schedule: diff --git a/back/loaders/deps.ts b/back/loaders/deps.ts index 89e0ca35..5efece2a 100644 --- a/back/loaders/deps.ts +++ b/back/loaders/deps.ts @@ -19,26 +19,35 @@ async function linkToNodeModule(src: string, dst?: string) { async function linkCommand() { const commandPath = await promiseExec('which node'); const commandDir = path.dirname(commandPath); - const linkShell = [ + const oldLinkShell = [ { src: 'update.sh', dest: 'ql', tmp: 'ql_tmp', }, - { - src: 'task.sh', - dest: 'task', - tmp: 'task_tmp', - }, ]; + // const newLinkShell = [ + // { + // src: 'task.mjs', + // dest: 'task', + // tmp: 'task_tmp', + // }, + // ]; - for (const link of linkShell) { + for (const link of oldLinkShell) { const source = path.join(config.rootPath, 'shell', link.src); const target = path.join(commandDir, link.dest); const tmpTarget = path.join(commandDir, link.tmp); await fs.symlink(source, tmpTarget); await fs.rename(tmpTarget, target); } + // for (const link of newLinkShell) { + // const source = path.join(config.rootPath, 'cli', link.src); + // const target = path.join(commandDir, link.dest); + // const tmpTarget = path.join(commandDir, link.tmp); + // await fs.symlink(source, tmpTarget); + // await fs.rename(tmpTarget, target); + // } } export default async (src: string = 'deps') => { diff --git a/cli/api.mjs b/cli/api.mjs new file mode 100755 index 00000000..64bd5f4e --- /dev/null +++ b/cli/api.mjs @@ -0,0 +1,274 @@ +#!/usr/bin/env zx +import path from 'path'; + +const dir_root = process.env.QL_DIR; +const file_auth_token = path.join(dir_root, 'static/auth.json'); +const token_file = path.join(dir_root, 'static/build/token.js'); + +let token; + +const createToken = async () => { + let tokenCommand = `tsx ${dir_root}/back/token.ts`; + if (await fs.exists(token_file)) { + tokenCommand = `node ${token_file}`; + } + token = (await $([tokenCommand])).stdout.trim(); +}; + +const getToken = async () => { + if (fs.existsSync(file_auth_token)) { + const authTokenData = JSON.parse(fs.readFileSync(file_auth_token, 'utf8')); + token = authTokenData.value; + const expiration = authTokenData.expiration; + const currentTimeStamp = Math.floor(Date.now() / 1000); + if (currentTimeStamp >= expiration) { + await createToken(); + } + } else { + await createToken(); + } +}; + +export const addCronApi = async (schedule, command, name, subId = null) => { + const currentTimeStamp = Math.floor(Date.now() / 1000); + const data = { + name, + command, + schedule, + sub_id: subId, + }; + + try { + const response = await fetch( + `http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json;charset=UTF-8', + }, + body: JSON.stringify(data), + }, + ); + + const responseData = await response.json(); + const { code, message } = responseData; + if (code === 200) { + console.log(`${name} -> 添加成功`); + } else { + console.log(`${name} -> 添加失败(${message})`); + } + } catch (error) { + console.error(`${name} -> 添加失败(${error.message})`); + } +}; + +export const updateCronApi = async (schedule, command, name, id) => { + const currentTimeStamp = Math.floor(Date.now() / 1000); + const data = { + name, + command, + schedule, + id, + }; + + try { + const response = await fetch( + `http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`, + { + method: 'PUT', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json;charset=UTF-8', + }, + body: JSON.stringify(data), + }, + ); + + const responseData = await response.json(); + const { code, message } = responseData; + if (code === 200) { + console.log(`${name} -> 更新成功`); + } else { + console.log(`${name} -> 更新失败(${message})`); + } + } catch (error) { + console.error(`${name} -> 更新失败(${error.message})`); + } +}; + +export const updateCronCommandApi = async (command, id) => { + const currentTimeStamp = Math.floor(Date.now() / 1000); + const data = { + command, + id, + }; + + try { + const response = await fetch( + `http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`, + { + method: 'PUT', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json;charset=UTF-8', + }, + body: JSON.stringify(data), + }, + ); + + const responseData = await response.json(); + const { code, message } = responseData; + if (code === 200) { + console.log(`${command} -> 更新成功`); + } else { + console.log(`${command} -> 更新失败(${message})`); + } + } catch (error) { + console.error(`${command} -> 更新失败(${error.message})`); + } +}; + +export const delCronApi = async (ids) => { + const currentTimeStamp = Math.floor(Date.now() / 1000); + + try { + const response = await fetch( + `http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`, + { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json;charset=UTF-8', + }, + body: JSON.stringify(ids), + }, + ); + + const responseData = await response.json(); + const { code, message } = responseData; + if (code === 200) { + console.log('成功'); + } else { + console.log(`失败(${message})`); + } + } catch (error) { + console.error(`删除失败(${error.message})`); + } +}; + +export const updateCron = async ( + ids, + status, + pid, + logPath, + lastExecutingTime = 0, + runningTime = 0, +) => { + const currentTimeStamp = Math.floor(Date.now() / 1000); + const data = { + ids, + status, + pid, + log_path: logPath, + last_execution_time: lastExecutingTime, + last_running_time: runningTime, + }; + + try { + const response = await fetch( + `http://0.0.0.0:5600/open/crons/status?t=${currentTimeStamp}`, + { + method: 'PUT', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json;charset=UTF-8', + }, + body: JSON.stringify(data), + }, + ); + + const responseData = await response.json(); + const { code, message } = responseData; + if (code !== 200) { + console.log(`更新任务状态失败(${message})`); + } + } catch (error) { + console.error(`更新任务状态失败(${error.message})`); + } +}; + +export const notifyApi = async (title, content) => { + const currentTimeStamp = Math.floor(Date.now() / 1000); + const data = { + title, + content, + }; + + try { + const response = await fetch( + `http://0.0.0.0:5600/open/system/notify?t=${currentTimeStamp}`, + { + method: 'PUT', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json;charset=UTF-8', + }, + body: JSON.stringify(data), + }, + ); + + const responseData = await response.json(); + const { code, message } = responseData; + if (code === 200) { + console.log('通知发送成功🎉'); + } else { + console.log(`通知失败(${message})`); + } + } catch (error) { + console.error(`通知失败(${error.message})`); + } +}; + +export const findCronApi = async (params) => { + const currentTimeStamp = Math.floor(Date.now() / 1000); + + try { + const response = await fetch( + `http://0.0.0.0:5600/open/crons/detail?${params}&t=${currentTimeStamp}`, + { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}`, + 'User-Agent': 'Mozilla/5.0', + 'Content-Type': 'application/json;charset=UTF-8', + }, + }, + ); + + const responseData = await response.json(); + const { data } = responseData; + if (data === 'null') { + console.log(''); + } else { + const { name } = data; + console.log(name); + } + } catch (error) { + console.error(`查找失败(${error.message})`); + } +}; + +await getToken(); diff --git a/cli/commands/update.ts b/cli/commands/update.ts deleted file mode 100644 index f3da511a..00000000 --- a/cli/commands/update.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CommandModule } from 'yargs'; -export const updateCommand: CommandModule = { - command: 'update', - describe: 'Update and restart qinglong', - builder: (yargs) => { - return yargs.option('repositority', { - type: 'string', - alias: 'r', - describe: `Specify the release warehouse address of the package`, - }); - }, - handler: async (argv) => {}, -}; diff --git a/cli/env.mjs b/cli/env.mjs new file mode 100644 index 00000000..cee04644 --- /dev/null +++ b/cli/env.mjs @@ -0,0 +1,22 @@ +#!/usr/bin/env zx + +import 'zx/globals'; +let initialVars = []; + +export const storeEnvVars = async () => { + const stdout = (await $`env`).lines(); + initialVars = stdout.map((line) => line.split('=')[0]); +}; + +export const restoreEnvVars = async () => { + const stdout = (await $`env`).lines(); + const currentVars = stdout.map((line) => line.split('=')[0]); + + for (const key of currentVars) { + if (!initialVars.includes(key)) { + await $`unset ${key}`; + } + } +}; + +await storeEnvVars(); diff --git a/cli/index.ts b/cli/index.ts deleted file mode 100644 index ce7ca0d1..00000000 --- a/cli/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as yargs from 'yargs'; -import { green, red } from 'chalk'; -import { updateCommand } from './commands/update'; - -yargs - .usage('Usage: ql [command] ') - .command(updateCommand) - .fail((err) => { - console.error(`${red(err)}`); - }) - .alias('h', 'help') - .showHelp() - .recommendCommands().argv; diff --git a/cli/otask.mjs b/cli/otask.mjs new file mode 100755 index 00000000..4800d020 --- /dev/null +++ b/cli/otask.mjs @@ -0,0 +1,297 @@ +#!/usr/bin/env zx + +import { + dirScripts, + dirLog, + handleTaskStart, + runTaskBefore, + runTaskAfter, + handleTaskEnd, + globalState, + formatLogTime, +} from './share.mjs'; +import { basename, dirname } from 'path'; + +function defineProgram(fileParam) { + if (fileParam.endsWith('.js') || fileParam.endsWith('.mjs')) { + return 'node'; + } else if (fileParam.endsWith('.py') || fileParam.endsWith('.pyc')) { + return 'python3'; + } else if (fileParam.endsWith('.sh')) { + return 'bash'; + } else if (fileParam.endsWith('.ts')) { + return $`command -v tsx` + .then(() => 'tsx') + .catch(() => 'ts-node-transpile-only'); + } else { + return ''; + } +} + +async function randomDelay(fileParam) { + const randomDelayMax = process.env.RandomDelay; + if (randomDelayMax && randomDelayMax > 0) { + const fileExtensions = process.env.RandomDelayFileExtensions || 'js'; + const ignoredMinutes = process.env.RandomDelayIgnoredMinutes || '0 30'; + const currentMin = new Date().getMinutes(); + + if ( + fileExtensions.split(' ').some((ext) => fileParam.endsWith(`.${ext}`)) + ) { + if ( + ignoredMinutes.split(' ').some((min) => parseInt(min) === currentMin) + ) { + return; + } + const delaySecond = Math.floor(Math.random() * randomDelayMax) + 1; + console.log( + `任务随机延迟 ${delaySecond} 秒,配置文件参数 RandomDelay 置空可取消延迟`, + ); + await sleep(delaySecond * 1000); + } + } +} + +async function genArrayScripts() { + const arrayScripts = []; + const arrayScriptsName = []; + const files = await fs.readdir(dirScripts); + + for (const file of files) { + if (file.endsWith('.js') && file !== 'sendNotify.js') { + arrayScripts.push(file); + const content = await fs.readFile(`${dirScripts}/${file}`, 'utf8'); + const match = content.match(/new Env\(['"]([^'"]+)['"]\)/); + arrayScriptsName.push(match ? match[1] : '<未识别出活动名称>'); + } + } + + return { arrayScripts, arrayScriptsName }; +} + +async function usage() { + const { arrayScripts, arrayScriptsName } = await genArrayScripts(); + console.log( + `task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “${dirScripts}/” 目录后的相对路径(定时任务中请写作相对路径),用法为:`, + ); + console.log( + `1.$cmdTask # 依次执行,如果设置了随机延迟,将随机延迟一定秒数`, + ); + console.log( + `2.$cmdTask now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中`, + ); + console.log( + `3.$cmdTask conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行`, + ); + console.log( + `4.$cmdTask desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行`, + ); + if (arrayScripts.length > 0) { + console.log(`\n当前有以下脚本可以运行:`); + arrayScripts.forEach((script, i) => + console.log(`${i + 1}. ${arrayScriptsName[i]}:${script}`), + ); + } else { + console.log(`\n暂无脚本可以执行`); + } +} + +export function parseDuration(d) { + if (typeof d == 'number') { + if (isNaN(d) || d < 0) throw new Error(`Invalid duration: "${d}".`); + return d; + } else if (/\d+s/.test(d)) { + return +d.slice(0, -1) * 1000; + } else if (/\d+ms/.test(d)) { + return +d.slice(0, -2); + } else if (/\d+m/.test(d)) { + return +d.slice(0, -1) * 1000 * 60; + } else if (/\d+h/.test(d)) { + return +d.slice(0, -1) * 1000 * 60 * 60; + } else if (/\d+d/.test(d)) { + return +d.slice(0, -1) * 1000 * 60 * 60 * 24; + } + throw new Error(`Unknown duration: "${d}".`); +} + +async function runWithTimeout(command) { + if (globalState.commandTimeoutTime) { + const timeoutNumber = parseDuration(globalState.commandTimeoutTime); + await $([command]).timeout(timeoutNumber).nothrow().pipe(process.stdout); + } else { + await $([command]).nothrow().pipe(process.stdout); + } +} + +async function runNormal(fileParam, scriptParams) { + if ( + !scriptParams.includes('now') && + process.env.realTime !== 'true' && + process.env.noDelay !== 'true' + ) { + await randomDelay(fileParam); + } + cd(dirScripts); + const relativePath = dirname(fileParam); + if (!fileParam.startsWith('/') && relativePath) { + cd(relativePath); + fileParam = fileParam.replace(`${relativePath}/`, ''); + } + await runWithTimeout( + `${globalState.whichProgram} ${fileParam} ${scriptParams}`, + ); +} + +async function runConcurrent(fileParam, envParam, numParam, scriptParams) { + if (!envParam || !numParam) { + console.log(`\n 缺少并发运行的环境变量参数 task xxx.js conc Test 1 3`); + process.exit(1); + } + + const array = (process.env[envParam] || '').split('&'); + const runArr = expandRange(numParam, array.length); + const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean); + + const singleLogTime = formatLogTime(new Date()); + + cd(dirScripts); + const relativePath = dirname(fileParam); + if (relativePath && fileParam.includes('/')) { + cd(relativePath); + fileParam = fileParam.replace(`${relativePath}/`, ''); + } + + await Promise.all( + arrayRun.map(async (env, i) => { + const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${ + i + 1 + }.log`; + await runWithTimeout( + `${envParam}="${env.replace('"', '\\"')}" ${ + globalState.whichProgram + } ${fileParam} ${scriptParams} &>${singleLogPath}`, + ); + }), + ); + + for (let i = 0; i < arrayRun.length; i++) { + const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${ + i + 1 + }.log`; + const log = await fs.readFile(singleLogPath, 'utf8'); + console.log(log); + await fs.unlink(singleLogPath); + } +} + +async function runDesignated(fileParam, envParam, numParam, scriptParams) { + if (!envParam || !numParam) { + console.log(`\n 缺少单独运行的参数 task xxx.js desi Test 1 3`); + process.exit(1); + } + + const array = (process.env[envParam] || '').split('&'); + const runArr = expandRange(numParam, array.length); + const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean); + const cookieStr = arrayRun.join('&'); + cd(dirScripts); + const relativePath = dirname(fileParam); + if (relativePath && fileParam.includes('/')) { + cd(relativePath); + fileParam = fileParam.replace(`${relativePath}/`, ''); + } + + console.log('cookieStr', cookieStr.length, arrayRun.length) + // ${envParam}="${cookieStr.replace('"', '\\"')}" + await runWithTimeout( + `${ + globalState.whichProgram + } ${fileParam} ${scriptParams}`, + ); +} + +async function runElse(fileParam, scriptParams) { + cd(dirScripts); + const relativePath = dirname(fileParam); + if (relativePath && fileParam.includes('/')) { + cd(relativePath); + fileParam = fileParam.replace(`${relativePath}/`, './'); + } + + await runWithTimeout( + `${globalState.whichProgram} ${fileParam} ${scriptParams}`, + ); +} + +function expandRange(rangeStr, max) { + const tempRangeStr = rangeStr + .replace(/-max/g, `-${max}`) + .replace(/max-/g, `${max}-`); + + return tempRangeStr.split(' ').flatMap((part) => { + const rangeMatch = part.match(/^(\d+)([-~_])(\d+)$/); + if (rangeMatch) { + const [, start, , end] = rangeMatch.map(Number); + return Array.from({ length: end - start + 1 }, (_, i) => start + i); + } + return Number(part); + }); +} + +async function main(taskShellParams, scriptParams) { + const [fileParam, action, envParam, ...others] = taskShellParams; + if (taskShellParams.length === 0) { + return await usage(); + } + if (fileParam && /\.(js|py|pyc|sh|ts)$/.test(fileParam)) { + switch (action) { + case undefined: + return await runNormal(fileParam, scriptParams); + case 'now': + return await runNormal(fileParam, scriptParams); + case 'conc': + return await runConcurrent( + fileParam, + envParam, + others.join(' '), + scriptParams, + ); + case 'desi': + return await runDesignated( + fileParam, + envParam, + others.join(' '), + scriptParams, + ); + } + } + await runElse(fileParam, taskShellParams.slice(1).concat(scriptParams)); +} + +async function run() { + const taskArgv = minimist(process.argv.slice(3), { + '--': true, + }); + const { _: taskShellParams, m, '--': scriptParams, GlobalState } = taskArgv; + const cacheState = JSON.parse(GlobalState || '{}'); + for (const key in cacheState) { + globalState[key] = cacheState[key]; + } + if (m) { + globalState.commandTimeoutTime = m; + } + globalState.whichProgram = await defineProgram(taskShellParams[0]); + + await handleTaskStart(); + await runTaskBefore(); + await main(taskShellParams, scriptParams); + await runTaskAfter(); + await handleTaskEnd(); +} + +async function singleHandle() {} + +const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP']; +signals.forEach((sig) => process.on(sig, singleHandle)); + +await run(); diff --git a/cli/share.mjs b/cli/share.mjs new file mode 100755 index 00000000..bf9f140b --- /dev/null +++ b/cli/share.mjs @@ -0,0 +1,500 @@ +#!/usr/bin/env zx +import 'zx/globals'; +// $.verbose = true; + +import { updateCron, notifyApi } from './api.mjs'; +import { restoreEnvVars } from './env.mjs'; + +export const dirRoot = process.env.QL_DIR; +export const dirTmp = path.join(dirRoot, '.tmp'); +export const dirData = process.env.QL_DATA_DIR + ? process.env.QL_DATA_DIR.endsWith('/') + ? process.env.QL_DATA_DIR.slice(-1) + : process.env.QL_DATA_DIR + : path.join(dirRoot, 'data'); +export const dirShell = path.join(dirRoot, 'shell'); +export const dirCli = path.join(dirRoot, 'cli'); +export const dirSample = path.join(dirRoot, 'sample'); +export const dirStatic = path.join(dirRoot, 'static'); +export const dirConfig = path.join(dirData, 'config'); +export const dirScripts = path.join(dirData, 'scripts'); +export const dirRepo = path.join(dirData, 'repo'); +export const dirRaw = path.join(dirData, 'raw'); +export const dirLog = path.join(dirData, 'log'); +export const dirDb = path.join(dirData, 'db'); +export const dirDep = path.join(dirData, 'deps'); +export const dirListTmp = path.join(dirLog, '.tmp'); +export const dirUpdateLog = path.join(dirLog, 'update'); +export const qlStaticRepo = path.join(dirRepo, 'static'); + +export const fileConfigSample = path.join(dirSample, 'config.sample.sh'); +export const fileEnv = path.join(dirConfig, 'env.sh'); +export const jsFileEnv = path.join(dirConfig, 'env.js'); +export const fileConfigUser = path.join(dirConfig, 'config.sh'); +export const fileAuthSample = path.join(dirSample, 'auth.sample.json'); +export const fileAuthUser = path.join(dirConfig, 'auth.json'); +export const fileAuthToken = path.join(dirConfig, 'token.json'); +export const fileExtraShell = path.join(dirConfig, 'extra.sh'); +export const fileTaskBefore = path.join(dirConfig, 'task_before.sh'); +export const fileTaskAfter = path.join(dirConfig, 'task_after.sh'); +export const fileTaskSample = path.join(dirSample, 'task.sample.sh'); +export const fileExtraSample = path.join(dirSample, 'extra.sample.sh'); +export const fileNotifyJsSample = path.join(dirSample, 'notify.js'); +export const fileNotifyPySample = path.join(dirSample, 'notify.py'); +export const fileTestJsSample = path.join(dirSample, 'ql_sample.js'); +export const fileTestPySample = path.join(dirSample, 'ql_sample.py'); +export const fileNotifyPy = path.join(dirScripts, 'notify.py'); +export const fileNotifyJs = path.join(dirScripts, 'sendNotify.js'); +export const fileTestJs = path.join(dirScripts, 'ql_sample.js'); +export const fileTestPy = path.join(dirScripts, 'ql_sample.py'); +export const nginxAppConf = path.join(dirRoot, 'docker/front.conf'); +export const nginxConf = path.join(dirRoot, 'docker/nginx.conf'); +export const depNotifyPy = path.join(dirDep, 'notify.py'); +export const depNotifyJs = path.join(dirDep, 'sendNotify.js'); + +export const listCrontabUser = path.join(dirConfig, 'crontab.list'); +export const listCrontabSample = path.join(dirSample, 'crontab.sample.list'); +export const listOwnScripts = path.join(dirListTmp, 'own_scripts.list'); +export const listOwnUser = path.join(dirListTmp, 'own_user.list'); +export const listOwnAdd = path.join(dirListTmp, 'own_add.list'); +export const listOwnDrop = path.join(dirListTmp, 'own_drop.list'); + +export const globalState = {}; + +export const initEnv = () => { + $.prefix += + 'export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules;'; + $.prefix += 'export PYTHONUNBUFFERED=1;'; + $.prefix += 'export TERM=xterm-color;'; +}; + +export const importConfig = async () => { + if (await fs.exists(fileConfigUser)) { + $.prefix += (await fs.readFile(fileConfigUser, 'utf8')); + } + // if (process.env.LOAD_ENV !== 'false' && (await fs.exists(fileEnv))) { + // $.prefix += (await fs.readFile(fileEnv, 'utf8')); + // } + require(jsFileEnv) + + globalState.qlBaseUrl = process.env.QlBaseUrl || '/'; + globalState.qlPort = process.env.QlPort || '5700'; + globalState.commandTimeoutTime = process.env.CommandTimeoutTime; + globalState.fileExtensions = process.env.RepoFileExtensions || 'js py'; + globalState.proxyUrl = process.env.ProxyUrl || ''; + globalState.currentBranch = process.env.QL_BRANCH; + + if (process.env.DefaultCronRule) { + globalState.defaultCron = process.env.DefaultCronRule; + } else { + globalState.defaultCron = `${Math.floor(Math.random() * 60)} ${Math.floor( + Math.random() * 24, + )} * * *`; + } + + globalState.cpuWarn = process.env.CpuWarn; + globalState.memWarn = process.env.MemoryWarn; + globalState.diskWarn = process.env.DiskWarn; +}; + +export const setProxy = (proxy) => { + if (proxy) { + globalState.proxyUrl = proxy; + } + if (globalState.proxyUrl) { + $`export http_proxy=${globalState.proxyUrl}`; + $`export https_proxy=${globalState.proxyUrl}`; + } +}; + +export const unsetProxy = () => { + $`unset http_proxy`; + $`unset https_proxy`; +}; + +export const makeDir = async (dir) => { + if (!(await fs.exists(dir))) { + await fs.mkdir(dir, { recursive: true }); + } +}; + +export const detectTermux = () => { + globalState.isTermux = process.env.PATH?.includes('com.termux') ? 1 : 0; +}; + +export const detectMacos = () => { + globalState.isMacos = os.type() === 'Darwin' ? 1 : 0; +}; + +export const genRandomNum = (number) => { + return Math.floor(Math.random() * number); +}; + +export const fixConfig = async () => { + await makeDir(dirTmp); + await makeDir(dirStatic); + await makeDir(dirData); + await makeDir(dirConfig); + await makeDir(dirLog); + await makeDir(dirDb); + await makeDir(dirScripts); + await makeDir(dirListTmp); + await makeDir(dirRepo); + await makeDir(dirRaw); + await makeDir(dirUpdateLog); + await makeDir(dirDep); + + if (!(await fs.exists(fileConfigUser))) { + console.log( + `复制一份 ${fileConfigSample} 为 ${fileConfigUser},随后请按注释编辑你的配置文件:${fileConfigUser}`, + ); + await fs.copyFile(fileConfigSample, fileConfigUser); + } + + if (!(await fs.exists(fileEnv))) { + console.log( + '检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...', + ); + await fs.writeFile(fileEnv, ''); + } + + if (!(await fs.exists(fileTaskBefore))) { + console.log(`复制一份 ${fileTaskSample} 为 ${fileTaskBefore}`); + await fs.copyFile(fileTaskSample, fileTaskBefore); + } + + if (!(await fs.exists(fileTaskAfter))) { + console.log(`复制一份 ${fileTaskSample} 为 ${fileTaskAfter}`); + await fs.copyFile(fileTaskSample, fileTaskAfter); + } + + if (!(await fs.exists(fileExtraShell))) { + console.log(`复制一份 ${fileExtraSample} 为 ${fileExtraShell}`); + await fs.copyFile(fileExtraSample, fileExtraShell); + } + + if (!(await fs.exists(fileAuthUser))) { + console.log(`复制一份 ${fileAuthSample} 为 ${fileAuthUser}`); + await fs.copyFile(fileAuthSample, fileAuthUser); + } + + if (!(await fs.exists(fileNotifyPy))) { + console.log(`复制一份 ${fileNotifyPySample} 为 ${fileNotifyPy}`); + await fs.copyFile(fileNotifyPySample, fileNotifyPy); + } + + if (!(await fs.exists(fileNotifyJs))) { + console.log(`复制一份 ${fileNotifyJsSample} 为 ${fileNotifyJs}`); + await fs.copyFile(fileNotifyJsSample, fileNotifyJs); + } + + if (!(await fs.exists(fileTestJs))) { + await fs.copyFile(fileTestJsSample, fileTestJs); + } + + if (!(await fs.exists(fileTestPy))) { + await fs.copyFile(fileTestPySample, fileTestPy); + } + + if (await fs.exists('/etc/nginx/conf.d/default.conf')) { + console.log('检测到你可能未修改过默认nginx配置,将帮你删除'); + await fs.unlink('/etc/nginx/conf.d/default.conf'); + } + if (!(await fs.exists(depNotifyJs))) { + console.log(`复制一份 ${fileNotifyJsSample} 为 ${depNotifyJs}`); + await fs.copyFile(fileNotifyJsSample, depNotifyJs); + } + + if (!(await fs.exists(depNotifyPy))) { + console.log(`复制一份 ${fileNotifyPySample} 为 ${depNotifyPy}`); + await fs.copyFile(fileNotifyPySample, depNotifyPy); + } +}; + +export const npmInstallSub = async () => { + if (globalState.isTermux === 1) { + await $`npm install --production --no-bin-links`; + } else if (!(await $`command -v pnpm`)) { + await $`npm install --production`; + } else { + await $`pnpm install --loglevel error --production`; + } +}; + +export const npmInstall = async (dirWork) => { + const dirCurrent = process.cwd(); + + await $`cd ${dirWork}`; + console.log(`安装 ${dirWork} 依赖包...`); + await npmInstallSub(); + await $`cd ${dirCurrent}`; +}; + +export const diffAndCopy = async (copySource, copyTo) => { + if ( + !(await fs.exists(copyTo)) || + (await $`diff ${copySource} ${copyTo}`).exitCode !== 0 + ) { + await fs.copyFile(copySource, copyTo); + } +}; + +export const gitCloneScripts = async (url, dir, branch, proxy) => { + const partCmd = branch ? `-b ${branch}` : ''; + console.log(`开始拉取仓库 ${globalState.uniqPath} 到 ${dir}`); + + setProxy(proxy); + + const res = await $`git clone -q --depth=1 ${partCmd} ${url} ${dir}`; + globalState.exitStatus = res.exitCode; + + unsetProxy(); +}; + +export const randomRange = (begin, end) => { + return Math.floor(Math.random() * (end - begin) + begin); +}; + +export const deletePm2 = async () => { + await $`cd ${dirRoot}`; + await $`pm2 delete ecosystem.config.js`; +}; + +export const reloadPm2 = async () => { + await $`cd ${dirRoot}`; + restoreEnvVars(); + await $`pm2 flush &>/dev/null`; + await $`pm2 startOrGracefulReload ecosystem.config.js`; +}; + +export const reloadUpdate = async () => { + await $`cd ${dirRoot}`; + restoreEnvVars(); + await $`pm2 flush &>/dev/null`; + await $`pm2 startOrGracefulReload other.config.js`; +}; + +export const diffTime = (beginTime, endTime) => { + let diffTime; + if (globalState.isMacos === 1) { + diffTime = (+new Date(endTime) - +new Date(beginTime)) / 1000; + } else { + diffTime = + (new Date(endTime).getTime() - new Date(beginTime).getTime()) / 1000; + } + return diffTime; +}; + +export const formatTime = (time) => { + // 秒 + return new Date(time).toLocaleString(); +}; + +function pad(n, min = 10) { + return n < min ? '0' + n : n; +} + +export function formatDate(date) { + const year = date.getFullYear(); + const month = pad(date.getMonth() + 1); + const day = pad(date.getDate()); + const hour = pad(date.getHours()); + const minute = pad(date.getMinutes()); + const second = pad(date.getSeconds()); + + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; +} + +export const formatLogTime = (date) => { + const year = date.getFullYear(); + const month = pad(date.getMonth() + 1); + const day = pad(date.getDate()); + const hour = pad(date.getHours()); + const minute = pad(date.getMinutes()); + const second = pad(date.getSeconds()); + const milliSecond = pad(date.getMilliseconds(), 100); + + return `${year}-${month}-${day}-${hour}-${minute}-${second}-${milliSecond}`; +}; + +export const formatTimestamp = (date) => { + return Math.floor(date.getTime() / 1000); +}; + +export const patchVersion = async () => { + await $`git config --global pull.rebase false`; + + if (await fs.exists(path.join(dirRoot, 'db/cookie.db'))) { + console.log('检测到旧的db文件,拷贝为新db...'); + await $`mv ${path.join(dirRoot, 'db/cookie.db')} ${path.join( + dirRoot, + 'db/env.db', + )}`; + await $`rm -rf ${path.join(dirRoot, 'db/cookie.db')}`; + } + + if (await fs.exists(path.join(dirRoot, 'db'))) { + console.log('检测到旧的db目录,拷贝到data目录...'); + await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`; + } + + if (await fs.exists(path.join(dirRoot, 'scripts'))) { + console.log('检测到旧的scripts目录,拷贝到data目录...'); + await $`cp -rf ${path.join(dirRoot, 'scripts')} ${dirData}`; + } + + if (await fs.exists(path.join(dirRoot, 'log'))) { + console.log('检测到旧的log目录,拷贝到data目录...'); + await $`cp -rf ${path.join(dirRoot, 'log')} ${dirData}`; + } + + if (await fs.exists(path.join(dirRoot, 'config'))) { + console.log('检测到旧的config目录,拷贝到data目录...'); + await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`; + } +}; + +export const initNginx = async () => { + await fs.copyFile(nginxConf, '/etc/nginx/nginx.conf'); + await fs.copyFile(nginxAppConf, '/etc/nginx/conf.d/front.conf'); + let locationUrl = '/'; + let aliasStr = ''; + let rootStr = ''; + let qlBaseUrl = globalState.qlBaseUrl; + let qlPort = globalState.qlPort; + if (qlBaseUrl !== '/') { + if (!qlBaseUrl.startsWith('/')) { + qlBaseUrl = `/${qlBaseUrl}`; + } + if (!qlBaseUrl.endsWith('/')) { + qlBaseUrl = `${qlBaseUrl}/`; + } + locationUrl = `^~${qlBaseUrl.slice(0, -1)}`; + aliasStr = `alias ${path.join(dirStatic, 'dist')};`; + const file = await fs.readFile( + path.join(dirStatic, 'dist/index.html'), + 'utf8', + ); + if (!file.includes(``)) { + await fs.writeFile( + path.join(dirStatic, 'dist/index.html'), + `\n${file}`, + ); + } + } else { + rootStr = `root ${path.join(dirStatic, 'dist')};`; + } + await $`sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf`; + await $`sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf`; + await $`sed -i "s,QL_BASE_URL_LOCATION,${locationUrl},g" /etc/nginx/conf.d/front.conf`; + let ipv6Str = ''; + const ipv6 = await $`ip a | grep inet6`; + if (ipv6.stdout.trim()) { + ipv6Str = 'listen [::]:${qlPort} ipv6only=on;'; + } + const ipv4Str = `listen ${qlPort};`; + await $`sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf`; + await $`sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf`; +}; + +async function checkServer() { + const cpuWarn = parseInt(process.env.cpuWarn || '0'); + const memWarn = parseInt(process.env.memWarn || '0'); + const diskWarn = parseInt(process.env.diskWarn || '0'); + + if (cpuWarn && memWarn && diskWarn) { + const topResult = await $`top -b -n 1`; + const cpuUse = parseInt( + topResult.stdout.match(/CPU\s+(\d+)\%/)?.[1] || '0', + ); + const memFree = parseInt( + (await $`free -m`).stdout.match(/Mem:\s+(\d+)/)?.[1] || '0', + ); + const memTotal = parseInt( + (await $`free -m`).stdout.match(/Mem:\s+\d+\s+(\d+)/)?.[1] || '0', + ); + const diskUse = parseInt( + (await $`df -P`).stdout.match(/\/dev.*\s+(\d+)\%/)?.[1] || '0', + ); + if (memFree && memTotal && diskUse && cpuUse) { + const memUse = Math.floor((memFree * 100) / memTotal); + + if (cpuUse > cpuWarn || memFree < memWarn || diskUse > diskWarn) { + const resource = topResult.stdout + .split('\n') + .slice(7, 17) + .map((line) => line.replace(/\s+/g, ' ')) + .join('\\n'); + await notifyApi( + '服务器资源异常警告', + `当前CPU占用 ${cpuUse}% 内存占用 ${memUse}% 磁盘占用 ${diskUse}% \n资源占用详情 \n\n ${resource}`, + ); + } + } + } +} + +export const handleTaskStart = async () => { + if (globalState.ID) { + await updateCron( + [globalState.ID], + '0', + String(process.pid), + globalState.logPath, + globalState.beginTimestamp, + ); + } + console.log(`## 开始执行... ${globalState.beginTime}\n`); +}; + +export const runTaskBefore = async () => { + if (globalState.isMacos === 0) { + await checkServer(); + } + await $`. ${fileTaskBefore} "$@"`; + + if (globalState.taskBefore) { + console.log('执行前置命令'); + await $`eval ${globalState.taskBefore}`; + console.log('执行前置命令结束'); + } +}; + +export const runTaskAfter = async () => { + await $`. ${fileTaskAfter} "$@"`; + + if (globalState.taskAfter) { + console.log('执行后置命令'); + await $`eval "${globalState.taskAfter}"`; + console.log('执行后置命令结束'); + } +}; + +export const handleTaskEnd = async () => { + const etime = new Date(); + const endTime = formatDate(etime); + const endTimestamp = formatTimestamp(etime); + let diffTime = endTimestamp - globalState.beginTimestamp; + + if (diffTime === 0) { + diffTime = 1; + } + + if (globalState.ID) { + await updateCron( + [globalState.ID], + '1', + `${process.pid}`, + globalState.logPath, + globalState.beginTimestamp, + diffTime, + ); + } + + console.log(`\n## 执行结束... ${endTime} 耗时 ${diffTime} 秒`); +}; + +initEnv(); +detectTermux(); +detectMacos(); +await importConfig(); diff --git a/cli/task.mjs b/cli/task.mjs new file mode 100755 index 00000000..cd16dd26 --- /dev/null +++ b/cli/task.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env zx +import 'zx/globals'; + +import { PassThrough } from 'node:stream'; +import { + listCrontabUser, + formatLogTime, + formatTimestamp, + formatDate, + dirLog, + dirCli, + globalState, + handleTaskEnd, +} from './share.mjs'; +import './api.mjs'; + +// $.verbose = true; + +async function singleHandle() {} + +const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP']; +signals.forEach((sig) => process.on(sig, singleHandle)); + +export async function handleLogPath(fileParam) { + let ID = $.env.ID; + if (!ID) { + const grepResult = + await $`grep -E "task.* ${fileParam}" ${listCrontabUser}`.nothrow(); + ID = grepResult.stdout.match(/ID=(\d+)/)?.[1]; + } + globalState.ID = ID; + + const suffix = ID && parseInt(ID, 10) > 0 ? `_${ID}` : ''; + globalState.time = new Date(); + const logTime = formatLogTime(globalState.time); + let logDirTmp = path.basename(fileParam); + let logDirTmpPath = ''; + + if (fileParam.includes('/')) { + logDirTmpPath = path.isAbsolute(fileParam) + ? fileParam.substring(1) + : fileParam; + logDirTmpPath = path.basename(path.dirname(logDirTmpPath)); + } + + if (logDirTmpPath) { + logDirTmp = `${logDirTmpPath}_${logDirTmp}`; + } + + const logDir = `${logDirTmp.replace(/\.[^/.]+$/, '')}${suffix}`; + globalState.logDir = logDir; + globalState.logPath = `${logDir}/${logTime}.log`; + if ($.env.real_log_path) { + globalState.logPath = realLogPath; + } + + await $`mkdir -p ${dirLog}/${logDir}`; +} + +export function initBeginTime() { + globalState.beginTime = formatDate(globalState.time); + globalState.beginTimestamp = formatTimestamp(globalState.time); +} + +async function main() { + const taskArgv = minimist(process.argv.slice(3), { + '--': true, + }); + const { + _: [scriptFile], + } = taskArgv; + + await handleLogPath(scriptFile); + initBeginTime(); + + cd(`${dirCli}`); + const logStream = fs.createWriteStream(`${dirLog}/${globalState.logPath}`); + const passThrough = new PassThrough(); + passThrough.pipe(logStream); + const p = $`./otask.mjs ${process.argv.slice( + 3, + )} --GlobalState=${JSON.stringify(globalState)} 2>&1`.nothrow(); + p.stdout.pipe(passThrough).pipe(process.stdout); + await p; +} + +main() + .then(() => { + process.exit(0); + }) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/package.json b/package.json index f6477b5b..0a521bff 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "test": "umi-test", "test:coverage": "umi-test --coverage" }, + "bin": { + "task": "./cli/task.mjs" + }, "gitHooks": { "pre-commit": "lint-staged" }, @@ -97,7 +100,7 @@ "uuid": "^8.3.2", "winston": "^3.6.0", "winston-daily-rotate-file": "^4.7.1", - "yargs": "^17.3.1", + "zx": "^8.1.4", "tough-cookie": "^4.0.0", "request-ip": "3.3.0", "ip2region": "2.3.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07a559cd..c3d8515d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,9 +134,9 @@ dependencies: winston-daily-rotate-file: specifier: ^4.7.1 version: 4.7.1(winston@3.9.0) - yargs: - specifier: ^17.3.1 - version: 17.7.2 + zx: + specifier: ^8.1.4 + version: 8.1.4 devDependencies: '@ant-design/icons': @@ -5114,6 +5114,15 @@ packages: resolution: {integrity: sha512-xbqnZmGrCEqi/KUzOkeUSe77p7APvLuyellGaAoeww3CHJ1AbjQWjPSCFtKIzZn8L7LpEax4NXnC+gfa6nM7IA==} dev: true + /@types/fs-extra@11.0.4: + resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} + requiresBuild: true + dependencies: + '@types/jsonfile': 6.1.4 + '@types/node': 17.0.45 + dev: false + optional: true + /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} dependencies: @@ -5177,6 +5186,14 @@ packages: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true + /@types/jsonfile@6.1.4: + resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + requiresBuild: true + dependencies: + '@types/node': 17.0.45 + dev: false + optional: true + /@types/jsonwebtoken@8.5.9: resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==} dependencies: @@ -5231,6 +5248,14 @@ packages: /@types/node@17.0.45: resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + /@types/node@20.14.9: + resolution: {integrity: sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==} + requiresBuild: true + dependencies: + undici-types: 5.26.5 + dev: false + optional: true + /@types/nodemailer@6.4.8: resolution: {integrity: sha512-oVsJSCkqViCn8/pEu2hfjwVO+Gb3e+eTWjg3PcjeFKRItfKpKwHphQqbYmPQrlMk+op7pNNWPbsJIEthpFN/OQ==} dependencies: @@ -15988,6 +16013,12 @@ packages: resolution: {integrity: sha512-ZqGrAgaqqZM7LGRzNjLnw5elevWb5M8LEoDMadxIW3OWbcv72wMMgKdwOKpd5Fqxe8choLD8HN3iSj3TUh/giQ==} dev: false + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + requiresBuild: true + dev: false + optional: true + /unescape@1.0.1: resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==} engines: {node: '>=0.10.0'} @@ -16650,6 +16681,15 @@ packages: strip-indent: 2.0.0 dev: true + /zx@8.1.4: + resolution: {integrity: sha512-QFDYYpnzdpRiJ3dL2102Cw26FpXpWshW4QLTGxiYfIcwdAqg084jRCkK/kuP/NOSkxOjydRwNFG81qzA5r1a6w==} + engines: {node: '>= 12.17.0'} + hasBin: true + optionalDependencies: + '@types/fs-extra': 11.0.4 + '@types/node': 20.14.9 + dev: false + github.com/whyour/node-sqlite3/3a00af0b5d7603b7f1b290032507320b18a6b741: resolution: {tarball: https://codeload.github.com/whyour/node-sqlite3/tar.gz/3a00af0b5d7603b7f1b290032507320b18a6b741} name: '@whyour/sqlite3' diff --git a/shell/share.sh b/shell/share.sh index e019c726..6010b2cd 100755 --- a/shell/share.sh +++ b/shell/share.sh @@ -59,11 +59,11 @@ list_own_drop=$dir_list_tmp/own_drop.list ## 软连接及其原始文件对应关系 link_name=( - task + # task ql ) original_name=( - task.sh + # task.sh update.sh ) @@ -136,28 +136,6 @@ gen_random_num() { echo $((${RANDOM} % $divi)) } -define_cmd() { - local cmd_prefix cmd_suffix - if type task &>/dev/null; then - cmd_suffix="" - if [[ -f "$dir_shell/task.sh" ]]; then - cmd_prefix="" - else - cmd_prefix="bash " - fi - else - cmd_suffix=".sh" - if [[ -f "$dir_shell/task.sh" ]]; then - cmd_prefix="$dir_shell/" - else - cmd_prefix="bash $dir_shell/" - fi - fi - for ((i = 0; i < ${#link_name[*]}; i++)); do - export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}" - done -} - fix_config() { make_dir $dir_tmp make_dir $dir_static @@ -481,6 +459,5 @@ handle_task_end() { init_env detect_termux detect_macos -define_cmd import_config $1