需求

在写程序的时候,经常会打很多log,以监测程序或业务的运行情况。现在有这样一个需求,需要将业务执行情况即一部分log输出或全部输出展示到前台的可视化界面上。用户在页面上进行不同的任务操作,可以直接看到相应的执行情况。

实现构想

基本思路

对当前进程进行监听,每次有输出事件时,则反馈到前台

实现工具

当前项目是用koa写的,那就koa+socket.io

可参见socket.io的官方文档,express等也是类似的用法。

基本代码

Client端

html部分

...
<div id="result">
  运行结果
  <pre id="console"></pre>
</div>
...
<script src="/javascript/cmd.js"></script>
...

cmd.js

/* global io,$,window */
/* eslint prefer-arrow-callback: 0 */
const elConsole = document.getElementById('console')
// 与Server连接
const socket = io.connect(location.host)
// 替换特殊字符
const escapeHtml = function(html) {
  return html.replace(/&/g, '&amp;')
    .replace(/>/g, '&gt;')
    .replace(/</g, '&lt;')
}

const output = function(data) {
  $('#console').append(`${escapeHtml(data)}\n`)
  elConsole.scrollTop = 9e5
}

// PubSub模型
// 监听Server端触发的output事件
socket.on('output', output)

socket.on('connect', function() {
  output('连接成功')
})

socket.on('disconnect', function() {
  output('连接已断开\n')
})
// 可自定义任意事件,emit触发即可
....

Server端

之前查找node的process相关资料时,发现了这样一段话

那就监听process.on('data',...)就好啦

于是,参考网上一些代码,贴出关键部分

// app.js
const app = require('koa')()

// 使用./public下的静态文件
app.use(serve('./public'));
...
// 使用路由
app.use(router(app));
.....

// 这一行代码一定要在最后一个app.use后面使用
var server = require('http').createServer(app.callback()),
    io = require('socket.io')(server);

....
app.get('/', function *(next) {
  yield this.render('index', { my: 'data' });
});
....

// npm install --save socket.io
const server = require('http').createServer(app.callback())
const io = require('socket.io')(server)
// 关键代码 Socket.io的标准用法
io.on('connection', function(socket){
  const output = function(msg) {
    socket.emit('output', msg.toString())
  }
  process.stdout.on('data', output)
});

// 开启服务器
server.listen(1337);
console.info('Now running on localhost:1337');

解释一下

app.callback()

返回一个可被http.createServer()接受的程序实例,也可以将这个返回函数挂载在一个 Connect/Express 应用中

哈哈,这下大功告成了有没有,来赶紧起一下server见证奇迹!

发现问题

界面上除了“连接成功”四个大字,啥也没有!看一下终端,哗哗——的满屏输出啊!

why??

这才发现尽管终端满屏输出, 但process并没有监听到任何data事件!为什么呢?

Console.prototype.log = function() {
  this._stdout.write(util.format.apply(this, arguments) + '\n');
};

可见,console.log底层调用了process.stdout.write,而这句调用并不会触发process监听的data事件!

那就简单啦,每次log的时候,加一句process.stdout.emit('data', msg)就ok啦!

很好,我们来写一个自己的console.log吧(最简单的哟)

// console.js

function log(msg){
  console.log(msg)
  process.stdout.emit('data', msg)
}
module.exports.log = log

function error(msg){
  console.error(msg)
  // error 可设置不向前台输出
  // process.stdout.emit('data', msg)
}
module.exports.error = error

function info(msg){
  console.info(msg)
  process.stdout.emit('data', msg)
}
module.exports.info = info

然后在app.js加上

global.myconsole = require('./console')
...
// 调用
myconsole.log('test')

ps:用法很多,随意随意啦!
来看一下结果

web-shell

需求解决啦,但是作为一个web-shell, 怎么能只有输出,没有输入呢!在原有的基础上加一些扩展就好啦!

Server端

对于每个连接的客户端,我们可以为其单独开一个shell,以供交互

// app.js
...
const spawn = require('child_process').spawn

io.on('connection', (socket) => {
  const shell = spawn('bash')
  const output = function(msg) {
    socket.emit('output', msg.toString())
  }
  shell.stdout.on('data', output)
  shell.stderr.on('data', output)
  shell.on('close', () => {
    output('Exit')
    socket.disconnect(true)
  })
  // 监听输入事件
  socket.on('input', (data) => {
    shell.stdin.write(data)
  })
  socket.on('disconnect', () => {
    shell.kill('SIGKILL')
  })
  myconsole.debug('控制台连接成功')
})

Client端

html

<input type='text' id='cmd' placeholder="输入命令"/>

cmd.js

...
window.unload = function() {
  // 离开页面则关闭该进程
  socket.emit('disconnect')
}

elCmd.addEventListener('keypress', function(e) {
  if (e.keyCode === 13) {
    const data = `${elCmd.value}`
    output(`$ ${data}`)
    socket.emit('input', `${data}\n`)
    elCmd.value = ''
  }
})

效果如下图

其他

以上例子中,使用
shell.stdout.pipe(process.stdout)可以传递shell的输出到父进程!
反着则不行,好像不是同一个类型的对象!

也是东拼西凑啦,还有很多完善的空间!

譬如,界面输入命令对ctrl+c等快捷键的支持,子进程父进程之间的通信,界面结果显示颜色高亮,console.js拓展等!

另外,是不是也有办法能直接监听到process.stdout.write呢,思考.......


jc小金金
73 声望6 粉丝

少写bug多吃肉