2022-01-10 在Django 4.0中实现Web方式执行系统命令和结果显示 (2)

在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>&nbsp;</p>

            <div id="cmd_result" style="padding: 15px; background-color: #e2e2e2; width: 50%;  font-size: 12px; min-height: 120px;">

            </div>

            <p>&nbsp;</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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容