├── requirements.txt ├── screenshot.png ├── client ├── babel.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── Xterm.js │ │ └── Code.vue │ ├── router │ │ └── index.js │ ├── App.vue │ └── main.js └── package.json ├── .gitignore ├── README.md ├── LICENSE └── server └── aioserver.py /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.5.4 2 | paramiko>=2.4.2 3 | aiohttp_cors -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikuh/python-online/HEAD/screenshot.png -------------------------------------------------------------------------------- /client/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikuh/python-online/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikuh/python-online/HEAD/client/src/assets/logo.png -------------------------------------------------------------------------------- /client/src/components/Xterm.js: -------------------------------------------------------------------------------- 1 | import { Terminal } from 'xterm' 2 | import * as fit from 'xterm/lib/addons/fit/fit' 3 | import * as attach from 'xterm/lib/addons/attach/attach' 4 | Terminal.applyAddon(fit) 5 | Terminal.applyAddon(attach) 6 | 7 | export default Terminal -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /client/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Code from '@/components/Code' 4 | 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | routes: [ 10 | { 11 | path: '/', 12 | name: 'Code', 13 | component: Code 14 | } 15 | ] 16 | }) 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python 在线编辑器 2 | 3 | 一个基于 vue,xtermjs,aiohttp,paramiko 的 在线python编辑器,并且实现了可交互的终端。 4 | 5 | # 后端启动 6 | - 安装依赖 `pip install -r requirement.txt ` 7 | - 启动服务器 `cd server`,`python aioserver.py` 8 | 9 | # 前端启动 10 | - cd client && npm install && npm run serve 11 | - 访问: http://localhost:8080/#/ 12 | 13 | # 效果图 14 | 15 | ![](screenshot.png) 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 23 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Python Online 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | // axios 6 | import axios from 'axios' 7 | axios.defaults.withCredentials=true; //让ajax携带cookie 8 | Vue.prototype.$axios = axios; 9 | 10 | // xterm 11 | import 'xterm/dist/xterm.css' 12 | 13 | // VueCodemirror 14 | import VueCodemirror from 'vue-codemirror' 15 | // require styles 16 | import 'codemirror/lib/codemirror.css' 17 | Vue.use(VueCodemirror, ) 18 | 19 | // VueMaterial 20 | import VueMaterial from 'vue-material' 21 | import 'vue-material/dist/vue-material.min.css' 22 | Vue.use(VueMaterial) 23 | 24 | 25 | Vue.config.productionTip = false 26 | 27 | 28 | // new Vue({ 29 | // render: h => h(App), 30 | // router, 31 | // }).$mount('#app') 32 | new Vue({ 33 | el: '#app', 34 | router, 35 | render: h => h(App) 36 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 mikuh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": ">=0.21.1", 12 | "core-js": "^2.6.5", 13 | "vue": "^2.6.6", 14 | "vue-codemirror": "^4.0.6", 15 | "vue-material": "^1.0.0-beta-10.2", 16 | "vue-router": "^3.0.3", 17 | "xterm": "^3.12.0" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "^3.5.0", 21 | "@vue/cli-plugin-eslint": "^3.5.0", 22 | "@vue/cli-service": "^3.5.0", 23 | "babel-eslint": "^10.0.1", 24 | "eslint": "^5.8.0", 25 | "eslint-plugin-vue": "^5.0.0", 26 | "vue-template-compiler": "^2.5.21" 27 | }, 28 | "eslintConfig": { 29 | "root": true, 30 | "env": { 31 | "node": true 32 | }, 33 | "extends": [ 34 | "plugin:vue/essential", 35 | "eslint:recommended" 36 | ], 37 | "rules": {}, 38 | "parserOptions": { 39 | "parser": "babel-eslint" 40 | } 41 | }, 42 | "postcss": { 43 | "plugins": { 44 | "autoprefixer": {} 45 | } 46 | }, 47 | "browserslist": [ 48 | "> 1%", 49 | "last 2 versions", 50 | "not ie <= 8" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /server/aioserver.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | import aiohttp 3 | import paramiko 4 | import threading 5 | import aiohttp_cors 6 | import asyncio 7 | 8 | 9 | async def rev_send(socket): 10 | while not socket.ws.closed: 11 | asyncio.sleep(0.1) 12 | try: 13 | data = socket.shell.recv(8192) 14 | await socket.ws.send_bytes(data) 15 | except Exception as e: 16 | print(type(e), str(e)) 17 | 18 | 19 | def start_loop(loop): 20 | loop.run_forever() 21 | 22 | 23 | def sftp_exec_command(ssh_client, command): 24 | try: 25 | std_in, std_out, std_err = ssh_client.exec_command(command, timeout=4) 26 | out = "".join([line for line in std_out]) 27 | return out 28 | except Exception as e: 29 | print(e) 30 | return None 31 | 32 | 33 | async def coding(request): 34 | data = await request.json() 35 | code_id = data["code_id"] 36 | code = data["code"] 37 | 38 | host = data["host"] 39 | port = int(data['port']) 40 | user = data['username'] 41 | password = data['password'] 42 | 43 | # code 不转义处理 44 | code = code.replace('"', '\\"') 45 | 46 | ssh_client = paramiko.SSHClient() 47 | ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 48 | ssh_client.connect(host, port, user, password) 49 | sftp_exec_command(ssh_client, f"mkdir -p ~/{code_id}") 50 | sftp_exec_command(ssh_client, f"echo \"{code}\" > ~/{code_id}/main.py") 51 | ssh_client.close() 52 | return web.json_response( 53 | {"data": {"ssh_command": f"python ~/{code_id}/main.py"}, "error_code": 0, "msg": "ok"}) 54 | 55 | 56 | class WebSocketHandler(web.View, aiohttp_cors.CorsViewMixin): 57 | 58 | async def get(self): 59 | self.ws = web.WebSocketResponse() 60 | await self.ws.prepare(self.request) 61 | data = self.request.query 62 | host = data["host"] 63 | port = int(data['port']) 64 | user = data['username'] 65 | password = data['password'] 66 | self.sshclient = paramiko.SSHClient() 67 | self.sshclient.load_system_host_keys() 68 | self.sshclient.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 69 | self.sshclient.connect(host, port, user, password) 70 | self.shell = self.sshclient.invoke_shell(term='xterm') 71 | self.shell.settimeout(90) 72 | self.status = True 73 | 74 | new_loop = asyncio.new_event_loop() 75 | t = threading.Thread(target=start_loop, args=(new_loop,)) 76 | t.start() 77 | 78 | asyncio.run_coroutine_threadsafe(rev_send(self), new_loop) 79 | 80 | async for msg in self.ws: 81 | if msg.type == aiohttp.WSMsgType.TEXT: 82 | if msg.data == 'close': 83 | await self.ws.close() 84 | else: 85 | self.shell.send(msg.data) 86 | elif msg.type == aiohttp.WSMsgType.ERROR: 87 | print('ws connection closed with exception %s' % 88 | self.ws.exception()) 89 | elif msg.type == aiohttp.WSMsgType.CLOSE: 90 | break 91 | 92 | print('websocket connection closed') 93 | new_loop.stop() 94 | print(t.is_alive()) 95 | return self.ws 96 | 97 | 98 | app = web.Application() 99 | 100 | app.router.add_routes([web.view('/terminals/', WebSocketHandler), web.post('/coding', coding), ]) 101 | 102 | cors = aiohttp_cors.setup( 103 | app, 104 | defaults={ 105 | "*": aiohttp_cors.ResourceOptions( 106 | allow_credentials=True, 107 | expose_headers="*", 108 | allow_headers="*", 109 | ) 110 | }) 111 | for route in list(app.router.routes()): 112 | cors.add(route) 113 | 114 | web.run_app(app, host="127.0.0.1", port=3000) 115 | -------------------------------------------------------------------------------- /client/src/components/Code.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 144 | --------------------------------------------------------------------------------