本文同步在我的博客shymean.com上,欢迎关注前端
最近研究了一些在线运行代码应用,感受颇为有趣,在此稍做总结,并尝试实现一种在浏览器运行可交互Python代码的方案。node
所谓“可交互Python代码”,指的是python中
input
等接受标准输入数据的APIpython
下面列举了一些在线编辑器,能够体验一番git
因为Python也是解释型代码,所以能够经过解析AST的方式,经过JavaScript运行Python代码,常见的库有github
相关API使用在文档中均有说明,本文再也不赘述。web
因为浏览器的限制,上面的这些库会缺乏一些功能如文件操做等;此外如input
方法,会经过window.prompt
进行mock。shell
所以,直接在浏览器运行python存在一些问题npm
py to js
库文件,且这些库或多或少缺乏部分API的支持,不能100%还原python代码运行另外的一种方案是:在服务端启动一个代码执行环境,经过网络提交python代码,而后将结果返回给前端。segmentfault
经过shelljs
,咱们能够在NodeJS中运行脚本命令浏览器
let shell = require('shelljs')
// 若是code是经过http传输的,就能够直接在服务端环境运行python代码
let code = `print('hello world')`
let res = shell.exec(`python3 -c "${code}"`)
// 经过res.stdout将输出返回给浏览器
复制代码
这种方式看起来比较简单,甚至不须要引入额外的库文件,只需一个提供python运行环境的服务器便可。在实现中碰见的一个问题是:如何解决python中input
的问题?
为了解决这个问题,咱们先来了解一下标准输入和标准输出的知识
下面是nodejs标准输入示例代码,须要了解process.stdin
和process.stdout
模块
process.stdin.resume();
process.stdin.setEncoding('utf-8');
var arr = [];
process.stdin.on('data', function (data) {
var number = data.slice(0, -1);
if (number == 'end') {
process.stdin.emit('end');
} else {
arr.push(number);
}
});
process.stdin.on('end', function () {
console.log(arr);
});
// process.stdin.emit('data', '1 ') // 向标准输入写入
// process.stdin.emit('data', 'end ')
复制代码
读取外界输入
在nodejs中能够直接使用process.argv
获取命令行参数。在线刷题时,须要从控制台读取输入,可使用readline
模块,参考Nodejs 按行读取控制台输入(stdin)的几种方法
脚本自动登陆
参考:nodejs中如何知道子进程正在等待输入,并向其输入数据
假设一个命令行工具login须要经过交互的方式依次输入username、password,如何编写一个自动化脚本auto-login将参数直接传递给login呢?
本来的登陆工具login
// login.js
const readline = require('readline')
function createInput(msg) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
return new Promise(function(resolve, reject) {
rl.question(`请输入${msg}: `, data => {
rl.close()
resolve(data)
})
})
}
Promise.resolve()
.then(() => {
return createInput('用户名').then(username => ({ username }))
})
.then(userInfo => {
return createInput('密码').then(password => ({
...userInfo,
password,
}))
})
.then(userInfo => {
return createInput('邮箱').then(email => ({
...userInfo,
email,
}))
})
.then(userInfo => {
console.log(userInfo)
process.exit(0)
})
复制代码
自动登陆auto-login,主要实现是获取子进程subProcess
的实例,而后经过subProcess.stdin.write
的方式传回数据
const fs = require('fs');
const { spawn } = require('child_process');
var subProcess = spawn('node', ['login.js'], { cmd: __dirname });
subProcess.on('error', function() {
console.log('error');
console.log(arguments);
});
subProcess.on('close', code => {
if (code != 0) {
console.log(`子进程退出码:${code}`);
} else {
console.log('登陆成功');
}
process.stdin.end();
});
subProcess.stdin.on('end', () => {
process.stdout.write('end');
});
let getNextInput = (()=>{
let cursor = 0
var config = {username:'txm',password:'123',email:'xx@123.com'}
let keys = Object.keys(config)
return ()=>{
return config[keys[cursor++]]
}
})()
subProcess.stdout.on('data', onData);
subProcess.stderr.on('data', onData);
function onData(data) {
process.stdout.write('# ' + data);
let answer = getNextInput()
subProcess.stdin.write(answer + '\n');
// 若是须要手动输入,则能够将父进程的输入重定向到子进程
// process.stdin.on('data', input => {
// input = input.toString().trim();
// subProcess.stdin.write(input + '\n');
// });
}
复制代码
上面login
的node脚本可使用python编写,大体以下
# 一个展现命令行交互的代码
username = input('input username:')
password = input('input password:')
email = input('input email:')
print('username:%s, password: %s, email:%s' % (username, password, email))
# exit(0)
# 忽然想起了“人生苦短,我用python”这句话
复制代码
片头的问题能够修改成:如何经过其余程序,向一个等待标准输入的python程序写入数据?
想象一下整个流程
input
时,等到输入能够对于整个过程,存在浏览器和服务端的屡次通讯,能够想到使用websocket
来进行实现,在父子进程通讯的各个时机进行socket消息的发送。
服务端代码实现
// server
socket.on("disconnect", function() {
console.log("user disconnected");
});
// 运行传回的代码
let subProcess;
socket.on("run code", function(msg) {
subProcess = runPython(socket, msg);
});
socket.on("code input", function(msg) {
subProcess.stdin.write(msg + "\n");
});
function runPython(socket, code) {
let fileName = "tmp.py"; // 能够换成随机文件名避免重复
fs.writeFileSync(fileName, code, "utf8");
let subProcess = spawn("python3", [fileName], { cmd: __dirname });
let isClose = false;
// 监听子进程是否运行完毕
subProcess.on("close", code => {
isClose = true;
console.log(code === 0 ? "登陆成功" : `子进程退出码:${code}`);
subProcess.stdout.off("data", onData);
subProcess.stderr.off("data", onData);
});
subProcess.stdout.on("data", onData);
subProcess.stderr.on("data", onData);
process.stdin.on("data", input => {
input = input.toString().trim();
if (!isClose) {
subProcess.stdin.write(input + "\n");
}
});
function onData(data) {
setTimeout(() => {
if (isClose) {
socket.emit("code response", data.toString());
} else {
socket.emit("stdout", data.toString());
}
}, 20);
}
return subProcess;
}
复制代码
客户端代码实现
let socket = io();
function createStdout(msg) {
let li = document.createElement("li");
li.innerHTML = `<li> >>> ${msg}:<input class="stdin"/></li>`;
list.appendChild(li);
}
function createResponse(msg) {
let li = document.createElement("li");
li.innerHTML = `<li> >>> ${msg}`;
list.appendChild(li);
}
// 注册响应
socket.on("stdout", function(msg) {
createStdout(msg);
});
socket.on("code response", function(msg) {
createResponse(msg);
});
// 注册事件
btn.onclick = function send() {
let code = content.value;
socket.emit("run code", code);
};
function codeInput(e) {
let target = e.target;
let val = target.value;
val && socket.emit("code input", val);
}
list.onclick = function(e) {
let target = e.target;
if (target.classList.contains("stdin")) {
target.removeEventListener("blur", codeInput);
target.addEventListener("blur", codeInput);
}
};
复制代码
至此,就实现了一个可交互的在线python运行工具,完整代码已放在github上了。
本文主要实现了一种在浏览器运行可交互python代码的方案,主要原理是借助服务器环境运行代码,并经过websocket传递标准输入与标准输入。
此外还存在一些未解决的问题
接下来会研究使用Docker构建运行沙盒来解决上述问题,后会有期。