在Django 4.x中实现Web方式执行系统命令和结果显示 (1) 里支持短时内返回结果的命令,而长时运行(阻塞式)的命令和脚本,需要使用 Websocket 来实现。本文使用第三方 Python 组件 Channels 完成 Websocket 服务端功能。
Channels: https://pypi.org/project/channels/
1. 开发环境
Windows 10 Home (20H2) or Ubuntu 18.04
Python 3.8.1
Pip 21.3.1
Django: 4.0
Windows下搭建开发环境,请参考Windows下搭建 Django 3.x 开发和运行环境
Ubuntu下搭建开发环境,请参考Ubuntu下搭建 Django 3.x 开发和运行环境
2. 创建 Django 项目
> django-admin startproject djangoWebsocketDemo
3. 添加 App
> cd djangoWebsocketDemo
> python manage.py startapp home
生成的项目目录结构,参考如何在Django中使用template和Bootstrap
修改 djangoWebsocketDemo/settings.py
ALLOWED_HOSTS = ['localhost', '192.168.0.5']
...
INSTALLED_APPS = [
...
'home',
]
...
# Create static folder
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
4. 静态资源和模板
1) 静态资源
从 https://jquery.com/ 下载 jQuery 包, 添加到 :
static/js/jquery-1.12.2.min.js
* static 等中间目录如果不存在,请新建它们,下同。
2) 添加 home/templates/home.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Home Page</title>
{% load static %}
<script language="javascript" src="{% static 'js/jquery-1.12.2.min.js' %}"></script>
</head>
<body>
<h3>Home Page</h3>
<form class="form-horizontal" role="form" action="" method="post" novalidate>
{% csrf_token %}
<p>
<label>System Environment:</label><br>
<select name="os_type" id="os_type" style="height: 32px; width: 50%">
<option value="windows">Server run on Windows</option>
<option value="ubuntu">Server run on Ubuntu</option>
</select>
</p>
<p>
<label>Cmd Type:</label><br>
<select name="cmd_type" id="cmd_type" style="height: 32px; width: 50%">
<option value="system_cmd_shell">System command or shell</option>
<option value="django_cmd_shell">Django command or shell</option>
</select>
</p>
<p>
<label>Command or shell:</label><br>
<textarea name="cmd_str" id="cmd_str" style="height: 64px; width: 50%"></textarea>
</p>
<p>
<label>Websocket:</label><br/>
<input type="text" name="ws_url" id="ws_url" value="ws://127.0.0.1:8000/ws/command/exec/" style="height: 32px; width:50%" />
</p>
<p>
<button type="button" class="btn btn-default btn-sm" onClick="javascript: execCmd();">
Execute
</button>
</p>
</form>
<p> </p>
<div id="cmd_result" style="padding: 15px; background-color: #e2e2e2; width: 50%; font-size: 12px; min-height: 120px;">
</div>
<p> </p>
<script type="text/javascript">
console.log("Home Page");
var globalSocket = null;
var globalSocketBusying = false;
$(document).ready(function() {
changeType();
$("#os_type").change(function(e) {
changeType();
});
$("#cmd_type").change(function(e) {
changeType();
});
});
function changeType() {
var osType = $("#os_type").val();
var cmdType = $("#cmd_type").val();
var cmdStrNode = $("#cmd_str");
if (osType == "windows") {
if (cmdType == "system_cmd_shell") {
cmdStrNode.val("dir C:\\");
cmdStrNode.removeAttr("disabled");
} else if (cmdType == "django_cmd_shell") {
cmdStrNode.val("DirCommand C:\\");
cmdStrNode.attr("disabled", "disabled");
} else {
}
} else if (osType == "ubuntu") {
if (cmdType == "system_cmd_shell") {
cmdStrNode.val("dir /");
cmdStrNode.removeAttr("disabled");
} else if (cmdType == "django_cmd_shell") {
cmdStrNode.val("DirCommand /");
cmdStrNode.attr("disabled", "disabled");
} else {
}
} else {
}
}
function execCmd() {
var cmdType = $("#cmd_type").val();
var cmdStr = $("#cmd_str").val();
if (cmdStr == '') {
alert("Please enter command or shell");
$("#cmd_str").focus();
return;
}
var wsUrl = $("#ws_url").val();
if (wsUrl == '') {
alert("Please enter url");
$("#ws_url").focus();
return;
}
var data = {
"cmd_type": cmdType,
"cmd_str": cmdStr,
}
var node = $("#cmd_result");
if (globalSocket == null) {
createWebsocket(wsUrl);
} else {
if (globalSocket.readyState == globalSocket.CONNECTING) {
node.append("The previous websocket connection is in progress, please try it later<br>");
} else if (globalSocket.readyState == globalSocket.CLOSING) {
node.append("The previous websocket connection is closing, please try it later<br>");
} else if (globalSocket.readyState == globalSocket.OPEN) {
if (globalSocketBusying == true) {
alert("The previous websocket connection is busying, please try it later");
return;
}
node.html("Executing command or shell ... <br><br>");
globalSocket.send(JSON.stringify(data));
} else {
node.append("Websocket error<br>");
}
}
}
function createWebsocket(url) {
if (globalSocket != null || url == '')
return;
globalSocket = new WebSocket(url);
globalSocket.onopen = funcWSOpen;
globalSocket.onclose = funcWSClose;
globalSocket.onerror = funcWSError;
globalSocket.onmessage = funcWSMessage;
}
function funcWSOpen(e) {
console.log("WebSocket connected: ", e);
$("#cmd_result").html("Executing command or shell ... <br><br>");
var data = {
"cmd_type": $("#cmd_type").val(),
"cmd_str": $("#cmd_str").val(),
};
console.log(data)
globalSocket.send(JSON.stringify(data));
}
function funcWSClose(e) {
console.log("WebSocket close: ", e);
$("#cmd_result").append("WebSocket close <br>");
globalSocket = null;
globalSocketBusying = false;
}
function funcWSError(e) {
console.error("WebSocket error: ", e);
$("#cmd_result").append("WebSocket error <br>");
globalSocket = null;
globalSocketBusying = false;
}
function funcWSMessage(e) {
console.log("WebSocket data: ", e.data);
//$("#cmd_result").append(e.data + "<br>");
var dataObj = JSON.parse(e.data);
if (dataObj['ret'] == "data") {
$("#cmd_result").append(dataObj['message'] + "<br>");
globalSocketBusying = true;
} else if (dataObj['ret'] == "finish") {
$("#cmd_result").append("<br>Execute finished<br>");
globalSocketBusying = false;
} else if (dataObj['ret'] == "error") {
$("#cmd_result").append(dataObj['description'] + "<br>");
globalSocketBusying = false;
} else {
$("#cmd_result").append("funcWSMessage: Invalid data format<br>");
}
}
</script>
</body>
</html>
5. 视图和路由
1) 添加 home/utils.py
import subprocess
def openPipe(rsyncStr, shell=True, b_print=True):
return subprocess.Popen(rsyncStr, shell=shell, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def systemExecute(rsyncStr, shell=True, b_print=True):
#print(rsyncStr)
p = openPipe(rsyncStr, shell, b_print)
out, err = p.communicate()
return out.decode("gbk", "ignore"), err.decode("gbk", "ignore")
def systemExecuteLines(rsyncStr, shell=True, b_print=True):
#print(rsyncStr)
p = openPipe(rsyncStr, shell, b_print)
out, err = p.communicate()
return out.decode("gbk", "ignore").splitlines(), err.decode("gbk", "ignore").splitlines()
* 显示中文时需要 decode 设置为 gbk
2) 修改 home/views.py
from django.shortcuts import render
# Create your views here.
def home(request):
return render(request, "home.html")
3) 修改 djangoWebsocketDemo/urls.py
from django.contrib import admin
from django.urls import path
from home import views
urlpatterns = [
path('', views.home, name='home'),
path('admin/', admin.site.urls),
]
6. Django 自定义命令
1) 添加 home/management/commands/DirCommand.py
from django.core.management.base import BaseCommand
from home.utils import systemExecute
class Command(BaseCommand):
help = "Run 'dir' command on Windows or Linux."
def add_arguments(self, parser):
parser.add_argument("path")
def handle(self, *args, **options):
data,err = systemExecute("dir " + options["path"])
if err:
print(err)
else:
print(data)
2) 命令行方式运行 DirCommand
> python manage.py DirCommand c:\ # On Windows
Volume in drive C is WINDOWS
Volume Serial Number is D46B-07AC
Directory of c:\
2021/12/28 10:57 <DIR> Program Files
2021/12/28 13:50 <DIR> Program Files (x86)
2021/12/20 12:15 <DIR> Users
2021/12/29 11:26 <DIR> Virtual
2022/01/05 18:18 <DIR> Windows
0 File(s) 0 bytes
...
$ python manage.py DirCommand / # On Ubuntu
bin dev root usr etc lib mnt var
home lib64 opt sbin sys cdrom
...
7. Channels
1) 安装 Channels
$ pip install channels # pip 版本 21.3.1,低版本pip可能无法安装
2) 修改 djangoWebsocketDemo/settings.py
INSTALLED_APPS = [
...
'channels', # Add
]
...
ASGI_APPLICATION = 'djangoWebsocketDemo.asgi.application' # Add
CHANNEL_LAYERS = { # 频道后端,这里采用内存存储,默认是redis
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
3) 添加 home/consumers.py
import json
from channels.generic.websocket import WebsocketConsumer
from home.utils import openPipe
class CmdConsumer(WebsocketConsumer):
def connect(self):
self.accept()
def disconnect(self, close_code):
pass
def receive(self, text_data):
print("CmdConsumer receive: text_data = ", text_data)
message = dict(json.loads(text_data))
if message['cmd_type'] == "system_cmd_shell":
execStr = message['cmd_str']
elif message['cmd_type'] == "django_cmd_shell":
execStr = "python manage.py " + message['cmd_str']
else:
execStr = ""
if execStr != "":
p = openPipe(execStr)
if p:
for line in p.stdout:
self.send(text_data=json.dumps({
"ret": "data",
"message": line.decode("gbk", "ignore").rstrip("\r\n")
}))
p.wait()
self.send(text_data=json.dumps({
"ret": "finish",
}))
else:
self.send(text_data=json.dumps({
"ret": "error",
"description": "Unable to open PIPE"
}))
else:
self.send(text_data=json.dumps({
"ret": "error",
"description": "Invalid cmd type"
}))
4) 添加 home/routing.py
from django.urls import path
from home import consumers
websocket_urlpatterns = [
path("ws/command/exec/", consumers.CmdConsumer.as_asgi()),
]
5) 修改 djangoWebsocketDemo/asgi.py
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from home import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoWebsocketDemo.settings')
application = ProtocolTypeRouter({
#"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
routing.websocket_urlpatterns
)
),
})
8. 运行
> python manage.py runserver
访问 http://localhost:8000/
> python manage.py runserver 192.168.0.5:8080
访问 http://192.168.0.5:8080/
Wesocket 的服务链接 ws://127.0.0.1:8000/ws/command/exec/,127.0.0.1 要改成 192.168.0.5。