├── 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 | 
16 |
17 |
18 |
--------------------------------------------------------------------------------
/client/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
在线python编辑器
4 |
5 |
6 |
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 |
2 |
3 |
配置服务器信息:
4 | wss 连接设置:
5 | host:
6 | port:
7 | username:
8 | password:
9 |
10 |
11 |
12 |
13 |
14 |
15 | mode: {{ cmOption.mode }}
16 |
17 | theme: {{ cmOption.theme }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
144 |
--------------------------------------------------------------------------------