本文同步在我的博客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構建運行沙盒來解決上述問題,後會有期。