接到需求
需要一個服務來執行shell腳本,要求可以實時打印shell腳本執行的過程,並看到腳本執行的結果。
明確任務目標:
-
這是一個web服務,需要執行shell腳本
-
當一個腳本執行的時候,再次發送請求需要等待當前腳本執行完畢,再自動執行這次請求
-
使用長連接而不是socket
-
添加腳本不需要重啟服務器
這里采用的是express框架
開始
首先搭好express基本框架
新建app.js
文件, npm install express
const express = require('express');
const app = express();
app.get('/:id', (req, res) => {
const { id } = req.params;
if (id === 'favicon.ico') {
res.sendStatus(200);
return;
}
// 執行腳本
});
app.set('port', 3018);
app.listen(app.get('port'), () => console.log(`server listening at ${app.get('port')}`));
新建文件
新建config.json
用於配置id和腳本名的對應關系,新建scripts
目錄用於存放腳本。
這里定義一個函數execute
參數為id和response對象,代碼如下:
const pidDict = {};
async function execute(id, res) {
delete require.cache[require.resolve('./config.json')];
const config = require('./config.json');
const filePath = config[id];
if (!filePath) {
res.sendStatus(404);
return;
}
console.log(`The script:${filePath} with ${id} begin execute`);
const readable = new Readable();
readable._read = () => {};
readable.pipe(res);
while (pidDict[id]) {
readable.push('\nWaiting for another script request.');
await wait(5000);
}
const handle = spawn('sh', [`./scripts/${filePath}`]);
pidDict[id] = handle.pid;
handle.stdout.on('data', (data) => {
readable.push(`\n${data}`);
getLogger(filePath).log(`\n${data}`);
});
handle.stderr.on('data', (data) => {
getLogger(filePath).warn(`\n${data}`);
readable.push(`\n${data}`);
});
handle.on('error', (code) => {
getLogger(filePath).error(`child process error with information: \n${code}`);
readable.push(`child process error with information: \n${code}`);
delete pidDict[id];
readable.push(null);
});
handle.on('close', (code) => {
getLogger(filePath).log(`child process close with code ${code}`);
delete pidDict[id];
readable.push(null);
});
}
解釋:
-
首先要加載
config.json
,需要注意的是,因為是需要動態引入,所以這里不能直接使用require('config.json')
,在這之前,需要先刪除引入的緩存:delete require.cache[require.resolve('./config.json')];
-
獲取文件路徑
const filePath = config[id];
-
新建讀寫流,可以直接發送到前端。
-
再執行腳本前,需要判斷當前有無腳本執行,這里在外部定義了一個pidDict,文件對應的id直接指向文件執行的handle的pid
-
緊接着就是輸入輸出流了
-
handle.stdout是標准輸出
-
handle.stderr是錯誤輸出,這里指的是輸出的警告
-
handle的error事件指的是腳本執行中遇到的錯誤,也就是腳本執行不成功報的錯誤信息
這里定義了兩個外部函數,一個是自定義的日志打印,另一個是遇到有腳本執行時的等待
新建
utility.js
const fs = require('fs'); /** * time wait * * @param time {number} time(ms) to wait */ /* eslint-disable compat/compat */ const wait = async (time = 1000) => { return new Promise((resolve) => { setTimeout(resolve, time); }); }; /** * set log * * getLogger(path).level * level: * log * trace * debug * info * warn * error * @param path */ function getLogger(path) { return require('tracer').console({ transport: (data) => { console.log(data.output); fs.appendFile(`./logs/${path}.log`, `${data.rawoutput} \n`, () => {}); }, }); } module.exports = { wait, getLogger, };
-
新建腳本
現在,新建scripts/hello-world.sh
#!/bin/sh
echo 'hello...'
sleep 5
echo 'world!'
config.json中注冊該腳本
{
"hello-world": "hello-world.sh"
}
執行node app.js
,通過curl http://localhost:3018/hello-world
即可觀察到運行結果。
附
這里放上app.js的完整代碼
const express = require('express');
const { spawn } = require('child_process');
const { Readable } = require('stream');
const { wait, getLogger } = require('./utility');
const app = express();
app.get('/:id', (req, res) => {
// 執行腳本
const { id } = req.params;
if (id === 'favicon.ico') {
res.sendStatus(200);
return;
}
execute(id, res).then();
});
const pidDict = {};
/**
* 執行sh腳本
*
* @param id 腳本id
* @param res response object
*/
/* eslint-disable no-underscore-dangle, no-await-in-loop */
async function execute(id, res) {
delete require.cache[require.resolve('./config.json')];
const config = require('./config.json');
const filePath = config[id];
if (!filePath) {
res.sendStatus(404);
return;
}
console.log(`The script:${filePath} with ${id} begin execute`);
const readable = new Readable();
readable._read = () => {};
readable.pipe(res);
while (pidDict[id]) {
readable.push('\nWaiting for another script request.');
await wait(5000);
}
const handle = spawn('sh', [`./scripts/${filePath}`]);
pidDict[id] = handle.pid;
handle.stdout.on('data', (data) => {
readable.push(`\n${data}`);
getLogger(filePath).log(`\n${data}`);
});
handle.stderr.on('data', (data) => {
getLogger(filePath).warn(`\n${data}`);
readable.push(`\n${data}`);
});
handle.on('error', (code) => {
getLogger(filePath).error(`child process error with information: \n${code}`);
readable.push(`child process error with information: \n${code}`);
delete pidDict[id];
readable.push(null);
});
handle.on('close', (code) => {
getLogger(filePath).log(`child process close with code ${code}`);
delete pidDict[id];
readable.push(null);
});
}
app.set('port', 3018);
app.listen(app.get('port'), () => console.log(`server listening at ${app.get('port')}`));