nodejs 執行腳本並實時輸出


接到需求

需要一個服務來執行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')}`));


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM